← Semua pembelajaran / Go Nol → Enterprise
Fase 9 · Performa & Observability

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.

Sumber asli go.dev Resmi Rangkuman ~8 menit baca

Intisari

  • GOGC (default 100) menentukan seberapa sering GC berjalan: heap tumbuh 100% sebelum siklus berikutnya.
  • GOMEMLIMIT adalah batas lunak yang membuat GC bekerja lebih agresif saat mendekatinya — pencegah OOM kill.
  • Setel GOMEMLIMIT sekitar 75–80% dari batas memori container.
  • Sejak Go 1.25, GOMAXPROCS sadar 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 containerGOMEMLIMIT
512 MB400MiB
1 GB800MiB
2 GB1600MiB
4 GB3GiB

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.

DampakBesarnya
Penurunan overhead GC10–40% pada program yang banyak mengalokasi
Tambahan pada CPU baru (Ice Lake / Zen 4+)Sekitar 10% lagi
Perubahan kode yang dibutuhkanTidak ada
Cara menonaktifkanGOEXPERIMENT=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()))
	}
}()
MetrikAlarm kalau
HeapAllocNaik terus tanpa pernah turun → kebocoran
NumGoroutineNaik terus tanpa kenaikan trafik → kebocoran goroutine
Frekuensi GCPuluhan kali per detik → tekanan alokasi
Pemakaian memori container> 85% dari batas
CPU throttling cgroup> 0 → task kekurangan CPU

Urutan yang benar saat memori bermasalah

  1. Ambil profil heap — apa yang menahan memori sekarang?
  2. Bandingkan dua profil berselang waktu — yang tumbuh itu kebocorannya.
  3. Cek profil goroutine — goroutine bocor menahan seluruh stack-nya.
  4. Kurangi alokasi di jalur terpanas (Fase 8).
  5. Setel GOMEMLIMIT supaya lonjakan tidak jadi OOM kill.
  6. 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.