其實(shí)業(yè)務(wù)被攻擊過一次之后,我就概覽過限流算法一次,當(dāng)時發(fā)現(xiàn)所用的庫主要是利用了Golang現(xiàn)成的標(biāo)準(zhǔn)庫來做的,沒很深入繼續(xù)研究下去。
前幾周回頭看這個問題,發(fā)現(xiàn)這個庫的Readme赫然寫著“以前的版本弄錯啦,根本提供不了之前說的那些功能, 大家趕緊改改吧”。頓時心里一驚,決定這次抽空好好研究一下。
簡單點(diǎn)說,常用的限流算法有兩種:令牌桶(token bucket)和漏桶(leaky bucket)。
漏桶:比較像街邊小攤的叫號,甭管排了多少人,只能隔一小段時間買一次商品。
令牌桶:比較像餐館的叫號,本來餐館有一定的容量, 填滿之前,顧客都可以進(jìn),填滿之后,就跟漏桶差不多了。
一般情況下,令牌桶就夠用了。你可以定義最初有多少個令牌(burst),并且定義令牌增加的頻率(每秒多少個),每次請求來的時候看有沒有令牌可用。Golang庫里一個好的實(shí)現(xiàn)是tollbooth,除了實(shí)現(xiàn)限流算法外,還附帶了很多方便的方法取ip,header域等。另外,要說明一點(diǎn),雖然令牌桶算法是一定時間放一個令牌,但是實(shí)現(xiàn)的時候,并不需要新開一個goroutine去隔一段時間增加計(jì)數(shù)(事實(shí)上,用戶量很小的情況下,完全可以這么做,見官方例子),而是邏輯上計(jì)算時間差對應(yīng)的令牌差額即可,詳細(xì)可見golang.org/x/time/rate的實(shí)現(xiàn),對CPU和Memory非常友好,所以不用擔(dān)心并發(fā)量大了要怎么辦。
不過,上述包里的令牌桶算法有幾個限制:
- 從寫法上看,頻率規(guī)則必須是以秒為單位。其實(shí)他們都會轉(zhuǎn)化成令牌增加速度。例如,1秒10次,那么邏輯上令牌桶里面每0.1秒會增加一個令牌(除非已經(jīng)滿了)。你當(dāng)然可以設(shè)置成1分鐘60次,但最終都會轉(zhuǎn)化成1秒1次。
- 從定義上可以看出,它并不能精確限制每個時間單位的個數(shù)。例如,定義桶里3個令牌, 每秒新增2個令牌, 那么一秒內(nèi)(閉區(qū)間的話,意味著首尾跨越了一個間隔),可能有2(0 + 2,對應(yīng)開始時是空桶)到 5(3 + 2, 對應(yīng)開始時是滿桶)個令牌,設(shè)定值時,需要注意這點(diǎn)。
有點(diǎn)迷糊?不要慌,下面舉個栗子。比如,需要實(shí)現(xiàn)“每分鐘60個請求”,可能令牌桶并不能特別好的勝任。 假設(shè)桶滿是20個(相當(dāng)于一個buffer), 每分鐘新增40, 那么就是60個了。但是,爆發(fā)的最大值(幾乎同一時刻請求)其實(shí)達(dá)不到60, 因?yàn)橥白疃嗑?0。同時,如果桶空了,其后的一分鐘最多只能接受40個請求。
所以, 爆發(fā)值太大,會造成每分鐘的請求限制抖動很大。爆發(fā)值設(shè)置太小,又可能扛不住突然的大量訪問。
這該如何是好?下面要講的東西還有一些,那就下回分解吧。
更新:
重讀文章的時候發(fā)現(xiàn)之前的理解有誤。
對于每分鐘60次這樣的限制,其實(shí)就是設(shè)置每秒1次的速率即可。桶大?。˙urst)只是用來處理突發(fā)的大請求的,即最多一次處理滿桶那么多請求。