面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

今天文章內容來自一位朋友出去面試碰到的問題:

「瞭解 Dubbo 服務預熱過程嗎?詳細聊聊它的原理。」

這個問題朋友沒有很好答出來,因為之前也沒了解過。說實話一開始我只是大概知道這塊預熱的程式碼位於何處,但是原理什麼的還是沒有仔細去了解。

所以這次仔細去看了下程式碼,查了一些 Github 這塊程式碼提交記錄,終於搞明白這塊的原理,跟大家一起分享下。

預熱

首先我們來看下什麼是服務預熱?

先舉一個生活的中的例子,買過新車的同學應該知道新車都有一個磨合期的,大概開個一兩千公里之後,才能達到最佳的狀態。

其實服務預熱也是這個意思,服務剛啟動的時候將存在一段

「磨合期」

,這段期間服務執行狀態沒有達到最佳,如果一下子將服務流量提升到平常的狀態,可能會存在大量的請求超時或者瞬間將系統壓垮。

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

所以服務剛啟動的時候我們要慢慢增加的流量,直到一段時間後增加到閾值的上限,給系統一個

「預熱過程」

,讓其執行狀態到達最佳。

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

那為什麼服務剛啟動的時候系統狀態沒有到達最佳狀態?

大概原因其實如下:

Java 應用存在一個類載入的過程,而這個過程是按需載入的。即服務剛啟動時候,JVM 只加載了啟動過程必需的類。

我們自己所需要的類,直到服務被呼叫之後才會被真正的載入。

另外對於一些

「熱點程式碼」

,JVM 將會使用 JIT 編譯器編譯成原生代碼,提高執行速度。

上面兩個過程是出於 JVM 系統層面的影響。

除此之外,我們服務系統中可能會需要一些快取資源。剛啟動的時候,由於資源不存在,服務需要去載入這些資源。

Dubbo 預熱實現方式

好了,瞭解完預熱是咋回事後,我們回到正題,來看下 Dubbo 是如何實現預熱的。

首先我們來看下 Dubbo 服務模型:

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

服務提供者啟動之後將會把節點相關資訊註冊到註冊中心,服務消費者透過註冊中心就可以及時獲取所有的服務節點。

當服務消費者呼叫服務時,內部將會透過負載均衡元件選擇一個節點,進行服務呼叫。

如上圖所示,假設 B 節點服務剛啟動,其需要一個預熱過程,這就需要服務消費者逐漸將流量分發給 B 節點。

下面我們就從 Dubbo 原始碼出發,觀察服務預熱具體實現方式,具體原始碼位於

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

ps: 當前原始碼 Dubbo 版本為2。7。4,低於這個版本程式碼實現存在少量差異,詳情見下文。

這段程式碼主要分為三步:

獲取服務提供者啟動時間

使用當前時間減去服務提供者啟動時間,計算服務提供者已執行時間

根據已執行時間動態計算服務預熱過程的權重

第三步動態權重計算方法如下:

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

這裡計算方式其實很簡單,簡單來說服務執行時間越久,權重越高,直到正常權重。

假如服務提供者已執行 1 分鐘,那麼 weight 最終結果為 10 。

假如服務提供者已執行 5 分鐘,那麼 weight 最終結果為 50 。

假如服務提供者已執行 11 分鐘,超過預設預熱時間的閾值 10分 鍾,那麼將不會再計算,直接返回 weight 預設權重。

這裡我們需要注意的是,Dubbo 預設提供五種負載均衡的策略:

Random LoadBalance :

「加權隨機」

策略

RoundRobin LoadBalance:

「加權輪詢」

策略

LeastActive LoadBalance:

「最少活躍呼叫數」

策略

ConsistentHash LoadBalance:

「一致性 Hash」

策略

ShortestResponse LoadBalance:

「最短響應時間」

策略

「ShortestResponse LoadBalance」

策略小夥伴們可能會比較陌生,官方文件中並沒有提到這個策略。

其實這個是 Dubbo 2。7。7 版本新增的負載均衡策略,官方文件估計還沒更新。

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

ps:感興趣的小夥伴,可以去修改下官方的文件,增加這個新的負載均衡的策略,為開源獻出我們的一份力量。

回到正文,從呼叫關係可以看到,

「ConsistentHash LoadBalance」

實現類是不支援服務預熱,這點需要注意一下。

Dubbo 預熱歷史 bug-反覆橫跳

雖然 Dubbo 預熱的相關程式碼,總體看起來不是很難,但是歷史版本還是存在幾個 Bug,導致預熱失效。

Dubbo 2。5。5 之前的版本

在 Dubbo 2。5。5 之前的版本,實現方式如下:

這個版本跟現在程式碼一樣,都是從節點的 獲取服務啟動時間。不過這個版本存在一些問題,Dubbo 沒有把服務提供者啟動時間傳給消費者,導致這裡獲取 是消費者啟動時間,這樣就導致預熱失效。

等到 Dubbo 2。5。6 ,修復這個問題,原始碼如下:

這個版本將服務提供者啟動時間單獨儲存在 屬性中,原始碼位於

透過這種方式修復預熱失效的問題。

Dubbo 2。7。2 預熱又失效了

當 Dubbo 版本升級到 2。7。2 ,這個預熱失效 Bug 又回來了。帶來這個問題主要原因是 原始碼中清除了,轉而統一使用 儲存服務啟動時間。

但是呢,由於修改沒有徹底, 還是依然使用 獲取服務啟動時間,這就導致預熱失效。

預熱程式碼的隱藏 bug

這個 Bug 在Dubbo 2。7。4 版本被徹底修復,另外還順帶優化了程式碼中存在缺陷。

先看下原先的程式碼中國缺陷,原先預熱程式碼實現採用如下方式計算服務啟動執行的時間。

但是這裡存在一個問題,如果服務提供者與消費者兩端時鐘是不一致,服務提供者啟動時間很有可能會大於消費者本地時間。

這種情況, 計算結果為一個負值,這就會導致權重將使用配置的預設值,預熱也失效了。

所以針對這種情況

「@aftersss」

提供了修復的方案,加入相關的判斷,當 為負值的時候,直接返回權重 1。

不過在

「Code review」

過程中,

「@beiwei30」

覺得不用加入額外 if 判斷,可以直接使用 相容。

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

不過這樣修改,還是存在一個問題: 精度丟失問題。

這是一個遠大於 的值,所以在 轉為 時候精度丟失,導致最後實際得到 int 值為

「67704」

而這個值小於服務預熱的預設時間(10 * 60 * 1000),所以進入動態計算權重環節,最終將得到一個比較小的權重,這就導致

「假預熱」

所以最後還是採用

「@aftersss」

修復的方案,採用 型別儲存時間戳計算結果,最終最佳化程式碼如下:

面試官:高併發下重啟服務,介面呼叫老是超時,你有什麼解決辦法?

總結

今天的文章主要介紹了服務預熱的作用,以及 Dubbo 服務預熱的實現方式。

這個實現方式整體來說不是很難,簡單來說就是隨著執行時間逐漸提高權重,從而增加服務節點的流量。

如果你當前使用框架並沒有這個功能,而你正需要服務預熱,可以參考 Dubbo 的實現方式。

另外由於 Dubbo 歷史版本存在一些 Bug,如果各小夥伴需要使用服務預熱功能,需要注意避免使用以下版本:

「Dubbo 2.5.5 之前的版本」

「Dubbo 2.7.2/ 2.7.3」