Як я побудував 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 секунд від поточного моменту. Тут немає меж, які можна було б використати.
Ми використовуємо два різні методи залежно від наших потреб:
- Sliding Window Log (для точності) Для автентифікованих користувачів ми використовуємо Redis Sorted Sets (ZSET). Ми зберігаємо мітку часу для кожного запиту.
- Ми використовуємо
ZREMRANGEBYSCORE, щоб видаляти старі записи. - Ми використовуємо
ZCARD, щоб підрахувати поточні запити. - Ми використовуємо
ZADD, щоб реєструвати нові.
Щоб запобігти станам гонитви (race conditions), ми запускаємо цю логіку в одному Lua-скрипті. Це робить операцію атомарною. Один скрипт обробляє все за один запит до Redis. Це не дозволяє двом запитам одночасно «думати», що вони не перевищують ліміт.
- Sliding Window Counter (для масштабованості) Для анонімного трафіку у нас мільйони ключів. Ми не можемо зберігати мітку часу для кожного запиту. Замість цього ми використовуємо лічильник, який зважує поточне та попереднє вікна. Це споживає набагато менше пам'яті та є дуже ефективним.
Ключові уроки з продакшену:
- Використовуйте Lua-скрипти. Це гарантує, що перевірка та запис відбуваються як одна цілісна дія.
- Використовуйте стандартні заголовки. Завжди надсилайте
X-RateLimit-RemainingтаRetry-After. Це допомагає коректним клієнтам автоматично зменшити частоту запитів. - Використовуйте стратегію fail open. Якщо Redis недоступний, дозволяйте запит. Перевантажений API кращий за повністю непрацюючий.
- Правильно ідентифікуйте клієнтів. Використовуйте API-ключі для користувачів та хешовані IP-адреси для гостей.
- Підбирайте ліміти відповідно до ендпоінтів. Важкий ендпоінт пошуку потребує суворіших обмежень, ніж простий ендпоінт зі списком.
Результат був миттєвим. Вечірні сплески трафіку зникли. Продуктивність нашої бази даних повернулася до норми.
