GC, memori & GOMAXPROCS di container
Runtime Go tidak tahu batas memori container-mu kecuali diberi tahu. Akibatnya, garbage collector bisa membiarkan heap tumbuh melewati batas cgroup — dan yang terjadi bukan GC, melainkan container yang dibunuh kernel.
Intisari
GOGC(default 100) menentukan seberapa sering GC berjalan: heap tumbuh 100% sebelum siklus berikutnya.GOMEMLIMITadalah batas lunak yang membuat GC bekerja lebih agresif saat mendekatinya — pencegah OOM kill.- Setel
GOMEMLIMITsekitar 75–80% dari batas memori container. - Sejak Go 1.25,
GOMAXPROCSsadar container: ia membaca batas CPU cgroup, bukan jumlah core host. - Go 1.26 memakai Green Tea GC secara bawaan — overhead GC turun sekitar 10–40% pada beban yang banyak mengalokasi.
Bagaimana GC Go memutuskan kapan berjalan
GOGC=100 (default):
Setelah GC, heap hidup = 100 MB
→ GC berikutnya saat heap mencapai 200 MB (tumbuh 100%)
GOGC=50:
→ GC berikutnya saat heap mencapai 150 MB (lebih sering, CPU lebih tinggi,
memori puncak lebih rendah)
GOGC=200:
→ GC berikutnya saat heap mencapai 300 MB (lebih jarang, CPU lebih rendah,
memori puncak lebih tinggi)
GOGC adalah pertukaran CPU melawan memori, dan tidak ada nilai yang benar untuk semua. Kalau
profilmu menunjukkan gcBgMarkWorker memakan porsi besar CPU dan kamu punya memori
lega, naikkan. Kalau memori yang ketat, turunkan. Tapi hampir selalu, mengurangi alokasi lebih baik
daripada menyetel GOGC.
GOMEMLIMIT: pencegah OOM kill
Tanpa GOMEMLIMIT, container 512 MB:
heap hidup 200 MB → GC menunggu sampai 400 MB
+ stack goroutine + buffer + runtime ≈ 520 MB
→ kernel membunuh container (exit 137), tanpa satu pun log dari aplikasimu
Dengan GOMEMLIMIT=400MiB:
saat total mendekati 400 MB, GC berjalan LEBIH AGRESIF
→ memori tertahan di bawah batas
→ aplikasi jadi lebih lambat, tapi TETAP HIDUP
// Bisa disetel dari kode, dihitung dari batas container.
import "runtime/debug"
func setelBatasMemori(batasContainerByte int64) {
debug.SetMemoryLimit(batasContainerByte * 80 / 100)
}
// Atau lewat environment di task definition ECS
"environment": [
{ "name": "GOMEMLIMIT", "value": "400MiB" }
]
// dengan "memory": 512 pada container definition
| Memori container | GOMEMLIMIT |
|---|---|
| 512 MB | 400MiB |
| 1 GB | 800MiB |
| 2 GB | 1600MiB |
| 4 GB | 3GiB |
Sisakan 20–25% untuk hal yang tidak dihitung heap Go: stack goroutine, memori runtime,
buffer jaringan, dan (kalau ada) alokasi cgo. Menyetel GOMEMLIMIT sama dengan batas
container justru mengembalikan risiko OOM.
Dan ia batas lunak. Kalau memori yang benar-benar hidup melebihi batas itu, GC akan berjalan terus-menerus (GC thrashing) dan aplikasimu jadi sangat lambat alih-alih mati. Itu biasanya lebih baik — tapi ia berarti alarm memori tetap dibutuhkan.
GOMAXPROCS dan container
Sebelum Go 1.25, Fargate task dengan 0,5 vCPU di host 64-core:
GOMAXPROCS = 64
→ runtime menjadwalkan 64 goroutine berjalan bersamaan
→ cgroup hanya memberi 0,5 CPU
→ CPU throttling parah, latensi ekor melonjak
Sejak Go 1.25:
GOMAXPROCS dibaca dari batas CPU cgroup → 1
→ penjadwalan sesuai kapasitas sungguhan
Kalau kamu masih di Go 1.24 atau lebih lama, ini salah satu perbaikan paling berdampak yang bisa
kamu lakukan — dan alasan kuat untuk naik versi. Untuk versi lama, pustaka automaxprocs
melakukan hal yang sama; sejak 1.25 ia tidak diperlukan lagi.
// Memeriksa nilai efektifnya saat startup
log.Info("runtime",
"gomaxprocs", runtime.GOMAXPROCS(0),
"num_cpu", runtime.NumCPU(),
"gomemlimit", debug.SetMemoryLimit(-1), // -1 = hanya membaca
"gogc", os.Getenv("GOGC"))
Green Tea GC (Go 1.26)
Go 1.26 menjadikan Green Tea sebagai garbage collector bawaan, setelah setahun tersedia sebagai eksperimen. Ia meningkatkan lokalitas saat menandai objek kecil dan memakai instruksi vektor pada CPU amd64 yang lebih baru.
| Dampak | Besarnya |
|---|---|
| Penurunan overhead GC | 10–40% pada program yang banyak mengalokasi |
| Tambahan pada CPU baru (Ice Lake / Zen 4+) | Sekitar 10% lagi |
| Perubahan kode yang dibutuhkan | Tidak ada |
| Cara menonaktifkan | GOEXPERIMENT=nogreenteagc (akan dihapus di 1.27) |
Memantau memori
go func() {
for range time.Tick(30 * time.Second) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
metrik.Gauge("go.heap_alloc_bytes", float64(m.HeapAlloc))
metrik.Gauge("go.heap_sys_bytes", float64(m.HeapSys))
metrik.Gauge("go.gc_pause_total_ns", float64(m.PauseTotalNs))
metrik.Gauge("go.num_gc", float64(m.NumGC))
metrik.Gauge("go.goroutines", float64(runtime.NumGoroutine()))
}
}()
| Metrik | Alarm kalau |
|---|---|
HeapAlloc | Naik terus tanpa pernah turun → kebocoran |
NumGoroutine | Naik terus tanpa kenaikan trafik → kebocoran goroutine |
| Frekuensi GC | Puluhan kali per detik → tekanan alokasi |
| Pemakaian memori container | > 85% dari batas |
| CPU throttling cgroup | > 0 → task kekurangan CPU |
Urutan yang benar saat memori bermasalah
- Ambil profil heap — apa yang menahan memori sekarang?
- Bandingkan dua profil berselang waktu — yang tumbuh itu kebocorannya.
- Cek profil goroutine — goroutine bocor menahan seluruh stack-nya.
- Kurangi alokasi di jalur terpanas (Fase 8).
- Setel
GOMEMLIMITsupaya lonjakan tidak jadi OOM kill. - Baru pertimbangkan menaikkan memori task.
Penyebab kebocoran memori paling umum di aplikasi Go — dan semuanya sudah kita bahas:
goroutine yang menunggu selamanya (Fase 2), rows/resp.Body yang tidak ditutup
(Fase 3–4), map cache tanpa batas dan tanpa pembersihan (Fase 6), dan slice besar yang tetap hidup
karena satu potongan kecilnya masih dirujuk (Fase 0).
Latihan: jalankan aplikasimu di container dengan --memory=256m tanpa
GOMEMLIMIT, bebani sampai mati, dan periksa exit code-nya (137 = OOM kill). Lalu setel
GOMEMLIMIT=200MiB dan ulangi — buktikan ia melambat tapi bertahan.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.