我网站的合法用户偶尔会使用导致不良结果的 API 请求来敲击服务器。我想制定一个不超过每 5 秒一次 API 调用或每分钟 n 次调用的限制(还没有弄清楚确切的限制)。我显然可以在数据库中记录每个 API 调用并对每个请求进行计算以查看它们是否超过限制,但是每个请求的所有这些额外开销都会违背目的。我可以使用哪些其他资源较少的方法来制定限制?我正在使用 PHP/Apache/Linux,这是值得的。
7 回答
好的,如果不向服务器写入任何内容,就无法执行我所要求的操作,但我至少可以消除记录每个请求的情况。一种方法是使用“漏桶”节流方法,它只跟踪最后一个请求 ( $last_api_request
) 和时间范围内的请求数/限制的比率 ( $minute_throttle
)。漏桶永远不会重置它的计数器(不像 Twitter API 的油门每小时重置一次),但如果桶变满(用户达到限制),他们必须等待n
几秒钟让桶清空一点,然后才能发出另一个请求。换句话说,它就像一个滚动限制:如果在时间范围内有先前的请求,它们正在慢慢地漏出桶外;只有当你填满水桶时,它才会限制你。
此代码段将为$minute_throttle
每个请求计算一个新值。我指定了分钟,$minute_throttle
因为您可以为任何时间段添加节流阀,例如每小时、每天等……尽管多个节流阀很快就会开始让用户感到困惑。
$minute = 60;
$minute_limit = 100; # users are limited to 100 requests/minute
$last_api_request = $this->get_last_api_request(); # get from the DB; in epoch seconds
$last_api_diff = time() - $last_api_request; # in seconds
$minute_throttle = $this->get_throttle_minute(); # get from the DB
if ( is_null( $minute_limit ) ) {
$new_minute_throttle = 0;
} else {
$new_minute_throttle = $minute_throttle - $last_api_diff;
$new_minute_throttle = $new_minute_throttle < 0 ? 0 : $new_minute_throttle;
$new_minute_throttle += $minute / $minute_limit;
$minute_hits_remaining = floor( ( $minute - $new_minute_throttle ) * $minute_limit / $minute );
# can output this value with the request if desired:
$minute_hits_remaining = $minute_hits_remaining >= 0 ? $minute_hits_remaining : 0;
}
if ( $new_minute_throttle > $minute ) {
$wait = ceil( $new_minute_throttle - $minute );
usleep( 250000 );
throw new My_Exception ( 'The one-minute API limit of ' . $minute_limit
. ' requests has been exceeded. Please wait ' . $wait . ' seconds before attempting again.' );
}
# Save the values back to the database.
$this->save_last_api_request( time() );
$this->save_throttle_minute( $new_minute_throttle );
您可以使用令牌桶算法控制速率,该算法与漏桶算法相当。请注意,您必须在进程(或您想要控制的任何范围)上共享存储桶的状态(即令牌的数量)。因此,您可能需要考虑锁定以避免竞争条件。
好消息:我为你做了所有这些:带宽限制/令牌桶
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();
}
最简单的解决方案是每 24 小时为每个 API 密钥提供有限数量的请求,并在某个已知的固定时间重置它们。
如果他们用尽了 API 请求(即计数器达到零或限制,具体取决于您计数的方向),请停止为他们提供数据,直到您重置他们的计数器。
这样一来,他们最好不要对你提出要求。
我不知道这个线程是否还活着,但我建议将这些统计信息保存在内存缓存中,如 memcached。这将减少将请求记录到数据库的开销,但仍然可以达到目的。
您说“每个请求的所有额外开销都会违背目的”,但我不确定这是正确的。目的不就是为了防止你的服务器被锤击吗?这可能是我实现它的方式,因为它实际上只需要快速读/写。如果您担心性能,您甚至可以将 API 服务器检查外包给不同的数据库/磁盘。
但是,如果您想要替代方案,您应该查看mod_cband,这是一个旨在帮助限制带宽的第三方 apache 模块。尽管主要用于带宽限制,但它也可以根据每秒请求数进行节流。我从来没有使用过它,所以我不确定你会得到什么样的结果。还有另一个名为 mod-throttle 的模块,但该项目现在似乎已关闭,并且从未针对 Apache 1.3 系列以上的任何内容发布。
除了从头开始实施之外,您还可以查看 API 基础架构,例如 3scale ( http://www.3scale.net ),它会限制速率以及其他一些东西(分析等)。它有一个 PHP 插件:https ://github.com/3scale/3scale_ws_api_for_php 。
你也可以在 API 前面贴上 Varnish 之类的东西,然后像这样限制 API 速率。
难道这不能简单地通过会话来完成吗?
比较。microtime()
_$_SESSION['last_access_microtime']