Фотография пользователя Wonderlane, Flickr
NGINX великолепен! Вот только его документация по ограничению скорости обработки запросов показалась мне, как бы это сказать, несколько ограниченной. Поэтому я решил написать это руководство по ограничению скорости обработки запросов (rate-liming) и шейпингу трафика (traffic shaping) в NGINX.
Мы собираемся:
- описать директивы NGINX,
- разобраться с accept/reject-логикой NGINX,
- визуализировать обработку всплесков трафика на различных настройках.
В дополнение я создал GitHub-репозиторий и Docker-образ, с которыми можно поэкспериментировать и воспроизвести приведенные в этой статье тесты. Всегда легче учиться на практике.
Директивы 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 запросов в секунду.
Примеры директив:
limit_req_zone $request_uri zone=zone1:10m rate=300r/m;
limit_req_zone $request_uri zone=zone2:10m rate=5/s;
Важно понимать, что эти две зоны имеют одинаковые лимиты. С помощью параметра 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-образе, ссылку на который можно найти в конце статьи):
limit_req_zone $request_uri zone=by_uri:10m rate=30r/m;
server {
listen 80;
location /by-uri/burst0 {
limit_req zone=by_uri;
try_files $uri /index.html;
}
location /by-uri/burst5 {
limit_req zone=by_uri burst=5;
try_files $uri /index.html;
}
location /by-uri/burst5_nodelay {
limit_req zone=by_uri burst=5 nodelay;
try_files $uri /index.html;
}
}
Тестовая конфигурация 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-адресу клиента. У вас будет возможность поиграть с этими настройками, используя специально для этого подготовленный Docker-образ.
Подведем итоги
Попробуем визуализировать то, как NGINX принимает входящие запросы и обрабатывает их на основе параметров rate
, burst
и nodelay
.
Чтобы не усложнять, отобразим количество входящих запросов (которые потом отклоняются либо принимаются и обрабатываются) на определяемой в настройках зоны временной шкале, разбитой на равные значению срабатывания таймера отрезки. Абсолютное значение временного интервала не существенно. Важно количество запросов, которое NGINX может обработать на каждом шаге.
Вот трафик, который мы будем прогонять через разные настройки ограничения скорости обработки запросов:
Входящие запросы и ограничение скорости обработки запросов, заданное для зоны
Принятые и отклоненные запросы (настройка burst не задана)
Без burst (то есть при burst=0
) NGINX выполняет функцию ограничителя скорости. Запросы либо сразу обрабатываются, либо сразу отклоняются.
Если же мы хотим разрешить небольшие всплески трафика, например, чтобы дозагрузить мощности в рамках установленного лимита, тогда можно добавить аргумент burst
, который подразумевает задержку при обработке запросов, принятых в рамках доступных burst-токенов:
Принятые, принятые с задержкой и отклоненные запросы (с использованием burst)
Мы видим, что общее число отклоненных запросов уменьшилось. Были отклонены только те превышающие установленную скорость запросы, которые пришли в моменты, когда не было доступных burst-токенов. С такими настройками NGINX выполняет полноценный шейпинг трафика.
Наконец, NGINX можно использовать для управления трафиком путем ограничения размера пачки запросов (burst), но при этом всплески запросов частично будут доходить до их обработчиков (вышестоящих или локальных), что в конечном счете приведет к менее стабильной исходящей скорости, но улучшит сетевую задержку (если вы, конечно, можете обработать эти дополнительные запросы):
Принятые, обработанные и отклоненные запросы (burst используется с nodelay)
Поиграйте с ограничением скорости обработки запросов
Теперь, чтобы лучше закрепить понимание изложенных концепций, вы можете изучить код, скопировать репозиторий и поэкспериментировать с подготовленным Docker-образом:
https://github.com/sportebois/nginx-rate-limit-sandbox.
Ссылки:
- Оригинал: NGINX rate-limiting in a nutshell.