微服務公用代碼組織實踐
我們知道,微服務架構由多個相對簡單的服務組成,依賴服務之間的隔離性降低系統復雜度。理論上拆解完備的微服務,不應當存在過多業務代碼復用的機會,因為服務之間的有效的隔離會使得各自代碼只關注自身的上下文,微服務的邊界清晰不但包含職責清晰,從代碼層面也應當清晰隔離。
但微服務群組產出的兩類代碼,我們仍然建議被公用:
第一類是交互協議代碼,微服務之間交互協議標準的代碼,由于每個獨立微服務單一職責都有自身邊界,微服務之間交互就被暴露為新的特征。交互協議標準代碼公用,也可以看做以微服務容易理解的方式公布出來,有利于維護微服務之間交互的便利性和精確性。
第二類是純工具類代碼,這部分代碼獨立于微服務群的業務特征,往往是提供更低層次的函數庫或組件,如版本比較、時間對比工具、上傳資源組件等。這一類代碼應當視為第三方獨立倉庫,類似我們引用的 Github 上的開源庫。
第二類代碼相對簡單,作為獨立于微服務業務群組的倉庫存在即可。對于第一類,微服務之間同步的信息傳遞,往往是通過 HTTP、PRC 等通信協議,并依據交互雙方定義的業務協議將傳遞的 JSON 或 Protobuf 等解析為業務可理解的信息。這部分交互相關的代碼可以被視為公用代碼。本文來探討這部分代碼在整個微服務體系中如何更好的被組織和使用,以提升研發效率并減少相關故障。
此部分代碼組織的問題,本質上并非為系統或模塊的依賴問題,而是在微服務架構體系中,如何更方便的同步和使用公用代碼的問題。我司在走上了微服務修行的不歸之路后,此部分交互代碼組織也經歷了不同的階段,本文會借此案例進行探討。Go 是我司的指定語言,本文的一些示例和特性是用 Golang 來展示,Git 是最流行的分布式版本控制系統,討論也基于 Git。
1. 交互示例
討論之初,我們首先列出微服務之間交互的代碼和目錄結構示例,及簡單的三個定義:
- 源服務:產出 Model、Client 的服務。
- Model:定義數據模型的代碼,作為微服務業務之間的交互協議編碼解析使用。
- Client:微服務發起請求的代碼,并使用 Model 來解析相互傳遞的數據,通常是源服務提供 API 時候,可以附帶提供。
微服務交互的簡化代碼及目錄如下,其中 model 存放數據模型代碼,client 存放網絡請求代碼:
數據模型實體代碼:project/base/model/user.go
- type User struct {
- UserID int64 `json:"user_id" form:"user_id"`
- UserName string `json:"user_name" form:"user_name"`
- Status int `json:"status" form:"status"`
- Avatar string `json:"avatar" form:"avatar"`
- AvatarSmall string `json:"avatar_small" form:"avatar_small"`
- }
交互請求代碼:
- project/base/client/user_client.go
- url := "http://<demo.com>/project/<user-info-api>"
- userClient := http.Client{
- Timeout: time.Second * 2, // Maximum of 2 secs
- }
- req, _ := http.NewRequest(http.MethodGet, url, nil)
- res, _ := userClient.Do(req)
- body, _ := ioutil.ReadAll(res.Body)
- user := model.User{}
- jsonErr := json.Unmarshal(body, &user)
- if jsonErr != nil {
- log.Fatal(jsonErr)
- }
- fmt.Println(user.UserName)
2. 練氣期——手工拷貝:
微服務的核心意義不止是服務拆分,也在于團隊的組織和溝通形式也在調整。團隊初始切換為微服務時候,各個成員從源服務拷貝 data 內 model 和 client 至各自相關服務。雖然代碼拷貝后各自修改完全不影響,但隨著服務數目的增多,找源服務拷貝代碼越來越麻煩,大家的溝通過程通常是:
A:“兄弟們,我代碼提了。”
B:“又改了啊!改了哪些。”
A:“xxx xxx xxx xxx xxx xxx xxx xxx xxx ”,
B:“靠,這么多,你把文件傳給我!”
G:“我也要!”
這種全憑人力來維護代碼的過程,容易缺失修改文件的。且更新操作不順暢,長期容易導致相關微服務和源服務維護的 base 差異較大。從源服務拷貝回來的代碼,和自身項目庫的約束并無二致,可以被任意修改,容易因本地的手工修改而引發協議的不一致,隨著團隊人員增加和規模擴大,這種方式開始被組員吐槽和詬病。
3. 筑基期——集中倉庫
程序員是不會滿足現狀的,程序員天生就是要解決手動操作的問題,拷貝代碼這種原始而粗暴的手段自然很容易被淘汰掉。有成員提出創建個單獨的倉庫來集中管理各微服務 base 代碼,代碼從源服務被手工拷貝至 base 倉庫,所有成員從這個單獨的 base 倉庫拉取更新,簡直是順理成章。團隊成員一致認為這簡直是修仙進階的必然趨勢,所有人一拍即合。
調整為統一的代碼倉庫后,雖然仍舊需要手動拷貝到 base 倉庫代碼。但使用方操作簡單,只需有事沒事,拉取下 base 倉庫就可以拉到最新的依賴代碼。而且整個團隊微服務之間交互的所有的 client 和 model 都可以在 base 里直接找到。大家對 base 倉庫的認知統一,一時間歌舞升平,相安無事。
整個代碼倉庫組織如下,其中 base 倉庫包含多個 project-base 目錄,該目錄與 project 倉庫里 base 目錄完全一致:
此時團隊成員溝通的過程通常是:
A、B 、C、D、E、F、G:“push!push!push!push!”
A:“兄弟們,我代碼提了,更新下 base!”
B、C、D、E、F、G:“pull!pull!pull!pull!”
在 95% 的情況下,大家的合作溝通都是是愉快的,然而也有倒血霉的時候,特別是當小 G 被迫緊急修復一個半年內都處于穩定維護期的項目時:
A、B、C、D、E、F、G:持續默默提交…
G:“pull!”
G:“蒼天,我只要更新 B 的一個更新,為啥 pull 下來幾百個更新,我編譯不過啊!嚶嚶嚶。”
小 G 很負責任,嚶嚶之后還得解決問題,好容易深夜兩點解決完畢,終于上線了,殊不知更慘的還在后面,深夜四點被監控告警叫起來,因為剛才的上線引發了一起線上事故。
老板:“這個線上事故影響面大,小 G 明天復盤一下!”
G:“我只是想更新了 B 的 base,A、B、C、D、E、F 幾個月那么多更新,我哪知道掉坑里了啊。”小 G 嚎啕大哭。
問題根源:
集中倉庫 Base 為各個微服務的 base 代碼合集,一次更新會導致全部更新,無法單獨更新某一部分 Base。如示例中 aggregation 項目更新 project1-base 時,project2-base、project3-base、project4-base 也會被 Git 無腦直接更新,這些代碼的改動,通常不在預期和測試的范圍之內。base 內只有數據結構和請求代碼相對比較簡單,雖然長期天下太平,但冷不丁也會禍起蕭墻。要牢記,任何更改都不安全,如何減少公用代碼倉庫的變更的影響范圍,是我們要去探究的。
4. 結丹期——獨立子倉庫
小 G 痛定思痛,第二天腫著雙眼來到了公司,拉上研發的小伙伴們進行復盤,復盤的結果是,這種統一集中倉庫管理的方式,肯定有改進空間,大家七嘴八舌:
B:“可以打 Tag”
G:“不行,解決不了我要部分更新,拉下一堆更新的問題。”
C:“可以直接引用源項目,Go vendor 只會復制需要文件到當前工程”
D:“不行,直接引用微服務代碼心理負擔比較重,也容易誘導直接使用源微服務里其他代碼引起混亂,而且,對于跨部門且代碼權限有差異時,此方案不適用,如保密級別高的工程。”
E:“……”
小 G 的眼淚沒有白流,一番討論后,大家明確了目標:應該將集中倉庫拆分或者分割引用,但又需要有便捷的子倉庫拆分和同步方案,避免手工拷貝,才容易被接受推廣,畢竟大家都是懶人。
目標明確了,小 G 一頓猛如虎的調研后,驚喜的發現:Git 雖然沒有可以使得 G 倉庫直接引用 B 倉庫某個目錄的功能,但已經有 B 項目的子目錄,直接和 B-Base 子倉庫保持同步的內置功能:git subtree 。B 倉庫子目錄和 B-Base 子倉庫保持同步,小 G 直接使用 B-Base 子倉庫即可。這一定是有不少同行也給 Git 提過類似的需求,雖然工具仍還不夠完美,但我們做些限制,只有源倉庫提交,只用它最基本的功能仍然夠用!小 G 感慨,早發現就好了。獨立子倉庫同步機制詳述下文列出。
此時的代碼倉庫組織形式如下:
A:“兄弟們,我代碼提了,有需要的更新下我的 Base”
B、C、D、E、F:無視之。
G:“好嘞!pull A-base!”
此方案雖然也有一些額外成本,比如小倉庫數增多,需要大家了解 git subtree 命令。但是權衡比較,正向收益居多,而且 git subtree 命令也可以被直接使用腳本或 Git 鉤子直接屏蔽,使得更新范圍更加可控的同時,更加便捷自動化。
這種方案使⽤用現有的工具體系,未增加學習成本,同時微服務的所有者可以決定何時選擇同步該修改到服務中,有效減少了未經測試代碼直接進⼊線上而引發故障概率。
對于前文所述的純工具類代碼,我們也建議避免包攬萬象的工具倉庫,例如 common 倉庫。更好的方法是把包攬萬象庫按功能拆分成具有獨立上下文的多個庫,例如創建基于上下文的 storage、util、log 等倉庫。這能夠把不經常改變的代碼和頻繁改動的代碼分離開,使用方自行控制每次變更范圍。
獨立子倉庫同步機制詳述:
格式約定:
約定對每個微服務,創建相對應的子倉庫,倉庫名以<-base>為后綴,比如,Project1 對應子倉庫為 Project1-base <git@gitlab.company.com:back-end/project1-base.git>
約定在源微服務代碼組織中創建 base 包,model 和 client 存放其中(命名也可以根據自己公司規范),如圖:
約定其他微服務需要和源微服務交互時候,直接使用子倉庫。由于子倉庫是完全看做獨立的倉庫來依賴,日常更新普通的 git pull 命令即可。
源服務 base 倉庫拆分:
源服務 base 倉庫拆分的核心命令如下:
- cd <project-folder>
- # 無base目錄時,直接添加子倉庫
- git subtree add -P base <git@gitlab.company.com:back-end/project-base.git> master
- # 如果base已經存在,先拆分提交至子倉庫,再刪除本地后關聯遠程子倉庫。
cd <project-folder># 無base目錄時,直接添加子倉庫git subtree add -P base <git@gitlab.company.com:back-end/project-base.git> master# 如果base已經存在,先拆分提交至子倉庫,再刪除本地后關聯遠程子倉庫。
日常同步:
日常提交限定在源服務內,避免過多使用高階 gitsubtree 命令。提交過多后定期 git subtree split --rejoin 來解決提交都需要重頭遍歷 commits 耗時過長的問題, 建議通過腳本和 Git 鉤子來自動化:
- # 提交
- git subtree push -P <name-of-folder> <git@gitlab.company.com:back-end/project1-base.git> master
- # 更新
- git subtree pull -P <name-of-folder> <git@gitlab.company.com:back-end/project1-base.git> master
5. 總結討論
公用代碼組織形式演進的過程,是繁雜宏大的研發工程中一個細小的工具化和規范化的流程。通常,集中倉庫的方式操作簡單直觀,也容易被認同,絕大部分時間也都可以運轉正常。伴隨著問題的驅動,我們切換至更精細的獨立子倉庫方式。獨立子倉庫方案使用現有的工具體系,在不增加復雜度的情況下,提供自動推送變更以及可選擇同步公用代碼能力。實踐中有效提高效率,也減少了未經測試代碼直接進入線上而引發故障概率,是本文推薦的方案。
當然,我們看到每種方案在各公司也有不同實踐,如有的公司手工拷貝代碼使用的也很好。在實踐中可以根據自己團隊的口味來選擇方案,更歡迎有更好經驗的小伙伴來交流。
6. 作者介紹
奇正,曾在奧多比 、百度任高級工程師,現任某互聯網公司后端業務線 Leader,先后從事過 C++、Android,Golang 開發工作。