模塊化golang工程
為什么要模塊化
微服務(wù)本身是比較徹底的模塊化方案,但是在部分場(chǎng)景下微服務(wù)不是最好的方案,例如:
事務(wù)比較復(fù)雜
希望內(nèi)存占用盡量低(比如客戶(hù)就給1c2g)
數(shù)據(jù)庫(kù)連接數(shù)量已經(jīng)過(guò)大,不希望引入中間件去解決連接數(shù)問(wèn)題
運(yùn)維能力不足
開(kāi)源項(xiàng)目,希望便于部署
在這些條件下,單服務(wù)+模塊化變成了最好的方案。
模塊化方式對(duì)比
作為云原生的默認(rèn)語(yǔ)言,微服務(wù)化似乎應(yīng)該是與生俱來(lái)的能力。相比其他語(yǔ)言,golang 的模塊化方案被較少的討論。
對(duì)于模塊的需求,我們大致上有: - 可以比較方便的載入,最好能是運(yùn)行時(shí)的 - 可以卸載 - 可以有能力去在某次業(yè)務(wù)邏輯中啟用和禁用
golang官方實(shí)際有模塊化方案:go plugin。有如下特點(diǎn):
- 1.8版本引入
- 模塊被打包為 so 文件,用代碼動(dòng)態(tài)加載
- 只能加載,不能卸載
- 主程序與plugin的共同依賴(lài)包的版本必須一致
- 如果采用mod=vendor構(gòu)建,那么主程序和plugin必須基于同一個(gè)vendor目錄構(gòu)建
- 主程序與plugin使用的編譯器版本必須一致
這些條件中,在常見(jiàn)的業(yè)務(wù)開(kāi)發(fā)中,最難做到的就是依賴(lài)版本一致 假設(shè)主程序與模塊都使用了內(nèi)部 log庫(kù)v1,現(xiàn)在 v1 提供了升級(jí) v1.1,某模塊需要這個(gè)升級(jí)。也就意味著主程序和所有的模塊必須一起升級(jí)才能解決這個(gè)問(wèn)題。如果有幾百個(gè)模塊呢。。。
因此 go plugin 這么久了也很少有人使用這樣的方案去做模塊化,有興趣的可以看這個(gè)例子:https://github.com/pingcap/tidb/blob/master/docs/design/2018-12-10-plugin-framework.md
不用 go plugin,還有什么其他方案嗎?
github.com/hashicorp/go-plugin 使用了“假微服務(wù)”方案,通過(guò)子主進(jìn)程進(jìn)行模塊化啟動(dòng),通信使用 go 喜聞樂(lè)見(jiàn)的 grpc。這個(gè)方案有效的解決了運(yùn)行時(shí)、卸載、依賴(lài)版本的問(wèn)題。 但是他引入了新的問(wèn)題:性能。 假設(shè)一個(gè)調(diào)用需要調(diào)用其他 1 個(gè)模塊的方法,傳了指針進(jìn)去。在 go 層面只占用了一塊內(nèi)存,經(jīng)由 grpc 傳輸后,內(nèi)存占用變成了 3 塊,也就是說(shuō)一個(gè)調(diào)用內(nèi)存就要翻 3 倍。普通業(yè)務(wù)開(kāi)發(fā)其實(shí)到也影響不大,在涉及大流量網(wǎng)絡(luò)開(kāi)發(fā)中,這個(gè)量級(jí)可能太嚇人了。

編輯
對(duì)性能要求比較高,或需要內(nèi)存占用比較低的場(chǎng)景中,這個(gè)方案不太行。如果要使用 rpc,為什么不真的使用微服務(wù)呢,畢竟微服務(wù)體系下是另一套完整的生態(tài)了。監(jiān)控、鏈路、編排等各類(lèi)需求都有完整便捷的解決方案。
如果用 go mod 作為模塊化方案,優(yōu)勢(shì)有:
- 完全支持多版本
- 沒(méi)有學(xué)習(xí)成本
- 沒(méi)有額外的性能開(kāi)銷(xiāo)
相對(duì)的,缺點(diǎn)有:
- 不可能運(yùn)行時(shí)載入
- 不能卸載
- 編譯的時(shí)候不能做單元測(cè)試
- 分支管理不能按照普通的方式來(lái)進(jìn)行
- 配置管理需要升級(jí)(普通的配置方式對(duì)模塊化不夠靈活)
- 不能支持 swagger
go mod模塊化實(shí)現(xiàn)細(xì)節(jié)
模塊化方案是微服務(wù)的前奏,如果模塊化成功,起碼有了向微服務(wù)發(fā)展的基礎(chǔ)。 從這個(gè)角度出發(fā),模塊化方案就必須滿足一些要求:
- 依賴(lài) interface 而不是指針
- 不使用導(dǎo)出變量的方式使用單例模式,盡量控制模塊權(quán)限
- 每個(gè)模塊的配置應(yīng)該是獨(dú)立的
- 盡管不要求運(yùn)行時(shí)載入和卸載,起碼載入和卸載應(yīng)該很方便
因此,模塊化方案在 go mod 的基礎(chǔ)上,還應(yīng)該有:
- 全面 ioc 的使用和支持,這樣才能夠依賴(lài) interface,盡量控制模塊權(quán)限
- 配置覆蓋或組合能力,每個(gè)模塊有獨(dú)立的配置,同時(shí)可被其上層模塊的配置覆蓋,或提供配置組合能力。
模塊的設(shè)計(jì)
模塊的設(shè)計(jì)和微服務(wù)的劃分可以對(duì)應(yīng),甚至可以更細(xì)粒度,畢竟還是單服務(wù),沒(méi)有微服務(wù)的劣勢(shì)。 常見(jiàn)的,按照流程/對(duì)象或者抽象/實(shí)現(xiàn)的方式去劃分,可以解決大部分的問(wèn)題。
什么是好的模塊(基本和微服務(wù)重疊):
- 可管理(動(dòng)態(tài)裝載和卸載,golang做不到)
- 原生可重用
- 可組合
- 無(wú)狀態(tài)
一個(gè)模塊設(shè)計(jì)的例子
需求:支付系統(tǒng)。
根據(jù)商品的不同,總價(jià)會(huì)有不同的折扣。但是折扣的計(jì)算比較復(fù)雜,會(huì)根據(jù)商品的不同屬性計(jì)算不同折扣。例如商品上架時(shí)間、庫(kù)存、供應(yīng)商、批次等
第一次設(shè)計(jì)
有一個(gè)獨(dú)立的服務(wù),支付服務(wù),來(lái)處理所有的需求。其會(huì)拉取商品服務(wù)的數(shù)據(jù)進(jìn)行邏輯判斷。
第一次模塊化重構(gòu)
抽取其中的商品拉取部分、數(shù)據(jù)庫(kù)存儲(chǔ)部分進(jìn)行獨(dú)立模塊?,F(xiàn)在邏輯部分在上,商品拉取、存儲(chǔ)部分在下。
遵循一個(gè)原則:上層模塊依賴(lài)下層,反之則不行
現(xiàn)在有2個(gè)模塊:商品拉取、數(shù)據(jù)庫(kù)存儲(chǔ)
第二次模塊化重構(gòu)
邏輯部分在直接調(diào)用商品拉取,抽象其中部分為interface,商品拉取模塊實(shí)現(xiàn)了這個(gè)interfcae。 同樣,商品折扣的計(jì)算也獨(dú)立出了一個(gè)模塊,同樣也實(shí)現(xiàn)了一個(gè)抽象的interface
遵循一個(gè)原則:只能依賴(lài)抽象而不是實(shí)現(xiàn),實(shí)現(xiàn)依賴(lài)了本來(lái)應(yīng)該依賴(lài)它的業(yè)務(wù),依賴(lài)倒置了。
現(xiàn)在有3個(gè)模塊:商品折扣計(jì)算、商品拉取、數(shù)據(jù)庫(kù)存儲(chǔ)
第三次模塊化重構(gòu)
折扣的計(jì)算包含多種多樣,且可能隨時(shí)增加或變更的邏輯。把它們?nèi)糠珠_(kāi)成不同的實(shí)現(xiàn),實(shí)現(xiàn)了相同的interface。用同一個(gè)管理器進(jìn)行管理
遵循一個(gè)原則:適配器模式的使用,帶來(lái)擴(kuò)展的便利性
現(xiàn)在有3+n個(gè)模塊:n個(gè)商品折扣計(jì)算、折扣計(jì)算管理、商品拉取、數(shù)據(jù)庫(kù)存儲(chǔ)
第四次模塊化重構(gòu)
支付的流程也成為了一個(gè)獨(dú)立的模塊,調(diào)用了一個(gè)通用的interface。折扣計(jì)算管理器實(shí)現(xiàn)了這個(gè)interface,因此將來(lái)不僅可以計(jì)算折扣,還通過(guò)擴(kuò)展其他模塊
遵循一個(gè)原則:main里面只剩下了di框架的初始化和配置,代碼不超過(guò)20行了
現(xiàn)在有4+n個(gè)模塊:n個(gè)商品折扣計(jì)算、支付模塊、折扣計(jì)算管理、商品拉取、數(shù)據(jù)庫(kù)存儲(chǔ)
結(jié)果
經(jīng)過(guò)4次重構(gòu)后,系統(tǒng)從以前的一坨變成了一堆,也沒(méi)有進(jìn)行微服務(wù)拆分,但是誰(shuí)都能看出來(lái)他的可維護(hù)性已經(jīng)增加了太多。
更多精彩內(nèi)容
純異步事件驅(qū)動(dòng)構(gòu)建的微服務(wù)體系
研發(fā)團(tuán)隊(duì)新鮮事兒,來(lái)公眾號(hào)「迷路idea」找我一起探討