無論是在我們日常的軟件使用中還是軟件開發中,我們總是會遇到速率限制的問題,例如短信驗證碼限制一小時最多只能發送5次,這是日常生活的情況;在工作中,我們可能會限制說 DB 的操作不能超過 100 qps,這也是一種限制操作,那么對於這些限制速率的行為,有沒有什么好一點的實踐或者理論,最近我就看了一些,但是理解可能並不是很深刻,但不妨寫出來和大家交流一番。
常用的限流策略
在看了不少的實踐文章之后,我發現有主要講得都是兩種方法,而且都是從網絡限流中遷移過來的,分別是:Leaky bucket 和 Token bucket,這兩種方法可能乍看之下差不多,而且有不少文章並沒有明確得指出他們的區別,所以很容易混淆誤導;但這不是唯一的原因,還有個原因就是在某些條件下,他們其實達到的效果和實現都是一致的,所以不免讓人混淆。
下面我就以我個人的理解分別介紹講解一下這兩種策略,同時,針對 Token bucket 我將會使用 Python 編程語言寫一個簡單的實現,方便大家有一個更清晰的認識。
Leaky bucket
Leaky bucket 最初也是用在網絡方面,用於計算機網絡和通信網絡中包交換的速率限制,后面也就遷移到其他領域了。Leaky bucket 的理論有兩種,分別稱為基於 meter 的和 基於 queue 的,他們實現的具體思路不同,而且當你真正實現的時候,會發現有很大的區別,下面我也分別介紹這兩種。
基於 meter 的 Leaky bucket
這種基於 meter 的 Leaky bucket 相對來說比較簡單,其實它就有一個計數器,然后有消息要發送的時候,就看計數器夠不夠,如果計數器沒有滿的話,那么這個消息就可以被處理,如果計數器不足以發送消息的話,那么這個消息將會被丟棄。
那么這個計數器是怎么來的呢,基於 meter 的形式的計數器就是發送的頻率,例如你設置得頻率是不超過 5條/s ,那么計數器就是 5,在一秒內你每發送一條消息就減少一個,當你發第 6 條的時候計時器就不夠了,那么這條消息就被丟棄了。
從這里可以看出基於 meter 的 Leaky bucket 的特點就是肯定不超過指定速率,而且可以一定程度保持原始消息的發送信息,但是不能很好得應對突發的短期流量。
基於 queue 的 Leaky bucket
另外一種基於 queue 的 Leaky bucket 實現起來比較復雜,但是原理卻比較簡單,和 meter 差不多,也是存在一個 counter,這個 counter 卻不表示速率限制,而是表示 bucket 的大小,這里就是當有消息要發送的時候看 bucket 中是否還有位置,如果有,那么就將消息放進 queue 中,注意,這里就是不一樣的地方,這里只是將消息放進 Leaky bucket 維護的一個 queue 的,這個 queue 以 FIFO 的形式提供服務;如果 bucket 沒有位置了,那么同樣得,消息將被拋棄。
在消息被放進 queue 之后,Leaky bucket 還維護了一個定時器,這個定時器的周期就是我們設置的頻率周期,例如我們設置得頻率是 5條/s,那么定時器的周期就是 200ms,定時器每 200ms 去 queue 里獲取一次消息,如果有消息,那么就發送出去,如果沒有,那么輪空了。
從上面的描述中,可以看出,對於 基於 queue 的 Leaky bucket 來說,它可以保證的是任務之間的執行間隔嚴格按照我們設置得頻率,不會超頻,但是也正是因為如此,完全失去了任務進來的相關信息,而且對於突發流量也完全無法應對,無論流量多或少,都是固定的頻率。
Token bucket
Token bucket,我們中文習慣性稱之為 令牌桶,它的特點就是有一個 bucket,然后在 bucket 中存放了一定數額的 token,每當你要發送消息的時候,需要從 bucket 中獲取一個 token,只要獲取成功,那么你就可以發送,否則,那么將被放棄。
既然 token 會被消耗,那么肯定有補充的方式,是的,Token bucket 的 token 補充方式就是以設定的頻率往bucket 里放置 token,而 bucket 是有大小的,如果要放置 token 的時候 bucket 滿了,那么 token 將被拋棄,否則 bucket 中的 token 數量增加。
這里就是問題的有趣之處,和基於 queue 的 Leaky bucket 的一點區別就在於 bucket 是有容量的,也就是說假設我們設置的頻率是 5條/s,但是我將 bucket 的 size 設置為 10,那么也就是說當 bucket 被放滿的時候,同一時間我可以提供的 token 數量是 10 個,這意味着我可以臨時支持 10條/s 的速率,這就是 token bucket 比較有意思的一點。
Token bucket 可以也能在一定程度上保持流量的來源特征,同時也支持一定的突發流量,但是,從另外一方面也可能導致超頻,當然這依賴於你的選擇,可以不要。
一個 Python 實現
根據上面的 Token bucket 的描述,我就以 Python 編程語言為例,給出一個

這個例子比較簡單, Token bucket 有兩個因子,分別是:頻率 和 bucket大小,這里我們就將他們定義為 fill_rate 和 tokens,然后當我們需要發送消息的時候,就調用 consume 申請指定數量的 token,如果我們發現 token 是夠的,那么 ok 沒問題,不夠的話就沒辦法了。
這里的判斷 token 數量的邏輯還是不錯的,值得同學們稍作思考一番。下面是一個使用的例子:

總結
最后,總結一下兩種不同的方式的不同點和優缺點:
Leaky bucket 的優點就是可以保證速率肯定不超過我們設定的速率,最多也就相等;同時,實現可以很簡單(meter),也可以很復雜(queue),不過更多情況下我們是認為它不能保持原始流量的特征的。在流量方面, Leaky bucket 不能應對突發流量,但是,反過來想,這個可以用來防御惡意流量,不過這個 bucket 的大小選擇是個難題。
Token bucket 的一個比較大的優點就是可以應對突發流量,同時如果我們希望也可以轉化為 meter leaky bucket,但是缺點也是比較明顯,就是可能會產生突發性的流量,例如一個小時的流量都在第一秒耗完了,當然,這有個比較麻煩得解決方案:滑動窗口,這里沒有提到。
