Сервер под сильной нагрузкой порой ведёт себя странно: процессоры почти спят, память свободна, а посетители жалуются на медленную загрузку. Знакомая картина для 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 нужен вдвое больший запас, чем чисто раздающей статике.
Расчёт стартовых значений собирают в четыре шага:
- подсчитать ядра командой nproc - столько воркеров создаст auto;
- снять пик одновременных соединений из stub_status в час наплыва;
- разделить пик на число воркеров и добавить запас в полтора-два раза;
- сверить итог с лимитом открытых файлов и запасом памяти сервера.
Конфигурация для сервера на 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 занимает свои законные десятки процентов, не съедая лимит, а график соединений перестаёт упираться в потолок. Сервер с запасом встречает наплыв, который раньше его придавливал, а разница с нетронутым конфигом - считанные директивы, каждая объяснима одним абзацем.