Перейти к содержимому

Production Debugging и Chaos Engineering

Зачем знать на Middle 3: Senior Go-инженер должен уметь дебажить production, не повторяя проблему в go test. Это значит: читать pprof с живого сервиса, ловить goroutine leaks через goroutine?debug=1, делать dlv attach, читать core dumps, использовать eBPF (Pixie, Parca, bpftrace) для kernel-level observability, и систематически проводить chaos experiments (Chaos Mesh, LitmusChaos, Toxiproxy), чтобы baby-systems становились бойцами. Это — пограничный stack между разработкой и SRE; именно тут вырастают «лидирующие» инженеры, способные диагностировать тёмные углы distributed systems.

  1. Концепция: production debugging vs chaos
  2. Production-deep dive: pprof, dlv, core dumps, eBPF, chaos tools
  3. Gotchas (10+)
  4. Real cases: Netflix, Discord, реальные инциденты
  5. Вопросы (25+)
  6. Practice
  7. Источники

АспектLocal / тестыProduction
ReproducibilityHopefully alwaysЧасто one-of
Pause-ableДа (debugger)Нет (downtime!)
ToolsIDE, Delvepprof, dlv attach, eBPF, profiles
LogsStdoutCentralized, structured
StateМожно сброситьCritical, не трогать
LatencyLowReal, network, contention

Главное правило production debug: наблюдай, не вмешивайся (без необходимости).

┌──────────────────────┐
│ Observe symptom │ (alert, customer report)
└─────────┬────────────┘
┌──────────────────────┐
│ Form hypothesis │ "P99 latency растёт из-за GC"
└─────────┬────────────┘
┌──────────────────────┐
│ Measure │ pprof heap, GC trace, metrics
└─────────┬────────────┘
┌────┴────┐
▼ ▼
Confirmed Refuted
│ │
▼ ▼
Fix New hypothesis

Chaos engineering — практика активного введения отказов в систему, чтобы:

  1. Обнаружить уязвимости до того, как они проявятся в проде.
  2. Построить уверенность в resilience.
  3. Тренировать команду в incident response.

Принципы (от Netflix Principles of Chaos Engineering):

  1. Сформулировать гипотезу о стабильном поведении.
  2. Варьировать real-world события.
  3. Запускать в production (на mature стадии).
  4. Автоматизировать эксперименты.
  5. Минимизировать blast radius.

net/http/pprof — стандартная библиотека, нужно один раз импортировать:

import _ "net/http/pprof"
func main() {
go func() {
// отдельный mux на admin-порт, не выставляйте в Internet!
http.ListenAndServe("127.0.0.1:6060", nil)
}()
// ...
}

Доступные endpoints:

/debug/pprof/ index
/debug/pprof/profile?seconds=30 CPU profile (sampling 30s)
/debug/pprof/heap heap snapshot (allocs + inuse)
/debug/pprof/allocs allocs since start
/debug/pprof/goroutine goroutine stack snapshot
/debug/pprof/goroutine?debug=2 full text dump
/debug/pprof/block blocking profile
/debug/pprof/mutex mutex contention
/debug/pprof/threadcreate OS thread creation
/debug/pprof/cmdline command line args
/debug/pprof/symbol symbolization
/debug/pprof/trace?seconds=10 execution trace
Окно терминала
kubectl port-forward pod/checkout-7d8f-xyz 6060:6060
curl -o cpu.pprof 'http://127.0.0.1:6060/debug/pprof/profile?seconds=30'
go tool pprof -http=:8080 cpu.pprof

В UI смотрим flame graph, top functions.

Окно терминала
curl -o heap-before.pprof http://127.0.0.1:6060/debug/pprof/heap
sleep 600 # 10 минут под нагрузкой
curl -o heap-after.pprof http://127.0.0.1:6060/debug/pprof/heap
go tool pprof -http=:8080 -base heap-before.pprof heap-after.pprof

-base показывает разницу между двумя heap’ами — что выросло. Так находят leak’ы быстрее всего.

Окно терминала
curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=1
# выводит группы goroutines, помечая, сколько штук в каждом стеке

Если видите 10000 goroutines в одной и той же stack frame chan recv — это leak.

Окно терминала
runtime.SetMutexProfileFraction(5) // в коде, sample 1/5 событий
curl -o mutex.pprof http://127.0.0.1:6060/debug/pprof/mutex
go tool pprof mutex.pprof

В 2024+ появились continuous profiling backends:

  • Grafana Pyroscope (open-source) — собирает pprof с тысяч сервисов, показывает flame graphs over time.
  • Parca — eBPF-based, low-overhead.
  • Polar Signals Cloud / Datadog Continuous Profiler — commercial.

В Go клиент — pyroscope-io/client-golang:

profiler.Start(profiler.Config{
ApplicationName: "checkout-api",
ServerAddress: "http://pyroscope:4040",
ProfileTypes: []profiler.ProfileType{
profiler.ProfileCPU,
profiler.ProfileAllocObjects, profiler.ProfileAllocSpace,
profiler.ProfileInuseObjects, profiler.ProfileInuseSpace,
profiler.ProfileGoroutines,
},
})

С 2024 года Pyroscope мерджится в Grafana stack.

Окно терминала
# В Pod'е (privileged + SYS_PTRACE)
dlv attach $(pgrep myapp)
(dlv) goroutines
(dlv) goroutine 42
(dlv) bt # backtrace
(dlv) locals
(dlv) p variable
(dlv) print mutex.state
(dlv) detach

⚠️ Critical: dlv attach ставит процесс на паузу. На production-сервисе это вызовет downtime. Делать только когда сервис уже degradation, или на одной replica из десяти.

В Kubernetes:

securityContext:
capabilities:
add: ["SYS_PTRACE"]

или (хуже) privileged: true.

Окно терминала
# Включаем core dumps
ulimit -c unlimited
echo "/var/cores/core.%e.%p" > /proc/sys/kernel/core_pattern
# Go программу — с GOTRACEBACK=crash чтобы упасть на panic с дампом
GOTRACEBACK=crash ./myapp
# Анализ
dlv core ./myapp /var/cores/core.myapp.12345
(dlv) goroutines
(dlv) goroutine 7
(dlv) bt

GOTRACEBACK значения:

  • none — только panic line.
  • single (default) — только goroutine упавшая.
  • all — все goroutines.
  • system — runtime goroutines тоже.
  • crash — все + abort signal (для дампа).

В Kubernetes core dumps часто хранят в volume или uploadat в S3 для последующего analysis.

Если pprof не доступен (нет HTTP-сервера), можно динамически:

func dumpStack() {
buf := make([]byte, 1<<20)
n := runtime.Stack(buf, true) // all goroutines
log.Printf("STACK DUMP:\n%s", buf[:n])
}
// или signal handler
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGUSR1)
go func() {
for range sig {
dumpStack()
}
}()

kill -SIGUSR1 PID — и получаешь stack dump в логи.

eBPF (extended Berkeley Packet Filter) — программируемый kernel hook system. Запускается в kernel-space с safety guarantees (verifier). Используется для:

  • Network observability (Cilium, Pixie).
  • Performance profiling (Parca, Pyroscope).
  • Security (Falco).
  • Tracing system calls (bpftrace, BCC tools).

Преимущества: low overhead, kernel-level visibility, без instrumentation кода.

Окно терминала
# CPU off-time (where threads block)
offcputime -p $(pgrep myapp) 30 > offcpu.txt
# Run queue latency
runqlat 5
# TCP connections
tcpconnect

Pixie — auto-instrumentation через eBPF. Поднимаете в кластере, и без изменений в коде получаете:

  • HTTP request tracing.
  • DB query analysis.
  • Service maps.
  • Flame graphs.

В 2026 году — must для k8s production.

В микросервисах debug одного сервиса недостаточно — request проходит через 10 систем. Pattern:

  1. Alert на симптом (e.g., checkout-api 5xx > 5%).
  2. Перейти на dashboard (Grafana) → видим spike P99.
  3. Из metric exemplar → trace_id.
  4. В trace (Tempo/Jaeger) видим span с ошибкой → service B.
  5. По trace_id ищем логи (Loki, Elasticsearch) → видим stack trace.
  6. По stack trace + recent deploys → root cause.

Этот flow называется observability triangle: metrics → traces → logs, связанные через trace_id.

Симптом: heap растёт монотонно, OOMKilled. Подход:

  1. Heap diff (2 snapshots).
  2. Top inuse_space objects.
  3. Look for unbounded slices / maps / caches without expiry.

Типичные причины в Go: []byte буферы не возвращаются в sync.Pool, goroutines с локальным state, забытые http.Response.Body.Close().

Симптом: число goroutines растёт. goroutine?debug=1 показывает группу:

runtime.gopark
3429 @ 0x1.../runtime/sema.go:...
# sync.runtime_Semacquire
# sync.(*WaitGroup).Wait
# myapp/worker.process

3429 goroutines stuck на WaitGroup.Wait. Где-то нет wg.Done().

Частые причины:

  • go func() { <-ch }() где ch никогда не пишется.
  • Forgotten cancel() для context.
  • HTTP request без Response.Body.Close().
  • Channels без consumer’а после producer завершился.

P50 = 20ms, P99 = 5s — кто-то страдает. Причины:

  • GC pauses (>100ms на больших heap’ах).
  • Lock contention.
  • Slow DB queries (uneven data).
  • Stop-the-world операции (snapshot, sync.Map deletion).
  • Network jitter.

Tools: runtime.ReadMemStats, GODEBUG=gctrace=1, Tempo для slowest traces.

Симптом: CPU = 100% на pod. Причины:

  • Tight loop (без time.Sleep / <-ctx.Done()).
  • JSON marshalling на каждый request (нет cache).
  • Regex compile в hot path.
  • Reflection (encoding/json) на больших структурах.

Tool: 30s CPU profile, look at flame graph.

Симптом: too many connections errors, services degrade. Причины:

  • db.SetMaxOpenConns слишком высокий, БД упирается.
  • Transaction leak (open but не closed → connection занят).
  • Long-running query держит connection.

Tools: db.Stats()OpenConnections, InUse, WaitDuration. Логи БД.

Service A → B → C. C тормозит. B retries, A retries. Suddenly все умерли.

Решения: circuit breakers, timeouts, exponential backoff, retry budgets.

Go runtime детектирует всем заблокированных (fatal error: all goroutines are asleep - deadlock!). Локальные дедлоки — не детектируются. Tools: mutex profile, blocking profile, eBPF.

Chaos Mesh — CNCF Kubernetes-native chaos. CRDs для типов:

apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: latency-injection
namespace: production
spec:
action: delay
mode: random-max-percent
value: "10" # 10% подов
selector:
namespaces: [production]
labelSelectors:
app: checkout
delay:
latency: "100ms"
correlation: "100"
jitter: "10ms"
duration: "5m"

Типы:

  • NetworkChaos — latency, packet loss, partition, corrupt, duplicate.
  • PodChaos — kill, failure (pod stays running but doesn’t respond), container-kill.
  • StressChaos — CPU/memory exhaustion.
  • IOChaos — slow disk, errors.
  • TimeChaos — clock skew.
  • DNSChaos — DNS resolution errors.
  • HTTPChaos — modify/abort HTTP requests.

LitmusChaos — другой CNCF проект. ChaosHub с готовыми experiments.

Toxiproxy (Shopify) — TCP proxy с инжекцией fault’ов. Между сервисом и зависимостью (БД, Redis). Поддерживает: latency, bandwidth limit, slow_close, timeout, slicer, reset_peer.

toxic := toxiproxy.Toxic{
Name: "latency",
Type: "latency",
Attributes: map[string]interface{}{
"latency": 1000, // ms
"jitter": 100,
},
}
proxy.AddToxic(toxic)

Useful для unit/integration tests с реалистичной latency.

Chaos Toolkit — open-source library для chaos engineering, agnostic от платформы.

Game day — запланированный chaos exercise:

  1. Pre-day: hypothesize (“если убить primary postgres, failover < 30s”).
  2. Day: участвует команда, injects fault, никто не знает заранее.
  3. Measure: SLO impact, RTO, RPO.
  4. Postmortem: что сработало, что нет, action items.

Netflix делает game days еженедельно. У вас должно быть минимум раз в квартал.

«Когда сломалось?» → бинарный поиск по deploys:

deploy v1.4.0 - 11:00 - OK
deploy v1.4.1 - 12:00 - ?
deploy v1.4.2 - 14:00 - BROKEN

Rollback на v1.4.1 — если OK, проблема в v1.4.2. Diff v1.4.1..v1.4.2 — look for suspects.

Pod-A работает медленно, Pod-B — нормально. Что отличается? Node, traffic, version, recent restart? — сужает.

Перечислите все гипотезы, оцените P(гипотеза верна) × cost(test). Тестируйте самую дешёвую сначала.

Можно сэкономить часы дебага в момент инцидента, если заранее заложить:

  1. Структурированные логи с trace_id (см. файл 35).
  2. Debug endpoints на admin-port:
    • /debug/pprof/*
    • /debug/vars (expvar) или /metrics (Prometheus)
    • /healthz, /readyz
    • /-/config (читать current config)
    • /-/flags (читать feature flags)
    • /debug/stack (custom, выводит runtime.Stack(...))
  3. Feature flags — выключить дефектный путь без deploy.
  4. Graceful degradation hooks — fallback при недоступности зависимости.
  5. Request ID в каждом HTTP-ответе (header X-Request-ID).
  6. Audit log для critical operations (приватная история изменений).

Пример debug HTTP-эндпоинта:

mux.HandleFunc("/debug/stack", func(w http.ResponseWriter, r *http.Request) {
buf := make([]byte, 1<<20)
n := runtime.Stack(buf, true)
w.Header().Set("Content-Type", "text/plain")
w.Write(buf[:n])
})
mux.HandleFunc("/debug/heap", func(w http.ResponseWriter, r *http.Request) {
runtime.GC()
pprof.Lookup("heap").WriteTo(w, 0)
})
mux.HandleFunc("/debug/flags", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(featureFlags.Snapshot())
})

Chaos не только в проде — можно интегрировать в CI:

.github/workflows/chaos-test.yaml
jobs:
chaos-test:
runs-on: ubuntu-latest
steps:
- uses: helm/kind-action@v1
- run: helm install chaos-mesh chaos-mesh/chaos-mesh -n chaos-testing
- run: make deploy-app
- run: |
kubectl apply -f chaos/network-latency.yaml
go test ./e2e/...
- run: kubectl delete -f chaos/network-latency.yaml

Такой test-под-нагрузкой ловит регрессии до прода. Сценарии: latency injection, packet loss, partial network partition.

Production debugging — это реактив; capacity planning — превентив:

  • Headroom: рекомендация Google — нагружать до 70% capacity, остальное — резерв.
  • Load shedding: при перегрузке отвергать запросы early (HTTP 503), не пытаясь обработать.
  • Backpressure: signal upstream’у замедлиться.
  • Forecasting: использовать Holt-Winters / ARIMA для прогноза нагрузки на 30 дней.

Метрики, за которыми смотрит SRE:

  • Утилизация CPU/memory/network/disk.
  • Queue depth (Redis, Kafka, RabbitMQ).
  • DB connection pool inuse / max.
  • HTTP keep-alive connections.
  • Goroutine count (Go) / thread count (других языков).

Хорошие SRE команды читают postmortems других компаний:

Чтение этих postmortems — must для Middle 3+ инженера. Лучше учиться на чужих ошибках.


net/http/pprof регистрирует handlers на http.DefaultServeMux. Если ваш API использует тот же mux, /debug/pprof/* доступно публично. Heap profile содержит переменные — утечка PII / secrets. Всегда регистрируйте pprof на отдельный internal-only port.

Pause на 30 секунд = 30 секунд clients ждут. На single-replica deployment = downtime. Drain pod из service сначала.

Heap profile показывает inuse (живые объекты). Чтобы видеть все аллокации с момента старта — /debug/pprof/allocs. Confusing для дебага «откуда столько GC».

runtime.Stack(buf, true) сериализует все goroutines. Если их миллион — буфер взрывается, stop-the-world на секунды. Используйте только при подозрении на leak.

goroutine?debug=1 показывает snapshot. Goroutines на коротких channel’ах могут выглядеть «стуком» в момент snapshot’а. Снимайте 2 snapshot’а с интервалом 1 минута, ищите рост.

Запуск Chaos Mesh experiment без mode: fixed-percent и без namespaces фильтра — выкосит весь кластер. Всегда задавайте scope, начинайте с одной replica.

Killing primary postgres — это не «pretty chaos», это реальный outage. Делайте только в staging, либо если у вас протестированная HA.

Toxiproxy listens между client и upstream. Если порты конфликтуют (нет separate netns) — service вообще не может стартовать. Test setup критичен.

eBPF требует CAP_BPF или privileged. На multi-tenant кластере (shared infra) это security risk. Используйте eBPF tools только на dedicated nodes / sidecar c review.

Core dump — это полная память процесса. Любой secret, env var, password в памяти — в дампе. Хранение dump’ов как обычных файлов = утечка. Шифруйте + ограничьте access.

Pyroscope/Parca добавляют ~1-3% CPU overhead. Для CPU-bound workloads — заметно. Тюньте sample rate.

gctrace=1 пишет на каждый GC cycle в stderr — megabytes логов в час. Используйте только во время investigation, выключайте после.


Знаменитая суит chaos tools от Netflix:

  • Chaos Monkey — рандомно убивает instances.
  • Chaos Gorilla — убивает entire AZ.
  • Chaos Kong — убивает entire region.
  • Latency Monkey — injects latency.
  • Conformity Monkey — finds instances violating best practices.

Netflix запускает Chaos Monkey в production еженедельно. Любой engineer должен предполагать, что его сервис будет убит в случайный момент.

Discord проводит «chaos game days» quarterly. Симулируют отказы regions, БД, очередей. После — публичные blog posts с lessons learned.

Stripe — известны debugging methodology. Public blog posts о реальных production debug-сессиях (memory leak в Ruby/Go runtime, kernel bugs).

Известный bug в Prometheus 1.x: HTTP client без Body.Close() оставлял goroutines. После 12 часов работы — миллионы. OOMKilled. Fix — добавили defer close. Урок: всегда defer Close.

Кластер: P99 latency 5s, P50 50ms. Investigation:

  1. CPU profile — DNS lookups доминируют.
  2. coredns logs — slow upstream.
  3. Fix: tune ndots: 1 в dnsConfig, чтобы не делать N лookups для FQDN. Tail latency упал до 200ms.

  1. Какие pprof endpoints вы знаете?
  2. Чем /debug/pprof/heap отличается от /debug/pprof/allocs?
  3. Как найти memory leak через heap diff?
  4. Как найти goroutine leak?
  5. Что такое runtime.SetMutexProfileFraction?
  6. Зачем нужны block и mutex профили?
  7. Как сделать execution trace и для чего он нужен?
  8. Что такое continuous profiling и какие backends его реализуют?
  9. Что такое Pyroscope?
  10. Как работает dlv attach и какие риски в проде?
  11. Что такое core dump и как его собрать в Go?
  12. Что такое GOTRACEBACK и какие у него значения?
  13. Как использовать runtime.Stack + signal для on-demand dump?
  14. Что такое eBPF и где используется?
  15. Что такое Pixie и какие проблемы он решает?
  16. Что такое BCC tools (offcputime, runqlat)?
  17. Что такое observability triangle (metrics → traces → logs)?
  18. Как ищется bug в distributed системе через trace_id?
  19. Какие типичные причины memory leak в Go?
  20. Какие типичные причины goroutine leak?
  21. Что такое tail latency и какие у неё причины?
  22. Что такое cascading failure и как его избежать?
  23. Что такое chaos engineering и кто его придумал?
  24. Какие принципы chaos engineering?
  25. Что такое Chaos Mesh и какие у него CRDs?
  26. Чем LitmusChaos отличается от Chaos Mesh?
  27. Что такое Toxiproxy и зачем он нужен?
  28. Что такое game day?
  29. Как делать chaos experiments в production безопасно?
  30. Как работать с deploy bisect?

  1. pprof basics. Добавьте net/http/pprof в свой сервис, поднимите нагрузку, соберите CPU/heap profiles, посмотрите в pprof UI.
  2. Memory leak. Сделайте сервис с искусственным leak’ом (var cache = map[string][]byte{}, добавляйте без удаления), найдите через heap diff.
  3. Goroutine leak. Запустите goroutine с <-ch без писателя. Подтвердите через goroutine?debug=1.
  4. Mutex contention. Сделайте hot mutex (один на 100 goroutines), включите mutex profile, посмотрите.
  5. dlv attach. Запустите Go-приложение, прицепитесь через dlv attach, выведите goroutines.
  6. Core dump. Сделайте panic с GOTRACEBACK=crash, проанализируйте core dump через dlv.
  7. Pyroscope. Установите Pyroscope, подключите Go-клиент, посмотрите continuous profile в UI.
  8. Chaos Mesh. Установите в kind, сделайте NetworkChaos с latency 200ms, проверьте, как реагирует ваш сервис.
  9. Toxiproxy. Поставьте между service и postgres, добавьте latency toxic, проверьте retry/timeout behavior.
  10. Game day. Запланируйте 1-часовой game day: симулируйте отказ Redis, измерьте impact.

  1. net/http/pprof docs.
  2. Profiling Go Programs (go.dev) — classic blog post.
  3. Delve docs.
  4. Grafana Pyroscope.
  5. Parca — eBPF profiling.
  6. Pixie.
  7. Cilium eBPF book.
  8. Brendan Gregg’s eBPF page — guru.
  9. BCC tools.
  10. Chaos Mesh docs.
  11. LitmusChaos.
  12. Toxiproxy.
  13. Principles of Chaos Engineering.
  14. “Chaos Engineering” (Casey Rosenthal, Nora Jones) — O’Reilly.
  15. Netflix Tech Blog — chaos и SRE posts.
  16. Bryan Cantrill: debugging under fire — классические доклады.