
高可用對於一個應用和API接口是至關重要的。如果我們提供一個接口,突然面臨流量爆發式增長,對於這種情況,不僅會影響網站的訪問速度,甚至可能會導致服務器崩潰,使得所有用戶都無法正常訪問。
對於這種情況,有的同學認為:“我們可以通過提高配置或者增加機器去解決這樣的問題”。這在某些情況下,確實是一種選擇。然而當我們使用一個接口或應用時,我們不僅需要通過技術手段(冪等性、熔斷等)去提高它們的穩定性,同時也確保因為其他突發原因(如新同事編寫的代碼導致意外的發生等)帶來的問題。
以下幾種情況,頻率限制可以幫助我們更好的確保API的可用性:
- 用戶使用腳本,向我們發送了大量的請求。
- 用戶向我們發送了許多低優先級接口數據的請求,而我們希望這些低優先級的請求盡量不影響我們高優先級接口請求(如電商,必須保證下單流程是正常的,其他的接口優先級自然低於下單流程相關的接口)。
- 因突然原因,無法同時正常訪問所有接口,因此需要臨時丟棄優先級低的請求(當然
Nginx層面也是可以實現的)。
令牌桶介紹
令牌桶算法的原理是系統會以一個恆定的速度往桶(bucket)放入令牌(token),而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務(denial of service)。
特點:
- 當
bucket滿的時候,將不再放入token,也就是token數量不會超過bucket最大容量。 - 由於
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接口設計中重要的安全策略之一,但是對於不同的業務和場景都應該使用最適合的方法(如登錄密碼錯誤,一天只能嘗試五次,這種情況計數器限頻就是更好的選擇了),萬事並無絕對。
