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 запросов в секунду.

Примеры директив:

1
2
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-образе, ссылку на который можно найти в конце статьи):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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-адресу клиента.


Источники