Как регулировать количество пользователей API моего сайта?
Законные пользователи моего сайта иногда забивают сервер запросами API, которые приводят к нежелательным результатам. Я хочу установить ограничение не более, чем, скажем, один вызов API каждые 5 секунд или n вызовов в минуту (точный предел еще не определен). Очевидно, что я мог бы регистрировать каждый вызов API в БД и выполнять вычисления для каждого запроса, чтобы увидеть, не превышен ли он, но все эти дополнительные издержки по КАЖДОМУ запросу будут побеждать цель. Какие еще менее ресурсоемкие методы я мог бы использовать, чтобы установить предел? Я использую PHP/Apache/Linux, для чего это стоит.
8 ответов
Хорошо, нет способа сделать то, что я просил, без каких-либо записей на сервер, но я могу, по крайней мере, исключить ведение журнала каждого отдельного запроса. Одним из способов является использование метода регулирования "неплотное ведро", при котором он отслеживает только последний запрос ($last_api_request
) и соотношение количества запросов / лимита за период времени ($minute_throttle
). Протекающая корзина никогда не сбрасывает свой счетчик (в отличие от дросселя API Twitter, который сбрасывается каждый час), но если корзина заполняется (пользователь достиг предела), они должны ждать 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 );
Вы можете управлять скоростью с помощью алгоритма корзины с токенами, который сопоставим с алгоритмом с утечкой памяти. Обратите внимание, что вам придется делиться состоянием корзины (то есть количеством токенов) с процессами (или с любой областью, которую вы хотите контролировать). Так что вы можете подумать о блокировке, чтобы избежать условий гонки.
Хорошие новости: я сделал все это для вас: https://github.com/bandwidth-throttle/token-bucket
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();
}
Простейшим решением было бы просто дать каждому API-ключу ограниченное количество запросов в сутки и сбросить их в известное фиксированное время.
Если они исчерпывают свои запросы API (т. Е. Счетчик достигает нуля или предела, в зависимости от того, в каком направлении вы рассчитываете), прекратите предоставлять им данные, пока вы не сбросите их счетчик.
Таким образом, в их интересах не забивать вас запросами.
Я не знаю, жив ли этот поток или нет, но я бы посоветовал сохранить эту статистику в кеше памяти, как memcached. Это уменьшит накладные расходы на регистрацию запроса в БД, но все же послужит цели.
Помимо реализации с нуля, вы также можете взглянуть на API-инфраструктуру, такую как 3scale ( http://www.3scale.net/), которая ограничивает скорость, а также на множество других вещей (аналитика и т. Д.). Для этого есть плагин PHP: https://github.com/3scale/3scale_ws_api_for_php.
Вы также можете вставить что-то вроде Varnish напротив API и сделать ограничение скорости API таким образом.
Вы говорите, что "все лишние затраты по КАЖДОМУ запросу будут побеждать цель", но я не уверен, что это правильно. Разве цель не состоит в том, чтобы предотвратить стук вашего сервера? Это, вероятно, способ, которым я реализовал бы это, поскольку это действительно только требует быстрого чтения / записи. Вы можете даже отправлять проверки сервера API на другую БД / диск, если беспокоитесь о производительности.
Однако, если вам нужны альтернативы, вы должны проверить mod_cband, сторонний модуль apache, разработанный для помощи в регулировании пропускной способности. Несмотря на то, что он предназначен главным образом для ограничения пропускной способности, он также может регулироваться на основе запросов в секунду. Я никогда не использовал его, поэтому я не уверен, какие результаты вы получите. Был и другой модуль, называемый mod-throttle, но этот проект, похоже, сейчас закрыт, и никогда не был выпущен ни для чего, кроме Apache 1.3.
В узле js есть пакет с именем expess-rate-limiter , который делает именно то, что вы пытаетесь выполнить.
Это ограничивает количество запросов за определенный период времени. Я не знаю, есть ли у нас то же самое в PHP.
Разве нельзя сделать это просто с помощью сеанса? Сравнить microtime () с $ _SESSION ['last_access_microtime']?