Полтора года назад я получил в наследство боевой сервис, который держал пиковую нагрузку около 10 тысяч запросов в секунду. На графиках всё выглядело прилично, пока не наступал вечерний прайм-тайм: p99 уезжал в полторы секунды, а в логах стабильно всплывали таймауты до бэкенда. Я был уверен, что дело в приложении, и три недели копал профилировщиком код, пока не решил наконец внимательно перечитать конфиг Nginx. Спойлер: код был ни при чём. Виновниками оказались пять настроек, которые либо не были прописаны вообще, либо стояли по умолчанию с самого дефолтного конфига из обучалки 2015 года.
Первое, что я бы посоветовал проверить каждому, это связка worker_processes, worker_connections и системный лимит файловых дескрипторов. У меня стоял worker_processes 1 из старого конфига, и это честно работало, пока трафик был маленький. На десяти тысячах RPS один воркер просто не мог разгрести очередь соединений, а worker_rlimit_nofile вообще отсутствовал, из-за чего лимит упирался в системный ulimit. Я поставил worker_processes auto, поднял worker_connections до 16384, задал worker_rlimit_nofile и добавил multi_accept on вместе с accept_mutex off. Latency сразу просела, а главное, перестали появляться сообщения о том, что воркеру не хватило сокетов. Отдельно напомню: менять эти значения без правки limits.conf в systemd или в sysctl смысла почти нет, потому что процесс упрётся в лимит раньше, чем в свою конфигурацию.
Второй пункт, который забывают буквально все, кого я знаю, это keepalive до апстрима. Люди настраивают keepalive_timeout для клиентов, радуются и совершенно не думают о том, что Nginx с бэкендом каждый раз поднимает новое TCP-соединение. У меня в upstream было просто перечисление серверов, и под нагрузкой это давало тысячи TIME_WAIT и заметную задержку на handshake. Достаточно добавить keepalive 64 в блок upstream, поднять proxy_http_version до 1.1 и обязательно сбросить заголовок Connection пустым значением, иначе соединения всё равно будут закрываться. Это буквально три строки, которые дали мне больше, чем неделя тюнинга приложения.
Третье — логирование. По умолчанию access_log пишется синхронно, а error_log стоит на уровне notice, что под нагрузкой превращается в постоянные операции записи на диск. Я сначала не поверил, что логи могут съесть столько ресурсов, пока не посмотрел на iowait. В итоге включил буферизацию через buffer и flush, убрал лишние поля из формата лога, поднял error_log до warn и включил open_file_cache для статики с разумным inactive и max. Отдельно предупрежу: не отключайте логи совсем, если у вас нет внешней системы сбора, иначе при следующем инциденте вы будете разбираться вслепую. Я оставил подробный формат только для пяти процентов трафика, а для остального сделал лёгкий.
Четвёртый забытый блок — буферизация проксирования. По умолчанию Nginx буферизует ответы бэкенда, и при медленном апстриме это спасает, но при больших телах запросов картина меняется. У меня загрузка файлов через приложение забивала диск временными файлами, потому что proxy_max_temp_file_size стоял по умолчанию, а proxy_request_buffering был включён, и Nginx сначала целиком складывал тело на диск и только потом передавал его дальше. Я аккуратно поднял proxy_buffers и proxy_buffer_size, ограничил размер временных файлов, а для эндпоинтов загрузки выключил request buffering. Если коротко: буферизация это не бинарный переключатель, её нужно настраивать под профиль трафика, а не копировать готовый кусок из чужого конфига.
Пятое, и самое недооценённое, это работа с сокетами на уровне системы. reuseport в директиве listen дал мне равномерное распределение соединений по воркерам вместо ситуации, когда один ядро перегрето, а остальные скучают. Дальше идут вещи за пределами конфига Nginx, о которых многие просто не знают: net.core.somaxconn, net.ipv4.tcp_max_syn_backlog, диапазон локальных портов, размеры очередей и параметры TCP. Мне помогло поднять somaxconn и расширить диапазон портов, потому что при большом количестве исходящих соединений к апстриму порты заканчивались быстрее, чем я успевал понять, что происходит. И да, перед любыми правками я снимал baseline, иначе невозможно доказать, что стало лучше, а не просто изменилось.
Главный вывод, который я вынес из этой истории, простой: Nginx почти никогда не является узким местом сам по себе, но его дефолтная конфигурация на порядок нагрузки просто не рассчитана. Не нужно копировать огромный конфиг с сорока директивами из интернета, нужно последовательно измерить, найти реальное узкое место и поменять две-три строки. Я прошёл путь от паники и поиска виноватых в коде до спокойных 10 тысяч RPS с запасом, и занял он меньше месяца именно потому, что я перестал верить в магические конфиги и начал смотреть на метрики. А теперь вопрос к вам, коллеги: какие настройки Nginx вы считаете самыми недооценёнными и что из вашего опыта дало самый большой прирост на высоких нагрузках?
Первое, что я бы посоветовал проверить каждому, это связка worker_processes, worker_connections и системный лимит файловых дескрипторов. У меня стоял worker_processes 1 из старого конфига, и это честно работало, пока трафик был маленький. На десяти тысячах RPS один воркер просто не мог разгрести очередь соединений, а worker_rlimit_nofile вообще отсутствовал, из-за чего лимит упирался в системный ulimit. Я поставил worker_processes auto, поднял worker_connections до 16384, задал worker_rlimit_nofile и добавил multi_accept on вместе с accept_mutex off. Latency сразу просела, а главное, перестали появляться сообщения о том, что воркеру не хватило сокетов. Отдельно напомню: менять эти значения без правки limits.conf в systemd или в sysctl смысла почти нет, потому что процесс упрётся в лимит раньше, чем в свою конфигурацию.
Второй пункт, который забывают буквально все, кого я знаю, это keepalive до апстрима. Люди настраивают keepalive_timeout для клиентов, радуются и совершенно не думают о том, что Nginx с бэкендом каждый раз поднимает новое TCP-соединение. У меня в upstream было просто перечисление серверов, и под нагрузкой это давало тысячи TIME_WAIT и заметную задержку на handshake. Достаточно добавить keepalive 64 в блок upstream, поднять proxy_http_version до 1.1 и обязательно сбросить заголовок Connection пустым значением, иначе соединения всё равно будут закрываться. Это буквально три строки, которые дали мне больше, чем неделя тюнинга приложения.
Третье — логирование. По умолчанию access_log пишется синхронно, а error_log стоит на уровне notice, что под нагрузкой превращается в постоянные операции записи на диск. Я сначала не поверил, что логи могут съесть столько ресурсов, пока не посмотрел на iowait. В итоге включил буферизацию через buffer и flush, убрал лишние поля из формата лога, поднял error_log до warn и включил open_file_cache для статики с разумным inactive и max. Отдельно предупрежу: не отключайте логи совсем, если у вас нет внешней системы сбора, иначе при следующем инциденте вы будете разбираться вслепую. Я оставил подробный формат только для пяти процентов трафика, а для остального сделал лёгкий.
Четвёртый забытый блок — буферизация проксирования. По умолчанию Nginx буферизует ответы бэкенда, и при медленном апстриме это спасает, но при больших телах запросов картина меняется. У меня загрузка файлов через приложение забивала диск временными файлами, потому что proxy_max_temp_file_size стоял по умолчанию, а proxy_request_buffering был включён, и Nginx сначала целиком складывал тело на диск и только потом передавал его дальше. Я аккуратно поднял proxy_buffers и proxy_buffer_size, ограничил размер временных файлов, а для эндпоинтов загрузки выключил request buffering. Если коротко: буферизация это не бинарный переключатель, её нужно настраивать под профиль трафика, а не копировать готовый кусок из чужого конфига.
Пятое, и самое недооценённое, это работа с сокетами на уровне системы. reuseport в директиве listen дал мне равномерное распределение соединений по воркерам вместо ситуации, когда один ядро перегрето, а остальные скучают. Дальше идут вещи за пределами конфига Nginx, о которых многие просто не знают: net.core.somaxconn, net.ipv4.tcp_max_syn_backlog, диапазон локальных портов, размеры очередей и параметры TCP. Мне помогло поднять somaxconn и расширить диапазон портов, потому что при большом количестве исходящих соединений к апстриму порты заканчивались быстрее, чем я успевал понять, что происходит. И да, перед любыми правками я снимал baseline, иначе невозможно доказать, что стало лучше, а не просто изменилось.
Главный вывод, который я вынес из этой истории, простой: Nginx почти никогда не является узким местом сам по себе, но его дефолтная конфигурация на порядок нагрузки просто не рассчитана. Не нужно копировать огромный конфиг с сорока директивами из интернета, нужно последовательно измерить, найти реальное узкое место и поменять две-три строки. Я прошёл путь от паники и поиска виноватых в коде до спокойных 10 тысяч RPS с запасом, и занял он меньше месяца именно потому, что я перестал верить в магические конфиги и начал смотреть на метрики. А теперь вопрос к вам, коллеги: какие настройки Nginx вы считаете самыми недооценёнными и что из вашего опыта дало самый большой прирост на высоких нагрузках?