限流後,你能夠經過指數退避來重試

1、背景

本文同步發表於 Prodesire 公衆號,和 Prodesire 博客
最近作雲服務 API 測試項目的過程當中,發現某些時候會大批量調用 API,從而致使限流的報錯。在遇到這種報錯時,傳統的重試策略是每隔一段時間重試一次。但因爲是固定的時間重試一次,重試時又會有大量的請求在同一時刻涌入,會不斷地形成限流。html

這讓我回想起兩年前在查閱Celery Task 文檔的時候發現能夠爲任務設置 retry_backoff 的經歷,它讓任務在失敗時以 指數退避 的方式進行重試。那麼指數退避到底是什麼樣的呢?python

2、指數退避

根據 wiki 上對 Exponential backoff 的說明,指數退避是一種經過反饋,成倍地下降某個過程的速率,以逐漸找到合適速率的算法。git

在以太網中,該算法一般用於衝突後的調度重傳。根據時隙和重傳嘗試次數來決定延遲重傳。github

c 次碰撞後(好比請求失敗),會選擇 0 和 $2^c-1$ 之間的隨機值做爲時隙的數量。算法

  • 對於第 1 次碰撞來講,每一個發送者將會等待 0 或 1 個時隙進行發送。
  • 而在第 2 次碰撞後,發送者將會等待 0 到 3( 由 $2^2-1$ 計算獲得)個時隙進行發送。
  • 而在第 3 次碰撞後,發送者將會等待 0 到 7( 由 $2^3-1$ 計算獲得)個時隙進行發送。
  • 以此類推……

隨着重傳次數的增長,延遲的程度也會指數增加。編程

說的通俗點,每次重試的時間間隔都是上一次的兩倍。segmentfault

3、指數退避的指望值

考慮到退避時間的均勻分佈,退避時間的數學指望是全部可能性的平均值。也就是說,在 c 次衝突以後,退避時隙數量在 [0,1,...,N] 中,其中 $N=2^c-1$ ,則退避時間的數學指望(以時隙爲單位)是dom

$$E(c)=\frac{1}{N+1}\sum_{i=0}^{N}{i}=\frac{1}{N+1}\frac{N(N+1)}{2}=\frac{N}{2}=\frac{2^c-1}{2}$$socket

那麼對於前面講到的例子來講:ide

  • 第 1 次碰撞後,退避時間指望爲 $E(1)=\frac{2^1-1}{2}=0.5$
  • 第 2 次碰撞後,退避時間指望爲 $E(2)=\frac{2^2-1}{2}=1.5$
  • 第 3 次碰撞後,退避時間指望爲 $E(3)=\frac{2^3-1}{2}=3.5$

4、指數退避的應用

4.1 Celery 中的指數退避算法

來看下 celery/utils/time.py 中獲取指數退避時間的函數:

def get_exponential_backoff_interval(
    factor,
    retries,
    maximum,
    full_jitter=False
):
    """Calculate the exponential backoff wait time."""
    # Will be zero if factor equals 0
    countdown = factor * (2 ** retries)
    # Full jitter according to
    # https://www.awsarchitectureblog.com/2015/03/backoff.html
    if full_jitter:
        countdown = random.randrange(countdown + 1)
    # Adjust according to maximum wait time and account for negative values.
    return max(0, min(maximum, countdown))

這裏 factor 是退避係數,做用於總體的退避時間。而 retries 則對應於上文的 c(也就是碰撞次數)。核心內容 countdown = factor * (2 ** retries) 和上文提到的指數退避算法思路一致。
在此基礎上,能夠將 full_jitter 設置爲 True,含義是對退避時間作一個「抖動」,以具備必定的隨機性。最後呢,則是限定給定值不能超過最大值 maximum,以免無限長的等待時間。不過一旦取最大的退避時間,也就可能致使多個任務同時再次執行。更多見 Task.retry_jitter

4.2 《UNIX 環境高級編程》中的鏈接示例

在 《UNIX 環境高級編程》(第 3 版)的 16.4 章節中,也有一個使用指數退避來創建鏈接的示例:

#include "apue.h"
#include <sys/socket.h>

#define MAXSLEEP 128

int connect_retry(int domain, int type, int protocol,
                  const struct sockaddr *addr, socklen_t alen)
{
    int numsec, fd;

    /*
    * 使用指數退避嘗試鏈接
    */
    for (numsec = 1; numsec < MAXSLEEP; numsec <<= 1)
    {
        if (fd = socket(domain, type, protocol) < 0)
            return (-1);
        if (connect(fd, addr, alen) == 0)
        {
            /*
            * 鏈接接受
            */
            return (fd);
        }
        close(fd);

        /*
        * 延遲後重試
        */
        if (numsec <= MAXSLEEP / 2)
            sleep(numsec);
    }
    return (-1);
}

若是鏈接失敗,進程會休眠一小段時間(numsec),而後進入下次循環再次嘗試。每次循環休眠時間是上一次的 2 倍,直到最大延遲 1 分多鐘,以後便再也不重試。

總結

回到開頭的問題,在遇到限流錯誤的時候,經過指數退避算法進行重試,咱們能夠最大程度地避免再次限流。相比於固定時間重試,指數退避加入了時間放大性和隨機性,從而變得更加「智能」。至此,咱們不再用擔憂限流讓整個測試程序運行中斷了~

相關文章
相關標籤/搜索