consilience/api-rate-monitor

通过PSR-18客户端在一个滚动窗口中监控请求

dev-master 2019-12-31 03:53 UTC

This package is auto-updated.

Last update: 2024-08-29 05:24:35 UTC


README

最初为Xero客户编写,此包是一个PSR-18装饰器,用于监控和统计API请求,并提供实施限流的所需数据。

此包本身不执行限流,但提供请求接近速率限制的详细信息,以便采取行动。行动可能包括暂停进程。它可能涉及在稍后时间重新调度进程。

作为一个PSR-18装饰器,可以分层多个规则,因此例如可以同时监控基于分钟的速率限制和基于小时的速率限制。

使用PSR-6缓存池来缓存记录请求的任何时间序列。缓存键由应用程序提供。目前客户端装饰器实例仅处理一个不可变的缓存键。未来的版本可能允许通过PSR-7请求中的元数据动态设置键,这可以由应用程序从任何来源提供。

为Xero API编写,为API请求的每个组织设置速率限制。因此在这种情况下,使用组织ID作为缓存键是有意义的。任何进一步的前缀都需要应用程序来区分这些缓存键和其他缓存项。

滚动窗口监控策略

目前只实现了滚动窗口速率限制策略,但可以插入其他策略,并且欢迎提交拉取请求。

此策略跟踪滚动时间窗口内的每秒请求。Xero允许在任何60秒滚动窗口内发送60个请求。如果在过去60秒内已发送60个请求,则另一个请求将导致速率限制拒绝。

让我们来看看如何保护应用程序免受这种影响。

首先我们需要一个缓存来跟踪请求,我们使用一个 PSR-6 缓存。如果使用Laravel,则 madewithlove/illuminate-psr-cache-bridge 包将Laravel缓存很好地桥接到 PSR-6 接口。

因此我们获取缓存池

$cachePool = new MyFavouriteCachePool();

// or inject it into your class if using laravel:

public function __construct(CacheItemPoolInterface $cachePool) {
   ...
}

现在我们需要 PSR-18 客户端。客户端可以按您喜欢的任何方式实例化。我使用 此Xero API客户端 来处理对Xero的认证,但您使用的任何 PSR-18 客户端都没有关系。

我们将客户端设置为 $httpClient

现在我们使用装饰器

use Consilience\Api\RateMonitor\HttpClient;
use Consilience\Api\RateMonitor\MonitorStrategy\RollingWindow;

$httpClient = {{ your base PSR-18 HTTP client }}

// $xeroOrganisationId is the Xero organisation we are connecting to.
// 60 = size of rolling window, in seconds.
// 60 = number of requests that can be made in that window.
// A bit of a safety margin could see the number of requests
// set to a lower figure, 55 for example.

$httpClient = new HttpClient(
   $httpClient,
   $cachePool,
   new RollingWindow(60, 60)
)->withKey($xeroOrganisationId);

装饰后的 $httpClient 可以像以前一样使用,但有一些额外的在 Psr\Http\Client\ClientInterface 接口之上的方法

  • getAllocationUsed() - 返回当前滚动窗口中已发送的请求数量。
  • getWaitSeconds() - 告诉我们在发送更多请求之前需要等待多少秒。

对于 getWaitSeconds(),您可以指定您希望快速发送的请求数量,即爆发发送,它将返回发送该数量请求所需的等待时间。

因此,一种使用此方法的方法是在发送下一个请求之前暂停

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

function sendRequest(
    ClientInterface $httpClient,
    RequestInterface $request
): ResponseInterface
{
    if ($waitSeconds = $httpClient->getWaitSeconds()) {
        sleep($waitSeconds);
    }

    return $httpClient->send($request);
}

这是一个简单的方法,并假设进程可以简单地暂停很长时间而不会丢失数据库和其他连接,但作为一个简单的例子。

另一种策略可能是更均匀地分散请求,每个请求之间保持最小时间间隔,并不断调整睡眠延迟,使其保持在半分配的滚动窗口附近。这样可以将睡眠时间缩短到最小,同时允许需要最多几十个请求的过程快速进行短时间爆发。

滚动窗口日志的工作原理

简而言之,每个键指向缓存中的一个数组。该数组包含每个请求的秒数内请求的计数。通过时间戳索引,我们可以看到最后一个滚动窗口中所有请求的执行时间。

在任何时候,可以将最后一个滚动窗口期间的请求计数相加,以获取当前滚动窗口中请求的数量。这告诉我们,在API速率限制启动之前,现在可以发起多少请求。

鉴于这一点,如果我们现在想发起十个请求,我们可以检查当前滚动窗口是否有足够的空闲插槽来执行这些操作。如果有,那么就没有问题,可以直接发起这些请求。

现在,如果剩余的插槽不足 - 滚动窗口可能每分钟允许60个请求,而在上一分钟我们已经发出了55个请求,因此我们需要找出在发起那十个请求之前,需要多少个插槽才能释放。

我们通过从当前滚动窗口开始的时间点(在这个案例中是60秒前)计算请求,来确定需要释放的插槽数量。当我们计算出需要释放的插槽数量时,我们可以看到这代表的时间。假设那些最旧的五个插槽是在30秒前被占用的,那么这意味着这些插槽将在60秒后完全释放,也就是未来30秒。因此,这个过程需要等待30秒才能发送那十个请求。

如果过程只想发送一个请求,那么它可能只需要等待更短的时间。然而,这实际上取决于请求的过去模式,即它们是如何分散或聚集的。

待办事项

  • 测试。
  • 支持动态键检测。如果没有设置键的单个客户端可以按请求逐个支持对不同键的请求。否则,我们需要为每个键创建一个新的装饰器类。这也可能可以,取决于应用程序如何组织其请求。
  • 支持节流策略插件。允许此包根据类中定义的规则进行节流。
  • 处理更新缓存项时的锁定。