Как регулировать количество пользователей 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']?

Другие вопросы по тегам