Перейти к основному содержимому

Ограничитель скорости запросов (триттлинг)

В сетевой системе ограничитель скорости запросов (rate limiter) используется для контроля скорости трафика, отправляемого клиентом или сервисом. В мире HTTP rate limiter ограничивает количество клиентских запросов, которые можно отправить за определенный период времени. Если число API-запросов превышает порог, заданный rate limiter, все лишние вызовы блокируются. Вот несколько примеров:

  • Пользователь может публиковать не более 2 постов в секунду.

  • С одного IP-адреса можно создавать максимум 10 аккаунтов в день.

  • С одного устройства можно получать награды не более 5 раз в неделю.

В этой главе нам нужно спроектировать rate limiter. Но прежде чем переходить к дизайну, сначала посмотрим, какие преимущества дает API rate limiter:

  • Предотвращение истощения ресурсов из-за атак типа Denial of Service (DoS) [1]. Почти все API, публикуемые крупными технологическими компаниями, используют ту или иную форму rate limiting. Например, Twitter ограничивает число твитов до 300 за 3 часа [2]. API Google Docs по умолчанию имеют следующее ограничение: 300 запросов на пользователя за 60 секунд для операций чтения [3]. Rate limiter предотвращает DoS-атаки, как намеренные, так и случайные, блокируя лишние вызовы.

  • Снижение затрат. Ограничение лишних запросов означает меньшее число серверов и возможность выделить больше ресурсов более приоритетным API. Rate limiting крайне важен для компаний, использующих платные сторонние API. Например, следующие внешние API тарифицируются за каждый вызов: проверка кредитоспособности, проведение платежа, получение медицинских записей и т. д. Поэтому ограничение количества вызовов критично для снижения расходов.

  • Предотвращение перегрузки серверов. Чтобы уменьшить нагрузку на серверы, rate limiter отфильтровывает лишние запросы, вызванные ботами или некорректным поведением пользователей.

Шаг 1 — Понять задачу и определить рамки дизайна​

Rate limiting можно реализовать разными алгоритмами, у каждого из которых есть свои плюсы и минусы. Взаимодействие между интервьюером и кандидатом помогает уточнить, какой именно тип rate limiter мы пытаемся построить.

Кандидат: Какой именно rate limiter мы проектируем? Это клиентский rate limiter или серверный API rate limiter?
Интервьюер: Отличный вопрос. Мы фокусируемся на серверном API rate limiter.

Кандидат: Ограничитель будет дросселировать API-запросы по IP-адресу, user ID или по другим свойствам?
Интервьюер: Rate limiter должен быть достаточно гибким, чтобы поддерживать разные наборы правил ограничения.

Кандидат: Каков масштаб системы? Она строится для стартапа или для крупной компании с большой пользовательской базой?
Интервьюер: Система должна уметь обрабатывать большое число запросов.

Кандидат: Система будет работать в распределенной среде?
Интервьюер: Да.

Кандидат: Rate limiter — это отдельный сервис, или он должен быть реализован в коде приложения?
Интервьюер: Это решение по дизайну остается за вами.

Кандидат: Нужно ли информировать пользователей, чьи запросы ограничиваются?
Интервьюер: Да.

Требования

Ниже приведено краткое резюме требований к системе:

  • Точно ограничивать избыточные запросы.

  • Низкая задержка. Rate limiter не должен замедлять время HTTP-ответа.

  • Использовать как можно меньше памяти.

  • Распределенное ограничение скорости. Один и тот же rate limiter может использоваться несколькими серверами или процессами.

  • Обработка исключений. Пользователи должны видеть понятные сообщения об ошибках, когда их запросы ограничиваются.

  • Высокая отказоустойчивость. Если с rate limiter возникают проблемы, например, кэш-сервер уходит в офлайн, это не должно влиять на всю систему целиком.

Шаг 2 — Предложить высокоуровневый дизайн и согласовать его​

Для простоты будем использовать базовую модель клиента и сервера.

Где размещать rate limiter?​

Интуитивно rate limiter можно реализовать либо на стороне клиента, либо на стороне сервера.

  • Реализация на стороне клиента. Вообще говоря, клиент — ненадежное место для реализации rate limiting, потому что клиентские запросы могут быть легко подделаны злоумышленниками. Кроме того, у нас может не быть контроля над реализацией клиента.

  • Реализация на стороне сервера. На рисунке 1 показан rate limiter, размещенный на стороне сервера.

Изображение показывает упрощенную клиент-серверную архитектуру с rate limiter. Светло-голубой прямоугольник с подписью 'Client' содержит значки ноутбука и смартфона, показывая, что запросы приходят с разных клиентских устройств. Толстая синяя стрелка с подписью 'HTTP request' идет от блока Client к группе из трех вертикально расположенных светло-зеленых прямоугольников, представляющих 'API Servers', заключенных в светло-голубую пунктирную область со скругленными углами. Рядом с API Servers находится белый прямоугольник с подписью 'Rate limiter', что показывает, что rate limiter расположен между клиентскими запросами и API-серверами. Стрелка обозначает, что HTTP-запрос идет от клиента через rate limiter и затем поступает на API-серверы. Такая схема визуально показывает систему, в которой rate limiter управляет потоком HTTP-запросов от клиентов, предотвращая перегрузку серверов.

Рисунок 1

Помимо клиентской и серверной реализации существует и альтернативный вариант. Вместо того чтобы размещать rate limiter прямо на API-серверах, можно создать middleware для rate limiting, которое будет ограничивать запросы к вашим API, как показано на рисунке 2.

Изображение показывает упрощенную клиент-серверную архитектуру с rate limiter. Светло-голубой прямоугольник со скругленными углами с подписью 'Client' содержит значки ноутбука и смартфона, обозначающие различные клиентские устройства. От блока Client идет сплошная синяя стрелка, показывающая поток запросов к вертикальному прямоугольному компоненту с подписью 'Rate limiter'. Этот rate limiter работает как шлюз, обрабатывая запросы до того, как они попадут на серверы. От него идет еще одна сплошная синяя стрелка к светло-голубой пунктирной области со скругленными углами с подписью 'API Servers'. Внутри находятся три вертикально расположенных зеленых прямоугольника, представляющих несколько API-серверов. Стрелки показывают однонаправленный поток запросов от клиентов через rate limiter к API-серверам. В целом схема иллюстрирует распространенный шаблон, в котором rate limiter контролирует число запросов, доходящих до backend-серверов, предотвращая перегрузку и повышая устойчивость системы.

Рисунок 2

Давайте используем пример с рисунка 3, чтобы показать, как в этом дизайне работает rate limiting. Предположим, наше API разрешает 2 запроса в секунду, а клиент отправляет на сервер 3 запроса за одну секунду. Первые два запроса доходят до API-серверов. Однако middleware rate limiter ограничивает третий запрос и возвращает HTTP-статус 429. Код ответа HTTP 429 означает, что пользователь отправил слишком много запросов.

Изображение показывает архитектурную диаграмму системы, иллюстрирующую rate limiting в API. Светло-голубой прямоугольник со скругленными углами с подписью 'Client' содержит значки ноутбука и мобильного телефона, обозначающие несколько клиентских устройств. От клиента к высокому темно-серому прямоугольнику с подписью 'Rate limiter' идут несколько сплошных синих стрелок, представляющих запросы. Некоторые запросы успешно проходят через Rate limiter, что показано продолжением стрелок к светло-голубой пунктирной области со скругленными углами с подписью 'API Servers', где изображены три вертикально расположенных зеленых значка серверов. Однако один запрос блокируется Rate limiter, что показано красным крестиком и пунктирной красной стрелкой обратно к Client с подписью '429: Too many requests', то есть HTTP 429 из-за превышения лимита. Диаграмма наглядно показывает поток запросов от клиентов, их фильтрацию rate limiter и возможность отклонения запросов при превышении заданного лимита.

Рисунок 3

Облачные микросервисы [4] стали очень популярными, и rate limiting обычно реализуется в компоненте, который называется API gateway. API gateway — это полностью управляемый сервис, поддерживающий rate limiting, SSL termination, аутентификацию, белые списки IP, обслуживание статического контента и т. д. Пока нам достаточно понимать, что API gateway — это middleware, поддерживающее rate limiting.

При проектировании rate limiter важно задать себе вопрос: где он должен быть реализован — на стороне сервера или в API gateway? Абсолютно правильного ответа нет. Это зависит от текущего технологического стека компании, инженерных ресурсов, приоритетов, целей и т. д. Вот несколько общих рекомендаций:

  • Оцените ваш текущий технологический стек: язык программирования, сервис кэширования и т. д. Убедитесь, что выбранный язык достаточно эффективен для реализации rate limiting на стороне сервера.

  • Определите алгоритм rate limiting, который соответствует потребностям бизнеса. Если вы реализуете все на стороне сервера, у вас есть полный контроль над алгоритмом. Однако если вы используете сторонний API gateway, ваш выбор может быть ограничен.

  • Если у вас уже используется микросервисная архитектура и в дизайне уже есть API gateway для аутентификации, белых списков IP и т. д., можно добавить rate limiter прямо в API gateway.

  • Создание собственного сервиса rate limiting требует времени. Если у вас недостаточно инженерных ресурсов для реализации rate limiter, коммерческий API gateway — лучший вариант.

Алгоритмы rate limiting​

Rate limiting можно реализовать разными алгоритмами, и у каждого из них свои плюсы и минусы. Хотя эта глава не сосредоточена исключительно на алгоритмах, понимание их на высоком уровне помогает выбрать правильный алгоритм или комбинацию алгоритмов под наши сценарии использования. Вот список популярных алгоритмов:

  • Token bucket

  • Leaking bucket

  • Fixed window counter

  • Sliding window log

  • Sliding window counter

Алгоритм token bucket​

Алгоритм token bucket широко используется для rate limiting. Он простой, хорошо изученный и часто применяется интернет-компаниями. И Amazon [5], и Stripe [6] используют этот алгоритм для ограничения своих API-запросов.

Алгоритм token bucket работает следующим образом:

  • Token bucket — это контейнер с заранее определенной емкостью. Ведро периодически пополняется токенами с заданной скоростью. Как только bucket заполняется, новые токены перестают добавляться. На рисунке 4 емкость token bucket равна 4. Пополняющий процесс добавляет в bucket 2 токена каждую секунду. Как только bucket заполнен, лишние токены переливаются через край.

Изображение показывает упрощенную систему управления ресурсом, возможно связанную с управлением буфером или rate limiting. Вертикальный прямоугольник с темной рамкой изображает источник, внутри которого находятся три золотистых значка, похожих на монеты, представляющие единицы ресурса. Направленная вниз стрелка с подписью 'refill...' соединяет этот источник с темно-зеленым bucket, символизирующим хранилище или буфер. Внутри bucket лежат четыре монеты, показывающие текущий уровень ресурса. Горизонтальная стрелка с подписью 'overfill...' выходит из bucket к одной монете снаружи, показывая механизм переполнения, при котором избыточные ресурсы сверх емкости bucket отбрасываются. В целом схема показывает ресурс, добавляемый в контейнер ограниченной емкости ('refill'), и выбрасывание лишних ресурсов ('overfill') при переполнении. Текст 'Viewer does not support full SVG 1.1' внизу — это сообщение браузера и не относится к сути диаграммы.

Рисунок 4

  • Каждый запрос расходует один токен. Когда приходит запрос, мы проверяем, достаточно ли токенов в bucket. На рисунке 5 показано, как это работает.

  • Если токенов достаточно, мы забираем по одному токену на каждый запрос, и запрос проходит.

  • Если токенов недостаточно, запрос отбрасывается.

Изображение показывает систему управления запросами и токенами. Блок с подписью 'requests' отправляет запросы к ромбу с подписью 'enough tokens?'. Этот узел решения проверяет уровень токенов в зеленом bucket, представляющем пул токенов. Токены добавляются в bucket через направленную вниз стрелку с подписью 'refill...', рядом с которой показана стопка золотых монет. Если токенов достаточно, ветка с подписью 'yes,...' ведет к блоку успешной обработки, где изображены монеты и светло-голубые прямоугольники, вероятно представляющие обработанные запросы. Если токенов недостаточно, ветка с подписью 'no,...' ведет к блоку ошибки с текстом 'Viewer does not support full SVG 1.1', что символизирует невозможность обработать запрос из-за нехватки токенов. В целом схема показывает систему обработки запросов на основе токенов: запросы пропускаются только при наличии достаточного числа токенов, иначе приводят к отказу.

Рисунок 5

На рисунке 6 показано, как работают логика расходования токенов, пополнения и ограничения запросов. В этом примере размер token bucket равен 4, а скорость пополнения — 4 токена в минуту.

Изображение показывает схему системы rate limiting на основе токенов в четырех различных моментах времени. Схема состоит из четырех пронумерованных прямоугольников, каждый из которых показывает состояние системы в конкретный момент. Блок 1 (1:00:00) показывает входящую стрелку 'request', направленную к блоку с четырьмя монетами, то есть четырьмя доступными токенами. Затем исходящая стрелка ведет к блоку 2. Блок 2 (1:00:05) показывает три входящих стрелки 'request', которые расходуют три исходных токена, после чего остаются три монеты. Затем из него выходят три стрелки. Блок 3 (1:00:20) показывает входящий 'request', направленный к пустому блоку (ноль токенов), то есть запрос, который будет задержан или отклонен из-за исчерпания токенов. Наконец, блок 4 (1:01:00) показывает, что четыре токена снова пополнены, что указывает на механизм refill после заданного временного интервала. Текст под каждым блоком дает дополнительный контекст, указывая число токенов на старте и результат обработки запросов. Стрелки показывают поток запросов, а монеты визуально обозначают доступные токены внутри системы rate limiting.

Рисунок 6

Алгоритм token bucket принимает два параметра:

  • Размер bucket: максимальное число токенов, разрешенное в bucket

  • Скорость пополнения: количество токенов, добавляемых в bucket каждую секунду

Сколько buckets нам нужно? Это зависит от правил rate limiting. Вот несколько примеров.

  • Обычно требуется иметь разные buckets для разных конечных точек API. Например, если пользователю разрешено делать 1 пост в секунду, добавлять 150 друзей в день и ставить 5 лайков в секунду, то для каждого пользователя нужны 3 отдельных bucket.

  • Если нам нужно ограничивать запросы по IP-адресам, каждому IP-адресу потребуется свой bucket.

  • Если система разрешает максимум 10 000 запросов в секунду, имеет смысл иметь один глобальный bucket, общий для всех запросов.

Плюсы:

  • Алгоритм легко реализовать.

  • Он эффективно использует память.

  • Token bucket допускает кратковременные всплески трафика. Запрос может пройти, пока в bucket остаются токены.

Минусы:

  • У алгоритма есть два параметра: размер bucket и скорость пополнения токенов. Однако корректно подобрать их значения бывает непросто.

Алгоритм leaking bucket​

Алгоритм leaking bucket похож на token bucket, за исключением того, что запросы обрабатываются с фиксированной скоростью. Обычно он реализуется с помощью очереди first-in-first-out (FIFO). Алгоритм работает следующим образом:

  • Когда приходит запрос, система проверяет, заполнена ли очередь. Если очередь не заполнена, запрос добавляется в нее.

  • Иначе запрос отбрасывается.

  • Запросы извлекаются из очереди и обрабатываются через равные промежутки времени.

На рисунке 7 показано, как работает алгоритм.

Изображение показывает систему обработки запросов с использованием механизма rate limiting. Группа входящих запросов, изображенных в виде нескольких светло-голубых прямоугольников внутри пунктирного блока с подписью 'requests', поступает в ромб с подписью 'bucket full?'. Если bucket не заполнен (ветка 'no'), запросы попадают в очередь, изображенную прямоугольником, разделенным на секции, часть из которых заполнена светло-голубым цветом, а часть белая, с подписью 'queue'. Эта очередь представляет буфер, удерживающий запросы до обработки. Из очереди запросы 'processed at a fixed rate' — это показано стрелкой к другой группе светло-голубых прямоугольников внутри пунктирного блока с подписью 'requests go through', то есть успешно обработанным запросам. Если bucket заполнен (ветка 'yes'), запросы уходят в затемненную область внизу с текстом 'Viewer does not support full SVG 1.1', что символизирует отклонение или иной путь обработки при переполнении. В целом схема показывает систему, которая управляет входящими запросами, буферизует их в очереди и обрабатывает с контролируемой скоростью, чтобы избежать перегрузки.

Рисунок 7

Алгоритм leaking bucket принимает следующие два параметра:

  • Размер bucket: он равен размеру очереди. Очередь хранит запросы, которые должны обрабатываться с фиксированной скоростью.

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

Компания Shopify, работающая в сфере электронной коммерции, использует leaky buckets для rate limiting [7].

Плюсы:

  • Эффективно использует память благодаря ограниченному размеру очереди.

  • Запросы обрабатываются с фиксированной скоростью, поэтому алгоритм подходит для сценариев, где нужен стабильный выходной поток.

Минусы:

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

  • У алгоритма есть два параметра, и подобрать их правильно не всегда просто.

Алгоритм fixed window counter​

Алгоритм fixed window counter работает следующим образом:

  • Он делит временную шкалу на окна фиксированного размера и назначает отдельный счетчик для каждого окна.

  • Каждый запрос увеличивает счетчик на единицу.

  • Как только счетчик достигает заранее заданного порога, новые запросы отбрасываются до тех пор, пока не начнется новое временное окно.

Рассмотрим конкретный пример. На рисунке 8 единица времени — 1 секунда, и система допускает максимум 3 запроса в секунду. В каждом секундном окне, если приходит больше 3 запросов, лишние запросы отбрасываются, как показано на рисунке 8.

Изображение показывает временной график, визуализирующий число успешных запросов и запросов, ограниченных rate limiter, на пятиминутном интервале. Горизонтальная ось представляет время и отмечена минутными делениями от 1:00:00 до 1:00:04. Вертикальная ось показывает количество запросов. Каждый прямоугольник представляет один запрос; светло-серые прямоугольники обозначают запросы, ограниченные rate limiter, а белые — успешные запросы. График показывает переменную картину: в 1:00:00 есть три успешных запроса; в 1:00:01 — три успешных и три ограниченных; в 1:00:02 — три успешных и один ограниченный; в 1:00:03 — один успешный запрос; в 1:00:04 — три успешных и два ограниченных. Легенда четко различает успешные и ограниченные запросы. В правом нижнем углу есть сообщение о том, что viewer не поддерживает full SVG 1.1.

Рисунок 8

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

Изображение показывает временной график, иллюстрирующий распределение запросов на интервале в две минуты. Горизонтальная временная линия идет от 2:00:00 до 2:02:00 и имеет вертикальные отметки на каждой минуте. Внутри пунктирного блока, который простирается примерно от 2:00:30 до 2:01:30, светло-голубые прямоугольники представляют отдельные запросы. Они расположены вертикально друг над другом, показывая, что в отдельные моменты приходит несколько запросов. Всего показано десять запросов, что отмечено двусторонней стрелкой над блоком с подписью '10 requests'. Запросы распределены неравномерно: в одни промежутки их больше, в другие меньше. Внизу графика есть текст 'Viewer does not support full SVG 1.1', указывающий на ограничение визуализатора.

Рисунок 9

На рисунке 9 система допускает максимум 5 запросов в минуту, а доступная квота сбрасывается на удобной для человека круглой минутной отметке. Как видно, между 2:00:00 и 2:01:00 проходит 5 запросов, и еще 5 запросов — между 2:01:00 и 2:02:00. Но в минутном окне от 2:00:30 до 2:01:30 успевают пройти 10 запросов. Это вдвое больше допустимого.

Плюсы:

  • Эффективно использует память.

  • Легко понимается.

  • Сброс доступной квоты в конце временного окна подходит для некоторых сценариев использования.

Минусы:

  • Всплеск трафика на границах окна может пропустить больше запросов, чем разрешает квота.

Алгоритм sliding window log​

Как мы уже обсуждали, алгоритм fixed window counter имеет серьезную проблему: он позволяет пропускать больше запросов на границах окна. Алгоритм sliding window log решает эту проблему. Он работает так:

  • Алгоритм отслеживает временные метки запросов. Обычно такие метки хранятся в кэше, например, в sorted sets Redis [8].

  • Когда приходит новый запрос, удаляются все устаревшие временные метки. Устаревшими считаются те, которые старше начала текущего временного окна.

  • В лог добавляется временная метка нового запроса.

  • Если размер лога равен или меньше допустимого количества, запрос принимается. Иначе он отклоняется.

Поясним алгоритм на примере, показанном на рисунке 10.

Изображение показывает блок-схему системы rate limiting, которая допускает только два запроса в минуту. На схеме показаны четыре этапа (пронумерованы 1-4). Каждый этап — это прямоугольный блок, представляющий единицу обработки. Стрелки показывают поток запросов, а временные метки — момент поступления каждого запроса. На этапе 1 поступает запрос в 1:00:01 и передается дальше. На этапе 2 этот запрос показан в 1:00:30 и также передается дальше. На этапе 3 поступает запрос в 1:00:50 и тоже проходит дальше. На этапе 4 видны запросы с метками 1:00:01 и 1:00:30, выделенные красным как находящиеся в пределах допустимой нормы, а затем поступает еще один запрос в 1:01:40, который также обрабатывается. Временные метки внутри каждого этапа показывают моменты поступления запросов на этом этапе. Заголовок 'Allow 2 requests per minute' задает ограничение системы. Диаграмма показывает, как система обрабатывает запросы внутри лимита и за его пределами, демонстрируя, что лишние запросы отклоняются на основе скользящего окна.

Рисунок 10

В этом примере rate limiter разрешает 2 запроса в минуту. Обычно в лог записываются временные метки Linux. Однако в нашем примере используется удобочитаемое представление времени для большей наглядности.

  • Лог пуст, когда новый запрос приходит в 1:00:01. Поэтому запрос разрешается.

  • Новый запрос приходит в 1:00:30, и временная метка 1:00:30 вставляется в лог. После вставки размер лога равен 2, что не превышает допустимое количество. Поэтому запрос разрешается.

  • Новый запрос приходит в 1:00:50, и временная метка вставляется в лог. После вставки размер лога становится равным 3, что больше допустимого размера 2. Поэтому этот запрос отклоняется, хотя временная метка все равно остается в логе.

  • Новый запрос приходит в 1:01:40. Запросы в диапазоне [1:00:40,1:01:40) попадают в последнее временное окно, а запросы, отправленные до 1:00:40, считаются устаревшими. Две устаревшие метки, 1:00:01 и 1:00:30, удаляются из лога. После удаления размер лога становится равен 2, поэтому запрос принимается.

Плюсы:

  • Ограничение, реализованное этим алгоритмом, очень точное. В любом скользящем окне число запросов не превысит лимит.

Минусы:

  • Алгоритм потребляет много памяти, потому что даже отклоненные запросы могут оставлять свои временные метки в памяти.

Алгоритм sliding window counter​

Алгоритм sliding window counter — это гибридный подход, объединяющий fixed window counter и sliding window log. Его можно реализовать двумя способами. В этом разделе мы разберем один из вариантов реализации, а ссылку на второй подход дадим в конце раздела. На рисунке 11 показано, как работает этот алгоритм.

Изображение показывает графическую схему скользящего rate limiter. Горизонтальная ось представляет время и разделена на сегменты 'previous minute' и 'current minute'. Вертикальная ось показывает число запросов. Светло-зеленый прямоугольник с подписью 'Rolling minute' пересекает границу между предыдущей и текущей минутой: 70% его площади находится в предыдущей минуте, а 30% — в текущей. Это и есть скользящее окно длиной в одну минуту. Внутри rolling minute находятся несколько светло-голубых прямоугольников, представляющих отдельные запросы. Черный прямоугольник выделяет запросы из предыдущей минуты, а светло-голубой — запросы из текущей. Сверху стрелка с подписью 'Current time' указывает на текущее положение внутри скользящего окна. Подпись 'Rate limit: 5 requests/min' задает максимальное число запросов в минуту. Диаграмма визуально показывает, как запросы считаются в скользящем минутном окне для ограничения скорости.

Рисунок 11

Предположим, что rate limiter допускает максимум 7 запросов в минуту, и в предыдущей минуте было 5 запросов, а в текущей — 3. Для нового запроса, который приходит в точке, соответствующей 30% текущей минуты, число запросов в скользящем окне вычисляется по следующей формуле:

  • Запросы в текущем окне + запросы в предыдущем окне * процент перекрытия между скользящим окном и предыдущим окном

  • По этой формуле получаем 3 + 5 * 0.7 = 6.5 запроса. В зависимости от сценария использования результат можно округлять вверх или вниз. В нашем примере он округляется вниз до 6.

Поскольку rate limiter допускает максимум 7 запросов в минуту, текущий запрос может пройти. Однако после еще одного запроса лимит будет достигнут.

Из-за ограничений по месту мы не будем обсуждать здесь другой вариант реализации. Заинтересованным читателям стоит обратиться к материалу [9]. Этот алгоритм неидеален. У него есть плюсы и минусы.

Плюсы

  • Он сглаживает всплески трафика, потому что ограничение рассчитывается на основе среднего темпа в предыдущем окне.

  • Эффективно использует память.

Минусы

  • Он работает только для не слишком строгого окна ретроспективного анализа. Это приближенная оценка реального темпа, потому что алгоритм предполагает равномерное распределение запросов в предыдущем окне. Однако на практике проблема может быть не такой серьезной, как кажется. Согласно экспериментам Cloudflare [10], среди 400 миллионов запросов лишь 0.003% были ошибочно разрешены или ограничены.

Высокоуровневая архитектура​

Базовая идея алгоритмов rate limiting проста. На высоком уровне нам нужен счетчик, который отслеживает, сколько запросов отправляет один и тот же пользователь, IP-адрес и т. д. Если значение счетчика больше лимита, запрос запрещается.

Где хранить счетчики? Использовать базу данных — плохая идея из-за медленного дискового доступа. Поэтому выбираем кэш в памяти: он быстрый и поддерживает стратегию истечения по времени. Например, Redis [11] — популярный вариант для реализации rate limiting. Это хранилище в памяти, которое предоставляет две команды: INCR и EXPIRE.

  • INCR: увеличивает сохраненный счетчик на 1.

  • EXPIRE: задает срок жизни для счетчика. Когда срок истекает, счетчик автоматически удаляется.

На рисунке 12 показана высокоуровневая архитектура rate limiting, которая работает следующим образом:

Изображение показывает архитектурную диаграмму системы, иллюстрирующую взаимодействие клиента и сервера с rate limiting. Прямоугольник с подписью 'Client' содержит значки ноутбука и мобильного телефона, представляющие различные клиентские устройства. Сплошная синяя стрелка соединяет Client со светло-голубым шестиугольником с подписью 'Rate limiter middleware', показывая, что клиентские запросы сначала проходят через этот компонент. От rate limiter идет еще одна сплошная синяя стрелка к пунктирному блоку с подписью 'API Servers', внутри которого изображены три вертикально расположенных зеленых значка серверов, представляющих backend-серверы приложения. Наконец, сплошная синяя стрелка идет от rate limiter к красному трехслойному кубу с подписью 'Redis', показывая, что rate limiter использует Redis — in-memory store — для отслеживания и управления частотой запросов. Стрелки показывают однонаправленный поток запросов; API Servers потенциально могут возвращать ответы обратно через rate limiter клиенту, хотя этот обратный путь явно не показан.

Рисунок 12

  • Клиент отправляет запрос в middleware rate limiting.

  • Middleware rate limiting получает значение счетчика из соответствующего bucket в Redis и проверяет, достигнут лимит или нет.

  • Если лимит достигнут, запрос отклоняется.

  • Если лимит не достигнут, запрос отправляется на API-серверы. Одновременно система увеличивает счетчик и сохраняет его обратно в Redis.

Шаг 3 — Глубокое погружение в дизайн​

Высокоуровневый дизайн на рисунке 12 не отвечает на следующие вопросы:

  • Как создаются правила rate limiting? Где они хранятся?

  • Как обрабатывать запросы, которые были ограничены?

В этом разделе мы сначала ответим на вопросы о правилах rate limiting, затем рассмотрим стратегии обработки ограниченных запросов. После этого обсудим rate limiting в распределенной среде, детальный дизайн, оптимизацию производительности и мониторинг.

Правила rate limiting​

Lyft открыл исходный код своего компонента для rate limiting [12]. Давайте немного заглянем внутрь и посмотрим на примеры правил:

domain: messaging
descriptors:
- key: message_type
value: marketing
rate_limit:
unit: day
requests_per_unit: 5

В приведенном выше примере система настроена так, чтобы разрешать максимум 5 маркетинговых сообщений в день. Вот еще один пример:

domain: auth
descriptors:
- key: auth_type
value: login
rate_limit:
unit: minute
requests_per_unit: 5

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

Превышение лимита​

Если запрос ограничивается rate limiter, API возвращает клиенту HTTP-код 429 (too many requests). В зависимости от сценария использования мы можем помещать ограниченные запросы в очередь для обработки позже. Например, если часть заказов была ограничена из-за перегрузки системы, мы можем сохранить их для последующей обработки.

Как клиент узнает, что его запросы ограничиваются? И как клиент понимает, сколько разрешенных запросов у него осталось до начала ограничений? Ответ заключается в HTTP-заголовках ответа. Rate limiter возвращает клиентам следующие HTTP-заголовки:

X-Ratelimit-Remaining: The remaining number of allowed requests within the window.

X-Ratelimit-Limit: It indicates how many calls the client can make per time window.

X-Ratelimit-Retry-After: The number of seconds to wait until you can make a request again without being throttled.

Когда пользователь отправил слишком много запросов, клиенту возвращаются ошибка 429 too many requests и заголовок X-Ratelimit-Retry-After.

Детальный дизайн​

На рисунке 13 показан детальный дизайн системы.

Изображение показывает архитектурную диаграмму системы, иллюстрирующую обработку API-запросов и rate limiting. Клиент (представлен значками ноутбука и телефона) отправляет запросы в middleware Rate Limiter. Если запрос укладывается в лимиты, middleware помечает его как 'success' и перенаправляет в CACHE, содержащий 'Cached rules'. CACHE пересылает запрос набору API Servers. Если нужных данных нет в кэше, API Servers обрабатывают запрос и сохраняют результат в Redis. Если запрос превышает лимиты, middleware помечает его как 'rate limited', и дальше есть два варианта: либо запрос отбрасывается (вариант 1, представлен кружком), либо отправляется в Message queue для последующей обработки (вариант 2). Над 'Workers', которые заполняют CACHE, расположен блок 'Rules', показывающий, что правила rate limiting приходят из этого источника. Если лимит превышен, система возвращает клиенту ошибку '429: too many requests'.

Рисунок 13

  • Правила хранятся на диске. Workers регулярно считывают их с диска и помещают в кэш.

  • Когда клиент отправляет запрос на сервер, запрос сначала попадает в middleware rate limiter.

  • Middleware rate limiter загружает правила из кэша. Также он получает из кэша Redis счетчики и временную метку последнего запроса. На основе ответа rate limiter принимает решение:

  • если запрос не ограничен, он пересылается на API-серверы.

  • если запрос ограничен, rate limiter возвращает клиенту ошибку 429 too many requests. При этом запрос либо отбрасывается, либо отправляется в очередь.

Rate limiter в распределенной среде​

Построить rate limiter, работающий на одном сервере, несложно. Однако масштабировать систему так, чтобы она поддерживала несколько серверов и конкурентные потоки, — уже совсем другая задача. Здесь есть две проблемы:

  • Race condition

  • Проблема синхронизации

Race condition​

Как обсуждалось ранее, на высоком уровне rate limiter работает так:

  • Считать значение counter из Redis.

  • Проверить, не превышает ли (counter + 1) порог.

  • Если не превышает, увеличить значение counter в Redis на 1.

Race conditions могут возникать в сильно конкурентной среде, как показано на рисунке 14.

Изображение показывает диаграмму race condition в системе со счетчиком. На схеме изображены два независимых запроса — 'request 1' и 'request 2', каждый из которых инициирует свою последовательность операций. Оба запроса сначала вызывают функцию 'read_counter', которая возвращает текущее значение счетчика (изначально 3). Затем оба запроса переходят к функции 'check_and_increment'. В этом примере 'check_and_increment' сначала считывает значение счетчика, затем увеличивает его. Request 1 читает 3, увеличивает до 4 и записывает счетчик. Request 2 параллельно тоже читает 3, увеличивает до 4 и записывает счетчик. Финальное значение счетчика получается 4, хотя после двух инкрементов от исходного значения 3 правильным результатом должно быть 5. Это и демонстрирует race condition: значение счетчика обновляется неатомарно, что приводит к неверному итоговому результату. Диаграмма наглядно показывает поток запросов и значение счетчика на каждом этапе, делая видимой конкуренцию операций и возникающую несогласованность.

Рисунок 14

Предположим, значение counter в Redis равно 3. Если два запроса одновременно прочитают значение counter до того, как кто-либо из них успеет записать новое значение обратно, каждый из них увеличит counter на единицу и запишет результат, не учитывая другой поток. Оба запроса (потока) будут считать, что правильное значение counter равно 4. Однако на самом деле правильное значение должно быть 5.

Самое очевидное решение для race condition — блокировки. Однако блокировки значительно замедляют систему. Поэтому обычно используют две другие стратегии: Lua scripts [13] и структуру данных sorted sets в Redis [8]. Если вам интересны эти стратегии, обратитесь к соответствующим материалам [8] [13].

Проблема синхронизации​

Синхронизация — еще один важный фактор в распределенной среде. Чтобы поддерживать миллионы пользователей, одного сервера rate limiting может быть недостаточно. Когда используются несколько серверов rate limiter, требуется синхронизация. Например, в левой части рисунка 15 клиент 1 отправляет запросы в rate limiter 1, а клиент 2 — в rate limiter 2. Поскольку веб-уровень не хранит состояние, клиенты могут начать отправлять запросы в другой rate limiter, как показано в правой части рисунка 15. Если синхронизации нет, rate limiter 1 не содержит данных о клиенте 2. Следовательно, rate limiter не сможет работать корректно.

Изображение показывает две разные архитектуры обработки rate limiting. Левая часть изображает более простую схему, где два клиента (Client 1 и Client 2), каждый представлен значками ноутбука и мобильного телефона внутри прямоугольника со скругленными углами, независимо подключаются к своим rate limiter. Каждый rate limiter изображен светло-голубым шестиугольником с подписью 'Rate limiter 1' и 'Rate limiter 2' соответственно и обрабатывает запросы только своего клиента. Однонаправленные стрелки показывают поток запросов от клиента к соответствующему rate limiter. Правая часть показывает более сложную архитектуру, где те же два клиента теперь могут обращаться к обоим rate limiter. Client 1 отправляет запросы и в 'Rate limiter 1', и в 'Rate limiter 2', и аналогично Client 2. Связи также показаны однонаправленными стрелками. Пунктирные рамки вокруг каждой системы подчеркивают, что это отдельные развертывания или логические группы.

Рисунок 15

Одно из возможных решений — использовать sticky sessions, чтобы клиент всегда отправлял трафик в один и тот же rate limiter. Этот вариант не рекомендуется, поскольку он плохо масштабируется и недостаточно гибок. Более удачный подход — использовать централизованные хранилища данных, такие как Redis. Дизайн показан на рисунке 16.

Изображение показывает архитектурную диаграмму системы rate limiting с использованием Redis. Два клиента — 'Client 1' и 'Client 2' — каждый представлены значками ноутбука и мобильного телефона внутри светло-голубых прямоугольников и отправляют запросы. Запросы каждого клиента проходят через отдельный rate limiter, изображенный в виде светло-голубого шестиугольника с подписью 'Rate limiter 1' и 'Rate limiter 2'. Толстые синие стрелки показывают поток запросов. Затем оба rate limiter отправляют данные в общую базу Redis, изображенную как красный трехслойный куб с геометрическими фигурами сверху и подписью 'Redis'. Стрелки показывают, что оба rate limiter работают с одним и тем же экземпляром Redis. В целом схема демонстрирует архитектуру клиент — rate limiter — база данных, где rate limiter управляют потоком запросов от нескольких клиентов к общему Redis, вероятно для учета частоты запросов и предотвращения злоупотреблений.

Рисунок 16

Оптимизация производительности​

Оптимизация производительности — частая тема на собеседованиях по проектированию систем. Здесь мы рассмотрим два направления улучшения.

Во-первых, конфигурация с несколькими дата-центрами критически важна для rate limiter, потому что для пользователей, находящихся далеко от дата-центра, задержка будет высокой. Большинство облачных провайдеров строят по всему миру множество edge-серверов. Например, по состоянию на 20.05.2020 у Cloudflare было 194 географически распределенных edge-сервера [14]. Трафик автоматически направляется к ближайшему edge-серверу, чтобы уменьшить задержку.

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

Рисунок 17 (Источник: [10])

Во-вторых, нужно синхронизировать данные с использованием модели eventual consistency. Если вы не до конца понимаете, что такое eventual consistency, обратитесь к разделу “Consistency” в главе “Design a Key-value Store”.

Мониторинг​

После того как rate limiter введен в работу, важно собирать аналитические данные, чтобы понимать, насколько он эффективен. Прежде всего мы хотим убедиться в двух вещах:

  • Алгоритм rate limiting действительно эффективен.

  • Правила rate limiting действительно эффективны.

Например, если правила слишком строгие, будет отбрасываться слишком много валидных запросов. В таком случае мы захотим немного ослабить правила. В другом примере мы замечаем, что при резком росте трафика, например во время флеш-распродаж, наш rate limiter становится неэффективным. В такой ситуации можно заменить алгоритм на тот, который лучше поддерживает всплески трафика. Хорошим вариантом здесь является token bucket.

Шаг 4 — Завершение​

В этой главе мы обсудили разные алгоритмы rate limiting и их плюсы/минусы. Рассмотренные алгоритмы включали:

  • Token bucket

  • Leaking bucket

  • Fixed window

  • Sliding window log

  • Sliding window counter

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

  • Жесткое и мягкое ограничение скорости.

  • Жесткое: число запросов не может превышать порог.

  • Мягкое: запросы могут кратковременно превышать порог.

  • Rate limiting на разных уровнях. В этой главе мы говорили только об ограничении на уровне приложения (HTTP: уровень 7). Но rate limiting можно применять и на других уровнях. Например, можно ограничивать запросы по IP-адресам с помощью Iptables [15] (IP: уровень 3). Примечание: модель Open Systems Interconnection (OSI model) имеет 7 уровней [16]: уровень 1 — физический, уровень 2 — канальный, уровень 3 — сетевой, уровень 4 — транспортный, уровень 5 — сеансовый, уровень 6 — представления, уровень 7 — прикладной.

  • Как избежать rate limiting. Проектируйте клиента с учетом лучших практик:

  • Используйте клиентский кэш, чтобы не делать слишком частые вызовы API.

  • Понимайте лимиты и не отправляйте слишком много запросов за короткий промежуток времени.

  • Добавьте код, который перехватывает исключения и ошибки, чтобы клиент мог корректно восстанавливаться после сбоев.

  • Добавьте достаточное время ожидания в логику повторных попыток.

Поздравляем, вы добрались до этого места! Теперь можете похлопать себя по плечу. Отличная работа!

Список литературы​

[1] Rate-limiting strategies and techniques:
https://cloud.google.com/solutions/rate-limiting-strategies-techniques

[2] Twitter rate limits: https://developer.twitter.com/en/docs/basics/rate-limits

[3] Google docs usage limits: https://developers.google.com/docs/api/limits

[4] IBM microservices: https://www.ibm.com/cloud/learn/microservices

[5] Throttle API requests for better throughput:
https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html

[6] Stripe rate limiters: https://stripe.com/blog/rate-limiters

[7] Shopify REST Admin API rate limits:
https://help.shopify.com/en/api/reference/rest-admin-api-rate-limits

[8] Better Rate Limiting With Redis Sorted Sets:
https://engineering.classdojo.com/blog/2015/02/06/rolling-rate-limiter/

[9] System Design — Rate limiter and Data modelling:
https://medium.com/@saisandeepmopuri/system-design-rate-limiter-and-data-modelling-9304b0d18250

[10] How we built rate limiting capable of scaling to millions of domains:
https://blog.cloudflare.com/counting-things-a-lot-of-different-things/

[11] Redis website: https://redis.io/

[12] Lyft rate limiting: https://github.com/lyft/ratelimit

[13] Scaling your API with rate limiters:
https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d#request-rate-limiter

[14] What is edge computing:
https://www.cloudflare.com/learning/serverless/glossary/what-is-edge-computing/

[15] Rate Limit Requests with Iptables: https://blog.programster.org/rate-limit-requests-with-iptables

[16] OSI model:
https://en.wikipedia.org/wiki/OSI_model#Layer_architecture