← назад к разделу

«Сервис не отвечает» — это не диагноз, а симптом. За ним прячется десяток разных причин: имя не разрешается, порт закрыт, соединение установилось, но ответа нет, ответ приходит, но через десять секунд. Пока причина не названа, чинить нечего.

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

Порядок такой: имя → порт → запрос целиком → соединения → трафик. Каждый следующий шаг дороже предыдущего, и до последнего доходят редко.

Шаг 1: разрешается ли имя

Первое, что делает клиент, — превращает имя в адрес. Если это не работает, всё остальное не имеет значения.

dig +short orders.internal
# 10.2.14.7

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

dig orders.internal | grep -E "SERVER|ANSWER"
# ;; ANSWER SECTION:
# ;; SERVER: 10.96.0.10#53(10.96.0.10)

Две ловушки на этом шаге. Первая: dig спрашивает сервер имён напрямую, а приложение может держать собственный кэш адресов — Java кэширует их по умолчанию на 30 секунд, и после переезда сервиса она ещё какое-то время ходит на старый адрес. Если dig показывает новый адрес, а приложение стучится в старый, дело именно в этом. Вторая: внутри кластера короткое имя достраивается до полного по списку доменов поиска, и запрос уходит не туда, куда вы думали, — полное имя с точкой на конце (orders.internal.) снимает вопрос.

Шаг 2: открыт ли порт

Имя разрешилось — проверяем, принимает ли кто-то соединения на нужном порту.

curl -v --connect-timeout 3 telnet://10.2.14.7:8080

Здесь важны не подробности вывода, а какая из трёх вещей случилась.

Соединение установилось («Connected to») — порт открыт, слушающий процесс есть. Проблема дальше, идите к третьему шагу.

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

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

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

ss -ltnp | grep 8080
# LISTEN 0 4096 *:8080 *:* users:(("java",pid=1,fd=42))

Смотрите на адрес слева от порта. *:8080 или 0.0.0.0:8080 — принимает отовсюду. А вот 127.0.0.1:8080 — только со своей машины: снаружи такой порт закрыт, и это частая причина «локально работает, в контейнере нет».

Шаг 3: где теряется время

Соединение устанавливается, ответ приходит, но медленно. Дальше нужно понять, на каком именно этапе уходит время, — и это curl умеет показывать сам.

curl -o /dev/null -s -w \
  'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
  https://orders.internal/api/orders/1
# dns=0.004 connect=0.005 tls=0.061 ttfb=1.842 total=1.845

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

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

Обратная картина — большой connect при мгновенном ttfb — говорит о том, что время съедает установка соединения: далёкий сервер, потери пакетов или переполненная очередь на приёме.

Шаг 4: сколько соединений и в каких они состояниях

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

ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
#  842 ESTAB
#  611 TIME-WAIT
#   47 CLOSE-WAIT
#    3 SYN-SENT

Что означают перекосы:

  • Много TIME-WAIT — норма для сервера, который часто закрывает соединения сам. Тревожно, когда их десятки тысяч на клиенте: значит, соединения не переиспользуются, и скоро кончатся свободные исходящие порты. Лечится включением keep-alive, а не настройками ядра.
  • Растущий CLOSE-WAIT — почти всегда ошибка в коде: собеседник закрыл свою сторону, а приложение не закрыло свою. Такие соединения не исчезнут сами и будут копиться, пока не упрётся лимит открытых файлов.
  • Много SYN-SENT — запросы уходят и остаются без ответа: та же картина, что и «молчит» на втором шаге, только видно её массово.

Шаг 5: посмотреть на сам трафик

Если предыдущие шаги не объяснили происходящее, остаётся посмотреть, что реально уходит с машины и приходит на неё.

sudo tcpdump -n -i any host 10.2.14.7 and port 8080
# 12:04:11.201 IP 10.2.9.3.51422 > 10.2.14.7.8080: Flags [S], seq 812...
# 12:04:12.203 IP 10.2.9.3.51422 > 10.2.14.7.8080: Flags [S], seq 812...

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

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

Если непонятно, где именно теряются пакеты, помогает mtr: он показывает путь до адреса и потери на каждом переходе.

mtr -rw -c 20 10.2.14.7

Потери на промежуточном узле сами по себе ничего не значат — маршрутизаторы часто не отвечают на служебные пакеты, экономя силы. Значимы потери, которые начинаются на каком-то узле и держатся до самого конца.

Коротко

  • Идите по порядку: имя, порт, время запроса, соединения, трафик. Каждый следующий шаг дороже, и до последнего доходят редко.
  • Connection refused и таймаут — разные диагнозы: первое означает «никто не слушает», второе «пакеты не доходят».
  • curl -w раскладывает запрос на этапы и за секунду отвечает, кто виноват: сеть или приложение.
  • Приложение может держать свой кэш адресов, поэтому dig и реальное поведение сервиса иногда расходятся.
  • Растущий CLOSE-WAIT — не сетевая проблема, а незакрытые соединения в коде.
  • Слушающий адрес важен не меньше порта: 127.0.0.1 снаружи недоступен.

Что почитать дальше

  • IP-адреса и порты — откуда берутся эфемерные порты и чем грозит их исчерпание.
  • TCP и UDP — что стоит за состояниями соединений.
  • DNS — кэши на всех уровнях и почему адрес обновляется не сразу.
  • Соединения и keep-alive — как переиспользование соединений убирает половину проблем из этой статьи.