限流 :对某段时间内拜访次数限度,保证系统的可用性和稳定性。避免忽然拜访暴增导致系统响应迟缓或者宕机。
场景:在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
类
<code class="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); }
具体实现办法
<code class="php"> /** * @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秒 申请到大略到第四次会进行限流
<code class="php">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
<code class="php"> protected $middlewareGroups = [ 'api' => [ 'throttle:60,1', //执行中间件 每分钟申请限度在60次 ], ];
源码剖析
- 判断是否设置api申请速率限度
- 执行判断限度速率办法
- 依据缓存key 判断api 设置工夫单位内申请次数达到了阀值
- 达到了申请阀值,进行速率限度
注入缓存实例
<code class="php"> protected $limiter; /** * Create a new request throttler. * * @param \Illuminate\Cache\RateLimiter $limiter * @return void */ public function __construct(RateLimiter $limiter) { $this->limiter = $limiter; }
判断是否配置了速率限度
<code class="php"> /** * 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, //寄存回调响应 ], ] ); }
判断是否达到阀值。
<code class="php">/** * 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
办法
<code class="php"> /** * 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; }
该办法实现的原理:周期性限流。通过次数/工夫来限度申请频率。
上面是我基于下面的逻辑实现一个这样的类,仅供参考。
<code class="php">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; } }