Progress · 0/8 phases
- All 120 Days Reference — Storage Engineering Roa
- Phase 8 — System Design & Interview Prep (Days 1
- Phase 7 — Performance Engineering (Days 91–105)
- Phase 6 — C++ Systems Programming (Days 76–90)
- Phase 4 — Distributed Storage Systems (Days 46–6
- Phase 3 — Storage Fundamentals (Days 31–45)
- Phase 2 — Linux Internals & OS (Days 16–30)
- Phase 1 — Go for Infrastructure (Days 1–15)
⚡ Phase 7 — Performance Engineering (Days 91–105)
10 min read · Days 91–105 · Notion
Core insight: The ability to debug a performance problem from first principles — without guessing — is the most valuable skill at a storage company. The USE method (Utilization, Saturation, Errors) gives you a systematic framework. Brendan Gregg's Linux perf tools give you the instruments. This phase combines both.
Day 91–93 — The USE Method for storage systems
USE Method: for every resource, check:
U = Utilization (how busy is the resource, 0-100%)
S = Saturation (is there a queue building up?)
E = Errors (are there error events?)
Storage resources to check:
1. CPU
U: top, mpstat -> %usr + %sys (for storage daemon)
S: load average > num_cpus -> runnable queue saturated
E: mcelog (hardware errors)
2. Memory
U: free -h -> used / total
S: vmstat -> si, so > 0 (swap in/out = memory saturated)
E: dmesg | grep -i 'oom\|out of memory'
3. Storage device (NVMe/HDD)
U: iostat -x -> %util (100% = fully utilized, but not necessarily saturated)
S: iostat -x -> avgqu-sz (average queue size > 1 = saturation for HDD)
E: dmesg | grep -i 'error\|failed\|reset'
smartctl -a /dev/nvme0n1 (SMART errors)
4. Network (for NFS/iSCSI/NVMe-oF)
U: sar -n DEV -> rxkB/s, txkB/s vs link speed
S: netstat -s -> retransmissions, dropped packets
E: ip -s link -> errors, dropped
ethtool -S eth0 -> rx_missed_errors
5. File descriptor limits
U: /proc/sys/fs/file-nr -> current / max
S: (no saturation concept)
E: dmesg | grep 'Too many open files'
/proc/PID/limits -> Max open filesDay 94–97 — fio deep dive: interpreting results
# Run a comprehensive benchmark and interpret every output field
fio --name=deep-dive \
--rw=randrw --rwmixread=70 \
--bs=4k --size=16G \
--numjobs=4 --iodepth=32 \
--direct=1 --ioengine=libaio \
--filename=/dev/nvme0n1 \
--runtime=60 --time_based \
--group_reporting
# Sample output and what each field means:
# read: IOPS=245k, BW=957MiB/s (1004MB/s)
# - IOPS: 245,000 random read operations per second
# - BW: 957 MiB/s throughput (4096 * 245000 / 1048576)
# lat (nsec): min=2085, max=8492k, avg=56785.23, stdev=48234.12
# - min: best case (cold path, cache hit?)
# - max: worst case (may be outlier/GC pause/thermal throttle)
# - avg: mean latency -- often misleading for storage!
# - stdev: high stdev = inconsistent latency = jitter problem
# lat (nsec): 50.00th=[ 47360], 75.00th=[ 59648]
# 90.00th=[ 77824], 95.00th=[ 94208]
# 99.00th=[139264], 99.50th=[165888]
# 99.90th=[223232], 99.95th=[272384]
# 99.99th=[612352]
# - p50 = median = 47us: half of all I/Os complete in 47us
# - p99 = 139us: 1 in 100 I/Os takes 139us
# - p99.99 = 612us: 1 in 10,000 I/Os takes 612us
# - For databases: p99.9 is usually the SLA target
# cpu: usr=8.45%, sys=22.34%, ctx=1842445/sec, majf=0, minf=0
# - sys% high: kernel overhead for I/O processing (normal for NVMe at high IOPS)
# - ctx: context switches per second
# - majf: major faults (should be 0 with direct=1)
# IO depths: 1=0.1%, 2=0.1%, 4=0.1%, 8=0.2%, 16=0.4%, 32=100%
# - This shows the actual queue depth distribution
# - 100% at depth=32 means device is always saturated with 32 in-flight I/Os
# Diagnosing performance problems from fio output:
# Problem: throughput lower than expected
# Check 1: is iodepth=1? -> too few in-flight I/Os, device underutilized
# Check 2: is numjobs too low? -> not enough parallelism
# Check 3: is bs too small? -> IOPS-bound, not throughput-bound
# Check 4: is CPU at 100%? -> software bottleneck, not storage
# Check 5: are there many retries? -> device errors causing retransmissions
# Problem: latency too high
# Check 1: queue depth too high -> queuing delay dominates actual device latency
# At QD=1, latency = device latency
# At QD=32, latency = device latency + queue wait time
# Check 2: mixed read/write on HDD -> head seeks cause latency spikes
# Check 3: %util too high -> device saturated, all I/Os waiting
# Check 4: is fsync() being called? -> each fsync adds ~device latency
# Latency percentiles explained for interview:
# "Why do we care about p99.9 rather than average?"
# If 1 in 1000 requests is slow and you serve 1M req/s,
# that's 1000 slow requests per second.
# With a microservice calling 10 backends, each at p99.9,
# P(at least one slow) = 1 - (1-0.001)^10 = ~1% of all requests see a slow response
# = tail latency amplificationDay 98–101 — pprof for production Go storage services
// Pattern: always run pprof server in production Go storage daemons
// It costs <1% CPU overhead and saves hours of debugging
import (
"net/http"
_ "net/http/pprof"
"runtime"
)
func startPProfServer() {
// Set mutex + block profiling rates
runtime.SetMutexProfileFraction(1) // profile mutex contention
runtime.SetBlockProfileRate(1) // profile goroutine blocking
go http.ListenAndServe(":6060", nil)
}
// 1. CPU profile: find what code consumes CPU
// Scenario: storage daemon is using 90% CPU but throughput is low
// -> CPU profile will show WHERE the CPU time is spent
curl -o cpu.prof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof -http=:8080 cpu.prof
// Look for: unexpected JSON marshaling, string conversions, map operations
// 2. Heap profile: find what is using memory
// Scenario: storage daemon grows to 8GB RSS over 24h and is OOM killed
curl -o heap.prof http://localhost:6060/debug/pprof/heap
go tool pprof -http=:8080 heap.prof
// Look for: large slices, cached items never evicted, goroutine closures
// pprof flags: -alloc_space (total allocated) vs -inuse_space (currently held)
// 3. Goroutine dump: find goroutine leaks
// Scenario: memory grows over time but heap profile shows little heap usage
// -> goroutines piling up, each holding stack memory
curl http://localhost:6060/debug/pprof/goroutine?debug=2
// Look for: goroutines blocked on channel send/receive with no consumer
// goroutines in same function repeatedly = leak
// 4. Mutex profile: find lock contention
// Scenario: storage service handles only 50K IOPS but you expected 500K
// -> CPU is not maxed, disk is not maxed, but throughput is low
// -> contention: goroutines waiting for a lock
curl -o mutex.prof http://localhost:6060/debug/pprof/mutex
go tool pprof -http=:8080 mutex.prof
// Look for: sync.Mutex.Lock calls with high cumulative wait time
// Fix: reduce lock scope, use sync.Map for read-heavy maps, shard the mutex
// 5. Block profile: find goroutines blocked on channel/select/syscall
curl -o block.prof http://localhost:6060/debug/pprof/block
go tool pprof -http=:8080 block.prof
// Real scenario from a storage service:
// pprof shows 60% of CPU time in runtime.mallocgc
// -> excessive allocations -> GC pressure
// Fix: use sync.Pool for 64KB buffers
// Result: CPU drops from 90% to 30%, throughput doublesDay 102–103 — iostat, vmstat, sar: system-level I/O diagnosis
# iostat: the primary tool for disk I/O analysis
iostat -x -d 1 nvme0n1
# Output columns explained:
# Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s
# nvme0n1 150000 50000 600000 200000 0.0 0.0
# r/s, w/s: read/write IOPS
# rkB/s, wkB/s: read/write throughput in KB/s
# rrqm/s, wrqm/s: I/O requests merged (HDD: high merge = sequential workload)
# Device %rrqm %wrqm r_await w_await aqu-sz %util
# nvme0n1 0.0 0.0 0.15 0.22 32.10 98.50
# r_await, w_await: average latency (ms) for read/write operations
# aqu-sz: average queue size -- critical metric!
# aqu-sz < 1: device underutilized (opportunity to increase parallelism)
# aqu-sz > 1: requests queuing up (may indicate saturation or just NVMe at depth)
# %util: % of time device was busy (NOT the same as saturated!)
# NVMe: %util can be 100% but aqu-sz=32 is normal (device is fast, just parallel)
# HDD: %util > 80% -> likely saturated (seeks dominate, no parallelism benefit)
# vmstat: system-wide memory and CPU
vmstat 1
# Key columns:
# si, so: swap in/out (pages/s) -- if > 0, memory is a problem
# bi, bo: blocks in/out (512B blocks/s) -- disk I/O summary
# wa: CPU % waiting for I/O -- high wa = storage bottleneck
# cs: context switches/s
# sar: historical performance data (if sysstat installed)
sar -n DEV 1 10 # network interface stats every 1s for 10 iterations
sar -d 1 10 # disk device stats
sar -u 1 10 # CPU stats
sar -B 1 10 # paging stats
# Diagnosing "slow I/O" complaint:
watch -n1 'iostat -x nvme0n1 | tail -4'
# 1. Check r_await/w_await: > 5ms for NVMe = problem
# 2. Check %util: 100% but low IOPS = queue depth too low (increase parallelism)
# 3. Check aqu-sz: > 64 for NVMe = too many in flight (back-pressure needed)
# 4. Check wrqm/s > 0: good for HDDs (kernel merging writes = sequential pattern)
# 5. Compare rkB/s to device spec: if 500 MB/s device only doing 100MB/s -> investigateDay 104–105 — Phase 7 Capstone: Performance Audit
Project: Full performance audit of the Go storage service from Phase 1
Deliverable: a written "Performance Investigation Report"
Step 1: Baseline measurement
- fio benchmark on the underlying disk: sequential + random IOPS + latency
- Run your Phase 1 Go service under load (wrk or hey HTTP load generator)
- Record: RPS, p50/p99/p999 latency, CPU%, memory RSS
Step 2: Profile under load
- CPU profile (30s): identify top 3 hot functions
- Heap profile: identify top 3 allocation sites
- Goroutine dump: count goroutines, look for blocked ones
Step 3: Introduce a deliberate bug and find it
- Add a global sync.Mutex protecting every read (even read-only operations)
- Observe: throughput drops significantly
- Find it with mutex profile
- Fix: use sync.RWMutex for reads
- Measure improvement
Step 4: Buffer allocation optimization
- Profile shows lots of allocs in hot path
- Add sync.Pool for I/O buffers
- Measure: GC pauses (GODEBUG=gctrace=1) and throughput improvement
Step 5: Write the report (1-2 pages)
- What was the bottleneck?
- How did you find it?
- What was the fix?
- What was the measured improvement?
- What would you investigate next?
This report format is exactly what you would use in a technical screen:
"Tell me about a performance problem you investigated and fixed."Interview questions
- "A storage daemon's write latency went from 1ms to 50ms at 3am. How do you debug it?" USE method: check CPU utilization (top/mpstat), disk saturation (iostat), memory pressure (vmstat), network errors (ip -s link). Then: check application logs for errors. Check dmesg for disk errors. Profile with pprof if the process is Go. Check if a cron job or backup started at 3am.
- "What does %util=100% in iostat mean? Is the disk saturated?" Not necessarily. %util means the device was busy 100% of the time in the measurement interval. For NVMe, a device can be 100% busy AND handle all I/Os at normal latency if the queue depth is well-matched. Saturation is better indicated by r_await/w_await increasing beyond baseline, or aqu-sz growing unboundedly.
- "How would you diagnose a Go service with growing memory but no obvious heap growth?" Goroutine leak:
curl /debug/pprof/goroutine?debug=2— count goroutines over time. Each goroutine holds 2-8KB stack minimum. If goroutine count grows unboundedly, you have a leak. Find the goroutine function from the dump and trace where it's created without a corresponding teardown. - "What is the difference between average latency and p99 latency?" Average smooths over outliers and can be dominated by the fast majority. P99 tells you what the slowest 1% of requests experience. For storage systems serving databases, a single slow I/O can cause a transaction to time out. P99.9 and P99.99 matter for high-QPS services where tail latency amplification turns rare slow I/Os into common user-visible slowness.