Всем привет! Кажется, настало время поговорить о том, как внедрялись ограничители частоты запросов на бэкенд в Wildberries. В статье — о том, с какими трудностями мы столкнулись на этом благородном пути и как прошли через четыре схемы реализации — от простейшей in-memory до собственных gRPC-сервисов. Не обойдём вниманием и парочку лайфхаков ;) Например, с помощью рейтлимитов мы неожиданно решили проблему плавного отключения старых версий API.

Немного обо мне. Меня зовут Дмитрий Виноградов, и я лид команды публичного API Wildberries. До этого почти 18 лет занимался промышленной автоматизацией в Schneider Electric — от программирования контроллеров и embedded-устройств до собственных SCADA-систем. Хочешь не хочешь, а научишься делать красивые интерфейсы :)

Эта статья написана по мотивам моего выступления на GolangConf. Если вам удобнее воспринимать на слух, на YouTube лежит запись выступления.

Оглавление

Контекст Wildberries

Наверняка вы понимаете, что Wildberries — компания с огромным охватом: мы работаем по всей России, в Беларуси, Казахстане, Кыргызстане, Армении, Узбекистане, а для продавцов — даже в Китае.

В день через платформу проходит больше 20 млн заказов (на 2025 год). При таком объёме цена любой ошибки очень высока. Это и прямые убытки (например, если продавец не может обновить остатки или обработать заказ), и репутационные издержки. А ещё любой сбой бьёт по нервам всех участников процесса, включая разработчиков.

API Wildberries — один из трёх способов управления магазином на платформе. Помимо API, продавцам доступно управление через веб-интерфейс и мобильное приложение.

API особенно популярен у крупных продавцов: представьте, у вас тысячи товаров, цены и остатки постоянно меняются, одни акции стартуют, другие — завершаются. Ручное управление — это долго и ненадёжно: не успеете оглянуться, как что-то забудете обновить. Поэтому большие селлеры активно пилят собственные интеграции с нашим API и автоматизируют всё что можно.

Однако API пользуются не только гиганты, но и маленькие предприниматели, которые продают, скажем, вязаные носки. У них нет собственных разработчиков, при этом они тоже хотят автоматизации — и берут готовые решения: плагины для 1С, интеграции с сервисом «МойСклад» и прочие сторонние продукты. В итоге через наш API работают клиенты самого разного масштаба и уровня подготовки.

Кроме того, всё многообразие возможностей API сейчас поделено примерно на 12 категорий по бизнес-функциям: каталог товаров, управление ценами, маркетинг, отчёты, финансы и т. д. У каждой категории — свои требования к работе API. Одним нужны мгновенные обновления, другим — тщательность и отсутствие нагрузки на систему. «Серебряной пули» для всех сразу быть не может, поэтому и алгоритм рейтлимитов мы рассматриваем через призму бизнес-категорий.

Бизнес-требования к ограничителям

Прежде чем углубляться в реализацию, определимся, что требует бизнес от системы лимитирования. Безапелляционных требований немного, фактически три:

  • Надёжность. API должен работать всегда, поэтому решение по лимитам не должно нарушать стабильность системы. Чем сложнее схема лимитирования и чем больше дополнительного кода мы пишем, тем больше рисков, что что-то пойдёт не так. Вдобавок нужен резервный вариант на случай сбоя.

  • Скорость работы. В кабинете продавца, нагрузка существенно ниже, чем на сайте для покупателей, однако есть сценарии использования, в которых высокая пропускная способность и минимальные задержки играют ключевую роль. Пример — оперативная синхронизация остатков, когда продавец ведёт учёт товара сразу на нескольких площадках. Товар может закончиться, а маркетплейс об этом не узнает и примет заказ на нулевой остаток.

  • Точность. Я подразумеваю под этим соблюдение лимитов там, где важна защита ресурсоёмких операций. Не все методы API равны. Есть «дешёвые» вызовы (получить название товара — пустяковая операция для бэкенда), а есть очень «дорогие» — как выгрузить отчёт по продажам за три месяца. Вторая задача при бесконтрольном запуске способна серьёзно нагрузить базу данных.

Разные категории по-разному приоритезируют эти требования. Также реализация должна учитывать, что ИТ-инфраструктура Wildberries распределена по нескольким дата-центрам в разных регионах.

Архитектура API и уровни рейтлимита

Теперь посмотрим, где в ландшафте API Wildberries находятся рейтлимитеры.

На упрощённой схеме архитектура API выглядит, наверное, как у всех. Есть некий клиент, который шлёт запросы к API. Чаще всего это машина, реже — человек вместе с Postman.

Сперва трафик попадает к балансировщику L7 — Application Load Balancer. У нас используется Jing. Он по заданной политике (Round Robin, Least Connections или другой) распределяет запросы между несколькими дата-центрами и кластерами. В конечном итоге запрос приземляется в сервис аутентификации.

Сервис аутентификации делает ровно то, что от него ожидается: проверяет токен и определяет, кому можно доверять. Алгоритм простой: берём авторизационный заголовок (JWT-токен), проверяем подпись и срок действия, достаём sellerID — идентификатор продавца. Далее на основе содержимого токена выполняется базовая авторизация — проверяем уровень доступа селлера. Если всё хорошо, пропускаем запрос дальше, в сервисы бизнес-логики соответствующей категории. Если что-то не так — возвращаем ошибку.

Где в этой цепочке притаились рейтлимитеры? Оказывается, ограничитель не один — их несколько, и они решают разные задачи.

Первый рубеж: лимитер по IP

Самый первый рейтлимитер стоит перед сервисом аутентификации. Он встречает абсолютно все входящие запросы.

Важно, что на этом уровне система ещё не знает конкретного селлера. У нас есть только IP-адрес клиента и неразобранный JWT. Мы ещё не проверили подпись и не извлекли sellerID. Поэтому верхний лимитер может оперировать лишь IP-адресом.

Основная роль этого ограничителя — фильтровать подозрительные запросы. Во-первых, когда начинается массированный поток запросов из каких-то подсетей, именно здесь они могут сходу отсеяться и отправиться в бан (временный или постоянный) — чтобы бэкенд вообще не почувствовал атаки.

Здесь же ловятся багованные, неумелые интеграции клиентов. Такое бывает с продавцом, который решает самостоятельно написать интеграцию, но не имеет достаточного опыта. Он отправляет запрос и, не получив ответа, думает: «Что-то не сработало, надо попробовать ещё раз». И он пробует — ещё и ещё, без задержек между попытками, в несколько потоков одновременно. Но благодаря верхнему лимитеру ни базы данных, ни брокеры сообщений даже не узнают о чьих-то хаотичных запросах.

Лимитер на уровне сервиса

Прошли балансировку, прошли первичный фильтр — запрос попадает в сервис аутентификации, где мы уже знаем, кто именно стучится. В работу включается основной рейтлимитер, который ограничивает число запросов для идентифицированного селлера по конкретному методу API. Мы наконец-то доверяем пользователю (точнее, его токену) и можем применять более тонкие ограничения.

В сервисе аутентификации реализованы разные тарифные лимиты для разных продавцов. Крупным селлерам мы даём возможность работать с большими лимитами, так называемые премиум-тарифы. Огромному магазину может требоваться хоть 100 RPS, а ИП с вязаными носочками хватит и трёх. Ограничитель устанавливает персональные пороги на основе sellerID и блокирует или замедляет сверхлимитные запросы.

Лимиты для плавного отключения старых версий API

В начале статьи я обещал рассказать о нестандартном применении лимитеров.

Мы в Wildberries стараемся где только можно сохранять обратную совместимость API — но жизнь вносит коррективы. Иногда приходится выпускать новую мажорную версию, которая несовместима со старой. Как аккуратно вывести из эксплуатации старое API, чтобы никого не поломать?

Недостаточно выпустить версию 4.0 вместо 3.0, дать грейс-период и через две недели отрубить старое. Почти никто не обновляется добровольно. Даже через две недели абсолютное большинство клиентов так и продолжат сидеть на 3.0. Если просто взять и отключить — получим массовое падение интеграций и грандиозный скандал.

Мы придумали более мягкий подход — с использованием рейтлимитера. Вместо полного отключения старой версии мы начинаем постепенно «придушивать» лимиты на ней. Каждый день слегка уменьшаем допустимый потолок запросов для старого API. Вчера было 100 RPS, сегодня 90, завтра 80 — и т. д. Клиенты в какой-то момент ощущают, что API стало работать хуже, начинают разбираться и (о чудо!) находят рекомендацию перейти на новую версию.

Трюк, может быть, не для слабонервных, но он уже успешно опробован.

Требования к технической реализации

Итак, высокоуровневые задачи понятны. Переходим к деталям.

Логика ограничения у нас вынесена в отдельный компонент, чтобы можно было гибко её развивать. Клиент рейтлимита — это Go-библиотека, подключаемая к каждому экземпляру сервиса аутентификации. Она берёт на себя учёт запросов и решение, пускать или не пускать очередной запрос дальше.

Мы сформулировали пять основных требований к такой библиотеке-лимитеру:

1. Неизменный интерфейс (контракт). Через сервис аутентификации проходит весь трафик к API. Таких сервисов много (по числу категорий), они в разных кластерах, но мы не любим лишний раз их перевыкатывать. Хотелось, чтобы даже при изменениях в логике лимитирования на сервере, интерфейс библиотеки для Auth-сервисов оставался прежним — без постоянных правок кода. Проще говоря, вне зависимости от внутренней реализации, клиентский вызов должен выглядеть тривиально: спросить, можно или нельзя пропустить запрос.

Мы реализовали это как обычный метод: на вход подаётся actor (идентификатор селлера), scope (категория API) и method (конкретный метод API). Сервис аутентификации говорит: «Продавец № 123 хочет дёрнуть метод X в категории Y», — и получает в ответ либо «да», либо «нет». Если «да», он возвращается уточнить, сколько запросов ещё можно сделать, если «нет» — когда можно попробовать снова.

type IRateLimiter interface {
Check(
ctx context.Context,
actor string,
scope string,
method string
) (*Result, error)
}
type Result struct {
Allowed bool
Remaining int64
RetryAfter time.Duration
ResetAfter time.Duration
}

2. Простота конфигурирования. Мы не хотим, чтобы при изменении лимитов приходилось перезапускать сервисы или, того хуже, заново компилировать код. Наша цель — вынести всю конфигурацию в админку, где можно задать параметры ограничений, не тревожа работу Auth-сервисов. При этом сами сервисы не должны тянуть лишних зависимостей, то есть им нежелательно напрямую ходить, например, в базу конфигурации. Идеально — один источник правды, откуда берутся текущие лимиты. Как это сделать, расскажу далее.

3. Прозрачная группировка методов в «корзины». С точки зрения лимитирования API-методы иногда нужно объединять логически. Например, у вас есть одна бизнес-функция «скачать отчёт», а реализована она тремя методами: CSV, XLS, JSON. Понятно, что все методы должны разделять одинаковый лимит, как будто это одна корзина. Клиенту (Auth-сервису) про группировку знать ничего не надо — он передаёт фактический метод. Объединение разных методов в общие бакеты происходит на серверной стороне прозрачно для клиента.

csv, _ := rl.Check(context.TODO(),
"seller-1", "analytics",
"/api/get_report_csv", 1)

xls, _ := rl.Check(context.TODO(),
"seller-1", "analytics",
"/api/get_report_xls", 1)

На сервере — единый счетчик get_report.

4. Резервная схема (fallback). Слишком сложный лимитер с множеством компонентов = высокий шанс отказа. Даже если код идеален, может подвести инфраструктура. Следует предусмотреть упрощённый резервный вариант. Если что-то пойдет не так, система должна автоматически переключиться на более простую и надёжную стратегию, лишь бы не остановить работу API. Про один такой резерв — полностью локальный in-memory лимитер — я расскажу дальше.

5. Поддержка заголовков ответа. Мы хотим, чтобы клиенты API могли сами подстраиваться под ограничения, не доводя дело до ошибок. Конечно, все интеграторы читают документацию и знают свои лимиты, но ситуации бывают разные. Например, лимиты выдаются на селлера (скажем, 100 RPS), а селлер может выписать два токена двум разным сервисам. Каждый сервис думает, что у него есть 100 RPS, а на деле у каждого по 50. Чтобы избежать неожиданностей, нужно в ответах API передавать информацию о лимитах — сколько запросов осталось, какое окно времени и т. п. Так клиенты смогут своевременно притормозить, не доходя до отказа (HTTP 429 Too Many Requests).

Кстати, ошибки 429 для нас нежелательны не только из-за клиентов API: это ведь HTTP 4XX, а такие ответы привлекают внимание наших антифрод-систем.

Схемы рейтлимитинга: эволюция в четырёх этапах

Переходим к самому интересному — реализации на серверной стороне. Мы прошли длинный путь экспериментов.

Схема № 1. Изолированный in-memory лимитер

Самая простая схема. Всё происходит локально: никакой распределённости, каждый сервис считает запросы только в своём собственном процессе.

Как это выглядит? В каждом экземпляре Auth-сервиса крутится свой изолированный счётчик запросов (HashMap в Go). Поступил запрос — смотрим внутри локального хранилища: сколько уже было за последнюю секунду? Если ещё можно — увеличиваем локальный счётчик и пропускаем дальше; если лимит исчерпан — возвращаем отказ (429) или задерживаем.

Плюсы очевидны: схема элементарная и супернадёжная — ломаться нечему, сеть не нужна, зависимых сервисов нет. Оверхед на запрос — минимальный: в наших измерениях чистая in-memory проверка лимита добавляла порядка 200–300 нс (наносекунд). Или 0,0002–0,0003 миллисекунды. Это ничтожно мало. Фактически, производительность ограничивается лишь скоростью доступа к памяти и синхронизации потоков.

Однако минусы у изолированного подхода тоже колоссальные. Главный — практически нулевая точность глобального лимита. Каждый экземпляр сервиса не знает о других, поэтому распределение общего лимита между ними — задача нетривиальная.

Предположим, мы хотим суммарно ограничить метод до 100 RPS, а сервисов у нас два. Что делать? Логично выставить на каждом по 50 RPS и надеяться, что трафик распределится ровно пополам. Но в реальности нагрузка никогда не делится равномерно. Балансировка Round Robin может дать перекос, разные клиенты ходят по-разному. Добавим к этому флуктуации: днём больше селлеров онлайн, ночью меньше; поды автоскейлятся горизонтально. В итоге один экземпляр может получить больше запросов, другой меньше.

Точность такой схемы стремится к ± бесконечности — то есть для серьёзных лимитов её вообще нельзя считать точной. Она хороша только для очень грубых прикидок.

Но не спешите отказываться от in-memory лимитера!

Во-первых, эту схему можно использовать как основную там, где требования к точности отсутствуют, зато критичны требования к производительности.

Во-вторых, изолированный лимитер — отличный fallback. Мы используем его как резерв для всех следующих схем. Если вдруг упадёт внешняя база лимитов (о ней ниже) или произойдёт сетевой разрыв между дата-центрами, мы всегда можем переключиться на локальный подсчёт и продолжать обслуживать API.

Только в отношениях с in-memory есть небольшой секрет. Мы обнаружили, что при большом числе разных ключей (селлеров и методов) локальная HashMap с блокировками начинает тормозить.

Сначала мы сделали одну общую Map и защищали её с помощью Mutex. Но когда через один под проходят десятки тысяч селлеров, все инкременты разных ключей лочат один и тот же Mutex — получается бутылочное горлышко. Пришлось ухитриться: разбили HashMap на 16 сегментов (по хэшу sellerID), то есть 16 экземпляров Mutex. Это и позволило нам добиться 200–300 нс на операцию; иначе было в десять раз больше.

Схема № 2. Централизованный Redis

Изолированная схема была бы идеальна, не требуй мы глобальной точности. Но бизнес-то требует, особенно для тяжёлых операций. Поэтому следующий естественный шаг — вынести подсчёт запросов в общий внешний стор. Классика жанра — взять Redis.

Да, сейчас у Redis поменялась политика лицензирования, появились форки и т. д., но это не важно. Нам нужен быстрый ключ-значение storage, работающий в памяти и поддерживающий инкременты. Мы разворачиваем Redis Ring — набор из N узлов Redis, распределяющих ключи по хэш-слоту (используем алгоритм HRW — Highest Random Weight). Каждый Auth-сервис (клиент лимитера) при старте смотрит на sellerID и определяет, на какую шарду Redis ему идти для данного селлера. Запросы начинают учитываться в Redis, а не локально.

Также на сцене появляется Lua.

Какие плюсы?

  1. Высокая точность и консистентность. Мы получаем единый централизованный счётчик для каждого ключа (селлер+метод). Общий лимит 100 RPS соблюдается практически чётко, независимо от количества инстансов сервиса — ведь все они ходят в один слот Redis для данного селлера.

  2. Централизованное обновление конфигурации. Помните требование о гибком конфигурировании через админку? Мы сделали отдельный интерфейс для настройки лимитов. Администратор задаёт в UI предельные значения RPS для методов, объединяет методы в корзины, отмечает селлеров с премиум-тарифом, временные баны и т. д. Затем специальный процесс каждые N минут раскидывает обновления конфигурации по всем Redis-шардам.

  3. Простота для клиента. Auth-сервису всё ещё не нужно ни о чём задумываться — никакой сложной логики. Он шлёт в Redis скрипт вида «увеличить счётчик X и проверить, не вышло ли за предел Y». На Lua мы написали небольшой скрипт, который учитывает все аспекты: и тариф пользователя, и тип метода (к какой корзине относится), и находится ли селлер в бане. Вся бизнес-логика лимитирования исполняется в Redis атомарно вместе с инкрементом счётчика.

Конечно, у Redis-схемы есть и минусы. Мы ощутили их на практике:

  1. Растёт задержка на проверку лимита. Про in-memory мы говорили — наносекунды. А Redis — это сеть + исполнение Lua. В среднем мы получаем около 0,1 мс задержки, когда клиент попадает в Redis, расположенный в том же дата-центре, и порядка 1,5 мс, если Redis-шарда находится в другом ДЦ. Это сильно больше, чем 300 нс, но всё ещё приемлемо для большинства операций. Я думаю, 1–2 миллисекунды — небольшая плата за глобальную точность.

  2. Сложнее разработка и сопровождение. Пришлось вспомнить Lua :) Честно говоря, у нас не было выделенного Lua-разработчика, поэтому скрипт писали обычные бэкендеры (Go). Это добавило боли при отладке, но разово пережить можно. Куда неприятнее другое: мы столкнулись с эффектом холодного Redis.

Что за эффект? Кластер Redis состоит из множества нод (от десяти). Они живут на отдельных машинах независимо от сервисов. Если какая-то нода Redis падает и потом перезапускается, она возвращается пустой, без ключей (ведь это кэш, а не постоянное хранилище).

И вот: Redis поднялся, конфигурация лимитов из админки на него ещё не

приехала (раскатывается по расписанию раз в несколько минут), а в это время клиент уже пытается выполнить скрипт проверки лимита! Он спрашивает, какой лимит для метода X у селлера Y, а Redis разводит руками — у него ещё нет данных об этом методе, он холодный. В итоге все запросы, попавшие на свежезапущенный Redis, начинают фейлиться или проходить бесконтрольно, что одинаково плохо.

Мы придумали 2 варианта решения этой проблемы.

Вариант первый — возвращать ошибку клиенту (HTTP 500) и надеяться, что код клиента обработает её и попытается пойти в другой дата-центр, где запрос удастся. Но это сложновато на уровне архитектуры и ненадёжно.

Вариант второй — хитрить с доступностью Redis для клиентов. Мы реализовали такой трюк: при старте Redis-ноде задаётся случайный пароль, который не известен клиентам. То есть только что поднявшийся Redis не будет принимать запросы от Auth-сервисов — они просто не авторизуются (Redis отвечает NOAUTH). Зато админка знает этот временный пароль и успешно заливает в пустой Redis всю конфигурацию. После этого конфигуратор меняет пароль Redis на рабочий, который известен клиентам. Вуаля — клиенты «видят» ноду только когда она готова к работе.

Отлаженная Redis-схема показала себя неплохо. Задержки я уже упомянул (0,1–1,5 мс). Пропускная способность тоже в порядке — в пределах десятков тысяч RPS на ноду нас вполне устраивает. Централизованный Redis обеспечил точность и гибкость конфигурирования, но ценой усложнения архитектуры и небольшой потери быстродействия.

Схема № 3. GoLimiter (gRPC-сервис)

Раз Redis внёс дополнительные задержки, мы задумались, нельзя ли сделать быстрее.

Протокол у Redis текстовый, парсинг RESP тоже съедает время. А есть модные бинарные протоколы (например, gRPC с ProtoBuf) — наверняка получится выжать больше!

Решили написать свой лимитер-сервис на Go, назовём его условно GoLimiter. По сути, мы вынесли логику in-memory счетчика во внешний Standalone-сервис, но общение с ним идёт по gRPC. Внутри всё то же, HashMap + Mutex, наружу торчит gRPC-интерфейс с двумя методами: check (можно или нельзя сейчас пройти) и, возможно, update (отметить выполнение запроса).

Мы запустили по экземпляру GoLimiter в каждом дата-центре, чтобы быть близко к Auth-сервисам. Auth-сервис при запросе стучится к локальному GoLimiter по gRPC (это очень быстро — внутри одного ДЦ задержки минимальны).

Чтобы лимиты были глобальными на все дата-центры, экземпляры GoLimiter синхронизируются между собой. Мы сделали между ними gRPC-стрим: каждый лимитер, пропуская через себя запрос, рассылает остальным сообщение: «Селлер X вызвал метод Y, уменьшите счетчик на 1». Таким образом, каждый лимитер знает не только о своих локальных запросах, но и о тех, что прошли через его коллег.

Какие же результаты? Взрывного прироста производительности по сравнению с Redis не получилось. Мы ожидали, что будет быстрее, а вышло примерно сопоставимо (0,3–1 мс).

Но кое-что GoLimiter дал нам полезное. Пропускная способность стрим-синхронизации между лимитерами составила порядка 300 тыс. RPS. Этого с огромным запасом хватает на любые наши категории API для селлеров. А ещё мы получили ценную информацию и опыт, чтобы двигаться дальше. Из GoLimiter выросло следующее, самое интересное решение.

Схема № 4. Гибридная (стриминг + локальный счётчик)

Ради эксперимента мы решили скрестить плюсы разных подходов. Так родилась несколько необычная схема-мутант.

Посмотрите, на картинке появился красный прямоугольник:

Если говорить просто, мы вернулись к идее полностью локального подсчёта (как в Схеме № 1), но при этом сохранили распределённую синхронизацию через стрим (как в Схеме № 3). 

Внутри каждого Auth-сервиса мы вновь держим локальный in-memory лимитер (HashMap с сегментами, как раньше). Он мгновенно считает входящие запросы, практически не добавляя задержки.

Параллельно каждый Auth-сервис поддерживает gRPC-стрим-соединение с соседями (или с центральным координатором) — образуя сеть обмена событиями, аналогичную связке GoLimiter. Когда один экземпляр Auth пропускает через себя запрос, он шлёт остальным: «Селлер X, метод Y — минус 1 по счётчику».

Таким образом, каждый сервис приблизительно знает общую картину, даже не обращаясь к внешнему хранилищу. Почему приблизительно? Потому что синхронизация идёт потоково, без подтверждения каждой операции. В обмен мы получаем сверхнизкие задержки.

Что по метрикам? Близко к in-memory: дополнительная задержка на 400–1000 нс. Для любой бизнес-категории Wildberries такая крошечная задержка абсолютно не критична — глаза не заметят. Почему немного хуже, чем чисто локально? Лишь потому, что помимо собственного счётчика теперь крутится горутинка, обрабатывающая входящие события стрима и корректирующая локальные счётчики.

Если требуются строгие гарантии — эта схема не даёт их. Но для самых высоконагруженных и чувствительных к задержкам категорий API такой гибрид подошёл идеально.

Комбинируя схемы, получаем лучшее

Мы прошли через четыре варианта реализации рейтлимитов и вынесли для себя несколько уроков.

Главный из них — нет единственно правильного решения для всех случаев. Выбор оптимальной схемы зависит от контекста и приоритетов: где-то важнее точность, где-то скорость, а надёжность всегда лидирует.

Почти каждая из рассмотренных схем, кроме GoLimiter, в итоге заняла своё место в нашей системе:

  1. Изолированная (in-memory) — обеспечивает минимальную задержку и абсолютную автономность. Мы используем её как fallback для всех остальных.

  2. Redis-схема — классический подход, который дал консистентность и удобство конфигурирования. Пришлось повозиться с Lua и решить проблему холодного старта, но в остальном схема довольно надёжная и масштабируемая. Этот вариант у нас сейчас основной для большинства средних по нагрузке задач, где нужен баланс точности и скорости.

  3. Гибридный стриминг — наша новейшая разработка, которая объединила низкую задержку локального лимитера с распределённой синхронизацией. Получилась почти идеальная схема на определённых сегментах нагрузки: незаметный оверхед и при этом приемлемая целостность данных (нет ситуации, чтобы один селлер смог сильно превысить глобальный лимит). Конечно, полной согласованностью мы пожертвовали, но где нужно — страхует fallback на Redis. Сейчас мы пилотируем стриминг на самых требовательных категориях API. Если эксперимент пройдёт успешно, возможно, расширим его применение.

В заключение повторю фразу из названия: «Быстро — не всегда хорошо». Гонка за минимальной задержкой не должна сказываться на надёжности и поддерживаемости системы. Идеальный рейтлимитер — это не тот, который максимизирует один показатель, а тот, который гибко подстраивается под разные требования. Где-то мы включаем максимальную скорость, где-то — железобетонную точность, а на случай непредвиденных проблем всегда держим план «Б». Многоуровневый подход защищает инфраструктуру от перегрузок и при этом не мешает пользователям работать.

Если у вас остались вопросы или вы готовы поделиться своим опытом реализации рейтлимитов в похожем окружении — добро пожаловать в комментарии!


Ещё больше интересного — в нашем телеграм-канале.

Комментарии (0)