從 gRPC 的重試策略說起

hechen發表於2020-03-28

本文首發在 技術成長之道 部落格,訪問 hechen0.com 檢視更多,或者微信搜尋「技術成長之道」關注我的公眾號,或者掃描下方二維碼關注 公眾號獲得第一時間更新通知!

微信

本文讓你瞭解

  1. 重試解決什麼問題
  2. 短時故障的產生原因
  3. 處理短時故障的挑戰
  4. 重試分為幾步
  5. gRPC 是如何進行重試的

1. 重試解決什麼問題

如今的網際網路服務早已不是單體應用,而是由若干個模組組成的微服務,每個模組可以進行單獨的擴容、縮容,獨立上線部署等等;模組與模組之間通過網路進行聯通。我們的應用必須對網路錯誤進行妥善的處理。從發生時長上而言,網路錯誤可以分為兩類:

  1. 長時間不可用,如光纖被挖斷,機房被炸等
  2. 短時間不可用,比如網路出現抖動,正在通訊的對端機器正好重新上線等

而重試是應對短時故障利器,簡單卻異常有效。

2. 短時間故障的產生原因

在任何環節下應用都會有可能產生短時故障。即使是在沒有網路參與的應用裡,軟體 bug 或硬體故障或一次意外斷電都會造成短時故障。短時故障是常態,想做到高可用不是靠避免這些故障的發生,而是去思考短時故障發生之後的應對策略。

就網際網路公司的服務而言,通過冗餘,各種切換等已經極大提高了整體應用的可用性,但其內部的短時故障卻是連綿不斷,原因有這麼幾個:

  1. 應用所使用的資源是共享的,比如 docker、虛擬機器、物理機混布等,如果多個虛擬單位 (docker 映象、虛擬機器、程式等) 之間的資源隔離沒有做好,就可能產生一個虛擬單位侵佔過多資源導致其它共享的虛擬單元出現錯誤。這些錯誤可能是短時的,也有可能是長時間的。
  2. 現在伺服器都是用比較便宜的硬體,即使是最重要的資料庫,網際網路公司的通常做法也是通過冗餘去保證高可用。貴和便宜的硬體之間有個很重要的指標差異就是故障率,便宜的機器更容易發生硬體故障,雖然故障率很低,但如果把這個故障率乘以網際網路公司數以萬計、十萬計的機器,每天都會有機器故障自然是家常便飯。這裡有個硬碟故障率統計很有意思可以看看。
  3. 除掉本身的問題外,現今的網際網路架構所需要的硬體元件也更多了,比如路由和負載均衡等等,更多的元件,意味著通訊鏈路上更多的節點,意味著增加了更多的不可靠。
  4. 應用之間的網路通訊問題,在架構設計時,對網路的基本假設就是不可靠,我們需要通過額外的機制彌補這種不可靠,有人問了,我的應用就是一個純內網應用,網路都是內網,也不可靠麼?嗯是的,不可靠。

3. 處理短時故障的挑戰

短時故障處理以下兩點挑戰

  1. 感知。應用需要能夠區分不同型別的錯誤,不同型別的錯誤對應的錯誤處理方式是不同的,沒有哪種應對手段可以處理所有的錯誤。比如網路抖動我們簡單重試即可,如果網路不可用,對於一個可靠的儲存系統,可能就需要經歷選主,副本切換等複雜操作才能保證資料的正確性。
  2. 處理。如何選擇一個合適的處理策略對於快速恢復故障、縮短響應時間以及減少對對端的衝擊是非常重要的。

4. 重試分為幾步

  1. 感知錯誤。通常我們使用錯誤碼識別不同型別的錯誤。比如在 REST 風格的應用裡面,HTTP 的 status code 可以用來識別不同型別的錯誤。
  2. 決策是否應該重試。不是所有錯誤都應該被重試,比如 HTTP 的 4xx 的錯誤,通常 4xx 表示的是客戶端的錯誤,這時候客戶端不應該進行重試操作。什麼錯誤可以重試需要具體情況具體分析,對於網路類的錯誤,我們也不是一股腦都進行重試,比如 zookeeper 這種強一致的儲存系統,發生了 network partition 之後,需要經過一系列複雜操作,簡單的重試根本不管用。
  3. 選擇重試策略。選擇一個合適的重試次數和重試間隔非常的重要。如果次數不夠,可能並不能有效的覆蓋這個短時間故障的時間段,如果重試次數過多,或者重試間隔太小,又可能造成大量的資源 (CPU、記憶體、執行緒、網路) 浪費。合適的次數和間隔取決於重試的上下文。舉例:如果是使用者操作失敗導致的重試,比如在網頁上點了一個按鈕失敗的重試,間隔就應該儘量短,確保使用者等待時間較短;如果請求失敗成本很高,比如整個流程很長,一旦中間環節出錯需要重頭開始,典型的如轉賬交易,這種情況就需要適當增加重試次數和最長等待時間以儘可能保證短時間的故障能被處理而無需重頭來過。
  4. 失敗處理與自動恢復。短時故障如果短時間沒有恢復就變成了長時間的故障,這個時候我們就不應該再進行重試了,但是等故障修復之後我們也需要有一種機制能自動恢復。

4.1 常見的重試時間間隔策略

  1. 指數避退。重試間隔時間按照指數增長,如等 3s 9s 27s 後重試。指數避退能有效防止對對端造成不必要的衝擊,因為隨著時間的增加,一個故障從短時故障變成長時間的故障的可能性是逐步增加的,對於一個長時間的故障,重試基本無效。
  2. 重試間隔線性增加。重試間隔的間隔按照線性增長,而非指數級增長,如等 3s 7s 13s 後重試。間隔增長能避免長時間等待,縮短故障響應時間。
  3. 固定間隔。重試間隔是一個固定值,如每 3s 後進行重試。
  4. 立即重試。有時候短時故障是因為網路抖動造成的,可能是因為網路包衝突或者硬體有問題等,這時候我們立即重試通常能解決這類問題。但是立即重試不應該超過一次,如果立即重試一次失敗之後,應該轉換為指數避退或者其它策略進行,因為大量的立即重試會給對端造成流量上的尖峰,對網路也是一個衝擊。
  5. 隨機間隔。當服務有多臺例項時,我們應該加入隨機的變數,比如 A 服務請求 B 服務,B 服務發生短時間不可用,A 服務的例項應該避免在同一時刻進行重試,這時候我們對間隔加入隨機因子會很好的在時間上平攤開所有的重試請求。

5. gRPC 是如何進行重試的

5.1 如何感知錯誤

gRPC 有自己一套類似 HTTP status code 的錯誤碼,每個錯誤碼都是個字串,如 INTERNAL、ABORTED、UNAVAILABLE。

5.2 如何決策

對於哪些錯誤可以重試是可配置的。通常而言,只有那些明確標識對端沒有接收處理請求的錯誤才需要被重試,比如對端返回一個 UNAVAILABLE 錯誤,這代表對端的服務目前處於不可用狀態。但也可以配置一個更加激進的重試策略,但關鍵是需要保證這些被重試的 gRPC 請求是冪等的,這個需要服務使用者和提供者共同協商出一個可以被重試的錯誤集合。

5.3 重試策略

gRPC 的重試策略分為兩類

  1. 重試策略,失敗後進行重試。
  2. 對衝策略,一次請求會給對端發出多個相同請求,只要有一個成功就認為成功。

先說下重試策略

重試策略

重試之時間策略

gPRC 用了上面我們提到的 指數避退 + 隨機間隔 組合起來的方式進行重試,詳見這裡

/* 偽碼 */

ConnectWithBackoff()
  current_backoff = INITIAL_BACKOFF
  current_deadline = now() + INITIAL_BACKOFF
  while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))
         != SUCCESS)
    SleepUntil(current_deadline)
    current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
    current_deadline = now() + current_backoff +
      UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)

上面的演算法裡有這麼幾個關鍵的引數

  1. INITIAL_BACKOFF:第一次重試等待的間隔
  2. MULTIPLIER:每次間隔的指數因子
  3. JITTER:控制隨機的因子
  4. MAX_BACKOFF:等待的最大時長,隨著重試次數的增加,我們不希望第 N 次重試等待的時間變成 30 分鐘這樣不切實際的值
  5. MIN_CONNECT_TIMEOUT:一次成功的請求所需要的時間,因為即使是正常的請求也需要有響應時間,比如 200ms,我們的重試時間間隔顯然要大於這個響應時間才不會出現請求明明已經成功,但卻進行重試的操作。

通過指數的增加每次重試間隔,gRPC 在考慮對端服務和快速故障處理中間找到了一個平衡點。

重試之次數策略

上面的演算法裡面沒有關於次數的限制,gRPC 中的最大重試次數是可配置的,硬限制的最大值為5 次,設定這個硬限制的目的我想主要還是出於對對端服務的保護,避免一些人為的錯誤。

再說下對衝策略

對衝策略

對衝之時間策略

對衝策略裡面,請求是按照如下邏輯發出的:

  1. 第一次正常的請求正常發出
  2. 在等待固定時間間隔後,沒有收到正確的響應,第二個對衝請求會被髮出
  3. 再等待固定時間間隔後,沒有收到任何前面兩個請求的正確響應,第三個會被髮出
  4. 一直重複以上流程直到發出的對衝請求數量達到配置的最大次數
  5. 一旦收到正確響應,所有對衝請求都會被取消,響應會被返回給應用層

對衝之次數策略

次數和上面重試是一樣的限制,都是 5 次。

其它需要注意的問題

  1. 不同的對衝請求應該被對端不同的例項處理
  2. 對衝策略應該只用於冪等的操作,因為不同的對衝的請求通常是由不同的對端例項處理的

5.4 重試失敗

當然不能一直重試,對於重試失敗,gRPC 有以下的策略以顧全大局,對於每個 server,客戶端都可配置一個針對該 server 的限制策略如下:

"retryThrottling": {
  "maxTokens": 10,
  "tokenRatio": 0.1
}

對於每個 server,gRPC 的客戶端都維護了一個 token_count 變數,變數初始值為配置的 maxTokens 值,每次 RPC 請求都會影響這個 token_count 變數值:

  1. 每次失敗的 RPC 請求都會對 token_count 減 1
  2. 每次成功的 RPC 請求都會對 token_count 增加 tokenRation 值

如果 token_count <= (maxTokens / 2),那麼後續發出的請求即使失敗也不會進行重試了,但是正常的請求還是會發出去,直到這個 token_count > (maxTokens / 2) 才又恢復對失敗請求的重試。這種策略可以有效的處理長時間故障。

當然重試失敗還能更進一步,比如 Netflix 出品的hytrix能對故障進行熔斷&降級處理,感興趣的讀者可以進一步瞭解。

總結

本文從問題出發,介紹『重試』這種簡單而又有效的故障處理手段,希望能對大家有所幫助,有任何問題歡迎在評論區留言交流,或掃描二維碼/微信搜尋『技術成長之道』關注公眾號後留言私信。

更多原創文章乾貨分享,請關注公眾號
  • 從 gRPC 的重試策略說起
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章