Это на самом деле вытекает из предыдущего вопроса, который у меня был, к сожалению, я не получил никаких ответов, поэтому я не совсем затаив дыхание, но понимаю, что решить эту проблему может быть немного сложнее.
В настоящее время я пытаюсь реализовать ограничение скорости для исходящих запросов к внешнему API, чтобы соответствовать пределу на их конце. Я попытался реализовать библиотеку токенов (https://github.com/bandwidth-throttle/token-bucket) в класс, который мы используем для управления запросами Guzzle для этого конкретного API.
Первоначально, это, казалось, работало как задумано, но теперь мы начали видеть 429 ответов от API, поскольку это, кажется, больше не корректно ограничивает количество запросов.
У меня есть ощущение, что происходит то, что число токенов в корзине теперь сбрасывается при каждом вызове API из-за того, как Symfony обрабатывает сервисы.
В настоящее время я устанавливаю расположение корзины, ставку и начальную сумму в конструкторе сервиса:
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, которая все равно будет работать с потребителями?
Я предполагаю, что это должно работать, но, возможно, попытаться переместить ->bootstrap(50)
звонить с каждого запроса? Не уверен, но это может быть причиной.
В любом случае, лучше сделать это только один раз, как часть вашего развертывания (каждый раз, когда вы развертываете новую версию). На самом деле это не имеет ничего общего с Symfony, потому что у фреймворка нет никаких ограничений на процедуру развертывания. Так что это зависит от того, как вы делаете развертывание.
Постскриптум Рассматривали ли вы просто обрабатывать 429 ошибок с сервера? ИМО можно подождать (вот что BlockingConsumer
делает внутри), когда вы получите 429 ошибку. Это проще и не требует дополнительного слоя в вашей системе.
Кстати, вы рассматривали nginx ngx_http_limit_req_module как альтернативное решение? Обычно он поставляется с nginx по умолчанию, поэтому никаких дополнительных действий для установки, только небольшая конфигурация не требуется.
Вы можете разместить прокси-сервер nginx за своим кодом и целевым веб-сервисом и включить для него ограничения. Затем в вашем коде вы будете обрабатывать 429 как обычно, но запросы будут обрабатываться вашим локальным прокси-сервером nginx, а не внешним веб-сервисом. Таким образом, конечный пункт получит только ограниченное количество запросов.
Я нашел трюк, используя пакет Guzzle для Symfony.
Я должен был улучшить последовательную программу отправки GET
запросы к API Google. В примере кода это URL скорости страницы.
Для ограничения скорости есть возможность отложить запросы до их асинхронной отправки.
Ограничение скорости страниц составляет 200 запросов в минуту.
Быстрый расчет дает 200/60 = 0,3 с на запрос.
Вот код, который я протестировал на 300 URL-адресах, получая фантастический результат без ошибок, кроме случаев, когда URL-адрес передан в качестве параметра GET
запрос выдает ошибку HTTP 400 (неверный запрос).
Я установил задержку в 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();