1. 1
  2. 2
  3. 3
  4. 4

# теория · шаг 2 из 4

Сколько воркеров ставить

Коротко

Число процессов и предел соединений задаются разными директивами.

  • worker_processes находится в главном контексте; auto выбирает число процессов по доступным ядрам.
  • worker_connections задаётся внутри events и ограничивает соединения одного воркера.
  • Проксирование обычно занимает соединение с клиентом и соединение с бэкендом. Поэтому произведение настроек не равно числу одновременно обслуживаемых клиентов.
  • Файловый дескриптор - номер открытого файла или сокета в процессе. Число доступных дескрипторов тоже ограничено. Сверяй настройки с метриками и сообщениями error_log.

Если worker_connections и nofile - знакомая пара, листай до «Теперь сам».

Сколько клиентов выдержит вот этот конфиг

/etc/nginx/nginx.confглавный контекст
worker_processes 4;

events {
    worker_connections 4096;
}

Наивный ответ: 4 × 4096 = 16 384 клиента.

Правильный ответ: зависит от того, чем занят nginx. Если он отдаёт файлы с диска - да, около шестнадцати тысяч. Если он работает обратным прокси - вдвое меньше, примерно восемь тысяч. Почему так - дальше.

На что это похоже

Call-центр, у оператора на пульте сорок линий.

Пока оператор просто отвечает на вопросы, сорок линий - это сорок клиентов. Но если по каждому звонку он сам набирает бухгалтерию и держит обе трубки, то на одного клиента уходит две линии, и клиентов помещается двадцать.

worker_connections - это линии на пульте, а не люди на другом конце. Считать надо соединения, и в режиме прокси их на каждого клиента два: одно с клиентом, второе с бэкендом.

Механизм: что считается соединением

Слот worker_connections тратится не только на клиентов. В счёт идут:

  • соединение с клиентом - всегда;
  • соединение с бэкендом - когда nginx проксирует;
  • слушающие сокеты (listen) - по одному на каждый, немного, но они есть;
  • соединения, которые клиент держит открытыми после ответа (keepalive), - они ещё живы и слот занимают.

Последний пункт неочевиден и на практике съедает больше всего: браузер удерживает соединение секунды после загрузки страницы, чтобы не устанавливать его заново.

Правило

worker_processes auto и worker_connections в несколько тысяч закрывают почти все случаи.

Грубая оценка числа одновременных проксируемых запросов - worker_processes × worker_connections ÷ 2: одно соединение с клиентом, другое с бэкендом. Это не гарантированный предел клиентов: часть слотов занимают другие соединения, а файловые дескрипторы ограничены отдельно.

Числа трогают после метрики, а не до. Если active connections болтается на сотне при потолке в шестнадцать тысяч, тюнинг этих строк не даст ничего, и проблема где-то ещё.

Разбор: считаем потолок целиком

Возьмём боевой случай: nginx проксирует магазин, worker_processes auto на 8-ядерной машине, worker_connections 4096.

слотов всего:      8 × 4096 = 32 768
на клиента нужно:  2 (клиент + бэкенд)
потолок клиентов:  32 768 ÷ 2 = 16 384

Теперь второе ограничение. Каждое соединение - это открытый файловый дескриптор, а их число ограничено на процесс:

$  команда
cat /proc/$(pgrep -f 'nginx: worker' | head -1)/limits | grep 'open files'
↳  выводcat /proc/$(pgrep -f 'nginx: worker' | head -1)/limits | grep 'open files'
Max open files            1024                 1024                 files

Тысяча двадцать четыре против четырёх тысяч заявленных. То есть настоящий потолок здесь - не 4096 на воркер, а 1024, и при подходе к нему в error_log появится:

worker_connections exceed open file resource limit: 1024

Лимит живёт в двух местах, и важно понимать, какое из них главнее:

/etc/nginx/nginx.confглавный контекст
worker_rlimit_nofile 8192;

worker_rlimit_nofile задаёт лимит РАБОЧИМ процессам, и поднять его можно в пределах ЖЁСТКОГО лимита, с которым запущен главный процесс. Жёсткий лимит выдаёт systemd (LimitNOFILE в юните), и у современных систем он большой: на Ubuntu 22.04 с systemd 249 systemctl show -p DefaultLimitNOFILE печатает 524288, а мягкий - 1024. То есть в типичном случае ОДНОЙ директивы в nginx.conf достаточно, юнит править не нужно.

Юнит нужен тогда, когда хочется выйти за жёсткий лимит - или когда дистрибутив выставил его низким. Замер на стенде показывает оба случая: контейнер запущен с лимитами 1024 мягкий и 4096 жёсткий.

↳  выводMax open files у рабочего процесса, /proc/PID/limits
без worker_rlimit_nofile          soft=1024  hard=4096
worker_rlimit_nofile 3000, reload soft=3000  hard=3000   ← reload применил
worker_rlimit_nofile 9000, reload soft=1024  hard=4096   ← откат, см. лог
↳  вывод/var/log/nginx/error.log
[alert] setrlimit(RLIMIT_NOFILE, 9000) failed (1: Operation not permitted)

Отсюда два вывода, которые стоит запомнить дословно. Первый: worker_rlimit_nofile применяется ПО RELOAD - новые рабочие процессы получают новый лимит, перезапуск службы не нужен. Второй: выше жёсткого лимита попросить нельзя - setrlimit отказывает, и рабочий процесс молча остаётся со старым значением. Ошибка видна только в error_log, и только с уровнем alert.

Что такое юнит systemd

Юнит systemd - текстовый файл, описывающий службу: чем её запускать, когда перезапускать и какие лимиты ядра ей выставить. Юнит nginx лежит в /lib/systemd/system/nginx.service, но править его руками не надо - обновление пакета перезапишет файл. Свои строки кладут отдельным довеском:

$  команда
sudo systemctl edit nginx

Команда открывает пустой файл /etc/systemd/system/nginx.service.d/override.conf, куда и пишется добавка:

/etc/systemd/system/nginx.service.d/override.confглавный
[Service]
LimitNOFILE=8192

После правки нужен sudo systemctl daemon-reload и полный sudo systemctl restart nginx: ЖЁСТКИЙ лимит выдаётся процессу при старте, и reload его не поднимает. А вот worker_rlimit_nofile внутри этого жёсткого лимита reload применяет - это проверено замером выше.

Разбор: где формула врёт

Из первого разбора легко унести «auto всегда правильно». Вот случай, где auto даёт заведомо плохое число.

nginx запущен в контейнере с ограничением в половину ядра на машине с 64 ядрами. auto считает ядра хоста, потому что видит их в /proc/cpuinfo:

worker_processes auto  →  64 воркера
доступно процессорного времени  →  0,5 ядра

Шестьдесят четыре процесса делят половину ядра. Каждый со своим набором соединений, каждому нужна память под буферы, и все они переключаются друг с другом. Итог хуже, чем от одного воркера.

Проверить, что видит контейнер, можно прямо из него:

$  команда
docker exec lab nproc
docker exec lab cat /sys/fs/cgroup/cpu.max

Если nproc показывает 64, а cpu.max говорит 50000 100000 (то есть полъядра), число воркеров надо ставить руками. Сам nginx квоту НЕ СМОТРИТ - проверено замером на 1.31.6:

↳  выводконтейнер с --cpus=2 на четырёхъядерной машине
nproc внутри контейнера:                    4
/sys/fs/cgroup/cpu.max:                     200000 100000   (квота = 2 ядра)
воркеров при worker_processes auto:         4               ← квота не учтена

auto считает число ONLINE-процессоров, и всё. Это открытый запрос в репозитории nginx, а не особенность сборки.

А вот у ОФИЦИАЛЬНОГО образа Docker есть свой автотюнинг - отдельный скрипт /docker-entrypoint.d/30-tune-worker-processes.sh, который читает и cpuset, и квоту (и v1, и v2), берёт минимум и переписывает директиву в nginx.conf. Включается он переменной окружения:

$  команда
docker run -e NGINX_ENTRYPOINT_WORKER_PROCESSES_AUTOTUNE=1 --cpus=2 nginx:1.31.6-alpine
↳  выводчто стало в /etc/nginx/nginx.conf
без переменной:  worker_processes  auto;     → 4 воркера при --cpus=2
с переменной:    worker_processes 2;         → 2 воркера

Разницу стоит держать в голове: это поведение ОБРАЗА, а не nginx. На своей сборке, в пакете из репозитория дистрибутива и в чужом образе этого скрипта нет, и число воркеров под квотой задаётся руками.

Как понять, что упёрся

Включи страницу статуса:

/etc/nginx/conf.d/shop.local.confhttp › server
location = /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}
$  команда
curl -s http://127.0.0.1/nginx_status
↳  выводcurl -s http://127.0.0.1/nginx_status
Active connections: 291
server accepts handled requests
 16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106

Смотреть надо на две вещи. Active connections против потолка - если подобралось, пора поднимать. И второе число в средней строке (handled) против первого (accepts): если handled меньше, значит соединения отбрасывались, и это и есть упёршийся потолок.

Что ломается без этого

worker_connections are not enough в error_log - слоты кончились. Клиенты в этот момент получают отказ или таймаут.

too many open files - кончились дескрипторы. То же самое, но упёрлось раньше, в системный лимит.

«В контейнере nginx медленнее, чем на голой машине» - почти всегда worker_processes auto, насчитавший ядра хоста.

Зачем это в работе

Первое - разбор отказов под нагрузкой. Когда сайт начинает отбрасывать соединения, stub_status за секунду отвечает, упёрлись слоты или нет, и это разводит две совершенно разные ветки расследования: настройки nginx против проблем бэкенда.

Второе - переезд в контейнеры. Конфиг, годами работавший на железе, в контейнере с квотой ведёт себя иначе, и worker_processes auto - первое, что стоит проверить, а не последнее.

Теперь сам

nginx проксирует API. worker_processes auto на 4 ядрах, worker_connections 2048, worker_rlimit_nofile не задан, системный лимит 1024. Сколько одновременных клиентов реально выдержит сервер?

Ответ. Заявленный потолок - 4 × 2048 = 8192 слота, то есть 4096 клиентов в режиме прокси. Но каждый воркер упрётся в 1024 дескриптора раньше, чем в 2048 слотов, поэтому реально это 4 × 1024 = 4096 слотов и около двух тысяч клиентов. Причём задолго до этого числа в error_log посыплется worker_connections exceed open file resource limit. Чинится поднятием worker_rlimit_nofile - и reload этого достаточно, пока запрошенное число не выше жёсткого лимита главного процесса. Если выше (setrlimit ... failed в error_log) - сначала LimitNOFILE в юните и полный перезапуск.

Главное

Ёмкость nginx ограничивают и worker_connections, и доступные файловые дескрипторы. Проксирование обычно требует соединения с клиентом и соединения с бэкендом. Прежде чем увеличивать настройки, найди достигнутый предел в метриках и логах; затем проверь жёсткий лимит процесса, запущенного systemd.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.