當前位置: 妍妍網 > 碼農

微服務迴圈依賴引發慘案,有坑!

2024-07-16碼農

👉 歡迎 ,你將獲得: 專屬的計畫實戰 / 1v1 提問 / Java 學習路線 / 學習打卡 / 每月贈書 / 社群討論

  • 新計畫: 【從零手擼:仿小紅書(微服務架構)】 正在持續爆肝中,基於 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., ;

  • 【從零手擼:前後端分離部落格計畫(全棧開發)】 2期已完結,演示連結: http://116.62.199.48/ ;

  • 截止目前, 累計輸出 50w+ 字,講解圖 2200+ 張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將 Java 領域典型的計畫都整一波,如秒殺系統, 線上商城, IM 即時通訊,Spring Cloud Alibaba 等等,

    最近的叠代轉測後遇到了一個比較有意思的問題。在測試環境整體執行還算平穩,但是過一段時間之後,就開始有介面超時了,日誌中出現非常多的 「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 的請求了。某種程度上來說就是死結了。到這裏,我們就可以很確定了,這個環形依賴呼叫就是導致出現呼叫超時的罪魁禍首。當 client 發起的請求速度大於這個環形呼叫鏈的處理速度的時候,慢慢的就會導致服務 Foo 的所有執行緒都進入這種死結狀態。

    驗證

    這裏只列出關鍵的程式碼,具體的程式碼可以參考 gitee 工程:https://gitee.com/donghbcn/CircularDependency

    Eureka 伺服器

    建個簡單工程將Eureka server啟動起來。

    服務 Foo

    建立 SpringBoot 工程實作 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
    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 模擬一個耗時呼叫
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("foo:another called, total cost:" + (System.currentTimeMillis() - start));
    return"another";
    }
    }

    服務 Boo

    建立 SpringBoot 工程實作 Boo 服務。Boo 透過 FeignClient 呼叫 Foo 服務。

    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 來模擬並行 Client 呼叫。配置了30 個 執行緒,無限迴圈。

    圖片

    很快服務 Foo 日誌就卡死了。過一會 Boo 的日誌開始出現 SocketTimeoutException,如下圖:

    圖片

    jstack

    透過 jstack 我們可以看到 Foo 行程的所有執行緒都卡在 hello() 呼叫上了。

    圖片

    總結

    微服務之間的環形依賴類似於類之間的迴圈依賴,當依賴關系形成了環,會造成比較嚴重的問題:

  • 微服務直接不能形成環形呼叫,否則非常容易出現死結狀態

  • 微服務之間的耦合性非常強,這嚴重違反了微服務的初衷;這種情況往往是服務之間的呼叫沒有約束導致的,為了方便取到或更新數據,服務之間可以隨意的呼叫,以」微服務「為設計目標的系統會逐漸演變成一個 分布式大單體

  • 👉 歡迎 ,你將獲得: 專屬的計畫實戰 / 1v1 提問 / Java 學習路線 / 學習打卡 / 每月贈書 / 社群討論

  • 新計畫: 【從零手擼:仿小紅書(微服務架構)】 正在持續爆肝中,基於 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., ;

  • 【從零手擼:前後端分離部落格計畫(全棧開發)】 2期已完結,演示連結: http://116.62.199.48/ ;

  • 截止目前, 累計輸出 50w+ 字,講解圖 2200+ 張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將 Java 領域典型的計畫都整一波,如秒殺系統, 線上商城, IM 即時通訊,Spring Cloud Alibaba 等等,


    1. 

    2. 

    3. 

    4. 

    最近面試BAT,整理一份面試資料Java面試BATJ通關手冊,覆蓋了Java核心技術、JVM、Java並行、SSM、微服務、資料庫、數據結構等等。

    獲取方式:點「在看」,關註公眾號並回復 Java 領取,更多內容陸續奉上。

    PS:因公眾號平台更改了推播規則,如果不想錯過內容,記得讀完點一下在看,加個星標,這樣每次新文章推播才會第一時間出現在你的訂閱列表裏。

    「在看」支持小哈呀,謝謝啦