Bài đo cấp phát cho biết một hàm có cấp phát bao nhiêu, nhưng trong một chương trình thật với hàng trăm hàm, câu hỏi là: hàm nào chịu trách nhiệm? Đó là việc của heap profiling với pprof. Nhưng có một cái bẫy tinh vi mà nhiều người mắc: pprof có hai chỉ số bộ nhớ hoàn toàn khác nhau, và chọn sai chỉ số sẽ chỉ bạn tới nhầm thủ phạm. Bài này đo trực tiếp sự khác biệt đó trên một chương trình mà hai chỉ số cho hai câu trả lời trái ngược.
Chụp và đọc heap profile
Cách chụp heap profile đơn giản nhất là pprof.WriteHeapProfile:
runtime.GC() // dọn rác trước để inuse phản ánh đúng phần sống
f, _ := os.Create("heap.pprof")
pprof.WriteHeapProfile(f)
Rồi đọc bằng go tool pprof -top. Nhưng đây là chỗ mấu chốt: heap profile chứa bốn chỉ số, và bạn phải chọn:
inuse_space: số byte đang sống ngay lúc chụp (RAM thực dùng).inuse_objects: số object đang sống.alloc_space: tổng byte đã cấp từ đầu chương trình, kể cả đã giải phóng.alloc_objects: tổng object đã cấp.

Hình 1: Hai hàm cấp phát khác kiểu — một giữ lại (inuse cao), một vứt ngay (alloc cao). WriteHeapProfile để chụp, go tool pprof -sample_index=... để chọn góc nhìn.
Đo thật: hai thủ phạm trái ngược
Đây là phần bất ngờ nhất. Chương trình có hai hàm: capVaGiu cấp ~48MB và giữ lại (giữ = append(...)), còn capVaVut cấp ~500MB rồi vứt ngay. Xem pprof chỉ ai:

Hình 2: inuse_space chỉ capVaGiu (47,51 MB, giữ RAM) — capVaVut vô hình vì GC đã dọn. alloc_space chỉ capVaVut (514,93 MB, 91%, churn) — dù giờ nó giữ 0 byte. Cùng chương trình, hai thủ phạm khác nhau.
Kết quả trái ngược hoàn toàn:
- Theo
inuse_space(đang sống):capVaGiuchiếm 47,51 MB, 100%. CòncapVaVutkhông xuất hiện — nó đã vứt hết, GC dọn sạch, giờ 0 byte sống. - Theo
alloc_space(tổng đã cấp):capVaVutchiếm 514,93 MB, 91,47%. Nó churn 500MB tổng cộng dù giờ giữ 0 byte.capVaGiuchỉ 48 MB (8,53%).
Cùng một chương trình, pprof chỉ hai thủ phạm hoàn toàn khác nhau tuỳ chỉ số bạn chọn. Nếu vấn đề là RAM footprint cao (chương trình ngốn bộ nhớ), thủ phạm là capVaGiu (giữ 47MB) — nhìn inuse_space. Nếu vấn đề là GC dày/chậm (áp lực GC), thủ phạm là capVaVut (churn 515MB) — nhìn alloc_space. Chọn sai chỉ số là đuổi nhầm hàm.
Ứng dụng thực tế
Rò bộ nhớ / RAM cao → dùng inuse_space. Nếu chương trình ngốn RAM tăng dần hoặc vượt giới hạn, inuse_space/inuse_objects chỉ ra hàm nào đang giữ bộ nhớ. Đây là công cụ chính để tìm rò bộ nhớ (bài sau) — object bị giữ mà lẽ ra phải thả.
GC dày / CPU cao vì GC → dùng alloc_space. Nếu gctrace cho thấy GC chạy quá thường (bài đọc gctrace), alloc_space/alloc_objects chỉ ra hàm nào sinh rác nhiều nhất — đó là nơi áp dụng các kỹ thuật giảm cấp phát. Hàm churn nhiều có thể vô hình trong inuse (như capVaVut).
Bật pprof qua HTTP trên production. import _ "net/http/pprof" mở endpoint /debug/pprof/heap — chụp profile của dịch vụ đang chạy mà không cần dừng: go tool pprof http://localhost:6060/debug/pprof/heap. Dùng -alloc_space hoặc -inuse_space tuỳ vấn đề. Đây là cách chẩn đoán bộ nhớ trên hệ thống thật.
Đánh đổi cần cân nhắc
Heap profile là mẫu, không phải đếm chính xác từng byte. Runtime lấy mẫu cấp phát theo runtime.MemProfileRate (mặc định lấy mẫu mỗi ~512KB cấp phát) để giảm chi phí. Nên con số là ước lượng thống kê, không chính xác tuyệt đối — đủ tốt để tìm thủ phạm lớn, nhưng đừng tin từng byte. Đặt MemProfileRate=1 cho chính xác hơn (tốn chi phí).
Nhớ runtime.GC() trước khi chụp inuse. inuse_space phản ánh phần sống tại thời điểm chụp. Nếu không GC trước, rác chưa dọn sẽ lẫn vào con số inuse, làm sai lệch. Chạy runtime.GC() ngay trước WriteHeapProfile để inuse phản ánh đúng phần thực sự sống.
pprof chỉ ra "cái gì", không tự sửa. Nó tìm hàm cấp phát nhiều, nhưng vì sao và sửa thế nào là việc của bạn (áp các kỹ thuật đã học). Và pprof heap không cho biết thời điểm — để hiểu bộ nhớ tăng theo thời gian, cần chụp nhiều profile và so sánh (bài phát hiện rò rỉ).
Ba ý mang về
- Heap profile có hai chỉ số trái ngược:
inuse_space(byte đang sống, RAM thực dùng) vàalloc_space(tổng byte đã cấp kể cả giải phóng, áp lực GC) — cùng object khác chỉ số, và chọn sai là đuổi nhầm thủ phạm. - Đo thật cho thấy hai thủ phạm khác nhau: cùng chương trình,
inuse_spacechỉ hàm giữ 47MB RAM cònalloc_spacechỉ hàm churn 515MB — hàm churn nhiều vô hình trong inuse vì GC đã dọn. - Chọn chỉ số theo vấn đề: rò bộ nhớ / RAM cao dùng
inuse_space, GC dày / chậm dùngalloc_space— chụp quaWriteHeapProfilehoặc endpoint HTTP/debug/pprof/heap, và nhớruntime.GC()trước khi chụp inuse.
Phần sau ta dùng chính công cụ này để giải một bài toán khó: Phần sau mổ xẻ cách phát hiện rò rỉ bộ nhớ thật trong Go — vì sao Go vẫn rò được dù có GC, cách chụp và so sánh nhiều heap profile để tìm object bị giữ, và các mẫu rò kinh điển.