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