浅谈Api限流

限流 :对某段时间内访问次数限制,保证系统的可用性和稳定性。防止突然访问暴增导致系统响应缓慢或者宕机。
场景:在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个查询 如果不需要限制突发延迟内的超额请求,则应使用 nodelaylimit_req zone=one burst= 5 nodelay; }

具体可以参考nginx文档 HttpLimitReqest模块
这是摘抄nginx文档中的一段关于限流的小例子。nginx使用的漏桶算法对用户访问频率进行限制。
通过百度、google 我们知道了。原来限流是基于算法来实现的。下面是限流的两种算法:
实现限流的算法
  • 漏桶算法
  • 令牌桶算法
当然我们不仅要知其然,还要知其所以然。
1.漏桶算法 漏桶算法:漏桶有一定的容量,且漏桶会漏水。
当单位时间内注入的水大于单位时间内流出的水。漏桶积攒的水越来越多。直到溢出,如果溢出,则需要限流。
算法描述:
当前水量: 上次容量-流出容量+注入水量
流出容量:(当前注水时间-上次注水时间)*流出速率
当 「当前水量」> 「桶子容量」 则溢出。否则正常,记录本次水量和注水时间。
通过图片描述漏桶算法 浅谈Api限流
文章图片

2. php+redis 实现漏桶算法限流类 【浅谈Api限流】新增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 =https://www.it610.com/article/$bucket->bucket(10); var_dump($data)."\n"; }

2. 令牌桶算法
令牌桶算法和漏桶算法刚好相反,指定速率向桶子里面投放令牌。每次请求都会想桶里面拿走一枚令牌,当桶子里面的令牌消费完毕,则限流。优点:可以方便改变投递令牌的速率。
使用案例 浅谈Api限流
文章图片

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 * @paramint|string$maxAttempts * @paramfloat|int$decayMinutes * @paramstring$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 * @paramarray$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. * * @paramstring$key * @paramint$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; } }

    推荐阅读