Phân tích bộ nhớ Go cho đột biến lưu lượng: hướng dẫn pprof
Phân tích bộ nhớ Go giúp xử lý đột biến lưu lượng. Hướng dẫn thực hành với pprof để tìm điểm nóng cấp phát trong JSON, quét DB và middleware.

Tác động của đột biến lưu lượng lên bộ nhớ dịch vụ Go
“Một spike bộ nhớ” trong môi trường production hiếm khi chỉ là một con số đơn lẻ tăng lên. Bạn có thể thấy RSS (bộ nhớ tiến trình) tăng nhanh trong khi heap của Go hầu như không đổi, hoặc heap tăng rồi rơi theo sóng khi GC chạy. Đồng thời, độ trễ thường tệ hơn vì runtime mất nhiều thời gian hơn để dọn dẹp.
Các mẫu phổ biến trong metrics:
- RSS tăng nhanh hơn mong đợi và đôi khi không giảm hoàn toàn sau spike
- Heap in-use tăng rồi giảm theo chu kỳ khi GC chạy nhiều hơn
- Tốc độ cấp phát tăng (bytes cấp phát mỗi giây)
- Thời gian pause GC và CPU dành cho GC tăng, ngay cả khi mỗi pause nhỏ
- Độ trễ request nhảy và độ trễ đuôi trở nên nhiễu hơn
Spike khuếch đại các cấp phát trên mỗi request vì “lãng phí nhỏ” tỉ lệ thuận với lưu lượng. Nếu một request cấp phát thêm 50 KB (bộ đệm JSON tạm thời, object scan cho mỗi hàng, dữ liệu context ở middleware), thì ở 2.000 RPS bạn chuyển cho bộ cấp phát ~100 MB mỗi giây. Go chịu được nhiều, nhưng GC vẫn phải dò và giải phóng các object ngắn sống đó. Khi tốc độ cấp phát vượt quá khả năng dọn dẹp, target heap tăng, RSS tiếp theo, và bạn có thể chạm giới hạn bộ nhớ.
Triệu chứng quen thuộc: orchestrator kill vì OOM, độ trễ tăng đột ngột, nhiều thời gian hơn cho GC, và dịch vụ trông “bận” ngay cả khi CPU không bị kẹp cứng. Bạn cũng có thể gặp GC thrash: service vẫn chạy nhưng liên tục cấp phát và thu gom nên throughput giảm đúng lúc cần cao nhất.
pprof giúp trả lời nhanh một câu hỏi: đường dẫn mã nào cấp phát nhiều nhất, và những cấp phát đó có cần thiết không? Một heap profile cho biết gì đang được giữ vào lúc chụp. Các view tập trung vào cấp phát (ví dụ alloc_space) cho thấy đâu là thứ được tạo ra và bị vứt bỏ nhanh.
Cái pprof không làm được là giải thích từng byte của RSS. RSS gồm nhiều hơn heap Go (stack, metadata runtime, mapping OS, cấp phát cgo, phân mảnh). pprof tốt nhất trong việc chỉ ra điểm nóng cấp phát trong mã Go của bạn, không phải để chứng minh tổng bộ nhớ ở cấp container chính xác.
Thiết lập pprof an toàn (từng bước)
pprof dùng dễ nhất qua các endpoint HTTP, nhưng những endpoint đó có thể tiết lộ nhiều về dịch vụ. Xem chúng như một tính năng admin, không phải API công khai.
1) Thêm endpoint pprof
Trong Go, cấu hình đơn giản nhất là chạy pprof trên một server admin riêng. Điều này giữ các route profiling tách khỏi router chính và middleware của bạn.
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
// Admin only: bind to localhost
log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
}()
// Your main server starts here...
// http.ListenAndServe(":8080", appHandler)
select {}
}
Nếu bạn không thể mở port thứ hai, có thể mount route pprof vào server chính, nhưng dễ vô tình phơi bày nó hơn. Port admin riêng là mặc định an toàn hơn.
2) Khóa truy cập trước khi triển khai
Bắt đầu với các kiểm soát khó sai. Bind vào localhost có nghĩa là các endpoint không thể truy cập từ Internet trừ khi ai đó cũng lộ port đó.
Checklist nhanh:
- Chạy pprof trên cổng admin, không phải cổng phục vụ người dùng
- Bind vào 127.0.0.1 (hoặc interface riêng) ở production
- Thêm allowlist ở rìa mạng (VPN, bastion, hoặc subnet nội bộ)
- Yêu cầu auth nếu rìa của bạn có thể thực thi (basic auth hoặc token)
- Xác minh bạn có thể lấy các profile bạn cần: heap, allocs, goroutine
3) Build và rollout an toàn
Giữ thay đổi nhỏ: thêm pprof, deploy, và xác nhận chỉ truy cập được từ nơi bạn mong muốn. Nếu có staging, test ở đó trước bằng cách giả lập tải và chụp heap và allocs profile.
Với production, rollout dần dần (một instance hoặc một phần nhỏ lưu lượng). Nếu pprof cấu hình sai, phạm vi ảnh hưởng nhỏ còn bạn sửa.
Chụp profile đúng lúc trong đợt spike
Trong đợt spike, một snapshot đơn lẻ hiếm khi đủ. Hãy chụp một timeline nhỏ: vài phút trước spike (baseline), trong spike (impact), và vài phút sau (recovery). Điều này giúp tách thay đổi cấp phát thực ra khỏi hành vi khởi động bình thường.
Nếu bạn có thể tái tạo spike với tải kiểm soát, cố gắng khớp môi trường production: tỉ lệ request, kích thước payload và concurrency. Spike của các request nhỏ hành xử rất khác so với spike của các phản hồi JSON lớn.
Chụp cả heap và profile tập trung vào cấp phát. Chúng trả lời các câu hỏi khác nhau:
- Heap (inuse) cho biết gì đang còn sống và giữ bộ nhớ ngay lúc chụp
- Cấp phát (alloc_space hoặc alloc_objects) cho biết gì đang bị tạo nhiều, ngay cả khi bị giải phóng nhanh
Mẫu chụp thực dụng: lấy một heap profile, rồi một allocation profile, sau đó lặp lại sau 30–60 giây. Hai điểm trong lúc spike giúp bạn thấy liệu đường dẫn nghi vấn ổn định hay đang tăng tốc.
# examples: adjust host/port and timing to your setup
curl -o heap_during.pprof "http://127.0.0.1:6060/debug/pprof/heap"
curl -o allocs_30s.pprof "http://127.0.0.1:6060/debug/pprof/allocs?seconds=30"
Bên cạnh file pprof, ghi lại vài thống kê runtime để giải thích GC đang làm gì cùng thời điểm đó. Kích thước heap, số lần GC, và thời gian pause thường là đủ. Một dòng log ngắn tại mỗi thời điểm chụp giúp đối chiếu “cấp phát tăng” với “GC bắt đầu chạy liên tục.”
Ghi chú sự cố khi làm: phiên bản build (commit/tag), phiên bản Go, flag quan trọng, thay đổi cấu hình, và lưu lượng đang diễn ra (endpoint, tenant, kích thước payload). Những chi tiết đó thường quan trọng khi so sánh các profile và nhận ra mix request đã đổi.
Cách đọc profile heap và allocation
Một heap profile trả lời câu hỏi khác nhau tuỳ view.
Inuse space cho thấy thứ gì còn trong bộ nhớ tại thời điểm chụp. Dùng cho rò rỉ, cache dài hạn, hoặc request để lại object.
Alloc space (tổng cấp phát) cho thấy thứ gì đã được cấp phát theo thời gian, ngay cả khi đã được giải phóng. Dùng khi spike gây nhiều công việc GC, độ trễ tăng, hoặc OOM do churn.
Sampling quan trọng. Go không ghi tất cả cấp phát; nó sampling các cấp phát (điều khiển bằng runtime.MemProfileRate), nên các cấp phát nhỏ, thường xuyên có thể bị đại diện thấp hơn và các con số chỉ là ước tính. Những kẻ gây hại lớn vẫn thường nổi bật, đặc biệt trong điều kiện spike. Tìm xu hướng và top contributor, đừng săn từng con số chính xác.
Các view pprof hữu ích nhất:
- top: đọc nhanh ai chiếm ưu thế ở inuse hoặc alloc (xem cả flat và cumulative)
- list
: nguồn cấp phát ở mức từng dòng trong một hàm nóng - graph: đường gọi cho thấy cách bạn đến đó
Diffs là nơi thực tế xảy ra. So sánh profile baseline (lưu lượng bình thường) với profile lúc spike để làm nổi bật gì thay đổi, thay vì đuổi theo nhiễu nền.
Xác thực phát hiện bằng một thay đổi nhỏ trước khi refactor lớn:
- Tái dùng bộ đệm (hoặc thêm
sync.Poolnhỏ) trên đường nóng - Giảm tạo object trên mỗi request (ví dụ, tránh dựng map trung gian cho JSON)
- Re-profile dưới cùng tải và xác nhận diff thu nhỏ ở nơi bạn mong đợi
Nếu các con số di chuyển theo hướng mong muốn, bạn đã tìm được nguyên nhân thực sự, không chỉ một báo cáo đáng sợ.
Tìm điểm nóng cấp phát trong JSON encoding
Trong đợt spike, công việc JSON có thể trở thành hoá đơn bộ nhớ lớn vì nó chạy trên mỗi request. Điểm nóng JSON thường hiện dưới dạng nhiều cấp phát nhỏ đẩy GC hoạt động mạnh.
Dấu hiệu đỏ cần chú ý trong pprof
Nếu heap hoặc allocation view chỉ vào encoding/json, nhìn kỹ dữ liệu bạn đưa vào nó. Các pattern sau thường phồng cấp phát:
- Dùng
map[string]any(hoặc[]any) cho response thay vì struct kiểu tĩnh - Marshal cùng object nhiều lần (ví dụ, log nó và trả lại nó)
- Pretty printing với
json.MarshalIndenttrong production - Xây dựng JSON qua các chuỗi tạm (
fmt.Sprintf, nối chuỗi) trước khi marshal - Chuyển
[]bytelớn thànhstring(hoặc ngược lại) chỉ để phù hợp API
json.Marshal luôn cấp phát một []byte mới cho toàn bộ output. json.NewEncoder(w).Encode(v) thường tránh được một buffer lớn đó vì nó ghi vào io.Writer, nhưng nó vẫn có thể cấp phát nội bộ, đặc biệt nếu v đầy các any, map, hoặc cấu trúc nhiều con trỏ.
Sửa nhanh và thử nghiệm nhanh
Bắt đầu bằng struct kiểu tĩnh cho hình dạng response. Chúng giảm công việc reflection và tránh boxing interface cho mỗi trường.
Sau đó loại bỏ các tạm thời không cần thiết: tái dùng bytes.Buffer qua sync.Pool (cẩn trọng), không indent trong production, và không marshal lại chỉ để log.
Thí nghiệm nhỏ xác nhận JSON là thủ phạm:
- Thay
map[string]anybằng struct cho một endpoint nóng và so sánh profile - Chuyển từ
MarshalsangEncoderghi trực tiếp vào response - Loại bỏ
MarshalIndenthoặc format chỉ dành cho debug và re-profile dưới cùng tải - Bỏ mã hóa JSON cho các response cache không đổi và đo sự giảm
Tìm điểm nóng cấp phát trong việc scan query
Khi bộ nhớ nhảy trong đợt spike, đọc từ DB thường là bất ngờ phổ biến. Dễ mắc lỗi chỉ nhìn thời gian SQL, nhưng bước scan có thể cấp phát nhiều cho mỗi hàng, đặc biệt khi bạn scan vào kiểu linh hoạt.
Những thủ phạm thường gặp:
- Scan vào
interface{}(hoặcmap[string]any) và để driver quyết định kiểu - Chuyển
[]bytethànhstringcho mỗi trường - Dùng wrapper nullable (
sql.NullString,sql.NullInt64) trong tập kết quả lớn - Lấy các cột text/blob lớn mà bạn không luôn cần
Một pattern âm thầm đốt bộ nhớ là scan dữ liệu hàng vào biến tạm rồi sao chép vào struct thật (hoặc dựng map cho mỗi hàng). Nếu bạn có thể scan thẳng vào struct với trường cụ thể, bạn tránh được cấp phát thêm và kiểm tra kiểu.
Kích thước batch và phân trang thay đổi hình dạng bộ nhớ. Lấy 10.000 hàng vào một slice sẽ cấp phát cho việc tăng trưởng slice và cho từng hàng, cùng lúc. Nếu handler chỉ cần một trang, đẩy điều đó vào truy vấn và giữ kích thước trang ổn định. Nếu phải xử lý nhiều hàng, stream chúng và gom tóm tắt nhỏ thay vì lưu mọi hàng.
Cột text lớn cần chăm sóc. Nhiều driver trả text dưới dạng []byte. Chuyển sang string sẽ copy dữ liệu, nên làm điều này cho mỗi hàng có thể bùng cấp phát. Nếu chỉ cần giá trị đôi khi, trì hoãn chuyển đổi hoặc scan ít cột hơn cho endpoint đó.
Để xác nhận driver hay mã của bạn chịu phần lớn cấp phát, kiểm tra thứ chiếm ưu thế trong profile:
- Nếu frame chỉ vào mã chuyển đổi/mapping của bạn, tập trung vào mục tiêu scan và chuyển đổi
- Nếu frame chỉ vào
database/sqlhoặc driver, giảm số hàng và cột trước, sau đó cân nhắc tuỳ chọn driver - Kiểm tra cả alloc_space và alloc_objects; nhiều cấp phát nhỏ có thể tệ hơn vài cấp phát lớn
Ví dụ: endpoint “list orders” scan SELECT * vào []map[string]any. Trong spike, mỗi request dựng hàng nghìn map và string nhỏ. Thay query chọn chỉ cột cần và scan vào []Order{ID int64, Status string, TotalCents int64} thường giảm cấp phát ngay. Ý tưởng tương tự áp dụng nếu bạn profile backend Go sinh từ AppMaster: điểm nóng thường nằm ở cách bạn tạo và scan dữ liệu kết quả, không phải ở database.
Các pattern middleware âm thầm cấp phát mỗi request
Middleware trông rẻ vì nó “chỉ là wrapper”, nhưng nó chạy trên mỗi request. Trong spike, các cấp phát nhỏ trên mỗi request cộng lại nhanh và hiện rõ dưới dạng tốc độ cấp phát tăng.
Logging middleware là nguồn phổ biến: format chuỗi, dựng map các trường, hoặc copy header để in đẹp. Helper tạo request ID có thể cấp phát khi tạo ID, chuyển thành string, rồi gắn vào context. Thậm chí context.WithValue cũng có thể cấp phát nếu bạn lưu object mới (hoặc string mới) trên mỗi request.
Nén và xử lý body là thủ phạm khác. Nếu middleware đọc toàn bộ body để “peek” hoặc validate, bạn có thể có buffer lớn cho mỗi request. Gzip middleware có thể cấp phát nhiều nếu tạo reader và writer mới mỗi lần thay vì tái dùng buffer.
Auth và session tương tự. Nếu mỗi request phân tích token, decode base64 cookie, hoặc load blob session vào struct mới, bạn có churn liên tục ngay cả khi handler làm việc nhẹ.
Tracing và metrics cũng có thể cấp phát nhiều khi label được dựng động. Nối tên route, user agent, hoặc tenant ID thành chuỗi mới cho mỗi request là chi phí ẩn kinh điển.
Các pattern thường xuất hiện như “death by a thousand cuts”:
- Xây log line với
fmt.Sprintfvà map mớimap[string]anycho mỗi request - Copy headers vào map hoặc slice mới để log hoặc ký
- Tạo buffer và reader/writer gzip mới thay vì pooling
- Tạo label metric có độ đa dạng cao (nhiều chuỗi khác nhau)
- Lưu struct mới vào context trên mỗi request
Để cô lập chi phí middleware, so sánh hai profile: một có toàn chuỗi middleware và một tạm thời tắt hoặc thay bằng no-op. Một test đơn giản là endpoint health gần như không cấp phát. Nếu /health cấp phát nhiều trong spike, handler không phải vấn đề.
Nếu bạn sinh backend Go bằng AppMaster, quy tắc vẫn vậy: giữ các tính năng chéo đo lường được, và xem mỗi-request allocation như một ngân sách cần audit.
Những sửa thường có lợi nhanh
Khi bạn đã có heap và allocs từ pprof, ưu tiên thay đổi giảm cấp phát trên mỗi request. Mục tiêu không phải mẹo tinh vi, mà là làm cho đường nóng tạo ít object ngắn sống hơn, nhất là dưới tải.
Bắt đầu với các giải pháp an toàn, đơn giản
Nếu kích thước dự đoán được, cấp phát trước. Nếu endpoint thường trả ~200 item, tạo slice với capacity 200 để nó không tự tăng kích thước và copy nhiều lần.
Tránh dựng chuỗi trong đường nóng. fmt.Sprintf tiện nhưng thường cấp phát. Với logging, ưu tiên trường cấu trúc và tái dùng buffer nhỏ khi hợp lý.
Nếu bạn tạo phản hồi JSON lớn, cân nhắc stream thay vì dựng một []byte hay string khổng lồ trong bộ nhớ. Mẫu spike phổ biến: request đến, bạn đọc body lớn, dựng response lớn, bộ nhớ nhảy cho đến khi GC bắt kịp.
Thay đổi nhanh thường thể hiện rõ trong profile trước/sau:
- Preallocate slice và map khi biết khoảng kích thước
- Thay format
fmtnặng bằng các lựa chọn rẻ hơn trong xử lý request - Stream JSON lớn (encode trực tiếp vào response writer)
- Dùng
sync.Poolcho object cùng hình dạng có thể tái dùng (buffer, encoder) và trả về nhất quán - Đặt giới hạn request (kích thước body, payload, page size) để giới hạn trường hợp xấu
Dùng sync.Pool cẩn thận
sync.Pool giúp khi bạn liên tục cấp phát cùng một kiểu, như bytes.Buffer cho mỗi request. Nó cũng gây hại nếu bạn pool object có kích thước không dự đoán được hoặc quên reset chúng, làm giữ lại các mảng backing lớn.
Đo trước và sau bằng cùng workload:
- Chụp allocs profile trong cửa sổ spike
- Áp dụng một thay đổi từng bước
- Chạy lại cùng request mix và so sánh tổng allocs/op
- Theo dõi độ trễ đuôi, không chỉ bộ nhớ
Nếu bạn sinh backend Go từ AppMaster, các sửa này vẫn áp dụng cho mã tùy chỉnh quanh handler, tích hợp và middleware. Đó là nơi các cấp phát ẩn theo spike thường ẩn.
Sai lầm thường gặp với pprof và báo động giả
Cách nhanh nhất để lãng phí một ngày là tối ưu cái không đúng. Nếu dịch vụ chậm, bắt đầu với CPU. Nếu nó bị kill bởi OOM, bắt đầu với heap. Nếu sống nhưng GC chạy không ngừng, nhìn vào allocation rate và hành vi GC.
Một bẫy khác là nhìn vào “top” rồi nghĩ xong. “Top” giấu ngữ cảnh. Luôn kiểm tra call stacks (hoặc flame graph) để thấy ai gọi bộ cấp phát. Sửa thường nằm một hoặc hai frame phía trên hàm nóng.
Cũng chú ý nhầm lẫn inuse vs churn. Một request có thể cấp phát 5 MB object ngắn sống, kích hoạt GC thêm, và kết thúc chỉ còn 200 KB inuse. Nếu chỉ nhìn inuse, bạn bỏ mất churn. Nếu chỉ nhìn tổng cấp phát, bạn có thể tối ưu thứ không ở lại và không ảnh hưởng rủi ro OOM.
Kiểm tra nhanh trước khi đổi code:
- Xác nhận bạn đang ở đúng view: heap inuse cho retention, alloc_space/alloc_objects cho churn
- So sánh stacks, không chỉ tên hàm (
encoding/jsonthường là triệu chứng) - Tái hiện lưu lượng thực tế: cùng endpoint, kích thước payload, header, concurrency
- Chụp baseline và spike profile, rồi diff
Test tải không thực tế gây báo động giả. Nếu test gửi body JSON nhỏ nhưng production gửi payload 200 KB, bạn sẽ tối ưu sai đường. Nếu test trả về một hàng DB, bạn sẽ không thấy hành vi scan xuất hiện với 500 hàng.
Đừng theo tiếng ồn. Nếu một hàm chỉ xuất hiện trong profile khi spike (không có ở baseline), đó là đầu mối mạnh. Nếu nó xuất hiện ở cả hai cùng mức, có thể đó là công việc nền bình thường.
Một walkthrough sự cố thực tế
Sáng thứ Hai có chương trình khuyến mãi và API Go của bạn nhận lưu lượng gấp 8 lần bình thường. Triệu chứng đầu tiên không phải crash. RSS tăng, GC bận hơn, và p95 latency nhảy. Endpoint nóng nhất là GET /api/orders vì app mobile refresh nó mỗi lần mở màn hình.
Bạn chụp hai snapshot: một lúc yên (baseline) và một lúc spike. Chụp cùng loại heap profile để so sánh công bằng.
Luồng làm việc:
- Lấy baseline heap profile và ghi RPS, RSS, p95 hiện tại
- Trong spike, lấy heap profile và một allocation profile trong cùng cửa sổ 1–2 phút
- So sánh top allocator giữa hai profile và tập trung vào thứ tăng nhiều nhất
- Đi từ hàm lớn nhất lên callers cho đến khi đến handler của bạn
- Thực hiện một thay đổi nhỏ, deploy lên một instance và re-profile
Trong trường hợp này, profile lúc spike cho thấy phần lớn cấp phát mới đến từ JSON encoding. Handler dựng map[string]any cho từng hàng, rồi gọi json.Marshal trên slice map. Mỗi request tạo nhiều string ngắn và giá trị interface.
Fix an toàn nhỏ nhất là ngừng dựng map. Scan hàng DB thẳng vào struct typed và encode slice đó. Không thay trường trả về: cùng trường, cùng response shape, cùng status code. Sau khi rollout thay đổi trên một instance, cấp phát ở đường JSON giảm, thời gian GC giảm, và độ trễ ổn định.
Chỉ sau đó rollout dần trong khi theo dõi memory, GC và lỗi. Nếu bạn xây dịch vụ trên nền tảng no-code như AppMaster, điều này nhắc rằng giữ model phản hồi typed và nhất quán giúp tránh chi phí cấp phát ẩn.
Bước tiếp theo để tránh spike bộ nhớ tiếp theo
Khi bạn đã ổn định một spike, hãy làm cho lần sau nhàm chán. Xem profiling như một drill lặp lại.
Viết runbook ngắn cho team để họ làm theo khi mệt: nên chụp gì, khi nào chụp, và cách so sánh với baseline biết là tốt. Giữ nó thực tế: lệnh chính xác, nơi lưu profile, và thế nào là “bình thường” cho các allocator hàng đầu của bạn.
Thêm monitoring nhẹ cho áp lực cấp phát trước khi OOM: kích thước heap, số GC trên giây, và bytes cấp phát trên mỗi request. Bắt “cấp phát mỗi request tăng 30% tuần qua” thường hữu dụng hơn chờ một cảnh báo bộ nhớ cứng.
Đẩy kiểm tra sớm hơn bằng test tải ngắn trong CI trên endpoint đại diện. Một thay đổi nhỏ trong response có thể nhân đôi cấp phát nếu nó gây copy thêm, và tốt hơn là phát hiện trước khi production gặp phải.
Nếu bạn chạy backend sinh tự động, xuất source và profile theo cách giống nhau. Code sinh vẫn là mã Go, và pprof sẽ chỉ vào hàm và dòng thực.
Nếu yêu cầu thay đổi thường xuyên, AppMaster (appmaster.io) có thể là cách thực tế để tái tạo và sinh backend Go sạch khi app tiến hoá, rồi profile mã xuất ra dưới tải thực trước khi đưa ra môi trường thực tế.
Câu hỏi thường gặp
Một đợt spike thường làm tăng tốc độ cấp phát (allocation rate) hơn bạn nghĩ. Những đối tượng tạm thời nhỏ trên mỗi request sẽ cộng dồn tỉ lệ với RPS, buộc GC chạy nhiều hơn và có thể đẩy RSS lên ngay cả khi heap sống không lớn.
Các số liệu heap phản ánh bộ nhớ do Go quản lý, còn RSS bao gồm nhiều thứ hơn: stack goroutine, metadata của runtime, mapping của hệ điều hành, phân mảnh và các cấp phát ngoài heap (bao gồm một số dùng cgo). Trong spike, RSS và heap có thể dịch chuyển khác nhau; dùng pprof để tìm điểm nóng cấp phát trong mã Go thay vì cố gắng “so khớp” RSS chính xác.
Nếu nghi ngờ giữ vật (retention) — tức là thứ gì đó vẫn còn sống — hãy bắt đầu bằng profile heap. Nếu nghi ngờ churn (nhiều đối tượng ngắn sống) — tức là nhiều cấp phát rồi nhanh chóng bị thu — hãy chụp profile theo hướng allocation như allocs/alloc_space. Trong các spike lưu lượng, churn thường là vấn đề vì nó làm tăng thời gian CPU cho GC và độ trễ đuôi.
Thiết lập an toàn nhất là chạy pprof trên một server admin riêng, bind vào 127.0.0.1 và chỉ cho truy cập nội bộ. Xử lý pprof như giao diện admin vì nó có thể phơi bày thông tin nội bộ của dịch vụ.
Ghi lại một chuỗi ngắn: một profile vài phút trước spike (baseline), một profile trong lúc spike (impact), và một profile sau đó (recovery). So sánh các điểm này giúp phân biệt thay đổi thực sự với hành vi khởi động bình thường.
inuse cho biết những gì đang giữ bộ nhớ tại thời điểm chụp; alloc_space cho biết thứ gì bị cấp phát nhiều theo thời gian. Dùng inuse để tìm rò rỉ hoặc cache dài hạn; dùng alloc_space/alloc_objects để tìm churn gây áp lực GC.
Khi encoding/json chiếm nhiều cấp phát, thường là do hình dạng dữ liệu của bạn, không phải package. Thay map[string]any bằng struct kiểu tĩnh, tránh json.MarshalIndent, và đừng dựng JSON qua các chuỗi tạm sẽ giảm cấp phát ngay lập tức.
Quét hàng (row scanning) nổ bộ nhớ khi bạn scan vào các đích linh hoạt như interface{} hoặc map[string]any, chuyển []byte thành string cho mỗi trường, hoặc lấy quá nhiều hàng/cột. Chọn đúng cột, phân trang, và scan trực tiếp vào struct cụ thể thường là các sửa dụng tác động lớn.
Middleware chạy trên mỗi request, nên những cấp phát nhỏ sẽ cộng dồn trong spike. Ví dụ: format log tạo chuỗi mới, tracing tạo label số lượng lớn, tạo request ID mỗi lần, tạo reader/writer gzip mới, hoặc lưu struct mới vào context đều gây churn ổn định trong profile.
Có. Quy trình dựa trên profile áp dụng cho mọi mã Go, dù là sinh tự động hay viết tay. Nếu bạn xuất source từ AppMaster, vẫn có thể chạy pprof, tìm đường gọi gây cấp phát và điều chỉnh model, handler, hay logic chéo để giảm cấp phát trên mỗi request trước khi ra production.


