ADR-0008: Централизованный rate limiter на Redis
Status: accepted Date: 2026-04-17 Deciders: команда проекта
Контекст
Внешние поставщики (начиная с ETM) имеют строгие rate limits:
- ETM: 1 req/s на
/goods/*, 1 req/2 мин на/user/login. - Другие поставщики — свои.
Превышение → блокировка IP. Неприемлемо.
В будущем — несколько реплик ingestor’а (масштабирование, HA), необходим shared rate state.
Решение
Централизованный rate limiter на Redis, token bucket. Два класса bucket’ов на credential: AuthBucket и DataBucket[endpoint_class].
- Отдельный модуль
platform/rate-limiter. - Конфиг per-supplier, per-credential, per-endpoint.
- Каждый connector получает токен перед исходящим запросом.
- При отсутствии токена — поведение зависит от класса bucket’а (см. ниже).
Два класса bucket’ов
| Класс | Пример (ETM) | Поведение при исчерпании | Почему |
|---|---|---|---|
AuthBucket | login — 1 req / 120s | Блокирующее ожидание, не fail | Проигрыш auth rate приводит к бану IP; запрос должен ждать. Connector не переключает схему auth при rate fail. |
DataBucket[endpoint_class] | price, stock, goods, async_poll — 1 req / 1s у ETM | EnrichmentJobSkipped + reschedule с backoff | Data-запрос можно пропустить — observation станет stale, Pricing graceful degrade. Нет смысла блокировать worker. |
AuthBucket никогда не делит бюджет с DataBucket. Даже при полном исчерпании data-budget — auth запрос проходит (нужен, чтобы восстановить session).
Ключи bucket’ов
AuthBucket:rl:auth:{supplier_id}:{credential_fingerprint}.DataBucket:rl:data:{supplier_id}:{credential_id}:{endpoint_class}.
Fingerprint используется для auth, потому что члены одной SupplierCredentialGroup делят фактические login-попытки (один логин/пароль — один bucket). credential_id используется для data, потому что token rate limit у поставщика чаще считается на активную сессию, не на пару login/pass.
Конфиг example (ETM)
rate_limit:
auth:
login:
rate: 1
period: 120s
mode: wait # блокирующее ожидание
data:
price:
rate: 1
period: 1s
mode: skip_and_reschedule
backoff: { initial: 500ms, max: 30s, factor: 2 }
goods:
rate: 1
period: 1s
mode: skip_and_reschedule
remains:
rate: 1
period: 1s
mode: skip_and_reschedule
async_poll:
rate: 2
period: 1s
mode: skip_and_rescheduleОтдельный EgressIPBucket (опционально)
Для поставщиков, проверяющих IP-клиента (ETM требует письмо для foreign hosting), добавляется информационный EgressIPBucket{ip} — не ограничивает, только собирает метрики, чтобы заметить rotate IP / неверный deploy.
Последствия
Плюсы
- Единая точка контроля.
- Корректно работает на нескольких репликах.
- Конфиг в одном месте.
- Централизованные метрики: “запросы к ETM в минуту”, “rate limit hits”.
Минусы
- Redis становится критичным компонентом для ingestion (mitigate: при недоступности Redis — fail-safe с локальным limiter, но более консервативным).
- Дополнительный network hop на каждый запрос (mitigate: client-side prefetch токенов пачками).
Нейтральные последствия
- Можно расширить до API rate limiting для нашего собственного API.
Рассмотренные альтернативы
Локальный rate limiter в каждой реплике
Ломается при > 1 реплики.
DB-based (PostgreSQL as limit store)
Слишком медленно для per-request check.
Envoy / API gateway rate limit
Overkill на текущей фазе, не даёт нужного уровня контроля per-endpoint.
Ссылки
- Principles: P7.
../integration-patterns.md- ADR-0024 (supplier connector contract) — двухуровневый rate budget как часть обязательного контракта connector.