微服務(wù)壞味道之循環(huán)依賴
歷史上有很多科學(xué)家為之背書的熵增定律,揭示了很多自然界現(xiàn)象的本質(zhì):任何孤立系統(tǒng),在沒有外力作用的情況下,其總混亂度(熵)會不斷增大。
軟件系統(tǒng)當(dāng)然也不例外,隨著軟件系統(tǒng)的功能不斷增加,系統(tǒng)的混亂度也在不斷增大。為了降低軟件系統(tǒng)混亂的速度,必須要對其施以外力(重構(gòu))。
重構(gòu)系統(tǒng)和重構(gòu)代碼一樣,首先要先識別系統(tǒng)的壞味道,Davide Taibi和Valentina Lenarduzzi在文章《On the Definition of Microservice Bad Smells》中定義了微服務(wù)的壞味道,今天我們就來聊聊如何消滅其中的一個壞味道——循環(huán)依賴。
循環(huán)依賴的危害
微服務(wù)之間的循環(huán)依賴類似于類之間的循環(huán)依賴,當(dāng)依賴關(guān)系形成了環(huán),會有很多危害:
首先是微服務(wù)之間的耦合性非常強(qiáng),服務(wù)很難做到獨立部署,這嚴(yán)重違反了微服務(wù)的初衷;這種情況往往是服務(wù)之間的調(diào)用沒有約束導(dǎo)致的,為了方便取到或更新數(shù)據(jù),服務(wù)之間可以隨意的調(diào)用,以”微服務(wù)“為設(shè)計目標(biāo)的系統(tǒng)會逐漸演變成一個分布式大單體,下圖展示了三個服務(wù)之間的循環(huán)依賴關(guān)系:
另外循環(huán)依賴很可能導(dǎo)致一些循環(huán)調(diào)用或并發(fā)問題,造成一些復(fù)雜難以定位的問題,以上圖中訂單和客戶的循環(huán)依賴為例:
訂單服務(wù)依賴客戶服務(wù)來獲取客戶信息,為了滿足可以根據(jù)客戶姓名查詢訂單的需求,除了記錄客戶的ID,同時將客戶的姓名冗余到訂單的數(shù)據(jù)中。當(dāng)客戶信息更新時,為保證訂單上數(shù)據(jù)的準(zhǔn)確,客戶服務(wù)會調(diào)用訂單服務(wù)更新訂單上的客戶信息。
循環(huán)依賴讓事情變得復(fù)雜,不同的實現(xiàn)會有不同的問題表現(xiàn),我們來看這樣的場景:當(dāng)訂單狀態(tài)變化時,客戶的狀態(tài)也要跟著變化,訂單服務(wù)會更新客戶的狀態(tài),而客戶信息的變化反過來需要更新訂單上冗余的客戶信息;假設(shè)在訂單上加了樂觀鎖,同一時刻的兩次訂單更新因為樂觀鎖會有一個失敗,排查問題過程中通過現(xiàn)象會優(yōu)先考慮并發(fā)的問題,然而真相卻恰恰相反,參見下圖時序:
循環(huán)依賴產(chǎn)生的原因
數(shù)據(jù)過度冗余
數(shù)據(jù)冗余是導(dǎo)致服務(wù)之間循環(huán)依賴的主要原因,為了保證多個服務(wù)之間數(shù)據(jù)的一致性,某個服務(wù)對數(shù)據(jù)的修改要同步到其他服務(wù),保證其他服務(wù)冗余字段的信息準(zhǔn)確。
上面訂單上冗余客戶姓名的場景就是個很好的例子,冗余數(shù)據(jù)在一定程度上增強(qiáng)了業(yè)務(wù)實現(xiàn)的簡便性,但為了保證數(shù)據(jù)一致性,也增加了服務(wù)間不必要的調(diào)用。
缺失業(yè)務(wù)概念
和數(shù)據(jù)過度冗余相對的是在軟件設(shè)計時缺少必要的業(yè)務(wù)概念,也同樣會導(dǎo)致循環(huán)依賴的情況發(fā)生;典型的場景是上游系統(tǒng)的數(shù)據(jù)狀態(tài)需要下游系統(tǒng)的數(shù)據(jù)進(jìn)行計算得到,當(dāng)需要對上游系統(tǒng)的數(shù)據(jù)狀態(tài)時,上游服務(wù)會調(diào)用下游服務(wù)的接口來進(jìn)行計算。
這種場景也很常見,繼續(xù)用訂單和客戶的例子幫助理解:
如果一個客戶的所有訂單要么取消,要么已經(jīng)完成開票結(jié)算流程,那么這個客戶就是一個成功客戶,否則是一個服務(wù)中客戶。
在軟件設(shè)計時如果沒有給客戶定義一個狀態(tài)來表達(dá)這個業(yè)務(wù)概念,那么每次需要拿到客戶狀態(tài)時只能通過訂單服務(wù)來進(jìn)行查詢計算。
濫用同步調(diào)用
上面兩種場景一旦產(chǎn)生循環(huán)依賴,往往也伴隨著同步調(diào)用的濫用,然而也有一些場景既沒有數(shù)據(jù)過度冗余,也沒有缺少業(yè)務(wù)概念,但仍然可能會濫用同步調(diào)用。
上文圖中用戶離職更新客戶所有者信息的場景就是一個很好的例子,雖然使用同步調(diào)用的方式從實現(xiàn)結(jié)果上來看是非常直接有效的方法,但長遠(yuǎn)來看,這種無腦的濫用同步調(diào)用讓系統(tǒng)的復(fù)雜度變的非常高,當(dāng)我們希望替換或重構(gòu)某個服務(wù),我們很難依據(jù)業(yè)務(wù)分析清楚到底有哪些服務(wù)會受到影響。
消滅循環(huán)依賴的方法
通過上面對循環(huán)依賴原因的分析(這里我們不考慮微服務(wù)劃分不合理的場景),可以看出循環(huán)依賴通常是為了滿足某種業(yè)務(wù)需求,在服務(wù)之間增加了不合理的調(diào)用。
要解決循環(huán)依賴,必須要在微服務(wù)之間建立一些原則來約束微服務(wù)之間的通信,定期通過這些原則來審視我們的系統(tǒng),找到問題并進(jìn)行重構(gòu),這些原則應(yīng)該包括:
- 定義服務(wù)上下游關(guān)系,下游服務(wù)可以直接依賴上游服務(wù),反之則不可
- 上游服務(wù)的變更對下游服務(wù)產(chǎn)生影響需要通過領(lǐng)域事件(異步)的方式來實現(xiàn)
- 服務(wù)之間要通過數(shù)據(jù)Id(或類Id,能夠唯一代表數(shù)據(jù)且不變的屬性)來進(jìn)行關(guān)聯(lián),盡量不做過多的數(shù)據(jù)冗余
- 一旦需要上游服務(wù)調(diào)用下游服務(wù)才能完成業(yè)務(wù)時,要考慮是否上游服務(wù)缺少業(yè)務(wù)概念
- 為滿足前端邏輯而導(dǎo)致的服務(wù)間交互邏輯要放到BFF(Backend for frontend)中,而不是增加服務(wù)間的調(diào)用
小結(jié)
微服務(wù)架構(gòu)提倡把服務(wù)拆的小而獨立,一個軟件系統(tǒng)往往會有很多服務(wù),大部分業(yè)務(wù)需求是需要多個服務(wù)一起配合才能完成;受限于交付壓力,在對架構(gòu)的理解不是很透徹的情況下,開發(fā)人員往往傾向于采用更容易的(熵增)方式實現(xiàn)功能,在這種情況下,循環(huán)依賴是一個非常容易發(fā)生的壞味道,對系統(tǒng)的健康具有巨大危害。
我們可以通過一些技術(shù)手段來發(fā)現(xiàn)系統(tǒng)中的循環(huán)依賴問題,比如通過鏈路追蹤系統(tǒng)(如Zipkin)將服務(wù)間依賴關(guān)系可視化出來;也可以在發(fā)現(xiàn)問題時將有問題的流程時序圖畫出來,可視化的方式可以很容易幫我們找到系統(tǒng)中循環(huán)依賴的問題,然后我們就可以對癥下藥,消滅壞味道。