Как контролировать распределённые лимиты запросов в системе с несколькими репликами
Что проверяют
Понимаете ли вы, что локальный лимитер в каждой реплике не держит общий лимит: при пяти репликах клиент получит впятеро больше. Следом обычно спрашивают, что делать, когда хранилище лимитов недоступно, и сколько задержки добавляет проверка.
Как отвечать
Сначала алгоритмы, потом где хранится состояние:
- алгоритмы: token bucket (допускает всплески до ёмкости ведра), fixed window (прост, но на стыке окон пропускает до двойного объёма), sliding window (точнее, но дороже), GCRA (тот же token bucket, но в одном значении времени на ключ);
- локально с делением: каждая реплика держит
rate.Limiterизgolang.org/x/time/rateс лимитом общий/N. Сетевых вызовов ноль, но точность зависит от ровной балансировки и от того, что N обновляется при автомасштабировании; - общее хранилище: Redis, где проверка и списание делаются атомарно одним Lua-скриптом через
EVALSHA. Для fixed window хватаетINCRиEXPIRE, для sliding window подойдёт sorted set сZREMRANGEBYSCOREиZCARD. В Go есть готоваяgo-redis/redis_rateна GCRA; - шардирование по ключу: балансировщик с консистентным хешированием шлёт все запросы клиента на одну реплику, и локального лимитера достаточно;
- гибрид: реплика забирает из Redis пачку токенов и тратит их локально, сетевой вызов нужен не на каждый запрос.
Обязательно проговорите отказ хранилища. Fail-open пропускает трафик без лимита и бережёт доступность, fail-closed бережёт бэкенд, но превращает сбой Redis в сбой API. Часто выбирают fail-open с грубым локальным лимитом как страховкой. Время для окон берите из Redis, а не с реплик: часы на разных машинах расходятся.
Где спотыкаются
Делают GET, потом SET из Go-кода. Между ними успевают пройти запросы с других реплик, и лимит пробивается ровно под нагрузкой. Проверка и списание должны быть одной атомарной операцией.
Пишут самодельный token bucket, который отбрасывает дробную часть прошедшего времени и тут же сдвигает отметку пополнения. При частых запросах токены не пополняются вообще.
Предлагают ZooKeeper или etcd как счётчик на каждый запрос. Это хранилища для координации, запись там идёт через консенсус и такую частоту не выдерживает.