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.

Ảnh chụp đoạn mã Go nền tối minh hoạ heap profiling với pprof tìm chính xác cái gì cấp phát, benchmem cho biết có cấp phát pprof cho biết cấp phát ở đâu hàm dòng nào điểm mấu chốt hai góc nhìn khác nhau bộ nhớ đang dùng vs tổng đã cấp, hai hàm cấp phát khác kiểu func capVaGiu cấp 50MB và giữ lại for 500 lần giữ append make byte 100 nhân 1024 còn sống, func capVaVut cấp 500MB rồi vứt ngay for 5000 lần b make byte 100 nhân 1024 rác ngay, dump heap profile f os Create heap pprof pprof WriteHeapProfile f, đọc bằng go tool pprof hai chỉ số khác nhau go tool pprof top sample index inuse_space bộ nhớ đang sống ngay bây giờ RAM thực dùng, go tool pprof top sample index alloc_space tổng đã cấp từ đầu kể cả đã giải phóng áp lực GC, bốn chỉ số của heap profile inuse_space byte đang sống inuse_objects số object sống alloc_space tổng byte đã cấp alloc_objects tổng object đã cấp chọn đúng chỉ số theo vấn đề sửa RAM footprint dùng inuse sửa áp lực GC dùng alloc

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:

Ảnh chụp bảng kết quả đo thật nền tối cùng chương trình hai thủ phạm khác nhau, go tool pprof top sample index inuse_space đang sống flat flat phần trăm hàm 47.51MB 100 phần trăm main capVaGiu giữ 500 nhân 100KB còn sống 0 0 phần trăm main main capVaVut không xuất hiện đã vứt hết GC dọn sạch 0 sống, go tool pprof top sample index alloc_space tổng đã cấp flat flat phần trăm hàm 514.93MB 91.47 phần trăm main capVaVut cấp 5000 nhân 100KB rồi vứt 48.02MB 8.53 phần trăm main capVaGiu capVaVut chiếm 91 phần trăm churn 500MB dù giờ 0 byte còn sống, cốt lõi cùng một chương trình pprof chỉ hai thủ phạm hoàn toàn khác nhau tuỳ chỉ số inuse_space cho thấy capVaGiu giữ 47MB RAM dùng khi cần giảm dung lượng bộ nhớ alloc_space cho thấy capVaVut churn 515MB dùng khi cần giảm áp lực GC

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): capVaGiu chiếm 47,51 MB, 100%. Còn capVaVut khô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): capVaVut chiếm 514,93 MB, 91,47%. Nó churn 500MB tổng cộng dù giờ giữ 0 byte. capVaGiu chỉ 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ề

  1. 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.
  2. Đo thật cho thấy hai thủ phạm khác nhau: cùng chương trình, inuse_space chỉ hàm giữ 47MB RAM còn alloc_space chỉ hàm churn 515MB — hàm churn nhiều vô hình trong inuse vì GC đã dọn.
  3. Chọn chỉ số theo vấn đề: rò bộ nhớ / RAM cao dùng inuse_space, GC dày / chậm dùng alloc_space — chụp qua WriteHeapProfile hoặ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.