Rate Limiting и Throttling в PHP: Обуздайте поток запросов
Представьте: вы запустили крутой API для своего сервиса. Всё работает идеально, пока один «шальной» клиент не начинает долбить ваш сервер тысячами запросов в секунду. Или, что ещё хуже, бот-парсер решает выкачать всю вашу базу данных. Знакомо? Если нет — повезло. Но лучше быть готовым. Именно здесь на сцену выходят rate limiting и throttling — два рыцаря, которые защитят ваш бэкенд от перегрузок.
В этой статье мы разберёмся, как реализовать ограничение запросов в PHP, рассмотрим реальные примеры кода и поймём, какой метод выбрать для вашего проекта. Спойлер: писать свой велосипед не придётся, но базовые алгоритмы знать полезно.
Rate Limiting vs Throttling: в чём разница?
Часто эти термины используют как синонимы, но между ними есть тонкая грань.
- Rate Limiting — это жёсткий лимит. Вы говорите: «Максимум 100 запросов в минуту». Как только лимит исчерпан — клиент получает HTTP 429 Too Many Requests и идёт пить чай.
- Throttling — это более мягкий контроль. Вместо блокировки вы просто замедляете обработку запросов сверх лимита, ставите их в очередь или снижаете приоритет.
На практике throttling в PHP часто реализуют через задержки (sleep) или очереди, а rate limiting — через подсчёт запросов в единицу времени. Для большинства API-шлюзов оптимальна комбинация обоих подходов.
Почему это критически важно для вашего проекта?
Причин внедрить ограничение запросов PHP как минимум три:
- Защита от DDoS и ботов — даже простой скрипт может положить сервер, если не контролировать частоту вызовов.
- Честное распределение ресурсов — один «жадный» клиент не должен лишать сервиса остальных пользователей.
- Стабильность и предсказуемость — вы знаете, какой пиковой нагрузки ждать, и можете правильно настроить инфраструктуру.
Алгоритмы Rate Limiting: выбираем стратегию
Прежде чем писать код, давайте разберём три популярных алгоритма. Каждый имеет свои плюсы и минусы.
1. Fixed Window (Фиксированное окно)
Самый простой подход. Делим время на отрезки (например, по минутам). Запоминаем, сколько запросов пришло в текущем окне. Достиг лимита — блокируем до следующего окна.
Минус: на стыке окон возможен всплеск — клиент может отправить 100 запросов в конце минуты и ещё 100 в начале следующей, что даст 200 за 2 секунды.
2. Sliding Window (Скользящее окно)
Более точный алгоритм. Вместо фиксированных отрезков мы смотрим на последние N секунд. Запросы «стареют» и выпадают из окна. Это исключает всплески на границах.
3. Token Bucket (Ведро токенов)
Элегантный алгоритм из мира сетей. В «ведре» лежат токены. Каждый запрос забирает один токен. Токены пополняются с заданной скоростью. Если ведро пусто — запрос отклоняется. Отлично подходит для throttling php, так как позволяет небольшие всплески трафика.
Практическая реализация: от простого к сложному
Давайте перейдём к коду. Начнём с базового примера на файлах, а затем посмотрим на промышленные решения.
Базовый Rate Limiter на файлах (для обучения)
Этот пример использует файлы для хранения счётчиков. Не годится для продакшена, но отлично демонстрирует принцип.
<?php
class SimpleRateLimiter {
private string $storageDir;
private int $maxRequests;
private int $windowSeconds;
public function __construct(string $storageDir, int $maxRequests = 100, int $windowSeconds = 60) {
$this->storageDir = rtrim($storageDir, '/') . '/';
$this->maxRequests = $maxRequests;
$this->windowSeconds = $windowSeconds;
if (!is_dir($this->storageDir)) {
mkdir($this->storageDir, 0755, true);
}
}
public function check(string $identifier): bool {
$file = $this->storageDir . md5($identifier) . '.lock';
$now = time();
$data = ['count' => 0, 'window_start' => $now];
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
// Если окно истекло — сбрасываем
if ($data['window_start'] + $this->windowSeconds < $now) {
$data = ['count' => 0, 'window_start' => $now];
}
}
$data['count']++;
file_put_contents($file, json_encode($data), LOCK_EX);
return $data['count'] <= $this->maxRequests;
}
}
// Использование
$limiter = new SimpleRateLimiter('/tmp/ratelimit/', 5, 10); // 5 запросов за 10 секунд
$userId = 'user_123';
if ($limiter->check($userId)) {
echo "Запрос разрешён";
} else {
http_response_code(429);
echo "Слишком много запросов. Подождите.";
}
?>
Этот код — классический fixed window rate limiting php. Он рабочий, но для серьёзных проектов используйте Redis.
Промышленный подход: Rate Limiter на Redis
Redis идеален для rate limiting благодаря атомарным операциям и встроенному TTL. Вот пример реализации алгоритма sliding window log.
<?php
class RedisRateLimiter {
private \Redis $redis;
private int $maxRequests;
private int $windowSeconds;
public function __construct(\Redis $redis, int $maxRequests = 100, int $windowSeconds = 60) {
$this->redis = $redis;
$this->maxRequests = $maxRequests;
$this->windowSeconds = $windowSeconds;
}
public function allow(string $key): bool {
$now = microtime(true);
$windowStart = $now - $this->windowSeconds;
$redisKey = "ratelimit:{$key}";
// Удаляем старые записи и добавляем текущую
$this->redis->multi();
$this->redis->zRemRangeByScore($redisKey, 0, $windowStart);
$this->redis->zAdd($redisKey, $now, "{$now}");
$this->redis->expire($redisKey, $this->windowSeconds + 1);
$this->redis->exec();
// Считаем количество запросов в окне
$count = $this->redis->zCard($redisKey