MAATRIX / Блог / Сервер держал 10 000 соединений и умер на 10 001: считаем свой настоящий потолок

Сервер держал 10 000 соединений и умер на 10 001: считаем свой настоящий потолок

MAATRIX

Если сервер держал 10 000 одновременных соединений, а на 10 001-м начал сыпать ошибками или зависать — это не значит, что вы нашли какой-то универсальный «потолок Linux» или «предел nginx». Вы нашли конкретный ресурс на конкретной машине, который закончился первым: файловый дескриптор, кусок памяти, слот в таблице ядра или строчку в конфиге, которую год назад кто-то поставил «с запасом» и забыл. Ниже — как разобрать падение по кусочкам и посчитать свой настоящий потолок, а не гадать по цифрам из чужих статей.

Почему число 10 000 ничего не говорит о вашем сервере

В интернете гуляют «типовые» цифры: nginx держит 10 000 соединений (легендарный C10K), «нормальный VPS» — столько-то тысяч сокетов, у Node.js — свой миф про предел event loop. Проблема в том, что все эти числа посчитаны на чужом железе, с чужой конфигурацией ядра и чужим профилем нагрузки — лёгкие короткие GET-запросы и WebSocket с долгими соединениями и большими буферами упираются в предел на разных числах даже на одинаковом сервере.

Реальный потолок — это минимум из нескольких независимых лимитов, выставленных в разных местах системы:

  • лимит файловых дескрипторов процесса (ulimit, systemd, PAM);
  • лимит файловых дескрипторов ядра (fs.file-max);
  • память, которую одно соединение съедает на буферы;
  • лимиты сетевого стека ядра (backlog, диапазон портов, conntrack);
  • собственные ограничения конфигурации веб-сервера (worker_connections, число воркеров).

Падение на 10 001-м соединении означает, что один из этих пяти лимитов оказался меньше остальных на вашей машине. На соседнем сервере с тем же дистрибутивом, но другим объёмом RAM или другим nf_conntrack_max, потолок будет на другом числе — может, на 6 000, может, на 40 000. Задача этой статьи — не дать вам «правильное» число, а дать метод, которым вы найдёте своё.

Файловый дескриптор: самый частый и самый обманчивый лимит

В Linux каждое TCP-соединение — это открытый файловый дескриптор процесса. Как только процесс упирается в свой лимит дескрипторов, accept() начинает возвращать EMFILE, и новые клиенты получают отказ или таймаут, хотя CPU и память свободны на 80%.

Лимиты дескрипторов выставлены в нескольких слоях, и не совпадают друг с другом:

# лимит текущей сессии/процесса, запущенного из шелла
ulimit -Sn   # мягкий (soft)
ulimit -Hn   # жёсткий (hard) — потолок, выше которого soft не поднять без root

# системный потолок на все дескрипторы во всей системе
cat /proc/sys/fs/file-max
sysctl fs.file-max

# сколько дескрипторов реально открыто прямо сейчас
cat /proc/sys/fs/file-nr

Ключевая грабля: ulimit -n в интерактивном шелле не имеет отношения к лимиту демона, запущенного через systemd. Демон получает лимит из своего юнита или из /etc/security/limits.conf, если сервис запускается через PAM-сессию (для systemd-юнитов limits.conf чаще всего вообще не применяется). Проверить фактический лимит уже работающего процесса:

# PID процесса, например nginx-воркера
pgrep -f "nginx: worker"

# фактический лимит именно этого процесса
cat /proc/<PID>/limits | grep "Max open files"

Если там стоят дефолтные 1024, потолок в тысячу с небольшим соединений — не случайность, а прямое следствие незамеченного дефолта; порядок настройки soft/hard лимитов подробно разобран в статье о настройке лимитов открытых файлов через ulimit. Поднять лимит для systemd-сервиса:

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65535
systemctl daemon-reload
systemctl restart nginx

Для nginx отдельно есть собственная директива, которая должна быть не меньше системного лимита процесса:

worker_rlimit_nofile 65535;
events {
    worker_connections 16384;
}

Обратите внимание: worker_connections считает и клиентские, и проксируемые upstream-соединения одного воркера. Если nginx работает как обратный прокси, каждое клиентское соединение может держать открытым ещё одно к бэкенду — то есть реальный расход дескрипторов на клиента вырастает вдвое, и заявленные 16 384 «соединений» на деле обслуживают вдвое меньше живых клиентов.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Память на соединение: считаем, а не гадаем

Файловый дескриптор — это почти бесплатно с точки зрения памяти. А вот буферы, которые ядро и приложение выделяют на каждое соединение, — нет. Каждый TCP-сокет получает буфер приёма и буфер отправки, размер которых регулируется автотюнингом ядра:

sysctl net.ipv4.tcp_rmem   # мин / дефолт / макс буфера приёма
sysctl net.ipv4.tcp_wmem   # мин / дефолт / макс буфера отправки

При большом числе одновременных соединений с активным трафиком ядро может раздувать буферы к максимуму из диапазона на каждый сокет — и тогда 10 000 «лёгких» долгоживущих соединений внезапно требуют памяти на порядки больше, чем предполагалось при планировании. Плюс к сокетным буферам ядра добавляется память самого приложения: у nginx это буферы на воркер плюс структуры на каждое соединение (connection_pool_size, буферы для проксирования — proxy_buffers, proxy_buffer_size), у Node.js — объекты сокетов и очереди в event loop, у Java-приложений — часто ещё и по потоку на соединение, если используется блокирующая модель ввода-вывода вместо асинхронной.

Грубая прикидка бюджета памяти на N соединений:

память_на_буферы ≈ N × (буфер_приёма + буфер_отправки + служебные_структуры_приложения)

Если у вас 8 ГБ RAM и часть уже занята базой данных или кэшем, оставшийся бюджет может закончиться на числе соединений, далёком от «типичных» цифр из статей про C10K — потому что там считали для голого nginx без прикладной логики за ним. Проверить, куда реально уходит память в момент нагрузки, помогает связка free -h, vmstat 1 и ss -m (последняя показывает размер буферов конкретного сокета). Если после теста нагрузки free показывает исчерпанную память и в dmesg появляется запись OOM killer — вы упёрлись именно в память, а не в дескрипторы или CPU, и решать нужно урезанием буферов или добавлением RAM, а не тюнингом ulimit.

Лимиты сетевого стека ядра, о которых обычно забывают

Даже если дескрипторов и памяти хватает с запасом, ядро выставляет собственные ограничения на уровне сетевого стека, о которые тоже можно упереться:

ПараметрЗа что отвечаетСимптом при исчерпании
net.core.somaxconnОчередь ожидающих accept() соединений на слушающем сокетеНовые SYN отбрасываются или зависают при резком всплеске нагрузки
net.ipv4.tcp_max_syn_backlogОчередь полуоткрытых соединений (SYN получен, ACK ещё нет)Клиенты видят долгое подключение или таймаут на handshake
net.ipv4.ip_local_port_rangeДиапазон исходящих (эфемерных) портов для соединений, инициированных этим серверомАктуально, если сервер сам ходит наружу — к БД, API, upstream'ам
net.netfilter.nf_conntrack_maxРазмер таблицы отслеживания соединений, если задействован iptables/nftables со stateful-правилами или NATЯдро роняет новые пакеты, в dmesgnf_conntrack: table full, dropping packet
fs.file-maxОбщесистемный потолок дескрипторов на все процессы сразуОшибки открытия файлов/сокетов у случайных процессов, не только у веб-сервера

Проверить текущее значение и заполненность conntrack, если он используется:

sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count
# или, если модуль называется иначе на старом ядре
cat /proc/sys/net/netfilter/nf_conntrack_max

somaxconn часто оказывается тихим убийцей при резких всплесках: соединения приходят пачкой быстрее, чем приложение успевает их разгребать через accept(), очередь переполняется, и часть клиентов получает Connection refused даже при свободных CPU и памяти. Дефолт в некоторых дистрибутивах до сих пор исторически низкий (128), и его стоит поднимать вместе с backlog в конфиге самого приложения — оба значения должны совпадать, иначе меньшее из них станет реальным лимитом:

sysctl -w net.core.somaxconn=4096
listen 443 ssl backlog=4096;

Конфигурация приложения: где вы сами поставили себе потолок

Отдельный класс лимитов — не системный, а тот, что вы (или предыдущий администратор) явно прописали в конфиге. Он маскируется под «системный предел», хотя на деле это просто число в файле:

  • worker_processes и worker_connections в nginx — их произведение и есть теоретический максимум соединений на машину;
  • число воркеров в PHP-FPM (pm.max_children) — упирается раньше сети, потому что каждый воркер — это отдельный процесс с собственной памятью;
  • пул соединений к базе данных приложения — если он меньше числа входящих запросов, лишние запросы встанут в очередь и начнут таймаутить независимо от сетевых лимитов;
  • keepalive_timeout — слишком долгий keep-alive держит соединение занятым дольше, чем нужно, и снижает эффективную пропускную способность при том же числе слотов.

Посчитать теоретический потолок именно веб-сервера:

теоретический_максимум = worker_processes × worker_connections

Если он выше, чем позволяют дескрипторы, память или ядро — по факту сервер упрётся в один из системных лимитов раньше, чем в этот теоретический. Если он ниже — вы сами обрезали себя конфигом, и «на 10 001-м» сервер падает не потому, что железо кончилось, а потому что nginx физически отказывается принимать больше по своей же настройке.

Методика: как измерить свой настоящий потолок

Вместо того чтобы ориентироваться на чужие цифры, поднимите нагрузку постепенно и смотрите, какой ресурс кончается первым именно у вас. Шаги:

  1. Готовим мониторинг в реальном времени. В отдельных терминалах или через tmux:
watch -n1 'ss -s'                                  # сводка по числу сокетов по состояниям
watch -n1 'cat /proc/sys/fs/file-nr'                # занятые дескрипторы / потолок
vmstat 1                                            # CPU, память, своп в реальном времени
watch -n1 'ss -tan state established | wc -l'       # число установленных TCP-соединений
  1. Генерируем нагрузку ступенями, а не сразу пиком. Например, инструментом wrk с постепенным увеличением конкурентности:
wrk -t4 -c1000  -d30s http://ваш-сервер/
wrk -t4 -c3000  -d30s http://ваш-сервер/
wrk -t4 -c6000  -d30s http://ваш-сервер/
wrk -t4 -c10000 -d30s http://ваш-сервер/

Для более тяжёлых сценариев (долгие соединения, WebSocket) больше подходят vegeta или hey с контролем именно числа одновременно открытых, а не запросов в секунду — учитывайте, что тест обязательно нужно гонять с отдельной машины, а не с самого сервера, иначе вы упрётесь в лимиты клиента, а не сервера. Как рассчитать исходную конфигурацию сервера под ожидаемую нагрузку ещё до первого теста — отдельная тема, разобранная в статье про расчёт конфигурации сервера под нагрузку; а к чему приводит нагрузочный тест, случайно запущенный не на том окружении, показывает история из статьи про нагрузочный скрипт, который три недели бил по проду — прежде чем гонять wrk с большой конкурентностью, убедитесь, что целитесь именно в тестовый контур.

  1. На каждой ступени фиксируйте, что показывает мониторинг в момент, когда начинают появляться ошибки или расти задержки: CPU близко к 100%, память подходит к пределу (и растёт своп), число открытых дескрипторов подходит к лимиту из /proc/<PID>/limits, или в dmesg появляются записи о переполнении conntrack/backlog.
  1. Смотрите в лог ошибок веб-сервера и в dmesg параллельно — они часто прямо называют причину:
tail -f /var/log/nginx/error.log
dmesg -T | tail -50

Записи вида accept4() failed (24: Too many open files) — это дескрипторы. nf_conntrack: table full — это conntrack. Запись OOM killer в dmesg с именем вашего процесса — это память. Если ни ошибок, ни насыщения ресурсов не видно, а задержки всё равно растут — вероятно, упор в собственный worker_connections или пул соединений к БД, и стоит сверить их с наблюдаемым числом одновременных соединений.

  1. Поднимайте лимит того ресурса, который упёрся первым, и повторяйте тест. Это итеративный процесс: подняли ulimit/worker_rlimit_nofile — упёрлись в память; добавили RAM или сократили буферы — упёрлись в somaxconn; подняли backlog — упёрлись в собственный worker_connections. Настоящий потолок сервера — это число, на котором вы упираетесь в ресурс, который физически нельзя увеличить на этом железе без апгрейда: ядра CPU, объём RAM или пропускная способность диска/сети.

Здесь же стоит сверить и сетевые лимиты, которые не всегда видны напрямую — например, через сколько минут заканчиваются исходящие порты при активном исходящем трафике с самого сервера, если он сам выступает клиентом к базе данных или внешним API — эта грабля отдельная от входящих соединений и требует своего замера.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли один раз настроить лимиты «с запасом» и больше не думать об этом?

Частично да — разумный запас (worker_rlimit_nofile 65535, somaxconn 4096) снимает большинство случайных отказов. Но «запас» не отменяет физику: если реальный трафик вырастет на порядок, вы всё равно упрётесь в память или CPU, просто на другом числе. Периодическая проверка после значимых изменений нагрузки или конфигурации остаётся нужной.

Почему на одинаковых по характеристикам серверах потолок разный?

Потому что «одинаковые характеристики» обычно означают одинаковое число ядер и RAM, но не одинаковые sysctl, версии ядра, включённые модули netfilter, конфиги приложения и профиль трафика (короткие запросы против долгих WebSocket-соединений). Любое из этих различий сдвигает точку отказа.

Нужно ли поднимать лимиты до максимума на всякий случай?

Нет — избыточно большие worker_connections или nf_conntrack_max без соответствующей памяти под них только отодвигают момент отказа на менее предсказуемый: вместо аккуратного Connection refused вы получите деградацию всей системы через своп или OOM killer, который может убить не тот процесс. Лимиты стоит поднимать пропорционально фактическим ресурсам, а не «с запасом на будущее».

Как понять, что предел — это диск, а не сеть или память?

Если при росте одновременных соединений растут именно задержки записи или чтения (проверяется iostat -x 1, столбец %util и await), а CPU и память свободны — упор в дисковый ввод-вывод, актуально для серверов с активным логированием или базой данных на той же машине.

Стоит ли переезжать на более мощный сервер, если тест показал упор в память?

Если методика показала, что именно объём RAM — единственный ресурс, который физически ограничивает число соединений при свободных CPU и дисках, апгрейд по памяти даёт прямой и предсказуемый прирост потолка. Дальнейшие шаги — тот же цикл замера на новой конфигурации, а не экстраполяция на глаз.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →