🖥️ OS✍️ Khoa📅 19/04/2026☕ 8 phút đọc

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


💡 Remember: "If you can't measure it, you can't improve it." Đo đúng chỗ, optimize đúng thứ, đi ngủ đúng giờ. 😴