Traffic limiting: Limits the number of access times in a certain period of time to ensure system availability and stability. This prevents slow response or system breakdown caused by sudden access surge. Scenario: In PHP-FPM, the number of child processes enabled by FPM is limited. When the number of concurrent requests exceeds the number of available child processes, the process pool cannot allocate additional child processes to handle HTTP requests, and the service starts to block. Causes nginx to throw 502.

Now that we know the general concept, we will focus on the use of limiting in single architecture.

1. Traffic limiting at the service proxy layer

Nginx current-limiting

The nginxHttpLimitRequestThe module

The module can specify the number of session requests and limit the frequency of requests by specifying IP addresses. Use the leaky bucket algorithm for request frequency limitation.

Example:

HTTP {// Session state is stored in the region named "one" at 10m. Limit_req_zone $binary_REMOTE_ADDR Zone =one:10m rate=1r/s; . server { ... Location /search/ {// No second No more than 1 requests No more than 5 queries If you do not need to limit the number of excessive requests within the burst delay, set nodelay limit_req zone=one burst= 5 nodelay. }Copy the code
See the nginx documentation for detailsHttpLimitReqest module

This is a small example of limiting traffic from the Nginx documentation. Nginx uses the leaky bucket algorithm to limit user access frequency. Through Baidu, Google we know. The original flow limiting is based on algorithms to achieve. Here are two algorithms for limiting traffic:

Implement the algorithm of limiting traffic
  • Bucket algorithm
  • Token bucket algorithm

Of course, we should not only know the why, but also the why.

1. Leaky bucket algorithm

Leaky bucket algorithm: Leaky bucket has a certain capacity and leaks water. When the amount of water injected per unit time is greater than the amount of water discharged per unit time. More and more water accumulates in the leaky bucket. Until it overflows, in which case, limiting the flow is required. Algorithm description: Current water volume: last capacity – Outflow capacity + Injected water Outflow capacity: (Current water injection time – last water injection time) x outflow rate If Current water volume > Bucket capacity, the bucket overflows. Otherwise, record the amount of water and injection time.

The leaky bucket algorithm is described by pictures

2. PHP +redis to implement leakage bucket algorithm traffic limiting class

New BucketLimit. PHP classes

  protected $capacity  = 60; // Total bucket capacity
  protected $addNum    = 20; // The capacity of each injection
  protected $rate      = 2;  // Water leakage rate
  protected $water_key = "water_capacity"; / / the cache key
  public $redis;        // Use redis to cache the current bucket water volume and the time of the last injection

  public function __construct()
  {
        $redis = new \Redis();
        $this->redis= $redis;
        $this->redis->connect('127.0.0.1'.6379);
  }

Copy the code

Concrete implementation method

 / * * *@param$API [string specifies interface traffic limiting] *@param $addNum [int 注水量 ]
     * @return bool
     */
    public function bucket($addNum.$api=' ')
    {
        $this->addNum = $addNum;
        // Get the last water injection time in the bucket
        list($waterCapacity.$waterTime.$lastTime) = $this->getLastWater();
        // Calculate the amount of water flowing out over time
        $lastWater = ($lastTime-$waterTime) *$this->rate;
        // The amount of water
        $waterCapacity = $waterCapacity-$lastWater;
        // The amount of water cannot be less than 0
        $waterCapacity = ( $waterCapacity> =0)?$waterCapacity : 0 ;
        $waterTime = $lastTime;
        // If the current amount of water exceeds the bucket capacity overflow returns false storage amount and injection time
        if(($waterCapacity+$addNum) < =$this->capacity ){
            $waterCapacity+ =$addNum;
            $this->setWater($waterCapacity.$waterTime);
            return true;
        }else{
           $this->setWater($waterCapacity.$waterTime);
            return false; }}/ * * *@returnArray [$waterCapacity,$waterTime,$lastTime] * Current capacity Last water leakage time Current time */
 private function getLastWater()
{
    $water = $this->redis->get($this->water_key);

    if($water) {
        $water = json_decode($water.true);
        $waterCapacity =$water['water_capacity'];  // Last capacity
        $waterTime =$water['time']; // Last injection time
        $lastTime = time(); // Water injection time
  } else{
        $this->redis->set($this->water_key,json_encode([
            'water_capacity'= >0.'time'=>time()
        ]));
        $waterCapacity =0;  // Last capacity
        $waterTime =time(); // Last injection time
        $lastTime = time(); // Water injection time
  }
    return [$waterCapacity.$waterTime.$lastTime];
}

/ * * *@param$waterCapacity [int Remaining capacity] *@param$waterTime [int waterTime] */
 private function setWater($waterCapacity.$waterTime)
{
    $this->redis->set($this->water_key,json_encode([
        'water_capacity'= >$waterCapacity.'time'= >$waterTime
  ]));
}
  
  
Copy the code
To begin testing

Use the for + sleep function to simulate the request normal 2s request a method normal unlimited flow less than 2 seconds request to about the fourth time will be limited

require_once 'BucketLimit.php';

$bucket = new BucketLimit();

for($i=1;$i< =100;$i{+ +)Sleep (1); sleep(1);
 $data =  $bucket->bucket(10);
  var_dump($data)."\n";
}
Copy the code

2. Token bucket algorithm

The token-bucket algorithm is the opposite of the leak-bucket algorithm, which drops tokens into the bucket at a specified rate. Each request takes a token from the bucket, and when all the tokens in the bucket are consumed, the flow is limited. Advantages: It is easy to change the delivery rate of tokens.

Use case

Hyperf token bucket algorithm to achieve traffic limiting code

3. API traffic limiting in Laravel frameworkapp/Http/Kernel.php

    protected $middlewareGroups = [
           'api'= > ['throttle: 60, 1'.// Execution middleware is limited to 60 requests per minute]];Copy the code
Source code analysis
  • Determines whether to set the API request rate limit
  • Execute the judge limit rate method
  • According to the cache key, the number of requests in the UNIT of API setting time reaches the threshold
  • The request threshold is reached, and the rate limit is performed
Injection cache instance
 protected $limiter;

    /**
     * Create a new request throttler.
     *
     * @param  \Illuminate\Cache\RateLimiter  $limiter
     * @return void
     */
    public function __construct(RateLimiter $limiter)
    {
        $this->limiter = $limiter;
    }
Copy the code
Check whether the rate limit is configured
 /**
     * 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 = ' ')
    {

        // Determine whether the user limits the frequency
        if (is_string($maxAttempts)
            && func_num_args() === 3&&! is_null($limiter = $this->limiter->limiter($maxAttempts))) {

            return $this->handleRequestUsingNamedLimiter($request.$next.$maxAttempts.$limiter);
        }
       // Execute frequency limit judgment parameters are:
        return $this->handleRequest(
            $request./ / request
            $next.// Middleware base class[(object) [
                    'key'= >$prefix.$this->resolveRequestSignature($request), / / the cache key
                    'maxAttempts'= >$this->resolveMaxAttempts($request.$maxAttempts), // Get the frequency threshold
                    'decayMinutes'= >$decayMinutes.'responseCallback'= >null.// Store the callback response]]); }Copy the code
Check whether the threshold is reached.
/**
     * 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) {
            Return true false This method uses the cache instance to fetch the cache key
            if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
                throw $this->buildException($request.$limit->key, $limit->maxAttempts, $limit->responseCallback);
            }
            // Similar to redis value increment and set expiration time
            $this->limiter->hit($limit->key, $limit->decayMinutes * 60);
        }

        $response = $next($request);
        // Put the response into the response callback function
        foreach ($limits as $limit) {
            $response = $this->addHeaders(
                $response.$limit->maxAttempts,
                $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
            );
        }
        // Return the response
        return $response;
    }
Copy the code
To obtain the frequency$this->limiter->tooManyAttemptsmethods

    /**
     * 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;
    }
Copy the code

The principle of this method is periodic current limiting. Limit the frequency of requests by number/time.

Below is my implementation of such a class based on the above logic, for reference only.
class CurrentLimiting
{

    protected $limit;
    protected $minutes;
    protected $redis;
    protected $key;

    /**
     * CurrentLimiting constructor.
     * @paramString $API *@param string $ip ip
     * @paramInt $limit Limit frequency *@paramInt $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;

    }
    // Get the number of requests
    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);
       }
       / / set the lock
       $this->redis->multi();
       $this->redis->watch();
       $this->redis->incr($this->key);

       return true; }}Copy the code