Performance Debugging Guide
1. The USE Method
For every resource: Utilization, Saturation, Errors.
| Resource | Utilization | Saturation | Errors |
|---|---|---|---|
| CPU | mpstat, top %cpu |
run-queue (vmstat r) |
machine-check errors |
| Memory | free -m used/total |
swap-in rate (vmstat si/so) |
OOM kills (dmesg) |
| Disk I/O | iostat %util |
await / queue depth | iostat err fields |
| Network | sar -n DEV %ifutil |
drops (netstat -s) |
ip -s link errors |
| File Descriptors | open/limit ratio | blocked on fd alloc | EMFILE errors |
| Goroutines (Go) | active / total | blocked goroutines | panics / timeouts |
| Thread Pool | active threads | pending work queue | rejected tasks |
| DB Conn Pool | active / pool size | waiting-for-conn time | conn refused errors |
Utilization = busy_time / total_time (aim < 70% sustained)
Saturation = queue length or wait time (any > 0 is a signal)
Errors = error events per second
graph TD
RES[Resources] --> CPU[CPU]
RES --> MEM[Memory]
RES --> DISK["Disk I/O"]
RES --> NET[Network]
RES --> FD[File Descriptors]
RES --> APP[App Resources]
APP --> POOL[Connection Pool]
APP --> GC["GC / Heap"]
APP --> GR[Goroutines]
CPU --> U1["Utilization:<br/>mpstat %cpu"]
CPU --> S1["Saturation:<br/>run queue vmstat r"]
CPU --> E1["Errors:<br/>MCE dmesg"]
MEM --> U2["Utilization:<br/>free -m used/total"]
MEM --> S2["Saturation:<br/>vmstat si/so swap"]
MEM --> E2["Errors:<br/>OOM kills dmesg"]
2. The RED Method
For every service endpoint: Rate, Errors, Duration.
| Signal | Meaning | Prometheus Metric |
|---|---|---|
| Rate | requests per second | rate(http_requests_total[5m]) |
| Errors | error rate (4xx/5xx) | rate(http_requests_total{code=~"5.."}[5m]) |
| Duration | latency percentiles | histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) |
Implementing RED in Prometheus (Go)
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
reqTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
}, []string{"method", "path", "status"})
reqDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Buckets: prometheus.DefBuckets,
}, []string{"method", "path"})
)
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r)
dur := time.Since(start).Seconds()
reqTotal.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(rw.status)).Inc()
reqDuration.WithLabelValues(r.Method, r.URL.Path).Observe(dur)
})
}
Prometheus alert rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
for: 2m
- alert: HighLatencyP99
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 1.0
for: 5m
3. Brendan Gregg 60-Second Linux Checklist
Run these in order. Each takes ~1 second. Total: ~60s.
uptime
load average: 1.68, 0.75, 0.39
- Look for: load average trend. Rising = saturation. Values > CPU count = run-queue saturation.
dmesg | tail -20
- Look for: OOM kills (
Out of memory: Kill process), disk errors (I/O error), NIC resets, kernel panics.
vmstat 1
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 182932 13340 504936 0 0 0 208 1265 2059 24 5 71 0 0
r= run queue (> CPU count = CPU saturation)si/so= swap in/out (any > 0 = memory pressure)us/sy= user/kernel CPU timewa= I/O wait (> 5% = disk bottleneck)
mpstat -P ALL 1
- Look for: single CPU at 100% (single-threaded bottleneck), imbalanced CPU usage.
pidstat 1
- Look for: which processes consume CPU.
%wait= off-CPU blocked time.
iostat -xz 1
Device r/s w/s rkB/s wkB/s await %util
sda 0.0 8.0 0.0 64.0 2.5 1.2
%util> 60% = disk saturationawait= avg I/O latency (ms). > 10ms is notable.r_awaitvsw_await= separate read/write latency.
free -m
total used free shared buff/cache available
Mem: 7982 4516 182 312 3283 2892
- Look for:
availablenear 0 = memory pressure.swap used> 0 = swapping.
sar -n DEV 1
- Look for:
%ifutilon network interfaces. Packets/s near NIC limit.
sar -n TCP,ETCP 1
active/s passive/s iseg/s oseg/s | atmptf/s estres/s retrans/s
retrans/s> 0 = network congestion or packet loss.atmptf/s= failed TCP connection attempts.
top
- Look for: Overall CPU summary, top processes by CPU and MEM.
wa%for I/O wait. Press1for per-CPU view.
4. Go pprof Profiles
Profile Types
| Profile | What it measures | When to use |
|---|---|---|
cpu |
on-CPU time (sampled at 100Hz) | CPU bottleneck |
heap |
live heap allocations | memory leak, GC pressure |
goroutine |
goroutine stack traces | goroutine leak |
mutex |
mutex contention time | lock bottleneck |
block |
blocking on chan/mutex/syscall | concurrency bottleneck |
trace |
full execution trace | scheduler, GC pauses |
graph LR
PPROF[pprof Profiles]
PPROF --> CPU["cpu<br/>on-CPU sampled"]
PPROF --> HEAP["heap<br/>live allocations"]
PPROF --> GR["goroutine<br/>stack traces"]
PPROF --> MUX["mutex<br/>lock contention"]
PPROF --> BLK["block<br/>chan/mutex wait"]
PPROF --> TRC["trace<br/>full execution"]
CPU --> CPU2[CPU bottleneck]
HEAP --> HEAP2["memory leak / GC"]
GR --> GR2[goroutine leak]
MUX --> MUX2[lock contention]
BLK --> BLK2[concurrency wait]
TRC --> TRC2["scheduler / GC pauses"]
Capturing Profiles
Enable HTTP endpoint (add to main):
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
CPU profile (30 seconds):
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# or save to file:
curl -o cpu.prof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof cpu.prof
Heap profile:
go tool pprof http://localhost:6060/debug/pprof/heap
# inuse_space (default): currently live objects
# alloc_space: all allocations (use -alloc_space flag)
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
Goroutine profile:
go tool pprof http://localhost:6060/debug/pprof/goroutine
# raw stack dump (human readable):
curl http://localhost:6060/debug/pprof/goroutine?debug=2
Mutex / Block profiles (must enable first):
runtime.SetMutexProfileFraction(1) // sample every mutex event
runtime.SetBlockProfileRate(1) // sample every block event (expensive!)
go tool pprof http://localhost:6060/debug/pprof/mutex
go tool pprof http://localhost:6060/debug/pprof/block
Execution trace:
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.out
In-process (benchmark / test):
// In a test:
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Heap snapshot:
f, _ := os.Create("heap.prof")
pprof.WriteHeapProfile(f)
Reading a Flame Graph
go tool pprof -http=:8080 cpu.prof
# Opens browser with flame graph at /ui/flamegraph
- X-axis: alphabetical order (NOT time). Width = % of total samples.
- Y-axis: call stack depth. Top frame = leaf (where CPU is spent).
- Wide boxes at top: hot functions — investigate these first.
runtime.mallocgcwide = allocation pressure.runtime.gcBgMarkWorkerwide = GC overhead.- Use
top10in pprof CLI to see cumulative vs flat time. flat= time in function itself.cum= time including callees.
5. bpftrace One-Liners
# Syscall latency by syscall name (microseconds)
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; }
tracepoint:raw_syscalls:sys_exit /@start[tid]/ {
@latency_us[probe] = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'
# Disk I/O latency histogram (microseconds)
bpftrace -e 'tracepoint:block:block_rq_issue { @start[args->sector] = nsecs; }
tracepoint:block:block_rq_complete /@start[args->sector]/ {
@disk_lat_us = hist((nsecs - @start[args->sector]) / 1000);
delete(@start[args->sector]); }'
# TCP retransmits with source/dest
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
printf("%s -> %s<br>", ntop(args->saddr), ntop(args->daddr)); }'
# Off-CPU time (blocked time) by stack
bpftrace -e 'tracepoint:sched:sched_switch /prev->state/ {
@start[prev->pid] = nsecs; }
tracepoint:sched:sched_switch /@start[next->pid]/ {
@offcpu_us[kstack] = hist((nsecs - @start[next->pid]) / 1000);
delete(@start[next->pid]); }'
6. Application-Level Debugging
Connection Pool Saturation
// Expose pool stats as Prometheus gauges
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
// Monitor with:
stats := db.Stats()
// stats.WaitCount → total waits for connection
// stats.WaitDuration → total time waited
// stats.MaxIdleClosed → connections closed due to idle limit
// stats.InUse → currently in use
poolWaiting.Set(float64(stats.WaitCount))
Signs of saturation: WaitDuration growing, timeouts acquiring connections, InUse == MaxOpenConnections.
GC Pressure
# Enable GC trace
GODEBUG=gctrace=1 ./myapp
# Output: gc 14 @2.345s 3%: 0.5+12+0.3 ms clock, 4+8/12/0+2 ms cpu, 45->48->24 MB, 50 MB goal
# Fields: gc_num @elapsed cpu% stop-the-world+concurrent+stw ms heap_before->heap_after->live goal
// Read GC stats programmatically
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
// stats.NumGC → total GC cycles
// stats.PauseNs → circular buffer of pause times
// stats.HeapInuse → bytes in in-use spans
// stats.HeapReleased → bytes returned to OS
High GC pressure signals: pause time > 1ms, NumGC > 10/sec, heap oscillates wildly.
Fix: reduce allocations (sync.Pool, pre-allocate slices), increase GOGC (default 100 = GC when heap doubles).
Goroutine Leak Detection
# Check goroutine count over time
curl -s http://localhost:6060/debug/pprof/goroutine?debug=1 | head -5
# goroutine profile: total 4231 ← watch this number grow
// In tests, use goleak
import "go.uber.org/goleak"
func TestMyFunc(t *testing.T) {
defer goleak.VerifyNone(t)
// test code
}
Common leak patterns:
- Channel send/receive with no goroutine on the other end
http.Clientrequest without timeout (blocks on read forever)- Ticker/Timer never stopped
- goroutine waiting on context that's never cancelled
Debugging Decision Flow
flowchart TD
START([Symptom reported]) --> Q1{"High latency<br/>or low throughput?"}
Q1 -->|Latency| Q2{CPU high?}
Q1 -->|Throughput| Q3{Error rate high?}
Q2 -->|Yes| CPU_PROF["CPU pprof<br/>flame graph"]
Q2 -->|No| Q4{"I/O wait high?"}
Q4 -->|Yes| DISK["iostat + bpftrace<br/>disk latency"]
Q4 -->|No| Q5{Goroutines growing?}
Q5 -->|Yes| GR_PROF["goroutine pprof<br/>leak detection"]
Q5 -->|No| BLOCK["block/mutex pprof<br/>contention"]
Q3 -->|Yes| LOGS["Check logs<br/>error details"]
Q3 -->|No| Q6{Memory growing?}
Q6 -->|Yes| HEAP["heap pprof<br/>alloc_space"]
Q6 -->|No| Q7{Network issues?}
Q7 -->|Yes| NET["sar + bpftrace<br/>TCP retransmits"]
Q7 -->|No| POOL["Check conn pool<br/>db.Stats()"]