Канал репликации между двумя серверами неделю работает ровно, а в понедельник приложение вдруг пишет в журнал отказ записи. Сессия ssh после обеденного перерыва не отвечает на первое нажатие клавиши. Пул соединений с базой раз в час приносит клиенту ошибку там, где минуту назад запросы проходили. У всех этих историй одна примета: разрыв случился не в момент появления ошибки, а заметно раньше, молча и без единого уведомления. Классическая причина - промежуточное устройство с таблицей состояний, которое удалило запись о неактивном соединении по своему таймауту. Ниже разобраны механика явления, команды чтения таблицы отслеживания соединений на шлюзе с Linux, настройка поддержки активности на уровнях системы, сокета и приложения, а в конце - эксперимент, который точно фиксирует момент потери и проверяет выбранное решение.

Симптомы скрытого разрыва и причина появления ошибки только при следующей записи

Первое, что бросается в глаза при разборе, - несовпадение времени события и времени сообщения о нём. Отправитель вызывает write, ядро принимает данные в буфер сокета и уходит в повторы, потому что подтверждений от партнёра нет. Ошибка всплывает, только когда серия повторов исчерпает лимит net.ipv4.tcp_retries2, по умолчанию равный пятнадцати попыткам, а суммарная пауза до отказа добирается до десятков минут за счёт растущих интервалов между повторами. Поэтому час тишины оборачивается ошибкой, которая приходит через час с четвертью, и со стороны выглядит совершенно случайной.

На принимающей стороне своя тишина: recv либо зависает навсегда, либо возвращает признак конца потока, если партнёр успел закрыть свою половину соединения. Текст ошибки зависит от языка и момента: "Connection reset by peer", "Broken pipe", "Connection timed out", а иногда вместо ошибки просто вечное ожидание без единого байта.

Диагноз становится почти доказанным, когда совпадают три наблюдения: соединение стабильно работает, пока по нему идёт трафик; разрыв наступает после паузы примерно одного масштаба; ошибка проявляется только при следующей попытке отправить данные. Совпали все три - подозревать нужно не кабель и не ядро, а таймаут неактивности где-то между конечными точками.

Порядок проверки, который экономит вечер, состоит из пяти шагов:

  1. воспроизвести разрыв с контролируемым простоем и зафиксировать время обоих событий;
  2. прочитать таблицу отслеживания соединений на шлюзе и таймаут для активных tcp сессий;
  3. проверить системные параметры поддержки активности и настройки сокета в приложении;
  4. сравнить интервал между пакетами с таймаутом таблицы состояний и найти самое строгое звено;
  5. включить поддержку активности или прикладные проверки и повторить простой для контроля.

Шаги идут от дешёвого к дорогому, и на практике виновник обнаруживается уже на втором или третьем.

Таблица состояний промежуточных узлов и механика молчаливого отбрасывания соединений

С точки зрения конечных точек tcp соединение держится, пока живы обе стороны и не пришло явного закрытия. Промежуточное оборудование смотрит на поток иначе. Шлюз с NAT, межсетевой экран с контролем состояний, балансировщик и даже офисный роутер ведут таблицу записей: каждая описывает четвёрку из адресов и портов, состояние соединения и таймер неактивности. Пока пакеты ходят в любую сторону, таймер обновляется при каждом сегменте. Как только поток затихает, таймер доходит до нуля, и запись уходит из таблицы.

Дальше наступает развязка. Пакет клиента по прежней четвёрке прибывает на устройство, а записи для него больше нет. Устройство не знает, что делать с сегментом вне известного состояния, и отбрасывает его без сброса и без уведомления сторон. Оба конца продолжают считать соединение рабочим: отправитель копит данные в буфере и честно повторяет их, получатель спокойно ждёт. О потере стороны узнают лишь тогда, когда повторы исчерпают лимит и write наконец вернёт ошибку.

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

Круг типичных виновников широк. Домашний и офисный роутер с NAT, шлюз хостинга, корпоративный межсетевой экран на границе сети, балансировщик перед веб-приложением, docker-сеть с трансляцией на хосте, мобильный оператор, экономящий адреса. Общее у них одно: таблица конечна, таймаут скрыт от пользователя, а поведение при переполнении непрозрачно. Чем длиннее путь, тем выше шанс встретить самый короткий лимит.

Проверка таймаутов conntrack на шлюзе с Linux и поиск своей записи в таблице

Если шлюз работает под Linux и трафик проходит через таблицу отслеживания соединений ядра, диагностика превращается в чтение конкретных чисел. Таймаут для установленных tcp сессий хранит параметр nf_conntrack_tcp_timeout_established, соседние параметры отвечают за прочие состояния:

sysctl net.netfilter.nf_conntrack_tcp_timeout_established
sysctl net.netfilter.nf_conntrack_tcp_timeout_time_wait
sysctl net.netfilter.nf_conntrack_tcp_timeout_close_wait

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

conntrack -L -p tcp --dport 5432

Вывод выглядит примерно так:

tcp  6  431997 ESTABLISHED src=192.168.10.30 dst=192.168.10.20 sport=54322 dport=5432

Шестёрка здесь - номер протокола, следующее число - остаток таймера в секундах, и он тикает вниз. Теперь включают наблюдение:

watch -n 5 "conntrack -L -p tcp --dport 5432"

Как только строка исчезла из вывода, а на конечных точках соединение всё ещё числится рабочим, диагноз готов: таблица отбросила состояние раньше, чем стороны узнали о потере. Полезны и соседние команды:

sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
conntrack -S
dmesg | grep -i conntrack

Первая пара показывает текущее и предельное число записей, conntrack -S выводит счётчики событий таблицы, а в журнале ядра отдельного внимания заслуживает строка nf_conntrack: table full, dropping packet - она означает, что таблица переполнена и шлюз не успевает держать состояния. Когда пакет conntrack-tools не установлен, те же записи читаются из файла /proc/net/nf_conntrack:

grep dport=5432 /proc/net/nf_conntrack

Даже когда виноват чужой роутер или оборудование хостинга, собственный шлюз с conntrack служит полигоном: на нём механика видна целиком, и любое решение можно проверить до переноса в рабочую среду.

Системные параметры поддержки активности tcp и их настройка через sysctl

Механизм поддержки активности встроен в ядро, но по умолчанию выключен у каждого сокета по отдельности: система молчит, пока приложение не включит опцию сокета SO_KEEPALIVE. После включения ядро выжидает паузу без исходящего трафика, отправляет лёгкий сегмент-зонд и ждёт подтверждения. Ответ партнёра заодно обновляет запись в таблице любого промежуточного устройства, и таймер неактивности там не успевает дойти до нуля. Поведением управляют три параметра:

sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes

Первый задаёт паузу до первого зонда, второй - интервал между зондами, третий - число безответных зондов, после которого ядро закрывает соединение с ошибкой. Классические значения по умолчанию - 7200 секунд, 75 секунд и 9 зондов. Отсюда наглядное объяснение сценария с потерей через час: даже при включённых зондах пауза в два часа не спасает, если промежуточный узел отбрасывает состояние уже через час. Значения правят в файле /etc/sysctl.d/99-keepalive.conf:

net.ipv4.tcp_keepalive_time = 120
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5

И применяют разовой командой:

sysctl -p /etc/sysctl.d/99-keepalive.conf

Общий принцип выбора чисел: пауза до первого зонда должна быть меньше таймаута самого строгого промежуточного узла, да ещё с запасом в несколько раз. При измеренном лимите простоя в час спокойную работу обеспечивают 120-300 секунд до зонда: любой зонд обновляет чужие таймеры, а потеря партнёра при интервале 30 секунд и пяти пробах фиксируется максимум за две с половиной минуты. Проверить, что сокеты действительно получили опции, позволяет ss с колонкой таймеров:

ss -tno state established

Для сокета с включённой поддержкой активности в колонке timer появляется слово keepalive и остаток времени до ближайшего зонда. Неприятная деталь: правка sysctl действует на новые подключения, поэтому после неё долгоживущие сессии стоит переустановить либо перезапустить службу, чтобы они поднялись уже с новыми значениями.

Параметры сокета в приложении и настройка поддержки активности в Python и ssh

Системные значения задают общий шаблон для всех служб машины, а точнее настраивать сокет в самом приложении: у репликации и у панели мониторинга допустимые паузы разные. На уровне сокета работают те же три числа через параметры TCP_KEEPIDLE, TCP_KEEPINTVL и TCP_KEEPCNT поверх включённой опции SO_KEEPALIVE. В Python это выглядит так:

import socket

sock = socket.create_connection(("192.168.10.20", 5432))
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4)

Такой код держит паузу до зонда в 60 секунд, интервал зондов в 10 секунд и закрывает соединение после четырёх неудач, то есть потеря партнёра обнаруживается примерно за полторы минуты. Константы определены в модуле socket на Linux, а в программах на C те же числа передаются в setsockopt напрямую.

Отдельная ниша - ssh, где поддержка активности настраивается без единой строки кода. Клиент шлёт проверочные запросы сам, настроенные в ~/.ssh/config:

Host db-01
    HostName 192.168.10.20
    ServerAliveInterval 30
    ServerAliveCountMax 3

Серверная сторона включается в /etc/ssh/sshd_config параметрами ClientAliveInterval и ClientAliveCountMax, после синтаксического контроля конфигурации и перезапуска службы. Оба механизма отправляют сообщение через шифрованный канал и ждут ответа, а после заданного числа неудач закрывают сессию. По сути это уже прикладная проверка из следующего раздела, встроенная в сам протокол.

Прикладные сообщения проверки состояния и интервал меньше таймаута промежуточного узла

Самый надёжный способ удержать соединение и вовремя опознать потерю - сообщения-проверки на уровне протокола приложения. Короткий запрос известного типа, на который партнёр обязан ответить: пустой SELECT у пула соединений с базой, кадр ping в websocket, служебное сообщение пульса у брокера очередей, запрос метаданных в собственном протоколе. Правило выбора интервала одно: период проверок берут меньше таймаута самого строгого промежуточного узла, с запасом минимум вдвое-втрое.

У прикладных проверок три преимущества перед настройками транспорта. Во-первых, они проходят через любые устройства независимо от операционной системы конечных точек и от прав на системные параметры, включая контейнеры, где sysctl часто закрыт для изменения. Во-вторых, любой ответ партнёра обновляет записи в таблицах состояний по всему пути, то есть решает задачу удержания автоматически. В-третьих, потеря обнаруживается быстро: если проверки ходят каждые 30 секунд, отказ фиксируется в пределах минуты, а не после часа молчания.

Есть и скрытая польза: прикладная проверка различает рабочую, но молчащую сторону и потерянное соединение. Транспортные зонды подтверждают лишь достижимость по tcp, а прикладная проверка свидетельствует, что процесс на той стороне действительно обрабатывает запросы. Для репликации и очередей различие критично: зависший процесс с открытым сокетом выглядит для транспорта полностью исправным.

Практический шаблон для пула соединений с базой: перед выдачей соединения клиенту выполнять лёгкий запрос-проверку либо опрашивать признак годности, который ставит фоновая задача после успешного обмена. Соединения, не прошедшие проверку, закрываются и переустанавливаются, и клиент вообще не встречается с чужой ошибкой записи.

Эксперимент с контролируемым простоем и точная фиксация момента разрыва

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

nc -l 9000

В некоторых сборках ключи слушателя пишутся иначе, например nc -l -p 9000, что легко уточнить в справке утилиты. На второй подключаются:

nc 192.168.10.20 9000

Ход опыта: отправить строку, убедиться, что она дошла, зафиксировать время и остановить руки. На шлюзе наблюдают запись и таймер:

watch -n 5 "conntrack -L -p tcp --dport 9000"

На обеих конечных точках параллельно слушают трафик:

tcpdump -ni eth0 tcp port 9000

Дальше записывают наблюдения. Таймер тикает вниз, в какой-то момент строка исчезает из вывода, при этом tcpdump на конечных точках не показал ни FIN, ни сброс - потеря прошла незаметно для обеих сторон. После исчезновения записи набирают строку в клиенте: ввод принимается локально, подтверждения нет, а ошибка приходит только после серии повторов, с задержкой в десятки минут. Разница между исчезновением записи и ошибкой записи и есть скрытая часть проблемы, из-за которой инциденты списывают на случайность.

Второй заход повторяет опыт с включённой поддержкой активности. Клиент с параметрами TCP_KEEPIDLE 60 из примера выше либо ssh с ServerAliveInterval 30 обновляет запись шлюза при каждом зонде, и после двух-трёх часов искусственного молчания соединение остаётся рабочим, а tcpdump показывает аккуратные пары сегментов каждые 30-60 секунд. Уменьшение интервала в несколько раз против измеренного таймаута держится как главное правило: запас перекрывает и задержки, и краткие перегрузки пути.

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