微服務循環依賴調用引發的血案
最近的迭代轉測后遇到了一個比較有意思的問題。在測試環境整體運行還算平穩,但是過一段時間之后,就開始有接口超時了,日志中出現非常多的 “java.net.SocketTimeoutException: Read timed out”。試了幾次重啟大法,每次都是只能堅持一會之后,再次出現 SocketTimeoutException。
注意:在測試環境于遇到問題重啟服務,并不是一個好的實踐,因為重啟可能會讓不容易出現的問題現場被破壞。如果問題在測試環境不能再重新,卻在發版后出現在生產環境的話,那不僅會造成生產運維事件,還要在巨大的壓力下去解決問題。
初步分析
順著測試匯報的出現問題的場景,跟蹤調用鏈上相關服務的日志,發現出現了微服務之間循依賴調用。大致情況可以抽象如下所示(圖中所有調用都是 http 協議):
- Client 調用服務 Foo.hello()
- Foo.hello() 邏輯中會調用服務 Boo.boo()
- Boo.boo() 又調用回服務 Foo 的另外一個方法 another()
當然真實的場景要比較這個復雜,調用鏈更長,不過最終形成了環形依賴調用。至于這個環形依賴為什么回導致超時,當時想了多種可能,比如數據庫慢查詢、數據庫鎖、分布式鎖等等。但是整個調用鏈上都是查詢請求,而且查詢相關的數據量也非常小,不會有鎖存在。發生問題的時候也沒有與查詢數據相關的數據庫寫請求。
鑒于這個環形依賴調用確實是這個迭代版本中引入的變更,以及雖然沒有理清其中的因果關系原理,但是這個環性依賴調用還是很可疑的,而且是不必要的環形調用。就抱著將環形依賴調用去掉試試看的態度,做了修復。修復完后,SocketTimeoutException 不再出現了。問題解決了。
探尋原因
問題雖然不再出現,但是憑運氣解決的問題,通常有可能不是真的的解決。只有弄清楚背后的原理,我們才能真正的確認問題是不是這個原因導致的,這樣的修復是不是真的把問題解決了。
通過假設環形調用就是導致調用超時的直接原因。我們看看能不能推出因果關系。通過把Foo 服務容器畫的更詳細一點,如下圖:
通過這個圖示,我們可以發現,如果容器中接收請求的線程池如果都在等待服務Boo.boo() 的響應,而 Boo 又需要調用回服務 Foo.another()。這個時候,如果所有的線程都處于這樣的狀態,我們就會發現服務 Foo 容器中以及沒有線程來處理 Boo 的請求了。關注公眾號:碼猿技術專欄,回復關鍵詞:1111 獲取阿里內部的Java性能調優手冊!某種程度上來說就是死鎖了。到這里,我們就可以很確定了,這個環形依賴調用就是導致出現調用超時的罪魁禍首。當 client 發起的請求速度大于這個環形調用鏈的處理速度的時候,慢慢的就會導致服務 Foo 的所有線程都進入這種死鎖狀態。
驗證
這里只列出關鍵的代碼,具體的代碼可以參考 gitee 工程:https://gitee.com/donghbcn/CircularDependency
Eureka 服務器
建個簡單工程將Eureka server啟動起來。
服務 Foo
創建 SpringBoot 工程實現 Foo 服務。Foo 通過 FeignClient 調用 Boo 服務。設置缺省的容器 Tomcat 的最大線程數為 16,Tomcat 默認配置最大線程數 200,對于驗證這個場景有點了大了,要看到效果需要等的時間有點長。
application.properties
服務 Boo
創建 SpringBoot 工程實現 Boo 服務。Boo 通過 FeignClient 調用 Foo 服務。
Jmeter
采用 Jmeter 來模擬并發 Client 調用。配置了30 個 線程,無限循環。
很快服務 Foo 日志就卡死了。過一會 Boo 的日志開始出現 SocketTimeoutException,如下圖:
jstack
通過 jstack 我們可以看到 Foo 進程的所有線程都卡在 hello() 調用上了。
總結
微服務之間的環形依賴類似于類之間的循環依賴,當依賴關系形成了環,會造成比較嚴重的問題:
- 微服務直接不能形成環形調用,否則非常容易出現死鎖狀態。
- 微服務之間的耦合性非常強,這嚴重違反了微服務的初衷;這種情況往往是服務之間的調用沒有約束導致的,為了方便取到或更新數據,服務之間可以隨意的調用,以”微服務“為設計目標的系統會逐漸演變成一個分布式大單體?。