Symfony中的限速Guzzle请求

时间:2017-08-07 16:07:28

标签: php symfony guzzle rate-limiting

这实际上是从我之前的一个问题开始的,遗憾的是,没有得到任何答案,所以我并没有完全屏住呼吸,但我知道这可能是一个棘手的问题需要解决。 / 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框架内移动到哪个仍然可以与消费者一起使用?

3 个答案:

答案 0 :(得分:0)

我认为它应该可行,但可能会尝试从每个请求移动->bootstrap(50)来电?不确定,但可能是原因。

无论如何,作为部署的一部分(每次部署新版本)时,最好只执行一次。它与Symfony没有任何关系,因为框架对部署过程没有任何限制。所以这取决于你如何进行部署。

P.S。您是否考虑过只处理来自服务器的429个错误?当你收到429错误时,IMO你可以等待(这是BlockingConsumer里面的内容)。它更简单,不需要系统中的附加层。

答案 1 :(得分:0)

顺便说一句,你有没有考虑过nginx的ngx_http_limit_req_module作为替代解决方案?它通常默认带有nginx,因此无需安装其他操作,只需要很小的配置。

您可以在代码和目标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();