保障服務(wù)穩(wěn)定之服務(wù)限流

一、前言

對于一個系統(tǒng)而言,最重要的要求之一肯定就是服務(wù)的穩(wěn)定性了,一個不穩(wěn)定的系統(tǒng)可能給企業(yè)帶來巨大的損失,包括經(jīng)濟(jì)和品牌等等方面的損失。

我們都希望系統(tǒng)能穩(wěn)定可靠地對外提供服務(wù),響應(yīng)快,不宕機,不故障,但是在實際情況中,常常會遇到一些異常的情況,這就考驗我們系統(tǒng)的穩(wěn)定性了。

今天就來講講保障服務(wù)穩(wěn)定性的手段之一的服務(wù)限流。

二、解決的問題

我們系統(tǒng)運行過程中有時候可能會遇到突發(fā)異常大流量,如果系統(tǒng)無法正確處理這些突然涌入大量請求,就會導(dǎo)致系統(tǒng)輕則響應(yīng)慢,經(jīng)常超時,重則導(dǎo)致整個系統(tǒng)宕機,因此這就要求我們系統(tǒng)能以一定的策略處理大流量的涌入,這樣才不對被突發(fā)大流量壓垮,導(dǎo)致完全無法對外提供服務(wù)。

注意,這里大流量說的是突發(fā)異常大流量,是非正常情況,所以我們的處理策略是對部分請求進(jìn)行丟棄或者排隊處理,保證我們系統(tǒng)對外還是可用的,而非全部請求都需要處理完。而對于系統(tǒng)正常的普通流量來說,如果其請求量逐漸達(dá)到了我們系統(tǒng)的負(fù)載能力的上限的話,這時候需要進(jìn)行的就是服務(wù)的擴容,而不是限流并丟棄請求了。

我們系統(tǒng)可能遇到的突發(fā)大流量的場景有很多,但統(tǒng)一的表現(xiàn)都是,某些接口請求量激增,導(dǎo)致超過了系統(tǒng)的處理能力了,例如:

  • 突發(fā)熱點事件(例如微博熱搜)
  • 爬蟲
  • 惡意攻擊
  • 惡意刷單(如12306)
    ···

面對突發(fā)大流量,我們系統(tǒng)能使用的手段之一就是服務(wù)限流了。限流是通過對一個時間段處理內(nèi)的請求量進(jìn)行限制來保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以丟棄請求,從而控制了系統(tǒng)處理的請求量不會超過其處理能力。

限流可能在整個網(wǎng)絡(luò)請求過程的各個層面發(fā)生,例如nginx,業(yè)務(wù)代碼層等,這里主要介紹的是限流的思想,也就是限流算法,并給出業(yè)務(wù)層的代碼實現(xiàn)例子。

三、限流算法

1、計數(shù)器算法

計數(shù)器限流算法是比較簡單粗暴的算法,主要通過一個或者多個計數(shù)器來統(tǒng)計一段時間內(nèi)的請求總量,然后判斷是否超過限制,超過則進(jìn)行限流,不超過則對應(yīng)的計數(shù)器數(shù)量加1。

計數(shù)器限流算法又可以分為固定窗口和滑動窗口兩種。

固定窗口

固定窗口計數(shù)器限流算法是統(tǒng)計固定一個時間窗口內(nèi)的請求總數(shù)來判斷是否進(jìn)行限流,例如限制每分鐘請求總數(shù)為100,則可以通過一個計數(shù)器來統(tǒng)計當(dāng)前分鐘的請求總數(shù),每來一個請求判斷當(dāng)前分鐘對應(yīng)的計數(shù)器的數(shù)量,沒有超過限制則在當(dāng)前分鐘對應(yīng)的計數(shù)器加1,超過則拒絕請求。

PHP實現(xiàn)邏輯如下:

/**
 * 固定窗口計數(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ā)的問題,代碼實現(xiàn)上要先加再判斷
    if ($current > $limit) {
        return false;
    }
    return true;
}

缺點

固定窗口計數(shù)器限流算法實現(xiàn)起來雖然很簡單,但是有一個十分致命的問題,那就是臨界突刺問題:最后一秒和最開始1秒的流量集中一起,會出現(xiàn)大量流量。

計數(shù)器的限制數(shù)量判斷是按時間段的,在兩個時間段的交界時間點,限制數(shù)量的當(dāng)前值會發(fā)生一個抖動的變化,從而使瞬間流量突破了我們期望的限制。例如以下的情況:


可以看到在0:59的時候,如果突然來了100個請求,這時候當(dāng)前值是100,而到了1:00的時候,因為是下一個時間段了,當(dāng)前值陡降到0,這時候又進(jìn)來100個請求,都能通過限流判斷,雖然兩個時間段平均下來還是沒超過限制,但是在臨界時間點的請求量卻達(dá)到了兩倍之多,這種情況下就可能壓垮我們的系統(tǒng)。

滑動窗口

上面會出現(xiàn)突刺的問題其實就在于固定窗口算法的窗口時間跨度太大,且是固定不變的,為了解決突刺的問題,我們就有了滑動窗口計數(shù)器限流算法。

滑動窗口算法是固定窗口算法的優(yōu)化版,主要有兩個特點:

  • 劃分多個小的時間段,各時間段各自進(jìn)行計數(shù)。
  • 根據(jù)當(dāng)前時間,動態(tài)往前滑動來計算時間窗口范圍,合并計算總數(shù)。


可以看到,每次時間往后,窗口也會動態(tài)往后滑動,從而丟棄一些更早期的計數(shù)數(shù)據(jù),從而實現(xiàn)總體計數(shù)的平穩(wěn)過度。當(dāng)滑動窗口劃分的格子越多,那么滑動窗口的滑動就越平滑,限流的統(tǒng)計就會越精確。事實上,固定窗口算法就是只劃分成一個格子的滑動窗口算法。

PHP實現(xiàn)邏輯如下:

/**
 * 滑動窗口計數(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)前時間所在小分片計數(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;
        //計算時間窗口內(nèi)的總數(shù)
        $current+=intval($redis->get($tmp_key));
        if ($current > $limit) {
            //超過限制的話要回滾本次加的次數(shù)
            $redis->decr($current_key);
            return false;
        }
    }
    
    return true;
}

缺點

滑動窗口限流算法雖然可以保證任意時間窗口內(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。

可以使用隊列來實現(xiàn),隊列設(shè)置最大容量,訪問請求先進(jìn)入隊列,隊列滿了的話就丟棄丟棄后續(xù)請求,然后通過另外一個woker以固定速率從隊列出口拿請求去處理。具體實現(xiàn)邏輯不展示了。

缺點
漏桶算法的缺陷很明顯,由于出口的處理速率是固定的,當(dāng)短時間內(nèi)有大量的突發(fā)請求時,即便此時服務(wù)器沒有任何負(fù)載,每個請求也都得在隊列中等待一段時間才能被響應(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實現(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ā)情況了
    //計算從上次到現(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));//計算當(dāng)前令牌數(shù)
    
    if($current>0){
        $redis->hMSet($current_key,[
            'num'=>$current-1,
            'time'=>time()
        ]);
        return true;
    }
    
    return false;
}

上面的實現(xiàn)方案是令牌按時間回復(fù)數(shù)量,事實上令牌的生成也可以通過另外的服務(wù)去生成,這樣可以按一定策略去調(diào)控令牌的生成速率。

版權(quán)聲明

轉(zhuǎn)載請注明作者和文章出處
作者: X先生
http://www.itdecent.cn/p/b91dc5fd5d05

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容