这实际上是从我之前的一个问题开始的,遗憾的是,没有得到任何答案,所以我并没有完全屏住呼吸,但我知道这可能是一个棘手的问题需要解决。 / p>
我目前正在尝试对外部API的传出请求实施速率限制,以匹配其结束时的限制。我试图在我们用来管理这个特定API的Guzzle请求的类中实现一个令牌桶库(https://github.com/bandwidth-throttle/token-bucket)。
最初,这似乎按预期工作,但我们现在已经开始看到来自API的429个响应,因为它似乎不再正确地限制请求。
我感觉发生的事情是,由于Symfony处理服务的方式,每次调用API时,存储桶中的令牌数量现在都会被重置。
我正在设置当前在服务构造函数中设置存储桶位置,速率和起始量:
public function __construct()
{
$storage = new FileStorage(__DIR__ . "/api.bucket");
$rate = new Rate(50, Rate::MINUTE);
$bucket = new TokenBucket(50, $rate, $storage);
$this->consumer = new BlockingConsumer($bucket);
$bucket->bootstrap(50);
}
然后我在每次请求之前尝试使用令牌:
public function fetch(): array
{
try {
$this->consumer->consume(1);
$response = $this->client->request(
'GET', $this->buildQuery(), [
'query' => array_merge($this->params, ['api_key' => $this->apiKey]),
'headers' => [ 'Content-type' => 'application/json' ]
]
);
} catch (ServerException $e) {
// Process Server Exception
} catch (ClientException $e) {
// Process Client Exception
}
return $this->checkResponse($response);
}
我看不到任何显而易见的事情,除非每次请求都重置可用令牌数量,否则每分钟要求超过50次。
这是提供给一组存储库服务,用于处理将每个端点的数据转换为系统中使用的对象。消费者使用适当的存储库来请求完成其过程所需的数据。
如果引导函数正在服务构造函数中重置令牌数量,那么它应该在Symfony框架内移动到哪个仍然可以与消费者一起使用?
答案 0 :(得分:0)
我认为它应该可行,但可能会尝试从每个请求移动->bootstrap(50)
来电?不确定,但可能是原因。
无论如何,作为部署的一部分(每次部署新版本)时,最好只执行一次。它与Symfony没有任何关系,因为框架对部署过程没有任何限制。所以这取决于你如何进行部署。
P.S。您是否考虑过只处理来自服务器的429个错误?当你收到429错误时,IMO你可以等待(这是BlockingConsumer
里面的内容)。它更简单,不需要系统中的附加层。
答案 1 :(得分:0)
您可以在代码和目标Web服务后面放置一个nginx代理,并对其启用限制。然后在您的代码中,您将像往常一样处理429,但请求将受到本地nginx代理的限制,而不是外部Web服务。因此,最终目的地将只获得有限数量的请求。
答案 2 :(得分:0)
我找到了一个使用Guzzle bundle for symfony的技巧。
我必须改进向Google API发送GET
请求的顺序程序。在代码示例中,它是一个pagespeed URL。
要获得速率限制,可以选择在异步发送请求之前延迟请求。
Pagespeed速率限制是每分钟200个请求。
快速计算可为每个请求提供200/60 = 0.3秒。
这是我在300个网址上测试的代码,得到了一个没有错误的奇妙结果,除非在GET
请求中作为参数传递的url给出了400 HTTP错误(错误请求)。
我将延迟时间设为0.4秒,平均结果时间小于0.2秒,而连续程序花费的时间超过一分钟。
use GuzzleHttp;
use GuzzleHttp\Client;
use GuzzleHttp\Promise\EachPromise;
use GuzzleHttp\Exception\ClientException;
// ... Now inside class code ... //
$client = new GuzzleHttp\Client();
$promises = [];
foreach ($requetes as $i=>$google_request) {
$promises[] = $client->requestAsync('GET', $google_request ,['delay'=>0.4*$i*1000]); // delay is the trick not to exceed rate limit (in ms)
}
GuzzleHttp\Promise\each_limit($promises, function(){ // function returning the number of concurrent requests
return 100; // 1 or 100 concurrent request(s) don't really change execution time
}, // Fulfilled function
function ($response,$index)use($urls,$fp) { // $urls is used to get the url passed as a parameter in GET request and $fp a csv file pointer
$feed = json_decode($response->getBody(), true); // Get array of results
$this->write_to_csv($feed,$fp,$urls[$index]); // Write to csv
}, // Rejected function
function ($reason,$index) {
if ($reason instanceof GuzzleHttp\Exception\ClientException) {
$message = $reason->getMessage();
var_dump(array("error"=>"error","id"=>$index,"message"=>$message)); // You could write the errors to a file or database too
}
})->wait();