MAATRIX / Блог / Таблица conntrack переполнилась: соединения рвутся выборочно

Таблица conntrack переполнилась: соединения рвутся выборочно

MAATRIX

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

Симптом, который сбивает с толку

Классическая ошибка на этом этапе — искать причину в конкретном запросе или конкретном клиенте. Разработчик открывает лог приложения, видит, что запрос ушёл, а ответа нет, и начинает подозревать таймауты в коде, проблемы с базой, странности сетевой библиотеки. Но признаки переполнения conntrack устроены иначе: они не привязаны к типу запроса, а привязаны к моменту времени и к текущей заполненности таблицы.

Обычно это выглядит так:

  • обрывы идут пачками — несколько минут всё нормально, потом за короткий промежуток рвётся заметная доля новых соединений;
  • страдают именно НОВЫЕ соединения, уже установленные сессии чаще всего продолжают работать;
  • проблема усиливается с ростом нагрузки (больше клиентов, пиковые часы, начало атаки) и сама отступает, когда нагрузка спадает;
  • один и тот же клиент, повторив попытку через секунду, может успешно подключиться — потому что за это время в таблице освободилось место под другую запись.

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

Как подтвердить переполнение таблицы conntrack

Первое, что нужно сделать — сравнить текущее число отслеживаемых соединений с лимитом таблицы:

cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

Если первое число вплотную приблизилось ко второму (условно — счётчик держится выше 90% от лимита или периодически в него упирается), это уже сильный сигнал. Удобнее наблюдать за соотношением в динамике:

watch -n 1 'echo -n "count: "; cat /proc/sys/net/netfilter/nf_conntrack_count; echo -n "max:   "; cat /proc/sys/net/netfilter/nf_conntrack_max'

Второй и самый однозначный источник — сообщения ядра. При каждом отброшенном из-за переполнения пакете ядро обычно пишет в кольцевой буфер:

dmesg -T | grep -i conntrack
dmesg -T | grep "table full"

Характерная строка выглядит так: nf_conntrack: table full, dropping packet. Если вы видите её регулярно (не единичный случай месяц назад, а повторяющиеся записи в интересующий вас период) — вы нашли причину обрывов, дальше остаётся разобраться в масштабе и источнике.

Третий инструмент — утилита conntrack из пакета conntrack-tools (в Debian/Ubuntu ставится apt install conntrack). Она показывает не только текущее число записей, но и статистику по действиям:

conntrack -S

В выводе на каждый CPU отдельная строка со счётчиками insert, insert_failed, drop, early_drop, ignore, invalid. Здесь важны именно insert_failed и drop — их устойчивый рост во времени и означает, что ядро не может создать новую запись состояния, потому что таблица занята. Разово посмотреть, что именно занимает таблицу, можно так:

conntrack -L | wc -l                                  # всего записей
conntrack -L -p udp | wc -l                           # сколько из них UDP
conntrack -L -p tcp --state ESTABLISHED | wc -l        # установленные TCP
conntrack -L -p tcp --state TIME_WAIT | wc -l          # TCP в TIME_WAIT

Если основная масса записей — короткие UDP-сессии или TCP в TIME_WAIT, которые подолгу висят до истечения таймаута, это прямая подсказка на причину (см. следующий раздел). Диагноз ставит именно совпадение трёх фактов: nf_conntrack_count рядом с nf_conntrack_max, растущий insert_failed/drop в conntrack -S и строки table full в dmesg за нужный период.

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

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

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

Почему в такой ситуации нет ни ошибки, ни явного отказа

Здесь стоит понимать механику, а не просто запомнить симптом. Netfilter (модуль nf_conntrack) работает как stateful-файрвол: чтобы правила вида «пропускать пакеты уже установленного соединения» вообще имели смысл, ядро должно где-то хранить состояние каждого активного соединения — его пятикортеж (протокол, адреса и порты источника/назначения), текущую стадию (для TCP — SYN_SENT, ESTABLISHED, TIME_WAIT и так далее) и таймаут, после которого запись можно удалить. Эта таблица конечна и имеет фиксированный размер, заданный nf_conntrack_max.

Когда приходит пакет, открывающий новое соединение, ядро пытается создать для него запись в таблице. Если свободного места нет, создать запись физически нечем — а раз состояния нет, netfilter не может решить, разрешать ли последующие пакеты этого соединения по правилам вроде -m state --state ESTABLISHED,RELATED. Самый безопасный с точки зрения ядра выход — тихо отбросить пакет и увеличить счётчик drop. Никакого TCP RST, никакого ICMP «порт недоступен», никакой ошибки приложению не отправляется — с точки зрения клиента пакет просто исчез в сети, будто потерялся где-то на маршруте.

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

Откуда берётся переполнение

Практика подсказывает несколько типичных причин, и они не взаимоисключающие — часто совпадают сразу две:

Резкий рост числа клиентов или соединений. Сервер стал шлюзом/NAT для выросшего числа устройств, добавился новый пул клиентов VPN, вырос трафик сайта — а лимит таблицы остался тем же, что был рассчитан под старую нагрузку.

Атака или сканирование портов. Массовые SYN-пакеты на разные порты (в том числе SYN-флуд) или сканер, перебирающий диапазон адресов/портов, создают огромное число «полуоткрытых» записей conntrack практически мгновенно — таблица может забиться за секунды. Отличить это от органического роста помогает conntrack -L: резкий скачок соединений от небольшого числа source-адресов на много разных портов назначения выглядит подозрительно.

Множество коротких UDP-сессий, не освобождающих запись вовремя. У UDP нет явного закрытия соединения (нет FIN/RST), поэтому запись в conntrack живёт до истечения таймаута независимо от того, продолжается ли обмен данными. Если сервис генерирует много коротких UDP-запросов (DNS-резолвер под нагрузкой, VoIP, игровые серверы, VPN-протоколы поверх UDP), записи накапливаются быстрее, чем истекают.

Слишком низкий лимит таблицы для реальной нагрузки. Значение nf_conntrack_max по умолчанию рассчитывается от объёма оперативной памяти по консервативной формуле и годами не пересматривается при апгрейде сервера — типичная ситуация, когда сервер уже вырос, а лимит остался прежним.

Что делать прямо сейчас: расширяем таблицу и подрезаем таймауты

Первая экстренная мера — поднять лимит таблицы, проверив текущее значение и увеличив его без перезагрузки:

sysctl net.netfilter.nf_conntrack_max
sysctl -w net.netfilter.nf_conntrack_max=262144

Закрепить значение постоянно:

cat >> /etc/sysctl.d/99-conntrack.conf << 'EOF'
net.netfilter.nf_conntrack_max = 262144
EOF
sysctl --system

Конкретное число подбирайте по факту — ориентируйтесь на то, каким был nf_conntrack_count в пике перед переполнением, и берите лимит с запасом (в разы, а не на 10-20%), учитывая доступную память: каждая запись в таблице занимает память ядра, и на серверах с малым объёмом ОЗУ бездумно задирать лимит до предельных значений не стоит.

Отдельный нюанс — размер хэш-таблицы (nf_conntrack_buckets), от которого зависит скорость поиска записи в conntrack. Если модуль загружен статически, размер задаётся параметром модуля через /etc/modprobe.d/nf_conntrack.conf:

options nf_conntrack hashsize=65536

После правки параметров модуля обычно требуется перезагрузка (или выгрузка/загрузка модуля, если он не используется другими правилами в моменте) — на боевом сервере с активными правилами файрвола проще запланировать окно, чем выгружать модуль на лету.

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

sysctl -a 2>/dev/null | grep nf_conntrack | grep timeout

Ключевые параметры и типичные значения по умолчанию (могут отличаться в зависимости от дистрибутива и версии ядра):

ПараметрТипичное значение по умолчаниюЧто регулирует
net.netfilter.nf_conntrack_udp_timeout30 сектаймаут UDP-записи без ответного трафика
net.netfilter.nf_conntrack_udp_timeout_stream180 сектаймаут UDP-сессии с двусторонним обменом
net.netfilter.nf_conntrack_tcp_timeout_established432000 сек (5 суток)таймаут установленного TCP-соединения
net.netfilter.nf_conntrack_generic_timeout600 сектаймаут для прочих протоколов без явного состояния

Если основная масса «мусорных» записей — это короткоживущие UDP-сессии, разумно уменьшить nf_conntrack_udp_timeout_stream (например, до 60-120 секунд вместо 180) — это освободит таблицу быстрее ценой чуть менее долгой памяти о недавно закрытых сессиях. Для TCP снижать nf_conntrack_tcp_timeout_established с 5 суток до нескольких часов тоже разумно на большинстве серверов — реальные соединения либо активно используются (таймер сбрасывается новым трафиком), либо давно должны были закрыться штатно:

sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=120
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=14400

Закрепите рабочие значения тем же файлом /etc/sysctl.d/99-conntrack.conf. Если переполнение вызвано сканированием или флудом, а не органической нагрузкой — увеличение лимита и подрезка таймаутов лишь выигрывают время: параллельно стоит заблокировать источник атаки на файрволе (см. отдельный разбор первых действий при DDoS-атаке), иначе таблица снова упрётся в новый, уже увеличенный лимит.

Как настроить мониторинг заполнения заранее

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

Самый простой вариант — cron-скрипт, который сравнивает nf_conntrack_count и nf_conntrack_max и пишет предупреждение в syslog при превышении порога:

cat > /usr/local/bin/check_conntrack.sh << 'EOF'
#!/bin/bash
count=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
max=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
percent=$(( 100 * count / max ))
if [ "$percent" -ge 80 ]; then
    logger -t conntrack-check "ВНИМАНИЕ: таблица conntrack заполнена на ${percent}% (${count}/${max})"
fi
EOF
chmod +x /usr/local/bin/check_conntrack.sh
echo "* * * * * root /usr/local/bin/check_conntrack.sh" > /etc/cron.d/conntrack-check

Если на сервере уже развёрнут Zabbix или Prometheus, разумнее завести отдельную метрику, а не полагаться на почтовые уведомления из cron — подробнее про начальную настройку систем мониторинга есть в разборах установки Zabbix на VPS и сравнения Zabbix и Prometheus — выбор конкретного инструмента зависит от того, что уже стоит в инфраструктуре.

Для Prometheus проще всего использовать node_exporter с текстовым коллектором: скрипт из примера выше можно переписать так, чтобы вместо logger он писал метрику в .prom-файл:

count=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
max=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
cat > /var/lib/node_exporter/textfile_collector/conntrack.prom << EOF
nf_conntrack_count ${count}
nf_conntrack_max ${max}
EOF

После этого в Grafana строится обычный график с порогом (например, алерт при nf_conntrack_count / nf_conntrack_max > 0.8), и вы получаете предупреждение за минуты или часы до фактического переполнения — время, достаточное чтобы поднять лимит спокойно, а не в аварийном режиме посреди инцидента. Параллельно с наблюдением за самой таблицей полезно понимать, кто вообще генерирует нагрузку на сервер — так проще отличить органический рост от атаки (как узнать, кто нагружает сервер).

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

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

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

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

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

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

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

Технически можно, но каждая запись таблицы занимает память ядра, и на сервере с ограниченным ОЗУ слишком большой лимит либо не даст эффекта (памяти не хватит физически), либо начнёт конкурировать за память с другими процессами. Разумнее подобрать лимит с запасом в 2-4 раза к реальному пику, чем ставить произвольно большое число.

Обязательно ли переполнение conntrack означает атаку?

Нет. Органический рост числа клиентов, всплеск легитимного трафика или просто устаревший лимит на выросшем сервере встречаются чаще, чем целенаправленная атака. Различать помогает conntrack -L: одинаковый source-адрес на десятках портов назначения — повод присмотреться внимательнее, равномерное распределение по многим разным клиентам — скорее органическая нагрузка.

Почему проблема то появляется, то исчезает сама по себе в течение дня?

Потому что таблица не переполнена постоянно — она упирается в лимит только в пиковые минуты (час пик трафика, всплеск подключений, короткая волна сканирования), а в остальное время освобождающиеся по таймауту записи держат заполнение ниже лимита. Отсюда и ощущение «плавающей», непоследовательной проблемы.

Помогает ли перезапуск сети или файрвола?

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

Как понять, что дело именно в conntrack, а не в лимите открытых файловых дескрипторов или в самом приложении?

Ключевой признак — строки nf_conntrack: table full, dropping packet в dmesg и растущий insert_failed/drop в conntrack -S. Если этих признаков нет, а обрывы всё равно есть, стоит проверить ulimit -n для процесса приложения и его собственные логи — там причина, скорее всего, другая.

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

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

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