Bài đọc gctrace nhắc tới con số goal — ngưỡng heap kích hoạt GC lần sau. Con số đó không phải cố định: nó do một biến duy nhất điều khiển, GOGC, và biến này là núm đánh đổi trực tiếp giữa CPU và bộ nhớ của toàn bộ chương trình Go. Hiểu nó là hiểu cách "điều tiết" GC. Bài này đo cùng một workload với các giá trị GOGC khác nhau và cho thấy đánh đổi rõ như ban ngày.
GC pacing: chạy theo tăng trưởng, không theo đồng hồ
Điểm mấu chốt: GC của Go không chạy theo lịch thời gian (mỗi X giây) mà theo mức tăng trưởng heap. Sau mỗi chu kỳ GC, runtime biết heap sống là bao nhiêu, rồi đặt ngưỡng cho lần sau theo công thức:
Ngưỡng GC lần sau = heap_sống × (1 + GOGC/100)
Với GOGC=100 (mặc định), ngưỡng = 2× heap sống — GC chạy khi heap tăng gấp đôi so với phần sống. Khi heap cấp phát chạm ngưỡng, GC chạy, dọn rác, tính lại heap sống, đặt ngưỡng mới. Đây gọi là GC pacing — điều tiết nhịp GC theo lượng rác sinh ra.

Hình 1: Công thức pacing. GOGC càng cao, heap được phình càng nhiều trước khi GC chạy — đổi bộ nhớ lấy ít lần GC hơn. Chỉnh bằng biến GOGC hoặc debug.SetGCPercent.
Đo thật: đánh đổi CPU và bộ nhớ
Chạy đúng một workload (cấp 64KB rồi bỏ liên tục, 3000 vòng, giữ 20 buffer sống) với các giá trị GOGC khác nhau, đo số lần GC (đại diện chi phí CPU) và đỉnh heap (bộ nhớ):

Hình 2: Từ GOGC=50 tới 400, số lần GC giảm từ 148 xuống 13 (đỡ CPU) nhưng đỉnh heap tăng từ 4 lên 15 MB. GOGC=off: 0 lần GC nhưng heap vọt lên 187 MB.
Con số vẽ ra đường đánh đổi hoàn hảo:
- GOGC=50: 148 lần GC, đỉnh heap 4 MB, STW 6,76 ms. GC chạy sớm (khi heap = 1,5× sống) → dày, tốn CPU, nhưng giữ bộ nhớ thấp.
- GOGC=100 (mặc định): 68 lần GC, 8 MB, 3,19 ms.
- GOGC=200: 33 lần GC, 10 MB, 1,06 ms.
- GOGC=400: 13 lần GC, đỉnh heap 15 MB, STW 0,57 ms. GC chạy muộn (5× sống) → thưa, đỡ CPU, nhưng tốn bộ nhớ.
- GOGC=off: 0 lần GC, heap vọt lên 187 MB. Tắt hẳn gom rác — giữ mọi thứ, bộ nhớ tăng không giới hạn.
Đường đánh đổi rất sạch: tăng GOGC gấp 8 lần (50→400) làm số lần GC giảm ~11 lần (148→13) nhưng đỉnh heap tăng ~4 lần (4→15 MB). Bạn đang mua ít CPU-cho-GC hơn bằng nhiều bộ nhớ hơn.
Ứng dụng thực tế
Tăng GOGC khi có RAM dư và GC ngốn CPU. Nếu dịch vụ của bạn có nhiều RAM trống và gctrace cho thấy %CPU-cho-GC cao, tăng GOGC (ví dụ 200-400) giảm số lần GC, trả CPU về cho công việc thật. Nhiều dịch vụ throughput cao chạy GOGC=200-400.
Giảm GOGC khi RAM eo hẹp. Trong container giới hạn bộ nhớ chặt, GOGC thấp (50-75) giữ heap nhỏ hơn, tránh chạm giới hạn — đổi lại GC chạy dày hơn. Nhưng cách hiện đại hơn là dùng GOMEMLIMIT (bài sau) thay vì hạ GOGC.
Chỉnh trong code với debug.SetGCPercent. Nếu cần đổi GOGC theo pha chương trình (ví dụ tắt GC trong lúc khởi tạo dữ liệu lớn rồi bật lại), debug.SetGCPercent(n) làm được lúc chạy. Trả về giá trị cũ để khôi phục.
Đánh đổi cần cân nhắc
GOGC=off cực kỳ nguy hiểm trên dịch vụ dài hạn. Đo cho thấy heap vọt lên 187 MB chỉ trong một workload ngắn. Tắt GC trên dịch vụ chạy lâu sẽ dẫn tới hết bộ nhớ (OOM) chắc chắn. Chỉ dùng GOGC=off cho công cụ chạy một lần, ngắn, biết trước lượng cấp phát (như trình biên dịch batch).
Không có con số GOGC "đúng" chung. Đánh đổi phụ thuộc hoàn toàn workload: tỉ lệ rác/sống, mẫu cấp phát, RAM khả dụng, yêu cầu độ trễ. Con số trong bài (4-15 MB) là của workload cụ thể này; workload của bạn khác. Phải đo trên chính chương trình và tải thật để chọn.
Đỉnh heap không chỉ do GOGC. Cấp phát đột biến (spike) có thể đẩy heap vượt ngưỡng goal trước khi GC kịp phản ứng, đặc biệt với GOGC cao. Nếu độ trễ và bộ nhớ đều quan trọng, GOGC một mình không đủ — cần kết hợp với GOMEMLIMIT để đặt trần cứng.
Ba ý mang về
- GC chạy theo tăng trưởng heap, không theo đồng hồ: GOGC đặt ngưỡng GC lần sau = heap_sống × (1 + GOGC/100), mặc định GOGC=100 nghĩa là GC khi heap tăng gấp đôi phần sống.
- GOGC là núm đánh đổi thẳng CPU ↔ bộ nhớ: đo thật, từ GOGC=50 tới 400, số lần GC giảm từ 148 xuống 13 (đỡ CPU) nhưng đỉnh heap tăng từ 4 lên 15 MB — tăng GOGC khi có RAM dư và GC ngốn CPU, giảm khi RAM eo hẹp.
- GOGC=off tắt hẳn GC, chỉ hợp công cụ ngắn hạn: đo thật heap vọt 187 MB không gom — trên dịch vụ dài hạn sẽ OOM; và không có con số GOGC đúng chung, phải đo trên workload thật.
Bài tập làm thử
Bài 1 (đọc hiểu công thức). Áp dụng công thức pacing Ngưỡng GC lần sau = heap_sống × (1 + GOGC/100). Nếu heap sống hiện tại là 50 MB và GOGC=200, ngưỡng kích hoạt GC lần sau là bao nhiêu?
Đáp án
Ngưỡng = 50 × (1 + 200/100) = 50 × 3 = 150 MB. Với GOGC=200, GC chạy khi heap tăng gấp 3 lần so với phần sống.
Bài 2 (đọc hiểu số liệu). Đo thật cho thấy GOGC=50 chạy 148 lần GC với đỉnh heap 4 MB, còn GOGC=400 chỉ chạy 13 lần GC nhưng đỉnh heap 15 MB. Giải thích vì sao tăng GOGC lại làm giảm số lần GC nhưng tăng đỉnh heap.
Đáp án
GOGC quyết định heap được phép phình bao nhiêu so với phần sống trước khi GC kích hoạt. GOGC=50 nghĩa là ngưỡng chỉ 1,5× heap sống nên GC chạy sớm và dày (nhiều lần, heap luôn nhỏ). GOGC=400 nghĩa là ngưỡng lên tới 5× heap sống nên GC chờ heap phình rất nhiều mới chạy — ít lần GC hơn (đỡ CPU) nhưng heap được phép lớn hơn nhiều trước khi bị dọn.
Bài 3 (sửa lỗi/vận dụng code). Đoạn code sau muốn tắt tạm GC trong lúc nạp một khối dữ liệu lớn lúc khởi động rồi bật lại đúng giá trị cũ, nhưng có lỗi khiến giá trị cũ bị mất. Sửa lại cho đúng.
import "runtime/debug"
func loadBigData() {
debug.SetGCPercent(-1) // tắt GC
doHeavyAllocation()
debug.SetGCPercent(100) // bật lại — nhưng lỡ trước đó GOGC=200 thì sao?
}
Đáp án
debug.SetGCPercent trả về giá trị GOGC cũ, nên phải lưu lại giá trị đó và khôi phục thay vì hard-code 100:
func loadBigData() {
old := debug.SetGCPercent(-1) // tắt GC, nhớ giá trị cũ
doHeavyAllocation()
debug.SetGCPercent(old) // khôi phục đúng giá trị trước đó
}
Bài 4 (bẫy/đánh đổi). Một dịch vụ chạy dài hạn (24/7) đang gặp GC ngốn nhiều CPU. Một kỹ sư đề xuất set GOGC=off để loại bỏ hoàn toàn chi phí GC. Đề xuất này có vấn đề gì?
Đáp án
GOGC=off tắt hẳn việc gom rác — mọi object không được thu hồi nữa. Đo thật trong bài cho thấy chỉ với một workload ngắn, heap đã vọt lên 187 MB. Trên dịch vụ chạy dài hạn, bộ nhớ sẽ tăng không giới hạn và chắc chắn dẫn tới hết bộ nhớ (OOM). GOGC=off chỉ nên dùng cho công cụ chạy một lần, ngắn, biết trước lượng cấp phát (như một trình biên dịch batch) — không hợp cho service dài hạn.
Bài 5 (vận dụng thực tế). Dịch vụ của bạn chạy trong container có nhiều RAM dư nhưng gctrace cho thấy %CPU dành cho GC khá cao. Theo bài viết, bạn nên điều chỉnh GOGC theo hướng nào, và tại sao không nên dùng GOGC thấp khi RAM đang eo hẹp mà nên cân nhắc công cụ khác?
Đáp án
Khi có RAM dư và GC ngốn CPU, nên tăng GOGC (ví dụ 200-400) để giảm số lần GC, trả CPU về cho công việc thật — đổi lại heap sẽ phình lớn hơn, nhưng đó là đánh đổi chấp nhận được khi có RAM dư. Ngược lại khi RAM eo hẹp, tuy có thể giảm GOGC (50-75) để giữ heap nhỏ hơn, bài viết lưu ý cách hiện đại hơn là dùng GOMEMLIMIT để đặt trần cứng cho bộ nhớ thay vì chỉ hạ GOGC — vì GOGC không đặt giới hạn tuyệt đối, cấp phát đột biến vẫn có thể đẩy heap vượt ngưỡng trước khi GC kịp phản ứng.
Phần sau ta xét công cụ bổ trợ giải quyết đúng điểm yếu của GOGC — đặt trần bộ nhớ: Phần sau mổ xẻ GOMEMLIMIT — giới hạn bộ nhớ mềm, cách nó phối hợp với GOGC để vừa đỡ CPU vừa không tràn RAM container.