Свой модуль в Nginx: где теряется производительность
Модуль Nginx может убрать лишний прокси-слой, но легко превращается в источник задержек и падений под нагрузкой. Разбираем, что измерять в Nginx 1.24.0, где возникают потери и как сравнить реализацию с внешним сервисом.

Модуль ускоряет Nginx только до первой общей блокировки
Собственный модуль в Nginx часто пишут ради одной операции: добавить заголовок, проверить запрос, обратиться к локальному хранилищу или встроить бизнес-правило в обработчик. На стенде это может убрать сетевой переход, но под нагрузкой узкое место обычно появляется в другом месте — в блокировке, выделении памяти, синхронном вводе-выводе или слишком дорогом обработчике на каждом запросе.
Ниже — разбор для Nginx 1.24.0 и совместимого с ним исходного API модулей. Цифры производительности нельзя переносить между проектами: их нужно получить на вашем железе, с вашим размером ответа, TLS, числом рабочих процессов и реальным трафиком.
Главная оптимизация модуля — не более быстрый код внутри обработчика, а отказ от работы, которую не нужно выполнять на каждом запросе.
Как устроен путь запроса
Nginx принимает соединение в рабочем процессе и проводит запрос через цепочку фаз. HTTP-модуль может подключиться к одной из фаз: например, проверить доступ, изменить URI, подготовить данные перед проксированием или сформировать ответ. У модуля нет отдельного потока на каждый запрос. Рабочий процесс обслуживает множество соединений, поэтому блокирующий вызов останавливает не только текущую операцию.
В типичном обработчике есть четыре участка, которые стоит разделить при измерениях:
- разбор входных данных и поиск нужного значения;
- выделение памяти через пул запроса;
- работа с внешним ресурсом: файл, сокет, DNS, база данных или другой сервис;
- создание ответа и передача управления следующей фазе.
Если модуль только добавляет заголовок или читает уже разобранное значение из запроса, его стоимость обычно определяется количеством операций и обращений к памяти. Если он ждёт сеть или файл, стоимость определяется ожиданием, а не скоростью C-кода.
Термин. Пул памяти запроса — область памяти, которую Nginx связывает с конкретным запросом и освобождает после его завершения. Это удобно для временных данных, но опасно для объектов, которые должны пережить запрос.
Что измерять до оптимизации
Сначала зафиксируйте базовую линию без модуля, затем включите модуль в том же окружении. Иначе разница между результатами будет отражать не оптимизацию, а изменение конфигурации или состояния системы.
- Задержку. Снимайте p50, p95 и p99, а не только среднее значение. Средняя задержка скрывает редкие запросы, которые уже тормозят пользователей.
- Пропускную способность. Сравнивайте запросы в секунду при одинаковом числе соединений и одинаковом размере ответа.
- Ошибки. Отдельно учитывайте ответы 4xx, 5xx, разрывы соединений и тайм-ауты. Рост RPS вместе с ростом 5xx не является улучшением.
- Загрузку ресурсов. Нужны CPU по рабочим процессам Nginx, системное время, RSS, количество открытых файлов и сетевой трафик.
- Профиль ожидания. Если CPU свободен, а p99 растёт, ищите блокирующий ввод-вывод, очередь или внешнюю зависимость.
Нагрузочный тест должен использовать прогретый и непрогретый сценарий отдельно. Для файлового кэша, DNS, базы и удалённого сервиса это разные режимы. Запишите версию Nginx, параметры сборки, тип процессора, число worker-процессов, размер запроса и ответа, а также схему сети.
Минимальный набор сравнений
Проведите четыре прогона: Nginx без модуля, Nginx с модулем на «холодном» пути, Nginx с модулем на «тёплом» пути и вариант с внешним сервисом, который решает ту же задачу. Каждый прогон должен длиться достаточно долго, чтобы исчез эффект короткого всплеска, но точную продолжительность выбирайте по стабильности метрик и характеру трафика.
| Вариант | Что показывает | Главный риск | Что фиксировать |
|---|---|---|---|
| Без модуля | Базовую стоимость Nginx и приложения | Слишком оптимистичная база без нужной логики | p95, p99, RPS, CPU, 5xx |
| Модуль в worker-процессе | Цена встроенной логики | Блокировка всего worker-процесса | CPU, задержка, тайм-ауты, RSS |
| Внешний сервис | Цена сетевого перехода и изоляции | Сетевая задержка и отказ зависимости | p99, ошибки upstream, размер очереди |
| Модуль с кэшем | Эффект повторного использования результата | Устаревшие данные и рост памяти | hit/miss, размер кэша, время вытеснения |
Не публикуйте результат в виде «модуль быстрее на 30%», если не указаны условия. Читателю и вашей команде важнее знать, при каком размере ответа и уровне параллельности получена разница.
Где модуль обычно теряет время
Блокирующий ввод-вывод
Самая дорогая ошибка — вызвать из обработчика синхронный системный или сетевой запрос и ждать его завершения. Пока worker-процесс ждёт, он не обслуживает другие события. Один медленный внешний ресурс превращается в очередь внутри Nginx.
Если зависимость нужна для принятия решения, вынесите её за пределы критического пути, используйте заранее подготовленные данные или ограничьте операцию локальным неблокирующим механизмом. Если без внешнего запроса нельзя обойтись, сравните модуль с обычным upstream: отдельный сервис хотя бы изолирует ожидание и позволяет независимо масштабировать его.
Общий кэш и блокировки
Общая память между worker-процессами полезна для небольшого кэша, счётчиков и таблиц быстрых решений. Но одна глобальная блокировка на чтение и запись превращает параллельную обработку в последовательную. Особенно быстро это проявляется на горячем ключе, к которому обращается почти каждый запрос.
Разделяйте данные по сегментам, уменьшайте критическую секцию и не выполняйте под блокировкой парсинг, логирование или выделение больших буферов. Для каждого кэша задайте верхнюю границу размера и стратегию устаревания. Бесконечный кэш — это не оптимизация, а отложенное исчерпание памяти.
Аллокации и копирование
Частые выделения памяти и копирование больших буферов заметны не по одному запросу, а в p99 и профиле CPU. Данные, которые живут только до конца запроса, разумно связывать с пулом запроса. Данные, которые должны пережить запрос, нельзя хранить в памяти этого пула.
Проверяйте, сколько раз значение разбирается и преобразуется. Если один и тот же заголовок или ключ нужен нескольким фазам, подготовьте его один раз и передайте результат дальше. Но не кэшируйте без ограничения строки, зависящие от пользовательского ввода.
Логирование на горячем пути
Подробный лог в обработчике меняет сам объект измерения. Форматирование строки, захват большого тела запроса и синхронная запись могут стоить дороже основной логики. Для диагностики добавляйте счётчики и выборочное логирование, а после теста повторяйте прогон с рабочим уровнем логов.
Как выбирать между модулем и внешним сервисом
Встраивание оправдано, когда решение локальное, короткое и не требует ожидания. Например, это проверка уже доступного признака запроса, преобразование небольшого значения или чтение компактной структуры из общей памяти.
Внешний сервис предпочтительнее, когда логика часто меняется, требует отдельного масштабирования, обращается к базе или должна переживать независимые релизы. Сетевой переход имеет цену, но взамен вы получаете изоляцию отказов, отдельные метрики и возможность обновлять бизнес-правила без пересборки Nginx.
Выбирайте модуль, если одновременно выполняются три условия:
- операция укладывается в короткий CPU-путь и не ждёт сеть;
- данные помещаются в ограниченный кэш или приходят из текущего запроса;
- команда готова тестировать модуль после каждого обновления Nginx.
Во всех остальных случаях сначала оцените вариант с upstream. Экономия одного сетевого перехода не компенсирует падение всего worker-процесса из-за ошибки в модуле.
Что ломается первым
- Долгий внешний вызов. Признак — растущий p99 при невысокой загрузке CPU. Ищите тайм-ауты, очередь и время ответа зависимости.
- Утечка или чрезмерный рост памяти. RSS worker-процессов постепенно увеличивается после одинаковых прогонов. Смотрите жизненный цикл объектов и размер кэша.
- Гонка в общей памяти. Результаты становятся непредсказуемыми только при нескольких worker-процессах. Проверяйте границы блокировок и атомарность счётчиков.
- Неверный переход между фазами. Запрос обрабатывается дважды или модуль конфликтует с rewrite, access или proxy. Тестируйте сочетания директив, а не только изолированный location.
- Несовместимость после обновления. Модуль может собраться, но изменить поведение из-за внутренних структур Nginx. Версию исходников и версию бинарного файла нужно проверять вместе.
Где подход ломается
Модуль не делает Nginx универсальным приложением. Он плохо подходит для тяжёлых вычислений, долгих транзакций, сложной бизнес-логики и задач, которым нужны полноценные библиотеки с независимым жизненным циклом.
Отдельный риск — динамическая сборка. Директива совместимости при сборке не превращает внутренний API Nginx в стабильный публичный контракт. Перед обновлением проверяйте исходный код модуля, собирайте его против целевой версии и прогоняйте тесты на конфигурациях с несколькими фазами обработки.
Не ограничивайтесь успешным ответом 200. Проверяйте отмену клиентом, тайм-аут upstream, большие заголовки, пустое тело, повторное использование keep-alive, несколько worker-процессов и исчерпание дискового или сетевого ресурса.
Что делать дальше
Соберите базовый профиль без модуля и сохраните конфигурацию, бинарную версию и условия теста. Затем добавьте измерения внутри модуля: количество обращений к кэшу, долю попаданий, время внешнего вызова, число ошибок и размер выделенной памяти.
После каждой оптимизации повторяйте тот же сценарий и сравнивайте не только среднее, но и p95, p99, RSS и 5xx. В проде начните с малого процента трафика, поставьте автоматический откат и отдельно наблюдайте worker-процессы. Если метрика ухудшается, сначала убирайте блокирующую операцию, а не переписывайте несколько строк внутреннего кода.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Nginx Development Guide, Исходный код Nginx: HTTP-модули
Частые вопросы
Можно ли заранее назвать прирост производительности от собственного модуля?
Почему модуль с простым кодом может замедлить весь Nginx?
Что лучше: модуль Nginx или отдельный сервис?
Можно ли использовать модуль, собранный для другой версии Nginx?
Как понять, что проблема именно в модуле, а не в приложении?
Нужно ли логировать каждый запрос во время поиска узкого места?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.