Performance Engineering & Profiling — Đo trước khi tối ưu
"Premature optimization is the root of all evil" — Knuth nói đúng. Nhưng "no optimization at all" cũng tệ không kém. Cái sai thật sự là optimize mà không đo.
Bạn có bao giờ ngồi đoán "chắc cái DB query này chậm" rồi dành 2 tuần refactor, chỉ để nhận ra bottleneck thật sự là... JSON marshaling? Chào mừng bạn đến với club "đoán mò optimization" 🎰
1. Tại sao Performance Profiling quan trọng?
1.1 Đoán vs Đo — Câu chuyện kinh điển
Câu chuyện thật:
Team có 1 API response trung bình 3.2 giây.
Hypothesis: "Chắc do DB query phức tạp"
→ Tuần 1: Tối ưu SQL, thêm index → DB time 800ms → 200ms
→ API vẫn 2.6 giây 😱
→ Tuần 2: Thêm Redis cache → cache hit 95%
→ API vẫn 2.4 giây 😭
→ Cuối cùng bật pprof:
→ 70% CPU time trong json.Marshal() cho struct 15MB
→ Fix: dùng jsoniter + giảm response size
→ API giảm từ 2.4s → 180ms 🎉
Bài học: 2 tuần đầu = wasted effort vì KHÔNG ĐO.
1.2 Performance Pyramid
┌─────────┐
│ Code │ ← Micro-optimization (cuối cùng)
┌─┴─────────┴─┐
│ Algorithm │ ← O(n²) → O(n log n)
┌─┴─────────────┴─┐
│ Architecture │ ← Caching, async, batching
┌─┴─────────────────┴─┐
│ Infrastructure │ ← Right-sizing, scaling
┌─┴─────────────────────┴─┐
│ Requirements │ ← Có cần real-time không?
└─────────────────────────┘
Rule: Tối ưu từ dưới lên.
2. CPU Profiling — Tìm CPU đang "bận" ở đâu
2.1 Sampling vs Instrumentation
Sampling (Go pprof mặc định):
→ Cứ mỗi 10ms, "chụp ảnh" call stack
→ Overhead thấp (~1-5%), dùng cho production
Instrumentation:
→ Đo chính xác mỗi function call
→ Overhead cao (10-50%), dùng cho development
2.2 Go pprof — Vũ khí chính của Gopher
HTTP server (production-ready):
import (
"net/http"
_ "net/http/pprof" // Magic import — đừng quên dấu _
)
func main() {
// QUAN TRỌNG: Đừng expose ra public internet! 🔒
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... application code ...
}
Thu thập profile:
# CPU profile 30 giây
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Memory profile
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutine profile (tìm goroutine leak)
go tool pprof http://localhost:6060/debug/pprof/goroutine
Trong benchmark:
go test -bench=BenchmarkProcessOrder -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Trong pprof interactive:
(pprof) top 10 # Top 10 functions by CPU
(pprof) list ProcessOrder # Line-by-line CPU usage
(pprof) web # Flame graph trong browser
2.3 Đọc Flame Graph
┌──────────────────────────────────────────┐
│ json.Marshal (45%) │ ← RỘNG = CHẬM
├────────────┬─────────────────────────────┤
│ reflect(20%)│ encodeStruct (25%) │
├──────┬─────┤────────────┬────────────────┤
│iter │type │ encodeInt │ encodeString │
└──────┴─────┴────────────┴────────────────┘
Trục X = % CPU time (RỘNG hơn = tốn CPU hơn)
Trục Y = call stack depth
Tìm "plateau" rộng nhất = bottleneck chính
2.4 CPU Hot Spots phổ biến trong Go
// ❌ Hot spot 1: JSON marshaling cho struct lớn
data, _ := json.Marshal(hugeStruct) // reflect-heavy
// ✅ Fix: code-generated marshaler (easyjson, sonic)
// ❌ Hot spot 2: Regex compilation trong loop
func validate(s string) bool {
matched, _ := regexp.MatchString(`pattern`, s) // Compile MỖI LẦN!
return matched
}
// ✅ Fix: compile 1 lần
var re = regexp.MustCompile(`pattern`)
func validate(s string) bool { return re.MatchString(s) }
// ❌ Hot spot 3: String concatenation trong loop
result := ""
for _, item := range items {
result += item.Name + "\n" // O(n²) allocations!
}
// ✅ Fix: strings.Builder
var b strings.Builder
b.Grow(len(items) * 50)
for _, item := range items {
b.WriteString(item.Name)
b.WriteByte('\n')
}
// ❌ Hot spot 4: Slice grow trong hot path
var results []Response // Grows dynamically → GC pressure
// ✅ Fix: pre-allocate
results := make([]Response, 0, len(requests))
3. Memory Profiling — Khi RAM "bốc hơi"
3.1 Heap vs Stack trong Go
Stack (nhanh, tự dọn):
→ Biến local, không escape khỏi function
→ Cost: gần như 0
Heap (chậm, cần GC):
→ Biến escape (return pointer, closure, interface{})
→ Cost: allocation + GC pressure
Kiểm tra:
go build -gcflags="-m" ./...
# ./main.go:15: &User{} escapes to heap ← Cần chú ý
3.2 Memory Profiling với pprof
# Ai allocate nhiều nhất
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
# Ai đang giữ memory
go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap
# So sánh 2 thời điểm (tìm leak)
go tool pprof -base=heap1.prof heap2.prof
3.3 Goroutine Leak — Silent Killer 🧟
// ❌ Classic leak: channel không ai đọc
func fetchData(ctx context.Context, url string) ([]byte, error) {
ch := make(chan []byte)
go func() {
data, _ := http.Get(url)
body, _ := io.ReadAll(data.Body)
ch <- body // Block FOREVER nếu parent return do ctx.Done()!
}()
select {
case data := <-ch:
return data, nil
case <-ctx.Done():
return nil, ctx.Err() // Goroutine ở trên = zombie 🧟
}
}
// ✅ Fix: buffered channel + context propagation
func fetchData(ctx context.Context, url string) ([]byte, error) {
ch := make(chan []byte, 1) // Buffered → goroutine send rồi exit
go func() {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil { return }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
ch <- body
}()
select {
case data := <-ch: return data, nil
case <-ctx.Done(): return nil, ctx.Err()
}
}
3.4 sync.Pool — Tái sử dụng thay vì allocate
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() // QUAN TRỌNG: reset trước khi dùng
defer bufPool.Put(buf) // Trả lại pool
io.Copy(buf, r.Body)
// process buf.Bytes()...
}
4. I/O Profiling — Khi "chờ đợi" là bottleneck
4.1 Network I/O — Connection Reuse
// ❌ Không reuse connections
func callAPI(url string) { http.Get(url) } // Mỗi lần = new TCP
// ✅ Reuse client
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
Timeout: 10 * time.Second,
}
4.2 strace — Kính hiển vi cho syscalls
strace -p <PID> -e trace=network -c
# Nhiều connect() calls = không reuse connections!
4.3 tcpdump — Network debugging
tcpdump -i any port 8080 -A | grep -E "^(GET|POST|HTTP)"
# Đếm new connections:
tcpdump -i any port 8080 -c 1000 | grep "Flags \[S\]" | wc -l
5. Latency Analysis — Percentile Thinking
5.1 Average nói dối
99 requests: 50ms, 1 request: 5000ms
Average: 99.5ms → "Ổn mà?"
p99: 5000ms → "1% users chờ 5 giây!" 😱
1M req/day → 10,000 users bị ảnh hưởng.
Nếu đó là checkout: $$ mất.
5.2 Percentile Cheat Sheet
p50: Trải nghiệm "bình thường"
p90: 10% tệ nhất
p95: Thường dùng cho SLO
p99: "Tail latency"
p99.9: Ultra-sensitive (payment, auth)
5.3 Tail Latency Amplification
1 API gọi 5 services song song, mỗi service p99 = 100ms
Xác suất ≥1 service chậm: 1 - 0.99^5 = 4.9%
Mitigation:
→ Hedged requests
→ Aggressive timeouts
→ Giảm fan-out
→ Cache ở gateway
6. Benchmarking Đúng Cách
6.1 Go Benchmark
func BenchmarkJSONMarshal(b *testing.B) {
user := User{Name: "Khoa", Age: 28}
b.ReportAllocs() // LUÔN BẬT
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(user)
}
}
go test -bench=. -benchmem -count=5 > old.txt
# ... optimize ...
go test -bench=. -benchmem -count=5 > new.txt
benchstat old.txt new.txt
6.2 Benchmark Pitfalls
// ❌ Compiler optimize away kết quả
func BenchmarkBad(b *testing.B) {
for i := 0; i < b.N; i++ {
computeHash("hello") // Result unused → compiler bỏ qua!
}
}
// ✅ Giữ result
var result []byte
func BenchmarkGood(b *testing.B) {
var r []byte
for i := 0; i < b.N; i++ { r = computeHash("hello") }
result = r
}
7. Continuous Profiling — Profile trong Production
Tại sao profile 1 lần không đủ:
→ Traffic patterns khác (peak hours, burst)
→ Data size khác (staging 1K rows, prod 100M)
→ Concurrency khác (1 user vs 10K)
Tools:
Pyroscope — Agent-based, Go native, ~2-5% overhead
Parca — eBPF-based, zero code change, K8s native
GCP Profiler — Managed, ~0.5% overhead
// Pyroscope integration
import "github.com/grafana/pyroscope-go"
func main() {
pyroscope.Start(pyroscope.Config{
ApplicationName: "order-service",
ServerAddress: "http://pyroscope:4040",
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileGoroutines,
},
})
defer pyroscope.Stop()
}
8. go tool trace — Goroutine Timeline
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.out
# → Goroutine analysis, network blocking, mutex contention
9. Optimization Checklist
Trước khi optimize:
□ Có baseline measurement?
□ Profile ở production load hay local?
□ Target cụ thể? (p99 < 200ms?)
□ Bottleneck chính? (CPU/Memory/IO/Network?)
Go optimizations (theo impact):
□ Pre-allocate slices/maps
□ sync.Pool cho hot paths
□ strings.Builder thay concatenation
□ Compile regex 1 lần
□ Reuse HTTP clients
□ Đọc hết response body trước close
□ Code-generated JSON (easyjson, sonic)
□ GOGC tuning, GOMEMLIMIT
Sau khi optimize:
□ Benchmark so sánh trước/sau (benchstat)
□ Regression test cho performance
□ Document lý do (ADR nếu cần)
Tài liệu tham khảo
- Go Blog: Profiling Go Programs
- Dave Cheney: High Performance Go
- Brendan Gregg: Systems Performance
- Pyroscope Docs
- Sách: Systems Performance — Brendan Gregg
💡 Remember: "If you can't measure it, you can't improve it." Đo đúng chỗ, optimize đúng thứ, đi ngủ đúng giờ. 😴