MAATRIX / Блог / TCP-рукопожатие по шагам: почему при потере одного пакета всё «висит»

TCP-рукопожатие по шагам: почему при потере одного пакета всё «висит»

MAATRIX

Знакомая ситуация: клиент открывает соединение, интерфейс замирает на несколько секунд, а потом всё само оживает — без единой ошибки в логах. Это не мистика и не «глюк сети», а вполне механическое поведение TCP: где-то в пути потерялся один пакет из трёх, которыми стороны обмениваются при установке соединения, и клиент честно ждёт таймаут повторной отправки. Разберём по шагам, что происходит внутри рукопожатия, почему ожидание растёт не линейно, и как это увидеть своими глазами через tcpdump и ss.

Three-way handshake по шагам

TCP — протокол с установлением соединения, и прежде чем полетит хоть один байт полезных данных, стороны обмениваются тремя служебными пакетами. Это классика, но полезно разложить её именно по ролям, потому что дальше мы будем разбирать, что происходит, если один из трёх пакетов пропадает.

  1. SYN — клиент отправляет серверу пакет с флагом SYN и своим начальным номером последовательности (ISN, initial sequence number). Фактически это «я хочу открыть соединение, вот с какого номера я буду считать байты».
  2. SYN-ACK — сервер, получив SYN, кладёт запрос в очередь полуоткрытых соединений (SYN queue / SYN backlog) и отвечает пакетом с флагами SYN и ACK одновременно: подтверждает клиентский ISN и присылает свой собственный ISN.
  3. ACK — клиент подтверждает ISN сервера обычным ACK-пакетом. С этого момента соединение считается установленным (ESTABLISHED) с обеих сторон, и сервер переносит его из очереди полуоткрытых в очередь принятых (accept queue), откуда его заберёт accept() в приложении.

Важный нюанс: соединение с точки зрения клиента становится ESTABLISHED сразу после отправки финального ACK — клиент не ждёт от сервера подтверждения этого ACK. А вот сервер узнаёт, что рукопожатие завершилось, только когда этот третий пакет до него дойдёт. Отсюда и вся последующая асимметрия: если теряется именно ACK, у клиента всё выглядит нормально, а сервер продолжает считать соединение полуоткрытым и может даже повторно прислать SYN-ACK.

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

Что происходит при потере SYN, SYN-ACK или ACK

Если сторона отправила пакет и не получила ожидаемого ответа за отведённое время, она повторяет отправку. Это называется retransmission timeout (RTO) — и именно он отвечает за то самое «подвисание».

  • Потерян SYN. Клиент отправил SYN и ничего не получил в ответ. Сервер вообще не узнал о попытке подключиться — с его стороны никакого состояния не создалось. Клиент ждёт RTO, потом повторяет SYN. Если повтор доходит — рукопожатие продолжается с задержкой на один RTO.
  • Потерян SYN-ACK. Сервер ответил, но ответ не дошёл до клиента. С точки зрения клиента ситуация неотличима от потери SYN — он просто не видит ответа и по истечении таймаута шлёт SYN повторно. А вот сервер уже создал запись в SYN-очереди и, если не получит ACK, тоже начнёт по своему таймеру повторно слать SYN-ACK — независимо от повтора клиента. Иногда это приводит к тому, что клиент получает сразу два SYN-ACK подряд: свой ожидаемый ответ и ретрансмит от сервера.
  • Потерян финальный ACK. Самый коварный случай. Клиент уже считает соединение установленным и может даже успеть отправить данные (TCP это разрешает — данные могут идти сразу за ACK). А сервер, не получив ACK, решает, что SYN-ACK не дошёл, и повторно шлёт его. Если и данные клиента при этом потерялись отдельно, ситуация усложняется ещё сильнее.

Во всех трёх случаях наблюдаемый эффект один и тот же: приложение на клиенте «висит» в ожидании ответа сервера ровно на длительность одного или нескольких RTO, а затем либо соединение устанавливается (после успешного повтора), либо клиент сдаётся и возвращает ошибку ETIMEDOUT.

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

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

Арендовать VPS

Экспоненциальный backoff: откуда берутся «те самые несколько секунд»

Ключевой момент, который объясняет ощущение «зависло на пару секунд, а потом само заработало»: интервалы между повторными попытками не постоянны, а растут. Это называется экспоненциальным backoff, и логика в нём простая — если сеть перегружена или недоступна, нет смысла долбить её повторами каждые 100 мс, лучше постепенно увеличивать паузу.

Ориентировочно (именно ориентировочно — конкретные значения зависят от ОС, версии ядра и настроек net.ipv4.tcp_syn_retries в Linux) прогрессия выглядит так: первая попытка почти мгновенная, затем при отсутствии ответа — пауза около секунды, следующая попытка — уже в несколько раз дольше, и так далее, пока не будет достигнут лимит попыток (в Linux по умолчанию около 5–6 повторов SYN), после чего соединение закрывается с таймаутом. В некоторых источниках приводят прогрессию вида «примерно 1с, 3с, 7с» — суть в том, что каждый следующий интервал заметно больше предыдущего, а не в конкретных секундах: на вашей системе они могут отличаться, и правильный способ узнать точные цифры — посмотреть их самому через tcpdump (см. ниже), а не доверять цифрам из статей.

Именно поэтому один потерянный на входе пакет ощущается не как мгновенная ошибка и не как «зависло навечно», а как пауза в несколько секунд с последующим самопроизвольным восстановлением: пока идёт первый или второй повтор, всё выглядит зависшим, а как только повтор доходит — соединение мгновенно устанавливается, и пользователь видит «само заработало». Если материал теряется на каждом первом пакете стабильно (а не случайно), суммарная задержка может доходить до многих секунд, прежде чем сработает нужный повтор — и тогда уже стоит смотреть, почему nginx отдаёт 504 Gateway Timeout, если между клиентом и приложением стоит прокси.

Как увидеть это своими глазами: tcpdump и ss

Теория теорией, но полезнее один раз увидеть ретрансмит в реальном трафике, чем гадать по симптомам.

Ловим сам факт потери и повтора через tcpdump. Запускаем захват на интересующем порту с таймстампами:

tcpdump -i eth0 -n -tttt 'tcp port 443 and host 203.0.113.10'

Если рукопожатие идёт штатно, вы увидите три строки подряд с минимальной разницей во времени:

10:00:01.001000 IP client.51234 > server.443: Flags [S], seq 1000
10:00:01.001500 IP server.443 > client.51234: Flags [S.], seq 5000, ack 1001
10:00:01.001900 IP client.51234 > server.443: Flags [.], ack 5001

А вот что вы увидите при потере SYN-ACK — повторный SYN от клиента спустя заметный интервал:

10:00:01.001000 IP client.51234 > server.443: Flags [S], seq 1000
10:00:02.010000 IP client.51234 > server.443: Flags [S], seq 1000
10:00:02.010400 IP server.443 > client.51234: Flags [S.], seq 5000, ack 1001
10:00:02.010800 IP client.51234 > server.443: Flags [.], ack 5001

Разница между первой и второй строкой — это и есть RTO в чистом виде, измеренный на вашей конкретной системе и сети, а не выдуманное число из статьи.

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

ss -tin

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

Отдельно проверяем очередь полуоткрытых соединений на сервере, потому что переполненная SYN-очередь — частая причина, по которой сервер вообще не отвечает на SYN:

ss -n state syn-recv
netstat -s | grep -i "syn"

Если syn-recv показывает много записей, а строка вида «SYNs to LISTEN sockets dropped» в статистике растёт — сервер физически не успевает обработать входящие подключения, и часть SYN просто отбрасывается на уровне ядра.

Где чаще всего теряются пакеты на практике

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

  • Переполненная очередь на сетевой карте или в ядре. При всплеске входящих подключений (например, массовый повторный коннект клиентов после сетевого сбоя) очередь netdev или SYN-backlog может переполниться, и лишние SYN просто отбрасываются без какого-либо лога — с точки зрения приложения это выглядит как «сервер не ответил».
  • Файрвол, который делает stateful-трекинг соединений. iptables/nftables через conntrack держат таблицу состояний; если она переполнена или правило неявно режет пакеты без REJECT (просто DROP), клиент не получает вообще ничего и ждёт полный RTO вместо мгновенного отказа.
  • NAT и переполненные conntrack-таблицы на промежуточных роутерах — типичная история для офисных сетей с большим числом одновременных соединений наружу.
  • Перегрузка канала или буферов на промежуточных узлах — обычная сетевая перегрузка, из-за которой пакеты роняются на буферах маршрутизаторов при пиковой нагрузке.
  • Проблемы MTU и фрагментация — реже мешают именно самому SYN (он маленький), но чаще проявляются на последующих пакетах с данными; если видите зависания уже после успешного рукопожатия, разумно проверить настройку MTU для VPN и фрагментацию.
  • Асимметричная маршрутизация, при которой пакет туда идёт одним путём, а ответ обратно — другим, попадая под файрвол, который не видит исходное соединение и роняет ответ как «неожиданный».

Что делать: практическая диагностика и настройка

Порядок действий, который реально работает, когда пользователи жалуются на «зависания при подключении»:

  1. Сначала подтвердите факт, а не гадайте. Запустите tcpdump на клиенте и на сервере одновременно (или хотя бы на одной стороне) во время воспроизведения проблемы. Если видите повторный SYN — вы нашли причину, дальше ищите, где теряется первый.
  2. Проверьте очереди на сервере:
ss -n state syn-recv | wc -l
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.somaxconn

Если очередь syn-recv регулярно близка к tcp_max_syn_backlog — стоит либо увеличить лимит, либо разбираться, почему приложение не успевает обрабатывать accept() быстро.

  1. Проверьте conntrack на файрволе:
sysctl net.netfilter.nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count

Если count регулярно упирается в max — таблица переполняется, и часть новых соединений будет молча отбрасываться.

  1. Проверьте правила файрвола на предмет тихого DROP вместо REJECT там, где это не оправдано защитой от сканирования — иногда разработчики сами не замечают, что правило роняет легитимный трафик без какого-либо ответа.
  2. Посмотрите на сетевую карту и её очереди, особенно если сервер под серьёзной нагрузкой:
ethtool -S eth0 | grep -i drop
ip -s link show eth0

Растущие счётчики drop/overrun на интерфейсе — прямое указание на перегрузку на уровне драйвера или буферов карты.

  1. Если проблема на стороне клиента, а не сервера, полезно повторить те же шаги в обратную сторону: tcpdump на клиенте, проверка локального файрвола, проверка домашнего роутера/провайдерского NAT.

Отдельно стоит проверять именно тот сегмент сети, где регулярно случаются потери — если это путь между вашим сервером и клиентами в конкретном регионе, разбор реального трафика через tcpdump на самом сервере часто даёт больше пользы, чем догадки; смежный разбор с похожей механикой описан в статье про анализ VPN-трафика через tcpdump.

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

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

Арендовать VPS

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

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

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

Можно ли ускорить сам RTO, если знаешь, что сеть быстрая?

На клиенте — обычно нет прямого простого параметра, RTO вычисляется адаптивно на основе RTT предыдущих обменов данными; для самого первого SYN используется начальное консервативное значение, потому что RTT ещё неизвестен. Уменьшать его вручную обычно не нужно и не даёт заметного выигрыша, если проблема не в единичной потере, а в систематическом дропе пакетов.

Почему иногда сервер отвечает SYN-ACK дважды на один SYN?

Потому что сервер и клиент ретранслируют независимо друг от друга по своим таймерам. Если ACK клиента слегка задержался, а сервер уже решил, что пора повторить SYN-ACK — вы увидите оба пакета в захвате. Это нормальное поведение протокола, а не баг.

Как отличить потерю пакета от того, что сервер просто медленно отвечает на прикладном уровне?

По tcpdump: если рукопожатие (SYN/SYN-ACK/ACK) прошло быстро, а пауза идёт уже после — это не про TCP-рукопожатие, а про то, что приложение долго обрабатывает запрос. Ретрансмиты handshake-пакетов видно явно — повторяются именно SYN или SYN-ACK с одинаковым seq.

Обязательно ли ждать полного таймаута, если пакет потерялся?

Да, если у приложения нет собственного более короткого таймаута на подключение. Библиотеки HTTP-клиентов часто задают свой connect-timeout короче системного RTO именно для того, чтобы не ждать секунды, а быстрее переключиться на повтор или другой сервер.

Влияет ли VPN или прокси на вероятность потери SYN?

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

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

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

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