NGINX великолепен! Вот только его документация по ограничению скорости обработки запросов показалась мне, как бы это сказать, несколько ограниченной. Поэтому я решил написать это руководство по ограничению скорости обработки запросов (rate-liming) и шейпингу трафика (traffic shaping) в NGINX.
Мы собираемся:
- описать директивы NGINX,
- разобраться с accept/reject-логикой NGINX,
- визуализировать обработку всплесков трафика на различных настройках.
Директивы NGINX по ограничению скорости обработки запросов
В этой статье мы будем говорить о ngx_http_limit_req_module, в котором реализованы директивы limit_req_zone
, limit_req
, limit_req_status
и limit_req_level
. Они позволяют управлять значением кода состояния HTTP-запроса для отклоненных (rejected) запросов, а также логированием этих отказов.
Чаще всего путаются именно в логике отклонения запроса.
Сначала нужно разобраться с директивой limit_req
, которой требуется параметр zone
. У него также есть необязательные параметры burst
и nodelay
.
Здесь используются следующие концепции:
zone
определяет «ведро» (bucket) — разделяемое пространство, в котором считаются входящие запросы. Все запросы, попавшие в одно «ведро», будут посчитаны и обработаны в его разрезе. Этим достигается возможность установки ограничений на основе URL, IP-адресов и т. д.burst
— необязательный параметр. Будучи установленным, он определяет количество запросов, которое может быть обработано сверх установленного базового ограничения скорости. Важно понимать, чтоburst
— это абсолютная величина количества запросов, а не скорость.nodelay
— также необязательный параметр, который используется совместно сburst
. Ниже мы разберемся, зачем он нужен.
Каким образом NGINX принимает решение о принятии или отклонении запроса?
При настройке зоны задается ее скорость. Например, при 300r/m
будет принято 300 запросов в минуту, а при 5r/s
— 5 запросов в секунду.
Примеры директив:
|
|
Важно понимать, что эти две зоны имеют одинаковые лимиты. С помощью параметра rate
NGINX рассчитывает частоту и, соответственно, интервал, после которого можно принять новый запрос. В данном случае NGINX будет использовать алгоритм под названием «дырявое ведро» (leaky bucket).
Для NGINX 300r/m
и 5r/s
одинаковы: он будет пропускать один запрос каждые 0,2 с. В данном случае NGINX каждые 0,2 секунды будет устанавливать флаг, разрешающий прием запроса. Когда приходит подходящий для этой зоны запрос, NGINX снимает флаг и обрабатывает запрос. Если приходит очередной запрос, а таймер, считающий время между пакетами, еще не сработал, запрос будет отклонен с кодом состояния 503. Если время истекло, а флаг уже установлен в разрешающее прием значение, никаких действий выполнено не будет.
Нужны ли ограничение скорости обработки запросов и шейпинг трафика?
Поговорим о параметре burst
. Представьте, что флаг, о котором мы говорили выше, может принимать значения больше единицы. В этом случае он будет отражать максимальное количество запросов, которые NGINX должен пропустить в рамках одной пачки (burst).
Теперь это уже не «дырявое ведро», «маркерная корзина» (token bucket). Параметр rate
определяет временной интервал между запросами, но мы имеем дело не с токеном типа true/false, а со счетчиком от 0
до 1 + burst
. Счетчик увеличивается каждый раз, когда проходит рассчитанный интервал времени (срабатывает таймер), достигая максимального значения в b+1
. Напомню еще раз: burst
— это количество запросов, а не скорость их пропускания.
Когда приходит новый запрос, NGINX проверяет доступность токена (счетчик > 0). Если токен недоступен, запрос отклоняется. В противном случае запрос принимается и будет обработан, а токен считается израсходованным (счетчик уменьшается на один).
Хорошо, если есть неизрасходованные burst-токены, NGINX примет запрос. Но когда он его обработает?
Мы установили лимит в 5r/s, при этом NGINX примет запросы сверх нормы, если есть доступные burst-токены, но отложит их обработку таким образом, чтобы выдержать установленную скорость. То есть эти burst-запросы будут обработаны с некоторой задержкой или завершатся по таймауту.
Другими словами, NGINX не превысит установленный для зоны лимит, а поставит дополнительные запросы в очередь и обработает их с некоторой задержкой.
Приведем простой пример: скажем, у нас установлен лимит 1r/s
и burst
равен 3
. Что будет, если NGINX получит сразу 5 запросов?
- Первый будет принят и обработан.
- Поскольку разрешено не больше 1+3, один запрос будет сразу отклонен с кодом состояния 503.
- Три оставшихся будут обработаны один за другим, но не мгновенно. NGINX пропустит их со скоростью
1r/s
, оставаясь в рамках установленного лимита, а также при условии, что не будут поступать новые запросы, которые также используют квоту. Когда очередь опустеет, счетчик пачки (burst counter) снова начнет увеличиваться (маркерная корзина начнет наполняться).
В случае использования NGINX в качестве прокси-сервера расположенные за ним сервисы будут получать запросы со скоростью 1r/s
и ничего не узнают о всплесках трафика, сглаженных прокси-сервером.
Итак, мы только что настроили шейпинг трафика, применив задержки для управления всплесками запросов и выравнивания потока данных.
nodelay
nodelay
говорит NGINX, что он должен принимать пакеты в рамках окна, определенного значением burst
, и сразу их обрабатывать (так же как и обычные запросы).
В результате всплески трафика все же будут достигать сервисов, расположенных за NGINX, но эти всплески будут ограничены значением burst
.
Визуализация ограничений скорости обработки запросов
Поскольку я верю, что практика очень помогает в запоминании чего бы то ни было, я сделал небольшой Docker-образ с NGINX на борту. Там настроены ресурсы, для которых реализованы различные варианты ограничения скорости обработки запросов: с базовым ограничением, с ограничением скорости, использующим burst
, а также с burst
и nodelay
. Давайте посмотрим, как они работают.
Здесь используется довольно простая конфигурация NGINX (она также есть в Docker-образе, ссылку на который можно найти в конце статьи):
|
|
Тестовая конфигурация NGINX с различными вариантами ограничения скорости обработки запросов
Во всех тестах, используя эту конфигурацию, мы отправляем одновременно по 10 параллельных запросов.
Давайте выясним вот что:
- сколько запросов будет отклонено из-за ограничения скорости?
- какова скорость обработки принятых запросов?
Делаем 10 параллельных запросов к ресурсу с ограничением скорости обработки запросов
10 одновременных запросов к ресурсу с ограничением скорости обработки запросов
В нашей конфигурации разрешено 30 запросов в минуту. Но в данном случае 9 из 10 будут отклонены. Если вы внимательно читали предыдущие разделы, такое поведение NGINX не станет для вас неожиданностью: 30r/m
значит, что проходить будет только один запрос в 2 секунды. В нашем примере 10 запросов приходят одновременно, один пропускается, а остальные девять отклоняются, поскольку NGINX они видны до того, как сработает таймер, разрешающий следующий запрос.
Я переживу небольшие всплески запросов к клиентам/конечным точкам
Хорошо! Тогда добавим аргумент burst=5
, который позволит NGINX пропускать небольшие всплески запросов к данной конечной точке зоны с ограничением скорости обработки запросов:
10 одновременных запросов к ресурсу с аргументом burst=5
Что здесь произошло? Как и следовало ожидать, с аргументом burst
было принято 5 дополнительных запросов, и мы улучшили отношение принятых запросов к общему их числу с 1/10 до 6/10 (остальные были отклонены). Здесь хорошо видно, как NGINX обновляет токен и обрабатывает принятые запросы — исходящая скорость ограничена 30r/m
, что равняется одному запросу каждые 2 секунды.
Ответ на первый запрос возвращается через 0,2 секунды. Таймер срабатывает через 2 секунды, один из ожидающих запросов обрабатывается, и клиенту приходит ответ. Общее время, затраченное на дорогу туда и обратно, составило 2,02 секунды. Спустя еще 2 секунды снова срабатывает таймер, давая возможность обработать очередной запрос, который возвращается с общим временем в пути, равным 4,02 секунды. И так далее и тому подобное…
Таким образом, аргумент burst
позволяет превратить систему ограничения скорости обработки запросов NGINX из простого порогового фильтра в шейпер трафика.
Мой сервер выдержит дополнительную нагрузку, но я бы хотел использовать ограничение скорости обработки запросов для предотвращения его перегрузки
В этом случае может оказаться полезным аргумент nodelay
. Давайте пошлем те же самые 10 запросов конечной точке с настройкой burst=5 nodelay
:
10 одновременных запросов к ресурсу с аргументом burst=5 nodelay
Как и ожидалось с burst=5
, у нас останется такое же соотношение 200-х и 503-х кодов состояния. Но исходящая скорость теперь не ограничена одним запросом каждые 2 секунды. Пока доступны burst-токены, входящие запросы будут приниматься и тут же обрабатываться. Скорость срабатывания таймера все так же важна с точки зрения пополнения количества burst-токенов, но на принятые запросы задержка теперь не распространяется.
Замечание. В данном случае zone
использует $request_uri
, но все последующие тесты работают точно так же и для опции binary_remote_addr
, при которой скорость ограничивается по IP-адресу клиента.