Penjadwal Swarm menutup chapter sebelumnya dengan disiplin seorang akuntan: reservasi dijumlahkan, node yang penuh ditolak, dan pembukuan selalu seimbang. Tetapi ada satu perhitungan yang tidak pernah dilakukan siapa pun — penjadwal tidak menghitung limits, cgroups hanya mengurus satu container pada satu waktu, dan tidak ada daemon yang memergoki total anggaran seluruh container melebihi RAM host. Di server staging toko keadaannya memang begitu: dua belas container berjalan di mesin 8 GB — enam service inti, worker-gambar, adminer, tiga stack percobaan milik Budi dan Siti, dan satu container alat diagnosa yang lupa dimatikan — semuanya memakai aturan seragam mem_limit: 1g. Dua belas gigabyte komitmen di atas delapan gigabyte kenyataan, dan tidak ada yang salah sedikit pun. Keadaan itu punya nama — overcommit — dan ia legal, umum, justru menjadi alasan container terasa murah; yang perlu kita kuasai adalah mengetahui kapan ia aman dan mengenali saat ia mulai menyengat.
Overcommit Itu Legal #
Alasan overcommit boleh dilakukan sama dengan alasan maskapai menjual kursi lebih banyak daripada jumlah bangku di pesawat: sebagian penumpang tidak akan datang. Container tidak bekerja penuh pada saat yang sama — frontend dan backend sibuk di jam makan siang, backup mingguan menyala pukul tiga pagi saat pengguna tidur, rekap bulanan hanya datang di awal bulan. Komitmen 12 GB pada host 8 GB berbahaya hanya bila puncak-puncak itu berteriak bersamaan:
| Skenario di staging 8 GB | Puncak bersama 12 container | Terhadap kapasitas | Keadaan |
|---|---|---|---|
| Pagi santai | 3,1 GB | 39% | semua lapang |
| Jam sibuk siang | 6,8 GB | 85% | aman, tapi mepet |
| Flash sale + backup mingguan + rekap | 9,4 GB | 118% | swap storm lalu OOM |
Batasan analogi maskapainya perlu dinyatakan tegas: maskapai bisa menolak Anda di pintu dan menjelaskan dengan sopan, sedangkan kernel tidak punya pintu — keputusannya jatuh tanpa peringatan dan tanpa ampun. Baris ketiga tabel itulah yang harus kita pahami sampai ke mekanismenya, karena baris itulah yang terjadi pada sore flash sale toko.
Saat Overcommit Menyengat #
Yang membedakan overcommit sehat dari yang menyengat adalah jenis resource yang kehabisan terlebih dulu. CPU dan memori sama-sama bisa dituntut melebihi kapasitas, tetapi cara kernel menanganinya berbeda total — dan asimetri itu menentukan seluruh strategi kita.
Memori: Swap Storm dan OOM Killer #
Pemakaian total menanjak melewati RAM, dan tindakan pertama kernel bukan membunuh siapa pun melainkan menunda: halaman-halaman yang jarang disentuh dipindahkan ke swap di disk. Host tetap hidup, tetapi setiap halaman yang dipindahkan harus dibaca kembali dari disk begitu pemiliknya menyentuhnya — dan disk ratusan kali lebih lambat dari RAM. Seluruh service melambat serentak: load average memanjak, kolom si/so di vmstat berdenyut, dan pelayanan yang biasanya 40 milidetik dijawab 400 milidetik. Inilah swap storm, dan kondisi host saat kegentingan itu terbaca dari alat paling sederhana:
$ free -mOutput yang Dihasilkan:
total used free shared buff/cache available
Mem: 7846 6912 112 201 822 564
Swap: 2047 1983 64Mari kita telaah dua baris yang menceritakan kegentingan:
availabletinggal 564 MB dari 7846 MB — memori yang masih bisa dijanjikan tanpa harus menukar halaman; angka ini, bukan kolomfree, yang patut dipercaya tentang ruang hidup host.- Swap terisi 1983 dari 2047 MB — cadangan penundaan tinggal 64 MB; beberapa menit setelah potret ini diambil, swap pun penuh dan kernel beralih ke tindakan terakhirnya.
Saat swap habis, OOM killer turun tangan — dan cara kernel memilih korbannya sudah kita intip sejak Chapter Resource Limit: setiap proses dinilai lewat oom_score, dihitung terutama dari RSS (memori yang benar-benar dipakai) plus swap, lalu disetel dengan oom_score_adj bila seseorang menandai proses itu penting atau rela berkorban. Skor tertinggi menjadi kandidat pertama. Saat kegentingan berlangsung, Siti sempat mencatat skor para kandidat dari dalam container:
$ docker compose exec db cat /proc/1/oom_scoreOutput yang Dihasilkan:
218Nilai 218 pada skala 0–1000 menjadikan postmaster PostgreSQL kandidat terdepan — RSS-nya memang yang terbesar di host. Dan itulah yang terjadi: db tumbang dengan Exited (137) dan .State.OOMKilled bernilai true, padahal pemakaian db saat itu baru sekitar 0,97 GB — masih di bawah limit cgroup-nya sendiri yang 1 GB. Kernel membunuh db bukan karena menabrak langit-langit pribadinya, melainkan karena host kehabisan dan skor db paling tinggi; dua penegak yang berbeda, satu korban.
CPU: Throttle yang Merata #
Overcommit CPU berujung jauh lebih sopan. Ketika jumlah kuota cpus seluruh container melebihi jumlah core host dan semuanya bekerja berat di saat bersamaan, penjadwal CFS tidak membunuh apa pun — ia hanya tidak sanggup melunasi seluruh kuota tepat waktu, sehingga tiap container menerima jatahnya terlambat. Gejalanya khas: latensi naik merata di semua service sekaligus, bukan hanya di yang rakus, dan tidak ada satu container pun yang mati. Permintaan yang biasanya dijawab 40 milidetik ditanggapi 90 milidetik oleh seluruh stack dengan tertib yang mencurigakan. Jejak pastinya tercatat di penghitung nr_throttled dalam berkas cpu.stat milik tiap container — angka yang akan kita angkat ke meja pemeriksaan pada Chapter Monitoring Resource.
Noisy Neighbor dari Dekat #
Overcommit adalah masalah jumlah; noisy neighbor adalah masalah satu pihak. Definisinya sudah kita pegang sejak Chapter Resource Limit: satu container yang rakus menekan resource bersama hingga tetangganya menderita. Dan resource bersama itu lebih banyak dari yang terlihat di docker stats — tiga yang paling sering menjadi arena:
- Waktu CPU dan cache prosesor — konteks proses si tetangga menyingkirkan data dari cache L3, sehingga container lain harus mengambil datanya dari RAM yang jauh lebih lambat meski kuota CPU-nya belum tersentuh.
- Antrean disk I/O — satu pekerja yang menulis berkas besar memperlambat seluruh barisan; write checkpoint db yang biasanya selesai 10 milidetik harus mengantre di belakang pembacaan image.
- Bandwidth jaringan — backup yang membanjiri link membuat jawaban API ikut tersendat; alat pembatas dan prioritas trafiknya hidup di luar cakupan Course ini, cukup kita tahu problem ini nyata.
Di staging toko, biang keroknya worker-gambar — service pemroses unggahan gambar produk yang sedang menelen ratusan berkas sekaligus. Aturan seragam staging hanya menyentuh memori, jadi CPU-nya bebas:
$ docker stats --no-streamOutput yang Dihasilkan (dipotong):
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
7389c56ce533 toko-proxy-1 12.41% 6.71MiB / 1GiB 0.65% 18.4MB / 27.1MB 31.2MB / 0B 3
20269502f039 toko-backend-1 97.86% 986.4MiB / 1GiB 96.3% 48.2MB / 61.3MB 214MB / 0B 11
6b2d4f8a1e3c toko-db-1 88.31% 989.7MiB / 1GiB 96.7% 22.7MB / 180MB 612MB / 41.2MB 14
9d4c7e2a1b8f worker-gambar-1 386.2% 701.9MiB / 1GiB 68.5% 1.2MB / 88.4MB 1.1GB / 0B 5
...Mari kita telaah papan skor kegentingan itu:
worker-gambar-1membaca386.2%— hampir empat core penuh tanpa satu pembatascpus; sementara ituBLOCK I/Opencernaannya sudah 1,1 GB baca, dan antrean disk itulah yang ikut menghambat db.backendmenempel96.3%dandb96.7%— bukan karena aplikasinya rakus, melainkan karena core dan antrean disk yang mereka butuhkan sedang dikuasai tetangga; inilah penderitaan noisy neighbor yang tidak pernah meninggalkan jejak crash.proxytampil12.41%— kecil secara angka, tetapi responsnya tersendat karena permintaan harus mengantre di belakang CPU yang habis.
Mitigasinya kembali ke perangkat yang sudah kita pegang: pasang cpus pada si pekerja agar kerakusannya berhenti di angka (atau deploy.resources.limits dalam compose, bentuk yang kita tulis pada Chapter Resource di Compose); sematkan pekerja ke core tertentu lewat cpuset agar empat core sisanya tetap bersih untuk db dan backend — pola isolasi dari Chapter CPU Limit dan Reservation; dan batasi throughput serta IOPS perangkat bloknya dengan flag blkio yang kita bedah pada Chapter IO dan Block Limit. Noisy neighbor tidak diatasi dengan menambah RAM, melainkan dengan pagar yang menunjuk pelaku.
Mendeteksi Lewat Tekanan: PSI #
docker stats menunjukkan angka pemakaian, tetapi angka bukanlah penderitaan — container bisa memakai 60% memori dengan tenang, atau 30% sambil tersiksa karena halaman-halaman terus diambil kembali dari disk. Kernel modern menulis penderitaan itu secara langsung lewat berkas Pressure Stall Information (PSI) di cgroups, dan membacanya semudah cat:
$ docker compose exec backend cat /sys/fs/cgroup/memory.pressureOutput yang Dihasilkan:
some avg10=41.20 avg60=12.75 avg300=2.40 total=918273645
full avg10=6.10 avg60=1.02 avg300=0.18 total=132845612Mari kita telaah formatnya yang padat itu:
some avg10=41.20— selama 41,2% dari jendela 10 detik terakhir, setidaknya satu task di cgroup backend sempat berhenti bekerja karena menunggu memori; di sore yang tenang, berkas yang sama membacaavg10=0.00.avg60danavg300— jendela yang lebih panjang untuk membedakan kegentingan sesaat dari tekanan yang menetap:avg10tinggi denganavg300rendah adalah dengusan, ketiganya tinggi adalah penyakit.full— porsi waktu ketika seluruh task dalam cgroup tertahan bersamaan, keadaan yang lebih parah daripada sebagian;totaldi akhir baris adalah akumulasi mikrodetik sejak container menyala.cpu.pressuredanio.pressure— dua berkas saudaranya memakai format yang sama untuk tekanan CPU dan tekanan I/O; tiga berkas bersama memberi diagnosis pertama yang jujur: sumber derita container ini memori, CPU, atau disk.
Gabungan indikator dari stats, PSI, dan penghitung cgroups kini cukup untuk menyusun satu tabel diagnosis yang layak ditempel di dinding tim:
| Resource yang overcommit | Indikator khas | Tindakan pertama |
|---|---|---|
| Memori | MEM % banyak container di atas 90% bersamaan; swap host membengkak; memory.events oom menanjak | turunkan puncak: pecah pekerjaan besar, rebalans limit, atau tambah RAM |
| CPU | latensi naik merata di semua service; nr_throttled menanjak | tenang dulu — throttle tidak mematikan; bagi core lewat cpuset atau tambah vCPU |
| Disk I/O | io.pressure tinggi; BLOCK I/O satu tetangga melonjak | pasang batas blkio si pelaku; pisahkan volume db ke disk sendiri |
Aturan Praktis: Asimetri CPU dan Memori #
Seluruh gejala di atas bermuara pada satu aturan praktis yang sudah kita bisikkan sejak Chapter Resource Limit: batas CPU membuat aplikasi melambat, batas memori membuat aplikasi mati. CPU bisa ditunda — penjadwal mengiris waktu dan kelebihan permintaan hanya mengantre; memori tidak bisa ditunda — halaman yang diminta aplikasi harus tersedia saat itu juga, atau aplikasinya diakhiri. Dari asimetri itulah dua kebijakan berbeda diturunkan:
| CPU | Memori | |
|---|---|---|
| Sifat waktunya | bisa ditunda — penjadwal mengiris waktu | tidak bisa ditunda — halaman harus ada saat diminta |
| Saat overcommit berat | container di-throttle, latensi naik merata | swap storm lalu OOM killer memilih korban |
| Aturan praktis | boleh agresif — 2× kapasitas masih wajar bila beban tidak bersamaan | jaga puncak bersama di bawah ±80% RAM host |
Semua gejala yang kita bedah di chapter ini — swap storm, korban OOM, throttle merata, tetangga yang rakus — punya satu kesamaan yang tidak nyaman: semuanya paling murah ditangani sebelum menjadi kematian, dan alat untuk itu bukan potret melainkan sejarah. docker stats menjawab “berapa sekarang”; keputusan angka butuh “berapa puncaknya, kapan puncaknya, dan seberapa sering ia kembali”. Chapter Monitoring Resource membangun penglihatan ke belakang waktu itu: membaca berkas cgroups langsung dari sumbernya, merekam tren ke CSV dengan skrip sederhana, dan menetapkan ambang-ambang yang berteriak sebelum kernel sempat bersuara.
Referensi #
- Runtime options with Memory, CPUs, and GPUs — mekanisme OOM killer, reservasi, dan penegakan batas memori.
- docker container stats — potret agregat seluruh container saat kegentingan.
- docker container run
— flag
--oom-score-adjuntuk menimbang kandidat korban OOM.