本文同步發表於 Prodesire 公衆號,和 Prodesire 博客。
最近作雲服務 API 測試項目的過程當中,發現某些時候會大批量調用 API,從而致使限流的報錯。在遇到這種報錯時,傳統的重試策略是每隔一段時間重試一次。但因爲是固定的時間重試一次,重試時又會有大量的請求在同一時刻涌入,會不斷地形成限流。html
這讓我回想起兩年前在查閱Celery Task 文檔的時候發現能夠爲任務設置 retry_backoff
的經歷,它讓任務在失敗時以 指數退避
的方式進行重試。那麼指數退避到底是什麼樣的呢?python
根據 wiki 上對 Exponential backoff 的說明,指數退避是一種經過反饋,成倍地下降某個過程的速率,以逐漸找到合適速率的算法。git
在以太網中,該算法一般用於衝突後的調度重傳。根據時隙和重傳嘗試次數來決定延遲重傳。github
在 c
次碰撞後(好比請求失敗),會選擇 0 和 $2^c-1$ 之間的隨機值做爲時隙的數量。算法
隨着重傳次數的增長,延遲的程度也會指數增加。編程
說的通俗點,每次重試的時間間隔都是上一次的兩倍。segmentfault
考慮到退避時間的均勻分佈,退避時間的數學指望是全部可能性的平均值。也就是說,在 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
來看下 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 。
在 《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 分多鐘,以後便再也不重試。
回到開頭的問題,在遇到限流錯誤的時候,經過指數退避算法進行重試,咱們能夠最大程度地避免再次限流。相比於固定時間重試,指數退避加入了時間放大性和隨機性,從而變得更加「智能」。至此,咱們不再用擔憂限流讓整個測試程序運行中斷了~