Skip to Content
TroubleshootingИнфра: DNS, TLS, диск, FD, сеть

Инфраструктурные инциденты

Шесть проблем, которые выглядят как «сервис сломался», а на самом деле — инфраструктура или окружающая среда: DNS, TLS-сертификаты, расхождение времени, заполнение диска, исчерпание file descriptors, проблемы сети между pod’ами. Каждая — отдельный тип симптома и отдельный fix. Собирать их в один отдельный runbook важно: при инциденте часто первый вопрос — «это наш баг или снаружи?», и ответить быстрее, чем лезть в код сервиса.

Связанные runbook’и: failure-modes-matrix для многоsymptom-инцидентов; db-slow-query, kafka-consumer-stuck, redis-unavailable — для специфичных зависимостей. Общие правила деградации — ../conventions/graceful-degradation.

Содержание

DNS stale / resolution failure

Симптом. HTTP-клиент падает с no such host, dial tcp: lookup ...: i/o timeout. Может затронуть один endpoint (upstream сервис) или все (внешние зависимости). Критичный признак: отказы периодические (resolver TTL expiry), не постоянные.

Типичные причины:

  • CoreDNS / node-local DNS кэширует записи дольше TTL, старый IP больше не отвечает.
  • Glibc/musl резолвер сервиса держит записи в /etc/resolv.conf cache слишком долго.
  • HOSTALIASES / search domain прописан неправильно, сервис резолвит user.backend как user.backend.cluster.local и промахивается.
  • Миграция downstream-сервиса на новый IP (scale, restart) — Kubernetes Service обновляет endpoints, но кэш клиента ещё держит старые.

Diagnostika:

# текущая резолюция с pod'а kubectl exec -it <pod> -n backend -- nslookup user.backend kubectl exec -it <pod> -n backend -- dig +short user.backend # что видит CoreDNS kubectl logs -n kube-system -l k8s-app=kube-dns --tail 200 | grep <hostname> # DNS latency — отдельная метрика (если есть) # prometheus query: # histogram_quantile(0.99, rate(coredns_dns_request_duration_seconds_bucket[5m]))

В Go-клиенте — включить debug для net.DefaultResolver:

import "net" // во время диагностики, не в prod d := &net.Dialer{Resolver: net.DefaultResolver} conn, err := d.DialContext(ctx, "tcp", "user.backend:8001")

Fix immediate:

  • Рестарт pod’а сервиса — обнуляет in-process DNS cache. kubectl rollout restart deployment/<svc> -n backend.

  • Если затронуты все pod’ы ноды — рестарт node-local-dns daemon (infra, не backend).

  • Для single-endpoint проблемы — обновить connection pool через close idle:

    httpClient.CloseIdleConnections()

Fix permanent:

  • Убедиться, что HTTP-клиент не кэширует IP дольше нужного. net/http.Transport не резолвит DNS для каждого запроса — он держит connection pool per-host (строка до DNS). После смены IP нужно вызвать CloseIdleConnections() или дождаться IdleConnTimeout. Убедиться, что IdleConnTimeout ≤ 90 секунд (default).
  • Смотреть CoreDNS upstream TTL (инфра, не сервис).
  • Для внешних (публичных) hostname’ов — поднять resolv.conf ndots:2 (а не 5), чтобы не тратить запросы на <host>.<search-domain> комбинации.

Prevention:

  • Healthcheck (/readyz) делает реальный dial в downstream, не просто ping — если DNS сломан, ready переходит в 503 и pod снимает трафик.
  • Alert rate(http_client_errors_total{reason="dns"}[5m]) > 0.1 → ticket.

TLS-сертификат истёк / не валидируется

Симптом. HTTP-клиент падает с x509: certificate has expired or is not yet valid, x509: certificate signed by unknown authority. Обычно все запросы к одному downstream (или к внешнему API).

Типичные причины:

  • Сертификат downstream’а/gateway истёк, автообновление (cert-manager / Let’s Encrypt) не сработало.
  • Сертификат downstream’а ротировался, но выдан новым CA, которого нет в trusted store pod’а.
  • NotBefore сертификата в будущем — потому что у pod’а сломанный clock (см. следующий раздел).
  • Кастомный CA для internal-трафика не монтируется в /etc/ssl/certs/ / SSL_CERT_FILE сервиса.

Diagnostika:

# expire-date сертификата endpoint'а kubectl exec -it <pod> -n backend -- \ sh -c "echo | openssl s_client -connect user.backend:443 -servername user.backend 2>/dev/null \ | openssl x509 -noout -dates" # список trusted CA внутри pod'а kubectl exec -it <pod> -n backend -- ls -la /etc/ssl/certs/ | head # если используется cert-manager в кластере kubectl get certificates -A | grep -v True # → все сертификаты в status False = не выпущены/просрочены

Fix immediate:

  • Если сертификат объективно истёк — новый выпустить (cert-manager CronJob / infra-процедура). Пока не выпущен — временно поставить InsecureSkipVerify: true только в одном клиенте для одного downstream’а, с явным флагом в env:

    // ОЧЕНЬ ВРЕМЕННО: DRT-<ticket>, cert expired on downstream X if cfg.Downstream.X.InsecureSkipVerify { tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} }

    Удалить через тот же PR, которым выдан новый сертификат. На code- review — обязательно указать тикет-link.

  • Никогда не ставь InsecureSkipVerify: true как дефолт «чтобы работало». См. ../conventions/security#crypto.

Fix permanent:

  • Исправить cert-manager/renewal job; добавить alert cert_expires_in_days < 14 → page (чтобы не узнавать в день истечения).
  • Монтировать кастомный CA как ConfigMap в /etc/ssl/certs/ custom-ca.pem, указать в SSL_CERT_DIR env.

Prevention:

  • Alert probe_ssl_earliest_cert_expiry - time() < 14*86400 (via blackbox-exporter) на все internal и external endpoint’ы сервиса.
  • CI-тест на TLS-config клиентов: не допустить InsecureSkipVerify: true в main-branch.

Расхождение времени (clock skew)

Симптом. Разные форматы:

  • JWT iat is in the future / exp is in the past от сервера.
  • HMAC-signed headers (см. ../authentication-flow) валятся из-за X-User-Iat > now() на consumer-стороне.
  • Postgres COMMIT TIMESTAMP скачет назад, now() в разных транзакциях не монотонно.
  • TLS-сертификаты not yet valid с датой, которая уже прошла.
  • Kafka message.timestamp.difference.max.ms отклоняет сообщения.

Clock skew — частый источник загадочных «иногда работает, иногда нет» багов: на большинстве нод время корректно, на одной уехало.

Diagnostika:

# сравнить локальное время пода со временем на ноде kubectl exec -it <pod> -n backend -- date # ожидаемое расхождение < 1 секунды # chrony / ntpd status на ноде kubectl debug node/<node> -it --image=busybox -- chronyc tracking # System time : 0.000012345 seconds fast of NTP time

Если есть Prometheus node-exporter:

node_timex_offset_seconds{instance="..."} — offset от NTP-источника; устойчиво > 100ms = проблема.

Fix immediate:

  • Drain + reboot ноды с clock skew — быстрее, чем чинить chrony на живом хосте.
  • Сервис, который выпускает JWT (user-service), — не допускает skew ± 60 секунд в валидации (leeway в JWT library). Если скорректировали — короткий период, пока не починится.

Fix permanent:

  • Правильно настроенный chrony / ntpd на всех нодах; alert abs(node_timex_offset_seconds) > 1 на 5 минут → page infra.
  • В коде: time.Now() использовать через inject’аемый clock func() time.Time, чтобы тесты не зависели от реального time и чтобы в будущем можно было подменить на monotonic source.

Prevention:

Disk full

Симптом. Postgres отвечает No space left on device, Kafka logs начинают fail’ить при append, pod’ы с emptyDir volumes крашатся с write error. В Prometheus node_filesystem_avail_bytes < 1 GB.

Типичные причины:

  • Postgres WAL переполнился из-за долго работающего pg_dump / логической replication slot, который не ack’ается.
  • /var/log/* заполнился текстовыми логами (не docker JSON — этот ротируется).
  • Kafka log retention не успевает удалять segment files (retention.bytes / retention.ms не настроены).
  • Outbox-таблица разрослась без cleanup (см. ../patterns/outbox#cleanup-и-retention).
  • Тестовые данные на persistent volume накопились за недели (integration-test runs, CI-snapshot’ы).

Diagnostika:

# сколько свободно на ноде kubectl debug node/<node> -it --image=busybox -- df -h # на самом pod'е (для PVC) kubectl exec -it <pod> -n backend -- df -h # что жрёт место kubectl debug node/<node> -it --image=busybox -- du -xh /var/lib/docker | sort -h | tail # Postgres WAL usage SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir(); SELECT slot_name, active, restart_lsn, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) FROM pg_replication_slots;

Fix immediate (disk на ноде с Postgres primary):

  1. Найти, что жрёт. du -xh / | sort -h | tail -20.
  2. Неактивный replication slot (active = f + большой lag) — дропать нельзя без подтверждения, может быть нужен для standby. Если подтверждено, что slot orphan:
    SELECT pg_drop_replication_slot('<slot_name>');
  3. Kafka log segments — retention пересмотреть, временно уменьшить:
    kafka-configs.sh --bootstrap-server "$BROKERS" \ --entity-type topics --entity-name <topic> \ --alter --add-config retention.ms=86400000
    Kafka сам удалит старые segment’ы в следующем cleanup cycle.
  4. Логи в /var/log — truncate (не удаление файла, иначе write-handle процесса указывает в никуда):
    : > /var/log/some-huge.log

Fix immediate (pod с emptyDir):

  • Рестарт pod’а — emptyDir очищается. Это худший fix, но на stateless-pod’е допустим.
  • Для PVC — экстренно resize (если StorageClass позволяет): kubectl patch pvc ... -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'.

Fix permanent:

File descriptor exhaustion

Симптом. too many open files, accept: too many open files, dial tcp: socket: too many open files. Обычно после длительного uptime.

Типичные причины:

  • Goroutine-leak с открытыми HTTP-соединениями, которые не закрываются (см. memory-leak#goroutine-leak).
  • resp.Body.Close() пропущен — каждый незакрытый response-body держит как минимум один FD.
  • pgx / redis connection pool имеют MaxConns выше, чем FD-лимит pod’а.
  • Лог-rotation сломан, *.log.* растёт количеством файлов.
  • inotify watch стрим заморожен, FD держатся годами.

Diagnostika:

# сколько FD у процесса kubectl exec -it <pod> -n backend -- sh -c 'ls /proc/1/fd | wc -l' # лимит kubectl exec -it <pod> -n backend -- cat /proc/1/limits | grep files # по типу FD kubectl exec -it <pod> -n backend -- sh -c 'ls -l /proc/1/fd | awk "{print \$NF}" | sort | uniq -c | sort -rn | head' # socket:[xxx] — сокеты, pipe:[xxx] — пайпы, /path — файлы

Для Go-процесса — pprof:

# если /debug/pprof доступен в pod'е curl -s http://$POD:6060/debug/pprof/goroutine?debug=2 | grep -c 'net/http.(\*persistConn).readLoop' # большое число persistConn'ов = leak HTTP-keep-alive'ов

Fix immediate:

  • Рестарт pod’а — освобождает FD.
  • Если рестарт повторяется (например, FD растут каждый час) — нужен root cause, не рестарт-loop.

Fix permanent:

  • Найти leak через pprof:
    curl -o goroutine.pprof http://$POD:6060/debug/pprof/goroutine go tool pprof -http=:8080 goroutine.pprof # смотреть, какие goroutine'ы доминируют; обычно handler без # resp.Body.Close(), consumer без Close() на subscriber.
  • Код-правило: resp.Body.Close() — всегда через defer, сразу после проверки error на запрос.
  • MaxConns pgx / redis < ulimit -n pod’а; запас ≥ 30% на system FD (сокеты listener’а, лог-файлы).
  • Alert process_open_fds / process_max_fds > 0.7 → ticket.

Prevention:

Partial network partition

Симптом. Один pod сервиса видит downstream, другой pod того же сервиса — нет. Или hit-rate балансера скачет; логи показывают connection refused только на части реплик.

Это — не полный outage. Complete outage диагностируется тривиально; partial — сложнее, потому что service “частично жив”, health-check на уровне балансера зелёный.

Типичные причины:

  • Kubernetes NetworkPolicy добавлена с ошибкой, блокирует связь между подмножеством namespaces.
  • CNI-плагин (Calico / Cilium) имеет rule update, который пропал на одной ноде.
  • Security group / firewall в облаке закрыл port между подсетями.
  • IPv4/IPv6 dual-stack несимметрично настроен.
  • MTU mismatch между нодами (сегментация packets).

Diagnostika:

# с двух разных pod'ов сервиса — одинаковый curl kubectl exec -it <pod-A> -n backend -- curl -sv http://user.backend:8001/healthz kubectl exec -it <pod-B> -n backend -- curl -sv http://user.backend:8001/healthz # какие NetworkPolicy применимы kubectl get networkpolicy -A | grep -v "<none>" # связность между всеми парами нод # (для этого нужен nodeport test-pod на каждой ноде; в большинстве кластеров # есть troubleshoot-podsetting)

Fix immediate:

  • Если причина — NetworkPolicy, kubectl delete networkpolicy <name> (временно, с немедленным follow-up’ом на правильную).
  • Если CNI — рестарт CNI-daemon’а на затронутой ноде (infra- процедура).

Fix permanent:

  • NetworkPolicy тестировать через kube-bench или netassert перед merge в infra-репо.
  • Alert probe_success{job="blackbox", target=~"user.backend:.+"} == 0 на кросс-pod тесты в кластере — за пределами одного namespace.

Prevention:

  • Regular chaos drill: drop-packet между случайной парой подов (через chaos-mesh NetworkChaos), убедиться, что graceful degradation срабатывает.

Diagnostic toolkit

Команды, которые стоит знать наизусть при любом инфра-инциденте:

# pod lifecycle kubectl get pods -n backend -o wide kubectl describe pod <pod> -n backend | tail -40 kubectl logs <pod> -n backend --previous --tail 200 # node state kubectl get nodes -o wide kubectl describe node <node> | grep -E "Taints|Conditions|Capacity|Allocated" kubectl top nodes # services / endpoints kubectl get svc,endpoints -n backend kubectl describe endpoints <svc> -n backend # DNS kubectl exec -it <pod> -n backend -- nslookup <hostname> # TLS kubectl exec -it <pod> -n backend -- sh -c \ 'echo | openssl s_client -connect <host>:<port> -servername <host> 2>/dev/null | openssl x509 -noout -dates -subject -issuer' # disk / FD kubectl exec -it <pod> -n backend -- df -h kubectl exec -it <pod> -n backend -- ls /proc/1/fd | wc -l # Postgres kubectl exec -it <pg-pod> -n db -- psql -U postgres -c \ "SELECT pid, usename, state, wait_event_type, wait_event, query_start, query FROM pg_stat_activity WHERE state != 'idle' ORDER BY query_start;" # Kafka kubectl exec -it <kafka-pod> -n messaging -- kafka-consumer-groups.sh \ --bootstrap-server localhost:9092 --describe --group <group>

Эти команды одинаковые при любой проблеме — поэтому живут в одном месте, а не дублируются в каждом runbook’е.

Что НЕ делать

  • Не рестартовать pod/node «для пробы», не зафиксировав состояние. Рестарт удалит улики (открытые FD, goroutine dumps, in-memory state). Сначала snapshot: kubectl exec, pprof, kubectl cp логов.
  • Не отключать NetworkPolicy «чтобы работало» без открытого тикета с root cause. Каждая такая «временная» правка остаётся навсегда и создаёт security-дыру.
  • Не менять InsecureSkipVerify: true без ticket-link’а в коммит-сообщении и следующего PR’а на откат. См. предупреждение в §TLS.
  • Не править настройки chronyd/ntpd руками в runtime. Конфигурация — в Ansible / Terraform / cloud-init, не vim /etc/chrony.conf.
  • Не удалять replication slot без подтверждения, что standby его не использует — потеряешь WAL и standby станет неконсистентным.
  • Не rm -rf в /var/log/*.log на работающем процессе — FD останутся открытыми на unlinked file, диск не освободится до рестарта процесса. Truncate: : > /var/log/x.log.
  • Не полагаться на «кратковременный DNS glitch», если повторяется — это симптом, не случайность. Глянь CoreDNS / node- local DNS, не ожидай, что «само пройдёт».

Связанные разделы

Last updated on