乐趣区

关于php:浅谈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 个查问 如果不须要限度突发提早内的超额申请,则应应用
 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;
    }
}
退出移动版