令牌桶限頻(Token Bucket)


token buckets-1.jpg

高可用對於一個應用和API接口是至關重要的。如果我們提供一個接口,突然面臨流量爆發式增長,對於這種情況,不僅會影響網站的訪問速度,甚至可能會導致服務器崩潰,使得所有用戶都無法正常訪問。

對於這種情況,有的同學認為:“我們可以通過提高配置或者增加機器去解決這樣的問題”。這在某些情況下,確實是一種選擇。然而當我們使用一個接口或應用時,我們不僅需要通過技術手段(冪等性、熔斷等)去提高它們的穩定性,同時也確保因為其他突發原因(如新同事編寫的代碼導致意外的發生等)帶來的問題。

以下幾種情況,頻率限制可以幫助我們更好的確保API的可用性:

  • 用戶使用腳本,向我們發送了大量的請求。
  • 用戶向我們發送了許多低優先級接口數據的請求,而我們希望這些低優先級的請求盡量不影響我們高優先級接口請求(如電商,必須保證下單流程是正常的,其他的接口優先級自然低於下單流程相關的接口)。
  • 因突然原因,無法同時正常訪問所有接口,因此需要臨時丟棄優先級低的請求(當然Nginx層面也是可以實現的)。

令牌桶介紹

令牌桶算法的原理是系統會以一個恆定的速度往桶(bucket)放入令牌(token),而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務(denial of service)。

特點:

  1. bucket滿的時候,將不再放入token,也就是token數量不會超過bucket最大容量。
  2. 由於token在一段時間內是有限的,所以即使發生突然流量,也能很好的保護服務。

實現方式

對於令牌桶的實現,一般常用的有兩種:

第一種

后台啟動一個線程,按照一定的時間顆粒度,不斷的往固定大小的桶(bucket)增加令牌(token),直到達到的最大容量。

這種做法不僅實現稍微繁瑣一點,需要額外維護一個腳本;而且在沒有請求的情況下,線程也會不斷的去檢查更新token,如果key比較多的情況下,對CPU會有較大的性能影響。

第二種(本文案例)

每次訪問,將本次訪問的時間和速率存入redis,並在下次新的請求訪問時,對比當前時間上次請求時間兩個時間差之間的可使用token數量,並將新的結果存入redis

代碼實現

  • initNum 桶初始大小
  • expire單位時間
  • nowTime 當前訪問的時間
  • limitData['time'] 上次請求的時間
<?php

namespace app\components;

use yii\base\Component;

class RateLimiter extends Component
{
    public $redis = null;

    public $cacheKey = null;

    // number of visits per minute by a single user
    // 單位時間下,單個用戶訪問的次數
    public $initNum = 30;

    // unit time
    // 單位時間
    public $expire = 60;

    /**
     * RateLimiter constructor.
     * @param string $initNum
     * @param string $expire
     */
    public function __construct($cacheKey = '', $initNum = '', $expire = '')
    {
        if (empty($cacheKey)) {
            return false;
        }

        $this->redis = \Yii::$app->redis;
        $this->initNum = $initNum ?? $this->initNum;
        $this->expire = $expire ?? $this->expire;
        $this->cacheKey = $cacheKey;
    }

    /**
     * handler
     * @return array
     */
    public function handler()
    {
        $ret = self::_limit($this->cacheKey, $this->initNum, $this->expire);
        if (empty($ret['status'])) {
            return false;
        }

        return true;
    }

    private function _limit($cacheKey = '', $initNum = '', $expire = '')
    {
        $nowTime = time();

        $this->redis->watch($cacheKey);
        $redisData = $this->redis->get($cacheKey);
        $limitData = $redisData ? json_decode($redisData, true) : ['num' => $initNum, 'time' => $nowTime];

        // (單位時間訪問頻率 / 單位時間)*(當前時間 - 上次訪問時間) = 上次請求至今可增加的訪問次數
        $addNum = ($initNum / $expire) * ($nowTime - $limitData['time']);
        $newNum = min($initNum, (($limitData['num'] - 1) + $addNum));
        if ($newNum <= 0) {
            return ['status' => false, 'msg' => '當前時刻令牌用完啦!'];
        }

        $limitData = json_encode(['num' => $newNum, 'time' => $nowTime]);
        $this->redis->multi();

        $this->redis->set($cacheKey, $limitData);

        if (!$this->redis->exec()) {
            return ['status' => false, 'msg' => '訪問頻次過多!'];
        }

        return ['status' => true, 'msg' => 'ok'];
    }
}

 

總結

令牌桶頻率限制是API接口設計中重要的安全策略之一,但是對於不同的業務和場景都應該使用最適合的方法(如登錄密碼錯誤,一天只能嘗試五次,這種情況計數器限頻就是更好的選擇了),萬事並無絕對。

參考

  1. https://medium.com/smyte/rate-limiter-df3408325846
  2. https://stripe.com/en-hk/blog/rate-limiters


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM