Компьютерные сети Диагностика сети: методика, инструменты, разбор реальных инцидентов
0%

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

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

«Сеть тормозит» — это не диагноз, а жалоба. Между жалобой и починкой лежит работа, которую большинство инженеров делает интуицией: перезапустить сервис, потыкать ping, посмотреть в дашборд, попробовать с другого ноутбука. Иногда срабатывает. Когда не срабатывает — начинается многочасовое блуждание по слоям, где каждая команда что-то показывает, но ничего не доказывает.

Эта статья — про то, как превратить блуждание в процедуру. Хорошая диагностика похожа на дифференциальную диагностику у врача: у вас есть набор гипотез, каждая гипотеза даёт предсказание, и вы выбираете тест, который отсекает половину гипотез сразу, а не тот, который проще набрать в терминале. Разница между инженером, который чинит сеть за 15 минут, и тем, кто чинит за 6 часов, — почти никогда не в знании протоколов. Она в дисциплине выбора следующего шага.

Это финальная статья трека. Всё, что было раньше — канальный уровень, IP и маршрутизация, TCP, UDP и QUIC, DNS, HTTP, TLS, прокси, CDN и NAT с фаерволами, — здесь превращается в рабочий инструмент. Карта уровней, если нужно освежить, — в обзоре трека.

Методика: делить пополам, а не гадать

Сеть между вашим клиентом и вашим сервером — это цепочка из десяти-пятнадцати звеньев: резолвер, локальный роутер, NAT, провайдер, транзит, anycast-точка CDN, edge-прокси, L4-балансировщик, L7-прокси, sidecar, приложение, база. Отказ находится ровно в одном месте. Наивный подход — проверять звенья по порядку. Правильный — бинарный поиск: каждый тест должен делить оставшееся пространство пополам.

Практически это выражается в трёх вопросах, которые нужно задать до первой команды:

  1. Что именно сломано? «Медленно» и «не работает» — разные болезни с непересекающимися наборами причин. «Работает, но иногда» — третья, самая тяжёлая. Отделите отказ от деградации, а деградацию — от нестабильности.
  2. Где граница между работающим и неработающим? Работает с одного хоста и не работает с другого? Работает по IP и не работает по имени? Работает для 99% запросов и падает для 1%? Каждая такая граница — половина пространства долой.
  3. Что изменилось? 90% инцидентов начинаются с деплоя, изменения конфига, ротации сертификата, скачка трафика или чужой миграции. Вопрос «а что вы сделали в 14:07?» экономит больше времени, чем любой tcpdump.

Три правила, которые экономят часы:

  • Не чините то, что не воспроизводится. Первая настоящая задача — получить команду, которая падает по требованию. Если падает 1 запрос из 200, напишите цикл на 2000 запросов и логируйте отличия. Без воспроизведения любая «починка» — совпадение.
  • Каждая гипотеза должна быть фальсифицируемой. «Наверное, сеть плохая» проверить нельзя. «Между edge и бэкендом теряется больше 0.1% пакетов» — можно, за две минуты.
  • Смотрите с обеих сторон одновременно. Половина сетевых загадок разгадывается моментально, когда вы видите: клиент отправил, сервер не получил. Или: сервер ответил, клиент не увидел. Один tcpdump с одной стороны сообщает вдвое меньше, чем два синхронных.

Карта диагностики: уровень, симптом, команда, типичная причина

Инструментарий: чем что меряют

Инструментов много, а слоёв мало. Ниже — минимальный набор, которым закрывается 95% инцидентов; всё остальное — специализация.

Инструмент Что отвечает на вопрос Когда бесполезен
dig как имя превращается в адрес, кто авторитетен, какой TTL если приложение резолвит через свой резолвер или через getaddrinfo с другим search
ping доходит ли ICMP и какой RTT ICMP часто приоритизируется или режется отдельно от TCP — «пинг идёт» ничего не доказывает
mtr / traceroute где на пути теряется и растёт задержка промежуточные потери почти всегда ложные, см. ниже
curl -v -w разбивка запроса по фазам: DNS, connect, TLS, TTFB не покажет, что происходит внутри соединения
ss состояния сокетов, очереди, RTT, ретрансмиссии на живом соединении не видит того, что не дошло до ядра
nstat / netstat -s счётчики стека: дропы, overflow, ретрансмиссии агрегат по хосту, не по соединению
tcpdump что реально ушло и пришло на интерфейс не видит того, что дропнул фаервол до интерфейса на выходе
Wireshark реконструкция диалога, экспертный анализ, графики тяжёлый; на проде почти всегда — только чтение дампа
openssl s_client всё про TLS-рукопожатие и цепочку ничего про HTTP выше
conntrack, nft list ruleset что делает NAT и фаервол на этом хосте требует прав и доступа именно на нужный хост
eBPF-утилиты (tcplife, tcpretrans, tcpconnlat) непрерывное наблюдение без дампа трафика нужен свежий ядерный стек и права

Ниже — по каждому инструменту не «список ключей», а то, как читать вывод.

Первый круг: пять команд за минуту

Когда прилетел инцидент, начинайте не с гипотез, а с фиксированной последовательности. Она даёт снимок реальности и почти всегда сразу отсекает половину дерева.

# 1. Имя: что видит именно этот хост и через какой резолвер
$ dig +short api.example.com A
198.51.100.24
$ dig +short api.example.com AAAA
2001:db8:4::18

# 2. Порт: доходит ли TCP до порта (никакого ICMP)
$ nc -vz -w 3 api.example.com 443
Connection to api.example.com (198.51.100.24) 443 port [tcp/https] succeeded!

# 3. Полный запрос с разбивкой по фазам
$ curl -sS -o /dev/null -w "dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code} ip=%{remote_ip}\n" https://api.example.com/health
dns=0.004 conn=0.021 tls=0.061 ttfb=0.238 total=0.239 code=200 ip=198.51.100.24

# 4. Что происходит с соединениями на этом хосте
$ ss -s
Total: 412
TCP:   1893 (estab 288, closed 1520, orphaned 3, timewait 1516)

# 5. Есть ли дропы на уровне стека
$ nstat -az | grep -Ei 'retrans|drop|overflow|listen'
TcpExtListenOverflows            0                  0.0
TcpExtListenDrops                0                  0.0
TcpRetransSegs                   1841               0.0
TcpExtTCPSynRetrans              12                 0.0

Что уже понятно из этого снимка: имя резолвится, порт открыт, TLS отвечает, но ttfb=0.238 при tls=0.061 — почти 180 мс ушло на генерацию ответа сервером, а не на сеть. Если жалоба была «медленно», сеть здесь ни при чём, и дальше надо идти в приложение. Один экран — и половина работы сделана.

Заведите ~/.curl-format.txt, чтобы не набирать формат каждый раз:

# В файле — строки вида "time_namelookup: %{time_namelookup}s\n" для каждой фазы
$ curl -w "@$HOME/.curl-format.txt" -o /dev/null -sS https://api.example.com/v1/orders
    time_namelookup:  0.004131s
       time_connect:  0.021884s
    time_appconnect:  0.061003s
   time_pretransfer:  0.061190s
 time_starttransfer:  1.238407s
         time_total:  1.241902s
       http_version:  2
          remote_ip:  198.51.100.24:443
      speed_download:  14833 B/s

Как читать эти шесть чисел — самый полезный навык во всей статье:

  • time_namelookup большой (> 50 мс) — проблема DNS: холодный кэш, недоступный первый резолвер, лишние запросы из-за search-доменов.
  • time_connect - time_namelookup большой — это ровно один RTT до сервера плюс возможные ретрансмиссии SYN. Если 1.0 или 3.0 секунды — это не сеть медленная, это потерянный SYN и ретрансмиссия.
  • time_appconnect - time_connect — TLS-рукопожатие: один-два RTT плюс возможная проверка OCSP. Внезапные 300 мс здесь — часто именно OCSP или ECDSA/RSA-подпись на слабом CPU.
  • time_starttransfer - time_pretransfer — TTFB, время думания сервера. Здесь сеть уже почти не при чём.
  • time_total - time_starttransfer — передача тела. Большая разница при маленьком теле означает потери в потоке.

Это разделение — важнейшая ментальная модель. Оно превращает «медленно» в конкретный слой.

DNS: где ломается чаще, чем кажется

Самая частая причина «у меня работает, у клиента нет» — не сеть, а разные ответы DNS. dig даёт полный контроль над тем, кого спрашивать.

# Что отвечает конкретный резолвер и сколько это заняло
$ dig @1.1.1.1 api.example.com A +noall +answer +stats
api.example.com.        43      IN      CNAME   api.example.com.cdn.net.
api.example.com.cdn.net. 20     IN      A       198.51.100.24
;; Query time: 12 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)

# Полная цепочка делегирования от корня — обходит все кэши
$ dig +trace api.example.com | tail -4
api.example.com.        300     IN      CNAME   api.example.com.cdn.net.
;; Received 98 bytes from 203.0.113.53#53(ns1.example.com) in 8 ms

# Что реально сделает приложение (через libc, с учётом search и nsswitch)
$ getent hosts api.example.com
198.51.100.24   api.example.com

Читаем сигналы:

  • status: NXDOMAIN — имени нет ни у кого. Опечатка, забытая запись, не тот домен.
  • status: SERVFAIL — резолвер не смог. Чаще всего: сломанный DNSSEC, недоступные NS, таймаут по пути. dig +cd (checking disabled) отличает DNSSEC-проблему от остальных: если с +cd работает, виноват DNSSEC.
  • status: REFUSED — резолвер вас не обслуживает (не тот сервер, ACL).
  • Разные ответы от разных резолверов — split-horizon DNS или частично применённая миграция. Это самый частый сценарий «у половины пользователей старый IP».
  • Огромный TTL при только что изменённой записи — вы смотрите в чужой кэш, а не в источник. Ходите к авторитетному NS напрямую: dig @ns1.example.com.

Отдельная классика — Kubernetes и ndots:5. В поде /etc/resolv.conf выглядит так:

$ cat /etc/resolv.conf
search prod.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

Имя api.example.com содержит 2 точки — меньше ndots:5, поэтому libc сначала перебирает все search-домены: api.example.com.prod.svc.cluster.local, api.example.com.svc.cluster.local, api.example.com.cluster.local и только потом спрашивает абсолютное имя. Четыре запроса вместо одного, каждый в двух семействах (A и AAAA) — восемь пакетов на каждый резолв внешнего адреса. Под нагрузкой это кладёт CoreDNS. Лечение: точка в конце имени (api.example.com.) или ndots:2 в dnsConfig пода. Подробности механики — в статье про DNS.

TCP: состояния сокетов как диагноз

ss показывает не только «кто с кем соединён», но и внутренности TCP-соединения: RTT, окно перегрузки, число ретрансмиссий. Это самый недооценённый инструмент.

$ ss -tinp state established '( dport = :443 )'
Recv-Q Send-Q  Local Address:Port    Peer Address:Port
0      0       10.0.1.21:51234       198.51.100.24:443   users:(("app",pid=8123,fd=42))
     cubic wscale:8,7 rto:236 rtt:34.612/2.104 ato:40 mss:1448 pmtu:1500
     rcvmss:1448 advmss:1448 cwnd:24 ssthresh:18 bytes_sent:184232
     bytes_retrans:8688 bytes_acked:175544 segs_out:142 segs_in:97
     data_segs_out:128 retrans:0/6 dsack_dups:1 rcv_rtt:36 rcv_space:65535
     send 8.03Mbps lastsnd:12 lastrcv:8 delivery_rate 6.9Mbps busy:2140ms
     retransmits:0 unacked:2

Что здесь важно:

  • rtt:34.612/2.104 — сглаженный RTT и его вариация. Вариация больше трети RTT означает нестабильный путь или буферизацию (bufferbloat).
  • retrans:0/6 — сейчас 0 неподтверждённых ретрансмиссий, всего за жизнь соединения 6. Отношение bytes_retrans/bytes_sent = 8688/184232 ≈ 4.7% — это очень много; норма для здорового пути внутри ДЦ — меньше 0.01%, для интернета — до 0.5%.
  • cwnd:24 при mss:1448 — окно всего ~34 КБ. При RTT 35 мс это потолок примерно 8 Мбит/с, что и подтверждает send 8.03Mbps. Если ждали 100 Мбит/с — проблема не в канале, а в том, что окно не растёт из-за потерь.
  • Recv-Q большой в established — приложение не читает из сокета: тормозит обработчик, а не сеть. Send-Q большой — данные не подтверждаются, значит проблема на пути.

Состояния сокетов — тоже диагноз. Их удобно держать в голове как жизненный цикл:

Практическая таблица «состояние → вывод»:

Что видите Что это значит Куда смотреть дальше
Много SYN_SENT у клиента SYN уходит, ответа нет tcpdump на сервере: дошёл ли SYN. Если дошёл — проблема на обратном пути
Много SYN_RECV у сервера приходят SYN, не приходят финальные ACK SYN-флуд, обрыв обратного пути, net.ipv4.tcp_max_syn_backlog
Растёт CLOSE_WAIT приложение не вызывает close() утечка дескрипторов в коде, не трогайте sysctl
Recv-Q в LISTEN не ноль очередь accept переполняется TcpExtListenOverflows растёт, увеличивайте somaxconn и число воркеров
Десятки тысяч TIME_WAIT нормально при высоком RPS исходящих соединений опасно только при исчерпании ip_local_port_range; лечится keepalive-пулом, а не tcp_tw_recycle (его давно удалили)

Счётчики стека дополняют картину:

$ nstat -az | grep -E 'TcpExtListenOverflows|TcpExtListenDrops|TcpExtTCPSynRetrans|TcpExtTCPLostRetransmit|TcpExtTCPTimeouts|TcpRetransSegs'
TcpExtListenOverflows           1847               0.0
TcpExtListenDrops               1847               0.0
TcpExtTCPSynRetrans             932                0.0
TcpExtTCPTimeouts               418                0.0
TcpRetransSegs                  20114              0.0

ListenOverflows больше нуля — это железный факт: сервер отбрасывал уже установленные соединения, потому что приложение не успевало их принимать. Никакие «сеть плохая» тут не нужны.

Путь: почему traceroute врёт и как им всё-таки пользоваться

traceroute посылает пакеты с растущим TTL и собирает ICMP «время жизни истекло» от каждого хопа. Отсюда три системных искажения, из-за которых люди годами делают неверные выводы:

  1. Промежуточные потери почти всегда ложные. Роутер обязан пересылать транзитный трафик, но генерация ICMP — низкоприоритетная задача на control plane, и она жёстко ограничена рейт-лимитом. 30% «потерь» на седьмом хопе при 0% на последнем означают ровно одно: седьмой хоп ленится отвечать. Значение имеет только последняя строка.
  2. Каждый пробник может идти своим путём. ECMP-балансировка раскладывает потоки по хешу от 5-tuple, а классический traceroute меняет порт на каждом пробнике — то есть каждый пробник попадает в свой поток. Отсюда «мигающие» хопы. Лечится paris-traceroute или traceroute -T -p 443 (фиксированный порт, TCP-пробники, заодно проходит там, где ICMP режут).
  3. Путь туда не равен пути обратно. Задержка, которую вы видите, — сумма RTT в обе стороны, а обратный путь вы вообще не наблюдаете. Асимметрия — норма в интернете, и она источник половины загадок с фаерволами.
# -r отчёт, -w широкий формат, -z показывать таймштампы, -b IP и имя, -c число проб
$ mtr -rwzbc 100 api.example.com
Start: 2026-07-16T09:41:12+0300
HOST: workstation                    Loss%  Snt  Last   Avg  Best  Wrst StDev
  1. AS???    192.168.1.1             0.0%  100   0.6   0.7   0.5   2.1   0.2
  2. AS12345  10.20.0.1               0.0%  100   3.1   3.4   2.9   9.8   0.7
  3. AS12345  212.0.0.9              28.0%  100   9.2   9.6   8.8  21.4   1.9
  4. AS3356   4.68.111.1              0.0%  100  14.7  15.1  14.2  28.0   1.6
  5. AS64500  198.51.100.1            0.0%  100  31.8  32.4  31.1  48.9   2.4
  6. AS64500  198.51.100.24           0.0%  100  32.0  32.6  31.4  46.2   2.1

Хоп 3 показывает 28% потерь, хопы 4–6 — ноль. Это не потери: транзитный трафик через хоп 3 проходит нормально, просто роутер режет собственные ICMP-ответы. Настоящая потеря выглядит иначе: процент не падает обратно к нулю на последующих хопах.

Ещё одна деталь: рост задержки на 20 мс между хопами 4 и 5 — это не «плохой роутер», а география. 15 мс ≈ 1500 км оптики туда-обратно. Скорость света в стекле — примерно 200 000 км/с, отсюда простое правило: 1 мс RTT ≈ 100 км в одну сторону. Если RTT до соседней стойки 8 мс, дело не в расстоянии.

tcpdump: снимать правильно с первого раза

На проде вы редко получаете второй шанс снять дамп. Поэтому — сразу правильно.

# Базовая форма: интерфейс, без резолва имён, полный пакет, в файл с ротацией
$ sudo tcpdump -i any -nn -s 0 -w /var/tmp/cap.pcap -C 100 -W 10 \
    'host 198.51.100.24 and tcp port 443'

# -nn  не резолвить ни имена, ни порты (иначе tcpdump сам генерирует DNS-трафик)
# -s 0 захватывать пакет целиком (по умолчанию в старых версиях резалось до 68 байт)
# -C 100 -W 10  кольцевой буфер: 10 файлов по 100 МБ, старые перезаписываются

Полезные фильтры, которые стоит помнить наизусть:

# Только SYN — кто пытается соединиться
$ sudo tcpdump -i any -nn 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'

# Только RST — кто рвёт соединения (самый ценный фильтр при «connection reset»)
$ sudo tcpdump -i any -nn 'tcp[tcpflags] & tcp-rst != 0'

# ICMP «нужна фрагментация» — прямое доказательство проблем с MTU
$ sudo tcpdump -i any -nn 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'

Как выглядит здоровое рукопожатие:

$ sudo tcpdump -i any -nn -ttt 'host 198.51.100.24 and port 443'
 00:00:00.000000 IP 10.0.1.21.51234 > 198.51.100.24.443: Flags [S], seq 2847362910, win 64240, options [mss 1460,sackOK,TS val 91827364 ecr 0,nop,wscale 7], length 0
 00:00:00.032118 IP 198.51.100.24.443 > 10.0.1.21.51234: Flags [S.], seq 118293746, ack 2847362911, win 65160, options [mss 1440,sackOK,TS val 55211893 ecr 91827364,nop,wscale 7], length 0
 00:00:00.000041 IP 10.0.1.21.51234 > 198.51.100.24.443: Flags [.], ack 1, win 502, length 0
 00:00:00.000198 IP 10.0.1.21.51234 > 198.51.100.24.443: Flags [P.], seq 1:518, ack 1, win 502, length 517
 00:00:00.033402 IP 198.51.100.24.443 > 10.0.1.21.51234: Flags [P.], seq 1:3421, ack 518, win 509, length 3420

Читается так: [S][S.] через 32 мс (это ваш RTT) → [.] мгновенно (ACK формируется локально) → 517 байт ClientHello → ответ сервера через ещё один RTT. Всё нормально: два RTT до первого байта прикладных данных, ровно как обещает TLS 1.3.

А вот как выглядит потерянный SYN:

 00:00:00.000000 IP 10.0.1.21.51235 > 198.51.100.99.443: Flags [S], seq 771823411, win 64240, length 0
 00:00:01.021847 IP 10.0.1.21.51235 > 198.51.100.99.443: Flags [S], seq 771823411, win 64240, length 0
 00:00:02.045912 IP 10.0.1.21.51235 > 198.51.100.99.443: Flags [S], seq 771823411, win 64240, length 0
 00:00:04.093774 IP 10.0.1.21.51235 > 198.51.100.99.443: Flags [S], seq 771823411, win 64240, length 0

Тот же seq, интервалы 1, 2, 4 секунды — экспоненциальный откат ретрансмиссий SYN. Ответа нет вообще. Это не «медленная сеть»: это молчаливый дроп. Если бы порт был закрыт, пришёл бы RST мгновенно; если бы фаервол делал REJECT — ICMP port unreachable. Тишина = DROP в фаерволе, отсутствие обратного маршрута или балансировщик без живых бэкендов. Подробнее про разницу DROP и REJECT — в статье про NAT и фаерволы.

Что видно в Wireshark

Wireshark — это tcpdump плюс реконструкция состояния. Он самостоятельно отслеживает номера последовательностей и помечает аномалии, чего tcpdump не делает.

Потеря пакета глазами Wireshark: fast retransmit против RTO

Первое, что нужно открыть в незнакомом дампе:

  • Statistics → Conversations, вкладка TCP, сортировка по Duration и по Bytes. За десять секунд видно, кто с кем разговаривает больше всего и какие соединения аномально долгие.
  • Analyze → Expert Information. Wireshark сам разложит по группам: Warning — ретрансмиссии и dup ACK, Note — keep-alive и window updates, Error — испорченные пакеты.
  • Statistics → I/O Graph с двумя графиками: tcp.analysis.retransmission и общий bps. Если пики ретрансмиссий совпадают с пиками трафика — вы упираетесь в полосу и переполняете буфер где-то по пути.
  • Statistics → TCP Stream Graphs → Time Sequence (tcptrace) — самая информативная картинка. Ровная лесенка с плотной упаковкой = здоровое соединение. Плоские «полки» = окно закрыто. Вертикальные всплески вниз = ретрансмиссии.

Фильтры отображения, которые нужно знать:

tcp.analysis.flags                     # всё, что Wireshark считает аномалией
tcp.analysis.retransmission            # ретрансмиссии
tcp.analysis.fast_retransmission       # быстрая ретрансмиссия по трём dup ACK
tcp.analysis.duplicate_ack             # дубликаты ACK
tcp.analysis.zero_window               # получатель закрыл окно
tcp.analysis.ack_rtt > 0.2             # медленные подтверждения
tcp.flags.reset == 1                   # кто рвёт соединения
tls.handshake.type == 1                # ClientHello, видно SNI и предлагаемые версии
http.time > 1                          # медленные HTTP-ответы, если трафик не шифрован

Как отличить потерю от медленного приложения — по картинке. Потеря: сегмент отсутствует, дальше идут dup ACK с SACK-блоками, затем ретрансмиссия примерно через один RTT, и поток продолжается. Медленное приложение: сегменты идут ровно, но между запросом и ответом — пустая пауза без единого пакета. Первое чинится в сети, второе — в коде. Эти два случая путают чаще всего.

Zero window — отдельный важный симптом. Получатель объявляет win=0, отправитель встаёт и начинает слать window probes. Это всегда означает: приложение на принимающей стороне не вычитывает данные из сокета. Сеть здорова, тормозит потребитель.

Отдельно про TLS: начиная с TLS 1.3 содержимое зашифровано, но ClientHello виден всегда, включая SNI и список версий/шифров. Это достаточная информация, чтобы понять, почему рукопожатие провалилось. Если у вас есть SSLKEYLOGFILE (браузеры и curl умеют его писать), Wireshark расшифрует поток целиком: Preferences → Protocols → TLS → (Pre)-Master-Secret log filename.

MTU и PMTUD: «всё работает, пока ответ маленький»

Самый коварный класс отказов. Симптом: рукопожатие проходит, короткие ответы приходят, а любой ответ больше килобайта-полутора виснет навсегда. Причина: где-то по пути MTU меньше вашего, роутер отправляет ICMP «Fragmentation Needed», а этот ICMP кто-то дропает — потому что «ICMP небезопасен». Отправитель не знает, что нужно уменьшить пакеты, и упорно шлёт слишком большие.

# Ищем реальный MTU пути: -M do запрещает фрагментацию, -s задаёт данные (без 28 байт заголовков)
$ ping -M do -s 1472 -c 2 api.example.com
PING api.example.com (198.51.100.24) 1472(1500) bytes of data.
ping: local error: message too long, mtu=1450

$ ping -M do -s 1422 -c 2 api.example.com
1430 bytes from 198.51.100.24: icmp_seq=1 ttl=54 time=32.4 ms

1422 + 28 = 1450 — реальный MTU пути. Классические источники уменьшения: VXLAN/Geneve в оверлейной сети Kubernetes (обычно 1450), IPsec и WireGuard (1420 и ниже), PPPoE у домашних провайдеров (1492).

Прямое доказательство блокировки ICMP:

$ sudo tcpdump -i any -nn 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'
# тишина при заведомо больших ответах = ICMP не доходит = PMTUD сломан

Лечение — по убыванию правильности:

  1. Разрешить ICMP типа 3 код 4 на всех фаерволах. Это не «дырка в безопасности», это обязательная часть работы IP.
  2. MSS clamping на пограничном узле: iptables -t mangle -A FORWARD -p tcp --syn -j TCPMSS --clamp-mss-to-pmtu. Роутер сам правит опцию MSS в SYN, и обе стороны договариваются о безопасном размере ещё до передачи данных.
  3. Явно уменьшить MTU на интерфейсах контейнерной сети до значения, которое точно проходит.

Проверка на живом сокете: ss -tin показывает pmtu:1500 и mss:1448 — если реальный путь 1450, значит PMTUD не сработал и вы в чёрной дыре.

Инцидент 1: «Каждый двухсотый запрос — 502»

Симптом. Через nginx-прокси примерно 0.5% запросов возвращают 502. Бэкенд в своих логах ошибок не видит. Воспроизводится только под нагрузкой, никогда — вручную.

Первый круг. curl в цикле на 2000 запросов ловит 9 пятьсот вторых. В error.log nginx:

upstream prematurely closed connection while reading response header from upstream,
client: 10.0.0.9, server: api.example.com, upstream: "http://10.0.1.21:8080/v1/orders"

Слово prematurely — ключевое. Соединение закрыл бэкенд, а не прокси.

Гипотеза и проверка. Похоже на гонку keepalive. Смотрим RST со стороны бэкенда:

$ sudo tcpdump -i any -nn 'host 10.0.0.9 and tcp port 8080 and tcp[tcpflags] & (tcp-fin|tcp-rst) != 0'
 09:41:02.118 IP 10.0.1.21.8080 > 10.0.0.9.44212: Flags [F.], seq 88213, ack 4192, win 501, length 0
 09:41:02.118 IP 10.0.0.9.44212 > 10.0.1.21.8080: Flags [P.], seq 4192:4671, ack 88214, win 502, length 479
 09:41:02.118 IP 10.0.1.21.8080 > 10.0.0.9.44212: Flags [R], seq 88214, win 0, length 0

Вот она, гонка, в трёх строках одной миллисекунды: бэкенд отправил FIN (истёк его idle timeout), но в этот же момент nginx уже положил в это соединение новый запрос. Бэкенд отвечает RST, nginx рапортует 502.

Причина. keepalive_timeout на бэкенде (5 с) был меньше или равен времени удержания соединения в пуле nginx. Кто закрывает первым — тот выигрывает гонку; проигрывает всегда клиент пула.

Починка. Правило простое: таймаут простоя у клиента (прокси) должен быть строго меньше, чем у сервера (бэкенда) — процентов на 20–30. Тогда соединение всегда закрывает прокси, контролируемо, без запроса в полёте.

upstream app {
    server 10.0.1.21:8080;
    keepalive 64;
    keepalive_timeout 30s;     # меньше, чем idle timeout бэкенда
    keepalive_requests 1000;
}
server {
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;              # обязательно, иначе keepalive не работает
        proxy_set_header Connection "";      # иначе уйдёт "Connection: close"
        proxy_next_upstream error timeout non_idempotent;  # осознанный ретрай
    }
}

Бэкенду ставим idle timeout 60–75 с. Дополнительно — идемпотентные ретраи на прокси, чтобы остаточная гонка не долетала до пользователя. Подробнее о балансировке — в статье про прокси.

Инцидент 2: «Ровно 20 секунд, потом ошибка»

Симптом. Часть запросов к внутреннему сервису падает по таймауту. Время всегда одинаковое — 20.0 секунд с точностью до десятых.

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

Наблюдаемое время Что это скорее всего
1 с, 3 с, 7 с, 15 с ретрансмиссии SYN (экспоненциальный откат 1-2-4-8)
5 с ровно таймаут DNS-резолвера по умолчанию в glibc
20 с / 21 с net.ipv4.tcp_syn_retries по умолчанию — итоговый таймаут connect
30 с / 60 с / 300 с idle timeout балансировщика или NAT-шлюза
75 с connect_timeout по умолчанию во многих HTTP-клиентах
120 с таймаут proxy_read_timeout в некоторых сборках, tcp_retries2

Проверка.

$ time curl -sS -o /dev/null http://internal-api.prod:8080/health
curl: (28) Failed to connect to internal-api.prod port 8080 after 21004 ms: Timeout was reached
real    0m21.007s

$ sudo tcpdump -i any -nn 'host internal-api.prod and port 8080'
 00:00:00.000 IP 10.0.1.21.39512 > 10.0.4.77.8080: Flags [S], seq 1042, length 0
 00:00:01.024 IP 10.0.1.21.39512 > 10.0.4.77.8080: Flags [S], seq 1042, length 0
 00:00:03.072 IP 10.0.1.21.39512 > 10.0.4.77.8080: Flags [S], seq 1042, length 0
 00:00:07.168 IP 10.0.1.21.39512 > 10.0.4.77.8080: Flags [S], seq 1042, length 0
 00:00:15.360 IP 10.0.1.21.39512 > 10.0.4.77.8080: Flags [S], seq 1042, length 0

Пять SYN, откат 1-2-4-8, ответа нет. Это в точности картина из раздела про tcpdump: молчаливый дроп. Значение tcp_syn_retries=6 даёт суммарно около 127 секунд, значение 5 — около 31; конкретное 21 с здесь — таймаут самого клиента.

Причина. Сервис имел два адреса в DNS: один под рабочим подом, второй — от пода, удалённого при масштабировании, чей IP успел уйти в чёрную дыру маршрутизации. Клиент случайно выбирал один из двух адресов — отсюда «часть запросов».

Как это ловится за 30 секунд. dig +short вернул два A-адреса; curl --resolve каждый по отдельности сразу показал, какой мёртв:

$ curl -sS -o /dev/null -w '%{http_code} %{time_connect}\n' --resolve internal-api.prod:8080:10.0.4.12 http://internal-api.prod:8080/health
200 0.002

$ curl -sS -o /dev/null -w '%{http_code} %{time_connect}\n' --resolve internal-api.prod:8080:10.0.4.77 http://internal-api.prod:8080/health
curl: (28) ... Timeout

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

Инцидент 3: «Connection reset by peer примерно раз в час»

Симптом. Долгоживущие соединения (WebSocket и пул к базе) рвутся с ECONNRESET. Периодичность плавающая, но всегда после периода тишины.

Проверка. Смотрим, кто именно шлёт RST, с обеих сторон одновременно:

# на клиенте
$ sudo tcpdump -i any -nn 'tcp[tcpflags] & tcp-rst != 0 and host 10.0.4.30'
 11:02:41.882 IP 10.0.4.30.5432 > 10.0.1.21.48822: Flags [R.], seq 1, ack 1, win 0, length 0
# на сервере — тишина, RST он не отправлял

RST приходит «от имени сервера», но сервер его не отправлял. Значит, его сгенерировал промежуточный узел: NAT-шлюз, stateful-фаервол или балансировщик, у которого истёк idle timeout на записи в таблице состояний.

$ conntrack -S | head -3
cpu=0 found=0 invalid=1204 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=88213
$ sysctl net.netfilter.nf_conntrack_tcp_timeout_established
net.netfilter.nf_conntrack_tcp_timeout_established = 3600

Час — знакомое число. Ровно оно и стояло в симптоме.

Причина. Запись в conntrack (или в таблице облачного NAT-шлюза, где обычно 350 секунд) удаляется по простою. Следующий пакет по «уже несуществующему» соединению получает RST или молча дропается.

Починка. Не увеличивать таймаут NAT (вы его часто и не контролируете), а обновлять соединение изнутри. TCP keepalive должен быть заметно меньше самого короткого idle timeout на пути:

# 60 с простоя → первый пробник, дальше каждые 10 с, 6 попыток
$ sudo sysctl -w net.ipv4.tcp_keepalive_time=60
$ sudo sysctl -w net.ipv4.tcp_keepalive_intvl=10
$ sudo sysctl -w net.ipv4.tcp_keepalive_probes=6

Важная тонкость: системные значения по умолчанию — 7200 секунд (два часа), и приложение обязано явно включить SO_KEEPALIVE на сокете, иначе эти параметры не применяются. В пулах соединений к БД включайте keepalive явно; на прикладном уровне для WebSocket — ping/pong кадры каждые 30 секунд (см. статью про реальное время).

Инцидент 4: «После деплоя часть клиентов не может подключиться»

Симптом. Браузеры работают, мобильное приложение на части устройств получает ошибку TLS, curl на CI падает с unable to get local issuer certificate.

Проверка — одна команда, полный ответ.

$ openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>&1 | head -30
CONNECTED(00000003)
depth=0 CN = api.example.com
verify error:num=20:unable to get local issuer certificate
verify return:1
---
Certificate chain
 0 s:CN = api.example.com
   i:C = US, O = Let's Encrypt, CN = R11
---
SSL handshake has read 2183 bytes and written 393 bytes
Verification error: unable to get local issuer certificate
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

В блоке Certificate chain ровно один сертификат. Промежуточный (R11) не отдан. Браузеры это переживают, потому что умеют докачивать недостающее звено по AIA (Authority Information Access) и держат большой кэш промежуточных CA. curl, Java, Go и мобильные стеки — не умеют или умеют не всегда. Отсюда «у одних работает, у других нет».

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

$ openssl s_client -connect good.example.com:443 -servername good.example.com </dev/null 2>&1 | sed -n '/Certificate chain/,/---/p'
Certificate chain
 0 s:CN = good.example.com
   i:C = US, O = Let's Encrypt, CN = R11
 1 s:C = US, O = Let's Encrypt, CN = R11
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1

Полезный набор для отладки TLS:

# Даты и SAN — проверить, что имя вообще покрыто сертификатом
$ echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
    | openssl x509 -noout -dates -subject -ext subjectAltName
notBefore=May  2 08:14:31 2026 GMT
notAfter=Jul 31 08:14:30 2026 GMT
X509v3 Subject Alternative Name:
    DNS:api.example.com, DNS:www.api.example.com

# Что отдаётся без SNI — частая причина «пришёл не тот сертификат»
$ openssl s_client -connect api.example.com:443 -noservername </dev/null 2>&1 | grep 'subject='

# Проверить поддержку конкретной версии — старые Android не умеют TLS 1.3
$ openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_2 </dev/null 2>&1 | grep 'Cipher is'

Причина в нашем инциденте. При деплое в nginx положили cert.pem вместо fullchain.pem. Разница в одном слове конфига и в двух днях расследования.

Профилактика. Проверка цепочки в CI после каждого выпуска сертификата — три строки, а ловит целый класс инцидентов:

$ openssl s_client -connect api.example.com:443 -servername api.example.com \
    -verify_return_error -CApath /etc/ssl/certs </dev/null >/dev/null 2>&1 \
    && echo "chain ok" || { echo "chain BROKEN"; exit 1; }

Подробности про доверие и цепочки — в статье про TLS и в треке безопасности: транспортная безопасность.

Инцидент 5: «Загрузка файлов из офиса виснет, из дома работает»

Симптом. Из офисной сети POST с файлом больше ~2 КБ виснет навсегда; GET и мелкие POST работают. Из дома всё нормально.

Симптом «маленькое проходит, большое виснет» — почти диагноз с первого предложения: MTU. ping -M do -s 1472 из офиса сразу вернул message too long, mtu=1420 (офисный VPN на WireGuard), а tcpdump по фильтру icmp[icmptype] == 3 and icmp[icmpcode] == 4 не поймал ни одного пакета — ICMP до отправителя не доходит.

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

Что запомнить. Формулировка «работает из одной сети, не работает из другой» плюс «мелкое проходит, крупное виснет» = MTU, пока не доказано обратное. Не тратьте время на приложение.

Наблюдаемость: как ловить это до инцидента

Диагностика по факту — это уже поражение. Минимальный набор непрерывных сигналов, который окупается с первого же инцидента:

На хостах:

# Доля ретрансмиссий — главный индикатор здоровья пути
$ nstat -az TcpRetransSegs TcpOutSegs
# retrans_rate = TcpRetransSegs / TcpOutSegs; тревога при > 0.5%

# Переполнение accept-очереди — прямое доказательство перегрузки сервиса
$ nstat -az TcpExtListenOverflows

# Заполнение conntrack — предвестник массовых необъяснимых дропов
$ cat /proc/sys/net/netfilter/nf_conntrack_count /proc/sys/net/netfilter/nf_conntrack_max
184213
262144        # тревога при заполнении выше 70%

eBPF вместо дампов. Современный способ смотреть на сеть непрерывно и почти бесплатно:

# Время установления соединения по каждому connect — ловит медленный DNS-round-robin
$ sudo tcpconnlat-bpfcc
PID    COMM       IP SADDR         DADDR          DPORT  LAT(ms)
8123   app        4  10.0.1.21     10.0.4.12      8080     0.42
8123   app        4  10.0.1.21     10.0.4.77      8080  1002.18   <-- ретрансмиссия SYN

# Кто и что ретрансмитит прямо сейчас (плюс tcplife-bpfcc — жизненный цикл соединений)
$ sudo tcpretrans-bpfcc
TIME     PID    IP LADDR:LPORT          T> RADDR:RPORT           STATE
11:42:03 8123   4  10.0.1.21:51234      R> 198.51.100.24:443     ESTABLISHED

Преимущество eBPF перед tcpdump в том, что он не копирует пакеты в userspace: накладные расходы измеряются процентами процента, и его можно держать включённым постоянно. Это, по сути, наблюдаемость на уровне ядра; про организацию дежурств и алертов вокруг таких метрик — в devops-треке, а про то, как ядро обрабатывает пакет — в сетевом стеке ОС.

Сквозной идентификатор запроса. Самая дешёвая и самая недооценённая мера: X-Request-Id, который генерируется на edge и проносится через все хопы в логи каждого. Без него сопоставление «этот 502 у клиента» с «эта строчка в логе бэкенда» превращается в археологию по таймштампам. С ним — один grep.

Антипаттерны, которые стоят часов

  • «Пинг идёт, значит сеть в порядке». ICMP и TCP на порт 443 — разные пути через фаерволы, разные приоритеты в очередях, разная судьба. Проверяйте тем же протоколом и портом, которым ходит приложение: nc -vz, curl, traceroute -T -p 443.
  • Смотреть только со своей стороны. «Мы отправили» и «они получили» — разные факты. Синхронный дамп с двух концов разгадывает половину загадок за минуту.
  • Верить промежуточным хопам traceroute. Значение имеет только последняя строка.
  • Крутить sysctl наугад. tcp_tw_reuse, somaxconn, размеры буферов — всё это лечит конкретные, диагностированные болезни. Без счётчика, который доказывает проблему, любая правка — карго-культ. tcp_tw_recycle вообще удалён из ядра с 4.12: он ломал клиентов за NAT.
  • Чинить симптом ретраями. Ретраи на 5xx без бюджета и без экспоненциального отката превращают частичную деградацию в полный отказ. Ретраить можно только идемпотентное, только с джиттером и только с ограничением доли ретраев от общего трафика.
  • Игнорировать «что изменилось». Проверять сеть, когда час назад выкатили новый образ, — почти всегда потеря времени.
  • Дамп без ротации на проде. tcpdump -w /var/log/cap.pcap без -C/-W однажды заполнит диск и превратит инцидент в аварию. Всегда кольцевой буфер.
  • Считать, что «повторяется раз в час» — это про нагрузку. Периодичность почти всегда указывает на таймер: idle timeout, ротацию, cron, обновление токена.

Рабочий чек-лист

Держите его в runbook. Порядок важен: каждый пункт отсекает ветку дерева.

  1. Что изменилось за последние сутки? Деплой, конфиг, сертификат, DNS, скачок трафика, чужая миграция.
  2. Воспроизведи. Получи команду, которая падает по требованию. Если 1 из 200 — цикл на 2000 с логированием.
  3. Найди границу. Работает/не работает: с какого хоста, по IP или по имени, для какого процента, с какого момента.
  4. Пять команд первого круга. dig +short, nc -vz, curl -w, ss -s, nstat -az.
  5. Раздели время по фазам. curl -w скажет, в какой фазе потеряны секунды: DNS, connect, TLS, TTFB или передача.
  6. Проверь обе стороны. Синхронный tcpdump на клиенте и сервере, фильтр по хосту и порту.
  7. Проверь круглые числа. 1/3/7 с — ретрансмиссии SYN. 5 с — DNS. 20/21 с — таймаут connect. 30/60/300 с — idle timeout.
  8. Проверь MTU. Если «мелкое проходит, крупное виснет» — ping -M do до отказа.
  9. Проверь счётчики. ListenOverflows, TcpRetransSegs, nf_conntrack_count — цифры, а не ощущения.
  10. Зафиксируй причину, а не симптом. «Перезапустили — прошло» означает, что вы вернётесь сюда через неделю. Напишите постмортем и добавьте метрику, которая поймала бы это раньше.

Мини-итог

Диагностика сети — навык, который целиком строится на дисциплине. Механизм один и тот же: сузить пространство поиска вдвое каждым тестом, а не проверять всё подряд.

Три вещи, которые стоит унести с собой. Первое: curl -w с разбивкой по фазам — самый информативный инструмент на единицу усилий; он превращает «медленно» в конкретный слой за одну команду. Второе: круглые числа в измерениях — это всегда таймер, а не сеть; научившись читать 1/3/7/20/30/60/300 секунд, вы будете угадывать причину до первого дампа. Третье: смотреть надо с двух сторон одновременно — асимметрия наблюдений («мы отправили, они не получили») сама по себе является диагнозом.

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

Источники

Что дальше

Трек «Компьютерные сети» закончен. Вы прошли путь от кадра Ethernet до разбора инцидентов — теперь запрос из браузера перестал быть чёрным ящиком на всём протяжении.

Куда идти дальше, в зависимости от того, что вам ближе:

Общая карта всех треков портала и рекомендованные маршруты обучения — в дорожной карте.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов