【问题标题】:PHP rate limiting clientPHP限速客户端
【发布时间】:2011-05-14 13:09:26
【问题描述】:

我使用各种第 3 方 Web API,其中许多都强制执行速率限制。拥有一个相当通用的 PHP 库会非常有用,我可以使用它来限制我的调用。我可以想出几种方法来做到这一点,也许可以通过将呼叫放入队列中,并带有可以拨打电话的时间戳,但如果其他人已经做得很好,我希望避免重新发明轮子。

【问题讨论】:

标签: php


【解决方案1】:

作为替代方案,我(过去)创建了一个“缓存”文件夹来存储 API 调用,因此如果我尝试在特定时间范围内再次进行相同的调用,它会首先从缓存中获取(更无缝),直到可以拨打新电话。短期内可能会以归档信息告终,但从长远来看,您可以避免 API 对您的阻碍。

【讨论】:

  • 缓存仅在我使用相同参数调用给定 API 时才有用。这是朝着正确方向迈出的一步,但我经常会改变参数并期待不同的结果。此外,一些 API 禁止在其 TOS 中进行缓存。
【解决方案2】:

我意识到这是一个旧线程,但我想我会发布我的解决方案,因为它是基于我在 SE 上找到的其他内容。我自己找了一会儿答案,但找不到好东西。它基于here 讨论的 Python 解决方案,但我添加了对可变大小请求的支持,并使用 PHP 闭包将其变成了函数生成器。

function ratelimiter($rate = 5, $per = 8) {
  $last_check = microtime(True);
  $allowance = $rate;

  return function ($consumed = 1) use (
    &$last_check,
    &$allowance,
    $rate,
    $per
  ) {
    $current = microtime(True);
    $time_passed = $current - $last_check;
    $last_check = $current;

    $allowance += $time_passed * ($rate / $per);
    if ($allowance > $rate)
      $allowance = $rate;

    if ($allowance < $consumed) {
      $duration = ($consumed - $allowance) * ($per / $rate);
      $last_check += $duration;
      usleep($duration * 1000000);
      $allowance = 0;
    }
    else
      $allowance -= $consumed;

    return;
  };
}

它可以用来限制任何东西。这是一个愚蠢的例子,它将一个简单的语句限制为默认每八秒五个“请求”:

$ratelimit = ratelimiter();
while (True) {
  $ratelimit();
  echo "foo".PHP_EOL;
}

以下是我如何使用它来根据批处理的大小将针对 Facebook Graph API 的批处理请求限制为每 600 秒 600 个请求:

$ratelimit = ratelimiter(600, 600);
while (..) {
  ..

  $ratelimit(count($requests));
  $response = (new FacebookRequest(
    $session, 'POST', '/', ['batch' => json_encode($requests)]
  ))->execute();

  foreach ($response->..) {
    ..
  }
}

希望这对某人有所帮助!

【讨论】:

  • 正是我想要的。谢谢大家!
【解决方案3】:

您可以使用token bucket algorithm 进行速率限制。我用 PHP 为你实现了:bandwidth-throttle/token-bucket :

use bandwidthThrottle\tokenBucket\Rate;
use bandwidthThrottle\tokenBucket\TokenBucket;
use bandwidthThrottle\tokenBucket\storage\FileStorage;

$storage = new FileStorage(__DIR__ . "/api.bucket");
$rate    = new Rate(10, Rate::SECOND);
$bucket  = new TokenBucket(10, $rate, $storage);
$bucket->bootstrap(10);

if (!$bucket->consume(1, $seconds)) {
    http_response_code(429);
    header(sprintf("Retry-After: %d", floor($seconds)));
    exit();
}

【讨论】:

    【解决方案4】:

    PHP 源代码通过允许任何用户每 5 秒发出一次请求并使用 Redix 来限制对 API 的访问。

    安装 Redis/Redix 客户端:

    composer 需要 predis/predis

    根据您的操作系统下载 Redix (https://github.com/alash3al/redix/releases),然后启动服务:

    ./redix_linux_amd64

    以下答案表明 Redix 正在侦听 RESP 协议的 6380 端口和 HTTP 协议的 7090 端口。

    redix resp 服务器位于:localhost:6380
    redix http 服务器位于:localhost:7090

    在您的 API 中,将以下代码添加到标头:

    <?php
     require_once 'class.ratelimit.redix.php';
    
     $rl = new RateLimit();
     $waitfor = $rl->getSleepTime($_SERVER['REMOTE_ADDR']);
     if ($waitfor>0) {
       echo 'Rate limit exceeded, please try again in '.$waitfor.'s';
       exit;    
     }
    
     // Your API response
     echo 'API response';
    

    脚本class.ratelimit.redix.php的源码是:

    <?php
    require_once __DIR__.'/vendor/autoload.php';
    Predis\Autoloader::register();
    
    class RateLimit {
    
      private $redis;
      const RATE_LIMIT_SECS = 5; // allow 1 request every x seconds
    
      public function __construct() {
         $this->redis = new Predis\Client([
             'scheme' => 'tcp',
             'host'   => 'localhost', // or the server IP on which Redix is running
             'port'   => 6380
         ]);
      }
    
     /**
      * Returns the number of seconds to wait until the next time the IP is allowed
      * @param ip {String}
      */
     public function getSleepTime($ip) {
         $value = $this->redis->get($ip);
         if(empty($value)) {
           // if the key doesn't exists, we insert it with the current datetime, and an expiration in seconds
             $this->redis->set($ip, time(), self::RATE_LIMIT_SECS*1000);
             return 0;
           } 
           return self::RATE_LIMIT_SECS - (time() - intval(strval($value)));
         } // getSleepTime
     } // class RateLimit
    

    【讨论】:

      【解决方案5】:

      我喜欢 mwp 的回答,我想将其转换为 OO,让我感到温暖和模糊。我最终彻底重写了它,以至于从他的版本中完全无法识别。所以,这是我受 mwp 启发的 OO 版本。

      基本解释:每次调用await,它会将当前时间戳保存在一个数组中,并丢弃所有不再相关的旧时间戳(大于间隔的持续时间)。如果超过了速率限制,那么它会计算直到它再次被释放并休眠到那时为止的时间。

      用法:

      $limiter = new RateLimiter(4, 1); // can be called 4 times per 1 second
      for($i = 0; $i < 10; $i++) {
          $limiter->await();
          echo microtime(true) . "\n";
      }
      
      

      我还为 run 方法添加了一点语法糖。

      $limiter = new RateLimiter(4, 1);
      for($i = 0; $i < 10; $i++) {
          $limiter->run(function() { echo microtime(true) . "\n"; });
      }
      
      <?php
      
      class RateLimiter {
          private $frequency;
          private $duration;
          private $instances;
       
          public function __construct($frequency, $duration) {
              $this->frequency = $frequency;
              $this->duration = $duration;
              $this->instances = [];
          }
      
          public function await() {
      
              $this->purge();
              $this->instances[] = microtime(true);
      
              if($this->is_free()) {
                  return;
              }
              else {
                  $wait_duration = $this->duration_until_free();
                  usleep($wait_duration);
                  return;
              }
          }
      
          public function run($callback) {
              if(!is_callable($callback)) {
                  return false;
              }
      
              $this->await();
              $callback();
      
              return true;
          }
          
          public function purge() {
              $this->instances = RateLimiter::purge_old_instances($this->instances, $this->duration);
          }
          
          public function duration_until_free() {
              return RateLimiter::get_duration_until_free($this->instances, $this->duration);
          }
      
          public function is_free() {
              return count($this->instances) < $this->frequency;
          }
      
          public static function get_duration_until_free($instances, $duration) {
              $oldest = $instances[0];
              $free_at = $oldest + $duration * 1000000;
              $now = microtime(true);
      
              if($free_at < $now) {
                  return 0;
              }
              else {
                  return $free_at - $now;
              }
          }
      
          public static function purge_old_instances($instances, $duration) {
              $now = microtime(true);
              $cutoff = $now - $duration;
              return array_filter($instances, function($a) use ($duration, $cutoff) {
                  return $a >= $cutoff;
              });
          }
      }
      

      【讨论】:

        【解决方案6】:

        这与@Jeff 的回答基本相同,但我已经整理了很多代码并添加了 PHP7.4 类型/返回提示。

        我也将此作为作曲家包发布:https://github.com/MacroMan/rate-limiter

        composer require macroman/rate-limiter

        /**
         * Class RateLimiter
         *
         * @package App\Components
         */
        class Limiter
        {
            /**
             * Limit to this many requests
             *
             * @var int
             */
            private int $frequency = 0;
        
            /**
             * Limit for this duration
             *
             * @var int
             */
            private int $duration = 0;
        
            /**
             * Current instances
             *
             * @var array
             */
            private array $instances = [];
        
            /**
             * RateLimiter constructor.
             *
             * @param int $frequency
             * @param int $duration #
             */
            public function __construct(int $frequency, int $duration)
            {
                $this->frequency = $frequency;
                $this->duration = $duration;
            }
        
            /**
             * Sleep if the bucket is full
             */
            public function await(): void
            {
                $this->purge();
                $this->instances[] = microtime(true);
        
                if (!$this->is_free()) {
                    $wait_duration = $this->duration_until_free();
                    usleep($wait_duration);
                }
            }
        
            /**
             * Remove expired instances
             */
            private function purge(): void
            {
                $cutoff = microtime(true) - $this->duration;
        
                $this->instances = array_filter($this->instances, function ($a) use ($cutoff) {
                    return $a >= $cutoff;
                });
            }
        
            /**
             * Can we run now?
             *
             * @return bool
             */
            private function is_free(): bool
            {
                return count($this->instances) < $this->frequency;
            }
        
            /**
             * Get the number of microseconds until we can run the next instance
             *
             * @return float
             */
            private function duration_until_free(): float
            {
                $oldest = $this->instances[0];
                $free_at = $oldest + $this->duration * 1000000;
                $now = microtime(true);
        
                return ($free_at < $now) ? 0 : $free_at - $now;
            }
        }
        

        用法一样

        use RateLimiter\Limiter;
        
        // Limit to 6 iterations per second
        $limiter = new Limiter(6, 1);
        
        for ($i = 0; $i < 50; $i++) {
            $limiter->await();
        
            echo "Iteration $i" . PHP_EOL;
        }
        

        【讨论】:

        • 嗨,我在duration_until_free() 的第一行一直收到Undefined offset: 0 异常。我通过在purge() 函数中过滤数组后使用$this-&gt;instances = array_values($this-&gt;instances); 重新索引数组来解决此问题。显然array_filter() 在数组中创建了间隙,并且某些索引可能为空/未定义。
        猜你喜欢
        • 1970-01-01
        • 2017-11-20
        • 1970-01-01
        • 2016-09-21
        • 2014-03-20
        • 1970-01-01
        • 2019-01-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多