微服務(wù)循環(huán)依賴調(diào)用引發(fā)的血案
最近的迭代轉(zhuǎn)測后遇到了一個比較有意思的問題。在測試環(huán)境整體運(yùn)行還算平穩(wěn),但是過一段時間之后,就開始有接口超時了,日志中出現(xiàn)非常多的 “java.net.SocketTimeoutException: Read timed out”。試了幾次重啟大法,每次都是只能堅(jiān)持一會之后,再次出現(xiàn) SocketTimeoutException。
注意:在測試環(huán)境于遇到問題重啟服務(wù),并不是一個好的實(shí)踐,因?yàn)橹貑⒖赡軙尣蝗菀壮霈F(xiàn)的問題現(xiàn)場被破壞。如果問題在測試環(huán)境不能再重新,卻在發(fā)版后出現(xiàn)在生產(chǎn)環(huán)境的話,那不僅會造成生產(chǎn)運(yùn)維事件,還要在巨大的壓力下去解決問題。
初步分析
順著測試匯報的出現(xiàn)問題的場景,跟蹤調(diào)用鏈上相關(guān)服務(wù)的日志,發(fā)現(xiàn)出現(xiàn)了微服務(wù)之間循依賴調(diào)用。大致情況可以抽象如下所示(圖中所有調(diào)用都是 http 協(xié)議):

- Client 調(diào)用服務(wù) Foo.hello()
- Foo.hello() 邏輯中會調(diào)用服務(wù) Boo.boo()
- Boo.boo() 又調(diào)用回服務(wù) Foo 的另外一個方法 another()
當(dāng)然真實(shí)的場景要比較這個復(fù)雜,調(diào)用鏈更長,不過最終形成了環(huán)形依賴調(diào)用。至于這個環(huán)形依賴為什么回導(dǎo)致超時,當(dāng)時想了多種可能,比如數(shù)據(jù)庫慢查詢、數(shù)據(jù)庫鎖、分布式鎖等等。但是整個調(diào)用鏈上都是查詢請求,而且查詢相關(guān)的數(shù)據(jù)量也非常小,不會有鎖存在。發(fā)生問題的時候也沒有與查詢數(shù)據(jù)相關(guān)的數(shù)據(jù)庫寫請求。
鑒于這個環(huán)形依賴調(diào)用確實(shí)是這個迭代版本中引入的變更,以及雖然沒有理清其中的因果關(guān)系原理,但是這個環(huán)性依賴調(diào)用還是很可疑的,而且是不必要的環(huán)形調(diào)用。就抱著將環(huán)形依賴調(diào)用去掉試試看的態(tài)度,做了修復(fù)。修復(fù)完后,SocketTimeoutException 不再出現(xiàn)了。問題解決了。
探尋原因
問題雖然不再出現(xiàn),但是憑運(yùn)氣解決的問題,通常有可能不是真的的解決。只有弄清楚背后的原理,我們才能真正的確認(rèn)問題是不是這個原因?qū)е碌?,這樣的修復(fù)是不是真的把問題解決了。
通過假設(shè)環(huán)形調(diào)用就是導(dǎo)致調(diào)用超時的直接原因。我們看看能不能推出因果關(guān)系。通過把Foo 服務(wù)容器畫的更詳細(xì)一點(diǎn),如下圖:

通過這個圖示,我們可以發(fā)現(xiàn),如果容器中接收請求的線程池如果都在等待服務(wù)Boo.boo() 的響應(yīng),而 Boo 又需要調(diào)用回服務(wù) Foo.another()。這個時候,如果所有的線程都處于這樣的狀態(tài),我們就會發(fā)現(xiàn)服務(wù) Foo 容器中以及沒有線程來處理 Boo 的請求了。關(guān)注公眾號:碼猿技術(shù)專欄,回復(fù)關(guān)鍵詞:1111 獲取阿里內(nèi)部的Java性能調(diào)優(yōu)手冊!某種程度上來說就是死鎖了。到這里,我們就可以很確定了,這個環(huán)形依賴調(diào)用就是導(dǎo)致出現(xiàn)調(diào)用超時的罪魁禍?zhǔn)住.?dāng) client 發(fā)起的請求速度大于這個環(huán)形調(diào)用鏈的處理速度的時候,慢慢的就會導(dǎo)致服務(wù) Foo 的所有線程都進(jìn)入這種死鎖狀態(tài)。
驗(yàn)證
這里只列出關(guān)鍵的代碼,具體的代碼可以參考 gitee 工程:https://gitee.com/donghbcn/CircularDependency
Eureka 服務(wù)器
建個簡單工程將Eureka server啟動起來。
服務(wù) Foo
創(chuàng)建 SpringBoot 工程實(shí)現(xiàn) Foo 服務(wù)。Foo 通過 FeignClient 調(diào)用 Boo 服務(wù)。設(shè)置缺省的容器 Tomcat 的最大線程數(shù)為 16,Tomcat 默認(rèn)配置最大線程數(shù) 200,對于驗(yàn)證這個場景有點(diǎn)了大了,要看到效果需要等的時間有點(diǎn)長。
application.properties
服務(wù) Boo
創(chuàng)建 SpringBoot 工程實(shí)現(xiàn) Boo 服務(wù)。Boo 通過 FeignClient 調(diào)用 Foo 服務(wù)。
Jmeter
采用 Jmeter 來模擬并發(fā) Client 調(diào)用。配置了30 個 線程,無限循環(huán)。

很快服務(wù) Foo 日志就卡死了。過一會 Boo 的日志開始出現(xiàn) SocketTimeoutException,如下圖:

jstack
通過 jstack 我們可以看到 Foo 進(jìn)程的所有線程都卡在 hello() 調(diào)用上了。

總結(jié)
微服務(wù)之間的環(huán)形依賴類似于類之間的循環(huán)依賴,當(dāng)依賴關(guān)系形成了環(huán),會造成比較嚴(yán)重的問題:
- 微服務(wù)直接不能形成環(huán)形調(diào)用,否則非常容易出現(xiàn)死鎖狀態(tài)。
- 微服務(wù)之間的耦合性非常強(qiáng),這嚴(yán)重違反了微服務(wù)的初衷;這種情況往往是服務(wù)之間的調(diào)用沒有約束導(dǎo)致的,為了方便取到或更新數(shù)據(jù),服務(wù)之間可以隨意的調(diào)用,以”微服務(wù)“為設(shè)計(jì)目標(biāo)的系統(tǒng)會逐漸演變成一個分布式大單體?。
























