【问题标题】:Rate limiting yourself from overloading external API's限制自己重载外部 API 的速率
【发布时间】:2023-04-01 08:39:01
【问题描述】:

我发现了很多关于如何对 API 的用户进行速率限制的信息和脚本示例,但是我找不到任何示例来说明在施加这些限制时如何对您自己的 API 请求进行速率限制.

我一直使用诸如sleepusleep 命令之类的代码来限制我的脚本,但这感觉像是一种低效的做事方式,尤其是当 API 端点具有相当高的速率限制并敲击 API 直到你命中时限制也是低效的。

例如,Google 的 API 限制因您使用的 API 而异,并且可以增加/减少,在这种情况下,硬编码到代码中的固定速率限制看起来像是原始的猜测工作!

我是否遗漏了一些非常明显的东西?还是这不像我预期的那么普遍?

【问题讨论】:

  • 你可以实现一个队列:将消息/动作放入队列,执行动作直到达到速率限制,解释限制消息并调整相同类型的队列消息,以便它们留在队列中直到你被允许再次处理消息。
  • 好问题,没有简单的答案。 API:s 中的速率限制可以通过多种方式完成,并且在不同的平台上有所不同。一些 API:s 可以在标头中发送状态响应,例如 Twitter API 发送 [HTTP 429 “Too Many Requests” 响应代码][1],但除此之外,通常 API 客户端很难检测到何时超过限制.赞成这个问题,因为我期待在这个广泛的主题上阅读其他可能有智能解决方案/cmets的答案。 [1]:dev.twitter.com/rest/public/rate-limiting
  • 谢谢两位,我一直按照你的建议 @NorbertvanNobelen 做过,但感觉有点原始。例如,谷歌对其某些 API 有每日限制和每秒限制,它更像是“每 X 秒/分钟”,这将是很好的管理,达到每日限制更多的是软件设计问题。我假设一个内存后端具有一个简单的接口,可以在需要时/在需要时处理暂停? (我想这可能很简单,我只是不想重新发明轮子!)。
  • +1。目前也在研究这个。想法是使用 Beanstalkd 等工作队列来解决秒/分钟限制。由于您的帖子被标记为laravel,因此queue 文档可能会引起您的兴趣。 Redis with Celery 上的另一篇文章涉及分布式服务器,而不是单个服务器。像 twitter 这样的 spotify API 返回一个 429 响应。循环端点似乎很脏,排队应该很好,也许只有在达到限制时才暂停队列。

标签: php laravel


【解决方案1】:

好吧,为了搞笑,我整理了一个限制器类,它允许您指定每秒、每分钟和每小时的限制。我无法抗拒使用循环队列的充分理由!

如果您有多个进程进行消费,无论是否同时进行,您都必须设计一种方法来自行存储和/或共享使用历史记录。

// LIMITER.PHP
class Limiter
{
  private $queue = array();
  private $size;
  private $next;

  private $perSecond;
  private $perMinute;
  private $perHour;

  // Set any constructor parameter to non-zero to allow adherence to the
  // limit represented. The largest value present will be the size of a
  // circular queue used to track usage.
  // -------------------------------------------------------------------
  function __construct($perSecond=0,$perMinute=0,$perHour=0)
  {
    $this->size = max($perSecond,$perMinute,$perHour);
    $this->next = 0;

    $this->perSecond = $perSecond;
    $this->perMinute = $perMinute;
    $this->perHour   = $perHour;

    for($i=0; $i < $this->size; $i++)
      $this->queue[$i] = 0;
  }

  // See if a use would violate any of the limits specified. We return true
  // if a limit has been hit.
  // ----------------------------------------------------------------------
  public function limitHit($verbose=0)
  {    
    $inSecond = 0;
    $inMinute = 0;
    $inHour   = 0;

    $doneSecond = 0;
    $doneMinute = 0;
    $doneHour   = 0;

    $now = microtime(true);

    if ( $verbose )
      echo "Checking if limitHit at $now<br>\n";

    for ($offset=1; $offset <= $this->size; $offset++)
    {
      $spot = $this->next - $offset;
      if ( $spot < 0 )
        $spot = $this->size - $offset + $this->next;

      if ( $verbose )
        echo "... next $this->next size $this->size offset $offset spot $spot utime " . $this->queue[$spot] . "<br>\n";

      // Count and track within second
      // -----------------------------
      if ( $this->perSecond && !$doneSecond && $this->queue[$spot] >= microtime(true) - 1.0 )
        $inSecond++;
      else
        $doneSecond = 1;

      // Count and track within minute
      // -----------------------------
      if ( $this->perMinute && !$doneMinute && $this->queue[$spot] >= microtime(true) - 60.0 )
        $inMinute++;
      else
        $doneMinute = 1;

      // Count and track within hour
      // ---------------------------
      if ( $this->perHour && !$doneHour && $this->queue[$spot] >= microtime(true) - 3600.0 )
        $inHour++;
      else
        $doneHour = 1;

      if ( $doneSecond && $doneMinute && $doneHour )
        break;
    }

    if ( $verbose )
      echo "... inSecond $inSecond inMinute $inMinute inHour $inHour<br>\n";

    if ( $inSecond && $inSecond >= $this->perSecond )
    {
      if ( $verbose )
        echo "... limit perSecond hit<br>\n";
      return TRUE;
    }
    if ( $inMinute && $inMinute >= $this->perMinute )
    {
      if ( $verbose )
        echo "... limit perMinute hit<br>\n";
      return TRUE;
    }
    if ( $inHour   && $inHour   >= $this->perHour   )
    {
      if ( $verbose )
        echo "... limit perHour hit<br>\n";
      return TRUE;
    }

    return FALSE;
  }

  // When an API is called the using program should voluntarily track usage
  // via the use function.
  // ----------------------------------------------------------------------
  public function usage()
  {
    $this->queue[$this->next++] = microtime(true);
    if ( $this->next >= $this->size )
      $this->next = 0;
  }
}

// ##############################
// ### Test the limiter class ###
// ##############################

$psec = 2;
$pmin = 4;
$phr  = 0;

echo "Creating limiter with limits of $psec/sec and $pmin/min and $phr/hr<br><br>\n";
$monitorA = new Limiter($psec,$pmin,$phr);

for ($i=0; $i<15; $i++)
{
  if ( !$monitorA->limitHit(1) )
  {
    echo "<br>\n";
    echo "API call A here (utime " . microtime(true) . ")<br>\n";
    echo "Voluntarily registering usage<br>\n";
    $monitorA->usage();
    usleep(250000);
  }
  else
  {
    echo "<br>\n";
    usleep(500000);
  }
}

为了实际演示它,我在限制检查函数中加入了一些“详细模式”语句。这是一些示例输出。

Creating limiter with limits of 2/sec and 4/min and 0/hr

Checking if limitHit at 1436267440.9957
... next 0 size 4 offset 1 spot 3 utime 0
... inSecond 0 inMinute 0 inHour 0

API call A here (utime 1436267440.9957)
Voluntarily registering usage
Checking if limitHit at 1436267441.2497
... next 1 size 4 offset 1 spot 0 utime 1436267440.9957
... next 1 size 4 offset 2 spot 3 utime 0
... inSecond 1 inMinute 1 inHour 0

API call A here (utime 1436267441.2497)
Voluntarily registering usage
Checking if limitHit at 1436267441.5007
... next 2 size 4 offset 1 spot 1 utime 1436267441.2497
... next 2 size 4 offset 2 spot 0 utime 1436267440.9957
... next 2 size 4 offset 3 spot 3 utime 0
... inSecond 2 inMinute 2 inHour 0
... limit perSecond hit

Checking if limitHit at 1436267442.0007
... next 2 size 4 offset 1 spot 1 utime 1436267441.2497
... next 2 size 4 offset 2 spot 0 utime 1436267440.9957
... next 2 size 4 offset 3 spot 3 utime 0
... inSecond 1 inMinute 2 inHour 0

API call A here (utime 1436267442.0007)
Voluntarily registering usage
Checking if limitHit at 1436267442.2507
... next 3 size 4 offset 1 spot 2 utime 1436267442.0007
... next 3 size 4 offset 2 spot 1 utime 1436267441.2497
... next 3 size 4 offset 3 spot 0 utime 1436267440.9957
... next 3 size 4 offset 4 spot 3 utime 0
... inSecond 1 inMinute 3 inHour 0

API call A here (utime 1436267442.2507)
Voluntarily registering usage
Checking if limitHit at 1436267442.5007
... next 0 size 4 offset 1 spot 3 utime 1436267442.2507
... next 0 size 4 offset 2 spot 2 utime 1436267442.0007
... next 0 size 4 offset 3 spot 1 utime 1436267441.2497
... next 0 size 4 offset 4 spot 0 utime 1436267440.9957
... inSecond 2 inMinute 4 inHour 0
... limit perSecond hit

Checking if limitHit at 1436267443.0007
... next 0 size 4 offset 1 spot 3 utime 1436267442.2507
... next 0 size 4 offset 2 spot 2 utime 1436267442.0007
... next 0 size 4 offset 3 spot 1 utime 1436267441.2497
... next 0 size 4 offset 4 spot 0 utime 1436267440.9957
... inSecond 2 inMinute 4 inHour 0
... limit perSecond hit

Checking if limitHit at 1436267443.5027
... next 0 size 4 offset 1 spot 3 utime 1436267442.2507
... next 0 size 4 offset 2 spot 2 utime 1436267442.0007
... next 0 size 4 offset 3 spot 1 utime 1436267441.2497
... next 0 size 4 offset 4 spot 0 utime 1436267440.9957
... inSecond 0 inMinute 4 inHour 0
... limit perMinute hit

Checking if limitHit at 1436267444.0027
... next 0 size 4 offset 1 spot 3 utime 1436267442.2507
... next 0 size 4 offset 2 spot 2 utime 1436267442.0007
... next 0 size 4 offset 3 spot 1 utime 1436267441.2497
... next 0 size 4 offset 4 spot 0 utime 1436267440.9957
... inSecond 0 inMinute 4 inHour 0
... limit perMinute hit

【讨论】:

  • 顺便说一下,如果有人想在项目中直接使用代码,但在对明确许可有严格规定的环境中,我在 Github 上有这个(具有令人难以置信的开放 MIT 许可证)。 github.com/gvroom/snippets
【解决方案2】:

首先,您应该只在实际需要时调用任何外部 API,供应商将非常感谢您。

我通常通过两种方式“强加”我自己的 API 使用限制 - 如果可能,将结果缓存 N 时间,通常比 API 本身的硬限制少很多。但是,这仅适用于非常特殊的情况。

第二个是持久/半持久计数器,您可以将计数器与限制期开始的时间一起存储在某种内存后端中。每次调用 API 之前检查存储,看看当前时间减去间隔开始和你已经发出的请求数是否小于 API 强加的。如果是,你可以发出请求——如果间隔较大,你可以重置限制,如果你的下一个请求将超过限制,而你仍然在上一个间隔,你可以显示一个漂亮的错误。在每个外部请求上,如果超过了间隔时间,则更新间隔时间并增加计数器。

【讨论】:

    【解决方案3】:
    1. 使用Jobs 包装您的 API 调用并将它们推送到单独的队列:

      ApiJob::dispatch()->onQueue('api');
      
    2. 使用Redismxl/laravel-queue-rate-limit 包(我是作者)使用队列速率限制。另见SO answer about its usage

    3. 如果使用mxl/laravel-queue-rate-limit,则在其设置运行队列工作者之后:

      $ php artisan queue:work --queue api
      

    【讨论】:

    • 整洁的方法!
    【解决方案4】:

    我想我们不能用几句话回答你的问题。它需要真实反映与您的应用程序相关的架构。为了对重复进行 API 速率限制,我使用了存储值和 API 利用率的缓存。到目前为止,我还没有发现任何代码。

    【讨论】:

      猜你喜欢
      • 2017-01-11
      • 2019-05-13
      • 1970-01-01
      • 2022-11-26
      • 1970-01-01
      • 2017-01-27
      • 2019-05-23
      • 2020-11-04
      • 1970-01-01
      相关资源
      最近更新 更多