新聞中心
最近的迭代轉(zhuǎn)測后遇到了一個比較有意思的問題。在測試環(huán)境整體運行還算平穩(wěn),但是過一段時間之后,就開始有接口超時了,日志中出現(xiàn)非常多的 “java.net.SocketTimeoutException: Read timed out”。試了幾次重啟大法,每次都是只能堅持一會之后,再次出現(xiàn) SocketTimeoutException。

成都創(chuàng)新互聯(lián)是一家集成都網(wǎng)站建設(shè)、成都網(wǎng)站制作、網(wǎng)站頁面設(shè)計、網(wǎng)站優(yōu)化SEO優(yōu)化為一體的專業(yè)網(wǎng)站建設(shè)公司,已為成都等多地近百家企業(yè)提供網(wǎng)站建設(shè)服務(wù)。追求良好的瀏覽體驗,以探求精品塑造與理念升華,設(shè)計最適合用戶的網(wǎng)站頁面。 合作只是第一步,服務(wù)才是根本,我們始終堅持講誠信,負(fù)責(zé)任的原則,為您進行細(xì)心、貼心、認(rèn)真的服務(wù),與眾多客戶在蓬勃發(fā)展的市場環(huán)境中,互促共生。
注意:在測試環(huán)境于遇到問題重啟服務(wù),并不是一個好的實踐,因為重啟可能會讓不容易出現(xiàn)的問題現(xiàn)場被破壞。如果問題在測試環(huán)境不能再重新,卻在發(fā)版后出現(xiàn)在生產(chǎn)環(huán)境的話,那不僅會造成生產(chǎ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)然真實的場景要比較這個復(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)用確實是這個迭代版本中引入的變更,以及雖然沒有理清其中的因果關(guān)系原理,但是這個環(huán)性依賴調(diào)用還是很可疑的,而且是不必要的環(huán)形調(diào)用。就抱著將環(huán)形依賴調(diào)用去掉試試看的態(tài)度,做了修復(fù)。修復(fù)完后,SocketTimeoutException 不再出現(xiàn)了。問題解決了。
探尋原因
問題雖然不再出現(xiàn),但是憑運氣解決的問題,通常有可能不是真的的解決。只有弄清楚背后的原理,我們才能真正的確認(rèn)問題是不是這個原因?qū)е碌?,這樣的修復(fù)是不是真的把問題解決了。
通過假設(shè)環(huán)形調(diào)用就是導(dǎo)致調(diào)用超時的直接原因。我們看看能不能推出因果關(guān)系。通過把Foo 服務(wù)容器畫的更詳細(xì)一點,如下圖:
通過這個圖示,我們可以發(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 的所有線程都進入這種死鎖狀態(tài)。
驗證
這里只列出關(guān)鍵的代碼,具體的代碼可以參考 gitee 工程:https://gitee.com/donghbcn/CircularDependency
Eureka 服務(wù)器
建個簡單工程將Eureka server啟動起來。
服務(wù) Foo
創(chuàng)建 SpringBoot 工程實現(xiàn) Foo 服務(wù)。Foo 通過 FeignClient 調(diào)用 Boo 服務(wù)。設(shè)置缺省的容器 Tomcat 的最大線程數(shù)為 16,Tomcat 默認(rèn)配置最大線程數(shù) 200,對于驗證這個場景有點了大了,要看到效果需要等的時間有點長。
application.properties
spring.application.name=demo-foo
server.port=8000
eureka.client.serviceUrl.defaultZnotallow=http://localhost:8080/eureka
server.tomcat.threads.max=16
package com.cd.demofoo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FooController {
@Autowired
BooFeignClient booFeignClient;
@RequestMapping("/hello")
public String hello(){
long start = System.currentTimeMillis();
System.out.println("[" + Thread.currentThread() +
"] foo:hello called, call boo:boo now");
booFeignClient.boo();
System.out.println("[" + Thread.currentThread() +
"] foo:hello called, call boo:boo, total cost:" +
(System.currentTimeMillis() - start));
return "hello world";
}
@RequestMapping("/another")
public String another(){
long start = System.currentTimeMillis();
try {
//通過 slepp 模擬一個耗時調(diào)用
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("foo:another called, total cost:" + (System.currentTimeMillis() - start));
return "another";
}
}
服務(wù) Boo
創(chuàng)建 SpringBoot 工程實現(xiàn) Boo 服務(wù)。Boo 通過 FeignClient 調(diào)用 Foo 服務(wù)。
package com.cd.demoboo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BooController {
@Autowired
FooFeignClient fooFeignClient;
@RequestMapping("/boo")
public String boo(){
long start = System.currentTimeMillis();
fooFeignClient.another();
System.out.println("boo:boo called, call foo:another, total cost:" +
(System.currentTimeMillis() - start));
return "boo";
}
}
Jmeter
采用 Jmeter 來模擬并發(fā) Client 調(diào)用。配置了30 個 線程,無限循環(huán)。
很快服務(wù) Foo 日志就卡死了。過一會 Boo 的日志開始出現(xiàn) SocketTimeoutException,如下圖:
jstack
通過 jstack 我們可以看到 Foo 進程的所有線程都卡在 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ù)之間的耦合性非常強,這嚴(yán)重違反了微服務(wù)的初衷;這種情況往往是服務(wù)之間的調(diào)用沒有約束導(dǎo)致的,為了方便取到或更新數(shù)據(jù),服務(wù)之間可以隨意的調(diào)用,以”微服務(wù)“為設(shè)計目標(biāo)的系統(tǒng)會逐漸演變成一個分布式大單體?。
網(wǎng)頁名稱:微服務(wù)循環(huán)依賴調(diào)用引發(fā)的血案
URL分享:http://m.fisionsoft.com.cn/article/cceooog.html


咨詢
建站咨詢
