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.

Ảnh chụp đoạn mã Go nền tối minh hoạ GC pacing GOGC điều khiển khi nào GC chạy, GC không chạy theo đồng hồ mà theo mức tăng trưởng heap GOGC quyết định heap được phép phình bao nhiêu so với phần sống trước khi kích hoạt chu kỳ GC tiếp theo, ngưỡng GC lần sau bằng heap sống nhân 1 cộng GOGC chia 100, GOGC 100 mặc định GC khi heap 2 lần phần sống tăng gấp đôi, GOGC 50 GC sớm hơn 1.5 lần sống GC dày ít bộ nhớ tốn CPU, GOGC 200 GC muộn hơn 3 lần sống GC thưa nhiều bộ nhớ đỡ CPU, GOGC off tắt hẳn GC không gom rác bộ nhớ tăng vô hạn, chỉnh bằng biến môi trường GOGC 200 chuong trinh hoặc trong code import runtime debug debug SetGCPercent 200, workload đo cấp 64KB rồi bỏ liên tục tạo rác đều 3000 vòng giữ 20 buffer sống bỏ phần dư, cơ chế sau mỗi lần GC runtime biết heap sống đặt ngưỡng lần sau bằng sống nhân 1 cộng GOGC chia 100 khi heap chạm ngưỡng GC chạy GOGC càng cao heap phình càng nhiều trước khi gom

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ớ):

Ảnh chụp bảng kết quả đo thật nền tối cùng workload GOGC khác nhau Go 1.23 arm64, 3000 vòng cấp 64KB giữ 20 buffer sống bỏ phần dư đo số lần GC và đỉnh heap, GOGC 50 số lần GC 148 đỉnh heap 4 MB tổng STW 6.76 ms GC dày ít bộ nhớ, GOGC 100 mặc định 68 lần 8 MB 3.19 ms, GOGC 200 33 lần 10 MB 1.06 ms, GOGC 400 13 lần 15 MB 0.57 ms GC thưa nhiều bộ nhớ, GOGC off 0 lần 187 MB 0.00 ms không GC nguy hiểm, cốt lõi GOGC là núm đánh đổi thẳng giữa CPU và bộ nhớ từ 50 lên 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 tắt hẳn GC 0 lần gom nhưng heap vọt lên 187 MB giữ mọi rác

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ề

  1. 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.
  2. 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.
  3. 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.

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.