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.

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 GBPuncak bersama 12 containerTerhadap kapasitasKeadaan
Pagi santai3,1 GB39%semua lapang
Jam sibuk siang6,8 GB85%aman, tapi mepet
Flash sale + backup mingguan + rekap9,4 GB118%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:

bash
$ free -m

Output yang Dihasilkan:

text
               total        used        free      shared  buff/cache   available
Mem:           7846        6912         112         201         822         564
Swap:          2047        1983          64

Mari kita telaah dua baris yang menceritakan kegentingan:

  1. available tinggal 564 MB dari 7846 MB — memori yang masih bisa dijanjikan tanpa harus menukar halaman; angka ini, bukan kolom free, yang patut dipercaya tentang ruang hidup host.
  2. 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:

bash
$ docker compose exec db cat /proc/1/oom_score

Output yang Dihasilkan:

text
218

Nilai 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:

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

bash
$ docker stats --no-stream

Output yang Dihasilkan (dipotong):

text
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:

  1. worker-gambar-1 membaca 386.2% — hampir empat core penuh tanpa satu pembatas cpus; sementara itu BLOCK I/O pencernaannya sudah 1,1 GB baca, dan antrean disk itulah yang ikut menghambat db.
  2. backend menempel 96.3% dan db 96.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.
  3. proxy tampil 12.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:

bash
$ docker compose exec backend cat /sys/fs/cgroup/memory.pressure

Output yang Dihasilkan:

text
some avg10=41.20 avg60=12.75 avg300=2.40 total=918273645
full avg10=6.10 avg60=1.02 avg300=0.18 total=132845612

Mari kita telaah formatnya yang padat itu:

  1. 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 membaca avg10=0.00.
  2. avg60 dan avg300 — jendela yang lebih panjang untuk membedakan kegentingan sesaat dari tekanan yang menetap: avg10 tinggi dengan avg300 rendah adalah dengusan, ketiganya tinggi adalah penyakit.
  3. full — porsi waktu ketika seluruh task dalam cgroup tertahan bersamaan, keadaan yang lebih parah daripada sebagian; total di akhir baris adalah akumulasi mikrodetik sejak container menyala.
  4. cpu.pressure dan io.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 overcommitIndikator khasTindakan pertama
MemoriMEM % banyak container di atas 90% bersamaan; swap host membengkak; memory.events oom menanjakturunkan puncak: pecah pekerjaan besar, rebalans limit, atau tambah RAM
CPUlatensi naik merata di semua service; nr_throttled menanjaktenang dulu — throttle tidak mematikan; bagi core lewat cpuset atau tambah vCPU
Disk I/Oio.pressure tinggi; BLOCK I/O satu tetangga melonjakpasang 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:

CPUMemori
Sifat waktunyabisa ditunda — penjadwal mengiris waktutidak bisa ditunda — halaman harus ada saat diminta
Saat overcommit beratcontainer di-throttle, latensi naik merataswap storm lalu OOM killer memilih korban
Aturan praktisboleh agresif — 2× kapasitas masih wajar bila beban tidak bersamaanjaga 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 #