Инфраструктурные инциденты
Шесть проблем, которые выглядят как «сервис сломался», а на самом деле — инфраструктура или окружающая среда: 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
- TLS-сертификат истёк / не валидируется
- Расхождение времени (clock skew)
- Disk full
- File descriptor exhaustion
- Partial network partition
- Diagnostic toolkit
- Что НЕ делать
- Связанные разделы
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.confcacheслишком долго. HOSTALIASES/searchdomain прописан неправильно, сервис резолвит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.confndots: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_DIRenv.
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на всех нодах; alertabs(node_timex_offset_seconds) > 1на 5 минут → page infra. - В коде:
time.Now()использовать через inject’аемыйclock func() time.Time, чтобы тесты не зависели от реального time и чтобы в будущем можно было подменить на monotonic source.
Prevention:
- JWT валидация с
leeway: 60s; HMACX-User-Iat— с допуском ±120 секунд (см.../authentication-flow#валидация-на-backend). - Alert на node-exporter
node_timex_offset_seconds— на уровне инфры, не сервиса.
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):
- Найти, что жрёт.
du -xh / | sort -h | tail -20. - Неактивный replication slot (
active = f+ большой lag) — дропать нельзя без подтверждения, может быть нужен для standby. Если подтверждено, что slot orphan:SELECT pg_drop_replication_slot('<slot_name>'); - Kafka log segments — retention пересмотреть, временно уменьшить:
Kafka сам удалит старые segment’ы в следующем cleanup cycle.
kafka-configs.sh --bootstrap-server "$BROKERS" \ --entity-type topics --entity-name <topic> \ --alter --add-config retention.ms=86400000 - Логи в
/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:
- Alert
node_filesystem_avail_bytes < 10% of total→ page. - Autovacuum tuned per-table для hot-таблиц (см.
../conventions/data-retention#bloat-и-vacuum). - Kafka retention.ms установлен явно на каждом топике (см.
../conventions/events#retention). - CronJob outbox-cleanup работает (см.
../patterns/outbox#cleanup-и-retention).
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 на запрос. MaxConnspgx / redis <ulimit -npod’а; запас ≥ 30% на system FD (сокеты listener’а, лог-файлы).- Alert
process_open_fds / process_max_fds > 0.7→ ticket.
Prevention:
- Linter
bodycloseвgolangci-lintловитresp.Body.Close()misses на этапе CI. - goleak в tests (см.
../conventions/testing#goroutine-leak-detection).
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-meshNetworkChaos), убедиться, что 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, не ожидай, что «само пройдёт».
Связанные разделы
failure-modes-matrix— разбор, когда симптомы в нескольких runbook’ах одновременно.memory-leak— goroutine leak как причина FD exhaustion.db-slow-query— диск disk-full часто влияет на БД в первую очередь.kafka-consumer-stuck— retention не успевает при disk-full.redis-unavailable— net-connection проблемы у Redis-клиента.../conventions/graceful-degradation— контракт сервиса при деградации.../conventions/security#crypto— запрет наInsecureSkipVerifyкак дефолт.../patterns/outbox#cleanup-и-retention— cleanup для predictable disk usage.