問題表現(xiàn)
最近的迭代轉測后遇到了一個比較有意思的問題。在測試環(huán)境整體運行還算平穩(wěn),但是過一段時間之后,就開始有接口超時了,日志中出現(xiàn)非常多的 “java.net.SocketTimeoutException: Read timed out”。試了幾次重啟大法,每次都是只能堅持一會之后,再次出現(xiàn) SocketTimeoutException。
注意 :在測試環(huán)境于遇到問題重啟服務,并不是一個好的實踐,因為重啟可能會讓不容易出現(xiàn)的問題現(xiàn)場被破壞。如果問題在測試環(huán)境不能再重新,卻在發(fā)版后出現(xiàn)在生產環(huán)境的話,那不僅會造成生產運維事件,還要在巨大的壓力下去解決問題。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
初步分析
順著測試匯報的出現(xiàn)問題的場景,跟蹤調用鏈上相關服務的日志,發(fā)現(xiàn)出現(xiàn)了微服務之間循依賴調用。大致情況可以抽象如下所示(圖中所有調用都是 http 協(xié)議):
![ef8cf1aa-953f-11ed-bfe3-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A0/00/wKgaomToJy-AOXJsAAB08JSrRIg605.png)
- Client 調用服務 Foo.hello()
- Foo.hello() 邏輯中會調用服務 Boo.boo()
- Boo.boo() 又調用回服務 Foo 的另外一個方法 another()
當然真實的場景要比較這個復雜,調用鏈更長,不過最終形成了環(huán)形依賴調用。至于這個環(huán)形依賴為什么回導致超時,當時想了多種可能,比如數據庫慢查詢、數據庫鎖、分布式鎖等等。但是整個調用鏈上都是查詢請求,而且查詢相關的數據量也非常小,不會有鎖存在。發(fā)生問題的時候也沒有與查詢數據相關的數據庫寫請求。
鑒于這個環(huán)形依賴調用確實是這個迭代版本中引入的變更,以及雖然沒有理清其中的因果關系原理,但是這個環(huán)性依賴調用還是很可疑的,而且是不必要的環(huán)形調用。就抱著將環(huán)形依賴調用去掉試試看的態(tài)度,做了修復。修復完后,SocketTimeoutException 不再出現(xiàn)了。問題解決了。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
探尋原因
問題雖然不再出現(xiàn),但是憑運氣解決的問題,通常有可能不是真的的解決。只有弄清楚背后的原理,我們才能真正的確認問題是不是這個原因導致的,這樣的修復是不是真的把問題解決了。
通過假設環(huán)形調用就是導致調用超時的直接原因。我們看看能不能推出因果關系。通過把Foo 服務容器畫的更詳細一點,如下圖:
![efa2417c-953f-11ed-bfe3-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A0/00/wKgaomToJy-ARxHvAADbKeLSCCk549.png)
通過這個圖示,我們可以發(fā)現(xiàn),如果容器中接收請求的線程池如果都在等待服務Boo.boo() 的響應,而 Boo 又需要調用回服務 Foo.another()。這個時候,如果所有的線程都處于這樣的狀態(tài),我們就會發(fā)現(xiàn)服務 Foo 容器中以及沒有線程來處理 Boo 的請求了。某種程度上來說就是死鎖了。到這里,我們就可以很確定了,這個環(huán)形依賴調用就是導致出現(xiàn)調用超時的罪魁禍首。當 client 發(fā)起的請求速度大于這個環(huán)形調用鏈的處理速度的時候,慢慢的就會導致服務 Foo 的所有線程都進入這種死鎖狀態(tài)。
驗證
這里只列出關鍵的代碼,具體的代碼可以參考 gitee 工程:https://gitee.com/donghbcn/CircularDependency
Eureka 服務器
建個簡單工程將Eureka server啟動起來。
服務 Foo
創(chuàng)建 SpringBoot 工程實現(xiàn) Foo 服務。Foo 通過 FeignClient 調用 Boo 服務。設置缺省的容器 Tomcat 的最大線程數為 16,Tomcat 默認配置最大線程數 200,對于驗證這個場景有點了大了,要看到效果需要等的時間有點長。
application.properties
spring.application.name=demo-foo
server.port=8000
eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka
server.tomcat.threads.max=16
packagecom.cd.demofoo;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;
@RestController
publicclassFooController{
@Autowired
BooFeignClientbooFeignClient;
@RequestMapping("/hello")
publicStringhello(){
longstart=System.currentTimeMillis();
System.out.println("["+Thread.currentThread()+
"]foo:hellocalled,callboo:boonow");
booFeignClient.boo();
System.out.println("["+Thread.currentThread()+
"]foo:hellocalled,callboo:boo,totalcost:"+
(System.currentTimeMillis()-start));
return"helloworld";
}
@RequestMapping("/another")
publicStringanother(){
longstart=System.currentTimeMillis();
try{
//通過slepp模擬一個耗時調用
Thread.sleep(100);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println("foo:anothercalled,totalcost:"+(System.currentTimeMillis()-start));
return"another";
}
}
服務 Boo
創(chuàng)建 SpringBoot 工程實現(xiàn) Boo 服務。Boo 通過 FeignClient 調用 Foo 服務。
packagecom.cd.demoboo;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;
@RestController
publicclassBooController{
@Autowired
FooFeignClientfooFeignClient;
@RequestMapping("/boo")
publicStringboo(){
longstart=System.currentTimeMillis();
fooFeignClient.another();
System.out.println("boo:boocalled,callfoo:another,totalcost:"+
(System.currentTimeMillis()-start));
return"boo";
}
}
Jmeter
采用 Jmeter 來模擬并發(fā) Client 調用。配置了30 個 線程,無限循環(huán)。
![efca1c60-953f-11ed-bfe3-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A0/00/wKgaomToJy-AVBR6AAPu32wZUVc842.png)
很快服務 Foo 日志就卡死了。過一會 Boo 的日志開始出現(xiàn) SocketTimeoutException,如下圖:
![efd8eed4-953f-11ed-bfe3-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A0/00/wKgaomToJy-AST_aAAPxN_8lyDE766.png)
jstack
通過 jstack 我們可以看到 Foo 進程的所有線程都卡在 hello() 調用上了。
![efe89a28-953f-11ed-bfe3-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A0/00/wKgaomToJy-Aa4l-AAEsiUszkGk220.png)
總結
微服務之間的環(huán)形依賴類似于類之間的循環(huán)依賴,當依賴關系形成了環(huán),會造成比較嚴重的問題:
- 微服務直接不能形成環(huán)形調用,否則非常容易出現(xiàn)死鎖狀態(tài)
- 微服務之間的耦合性非常強,這嚴重違反了微服務的初衷;這種情況往往是服務之間的調用沒有約束導致的,為了方便取到或更新數據,服務之間可以隨意的調用,以”微服務“為設計目標的系統(tǒng)會逐漸演變成一個分布式大單體
審核編輯 :李倩
-
線程
+關注
關注
0文章
507瀏覽量
19763 -
微服務
+關注
關注
0文章
143瀏覽量
7443
原文標題:微服務循環(huán)依賴調用引發(fā)的血案
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
NVIDIA發(fā)布全新NIM AI Guardrail微服務
NVIDIA 發(fā)布保障代理式 AI 應用安全的 NIM 微服務
微服務容器化部署好處多嗎?
容器化能替代微服務嗎?兩者有何區(qū)別
寶藏級微服務架構工具合集
NVIDIA NIM微服務登陸亞馬遜云科技
SSR與微服務架構的結合應用
微服務架構與容器云的關系與區(qū)別
入門級攻略:如何容器化部署微服務?
Proxyless的多活流量和微服務治理
![Proxyless的多活流量和<b class='flag-5'>微服務</b>治理](https://file1.elecfans.com//web2/M00/04/79/wKgZombO5bOALV66AABWmg83ey8199.jpg)
NVIDIA NIM微服務帶來巨大優(yōu)勢
采用OpenUSD和NVIDIA NIM微服務創(chuàng)建精準品牌視覺
全新 NVIDIA NeMo Retriever微服務大幅提升LLM的準確性和吞吐量
![全新 NVIDIA NeMo Retriever<b class='flag-5'>微服務</b>大幅提升LLM的準確性和吞吐量](https://file1.elecfans.com/web2/M00/FE/A1/wKgZomajFTOAPHuKAAAbwwpa0vg402.png)
評論