Как я создал Rate Limiter на основе скользящего окна в Redis
Наш видео-API раньше падал каждый вечер в 20:00 UTC.
Это был не реальный трафик. Несколько скраперов нашли наш эндпоинт с трендами и начали засыпать его запросами. Запросы к нашей базе данных накапливались в очереди. Реальные пользователи видели индикаторы загрузки вместо видео.
Нам нужно было ограничение частоты запросов (rate limiting). Большинство простых методов только усугубляли проблему.
Проблема счетчиков с фиксированным окном (Fixed Window):
Большинство начинают с фиксированных окон. Вы считаете запросы в определенном временном блоке. Если ваш лимит — 100 запросов в минуту, пользователь может отправить 100 запросов в 11:59:59 и еще 100 в 12:00:00. Это 200 запросов за одну секунду. Скраперы используют эти границы, чтобы обходить ваши лимиты.
Решение: Скользящее окно (Sliding Window).
Скользящее окно считает запросы за последние N секунд относительно текущего момента. Здесь нет границ, которые можно использовать для обхода.
Мы используем два разных метода в зависимости от наших потребностей:
1. Лог скользящего окна (Sliding Window Log — для точности)
Мы используем Redis Sorted Sets (ZSET) для аутентифицированных пользователей. Мы сохраняем временную метку (timestamp) для каждого запроса.
- Мы используем
ZREMRANGEBYSCORE, чтобы удалять старые записи. - Мы используем
ZCARD, чтобы считать текущие запросы. - Мы используем
ZADD, чтобы записывать новые.
Чтобы предотвратить состояние гонки (race conditions), мы запускаем эту логику в одном Lua-скрипте. Это делает операцию атомарной. Один скрипт выполняет всё за один проход к Redis. Это предотвращает ситуацию, когда два запроса одновременно считают, что они не превышают лимит.
2. Счетчик скользящего окна (Sliding Window Counter — для масштабируемости)
Для анонимного трафика у нас миллионы ключей. Мы не можем хранить каждую временную метку. Вместо этого мы используем счетчик, который взвешивает текущее и предыдущее окна. Это потребляет гораздо меньше памяти и работает очень эффективно.
Ключевые уроки из продакшена:
- Используйте Lua-скрипты. Это гарантирует, что проверка и запись происходят как одно единое действие.
- Выпускайте стандартные заголовки. Всегда отправляйте
X-RateLimit-RemainingиRetry-After. Это помогает корректным клиентам автоматически снижать нагрузку. - Принцип "fail open". Если Redis упадет, разрешайте запрос. Перегруженный API лучше, чем полностью неработающий.
- Правильно идентифицируйте клиентов. Используйте API-ключи для пользователей и хешированные IP для гостей.
- Соответствуйте лимиты эндпоинтам. Тяжелому эндпоинту поиска нужны более строгие лимиты, чем простому эндпоинту со списком.
Результат был мгновенным. Вечерние скачки исчезли. Производительность нашей базы данных вернулась в норму.
