PHP限速客户端

时间:2010-11-23 15:28:58

标签: php

我使用各种第三方Web API,其中许多都强制执行速率限制。拥有一个相当通用的PHP库非常有用,我可以用它来限制我的调用。我可以考虑一些方法来实现它,也许是通过将调用放入具有时间戳的队列来进行调用,但我希望避免重新发明轮子,如果其他人已经做得很好。

7 个答案:

答案 0 :(得分:6)

我意识到这是一个老线程,但我认为我发布了我的解决方案,因为它是基于我在SE上找到的其他东西。我自己找了一会儿答案,但却找不到好的东西。它基于所讨论的Python解决方案here,但我已经添加了对可变大小请求的支持,并使用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;
  };
}

它可以用来限制任何事情。这是一个愚蠢的例子,它限制了默认的五个&#34;请求&#34;每八秒钟:

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

以下是我如何使用它根据批量大小以每600秒600次请求限制针对Facebook Graph API的批量请求:

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

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

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

希望这有助于某人!

答案 1 :(得分:4)

您可以使用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();
}

答案 2 :(得分:2)

我不能相信,但我使用了这种方法,因为没有“通用”包 - 但我想你可以根据你的编码方法做到这一点。

How do I throttle my site's API users?

答案 3 :(得分:1)

作为替代方案,我(过去)创建了一个存储API调用的“缓存”文件夹,因此如果我尝试再次在同一时间段内进行相同的调用,则首先从缓存中获取(更无缝)直到可以拨打新电话。可能会在短期内收到存档信息,但会使您远离阻止您长期使用的API。

答案 4 :(得分:0)

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

安装Redis / Redix客户端:

撰写者需要predis / predis

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

./ redix_linux_amd64

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

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 :(得分:0)

我喜欢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 :(得分:0)

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

我也将其发布为 Composer 包: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;
}