Як я побудував Rate Limiter із ковзним вікном у Redis

Наш відео-API щовечора о 20:00 UTC падав.

Це не був реальний трафік. Кілька скреперів знайшли наш trending endpoint і почали його «бомбити». Запити до бази даних накопичувалися. Замість відео реальні користувачі бачили лише індикатори завантаження.

Нам потрібен був 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). Ми зберігаємо мітку часу для кожного запиту.
  • Ми використовуємо ZREMRANGEBYSCORE, щоб видаляти старі записи.
  • Ми використовуємо ZCARD, щоб підрахувати поточні запити.
  • Ми використовуємо ZADD, щоб реєструвати нові.

Щоб запобігти станам гонитви (race conditions), ми запускаємо цю логіку в одному Lua-скрипті. Це робить операцію атомарною. Один скрипт обробляє все за один запит до Redis. Це не дозволяє двом запитам одночасно «думати», що вони не перевищують ліміт.

  1. Sliding Window Counter (для масштабованості) Для анонімного трафіку у нас мільйони ключів. Ми не можемо зберігати мітку часу для кожного запиту. Замість цього ми використовуємо лічильник, який зважує поточне та попереднє вікна. Це споживає набагато менше пам'яті та є дуже ефективним.

Ключові уроки з продакшену:

  • Використовуйте Lua-скрипти. Це гарантує, що перевірка та запис відбуваються як одна цілісна дія.
  • Використовуйте стандартні заголовки. Завжди надсилайте X-RateLimit-Remaining та Retry-After. Це допомагає коректним клієнтам автоматично зменшити частоту запитів.
  • Використовуйте стратегію fail open. Якщо Redis недоступний, дозволяйте запит. Перевантажений API кращий за повністю непрацюючий.
  • Правильно ідентифікуйте клієнтів. Використовуйте API-ключі для користувачів та хешовані IP-адреси для гостей.
  • Підбирайте ліміти відповідно до ендпоінтів. Важкий ендпоінт пошуку потребує суворіших обмежень, ніж простий ендпоінт зі списком.

Результат був миттєвим. Вечірні сплески трафіку зникли. Продуктивність нашої бази даних повернулася до норми.

Джерело: https://dev.to/ahmet_gedik778845/how-i-built-a-sliding-window-rate-limiter-for-our-video-api-in-redis-4k1c