Диагностика сети: методика, инструменты, разбор реальных инцидентов
«Сеть тормозит» — это не диагноз, а жалоба. Между жалобой и починкой лежит работа, которую большинство инженеров делает интуицией: перезапустить сервис, потыкать ping, посмотреть в дашборд, попробовать с другого ноутбука. Иногда срабатывает. Когда не срабатывает — начинается многочасовое блуждание по слоям, где каждая команда что-то показывает, но ничего не доказывает.
Эта статья — про то, как превратить блуждание в процедуру. Хорошая диагностика похожа на дифференциальную диагностику у врача: у вас есть набор гипотез, каждая гипотеза даёт предсказание, и вы выбираете тест, который отсекает половину гипотез сразу, а не тот, который проще набрать в терминале. Разница между инженером, который чинит сеть за 15 минут, и тем, кто чинит за 6 часов, — почти никогда не в знании протоколов. Она в дисциплине выбора следующего шага.
Это финальная статья трека. Всё, что было раньше — канальный уровень, IP и маршрутизация, TCP, UDP и QUIC, DNS, HTTP, TLS, прокси, CDN и NAT с фаерволами, — здесь превращается в рабочий инструмент. Карта уровней, если нужно освежить, — в обзоре трека.
Методика: делить пополам, а не гадать
Сеть между вашим клиентом и вашим сервером — это цепочка из десяти-пятнадцати звеньев: резолвер, локальный роутер, NAT, провайдер, транзит, anycast-точка CDN, edge-прокси, L4-балансировщик, L7-прокси, sidecar, приложение, база. Отказ находится ровно в одном месте. Наивный подход — проверять звенья по порядку. Правильный — бинарный поиск: каждый тест должен делить оставшееся пространство пополам.
Практически это выражается в трёх вопросах, которые нужно задать до первой команды:
- Что именно сломано? «Медленно» и «не работает» — разные болезни с непересекающимися наборами причин. «Работает, но иногда» — третья, самая тяжёлая. Отделите отказ от деградации, а деградацию — от нестабильности.
- Где граница между работающим и неработающим? Работает с одного хоста и не работает с другого? Работает по IP и не работает по имени? Работает для 99% запросов и падает для 1%? Каждая такая граница — половина пространства долой.
- Что изменилось? 90% инцидентов начинаются с деплоя, изменения конфига, ротации сертификата, скачка трафика или чужой миграции. Вопрос «а что вы сделали в 14:07?» экономит больше времени, чем любой tcpdump.
имя?"} Q1 -->|нет| DNS["DNS: dig +trace,
dig @резолвер, /etc/resolv.conf"] Q1 -->|да| Q2{"TCP-соединение
устанавливается?"} Q2 -->|висит до таймаута| FW["Пакеты дропаются молча:
фаервол DROP, чёрная дыра маршрута,
security group"] Q2 -->|мгновенный refused| NOLISTEN["Никто не слушает порт
или REJECT: ss -tlnp на сервере"] Q2 -->|reset уже после установки| RST["RST от промежуточного узла:
conntrack, idle timeout LB, DPI"] Q2 -->|да| Q3{"TLS-рукопожатие
проходит?"} Q3 -->|нет| TLS["openssl s_client:
цепочка, SNI, версия, часы"] Q3 -->|да| Q4{"Ответ приходит
целиком?"} Q4 -->|виснет после заголовков| MTU["Подозрение на MTU/PMTUD:
ping -M do, MSS clamping"] Q4 -->|код 5xx| L7["L7: логи прокси, upstream,
сопоставить по request-id"] Q4 -->|медленно| PERF["curl -w: какая фаза съела время"] Q4 -->|да| APP["Сеть здорова. Проблема выше:
приложение, БД, логика"]
Три правила, которые экономят часы:
- Не чините то, что не воспроизводится. Первая настоящая задача — получить команду, которая падает по требованию. Если падает 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 «время жизни истекло» от каждого хопа. Отсюда три системных искажения, из-за которых люди годами делают неверные выводы:
- Промежуточные потери почти всегда ложные. Роутер обязан пересылать транзитный трафик, но генерация ICMP — низкоприоритетная задача на control plane, и она жёстко ограничена рейт-лимитом. 30% «потерь» на седьмом хопе при 0% на последнем означают ровно одно: седьмой хоп ленится отвечать. Значение имеет только последняя строка.
- Каждый пробник может идти своим путём. ECMP-балансировка раскладывает потоки по хешу от 5-tuple, а классический traceroute меняет порт на каждом пробнике — то есть каждый пробник попадает в свой поток. Отсюда «мигающие» хопы. Лечится
paris-tracerouteилиtraceroute -T -p 443(фиксированный порт, TCP-пробники, заодно проходит там, где ICMP режут). - Путь туда не равен пути обратно. Задержка, которую вы видите, — сумма 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 не делает.
Первое, что нужно открыть в незнакомом дампе:
- 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 сломан
Лечение — по убыванию правильности:
- Разрешить ICMP типа 3 код 4 на всех фаерволах. Это не «дырка в безопасности», это обязательная часть работы IP.
- MSS clamping на пограничном узле:
iptables -t mangle -A FORWARD -p tcp --syn -j TCPMSS --clamp-mss-to-pmtu. Роутер сам правит опцию MSS в SYN, и обе стороны договариваются о безопасном размере ещё до передачи данных. - Явно уменьшить 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.
соединение числится живым N->>B: POST /v1/orders (новый запрос) B-->>N: RST (сокет уже закрывается) N->>N: upstream prematurely closed N-->>N: 502 Bad Gateway Note over N,B: Окно гонки = время полёта FIN.
Вероятность ≈ RPS_idle × RTT / timeout
Причина. 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. Порядок важен: каждый пункт отсекает ветку дерева.
- Что изменилось за последние сутки? Деплой, конфиг, сертификат, DNS, скачок трафика, чужая миграция.
- Воспроизведи. Получи команду, которая падает по требованию. Если 1 из 200 — цикл на 2000 с логированием.
- Найди границу. Работает/не работает: с какого хоста, по IP или по имени, для какого процента, с какого момента.
- Пять команд первого круга.
dig +short,nc -vz,curl -w,ss -s,nstat -az. - Раздели время по фазам.
curl -wскажет, в какой фазе потеряны секунды: DNS, connect, TLS, TTFB или передача. - Проверь обе стороны. Синхронный
tcpdumpна клиенте и сервере, фильтр по хосту и порту. - Проверь круглые числа. 1/3/7 с — ретрансмиссии SYN. 5 с — DNS. 20/21 с — таймаут connect. 30/60/300 с — idle timeout.
- Проверь MTU. Если «мелкое проходит, крупное виснет» —
ping -M doдо отказа. - Проверь счётчики.
ListenOverflows,TcpRetransSegs,nf_conntrack_count— цифры, а не ощущения. - Зафиксируй причину, а не симптом. «Перезапустили — прошло» означает, что вы вернётесь сюда через неделю. Напишите постмортем и добавьте метрику, которая поймала бы это раньше.
Мини-итог
Диагностика сети — навык, который целиком строится на дисциплине. Механизм один и тот же: сузить пространство поиска вдвое каждым тестом, а не проверять всё подряд.
Три вещи, которые стоит унести с собой. Первое: curl -w с разбивкой по фазам — самый информативный инструмент на единицу усилий; он превращает «медленно» в конкретный слой за одну команду. Второе: круглые числа в измерениях — это всегда таймер, а не сеть; научившись читать 1/3/7/20/30/60/300 секунд, вы будете угадывать причину до первого дампа. Третье: смотреть надо с двух сторон одновременно — асимметрия наблюдений («мы отправили, они не получили») сама по себе является диагнозом.
И самое главное: сеть виновата реже, чем на неё думают. В большинстве случаев, когда «тормозит сеть», тормозит приложение, переполняется очередь, срабатывает чужой таймаут или расходятся конфиги двух соседних компонентов. Задача диагностики — не найти виноватого, а честно и быстро определить границу, за которой начинается настоящая проблема.
Источники
- Wireshark User’s Guide и Wireshark Display Filter Reference — справочник, к которому возвращаются постоянно.
- Chris Sanders, «Practical Packet Analysis», 3-е изд. — лучшая книга-введение в чтение дампов.
- Richard Stevens, «TCP/IP Illustrated, Volume 1», 2-е изд. — фундамент; глава про таймеры и ретрансмиссии объясняет большинство «круглых чисел».
- Brendan Gregg: Linux Performance и BPF Performance Tools — сетевые инструменты eBPF и методика USE.
- man tcpdump и man pcap-filter — синтаксис фильтров BPF.
- Julia Evans: networking zines и статьи — короткие практические разборы, отлично заходят перед дежурством.
- RFC 1191 — Path MTU Discovery и RFC 4821 — Packetization Layer PMTUD.
- RFC 5681 — TCP Congestion Control и RFC 2018 — TCP SACK — что именно происходит при потере.
- Cloudflare Blog: The curious case of slow downloads — образцовый разбор реального инцидента.
- Google SRE Workbook: Incident Response — как организовать расследование, когда людей больше одного.
Что дальше
Трек «Компьютерные сети» закончен. Вы прошли путь от кадра Ethernet до разбора инцидентов — теперь запрос из браузера перестал быть чёрным ящиком на всём протяжении.
Куда идти дальше, в зависимости от того, что вам ближе:
- Распределённые системы — прямое продолжение. Сеть ненадёжна: это аксиома, из которой растут модели отказов, консенсус и наблюдаемость.
- Производительность — если вас затянула тема измерений: как правильно мерить и ввод-вывод с системными вызовами.
- Безопасность — от транспортной безопасности к моделированию угроз и защите API.
- Операционные системы — что происходит с пакетом внутри ядра: сетевой стек и инструментарий Linux.
- DevOps — эксплуатация того, что вы научились чинить: Kubernetes и наблюдаемость с дежурствами.
Общая карта всех треков портала и рекомендованные маршруты обучения — в дорожной карте.