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

java江南 發佈 2022-05-13T21:17:26.740520+00:00

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

引言


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


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


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



一、 開發現狀


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


  • 沒有合理分包,或者僅邏輯職責分包(分目錄)


  • 面向過程編程,函數調用鏈很長,在各種包之間穿插。


  • 沒有依賴注入,依賴是以全局引入的方式存在,作用域很大,潛在的並發問題。


最終導致


  • 不通用,一切都不通用,每次修改一個邏輯部分可能要牽扯到多個函數調用的地方去修改參數關係。


  • 測試用例難寫,import進來的函數,是mock呢還是不mock呢,mock函數一個個寫,monkey mock是否合理?


  • 每個模塊不獨立,看似按邏輯分了模塊,比如order_hanlder,conf,XXX_helper,database等,但沒有明確的上下層關係,每個模塊里可能都存在配置讀取,外部服務調用,協議轉換等。


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



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


  • 服務1除了main全部都放在logic中,logic實際上已經職責不清了。


  • 服務2全部平鋪式的,為什麼作者要這麼幹,因為他寫了很多monkey func mock,因為沒有抽象,不同函數之間調用導致很多函數的mock需要復用,但測試文件中的內容不支持import,所以為了避免底層邏輯函數要重複在不同包里寫mock,乾脆平鋪了。


  • 服務3常見組織方式,以邏輯為單元進行模塊分包解耦,基本符合單一職責原則,但這種微服務隨著需求的增長會產生網狀調用的問題。


  • 服務4對外部調用有一定抽象的目錄設計,但組織方式並不一眼清晰,沒有合理的分包,邏輯代碼寫在接入層。


(一)沒有架構



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


  • 服務不同包函數之間的調用慢慢演變成網狀結構。


  • 數據流的流向和邏輯的梳理變得越來越複雜。


  • 很難不看代碼調用的情況下搞清楚數據流向。


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


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



(二)沒有分層


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



二、 探索最佳架構實踐


(一)簡潔架構



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


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



這種結構的優勢體現在:


  • 標準結構:層+模塊


  1. 結構分層,每層之間劃分模塊。
  2. 數據流向固定,自上而下單一方向。
  3. 架構清晰,需求代碼增長是結構化的,組織關係不是網狀。


  • 一致性


  1. 架構通用,可以統一規範。
  2. 協作開發時不同服務的架構一樣,無理解成本。


  • 易於操作


  1. 相關概念簡單,易於操作,符合開發直覺,便於正確分類代碼。
  2. 不涉及領域建模等額外問題。


  • 減緩代碼膨脹


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



(二)目錄規範



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


  • gateway


  • 接口實現的地方,服務接口入口處,對應trpc-go的service。


  • 只進行協議解析轉換,協議的整理,不涉及業務邏輯。



  • logic


  • 服務核心業務邏輯實現的地方。


  • 內部實現分模塊分包。



  • repo


  • 外部依賴層,包括外部資料庫、RPC調用等。


  • 每個包提供抽象接口,將外部數據調用並整理後以接口的方式提供給logic。


  • 僅做外部調用和數據整理,不包含業務邏輯。



  • entity


  • 貫穿整個服務的數據結構,類似常量,錯誤碼。


  • 貧血模型,即僅包含數據結構讀寫方法的對象。



  • 防腐層


  • 每層對外暴露的都以抽象接口方式,用依賴倒置的方式實現每層之間的防腐。


  • 抽象接口天然可以gomock生成樁代碼,上層單測時只需要用下層對應的樁代碼mock下層依賴即可。


三、 實現規範



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


  • 每個服務一個遠程配置。


  • 遠程配置為json格式(yaml一樣,內部統一即可)


  • 遠程配置定義在entity/config包中,結構體為Config。


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




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


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



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


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



(五)配置的使用


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




四、落地方法


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


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



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



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



五、總結


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



(一)為什麼不是DDD


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


DDD的優勢和劣勢



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



後記


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


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


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


小夥伴們有興趣想了解內容和更多相關學習資料的請點讚收藏+評論轉發+關注我,後面會有很多乾貨。我有一些面試題、架構、設計類資料可以說是程式設計師面試必備!

所有資料都整理到網盤了,需要的話歡迎下載!私信我回復【111】即可免費獲取

















































































作者:楊帥 K8S中文社區

連結:https://mp.weixin.qq.com/s?__biz=MzI5ODQ2MzI3NQ==&mid=2247507007&idx=1&sn=d3e7dfcf0358bcf114a840571222cbf8&chksm=eca7e57bdbd06c6d18bee4d1f51643a070414daa98acbcd100087889e61df8a688cb505b264c&scene=27#wechat_redirect

關鍵字: