Сервер под сильной нагрузкой порой ведёт себя странно: процессоры почти спят, память свободна, а посетители жалуются на медленную загрузку. Знакомая картина для Nginx с нетронутым конфигом: исходные умолчания дают воркеру всего 512 соединений, пакет Ubuntu поднимает планку лишь до 768, очередь приёма ядра ограничена 511, кэш открытых файлов выключен. Железо справляется, а программные лимиты не пускают. Хорошие новости: почти всё упирается в несколько директив и подтяжка занимает один вечер. Статья идёт от симптомов к цифрам: где увидеть упор в лимит, как посчитать worker_connections под свой пик, зачем нужны keepalive и sendfile, что даёт кэш файлов и как проверить результат тестом.

Симптомы упора в лимиты видны в журнале ошибок Nginx

При исчерпании лимитов Nginx не молчит, а пишет в error.log предупреждения уровня alert. Классическая пара строк:

[alert] 4231#4231: *6 worker_connections are not enough
[alert] 4231#4231: accept() failed (24: Too many open files)

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

Второй инструмент - встроенная страница метрик stub_status:

location = /stub_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

Типичный ответ:

Active connections: 291
server accepts handled requests
 1568423 1568423 4871022
Reading: 6 Writing: 1 Waiting: 284

Расшифровка: Active - открытые соединения прямо сейчас, accepts и handled - принятые и обслуженные, Reading - читающие запрос, Writing - отдающие ответ, Waiting - держат соединение открытым ради keepalive, ничего не передавая. Для дальнейшего важны два числа: Active в час пика и доля Waiting.

Третий взгляд - со стороны системы:

ss -s
cat /proc/$(cat /run/nginx.pid)/limits | grep "open files"

Первая команда покажет общее число открытых сокетов, вторая - фактический лимит файлов, внутри которого живут воркеры. Если Active в пике упирается в произведение числа воркеров на worker_connections, диагноз готов.

Worker_connections и worker_processes считают от пиковой нагрузки

Число воркеров задаёт директива worker_processes, и значение auto создаёт по одному процессу на каждое логическое ядро - ровно то, что нужно для приёма и отдачи. Пакет Ubuntu дополнительно закрепляет воркеры за ядрами через worker_cpu_affinity auto.

Тонкость лимита worker_connections из документации: в него входят все соединения воркера, включая исходящие к бэкендам, а не только клиентские. Запрос, передаваемый бэкенду, занимает два слота сразу - входящий и исходящий, поэтому серверу с proxy_pass или FastCGI нужен вдвое больший запас, чем чисто раздающей статике.

Расчёт стартовых значений собирают в четыре шага:

  1. подсчитать ядра командой nproc - столько воркеров создаст auto;
  2. снять пик одновременных соединений из stub_status в час наплыва;
  3. разделить пик на число воркеров и добавить запас в полтора-два раза;
  4. сверить итог с лимитом открытых файлов и запасом памяти сервера.

Конфигурация для сервера на 8 ядрах с пиком около 20 тысяч соединений:

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    multi_accept on;
}

Восемь воркеров по 4096 соединений покрывают такой пик с запасом даже с учётом исходящих к бэкендам. Директива worker_rlimit_nofile обязательна: фактическое число одновременных соединений не может превысить лимит открытых файлов, это прямое требование документации. После reload фактический предел проверяют через /proc.

Память - второй ограничитель. Пустое keepalive-соединение стоит копейки, активное с буферами - десятки килобайт, передаваемое бэкенду с телом запроса и ответа - заметно больше. Лимит планируют от свободной памяти, а не от круглого числа из чужого конфига: 100 тысяч соединений на воркер на маленьком VPS превращаются в падение при первом наплыве.

Keepalive держит соединение открытым и экономит рукопожатия TCP

Каждое новое TCP-соединение - это тройное рукопожатие плюс медленный старт: сеть тратит лишние миллисекунды до первого байта, ядро - ресурсы на обработку SYN-пакетов. Keepalive разрешает браузеру переиспользовать уже открытое соединение для следующих запросов. Умолчания щедрые: keepalive_timeout держит соединение 75 секунд после ответа, keepalive_requests разрешает до 1000 запросов на соединение.

Для публичного сайта 75 секунд - расточительство. Браузер держит до шести соединений на хост, и после ухода посетителя со страницы они продолжают висеть, занимая слоты воркера. Именно они отображаются как Waiting в stub_status. Пара тысяч посетителей за вечер - уже больше десяти тысяч висящих сокетов, которые ничего не передают, но мешают принимать новых клиентов. Рабочие значения:

keepalive_timeout 30s;
keepalive_requests 1000;

Тридцати секунд хватает на типичную сессию страницы, лимит запросов предохраняет от соединений, застрявших на плохой сети.

Вторая половина выгоды - соединения Nginx с бэкендом. Без явной настройки каждый запрос к upstream открывает новое соединение и закрывает его после ответа. Пул keepalive в блоке upstream решает это:

upstream app {
    server 127.0.0.1:9000;
    keepalive 32;
}

location / {
    proxy_pass http://app;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

Директива keepalive держит до 32 простаивающих соединений к бэкенду наготове, а заголовок Connection с пустым значением запрещает модулю передачи запросов дописывать Connection close. Для FastCGI аналогично работает fastcgi_keep_conn on. Бэкенд перестаёт тратить время на установку соединений и отвечает стабильными миллисекундами.

В копилку - HTTP/2: мультиплексирование несёт десятки запросов в одном соединении, снижая и число слотов, и очередь рукопожатий. Достаточно включить http2 в настройках сервера - и браузеры сами воспользуются.

Sendfile с tcp_nopush ускоряют отдачу больших файлов

Без sendfile Nginx читает данные с диска в свой буфер и отдаёт их ядру для отправки в сокет - лишнее копирование на каждый фрагмент файла. Директива sendfile on просит ядро передавать страницы файла прямо в сокет, мимо буферов процесса. Экономятся память и процессорное время, на больших файлах выигрыш становится существенным.

Пара к ней - tcp_nopush: сокет затыкается пробкой до заполнения полноразмерных пакетов, и заголовки ответа с первыми кусками файла уезжают одним крупным сегментом. Третья строка, tcp_nodelay on, стоит по умолчанию и гарантирует мгновенную отдачу хвоста ответа. Классическая тройка:

sendfile on;
tcp_nopush on;
tcp_nodelay on;

В пакете Ubuntu sendfile и tcp_nopush уже включены, у исходного Nginx оба выключены - перед тюнингом достаточно проверить их наличие.

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

Два предостережения: sendfile помогает только файлам на локальном диске, ответы бэкендов идут мимо него, а на сетевых файловых системах вроде NFS прямая передача может конфликтовать с кэшем страниц - там решают по собственному замеру.

Кэш открытых файлов снижает число системных вызовов

Без кэша каждый запрос к статике - цепочка системных вызовов: открыть файл, прочитать метаданные, прочитать данные, закрыть. Для горячих файлов с сотнями запросов в минуту это чистые траты. Директива open_file_cache включает кэш дескрипторов и метаданных:

open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

Смысл параметров: max ограничивает кэш десятью тысячами записей, inactive выбрасывает запись после 30 секунд без обращений, valid заставляет раз в 60 секунд перепроверять метаданные, min_uses держит файлы минимум с двумя обращениями, а errors кэширует и отрицательные ответы вроде "файл не найден". По умолчанию механизм выключен целиком, перепроверка по умолчанию идёт раз в 60 секунд - эти же значения служат разумной отправной точкой.

Выигрыш зависит от доли статики: на раздаче картинок и скриптов счётчик системных вызовов падает в разы, процессор уходит с накладных расходов на полезную работу.

Обратная сторона - развёртывания. Новый файл под старым адресом до ближайшей перепроверки может не существовать для воркеров, а при включённом errors свежий ассет иногда минуту отвечает 404. Практические решения известны: версионные имена файлов, reload Nginx сразу после выката, короткий inactive в дни частых релизов. С переключением релизных символьных ссылок та же история: воркеры держат открытые дескрипторы старых файлов, и без reload сайт недолго отдаёт смесь двух версий.

События epoll и очередь приёма соединений на стороне ядра

Приём соединений на Linux крутится на epoll, Nginx выбирает его сам, а строка use epoll в секции events работает самодокументацией. Интереснее соседи. multi_accept off по умолчанию: воркер берёт из очереди по одному соединению за проход цикла, нагрузка распределяется ровно. Значение on заставляет брать всю очередь разом - полезно при шквале новых клиентов. В пакете Ubuntu строка закомментирована, действует экономное умолчание.

Директива accept_mutex по умолчанию выключена: споры за общий сокет почти не стоят времени, а там, где стоят, выручает reuseport из директивы listen, появившийся в ветке 1.9.1 и требующий ядра не старше 3.9. Каждому воркеру создаётся отдельный слушающий сокет, и ядро раздаёт соединения напрямую:

listen 443 ssl reuseport;

Выигрыш проявляется на десятках тысяч новых соединений в секунду, при скромных потоках разница тонет в погрешности.

Очередь приёма ограничена с двух сторон: параметром backlog в listen (умолчание 511) и системным net.core.somaxconn. Ядро берёт меньшее из них, а на свежих Ubuntu somaxconn равен 4096 - значит, запас ядра не используется, пока backlog не поднят явно:

listen 443 ssl reuseport backlog=4096;

Системные значения меняют файлом /etc/sysctl.d/99-tuning.conf:

net.core.somaxconn = 4096
net.ipv4.ip_local_port_range = 10240 65000

Вторая строка расширяет диапазон исходящих портов: стандартные 32768-60999 дают около 28 тысяч одновременных исходящих соединений на весь сервер, и фронт, передающий запросы бэкендам, под нагрузкой упирается в них раньше, чем кажется. Применение и проверка:

sudo sysctl -p /etc/sysctl.d/99-tuning.conf
sysctl net.core.somaxconn net.ipv4.ip_local_port_range

Нагрузочное тестирование через ab и чтение метрик в момент пика

Мерить надо с отдельной машины: тест по localhost не платит за сеть и врёт в сторону оптимизма. Классический инструмент ab ставится пакетом apache2-utils:

sudo apt install apache2-utils
ab -k -n 100000 -c 500 http://192.168.1.10/

Флаг -k включает keepalive, -n задаёт число запросов, -c - число одновременных клиентов. В отчёте смотрят три строки: Requests per second, Failed requests и счётчик keep-alive запросов. Полезный приём - прогнать тест дважды, с -k и без: разница показывает, сколько именно экономит keepalive на конкретном сайте. Для сценариев с тысячами параллельных соединений ближе wrk:

wrk -t4 -c2000 -d30s http://192.168.1.10/

Во время теста держат открытыми три окна: хвост error.log, stub_status и top. Если alert-строки не появляются, Active держится ниже лимита, а ядра не забиты в потолок - настройка выдерживает пик. Если процессор уже упёрся, тюнинг соединений не поможет: сначала кэширование и упрощение обработки, потом лимиты.

Типичные ошибки повторяются из конфига в конфиг. Worker_connections выше лимита открытых файлов - соединения всё равно обрежутся. Backlog выше somaxconn - ядро молча урежет очередь. Копипаст чужих 50000 соединений на VPS с гигабайтом памяти - первый же наплыв съест всю память. И главное разочарование новичков: тюнинг соединений не лечит медленный бэкенд. Nginx готов отдавать десятки тысяч ответов в секунду, но если страница ждёт PHP-скрипт полсекунды, посетитель видит те же полсекунды. Сначала время ответа upstream, потом сетевые лимиты.

После правок картина в метриках меняется тихо и убедительно: alert-строки исчезают из журнала, Waiting занимает свои законные десятки процентов, не съедая лимит, а график соединений перестаёт упираться в потолок. Сервер с запасом встречает наплыв, который раньше его придавливал, а разница с нетронутым конфигом - считанные директивы, каждая объяснима одним абзацем.