當(dāng)網(wǎng)站的訪問量比較大的時候,比如搶購、限時活動,或者有的時候為了防止黑客攻擊,服務(wù)器資源是承載不了的。服務(wù)接口的流量控制策略一般有緩存、降級、限流等。那今天我們來聊下限流這種方式,限流就是控制流量,從而達(dá)到保護(hù)系統(tǒng)的目的。下面向大家展示幾種方法。
1. 計算器法
描述:比如我們限制每個用戶每分鐘最多能訪問頁面100次,如果超過100次,則返回太頻繁,下面使用PHP+Redis偽代碼來實現(xiàn)
<?php
$initMum = 100;
$ip = $_SERVER['REMOTE_ADDR'];
$redis = new Redis();
$key = "rate_limiting:$ip"
$isExist = $redis->exists($key);
if ($isExist) {
$times = $redis->Incr($key);
if ($times > 100) {
echo '訪問頻率超過了限制,請稍后再試';
exit();
}
}
else {
$redis->setex($key,1,60);
}
一般情況下這個就能滿足需求了,但是這樣做小伙伴們可能發(fā)現(xiàn)有一個問題,就是臨界問題。比如某個用戶在鍵過期前10秒內(nèi)訪問了頁面60次,那剛好下一秒這個鍵就過期了,redis會銷毀這個key,然后這個用戶接著在10秒內(nèi)又訪問了該頁面60次,那其實在不到半分鐘內(nèi)這個用戶訪問了該頁面120次,顯然這沒有達(dá)到我們預(yù)期的要求,那接下來再向大家展示一個更為精確的一個方法。
就是我們把用戶每次訪問的時間都保存到一個隊列中,用戶的IP作為這個隊列的鍵,當(dāng)隊列的長度不超過100的時候就會直接往隊列里面添加最新的訪問時間,如果隊列的長度大于100,那么這個時候就會比較這次訪問的時間和上一次訪問的時間,如果超過一分鐘,這把最早訪問的那個時間從隊列中移除,并把新的最新的訪問時間添加到隊列中;如果這次訪問的時間具體上次的訪問時間不超過一分鐘,則返回訪問太頻繁,下面使用偽代碼來實現(xiàn)下:
<?php
$initMum = 100;
$ip = $_SERVER['REMOTE_ADDR'];
$redis = new Redis();
$key = "rate_limiting:$ip"
$times = $redis->llen($key);
if ($times < 100 ) {
$res = $redis->lpush($key,$now);
}
else {
$time = $redis->lindex($key,-1);
if (now() - $time < 60) {
echo '訪問頻率超過了限制,請稍后再試';
exit();
}
else {
$res = $redis->lpush($key,now());
$res1 = $redis->ltrim($key,0,9);
}
}
這個方法雖然能解決我們上面提到的問題,但是它也有它的弊端,比如當(dāng)initMum這個值比較大的時候,這種方法會占用比較大的內(nèi)存,所以實際的應(yīng)用中還得我們自己去權(quán)衡,另外在lpush和ltrim這兩個操作之間也可能會存在競態(tài)的情況,我們可以使用事務(wù)或者lua腳本功能去實現(xiàn),不過如果使用事務(wù)去處理的話,可能還要考慮失敗的情況下重新執(zhí)行事務(wù)。關(guān)于lua腳本這里就不做詳細(xì)的介紹。下面來介紹兩種比較經(jīng)典的限流算法,它們使用一層中間層(桶)來緩沖部分客戶端的請求。
2. 漏桶算法
描述:我們有一個固定容量的桶,有水流進(jìn)來,也有水流出去。流進(jìn)來的水就類似我們的請求,這個量的大小是不確定的;對于流出去的水,這個速率可以設(shè)置一個固定值。當(dāng)桶滿了后,多余的水就會溢出。下面本人從網(wǎng)上找了一些圖來幫助大家理解。

關(guān)于漏桶算法需要注意以下兩點:
1)桶的最大容量怎么設(shè)置?
2)水流出的速度怎么設(shè)置?
先來說下水流出的速度,這個其實需要根據(jù)后臺程序的處理請求的速度來設(shè)置,不過往往我們的機(jī)器上會運(yùn)行多個程序,它們共享計算機(jī)資源,這個時候可能會相互影響,所以程序的處理速度是變化的,那我們可以觀察一段時間內(nèi)程序處理請求的數(shù)量,然后分析得到平均值或者中位數(shù)等數(shù)據(jù)來作為水流出的速度。
桶的最大容量怎么設(shè)置,這個獲取要結(jié)合請求的處理速度、系統(tǒng)一段時間內(nèi)能處理的最大請求數(shù)、客戶端的請求數(shù)、用戶體驗等等來進(jìn)行設(shè)置。比如如果請求的處理速度很慢,但是桶設(shè)置得很大,就會導(dǎo)致很多請求得不到及時的處理。
下面使用偽代碼來實現(xiàn)下:
class LeakyDemo{
$capacity = 200; // 桶的容量,假如是200bit
$rate = 5; //水流出的速度,假如是5bit/秒
$water; //當(dāng)前的水量
$timeStamp =1587355877; //初始時間
//加水操作
public function addWater ($num = 1) {
$this->water = max(0,$this->water - ( time()-$this->timestamp) * $rate );
if ( $this->water + $num < $capacity ) {
$this->water = $this->water + $num;
return true;
}
else {
echo '桶已滿';
return false;
}
}
}
$obj = new LeakyDemo();
$res = $obj->addWater(1);// 請求數(shù)
if ($res) {
echo '加水成功,可以進(jìn)行后續(xù)操作';
}
else {
echo '加水失敗,拒絕請求或者進(jìn)行一些其他的處理';
}
這種限流算法其實是不管當(dāng)前有多少并發(fā)數(shù),通過這個出水速率保證后臺程序接到的請求數(shù)是一定的,從而達(dá)到了限流的目的。由于出水速率是固定的,所以會有個缺點就是對于突發(fā)流量的處理效率并不高,為了解決這個問題下面來介紹下另外一種限流算法。
3. 令牌桶算法
描述:簡單來說就是客戶端要訪問服務(wù)器(后端接口),需要先從令牌桶里面獲得令牌才能訪問,否則拒絕請求或者加入隊列進(jìn)行排隊等等。令牌桶這個桶它的容量是一定的,令牌是以一定的速率加進(jìn)去的,如果桶已經(jīng)滿了就不再繼續(xù)添加。

看到這張圖,相信小伙伴們對令牌桶算法流程會有個清晰的認(rèn)識了吧。
不過如果要深層次的考慮,還需注意以下幾點:
1)生成令牌的速度怎么定?也就是生成令牌的算法
2)令牌桶的大小怎么定比較合適?
如果某段時間內(nèi)請求量很大,并且這些請求全部拿到了token(拿到token的速度是很快的),那他們會同時去請求后臺接口,這個時候服務(wù)器勢必會扛不住,所以為了使系統(tǒng)在突發(fā)流量的時候不至于崩潰,令牌桶的大小會設(shè)置成我們系統(tǒng)能處理的最大并發(fā)數(shù)。當(dāng)然這個最大并發(fā)數(shù)需要我們對系統(tǒng)進(jìn)行相關(guān)的測試等方式獲得。
另外,對于生成令牌的速度可能要結(jié)合系統(tǒng)正常情況下的客戶端請求次數(shù)、系統(tǒng)的一般情況下的并發(fā)數(shù)、限流的大小等因素來考慮。如果小于系統(tǒng)正常情況下處理請求的速度的話,那系統(tǒng)的利用率就沒有達(dá)到最大值,又或者生成令牌的速率比較小,這樣對突發(fā)流量的處理也不是很好,會導(dǎo)致大量的請求拿不到token而被丟棄或者加入排隊進(jìn)行等待,所以設(shè)置一個合理的大小是比較重要的。下面使用偽代碼來實現(xiàn)下:
public function TokenBucketDemo {
$capacity = 200; // 桶的容量,假如是200bit
$rate = 5; //令牌放入速度,假如是5bit/秒
$tokens = 10; //當(dāng)前令牌數(shù)量,初始化為10
$timestamp = 1587355877;// 初始時間
public function getToken($num = 1) {
$now = time();
$this->tokens = min( $this->capacity,$this->tokens+($now- $this->timestamp) * $this->rate );
$this->timestamp = $now;
if ($tokens < 1) {
echo '沒有令牌了,拒絕請求';
return false;
}
else {
echo '還有令牌';
$this->token -= $num;
return true;
}
}
}
這里的代碼只是簡單的展示下這種算法的實現(xiàn)思路,實際不一定是這么寫,比如我們可以使用redis來存儲當(dāng)前的token數(shù)量,然后使用一個定時任務(wù)不斷的往這個里面加入token,當(dāng)有請求過來的時候就會實時的去獲取當(dāng)前的token數(shù),如果請求需要的token數(shù)大于當(dāng)前令牌桶中的令牌數(shù),就會被認(rèn)為超出了當(dāng)前的限制,請求就會被拒絕或者進(jìn)行一些其他的處理。
漏桶算法VS令牌桶算法
1) 漏桶算法進(jìn)水速率是不確定的,但是出水速率是一定的,當(dāng)大量的請求到達(dá)時勢必會有很多請求被丟棄。
2) 令牌桶算法會根據(jù)限流大小,設(shè)置一定的速率往桶中加令牌,這個速率可以很方便的修改,如果我們要提高系統(tǒng)對突發(fā)流量的處理,我們可以適當(dāng)?shù)奶岣呱蓆oken的速率。
關(guān)于限流的方法暫時就寫到這,其實使用過nginx的小伙伴知道,對nginx 進(jìn)行相關(guān)的配置也可以實現(xiàn)限流,又或者是使用第三方的開源工具比如Google的開源工具包Guava提供了限流工具類RateLimiter,該類是基于令牌桶算法來完成限流的。這里就不具體說了。后面會專門寫一篇文章來介紹。
如果有地方需要補(bǔ)充的,歡迎小伙伴們在下面給我留言,看到會及時回復(fù)的。