Фотография пользователя Wonderlane, Flickr NGINX великолепен! Вот только его документация по ограничению скорости обработки запросов показалась мне, как бы это сказать, несколько ограниченной. Поэтому я решил написать это руководство по ограничению скорости обработки запросов (rate-liming) и шейпингу трафика (traffic shaping) в NGINX. Мы собираемся: В дополнение я создал GitHub-репозиторий и Docker-образ, с которыми можно поэкспериментировать и воспроизвести приведенные в этой статье тесты. Всегда легче учиться на практике. В этой статье мы будем говорить о 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 — это абсолютная величина количества запросов, а не скорость. При настройке зоны задается ее скорость. Например, при 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 запросов? В случае использования NGINX в качестве прокси-сервера расположенные за ним сервисы будут получать запросы со скоростью 1r/s и ничего не узнают о всплесках трафика, сглаженных прокси-сервером. Итак, мы только что настроили шейпинг трафика, применив задержки для управления всплесками запросов и выравнивания потока данных. nodelay говорит NGINX, что он должен принимать пакеты в рамках окна, определенного значением burst, и сразу их обрабатывать (так же как и обычные запросы). В результате всплески трафика все же будут достигать сервисов, расположенных за NGINX, но эти всплески будут ограничены значением burst. Поскольку я верю, что практика очень помогает в запоминании чего бы то ни было, я сделал небольшой Docker-образ с NGINX на борту. Там настроены ресурсы, для которых реализованы различные варианты ограничения скорости обработки запросов: с базовым ограничением, с ограничением скорости, использующим burst, а также с burst и nodelay. Давайте посмотрим, как они работают. Здесь используется довольно простая конфигурация NGINX (она также есть в Docker-образе, ссылку на который можно найти в конце статьи): Тестовая конфигурация NGINX с различными вариантами ограничения скорости обработки запросов Во всех тестах, используя эту конфигурацию, мы отправляем одновременно по 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. Ссылки: CEO в Southbridge