問題描述
當系統面對高併發情況時,某些接口達到處理請求瓶頸從來出現拒絕訪問,並引發連鎖反應導致整個系統崩潰。面對這種特殊情況如何解決?
我們生活中也有類似情景,比如老式電閘都安裝了保險絲,一旦有人使用超大功率的設備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理我們的接口也需要安裝上“保險絲”,以防止非預期的請求對系統壓力過大而引起的系統癱瘓,當流量過大時,可以採取拒絕或者引流等機制。
方案方案
在開發高併發系統時,有三把利器用來保護系統:緩存、降級和限流。
這裏介紹限流。
限流算法
常用的限流算法有兩種:漏桶算法和令牌桶算法。
漏桶算法
漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以一定的速度出水,當水流入速度過大會直接溢出(等待、丟棄),可以看出漏桶算法能強行限制數據的傳輸速率。漏桶的剩餘空間就代表着當前行爲可以持續進行的數量,漏嘴的流水速率代表着系統允許該行爲的最大頻率。如下圖:
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