【问题标题】:Restrict function to maximum 100 executions per minute将函数限制为每分钟最多 100 次执行
【发布时间】:2018-01-31 17:22:05
【问题描述】:

我有一个向 API 发出多个 POST 请求的脚本。脚本的大致轮廓如下:

define("MAX_REQUESTS_PER_MINUTE", 100);

function apirequest ($data) {
    // post data using cURL
}

while ($data = getdata ()) {
    apirequest($data);
}

API 受到限制,它允许用户每分钟最多发布 100 个请求。其他请求返回 HTTP 错误 + Retry-After 响应,直到 窗口重置。请注意,服务器处理请求可能需要 100 毫秒到 100 秒之间的任何时间。

我需要确保我的函数每分钟执行的次数不超过 100 次。我已经尝试过usleep 函数来引入 0.66 秒的恒定延迟,但这只会每分钟增加一分钟。诸如 0.1 秒之类的任意值会导致一次或另一次错误。我在数据库表中记录所有请求以及时间,我使用的另一个解决方案是探测表并计算过去 60 秒内发出的请求数。

我需要一个尽可能少浪费时间的解决方案。

【问题讨论】:

    标签: php sql time


    【解决方案1】:

    我会首先记录发出第一个请求的初始时间,然后计算发出了多少请求。一旦发出 60 个请求,请确保当前时间至少比初始时间晚 1 分钟。如果没有睡多久,直到一分钟。当达到分钟时,重置计数和初始时间值。

    【讨论】:

      【解决方案2】:

      这是我的尝试:

      define("MAX_REQUESTS_PER_MINUTE", 100);
      
      function apirequest() {
          static $startingTime;
          static $requestCount;
          if ($startingTime === null) {
              $startingTime = time();
          }
          if ($requestCount === null) {
              $requestCount = 0;
          }
      
          $consumedTime = time() - $startingTime;
      
          if ($consumedTime >= 60) {
              $startingTime = time();
              $requestCount = 0;
          } elseif ($requestCount === MAX_REQUESTS_PER_MINUTE) {
              sleep(60 - $consumedTime);
              $startingTime = time();
              $requestCount = 0;
          }
          $requestCount++;
      
          echo sprintf("Request %3d, Range [%d, %d)", $requestCount, $startingTime, $startingTime + 60) . PHP_EOL;
          file_get_contents("http://localhost/apirequest.php");
          // the above script sleeps for 200-400ms
      }
      
      for ($i = 0; $i < 1000; $i++) {
          apirequest();
      }
      

      【讨论】:

        【解决方案3】:

        我已将 Derek 的建议写入代码。

        class Throttler {
            private $maxRequestsPerMinute;
            private $getdata;
            private $apirequest;
        
            private $firstRequestTime = null;
            private $requestCount = 0;
        
            public function __construct(
                int $maxRequestsPerMinute,
                $getdata,
                $apirequest
            ) {
                $this->maxRequestsPerMinute = $maxRequestsPerMinute;
                $this->getdata = $getdata;
                $this->apirequest = $apirequest;
            }
        
            public function run() {
                while ($data = call_user_func($this->getdata)) {
                    if ($this->requestCount >= $this->maxRequestsPerMinute) {
                        sleep(ceil($this->firstRequestTime + 60 - microtime(true)));
                        $this->firstRequestTime = null;
                        $this->requestCount = 0;
                    }
                    if ($this->firstRequestTime === null) {
                        $this->firstRequestTime = microtime(true);
                    }
                    ++$this->requestCount;
                    call_user_func($this->apirequest, $data);
                }
            }
        }
        
        $throttler = new Throttler(100, 'getdata', 'apirequest');
        $throttler->run();
        

        UPD。我已将其更新版本放在 Packagist 上,以便您可以将其与 Composer 一起使用:https://packagist.org/packages/ob-ivan/throttler

        安装:

        composer require ob-ivan/throttler
        

        使用方法:

        use Ob_Ivan\Throttler\JobInterface;
        use Ob_Ivan\Throttler\Throttler;
        
        class SalmanJob implements JobInterface {
            private $data;
            public function next(): bool {
                $this->data = getdata();
                return (bool)$this->data;
            }
            public function execute() {
                apirequest($this->data);
            }
        }
        
        $throttler = new Throttler(100, 60);
        $throttler->run(new SalmanJob());
        

        请注意还有其他提供相同功能的包(我没有测试过它们):

        【讨论】:

        • 我正在看。但是睡眠后不需要重新设置第一次请求时间吗?
        • 哈哈,对了,我什至忘记设置初始值了。现在修好了。这个功能看起来很有趣,我想我很快就会把它放在作曲家上。
        • @SalmanA 请看我的编辑,它现在可以与作曲家一起使用。并查看其他软件包。你的问题解决了吗?
        • 我没有用过代码(原来API的一分钟比一分钟长!)但我接受了。 PS:我认为计数器也需要每分钟重置一次。
        • @SalmanA 谢谢,我会努力改进它。您提到使用 DB 来存储请求时间。这是否意味着请求是由单独的脚本运行发出的?我不知何故假设了一个来自 CLI 的长时间运行的脚本。我猜错了吗?
        【解决方案4】:

        我尝试过静态睡眠、计数请求和做简单数学的简单解决方案,但它们往往非常不准确、不可靠,并且通常会引入更多睡眠,而这在他们本来可以工作时是必需的。您想要的是仅在您接近速率限制时才开始发出相应的睡眠。

        previous problem 提出我的解决方案以获得那些甜蜜、甜蜜的互联网点:


        我使用了一些数学来计算出一个函数,该函数会在给定请求的正确时间总和内休眠,并允许我在最后以指数方式增加它。

        如果我们将睡眠表示为:

        y = e^( (x-A)/B )
        

        其中AB 是控制曲线形状的任意值,那么从0N 请求的所有睡眠M 的总和将是:

        M = 0∫N e^( (x-A)/B ) dx
        

        这相当于:

        M = B * e^(-A/B) * ( e^(N/B) - 1 )
        

        关于 A 可以解为:

        A = B * ln( -1 * (B - B * e^(N/B)) / M )
        

        虽然求解 B 会更有用,因为指定 A 可以让您定义图形急剧上升的点,该解决方案在数学上很复杂,我自己或找到可以的人其他

        /**
         * @param int $period   M, window size in seconds
         * @param int $limit    N, number of requests permitted in the window
         * @param int $used x, current request number
         * @param int $bias B, "bias" value
         */
        protected static function ratelimit($period, $limit, $used, $bias=20) {
            $period = $period * pow(10,6);
            $sleep = pow(M_E, ($used - self::biasCoeff($period, $limit, $bias))/$bias);
            usleep($sleep);
        }
        
        protected static function biasCoeff($period, $limit, $bias) {
            $key = sprintf('%s-%s-%s', $period, $limit, $bias);
            if( ! key_exists($key, self::$_bcache) ) {
                self::$_bcache[$key] = $bias * log( -1 * ( ($bias - $bias * pow(M_E, $limit/$bias)) / $period ) );
            }
            return self::$_bcache[$key];
        }
        

        经过一番修改,我发现B = 20 似乎是一个不错的默认值,尽管我没有数学基础。 某事斜率mumble mumble指数bs bs

        另外,如果有人想为我解 B 的方程式 I've got a bounty up on math.stackexchange


        尽管我认为我们的情况略有不同,因为我的 API 提供者的响应都包括可用 API 调用的数量,以及仍然保留在窗口内的数量。您可能需要额外的代码来跟踪这一点。

        【讨论】:

          猜你喜欢
          • 2020-01-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-02-03
          • 1970-01-01
          • 2014-05-25
          • 1970-01-01
          相关资源
          最近更新 更多