1 1 1 1 1 Rating 0.00 (0 Votes)

NGINX великолепен! Вот только его документация по ограничению скорости обработки запросов показалась мне, как бы это сказать, несколько ограниченной. Поэтому я решил написать это руководство по ограничению скорости обработки запросов (rate-liming) и шейпингу трафика (traffic shaping) в NGINX.


Мы собираемся:


  • описать директивы NGINX,
  • разобраться с accept/reject-логикой NGINX,
  • визуализировать обработку всплесков трафика на различных настройках.

Директивы NGINX по ограничению скорости обработки запросов


В этой статье мы будем говорить о ngx_http_limit_req_module, в котором реализованы директивы limit_req_zonelimit_reqlimit_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-адресу клиента.