高併發系統解決之限流

問題描述

當系統面對高併發情況時,某些接口達到處理請求瓶頸從來出現拒絕訪問,並引發連鎖反應導致整個系統崩潰。面對這種特殊情況如何解決?

我們生活中也有類似情景,比如老式電閘都安裝了保險絲,一旦有人使用超大功率的設備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理我們的接口也需要安裝上“保險絲”,以防止非預期的請求對系統壓力過大而引起的系統癱瘓,當流量過大時,可以採取拒絕或者引流等機制。

方案方案

在開發高併發系統時,有三把利器用來保護系統:緩存、降級和限流。

這裏介紹限流。

限流算法

常用的限流算法有兩種:漏桶算法和令牌桶算法。

漏桶算法

漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以一定的速度出水,當水流入速度過大會直接溢出(等待、丟棄),可以看出漏桶算法能強行限制數據的傳輸速率。漏桶的剩餘空間就代表着當前行爲可以持續進行的數量,漏嘴的流水速率代表着系統允許該行爲的最大頻率。如下圖:
在這裏插入圖片描述

PHP 實現
<?php

/**
 * 漏捅算法
 * Class LeakyBucket
 */
class LeakyBucket
{
    private $timeStamp;      //當前時間戳
    private $capacity = 10;  // 桶的容量
    private $rate = 2;       // 水漏出的速度
    private $water;          // 當前水量(當前累積請求數)

    public function __construct()
    {
        $this->timeStamp = time();
    }

    /**
     * 桶是否滿了
     * @return bool
     */
    public  function grant(){
        $now = time();
        $this->water = max(0,$this->water - ($now - $this->timeStamp) * $this->rate);// 先執行漏水,計算剩餘水量
        $this->timeStamp = $now;
        if(($this->water + 1) < $this->capacity){
            // 嘗試加水,並且水還未滿
            $this->water += 1;
            return true;
        }else{
            // 水滿,拒絕加水
            return false;
        }
    }
}

$leakyBucket = new LeakyBucket();
for($i = 0; $i < 20; $i ++) {
    $res = $leakyBucket->grant();

    var_dump($res);echo "<br>";

    //每5秒停1秒,模擬單位時間的請求數
    if($i % 5 == 0){
        sleep(1);
    }
}

//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(false)
//bool(false)
//bool(true)
//bool(true)
//bool(false)
//bool(false)

其他實現
在Redis中,Redis 4.0 版本提供了一個限流 Redis 模塊,叫 redis-cell。該模塊也使用了漏斗算法,並提供了原子的限流指令。

應用
限制接口、會員、IP單位時間內的訪問。

令牌桶算法

對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。如下圖所示,令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。
在這裏插入圖片描述

PHP + Redis 實現

此算法是對接口訪問次數的限制,還有一種是對傳輸字節數的限制,實現都大同小異,把接口訪問次數改成傳輸的字節大小即可。

<?php

/**
 * PHP基於Redis實現令牌桶算法
 * Class TokenBucket
 */
class TokenBucket{

    // redis連接配置
    private $_config = array(
        'host' => 'localhost',
        'port' => 6379,
        'index' => 0,
        'auth' => '',
        'timeout' => 1,
        'reserved' => NULL,
        'retry_interval' => 100,
    );
    private $_redis;  // redis對象
    private $_queue;  // 令牌桶
    private $_max;    // 最大令牌數

    /**
     * 初始化
     * @param Array $config redis連接設定
     * @param string $queue 令牌桶隊列
     * @param int $max 令牌桶最大令牌數
     * @throws Exception
     */
    public function __construct($queue, $max, array $config = []){

        //傳進去的配置優先
        if($config){
            $this->_config = array_merge($this->_config,$config);
        }

        $this->_queue = $queue;
        $this->_max = $max;
        $this->_redis = $this->connect();
    }

    /**
     * 加入令牌
     * @param  Int $num 加入的令牌數量
     * @return Int 加入的數量
     */
    public function add($num = 0){

        // 當前剩餘令牌數
        $curnum = intval($this->_redis->lSize($this->_queue));

        // 最大令牌數
        $maxnum = intval($this->_max);

        // 計算最大可加入的令牌數量,不能超過最大令牌數
        $num = $maxnum >= $curnum + $num ? $num : $maxnum - $curnum;

        // 加入令牌
        if($num > 0){
            $token = array_fill(0, $num, 1);
            $this->_redis->lPush($this->_queue, ...$token);
            return $num;
        }

        return 0;
    }

    /**
     * 獲取令牌
     * @return Boolean
     */
    public function get(){
        return $this->_redis->rPop($this->_queue)? true : false;
    }

    /**
     * 重設令牌桶,填滿令牌
     */
    public function reset(){
        $this->_redis->delete($this->_queue);
        $this->add($this->_max);
    }

    /**
     * 創建redis連接
     * @return Link
     * @throws Exception
     */
    private function connect(){
        try{
            $redis = new Redis();
            $redis->connect($this->_config['host'],$this->_config['port'],$this->_config['timeout'],$this->_config['reserved'],$this->_config['retry_interval']);
            if(empty($this->_config['auth'])){
                $redis->auth($this->_config['auth']);
            }
            $redis->select($this->_config['index']);
        }catch(RedisException $e){
            throw new Exception($e->getMessage());
            return false;
        }
        return $redis;
    }
}

/**
 * 測試
 * Class TestTokenBucket
 */
class ApiTest
{
    /**
     * 接口限流
     * @param array $config 配置
     * @param string $apiName 請求的接口
     * @return bool
     */
    public function ApiCurrentLimiting(array $config = [],$apiName = '')
    {

        // 該接口無需限流
        if(!array_key_exists($apiName,$config)){
            return true;
        }

        // 創建TokenBucket對象
        $tokenBucket = new TokenBucket($config[$apiName]['queue'], $config[$apiName]['max']);

        // 獲取令牌
        $res = $tokenBucket->get();

        return $res;
    }
}

/**
 * 接口需要限流配置
 */
$apiConfig = [
    //配置限流接口
    'api/test' => [
        'queue' => 'testTokenBucket', //令牌桶名稱
        'max'   => '10', //令牌桶大小
    ],

    //其他接口
    'api/test01' => [],
    'api/test002' => [],
    //...
];

$test = new ApiTest();
$api = 'api/test';
// 獲取令牌桶實例
$tokenBucket = new TokenBucket($apiConfig[$api]['queue'],$apiConfig[$api]['max']);
// 重設令牌桶,填滿令牌,後期可用定時器更新令牌
$tokenBucket->reset();

// 增加令牌
//$add_num = $tokenBucket->add(10);

echo "開始令牌是10個,後面5次拿不到令牌,拒絕處理:<br>";
for($i = 0; $i < 15; $i ++){
    //請求接口
    $res = $test->ApiCurrentLimiting($apiConfig,'api/test');
    var_dump($res);echo "<br>";
}
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(true)
//bool(false)
//bool(false)
//bool(false)
//bool(false)
//bool(false)

//下一次添加令牌之後才能繼續訪問接口,達到單位時間內的接口限流效果
....

更新令牌策略

  • 定期加入令牌,我們可以使用crontab實現,每分鐘調用add方法加入若干令牌。crontab最小單位是1分鐘,可以結合shell腳本,細化到秒爲單位的更新。這個效率比較低,每個接口都需要開一個定時任務添加令牌。
  • 獲取令牌前觸發更新機制,判斷上次更新時間和現在的時間間隔,大於指定更新時間則添加令牌。增加的令牌和現存令牌之和不能大於令牌桶最大令牌上限。

區別

漏桶算法(Leaky Bucket):主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。漏桶算法提供了一種機制,通過它,突發流量可以被整形以便爲網絡提供一個穩定的流量。
令牌桶算法:用來控制發送到網絡上的數據的數目,並允許突發數據的發送。

兩者主要區別在於“漏桶算法”能夠強行限制數據的傳輸速率,而“令牌桶算法”在能夠限制數據的平均傳輸速率外,還允許某種程度的突發傳輸。在“令牌桶算法”中,只要令牌桶中存在令牌,那麼就允許突發地傳輸數據直到達到用戶配置的門限,所以它適合於具有突發特性的流量。

比較
  • 令牌桶是按照固定速率往桶中添加令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減爲零時則拒絕新的請求;
  • 漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;
  • 令牌桶限制的是平均流入速率(允許突發請求,只要有令牌就可以處理,支持一次拿3個令牌,4個令牌),並允許一定程度突發流量;
  • 漏桶限制的是常量流出速率(即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發流入速率;
  • 令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;
  • 兩個算法實現可以一樣,但是方向是相反的,對於相同的參數得到的限流效果是一樣的。

參考資料:
https://blog.csdn.net/fdipzone/article/details/79352685
https://www.jianshu.com/p/9f76dd2757c7
https://segmentfault.com/a/1190000019556686?utm_source=tag-newest
https://www.ucloud.cn/yun/22455.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章