Bài GOGC kết thúc với một vấn đề chưa giải: GOGC điều tiết GC theo tăng trưởng heap, nhưng hoàn toàn mù về trần RAM thực tế. Đặt GOGC cao để đỡ CPU thì có nguy cơ heap phình vượt RAM container và bị OOM kill. Go 1.19 giải quyết bằng GOMEMLIMIT — một trần bộ nhớ mềm. Bài này đo cách nó phối hợp với GOGC để đạt điều tưởng như mâu thuẫn: GC chạy ít nhất có thể mà vẫn không bao giờ tràn giới hạn.

Hai công cụ, hai vai trò

GOGC và GOMEMLIMIT không thay thế nhau — chúng phối hợp:

  • GOGC: điều tiết bình thường. GC chạy khi heap tăng gấp GOGC% so với phần sống (bài trước).
  • GOMEMLIMIT: một trần mềm cho tổng bộ nhớ Go. Khi bộ nhớ tiến gần trần, GC chạy gắt hơn (bất kể GOGC) để kéo bộ nhớ xuống.

Cơ chế phối hợp: cái nào kích hoạt GC trước thì thắng. Nếu heap tăng gấp đôi trước khi chạm gần trần → GOGC kích hoạt. Nếu bộ nhớ gần chạm trần trước → GOMEMLIMIT kích hoạt. Điều này mở ra một mẫu rất mạnh: GOGC=off + GOMEMLIMIT — tắt điều tiết theo tăng trưởng, chỉ để GC chạy khi cần để giữ dưới trần.

Ảnh chụp đoạn mã Go nền tối minh hoạ GOMEMLIMIT trần bộ nhớ mềm cho GC, GOGC điều tiết theo tăng trưởng nhưng không biết trần RAM GOMEMLIMIT đặt một mức bộ nhớ mềm khi tổng bộ nhớ Go tiến gần mức đó GC chạy gắt hơn để giữ dưới trần, GOGC bằng điều tiết bình thường heap tăng gấp GOGC phần trăm thì GC, GOMEMLIMIT bằng trần mềm GC chạy gắt khi bộ nhớ gần chạm bất kể GOGC, hai cái phối hợp cái nào kích hoạt GC trước thì thắng, mẫu hiện đại GOGC off cộng GOMEMLIMIT bằng trần GC lười ít lần khi bộ nhớ còn xa trần chỉ gắt khi gần chạm trần, đặt bằng biến môi trường đơn vị B KiB MiB GiB GOMEMLIMIT 50MiB GOGC off chuong trinh import runtime debug debug SetMemoryLimit 50 dịch trái 20 50 MiB tính bằng byte, vì sao là trần mềm Go cố giữ dưới GOMEMLIMIT bằng cách chạy GC gắt hơn nhưng không bao giờ giết chương trình để tuân trần nếu bộ nhớ sống thật sự vượt trần nó vẫn cấp phát thà chậm vì GC liên tục còn hơn crash khác với giới hạn cứng của container chạm là bị OOM kill

Hình 1: GOGC điều tiết bình thường, GOMEMLIMIT là trần mềm. Mẫu GOGC=off + GOMEMLIMIT để GC lười khi bộ nhớ còn dư, chỉ gắt khi gần trần.

Đo thật: cùng workload, thêm trần

Chạy đúng workload của bài GOGC (từng vọt 187 MB với GOGC=off), giờ thêm GOMEMLIMIT:

Ảnh chụp bảng kết quả đo thật nền tối GOMEMLIMIT chặn trần Go 1.23 arm64, cùng workload bài GOGC 3000 vòng cấp 64KB giữ 20 buffer sống bỏ phần dư, GOGC off không giới hạn 0 lần GC đỉnh heap 187 MB không GC tràn vô hạn, GOGC off GOMEMLIMIT 50MiB 4 lần GC 43 MB GC ít nhưng giữ dưới 50, GOGC off GOMEMLIMIT 20MiB 15 lần GC 16 MB GC gắt hơn giữ dưới 20, GOGC 100 không giới hạn 73 lần GC 6 MB điều tiết mặc định GC dày, cốt lõi GOMEMLIMIT giải đúng điểm yếu của GOGC mẫu GOGC off cộng GOMEMLIMIT 50MiB cho kết quả tuyệt chỉ 4 lần GC so với 73 lần của GOGC 100 mà vẫn giữ heap dưới 43 MB GC lười khi bộ nhớ còn dư chỉ gắt khi gần trần đặt GOMEMLIMIT khoảng 90 tới 95 phần trăm giới hạn RAM container

Hình 2: GOGC=off không trần → 187 MB. GOGC=off + GOMEMLIMIT=50MiB → chỉ 4 lần GC mà giữ heap dưới 43 MB. GOMEMLIMIT=20MiB → 15 lần GC, dưới 16 MB. So với GOGC=100 → 73 lần GC.

Con số cho thấy sức mạnh của GOMEMLIMIT:

  • GOGC=off, không trần: 0 lần GC, heap vọt 187 MB — không kiểm soát.
  • GOGC=off + GOMEMLIMIT=50MiB: chỉ 4 lần GC, heap giữ dưới 43 MB. GC hầu như không chạy khi bộ nhớ còn xa trần, chỉ can thiệp khi gần chạm.
  • GOGC=off + GOMEMLIMIT=20MiB: 15 lần GC, dưới 16 MB. Trần thấp hơn → GC phải chạy gắt hơn.
  • GOGC=100 (để so sánh): 73 lần GC, heap 6 MB.

Điểm mấu chốt: mẫu GOGC=off + GOMEMLIMIT=50MiB cho 4 lần GC so với 73 lần của GOGC=100 — ít hơn ~18 lần — mà vẫn tôn trọng trần bộ nhớ. Đây là điều GOGC một mình không làm được: GC lười tối đa (đỡ CPU) nhưng không bao giờ tràn (an toàn RAM).

Vì sao là trần "mềm"

Từ "mềm" rất quan trọng. Go cố giữ tổng bộ nhớ dưới GOMEMLIMIT bằng cách chạy GC gắt hơn, nhưng không bao giờ giết chương trình để tuân trần. Nếu lượng bộ nhớ thật sự sống (không phải rác) vượt trần, Go vẫn cấp phát tiếp — thà chậm vì GC chạy liên tục còn hơn crash. Đây là khác biệt cốt lõi với giới hạn cứng của container (chạm là bị OOM kill ngay). GOMEMLIMIT giúp tránh chạm giới hạn cứng đó, chứ không thay thế nó.

Ứng dụng thực tế

Đặt GOMEMLIMIT ≈ 90-95% giới hạn RAM container. Đây là cách hiện đại nhất chạy dịch vụ Go trong Kubernetes/Docker. Để lại 5-10% cho bộ nhớ ngoài heap Go (stack OS, bộ đệm kernel). Ví dụ container giới hạn 512Mi → GOMEMLIMIT=460MiB. Go tự điều tiết GC để không chạm 460, tránh OOM kill.

Kết hợp GOGC=off + GOMEMLIMIT cho dịch vụ ổn định. Với dịch vụ có lượng bộ nhớ sống tương đối ổn định, mẫu này cho GC chạy tối thiểu (đỡ CPU) mà vẫn an toàn. Nhưng nếu lượng sống dao động mạnh, giữ GOGC ở giá trị vừa phải (như 50-100) cùng GOMEMLIMIT an toàn hơn.

Đặt trong code khi cần đọc giới hạn động. debug.SetMemoryLimit(n) (byte) cho phép đọc giới hạn container lúc chạy (từ cgroup) rồi đặt tương ứng — hữu ích khi không control được biến môi trường. Thư viện như automemlimit tự làm việc này.

Đánh đổi cần cân nhắc

GOMEMLIMIT quá thấp gây "GC thrashing". Nếu đặt trần sát hoặc dưới lượng bộ nhớ sống thật, Go sẽ chạy GC liên tục để cố kéo xuống dưới trần bất khả thi — CPU vọt lên, chương trình chậm thảm hại (nhưng không crash). Đây là bẫy: trần quá chặt biến vấn đề bộ nhớ thành vấn đề CPU. Đặt trần cao hơn lượng sống dự kiến một khoảng an toàn.

Trần mềm không cứu được rò bộ nhớ thật. Nếu chương trình rò bộ nhớ (giữ tham chiếu không thả), lượng sống tăng mãi, cuối cùng vượt GOMEMLIMIT và Go rơi vào thrashing rồi vẫn có thể OOM. GOMEMLIMIT là công cụ điều tiết, không phải bản vá cho bug bộ nhớ — vẫn phải tìm và sửa rò (bằng pprof, bài sau).

Cần đo lượng sống thật để đặt đúng. Đặt GOMEMLIMIT bừa có thể phản tác dụng (quá thấp → thrash, quá cao → vô dụng). Dùng gctrace hoặc metrics để biết heap sống điển hình và đỉnh, rồi đặt trần trên đỉnh một khoảng.

Ba ý mang về

  1. GOMEMLIMIT là trần bộ nhớ mềm phối hợp với GOGC: GOGC điều tiết bình thường, GOMEMLIMIT làm GC chạy gắt khi bộ nhớ gần trần — cái nào kích hoạt GC trước thì thắng.
  2. Mẫu GOGC=off + GOMEMLIMIT là hiện đại nhất cho container: đo thật, GOGC=off + GOMEMLIMIT=50MiB chỉ chạy 4 lần GC (so với 73 lần của GOGC=100) mà vẫn giữ heap dưới 43 MB — GC lười tối đa nhưng không tràn trần.
  3. Trần MỀM, không thay giới hạn cứng: Go không giết chương trình để tuân trần (thà thrash còn hơn crash), nên đặt GOMEMLIMIT ≈ 90-95% RAM container để tránh OOM — nhưng trần quá thấp gây GC thrashing, và không cứu được rò bộ nhớ thật.

Phần sau ta quay lại một cơ chế GC đã nhắc mà chưa đào sâu: Phần sau mổ xẻ write barrier — vì sao GC chạy đồng thời bắt buộc phải có nó, nó hoạt động thế nào ở mức con trỏ, và chi phí thật của nó.