一钱贯、前言
對于一個系統(tǒng)而言,最重要的要求之一肯定就是服務(wù)的穩(wěn)定性了侦另,一個不穩(wěn)定的系統(tǒng)可能給企業(yè)帶來巨大的損失秩命,包括經(jīng)濟(jì)和品牌等等方面的損失。
我們都希望系統(tǒng)能穩(wěn)定可靠地對外提供服務(wù)褒傅,響應(yīng)快弃锐,不宕機(jī),不故障殿托,但是在實(shí)際情況中霹菊,常常會遇到一些異常的情況,這就考驗(yàn)我們系統(tǒng)的穩(wěn)定性了碌尔。
今天就來講講保障服務(wù)穩(wěn)定性的手段之一的服務(wù)限流浇辜。
二、解決的問題
我們系統(tǒng)運(yùn)行過程中有時候可能會遇到突發(fā)異常大流量唾戚,如果系統(tǒng)無法正確處理這些突然涌入大量請求柳洋,就會導(dǎo)致系統(tǒng)輕則響應(yīng)慢,經(jīng)常超時叹坦,重則導(dǎo)致整個系統(tǒng)宕機(jī)熊镣,因此這就要求我們系統(tǒng)能以一定的策略處理大流量的涌入,這樣才不對被突發(fā)大流量壓垮,導(dǎo)致完全無法對外提供服務(wù)绪囱。
注意测蹲,這里大流量說的是突發(fā)異常大流量,是非正常情況鬼吵,所以我們的處理策略是對部分請求進(jìn)行丟棄或者排隊(duì)處理扣甲,保證我們系統(tǒng)對外還是可用的,而非全部請求都需要處理完齿椅。而對于系統(tǒng)正常的普通流量來說琉挖,如果其請求量逐漸達(dá)到了我們系統(tǒng)的負(fù)載能力的上限的話,這時候需要進(jìn)行的就是服務(wù)的擴(kuò)容涣脚,而不是限流并丟棄請求了示辈。
我們系統(tǒng)可能遇到的突發(fā)大流量的場景有很多,但統(tǒng)一的表現(xiàn)都是遣蚀,某些接口請求量激增矾麻,導(dǎo)致超過了系統(tǒng)的處理能力了,例如:
- 突發(fā)熱點(diǎn)事件(例如微博熱搜)
- 爬蟲
- 惡意攻擊
- 惡意刷單(如12306)
···
面對突發(fā)大流量芭梯,我們系統(tǒng)能使用的手段之一就是服務(wù)限流了险耀。限流是通過對一個時間段處理內(nèi)的請求量進(jìn)行限制來保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以丟棄請求粥帚,從而控制了系統(tǒng)處理的請求量不會超過其處理能力胰耗。
限流可能在整個網(wǎng)絡(luò)請求過程的各個層面發(fā)生,例如nginx芒涡,業(yè)務(wù)代碼層等柴灯,這里主要介紹的是限流的思想,也就是限流算法费尽,并給出業(yè)務(wù)層的代碼實(shí)現(xiàn)例子赠群。
三、限流算法
1旱幼、計(jì)數(shù)器算法
計(jì)數(shù)器限流算法是比較簡單粗暴的算法查描,主要通過一個或者多個計(jì)數(shù)器來統(tǒng)計(jì)一段時間內(nèi)的請求總量,然后判斷是否超過限制柏卤,超過則進(jìn)行限流冬三,不超過則對應(yīng)的計(jì)數(shù)器數(shù)量加1。
計(jì)數(shù)器限流算法又可以分為固定窗口和滑動窗口兩種缘缚。
固定窗口
固定窗口計(jì)數(shù)器限流算法是統(tǒng)計(jì)固定一個時間窗口內(nèi)的請求總數(shù)來判斷是否進(jìn)行限流勾笆,例如限制每分鐘請求總數(shù)為100,則可以通過一個計(jì)數(shù)器來統(tǒng)計(jì)當(dāng)前分鐘的請求總數(shù)桥滨,每來一個請求判斷當(dāng)前分鐘對應(yīng)的計(jì)數(shù)器的數(shù)量窝爪,沒有超過限制則在當(dāng)前分鐘對應(yīng)的計(jì)數(shù)器加1弛车,超過則拒絕請求。
PHP實(shí)現(xiàn)邏輯如下:
/**
* 固定窗口計(jì)數(shù)器限流算法
* @param $key string 限流依據(jù)蒲每,例如uid纷跛,url等
* @param $time int 限流時間段,單位秒
* @param $limit int 限流總數(shù)
*/
function limit($key, $time, $limit) {
//當(dāng)前時間所在分片
$current_segment=floor(time() / $time);
//按當(dāng)前時間和限流參數(shù)生成key
$current_key = $key . '_' . $time . '_' . $limit . '_' . $current_segment;
$redis = new Redis();
//key不存在才設(shè)置邀杏,且設(shè)置過期時間
$redis->set($current_key, 0, ['nx', 'ex' => $time]);
$current = $redis->incr($current_key);
//為了解決請求并發(fā)的問題贫奠,代碼實(shí)現(xiàn)上要先加再判斷
if ($current > $limit) {
return false;
}
return true;
}
缺點(diǎn)
固定窗口計(jì)數(shù)器限流算法實(shí)現(xiàn)起來雖然很簡單,但是有一個十分致命的問題淮阐,那就是臨界突刺問題:最后一秒和最開始1秒的流量集中一起叮阅,會出現(xiàn)大量流量刁品。
計(jì)數(shù)器的限制數(shù)量判斷是按時間段的泣特,在兩個時間段的交界時間點(diǎn),限制數(shù)量的當(dāng)前值會發(fā)生一個抖動的變化挑随,從而使瞬間流量突破了我們期望的限制状您。例如以下的情況:
可以看到在0:59的時候,如果突然來了100個請求兜挨,這時候當(dāng)前值是100膏孟,而到了1:00的時候,因?yàn)槭窍乱粋€時間段了拌汇,當(dāng)前值陡降到0柒桑,這時候又進(jìn)來100個請求,都能通過限流判斷噪舀,雖然兩個時間段平均下來還是沒超過限制魁淳,但是在臨界時間點(diǎn)的請求量卻達(dá)到了兩倍之多,這種情況下就可能壓垮我們的系統(tǒng)与倡。
滑動窗口
上面會出現(xiàn)突刺的問題其實(shí)就在于固定窗口算法的窗口時間跨度太大界逛,且是固定不變的,為了解決突刺的問題纺座,我們就有了滑動窗口計(jì)數(shù)器限流算法息拜。
滑動窗口算法是固定窗口算法的優(yōu)化版,主要有兩個特點(diǎn):
- 劃分多個小的時間段净响,各時間段各自進(jìn)行計(jì)數(shù)少欺。
-
根據(jù)當(dāng)前時間,動態(tài)往前滑動來計(jì)算時間窗口范圍馋贤,合并計(jì)算總數(shù)赞别。
可以看到,每次時間往后掸掸,窗口也會動態(tài)往后滑動氯庆,從而丟棄一些更早期的計(jì)數(shù)數(shù)據(jù)蹭秋,從而實(shí)現(xiàn)總體計(jì)數(shù)的平穩(wěn)過度。當(dāng)滑動窗口劃分的格子越多堤撵,那么滑動窗口的滑動就越平滑仁讨,限流的統(tǒng)計(jì)就會越精確。事實(shí)上实昨,固定窗口算法就是只劃分成一個格子的滑動窗口算法洞豁。
PHP實(shí)現(xiàn)邏輯如下:
/**
* 滑動窗口計(jì)數(shù)器限流算法
* @param $key string 限流依據(jù),例如uid荒给,url等
* @param $time int 限流時間段丈挟,單位秒
* @param $limit int 限流總數(shù)
* @param $segments_num int 分段個數(shù)
*/
function limit($key, $time, $limit, $segments_num) {
//小分片時間長度
$segments_time=floor($time/$segments_num);
//當(dāng)前時間所在小分片
$current_segment=floor(time() / $segments_time);
//按限流時間段生成key
$current_key = $key . '_' . $time . '_' . $limit . '_' . $current_segment;
$redis = new Redis();
//先更新當(dāng)前時間所在小分片計(jì)數(shù),key不存在才設(shè)置志电,且設(shè)置過期時間
$redis->set($current_key, 0, ['nx', 'ex' => $time]);
$current = $redis->incr($current_key);
for($window=$segments_time;$window<$time;$window+=$segments_time){
$current_segment=$current_segment-1;
$tmp_key = $key . '_' . $time . '_' . $limit . '_' . $current_segment;
//計(jì)算時間窗口內(nèi)的總數(shù)
$current+=intval($redis->get($tmp_key));
if ($current > $limit) {
//超過限制的話要回滾本次加的次數(shù)
$redis->decr($current_key);
return false;
}
}
return true;
}
缺點(diǎn)
滑動窗口限流算法雖然可以保證任意時間窗口內(nèi)接口請求次數(shù)都不會超過最大限流值曙咽,但是相對來說對系統(tǒng)的瞬時處理能力還是沒有考慮到,無法防止在更細(xì)的時間粒度上訪問過于集中的問題挑辆,例如在同一時刻(同一秒)大量請求涌入例朱,還是可能會超過系統(tǒng)負(fù)荷能力。
2鱼蝉、漏桶算法
漏桶算法就是一種從系統(tǒng)的處理能力出發(fā)進(jìn)行限流的算法洒嗤。類似生活用到的漏斗,上面進(jìn)水魁亦,下面有個小口出水渔隶,當(dāng)請求進(jìn)來時,相當(dāng)于水倒入漏斗洁奈,然后從下端小口慢慢勻速的流出间唉。不管上面流量多大,下面流出的速度始終保持不變睬魂,超過漏斗容量的則丟棄终吼。漏桶算法以固定的速率釋放訪問請求(即請求通過),直到漏桶為空氯哮。
漏桶算法有兩個關(guān)鍵數(shù)據(jù):桶的容量V和流出的速率R际跪。假設(shè)每個請求的平均處理時間是S,最大超時時間是SS喉钢,則V/R+S<=SS姆打。
可以使用隊(duì)列來實(shí)現(xiàn),隊(duì)列設(shè)置最大容量肠虽,訪問請求先進(jìn)入隊(duì)列幔戏,隊(duì)列滿了的話就丟棄丟棄后續(xù)請求,然后通過另外一個woker以固定速率從隊(duì)列出口拿請求去處理税课。具體實(shí)現(xiàn)邏輯不展示了闲延。
缺點(diǎn)
漏桶算法的缺陷很明顯痊剖,由于出口的處理速率是固定的,當(dāng)短時間內(nèi)有大量的突發(fā)請求時垒玲,即便此時服務(wù)器沒有任何負(fù)載陆馁,每個請求也都得在隊(duì)列中等待一段時間才能被響應(yīng),因此漏桶算法無法應(yīng)對突發(fā)流量合愈。
3叮贩、令牌桶算法
令牌桶算法也是有一個桶,但是它不是通過限制漏出的請求來控制流量佛析,而是通過控制桶的令牌的生成數(shù)量來達(dá)到限流的目的的益老。令牌桶定時往桶里面丟一定的令牌,令牌桶滿了就不再往里面加令牌寸莫。每來一個請求就要先在桶里拿一個令牌捺萌,拿到令牌則通過,拿不到則拒絕储狭。
當(dāng)訪問量小于令牌桶的生成速率時互婿,令牌桶可以慢慢積累令牌直到桶滿,這樣當(dāng)短時間的突發(fā)訪問量來時辽狈,其積累的令牌數(shù)保證了大量請求都能立刻拿到令牌進(jìn)行后續(xù)處理。當(dāng)訪問量持續(xù)大量流入時呛牲,積累的令牌被消耗完了之后刮萌,后續(xù)請求又依賴于一定速率產(chǎn)生的新令牌,這時候就變成類似漏桶算法那樣的固定流量限制娘扩。
由此可見着茸,相比于漏桶算法,令牌桶算法能夠在限制調(diào)用的平均速率的同時還允許一定程度的突發(fā)調(diào)用琐旁。
PHP實(shí)現(xiàn)邏輯如下:
/**
* 令牌桶限流算法
* @param $key string 限流依據(jù)涮阔,例如uid,url等
* @param $rate float 令牌生成速率灰殴,每秒$rate個
* @param $volume int 容量
* @return bool
*/
function limit($key, $rate, $volume) {
//按限流參數(shù)生成key
$current_key = $key . '_' . $rate . '_' . $volume;
$redis = new Redis();
$time=time();
//沒有則初始化
$redis->hSetNx($current_key, 'num', $volume);
$redis->hSetNx($current_key, 'time', $time);
//以下邏輯在高并發(fā)情況下要用lua腳本或者加分布式鎖敬特,這里僅用于說明算法的邏輯,就不考慮并發(fā)情況了
//計(jì)算從上次到現(xiàn)在牺陶,需要添加的令牌數(shù)量
$last=$redis->hMGet($current_key,['num','time']);
$last_time=$last['time'];
$last_num=$last['num'];
$incr=($time-$last_time)*$rate;
$current=min($volume,($last_num+$incr));//計(jì)算當(dāng)前令牌數(shù)
if($current>0){
$redis->hMSet($current_key,[
'num'=>$current-1,
'time'=>time()
]);
return true;
}
return false;
}
上面的實(shí)現(xiàn)方案是令牌按時間回復(fù)數(shù)量伟阔,事實(shí)上令牌的生成也可以通過另外的服務(wù)去生成,這樣可以按一定策略去調(diào)控令牌的生成速率掰伸。
版權(quán)聲明
轉(zhuǎn)載請注明作者和文章出處
作者: X先生
http://www.reibang.com/p/b91dc5fd5d05