Peta dari Resource Limit kini tergeletak lengkap di hadapan kita: noisy neighbor sebagai musuh, cgroups sebagai penegak, empat flag sebagai perangkat, dan OOM killer sebagai algojo yang menunggu di ujung langit-langit memori. Chapter ini mengubah peta itu menjadi otot — kita akan memasang tiap flag dengan tangan sendiri, membaca jejaknya di docker inspect, memantau dampaknya di docker stats, lalu menutup semuanya dengan satu eksperimen yang paling banyak dicari saat orang belajar resource limit: memprovokasi OOM killer secara sengaja dan menyaksikan container dibunuh oleh kernel di depan mata kita. Pada akhir chapter, angka Exited (137) dan field .State.OOMKilled tidak lagi menjadi teka-teki, melainkan bukti yang bisa kita baca seperti berita satu baris.

Tiga Cara Membatasi CPU #

Kernel memberi Docker tiga pintu yang berbeda untuk mengurung jatah CPU sebuah container, dan ketiganya menjawab pertanyaan yang berbeda pula. Tabel berikut menjadi peta lengkapnya:

FlagPertanyaan yang dijawabContoh nilai
--cpusberapa banyak core boleh dipakai rata-rata?--cpus 1.5
--cpuset-cpuscore yang mana saja yang boleh dipakai?--cpuset-cpus 0-1
--cpu-sharesbila berebut, porsi saya seberapa besar?--cpu-shares 512

Mari kita telaah ketiganya sebelum praktik:

  1. --cpus adalah jumlah, bukan nomor core — nilai 1.5 berarti container boleh memakai rata-rata satu setengah core dari core mana pun yang tersedia; kelebihannya di-throttle oleh penjadwal kernel, bukan ditolak.
  2. --cpuset-cpus menyematkan lokasi — container hanya diizinkan berjalan pada core yang disebut; nomor boleh tunggal (0), rentang (0-1), maupun daftar dipisah koma (0,2,4).
  3. --cpu-shares adalah bobot relatif — bawaannya 1024, dan angka ini hanya berpengaruh saat host sibuk: pada host yang lapang, container kecil sekalipun bebas melaju penuh; saat perebutan terjadi, dua container dengan shares 1024 dan 512 akan membagi waktu CPU menjadi dua banding satu.

–cpus dan jejaknya di inspect #

Kita hidupkan web dengan paket lengkap — satu setengah core, langit-langit memori 256 MB, jaminan lunak 128 MB — lalu kita buktikan bahwa Docker menyimpan seluruhnya dalam angka yang bisa dibaca ulang:

bash
$ docker run -d --name web --cpus 1.5 --memory 256m --memory-reservation 128m nginx

Output yang Dihasilkan:

text
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a
bash
$ docker inspect --format '{{.HostConfig.NanoCpus}}' web

Output yang Dihasilkan:

text
1500000000
bash
$ docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}}' web

Output yang Dihasilkan:

text
268435456 134217728

Mari kita telaah kedua angka raksasa itu:

  1. 1500000000 adalah 1.5 yang diubah satuan — kernel mencatat kuota CPU dalam satuan nanodetik per detik penjadwalan, sehingga 1.5 core dikalikan satu miliar; angka ini yang ditulis daemon ke berkas kuota cgroup CPU.
  2. 268435456 adalah 256m yang diubah ke byte — 256 dikali 1024 dua kali; cgroups memang hanya berbicara dalam byte polos.
  3. 134217728 adalah pasangan lunaknya — reservasi 128 MB dalam byte; perhatikan bahwa ia lebih kecil dari limit, persis resep yang kita pelajari pada chapter sebelumnya.

Menyemat Core dengan –cpuset-cpus #

Skenario khasnya adalah isolasi beban: server laporan bulanan yang kita pin pada core 0-1 agar mesin virtualisasi berat di core yang lain tidak saling mengganggu. Kita simulasikan dengan container tidur sederhana:

bash
$ docker run -d --name terpasang --cpuset-cpus 0-1 alpine:3.20 sleep 600

Output yang Dihasilkan:

text
bc373cda2b8d8bc6f06ef03efa7a6a424bef25ade1ba1cef4fdf43a8c65e5f11
bash
$ docker inspect --format '{{.HostConfig.CpusetCpus}}' terpasang

Output yang Dihasilkan:

text
0-1

Nilai 0-1 tersimpan utuh sebagai string konfigurasi — kernel kemudian menegaskan bahwa container ini hanya pernah dijadwalkan pada dua core pertama host. Satu peringatan praktis: daemon memvalidasi daftar core terhadap yang benar-benar tersedia — bila host hanya memiliki satu core, nilai 0-1 akan ditolak sejak docker run, jadi sesuaikan angka dengan mesin tempat kita berlatih.

Bobot dengan –cpu-shares #

Pintu ketiga tidak memasang langit-langit sama sekali — hanya menyerahkan bukti pembagian saat pertengkaran terjadi:

bash
$ docker run -d --name ringan --cpu-shares 512 alpine:3.20 sleep 600

Output yang Dihasilkan:

text
f851b837dfeef750a2a249a769f0f058e1123ae3bf44f5ca136e394790d83bc9
bash
$ docker inspect --format '{{.HostConfig.CpuShares}}' ringan

Output yang Dihasilkan:

text
512

Angka 512 tercatat setengah dari bawaan 1024. Maka bila kelak web dan ringan sama-sama bekerja berat pada host yang sibuk, keduanya bersaing dengan bobot dua banding satu — tetapi pada host yang lapang, keduanya sama-sama bebas memakan core penuh. Inilah alasan shares disebut bobot, bukan batas: ia tidak pernah menahan container yang sendirian.

Membatasi Memori #

Pembatasan memori memakai satu pintu utama dengan dua satuan kepercayaan. Flag --memory memasang langit-langit keras — nilainya ditulis ke cgroups memori dan ditegakkan dengan nyawa: container yang menabraknya akan kehilangan prosesnya lewat OOM killer. Flag --memory-reservation memasang jaminan lunak di bawahnya — kernel berusaha menjaga porsi itu tersedia saat memori host menipis, tanpa pernah membunuh siapa pun. Nilai keduanya ditulis dengan huruf satuan yang perlu kita hafal:

SatuanArtiContohNilai byte
bbyte268435456b268435456
kkilobyte (1024 byte)256k262144
mmegabyte (1024² byte)256m268435456
ggigabyte (1024³ byte)2g2147483648

Satu pasangan terakhir melengkapi gerbang memori: --memory-swap, yang mengatur berapa besar ruang swap yang menyertai langit-langit RAM. Tabel berikut memetakan keempat wujud nilainya:

Nilai --memory-swapMaknanya bagi container
tidak diseteltotal RAM + swap = dua kali --memory — jatah swap sebesar memori
sama dengan --memorytanpa swap sama sekali — menabrak RAM berarti langsung mati
-1swap tanpa batas — sebatas sisa swap yang dimiliki host
nilai lainjatah swap tambahan = nilai dikurangi --memory

Memantau dengan docker stats #

Sebelum mengundang algojo, mari kita belajar memasang kamera. docker stats menampilkan pemakaian resource seluruh container yang sedang berjalan, dan flag --no-stream memotretnya sekali alih-alih mengalir terus:

bash
$ docker stats --no-stream

Output yang Dihasilkan:

text
CONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT     MEM %     NET I/O     BLOCK I/O     PIDS
f7c2a91b3e55   web         0.00%     5.312MiB / 256MiB     2.07%     656B / 0B   9.22MB / 0B   3
bc373cda2b8d   terpasang   0.00%     1.109MiB / 7.63GiB   0.01%     0B / 0B     0B / 0B       1
f851b837dfee   ringan      0.00%     1.105MiB / 7.63GiB   0.01%     0B / 0B     0B / 0B       1

Mari kita telaah keenam kelompok kolom utamanya — header ini berdelapan kolom, dan sebagian berpasangan:

  1. CONTAINER ID dan NAME — identitas container yang dipantau; tanpa argumen, docker stats melaporkan semua container hidup sekaligus, seperti docker ps tanpa -a.
  2. CPU % — persentase pemakaian terhadap satu core penuh; angka ini boleh melampaui 100% pada mesin multi-core, dan pada web plafonnya adalah 150% karena kuota --cpus 1.5 — nginx yang sedang idle membaca 0.00%.
  3. MEM USAGE / LIMIT — pemakaian memori saat ini berdampingan dengan langit-langitnya; perhatikan kontras yang kita tanam pada chapter sebelumnya: web membaca 256MiB, sementara kedua container alpine tanpa --memory masih bergantung pada seluruh memori host.
  4. MEM % — pemakaian relatif terhadap limit pada kolom sebelumnya; 5.312 MiB dari 256 MiB memang sekitar dua persen.
  5. NET I/O dan BLOCK I/O — lalu lintas jaringan dan blok yang dibaca-ditulis container, masing-masing dalam format keluar-masuk; web sempat bertukar beberapa ratus byte karena pemetaan jaringannya, sementara dua container tidur itu sama sekali tidak berbicara.
  6. PIDS — jumlah proses yang hidup di dalam container; web memiliki tiga (master nginx beserta dua worker), dan kolom kecil ini diam-diam menjadi jembatan: siapa yang menjadi proses pertama di antara ketiganya, dan apa bedanya jadi nomor satu — itulah pertanyaan yang menanti di chapter berikutnya.

Uji OOM Killer secara Sengaja #

Tiba saatnya mengundang sang algojo dengan sengaja. Korban kita bernama lapar — nama yang menyesuaikan nafsunya: image polinux/stress membawa alat stress klasik, dan kita perintahkan ia mengalokasikan satu pekerja memori sebesar 150 MB di dalam container yang langit-langitnya hanya 100 MB. Perintah berikut berjalan terpasang — tanpa -d — supaya kita menyaksikan seluruh detik-detiknya:

bash
$ docker run --name lapar -m 100m polinux/stress --vm 1 --vm-bytes 150M --vm-hang 0

Output yang Dihasilkan:

text
Unable to find image 'polinux/stress:latest' locally
latest: Pulling from polinux/stress
...
Digest: sha256:a7250dbeec262930bc2d71c32166a41a0b72fb2cfa05e1fc87e2b6d9c876fc44
Status: Downloaded newer image for polinux/stress:latest
stress: info: [1] dispatching hogs: 1 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL [1] (416) <-- worker 6 got signal 9
stress: WARN [1] (417) <-- reaping child worker 6
stress: FAIL [1] (416) <-- worker 7 got signal 9
stress: WARN [1] (417) <-- reaping child worker 7
...

Mari kita telaah baris demi baris drama di atas:

  1. Empat baris pertama adalah unduhan — image pihak ketiga belum pernah dipakai di mesin kita; baris-baris layer di tengahnya dipotong, dan digest 64 karakter heksadesimal menjadi tanda tangan image yang barusan masuk.
  2. dispatching hogs: 1 vm — stress melapor membentuk satu pekerja memori; --vm-bytes 150M menugaskan pekerja itu mengalokasikan dan menyentuh halaman-halaman memori sebesar 150 MB.
  3. worker 6 got signal 9 — pekerja berhasil mengalokasikan sampai sekitar 100 MB lalu menabrak langit-langit cgroups; kernel menjatuhkan SIGKILL (sinyal 9) tepat pada pekerja itu — inilah OOM killer yang sedang bekerja di dalam cgroup lapar.
  4. reaping child worker 6 — stress induk memungut jasad anaknya, lalu melahirkan pekerja pengganti — yang menabrak langit-langit yang sama dan dibunuh dengan cara yang sama; pola FAIL-WARN ini berulang beberapa kali.
  5. ... menandai pengulangan itu — sampai akhirnya kernel membunuh stress induk itu sendiri, PID 1 tumbang, container berhenti, dan terminal kita kembali ke prompt dengan sunyi yang janggal setelah keributan tadi.

Kini kita kumpulkan ketiga jejak janji chapter sebelumnya. Jejak pertama dan kedua tinggal satu perintah:

bash
$ docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' lapar

Output yang Dihasilkan:

text
true 137

Jejak ketiga bersembunyi di log kernel host — pengakuan tertulis dari algojonya sendiri:

bash
$ sudo dmesg | grep --ignore-case "out of memory"

Output yang Dihasilkan:

text
[ 8412.396711] Memory cgroup out of memory: Killed process 8421 (stress) total-vm:172032kB, anon-rss:98304kB, shmem-rss:0kB, UID:0 pgtables:348kB oom_score_adj:0

Mari kita telaah ketiga jejak yang kini terbaring lengkap:

  1. true 137 — field .State.OOMKilled menyatakan resmi bahwa kematian ini adalah karya OOM killer, dan exit code 137 alias 128 + 9 mencatat bahwa senjatanya SIGKILL; keduanya bersesuaian dengan apa yang kita prediksi pada Chapter Memahami Exit Code dan janjikan pada Resource Limit.
  2. Memory cgroup out of memory — baris dari dmesg ini adalah suara kernel itu sendiri: cgroups milik container lapar kehabisan memori, dan kernel memilih membunuh proses bernama stress dengan nomor 8421.
  3. anon-rss:98304kB — memori yang benar-benar dipakai proses korban sekitar 96 MB — angka yang pas dengan langit-langit 100 MB yang kita pasang; korban tumbang tepat di tempat kejadian, bukan karena kebetulan.
  4. Korban terkurung rapi — inilah perbedaan besar dibanding OOM di level host yang kita bahas pada chapter sebelumnya: hanya proses di dalam cgroup lapar yang ikut dinilai dan dibunuh; web, terpasang, dan ringan sama sekali tidak tersentuh.
  5. Bila lapar diberi --restart always — ia akan bangkit, lapar lagi, dibunuh lagi, berulang selamanya dengan jeda backoff yang berlipat; kombinasi OOM dan restart policy adalah resep crash-loop yang paling sering menjebak pemula, seperti kita bahas pada Chapter Restart Policy.

Kondisi terakhir mesin kita sempat kita potret sebelum membereskan panggung:

bash
$ docker ps -a

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE            COMMAND                              CREATED          STATUS                    PORTS   NAMES
841b129b247a   polinux/stress   "stress --vm 1 --vm-bytes 150M --v…"   2 minutes ago    Exited (137) 2 minutes ago        lapar
f851b837dfee   alpine:3.20      "sleep 600"                           4 minutes ago    Up 4 minutes                      ringan
bc373cda2b8d   alpine:3.20      "sleep 600"                           5 minutes ago    Up 5 minutes                      terpasang
f7c2a91b3e55   nginx            "/docker-entrypoint.…"                7 minutes ago    Up 7 minutes                      web

Seluruh aktor uji hari ini kita pamitkan dalam satu ayunan:

bash
$ docker rm -f web terpasang ringan lapar

Output yang Dihasilkan:

text
web
terpasang
ringan
lapar

Ada satu perkara terakhir yang membuat pembunuhan memori ini terasa dingin: SIGKILL adalah sinyal yang tidak bisa ditangkap, tidak bisa diabaikan, dan tidak bisa dinegosiasikan oleh aplikasi apa pun. Container lapar tidak sempat berpamitan — tidak ada kesempatan menutup koneksi, mem flush berkas, atau menyimpan posisi terakhir. Padahal Docker memiliki jalur pematian yang jauh lebih sopan lewat SIGTERM — tetapi jalur sopan itu hanya berguna bila ada yang mendengarkan sinyal di dalam container, dan siapa yang mendengarkan bukan perkara kecil: di dalam container tidak ada systemd, dan proses nomor satu kita adalah aplikasi itu sendiri. Chapter berikutnya, Container Init Process, membedah masalah PID 1 ini tuntas — lalu memberikan dua jalan keluarnya: memperbaiki aplikasi agar menangani sinyal, atau menghadirkan inisialisator pembantu lewat flag --init.

Referensi #