consilience / api-rate-monitor
通过PSR-18客户端在一个滚动窗口中监控请求
Requires
- php: ^7.2|^7.3
- php-http/client-implementation: 1.0
- psr/cache-implementation: 1.0
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秒才能发送那十个请求。
如果过程只想发送一个请求,那么它可能只需要等待更短的时间。然而,这实际上取决于请求的过去模式,即它们是如何分散或聚集的。
待办事项
- 测试。
- 支持动态键检测。如果没有设置键的单个客户端可以按请求逐个支持对不同键的请求。否则,我们需要为每个键创建一个新的装饰器类。这也可能可以,取决于应用程序如何组织其请求。
- 支持节流策略插件。允许此包根据类中定义的规则进行节流。
- 处理更新缓存项时的锁定。