保姆級教程!Golang 微服務簡潔架構實戰

 本文從簡潔架構的理論出發,依託 trpc-go 目錄規範,簡單闡述了整體代碼架構如何劃分,具體 trpc-go 服務代碼實現細節,和落地步驟,並討論了和 DDD 的區別。文章源於我們組內發起的 go 微服務最佳實踐的第一部分,希望從開發和閱讀學習中總結出一套 go 微服務開發的方法論,互相分享一下在尋求最佳的實踐過程中的思考和取捨的過程。本次主要討論目錄如何組織,目錄的組織其實就是架構的設計,一套通用架構的設計,可以讓開發專注於邏輯設計和具體場景的代碼設計,好的架構設計可以預防代碼的腐敗,並且相關的規範操作簡單,可以按步驟根據情況分步落地,可操作性強。

引言

現在有了高效的 go 語言和成熟的 trpc-go 框架和一系列的中臺 SDK,發佈平臺,一個新手也可以通過教程快速寫出簡單功能的微服務,以此入門,開始 go 的微服務開發,並應對大部分開發需求。

但是一旦開始了就會發現隨着需求的增加我們常常不得不去花很多時間去維護代碼,變更已有的邏輯,不斷的抽象,提高部分常用能力的可擴展性,但往往隨着多個人在同一份微服務代碼裏協作,維護這件事情越來越難做了,不僅僅是因爲大家的抽象風格不同,對於抽象的標準,模塊的分割,數據的流向,分層的邏輯都是不同的,看每個服務都像是一個新的生命,千姿百態。

千姿百態的代碼庫不是我們希望的,我們希望在代碼的架構上保持易讀性可擴展性可維護性,這樣除了對於代碼細節的一致性(代碼標準)外,還希望有架構上的標準,讓開發專注於邏輯設計和具體場景的代碼設計,在海量之道的知識下把服務相關內容做好,而不是將時間和精力浪費在糾結如何重新組織和解亂麻、重構等工作上,如果每個服務的架構都足夠簡潔清晰,團隊內部每個倉庫都像是自己寫的,上手也會很快,團隊的效率就會幾何的速度提升。

一、 開發現狀

不同的業務場景不太一樣,在增值的業務場景下,大部分的需求邊界或者服務全部職能一開始並不能確定,一般就是一個小需求開始的微服務,後續可能隨着業務的增長慢慢變得複雜的,彷彿是從一顆小樹苗漸漸長成一顆枝繁葉茂的大樹,可能一開始這個服務的職責很單一,很簡單,一個 service 搭配一個 logic 就 ok 了,但後面加入了各種依賴,logic 就開始變複雜,更可怕的是,因爲來一個需求做一個需求(假設最壞的情況下無法預測產品的需求),對於落後的開發模式,或者沒有架構概念來說,多一個需求,無非就是加個函數,加個分支,需要啥,import 啥就完事了,漸漸地,絕大多數服務成爲:

最終導致

先看目前的微服務代碼狀態,截幾個常見的微服務目錄組織風格:

四種目前常見的微服務目錄組織方式,從左至右分別爲 1,2,3,4,可以看到:

(一)沒有架構

如上述例子中,大多數服務沒有架構上的概念,多數業務是以邏輯單元的方式去分包(分目錄),每個包之間關係是平級關係,每個包的邏輯獨立,理論上使用包功能時 import 進來即可,隨着服務的成長:

這是目前常見的一個實際問題,業務增長過程中,微服務很容易長成一個垃圾山,開發心累,改不動的情況出現。

所謂的代碼腐敗即在代碼增量到一定程度後,服務內部的函數調用組織是網狀結構,沒有層級結構,即使微觀上可能是解耦的,但宏觀上是亂成一團的,DDD 等設計思想都是爲了解決這樣的問題。

(二)沒有分層

常見的微服務只有分包沒有分層的概念,數據流沒有分層,因爲沒有合理的分層,自然沒有上下調用的關係,最多就是邏輯上分個包而已,用到啥 import 進來就完善,沒有層次的系統就是一盤散沙,一盤散沙的接口,互相隨意調用,關係亂成一團,這就是日後維護和調試的噩夢。

二、 探索最佳架構實踐

(一)簡潔架構

出自《架構整潔之道》,此架構模型是不區分前後端的廣義上的抽象架構我們希望每個微服務的代碼在微觀上也是符合簡潔架構。

在後臺服務的場景下,以 trpc-go 目錄規範可以抽象出一種金字塔結構的架構:

這種結構的優勢體現在:

  1. 結構分層,每層之間劃分模塊。

  2. 數據流向固定,自上而下單一方向。

  3. 架構清晰,需求代碼增長是結構化的,組織關係不是網狀。

  1. 架構通用,可以統一規範。

  2. 協作開發時不同服務的架構一樣,無理解成本。

  1. 相關概念簡單,易於操作,符合開發直覺,便於正確分類代碼。

  2. 不涉及領域建模等額外問題。

  1. 分層將代碼上升或下層,以三層的結構可以一定程度上降低每一層的代碼膨脹的速度。

(二)目錄規範

分層按數據流向分爲接口層(網關層)、邏輯層,外部依賴層,劃分方式和理解成本都不會很高,詳細如下:

三、 實現規範

在實踐過程將代碼目錄按標準劃分歸類只是第一步,重要的是層與層之間的隔離和模塊與模塊之間的解耦,所以需要用到依賴倒置、依賴注入、封裝、測試規範來實現具體的代碼,其中測試規範是反向校驗代碼設計是否合格的一把尺子,如果每個接口無法使用 gomock 打樁,那麼依賴倒置就是沒有做好。

(一)依賴倒置、接口隔離

實現要求:不同層之間對外的接口一律以 interface 的方式提供,並且單一職責的設計,接口儘可能簡單清晰,接口文件單獨存放,不放在具體實現的文件中,依賴參數定義和接口聲明放在一起。

例,msg 包下 api.go 定義消息接口:

(二)依賴注入

依賴注入(DI,Dependency Injection)指的是實現控制反轉(IOC,Inversion of Control)的一種方式,其實很好理解就是內部依賴什麼,不要在內部創建,而是通過參數由外部注入。例:

例:

(三)不引入 gomock 以外的 mock 包

如果一定要 monkey mock 來對函數打樁時, 說明代碼沒有符合接口原則。並且 Monkey mock 的 mock 函數不可導出 在每個調用的此函數的包內單測時,都需要重新寫一遍 mock。

Gomock 樁代碼可自動生成,上層需要 mock 下層依賴時,只需要將 mock 的樁作爲依賴注入即可。

(四)配置(遠程配置)

現在幾乎每個服務除了框架配置外,會接入遠程配置(七彩石配置),讀取遠程配置的邏輯幾乎每個服務都要重新實現一遍,因爲配置的最終輸出一定是一個個性化的結構體(每個服務的配置肯定都不一樣),所以很難用一套代碼解決,這裏採用了一個包替換的方式,將出口的結構體通過引入不同的 config entity 定義,來實現代碼的通用(僅是通用,還實現不了零 copy)

這樣可以複用如下遠程配置實現:

這裏如果服務有多個配置:

例:這個服務是重構過的,之前沒有規範,所以弄了三個不同的遠程配置(實際上一個即可):

因爲 Get 返回的結構不同,所以不同配置使用不同的接口實例來實現,每個不同結構的配置在解析時是固定的結構體,get 返回也是固定的結構體,在 go 模板特性未支持的情況下每個不同文件的配置,以不同實現 impl 來完成解析, 看起來代碼上有一些重複,但這樣表達能保證清晰易懂,一般情況一個服務業務配置放在一個文件中。

一個服務一個配置,對於配置初始化等代碼的減少,有很大的幫助。

(五)配置的使用

接口化的配置很方便實現依賴注入,摒棄之前那種引入配置包,讀取全局配置的方式,通過依賴注入來實現配置作用域減小,避免很多併發問題:

四、落地方法

理想很豐滿現實很骨感,需求進度和代碼質量的矛盾,如果要一步到位,在實踐中等於一步也實行不下去。

實際情況往往是需求很緊急,並沒有太多時間給開發用來設計和優化代碼,所以我們希望走第一步的時候不會佔用開發太多的時間,最好時間分配可以從 1:9 的方式開始,並且在任何階段都可以以需求快速完成爲優先(即容忍一定程度的不遵守也不會破壞整體),即一開始你可以在 90% 的自由度上保持你自己舊的風格,抽出 10% 的時間來設計,這樣落實規範並不會很痛苦。

整體落地的步驟可以分爲三個階段(不是必要經歷的,時間不緊張可以直接按標準實現來)

根據當前需求的緊急程度和個人時間安排來分階段實踐即可。

五、總結

微服務代碼架構的一致性和實現規範的一致性可以帶來很多好處:

(一)爲什麼不是 DDD

其實之所以要提 DDD,是因爲這是個避不開的問題,但答案其實已經有了 DDD 是把控中大型項目的殺手鐧,但使用 DDD 並不能使開發新項目變得更快,更方便,而是爲了今後考慮,讓一個龐大的系統可以更快的迭代,更新,也就是說新的項目不用太在意領域驅動設計,甚至新項目開始可以不用領域驅動設計。

DDD 的優勢和劣勢

不同的業務可能面臨不一樣的問題,很多實踐中的需求往往不是一開始就有頂層設計的大需求、大項目,甚至很多微服務還沒確定自己領域內的元素,就伴隨着業務死亡了,創建服務之初領域模型和邊界並不清楚,一個一個接口的新服務,從一開始設計時就去事件風暴、劃分元素、子域等也是不切實際的,所以越是微小的服務,越是不需要 DDD。很多時候我們不得不考慮團隊的新成員快速成長的問題,一個新同學或者實習生同學很難快速上手 DDD,並把 DDD 落地到每個服務裏,不能全部落地,這樣就會存在不同需求服務之間的不一致性,接手同事的服務時,還是會存在理解結構的心智負擔。

後記

整體的規則描述了大概,但是實踐的過程中,對於內部具體細節,函數的抽象,聚類,子模塊的劃分,都是經驗和實踐的積累,還是很考驗一個人的代碼功底,這點架構規範並不能給予幫助。

好的架構或者說目錄設計像是垃圾分類的垃圾桶,預先設置好分類規則,垃圾就可以很輕鬆的進行分類,分類好的垃圾就可以變廢爲寶,成爲可利用的資源,所以面對垃圾山一樣的代碼,重構時我們首先要遵循正確的架構進行垃圾分類。

雖然進行了有效的分層,但是對於 logic 層裏面的模塊拆分並不要求嚴格,即提供了抽象接口之後,具體實現是細節問題,隨着需求的增長實際上還是面臨增長之後帶來複雜度關係,但由於拆分了外部調用在 repo 和數據實例在 entity,微服務最終 logic 的代碼並不會膨脹的很快,三層結構可以一定程度的減緩複雜度膨脹的速度,如果有一天膨脹大了,那麼使用 DDD 進行重構可能是另一種解法。

本文是記錄一下在尋求最佳的實踐過程中的思考和取捨的過程,畢竟對於微服務代碼架構的實踐沒有銀彈,不存在哪一種更好的情況,只有相對容易落地和簡單有效的方案才比較通用

參考資料:

1.《架構整潔之道》

2.《tRPC-Go 目錄規範》

3.《go-clean-arch》

** 作者簡介**

楊帥

騰訊後臺開發工程師

騰訊後臺開發工程師,深圳大學畢業,目前負責社交增值商業廣告相關後臺開發,主要開發語言爲 go 語言,在渠道投放系統管理端建設,和增值商業廣告中臺建設等領域積累了豐富的開發經驗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7ZDhJgq4euiI7bf3wGfdYw