MAATRIX / Блог / Сколько строк conntrack вам нужно: расчёт размера таблицы под свою нагрузку

Сколько строк conntrack вам нужно: расчёт размера таблицы под свою нагрузку

MAATRIX

Дефолтный лимит таблицы conntrack на большинстве систем подобран не под вашу нагрузку, а под усреднённый случай — и на сервере с NAT, VPN-шлюзом или высоконагруженным бэкендом он часто оказывается тесен. Проблема в том, что узнаёте вы об этом обычно не заранее, а в момент, когда часть новых соединений начинает молча отваливаться при внешне здоровом сервере. Разберём, как посчитать нужный размер таблицы заранее, по цифрам вашей собственной нагрузки, а не подбором на живую систему.

Почему тут нельзя просто взять число побольше

Соблазн решить вопрос одной командой — поднять nf_conntrack_max в несколько раз и забыть — понятен, но у него есть цена. Каждая запись в таблице conntrack занимает память ядра, невыгружаемую в своп: она живёт в слабах, пока соединение активно или пока не истёк таймаут записи. На сервере с ограниченным объёмом RAM лимит в несколько миллионов записей «с запасом на всякий случай» — это гарантированное резервирование памяти под таблицу, которая физически может никогда не заполниться настолько, а может, наоборот, стать тем местом, где закончится память при внезапном всплеске UDP-сканирования или DDoS.

Есть и обратная сторона: маленький лимит на нагруженном шлюзе или NAT-сервере рвёт соединения не потому, что не хватает CPU или канала, а потому что таблица состояний физически не может завести новую запись. Подробно механика этого сбоя — почему один атакующий или один «сломанный» клиент кладёт NAT для всех остальных — разобрана в статье что такое conntrack и как таблица соединений кладёт сервер. Здесь же задача другая: не разбирать уже случившуюся аварию, а посчитать размер таблицы заранее, чтобы до неё не дойти.

Из чего складывается число одновременных соединений

Ключевая ошибка при расчёте — путать «запросов в секунду» и «одновременных соединений в таблице conntrack». Это разные величины, и разница между ними — время жизни записи. Базовая формула (это частный случай закона Литтла из теории очередей):

Одновременные соединения ≈ RPS × средняя_продолжительность_жизни_соединения

Где RPS — не средний трафик за сутки, а пиковая частота новых соединений, а «продолжительность жизни соединения» — это не время самого запроса, а время, в течение которого запись о соединении вообще существует в таблице conntrack, включая период после закрытия (см. следующий раздел про TIME_WAIT).

Для разных типов серверов эта величина считается по-разному:

  • Веб-сервер с HTTP keep-alive. Одно TCP-соединение обслуживает много запросов подряд, поэтому число одновременных соединений ближе к числу активных клиентов, а не к RPS напрямую — но каждое такое долгоживущее соединение всё равно держит одну запись в таблице все время, пока живо.
  • API-бэкенд с короткими запросами без keep-alive или воркер, открывающий новое соединение на каждый вызов, — здесь именно RPS и время жизни каждого короткого соединения (включая TIME_WAIT) напрямую определяют число одновременных записей, и оно может быть неожиданно большим при высоком RPS.
  • NAT-шлюз или VPN-концентратор. Считать нужно суммарно по всем клиентам за шлюзом: если за одним внешним IP сидит 500 пользователей, каждый из которых держит 20–40 фоновых соединений (мессенджеры, синхронизация, keep-alive к разным сервисам), совокупная нагрузка на таблицу может исчисляться десятками тысяч записей уже в состоянии покоя, без единого активного действия пользователей.
  • UDP-трафик (DNS, VoIP, игровые протоколы, часть VPN). У UDP нет явного закрытия соединения, поэтому запись в conntrack живёт весь таймаут UDP (по умолчанию заметно короче TCP-established, но всё равно не мгновенно) — при интенсивном DNS-трафике через NAT это может дать заметную долю таблицы.

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

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

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

TIME_WAIT: почему закрытые соединения всё ещё занимают место

Одна из самых частых причин, по которой реальное число записей в таблице оказывается сильно больше, чем «число активных клиентов», — TIME_WAIT. Когда TCP-соединение закрывается штатно (после FIN), сторона, инициировавшая закрытие, переходит в состояние TIME_WAIT и держит его некоторое время — это часть протокола TCP, защита от переиспользования той же пары адрес:порт для «призрачных» пакетов от предыдущего соединения. Соединение в TIME_WAIT — это всё ещё активная запись в таблице conntrack, а не освободившееся место.

У самого conntrack есть собственный таймаут для этого состояния — параметр nf_conntrack_tcp_timeout_time_wait, по умолчанию в конфигурации ядра он выставлен в районе пары минут (это отдельная величина от tcp_fin_timeout на уровне TCP-стека — они близки по смыслу, но настраиваются разными ручками и не обязаны совпадать). Посмотреть свои значения:

cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_time_wait
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established

Второе значение стоит проверить отдельно и внимательно: у установленных (ESTABLISHED) TCP-соединений таймаут в дефолтной конфигурации ядра исторически выставлен очень большим — порядка нескольких суток. Это осмысленно для долгоживущих соединений (SSH-сессия, постоянный вебсокет), но означает, что «зависшее» соединение — клиент ушёл в оффлайн без корректного FIN, сеть между ним и сервером разорвалась — будет держать запись в таблице почти неделю, если не полагаться на TCP keepalive или собственную логику приложения по разрыву мёртвых соединений. На сервере с большим числом коротких неаккуратно закрываемых клиентских соединений именно это, а не пиковый RPS, часто оказывается основным источником раздутия таблицы. Смежная тема — как эфемерные порты и TIME_WAIT считаются на уровне самого TCP-стека, а не conntrack — разобрана в статье 65 535 портов — миф о потолке: сколько исходящих соединений вы откроете на самом деле.

При расчёте размера таблицы TIME_WAIT нужно закладывать явно: если у вас 1000 новых коротких соединений в секунду и запись живёт в TIME_WAIT ещё 120 секунд после закрытия, то только за счёт TIME_WAIT в таблице одновременно может находиться порядка 120 000 записей — и это без учёта самих активных соединений.

Формула расчёта и пример

Соберите три числа для профиля вашей нагрузки:

  1. Пиковый RPS новых соединений — не средний за сутки, а именно пиковый, за минуту-две максимальной нагрузки.
  2. Среднее время жизни записи — от установления соединения до истечения таймаута conntrack после его закрытия (то есть включая TIME_WAIT или соответствующий UDP-таймаут).
  3. Коэффициент запаса — на всплески, ретраи клиентов при деградации, короткие атаки сканирования портов и погрешность собственных оценок. На практике закладывают запас в 2–3 раза сверх расчётного пикового значения, а не впритык.

Условный иллюстративный пример (числа ниже — не измерение, а пример для арифметики; на вашей нагрузке подставьте свои): пусть пиковый RPS новых соединений — 3000, среднее время жизни записи с учётом TIME_WAIT — 90 секунд. Тогда расчётное число одновременных записей: 3000 × 90 = 270 000. С запасом ×3 получаем ориентир для nf_conntrack_max около 800 000. Дальше это число проверяется практикой — фактическим наблюдением за nf_conntrack_count под реальной нагрузкой (следующий раздел), а не остаётся разовым теоретическим расчётом.

Отдельно закладывайте в расчёт UDP и ICMP, если сервер их прокси́рует или NAT'ит: они считаются в ту же таблицу и в ту же формулу, просто с собственным RPS и собственными таймаутами.

Как посмотреть текущее использование и лимит

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

cat /proc/sys/net/netfilter/nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count
# или эквивалентно через sysctl
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count

Утилита conntrack из пакета conntrack-tools (устанавливается отдельно на большинстве дистрибутивов, apt install conntrack / dnf install conntrack-tools) даёт больше деталей, включая счётчик отброшенных пакетов из-за переполнения таблицы:

conntrack -C            # число текущих записей (то же, что nf_conntrack_count)
conntrack -S             # статистика по каждому CPU, включая insert_failed и drop

Разовый снимок не показывает динамику, а именно динамика важна для расчёта запаса — заполнение таблицы обычно не растёт линейно, а скачет вслед за трафиком. Снимайте nf_conntrack_count в мониторинг регулярно (у большинства экспортеров метрик для node/host есть готовый сборщик этого значения) и смотрите не только на текущее значение, а на пиковое за неделю-две, желательно захватив хотя бы один настоящий пик нагрузки, а не только будний день.

Полезно смотреть на процент заполнения, а не на абсолютное число:

echo "scale=1; $(cat /proc/sys/net/netfilter/nf_conntrack_count) * 100 / $(cat /proc/sys/net/netfilter/nf_conntrack_max)" | bc

Если это значение регулярно подбирается к 70–80% на пиках — это уже сигнал считать новый лимит, не дожидаясь, пока таблица переполнится в проде.

Симптомы упора в лимит

Когда таблица заполняется целиком, ядро не может завести запись для нового соединения и молча отбрасывает пакет, который должен был её создать (первый SYN для TCP, первый пакет UDP-потока). Для клиента это выглядит как таймаут или сброс соединения без внятной причины — никакой ошибки уровня приложения не возникает, потому что до приложения пакет просто не доходит. Как отличить эту причину от других (перегрузка приложения, проблемы с диском, сетевой затор) и как выглядит характерная выборочность обрывов — подробно в статье таблица conntrack переполнилась: соединения рвутся выборочно.

Прямое подтверждение — сообщение в логе ядра:

dmesg -T | grep -i conntrack

Классическая строка выглядит как nf_conntrack: table full, dropping packet (в разных версиях ядра формулировка может немного отличаться). Если такая строка появляется — таблица гарантированно переполнялась хотя бы кратковременно, и часть новых соединений в этот момент была потеряна безвозвратно, без возможности разобрать задним числом, сколько именно и чьих.

Второй источник — счётчик insert_failed (или аналог drop) в выводе conntrack -S: он растёт именно в момент отказов по переполнению и не сбрасывается сам по себе, поэтому его стоит снимать как метрику, а не смотреть разово. Растущий на пиках insert_failed при значении nf_conntrack_count, близком к nf_conntrack_max, — однозначный диагноз: дело в размере таблицы, а не в чём-то ещё.

Как безопасно увеличить лимит с учётом памяти

Прежде чем поднимать nf_conntrack_max, оцените, во что это выльется по памяти. Точный размер одной записи зависит от версии ядра, архитектуры и включённых расширений conntrack (NAT, хелперы протоколов, таймстемпы) — не берите чужую цифру как константу, а посмотрите свою через slabtop:

slabtop -o | grep -i nf_conntrack

Столбец OBJSIZE покажет фактический размер одного объекта на вашем ядре. Дальше расчёт памяти под таблицу — простое умножение: желаемый nf_conntrack_max, умноженный на размер записи из slabtop, плюс отдельно память под саму хеш-таблицу (её размер задаётся параметром hashsize/nf_conntrack_buckets). Общепринятое соотношение — buckets примерно в 4 раза меньше nf_conntrack_max (это соотношение по умолчанию использует и сам модуль ядра), но при явном заданном nf_conntrack_max стоит явно выставить и hashsize, а не полагаться на автоподбор при загрузке модуля:

# текущее значение хеш-таблицы
cat /sys/module/nf_conntrack/parameters/hashsize

# изменить на лету (root)
echo 262144 > /sys/module/nf_conntrack/parameters/hashsize

Слишком маленький hashsize относительно nf_conntrack_max не ломает работу таблицы напрямую, но увеличивает число коллизий в хеше и потому чуть замедляет поиск при большом количестве записей — для большинства нагрузок это несущественно, но при действительно больших таблицах (миллионы записей) стоит держать это соотношение осознанно.

Сам лимит меняется через sysctl, постоянно — файлом в /etc/sysctl.d/:

# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 800000
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
sysctl --system

Последняя строка примера — tcp_timeout_established, сокращённый с дефолтных нескольких суток до часа, — не обязательна, но часто полезнее самого увеличения nf_conntrack_max: она не даёт мёртвым, никем не закрытым соединениям копиться в таблице неделями. Меняйте её осторожно: если у вас есть соединения, которым штатно нужно жить дольше часа без трафика (некоторые VPN-туннели, постоянные интеграции), для них лучше поднимать точечный таймаут через conntrack-хелперы, а не резать общий.

Прежде чем применять новое значение в проде, сверьте его с фактическим запасом RAM после вычета всего остального (приложение, кэши, СУБД) — память conntrack невыгружаемая: если её не хватит, ядро либо откажет в выделении новых записей раньше срока, либо начнёт давить на память всей системы. Похожая по логике задача — расчёт лимита файловых дескрипторов под пиковую нагрузку, а не под дефолт — разобрана в статье настройка лимитов открытых файлов (ulimit).

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

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

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

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

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

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

Можно ли просто отключить conntrack, чтобы не думать про лимит?

Частично да, если серверу не нужен ни NAT, ни stateful-фильтрация — трафик, который не подпадает ни под одно nftables/iptables правило conntrack-типа (ct state, -m state/-m conntrack) и не проходит через NAT, можно исключить из отслеживания через NOTRACK/raw-таблицу. Но для любого сервера за NAT или с stateful firewall conntrack — не опция, а обязательная часть механизма; отключать его целиком в этом случае нельзя.

Что будет, если поставить nf_conntrack_max заведомо избыточным — например, в 10 раз больше расчёта?

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

Нужно ли перезагружать сервер, чтобы новый nf_conntrack_max применился?

Нет, sysctl-параметры conntrack применяются на лету через sysctl -p или запись в /proc, без перезагрузки и без разрыва уже установленных соединений. Перезагрузка модуля потребовалась бы только для изменения некоторых параметров, задаваемых исключительно при загрузке модуля — но hashsize тоже можно менять на лету через /sys/module/nf_conntrack/parameters/.

Разные лимиты нужны для TCP и UDP отдельно?

Общий потолок nf_conntrack_max один на все протоколы, но таймауты у них разные и настраиваются отдельными параметрами (nf_conntrack_udp_timeout, nf_conntrack_tcp_timeout_* и так далее) — при расчёте нагрузки учитывайте вклад каждого протокола отдельно, по его собственному RPS и таймауту, а не смешивайте их в одну цифру.

Как понять, что новый лимит выставлен верно, а не просто «побольше на всякий случай»?

Ориентир — под реальным пиковым трафиком заполнение таблицы (nf_conntrack_count / nf_conntrack_max) держится заметно ниже 100%, с запасом хотя бы в четверть-треть, а счётчик insert_failed в conntrack -S не растёт даже на самых нагруженных отрезках. Если запас держится месяцами и характер нагрузки не меняется — это подтверждение расчёта, а не повод резать лимит обратно.

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

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

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