Search
Write a publication
Pull to refresh
DDoS-Guard
Эксперт в сфере защиты от DDoS-атак

Загадка от Жака Фреско: как построить свой Rate Limiter и не утонуть в море компромиссов

Level of difficultyMedium
Reading time12 min
Views1.8K

В DDoS-Guard мы ежедневно защищаем клиентские сервисы от самых разных атак. Одним из ключевых инструментов защиты становится Rate Limiter — система, которая ограничивает количество запросов от одного пользователя или группы пользователей.

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

На первый взгляд задача кажется простой: «ограничь 100 запросов в минуту». Но когда речь идет о геораспределенной системе с высокими требованиями к задержкам, всё становится сложнее.

Меня зовут Казбек, я тимлид команды разработки защиты на уровне L7. В этой статье я хочу рассказать о том, как мы реализовали Rate Limiter, какие архитектурные решения использовали, почему стандартные подходы не подошли, и как клиенты могут эффективно использовать этот инструмент.

Что такое Rate Limiter?

Основная цель Rate Limiter — защитить ресурсы от чрезмерной нагрузки. Эта нагрузка может быть вызвана как злонамеренными действиями (например, DDoS), так и случайным перерасходом доступа, например, при массовых обращениях к API.

Классический пример использования — ограничение количества HTTP-запросов на определённый URL. Например, можно установить лимит в 100 запросов в минуту на /api/login, чтобы предотвратить брутфорс паролей. Если пользователь превышает лимит, система возвращает код ответа 429 Too Many Requests.

Общая концепция

  1. Получение запроса. Когда клиент отправляет запрос к серверу, первым делом система проверяет, соответствует ли этот запрос условиям правила. Это может быть URL, IP-адрес пользователя, заголовки запроса и так далее. Если запрос не соответствует ни одному из правил, он проходит без дополнительной обработки.

  2. Формирование ключа группировки. Если запрос соответствует правилам, система формирует ключ группировки (grouping key). Этот ключ определяет, какие запросы будут объединены в один лимит. Например, если ключом является IP, то все запросы от одного IP-адреса попадают под один лимит, а если ключом является IP + URL, то запросы от одного IP-адреса к разным URL-ам будут подпадать под разные лимиты. Это позволяет гибко настраивать лимиты для разных групп пользователей или типов запросов.

  3. Проверка наличия токенов. Сформированный ключ используется для обращения к токен-бакету (token bucket). Токен-бакет — это простая модель, которая хранит текущее количество доступных токенов для данного ключа. Каждый запрос требует один токен.

    Если в бакете есть достаточное количество токенов:

    • Система выдает токен клиенту.

    • Уменьшает количество доступных токенов в бакете.

    • Пропускает запрос дальше.

    Если токенов недостаточно:

    • Система блокирует запрос.

    • Отправляет клиенту код ответа 429 Too Many Requests.

Наглядная схема лимитирования запросов
Наглядная схема лимитирования запросов

Формы реализации Rate Limiter

Rate Limiter может быть реализован разными способами, в зависимости от масштаба, потребностей бизнеса и технических возможностей. В целом можно выделить три основные формы.

1. Встроенный программный модуль

Это когда компания разрабатывает свой собственный Rate Limiter как часть своей внутренней инфраструктуры. Такой подход встречается довольно часто, когда важны гибкость, производительность и контроль над кодом.

Плюсы

Минусы

Прямая интеграция с веб-приложением

Разработка собственного решения

Возможна любая кастомизация: правила, логика, метрики

Поддержка и обновление

Полный контроль над состоянием системы

2. Модуль обратного прокси (Nginx, HAProxy и т.п.)

Для малого и среднего бизнеса достаточно часто используется подход, при котором rate limiter внедряется как часть reverse proxy. Например, Nginx предоставляет встроенные механизмы ограничения частоты запросов.

Плюсы

Минусы

Простота настройки

Ограниченная гибкость

Надежность и проверенность решений

Ограниченная поддержка распределённого состояния

Интеграция в уже существующую цепочку обработки трафика

Этот вариант хорошо подходит для сервисов, которые не сталкиваются с высокими нагрузками или геораспределением, и где главное — простота и скорость внедрения.

3. Cloud-решение (SaaS)

Еще один популярный подход — использование готовых облачных решений, таких как Cloudflare, AWS WAF, Google Cloud Armor и других. Они предоставляют мощные инструменты настройки через UI или API, а также берут на себя заботу об обновлениях, масштабировании и поддержке.

Плюсы

Минусы

Не нужно ничего разрабатывать самостоятельно

Стоимость использования

Готовые интерфейсы управления

Гибкость системы зависит от провайдера

Высокая доступность и отказоустойчивость

Политическая и юридическая зависимость от провайдера

Такое решение отлично подходит тем, кто хочет быстро внедрить защиту без больших усилий и доверяет сторонним платформам.

Почему мы решили разработать свой Rate Limiter?

Когда мы начали работу над реализацией собственного Rate Limiter’а, первым делом изучили существующие подходы и популярные схемы. Мы хотели найти готовое решение или как минимум понять, как можно адаптировать стандартные практики под наши задачи.

Поиск типовой архитектуры

В интернете легко найти примеры реализации Rate Limiter’ов. Один из самых распространённых вариантов — использование базы данных Redis в качестве центрального хранилища токен-бакетов. В этой схеме:

  • Запрос проходит через балансировщик нагрузки.

  • Нода Rate Limiter'а проверяет состояние бакета в Redis.

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

  • Иначе возвращается 429 Too Many Requests.

Пример стандартной архитектуры с использованием Redis
Пример стандартной архитектуры с использованием Redis

На первый взгляд, это простое и понятное решение. Однако при масштабировании и геораспределении оно сталкивается с рядом серьезных проблем.

Проблема задержек из-за удаленного хранилища

Если ваша инфраструктура географически распределена (например, дата-центры находятся в разных странах), обращение к централизованному хранилищу становится узким местом по задержкам. При разработке распределенной системы хранилище будет разбито на шарды, из-за чего данные будут размазаны по всем датацентрам. Из-за этого запрашиваемый ключ у БД может оказаться в другой локации, что повлечет за собой сетевые задержки. Вот как это может выглядеть на практике:

  • Запрос приходит в дата-центр в Москве.

  • Чтобы проверить лимит, система должна обратиться к redis ноде в Германии.

  • Это занимает порядка 50 мс, что неприемлемо для нас.

Почему? Потому что Rate Limiter лишь один из множества модулей фильтрации в нашей цепочке. Каждый шаг должен быть максимально быстрым, чтобы не ухудшать пользовательский опыт. Если юзер получает задержку в 50 мс только на этапе проверки лимита, он может просто уйти.

Потери производительности из-за удаленного взаимодействия
Потери производительности из-за удаленного взаимодействия

Принцип работы нашего Rate Limiter

Как я уже говорил ранее, ключевой задачей при разработке нашего Rate Limiter’а было минимизировать сетевые задержки и обеспечить отказоустойчивость в географически распределенной системе. Чтобы этого достичь, мы отказались от традиционных решений вроде Redis-бэкенда и построили собственную систему хранения данных, оптимизированную под нашу специфику.

Локальное и удаленное состояние

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

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

Распределение бакетов между нодами

Чтобы определить, какая нода является «владельцем» конкретного бакета, мы используем консистентное хеширование. Это позволяет равномерно распределить нагрузку между всеми участниками кластера и минимизировать количество перераспределений при добавлении или удалении нод.

Про хеширование

Более подробно на теме алгоритма консистентного хеширования останавливаться не будем. В интернете можно найти много полезного материала, вбив в поисковую строку “Консистентное хеширование”, “Согласованное хеширование” и так далее.

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

Для отслеживания текущего состояния кластера мы используем систему обнаружения сервисов Consul от Hashicorp. Если говорить коротко: сервис можно зарегистрировать в Consul, который записывает в каталог адрес ноды. Затем Consul обновляет запись каталога экземпляра с результатами каждой проверки работоспособности, которую он выполняет. То есть если экземпляр сервиса на какой-то из нод перестал отвечать, то запись о нем будет убрана из каталога.

Агенты Consul’а реплицируют эту информацию каталога между собой, используя протокол консенсуса Raft, что обеспечивает высокую доступность сети служб через любого агента Consul. Это помогает нам оперативно реагировать на изменения в топологии сети, такие как добавление новых нод или временная недоступность существующих.

Синхронизация с удаленным бакетом

Процесс обработки разбит на три независимых потока:

  1. Обработка запроса от клиента (черная стрелка)

  2. Синхронизация с удаленным бакетом владельца (синяя штриховая линия)

  3. Обновление локального состояния (синяя сплошная стрелка)

Такое разбиение не блокирует обработку запроса от клиента.

Примерно так мы разбили процесс на три потока
Примерно так мы разбили процесс на три потока

Рассмотрим первый поток. Процесс извлечения токена состоит из нескольких шагов:

  1. Запрос попадает в ноду одного из дата-центров.

  2. Определяется ключ группировки (например, IP-адрес клиента).

  3. Если токены есть, запрос пропускается, а токен списывается с локального бакета.

  4. Далее определяется, принадлежит ли бакет этой ноде или нет. Если локальный бакет не является владельцем, система планирует извлечение токенов у владельца через Sync Scheduler.

Sync Scheduler — это внутренний модуль, который периодически отправляет все запланированные изменения владельцу бакета. Это позволяет синхронизировать локальное состояние с актуальным без блокировки основного потока.

Процесс извлечения токена из локального бакета
Процесс извлечения токена из локального бакета

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

Далее у нас на фоне идет синхронизация, когда Sync Scheduler отправляет все запланированные запросы владельцу.

Простая иллюстрация процесса взаимодействия между планировщиком и владельцем ноды
Простая иллюстрация процесса взаимодействия между планировщиком и владельцем ноды

Ну и далее последний поток отвечает за актуализацию локального состояния, когда владелец сообщает свое состояние после синхронизации.

Инертность системы

Из-за асинхронного характера обмена данными между нодами и владельцем бакета возникает инертность, то есть временная несогласованность данных между локальными копиями и удаленным хранилищем. Это означает, что локальное состояние может немного отличаться от актуального состояния владельца бакета на момент обработки запроса.

Например, если клиент делает несколько быстрых запросов, локальная нода может считать, что токены еще доступны, хотя владелец уже израсходовал их. В результате клиент может получить больше токенов, чем разрешено правилами. Однако это происходит только в течение нескольких миллисекунд, так как система довольно быстро синхронизируется.

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

Потеря сообщений

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

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

  1. Скорость важнее. Использование механизмов повторных попыток (retry) или гарантий доставки может привести к значительному увеличению задержек, что для нас является проблемой.

  2. Ограниченные временные рамки. Обновление токенов ограничено временными интервалами (например, 1 минута). Это означает, что даже при потере сообщения система в конечном счете восстановит согласованность через следующее обновление.

  3. Реалистичная вероятность. Ситуации с потерей сообщений действительно очень редки, и их влияние минимально для большинства клиентов.

Восстановление после перезагрузки ноды

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

Для этого система ведет специальную таблицу — лог синхронизаций, которая хранит все изменения, произведенные с бакетами за определенный период времени.

Каждый владелец фиксирует свои изменения
Каждый владелец фиксирует свои изменения

При перезапуске нода проверяет эту таблицу и восстанавливает свое состояние, используя последние записи.

Node 3 вывели на время из эксплуатации
Node 3 вывели на время из эксплуатации
Как только Node 3 вернули, Rate Limiter начинает восстанавливать свое последнее состояние
Как только Node 3 вернули, Rate Limiter начинает восстанавливать свое последнее состояние

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

Создание и настройка правил

Настройка правил в Rate Limiter — это ключевой аспект, который позволяет клиентам гибко адаптировать защиту сайта под свои бизнес-задачи. Мы предоставляем интуитивно понятный интерфейс для создания и настройки правил, которые могут быть как простыми, так и сложными, в зависимости от потребностей клиента.

Каждое правило состоит из нескольких основных частей:

Шаг 1: Установка ограничений

Здесь клиент указывает:

  • Число запросов: сколько раз за определённый период времени может быть выполнен запрос, соответствующий данному правилу.

  • Временной интервал: за какой промежуток времени учитывается это число (например, 300 запросов в минуту).

  • Действие при превышении лимита: что делать с запросами, если лимит исчерпан.

Пример:

«Ограничить до 300 запросов в минуту. Если больше, то вернуть статус 429 Too Many Requests»

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

Шаг 2: Условия применения правила

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

Возможные условия:

  • HTTP-метод: GET, POST, PUT и т. д.

  • Путь URL: например, /catalog/, /api/v1/login и т. д.

  • Значение заголовков: User-Agent, Referer и т. д.

  • IP-адрес клиента

  • Страна запроса

Пример:

«Применять правило только к тем запросам, путь которых начинается со слова /catalog, и страна запроса не Россия»

Шаг 3: Группировка запросов

Этот шаг определяет, по какому ключу будут считаться лимиты. То есть, какие запросы считаются принадлежащими одному бакету, и соответственно, будут ограничиваться вместе.

Что можно использовать в качестве ключа:

  • Один параметр: IP-адрес, Host (домен) и т. д.

  • Комбинация нескольких параметров, например:

    • IP + Host

    • User-Agent + Host

    • IP + путь URL

Пример:

«Группировать запросы по комбинации IP-адреса клиента и домена, чтобы в каждом поддомене лимит считался отдельно»

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

Аналитика

Аналитика позволяет клиентам понять, как их правила влияют на трафик и насколько эффективно работает система защиты. Мы предоставляем два основных графика для анализа:

  1. Распределение групп по количеству запросов

  2. Распределение групп по остатку лимита

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

1. Распределение групп по количеству запросов

Этот график показывает, сколько запросов было сделано каждой группой пользователей (бакетами) за определенный период времени. По оси X отображается время, а по оси Y количество запросов. Цвет каждого квадрата указывает количество групп, которые соответствуют этому уровню активности: светлее — меньше групп, темнее — больше.

2. Распределение групп по остатку лимита

Этот график показывает, сколько токенов осталось в каждом бакете после обработки запросов. По оси X — время, по оси Y — остаток лимита. Цвет каждого квадрата указывает количество групп, которые соответствуют этому уровню активности: светлее — меньше групп, темнее — больше.

Примеры интерпретации графиков

Давайте разберем несколько типичных сценариев, которые могут возникнуть при анализе этих графиков.

Сценарий 1: Типичная ситуация

  • Распределение по количеству запросов: большинство групп показывает низкую активность. Это нормально, так как большинство пользователей делают небольшое количество запросов.

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

Сценарий 2: Ложные срабатывания

  • Распределение по количеству запросов: нормальное распределение, большинство групп показывает низкую активность.

  • Распределение по остатку лимита: многие группы упираются в ноль (темные цвета), хотя активность запросов не очень высока.

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

Сценарий 3: Слишком мягкие лимиты

  • Распределение по количеству запросов: низкая активность у большинства групп.

  • Распределение по остатку лимита: остаток лимита всегда находится в верхней части графика.

Сценарий 4: Выявление ботов

  • Распределение по количеству запросов: большинство групп демонстрируют высокую интенсивность запросов, примерно в диапазоне 55–77 запросов в секунду. Это резко отличается от типичной картины, где большинство пользователей делают небольшое количество запросов (обычно около нуля). Такая стабильная и высокая активность указывает на автоматизированный трафик.

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

Вывод: такая четкая периодичность и высокая частота запросов указывают на работу ботов или индексаторов (например, Яндекс ботов, Googlebot или других поисковых систем).

Заключение

Rate Limiter — это как сканер в аэропорту: если ты прошёл через него без замечаний, ты даже не заметил, что тебя проверили. А вот если ты пытался протащить тапки на борт — добро пожаловать в 429.

Он может показаться простым инструментом, но за этим «простым» скрывается целый мир компромиссов. Это место, где скорость важнее консистентности, отказоустойчивость важнее контроля, а гибкость важнее идеальной точности. И да, иногда система работает немного «по инерции». Но разве в реальном мире все идеально?

Мы не делали Rate Limiter по учебнику. Мы делали его для мира, где миллисекунды решают все, где клиенты хотят гибкости, а серверы могут упасть. Для мира, где запросы летят из Москвы в Нью-Йорк быстрее, чем ты успеваешь сказать “Hello”, и где боты могут ходить так плотно, что их можно вычислить по графику.

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

А мы будем дальше развивать наш Rate Limiter — изучать данные, учиться на ошибках и слушать фидбек клиентов. Потому что лучший Rate Limiter — тот, который работает незаметно… но блокирует именно тех, кого нужно.

Tags:
Hubs:
+11
Comments2

Articles

Information

Website
ddos-guard.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
netguard