限流 :对某段时间内拜访次数限度,保证系统的可用性和稳定性。避免忽然拜访暴增导致系统响应迟缓或者宕机。
场景:在php-fpm中,fpm开启的子过程数是无限的,当并发申请大于可用子过程数时,过程池调配不了多余的子过程解决http申请,服务就会开始阻塞。导致nginx抛出502。
晓得了大略的概念,当初咱们次要讲限流在单体架构外面的应用。
1.服务代理层限流
nginx 限流
nginx的 HttpLimitRequest
模块
该模块能够指定会话申请数量,能够通过指定ip进行申请频率限度。应用漏桶算法进行申请频率限度。
示例:
http {
//会话状态存储在了10m的名称为"one"这个区域。该区域均匀查问限度在每秒1个申请
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
... server { ... location /search/ {
// 没秒均匀申请不超过1个申请 突发不超过5个查问 如果不须要限度突发提早内的超额申请,则应应用
nodelay limit_req zone=one burst= 5 nodelay;
}
具体能够参考nginx文档 HttpLimitReqest模块
这是摘抄nginx文档中的一段对于限流的小例子。nginx应用的漏桶算法对用户拜访频率进行限度。
通过百度、google 咱们晓得了。原来限流是基于算法来实现的。上面是限流的两种算法:
实现限流的算法
- 漏桶算法
- 令牌桶算法
当然咱们不仅要知其然,还要知其所以然。
1.漏桶算法
漏桶算法:漏桶有肯定的容量,且漏桶会漏水。
当单位工夫内注入的水大于单位工夫内流出的水。漏桶积攒的水越来越多。直到溢出,如果溢出,则须要限流。
算法形容:
以后水量: 上次容量-流出容量+注入水量
流出容量:(以后注水工夫-上次注水工夫)*流出速率
当 「以后水量」> 「桶子容量」 则溢出。否则失常,记录本次水量和注水工夫。
通过图片形容漏桶算法
2. php+redis 实现漏桶算法限流类
新增BucketLimit.php
类
protected $capacity = 60; //桶子总容量
protected $addNum = 20; //每次注入水的容量
protected $rate = 2; //漏水速率
protected $water_key = "water_capacity"; //缓存key
public $redis; //应用redis 缓存以后桶水量和上次注水工夫
public function __construct()
{
$redis = new \Redis();
$this->redis= $redis;
$this->redis->connect('127.0.0.1',6379);
}
具体实现办法
/**
* @param $api [string 指定接口限流]
* @param $addNum [int 注水量 ]
* @return bool
*/
public function bucket($addNum,$api='')
{
$this->addNum = $addNum;
// 获取上次 桶内水量 注水工夫
list($waterCapacity,$waterTime,$lastTime) = $this->getLastWater();
//计算出工夫内流出的水量
$lastWater = ($lastTime-$waterTime)*$this->rate;
//本次水量
$waterCapacity = $waterCapacity-$lastWater;
//水量不能小于0
$waterCapacity = ( $waterCapacity>=0 ) ? $waterCapacity : 0 ;
$waterTime = $lastTime;
//以后水量大于桶子容量 溢出返回 false 存储水量和注水工夫
if( ($waterCapacity+$addNum) <= $this->capacity ){
$waterCapacity += $addNum;
$this->setWater($waterCapacity,$waterTime);
return true;
}else{
$this->setWater($waterCapacity,$waterTime);
return false;
}
}
/**
* @return array [$waterCapacity,$waterTime,$lastTime] * 以后容量 上次漏水工夫 以后工夫
*/
private function getLastWater()
{
$water = $this->redis->get($this->water_key);
if($water) {
$water = json_decode($water,true);
$waterCapacity =$water['water_capacity']; //上一次容量
$waterTime =$water['time']; //上一次注水工夫
$lastTime = time(); //本次注水工夫
} else{
$this->redis->set($this->water_key,json_encode([
'water_capacity'=>0,
'time'=>time()
]));
$waterCapacity =0; //上一次容量
$waterTime =time(); //上一次注水工夫
$lastTime = time(); //本次注水工夫
}
return [$waterCapacity,$waterTime,$lastTime];
}
/**
* @param $waterCapacity [int 本次残余容量]
* @param $waterTime [int 本次注水工夫]
*/
private function setWater($waterCapacity,$waterTime)
{
$this->redis->set($this->water_key,json_encode([
'water_capacity'=>$waterCapacity,
'time'=>$waterTime
]));
}
开始测试
应用 for + sleep函数模仿申请 失常2s申请一次 办法失常不限流 小于2秒 申请到大略到第四次会进行限流
require_once 'BucketLimit.php';
$bucket = new BucketLimit();
for($i=1;$i<=100;$i++) {
//依据for + sleep函数模仿申请 失常2s申请一次 办法失常不限流 sleep(1);
$data = $bucket->bucket(10);
var_dump($data)."\n";
}
2. 令牌桶算法
令牌桶算法和漏桶算法刚好相同,指定速率向桶子外面投放令牌。每次申请都会想桶外面拿走一枚令牌,当桶子外面的令牌生产结束,则限流。长处:能够不便扭转投递令牌的速率。
应用案例
hyperf 令牌桶算法实现限流代码
3.laravel框架中对api限流 app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
'throttle:60,1', //执行中间件 每分钟申请限度在60次
],
];
源码剖析
- 判断是否设置api申请速率限度
- 执行判断限度速率办法
- 依据缓存key 判断api 设置工夫单位内申请次数达到了阀值
- 达到了申请阀值,进行速率限度
注入缓存实例
protected $limiter;
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
判断是否配置了速率限度
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int|string $maxAttempts
* @param float|int $decayMinutes
* @param string $prefix
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '')
{
//判断用户是否限度频率
if (is_string($maxAttempts)
&& func_num_args() === 3
&& ! is_null($limiter = $this->limiter->limiter($maxAttempts))) {
return $this->handleRequestUsingNamedLimiter($request, $next, $maxAttempts, $limiter);
}
//执行频率限度判断 参数别离是:
return $this->handleRequest(
$request, //申请类
$next, //中间件基类
[
(object) [
'key' => $prefix.$this->resolveRequestSignature($request), //缓存key
'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts), //获取频繁阀值
'decayMinutes' => $decayMinutes,
'responseCallback' => null, //寄存回调响应
],
]
);
}
判断是否达到阀值。
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param array $limits
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
//判断速率是否达到阀值 返回 true false 该办法应用缓存实例取出缓存的key
if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
//相似于redis数值自增 并且设置过期工夫
$this->limiter->hit($limit->key, $limit->decayMinutes * 60);
}
$response = $next($request);
//将响应放入响应回调函数中
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
);
}
//返回响应
return $response;
}
获取频率 $this->limiter->tooManyAttempts
办法
/**
* Determine if the given key has been "accessed" too many times.
*
* @param string $key
* @param int $maxAttempts
* @return bool
*/
public function tooManyAttempts($key, $maxAttempts)
{
if ($this->attempts($key) >= $maxAttempts) {
if ($this->cache->has($key.':timer')) {
return true;
}
$this->resetAttempts($key);
}
return false;
}
该办法实现的原理:周期性限流。通过次数/工夫来限度申请频率。
上面是我基于下面的逻辑实现一个这样的类,仅供参考。
class CurrentLimiting
{
protected $limit;
protected $minutes;
protected $redis;
protected $key;
/**
* CurrentLimiting constructor.
* @param string $api 接口
* @param string $ip ip
* @param int $limit 限度频率
* @param int $minutes 分钟
*/
public function __construct(string $api,string $ip,int $limit,int $minutes)
{
$redis = new \Redis();
$redis->connect('127.0.0.1','6379',3);
$this->redis = $redis;
$this->limit = $limit;
$this->minutes = $minutes;
$this->key = $ip.$api;
}
//获取申请次数
public function attempts()
{
$count = $this->redis->get($this->key);
return is_null($count) ? 0 : $count;
}
/**
*
* @return bool
*/
public function CurrentLimit()
{
$count = $this->attempts();
if($count >= $this->limit) {
return false;
}
if($count==0){
$this->redis->set($this->key,0,$this->minutes*60);
}
//设置锁
$this->redis->multi();
$this->redis->watch();
$this->redis->incr($this->key);
return true;
}
}
发表回复