Go pprof thực chiến — Tìm bottleneck và rò rỉ memory
Hồi đó app của mình chạy ngon lành, response toàn dưới 100ms. Rồi tự nhiên một ngày, latency tăng dần theo tuần, RAM thì cứ vèo vèo lên như đồng hồ đếm ngược. Restart cái là hết, chạy ít bữa lại dính. Restart hoài thì khách than, mà nhìn code thì thấy chỗ nào cũng bình thường. Cái mình cần lúc đó không phải đoán mò, mà là nhìn thẳng vào bên trong app đang chạy — và đó là lúc mình học xài pprof.
Ảnh: Myburgh Roux — Pexels
Bật pprof — 3 dòng là xong
net/http/pprof là package chuẩn của Go, không cần cài gì thêm. Chỉ cần import blank và mở một HTTP server là app đã có nguyên dàn endpoint /debug/pprof/ để chẩn đoán:
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
Xong. Giờ muốn biết 30 giây tới app đang xào CPU chỗ nào, chạy:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
Lệnh này tải profile về rồi mở giao diện tương tác. Gõ top là thấy ngay top 10 hàm ngốn CPU nhất kèm phần trăm. Thấy thủ phạm rồi thì gõ list tênHàm để soi từng dòng code tốn bao nhiêu. Muốn trực quan hơn nữa thì gõ web — nó render ra flame graph, nhìn phát biết ngay hàm nào béo nhất.
Heap profile — tìm chỗ cấp phát quá tay
CPU xong tới memory. Rò rỉ RAM thì profile heap là bạn thân:
go tool pprof http://localhost:6060/debug/pprof/heap
Profile heap cho thấy chỗ nào đang cấp phát nhiều object nhất — chứ không phải chỗ nào chiếm RAM nhiều nhất tại thời điểm đó, nhiều người hay hiểu lầm chỗ này. Mẹo hay: chụp 2 profile heap cách nhau vài giờ rồi so bằng flag -base, phần chênh lệch chính là thứ đang "lớn lên" — tức chỗ rò rỉ:
go tool pprof -base heap_1h_truoc.prof heap_hien_tai.prof
Ảnh: weCare Media — Pexels
Goroutine profile — bắt kẻ giết RAM thầm lặng
Như bài goroutine leak tuần trước mình có kể, rò rỉ RAM ở Go thường không phải do cấp phát quá tay, mà do goroutine mắc kẹt không bao giờ thoát — kéo theo object của nó không được GC dọn. Profile goroutine sẽ lộ ngay:
curl http://localhost:6060/debug/pprof/goroutine?debug=1
Nó in ra stack trace của từng goroutine đang sống. Case mình từng gặp: hàng trăm goroutine kẹt hết ở cùng một dòng ch <- result — channel không ai đọc. Nhìn stack là biết ngay, khỏi đoán. Vài trăm goroutine chết đứng, mỗi đứa ôm một object to đùng, RAM tăng vèo vèo là vậy đó.
Ảnh: Pixabay — Pexels
Kinh nghiệm thực tế
- Profile ở môi trường gần production nhất có thể. Profile trên máy local với vài request rảnh tay thì toàn thấy
runtime.GCvớitime.Sleep— vô nghĩa. Dùng workload thật hoặc benchmark tải nặng. - Đừng chỉ nhìn
top. Có lần top toàn thấyruntime.mallocgc, mình tưởng hết thuốc, hóa ra do một chỗ gọifmt.Sprintftrong hot loop —listxuống là thấy liền. - Endpoint pprof để ở port riêng, không lộ ra ngoài. Nó là công cụ chẩn đoán, không phải API public — ai vô được là biết hết bên trong app.
Bài học của mình: đừng đoán, hãy đo. pprof cho mình nhìn thẳng vào app đang chạy, thay vì ngồi rải log rồi hy vọng. Còn bạn, lần đầu dính bottleneck bạn xử lý kiểu gì? Đã từng profile bằng pprof chưa?
📋 Phụ lục thuật ngữ
- pprof — công cụ profiling chuẩn của Go, đọc profile CPU/memory/goroutine
- Flame graph — biểu đồ hình ngọn lửa biểu diễn hàm nào tốn thời gian thực thi nhiều nhất
- Heap profile — snapshot các object đang cấp phát, giúp tìm chỗ tốn memory
- Goroutine leak — goroutine mắc kẹt không bao giờ kết thúc, kéo theo object không được GC dọn
- Allocation — việc cấp phát bộ nhớ cho object mới; cấp phát quá nhiều thì GC chạy nhiều, app chậm