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:
| Flag | Pertanyaan yang dijawab | Contoh nilai |
|---|---|---|
--cpus | berapa banyak core boleh dipakai rata-rata? | --cpus 1.5 |
--cpuset-cpus | core yang mana saja yang boleh dipakai? | --cpuset-cpus 0-1 |
--cpu-shares | bila berebut, porsi saya seberapa besar? | --cpu-shares 512 |
Mari kita telaah ketiganya sebelum praktik:
--cpusadalah jumlah, bukan nomor core — nilai1.5berarti container boleh memakai rata-rata satu setengah core dari core mana pun yang tersedia; kelebihannya di-throttle oleh penjadwal kernel, bukan ditolak.--cpuset-cpusmenyematkan lokasi — container hanya diizinkan berjalan pada core yang disebut; nomor boleh tunggal (0), rentang (0-1), maupun daftar dipisah koma (0,2,4).--cpu-sharesadalah bobot relatif — bawaannya1024, dan angka ini hanya berpengaruh saat host sibuk: pada host yang lapang, container kecil sekalipun bebas melaju penuh; saat perebutan terjadi, dua container dengan shares1024dan512akan 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:
$ docker run -d --name web --cpus 1.5 --memory 256m --memory-reservation 128m nginxOutput yang Dihasilkan:
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a$ docker inspect --format '{{.HostConfig.NanoCpus}}' webOutput yang Dihasilkan:
1500000000$ docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}}' webOutput yang Dihasilkan:
268435456 134217728Mari kita telaah kedua angka raksasa itu:
1500000000adalah1.5yang diubah satuan — kernel mencatat kuota CPU dalam satuan nanodetik per detik penjadwalan, sehingga1.5core dikalikan satu miliar; angka ini yang ditulis daemon ke berkas kuota cgroup CPU.268435456adalah256myang diubah ke byte — 256 dikali 1024 dua kali; cgroups memang hanya berbicara dalam byte polos.134217728adalah 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:
$ docker run -d --name terpasang --cpuset-cpus 0-1 alpine:3.20 sleep 600Output yang Dihasilkan:
bc373cda2b8d8bc6f06ef03efa7a6a424bef25ade1ba1cef4fdf43a8c65e5f11$ docker inspect --format '{{.HostConfig.CpusetCpus}}' terpasangOutput yang Dihasilkan:
0-1Nilai 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:
$ docker run -d --name ringan --cpu-shares 512 alpine:3.20 sleep 600Output yang Dihasilkan:
f851b837dfeef750a2a249a769f0f058e1123ae3bf44f5ca136e394790d83bc9$ docker inspect --format '{{.HostConfig.CpuShares}}' ringanOutput yang Dihasilkan:
512Angka 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:
| Satuan | Arti | Contoh | Nilai byte |
|---|---|---|---|
b | byte | 268435456b | 268435456 |
k | kilobyte (1024 byte) | 256k | 262144 |
m | megabyte (1024² byte) | 256m | 268435456 |
g | gigabyte (1024³ byte) | 2g | 2147483648 |
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-swap | Maknanya bagi container |
|---|---|
| tidak disetel | total RAM + swap = dua kali --memory — jatah swap sebesar memori |
sama dengan --memory | tanpa swap sama sekali — menabrak RAM berarti langsung mati |
-1 | swap tanpa batas — sebatas sisa swap yang dimiliki host |
| nilai lain | jatah 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:
$ docker stats --no-streamOutput yang Dihasilkan:
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 1Mari kita telaah keenam kelompok kolom utamanya — header ini berdelapan kolom, dan sebagian berpasangan:
CONTAINER IDdanNAME— identitas container yang dipantau; tanpa argumen,docker statsmelaporkan semua container hidup sekaligus, sepertidocker pstanpa-a.CPU %— persentase pemakaian terhadap satu core penuh; angka ini boleh melampaui 100% pada mesin multi-core, dan padawebplafonnya adalah 150% karena kuota--cpus 1.5— nginx yang sedang idle membaca 0.00%.MEM USAGE / LIMIT— pemakaian memori saat ini berdampingan dengan langit-langitnya; perhatikan kontras yang kita tanam pada chapter sebelumnya:webmembaca256MiB, sementara kedua container alpine tanpa--memorymasih bergantung pada seluruh memori host.MEM %— pemakaian relatif terhadap limit pada kolom sebelumnya; 5.312 MiB dari 256 MiB memang sekitar dua persen.NET I/OdanBLOCK I/O— lalu lintas jaringan dan blok yang dibaca-ditulis container, masing-masing dalam format keluar-masuk;websempat bertukar beberapa ratus byte karena pemetaan jaringannya, sementara dua container tidur itu sama sekali tidak berbicara.PIDS— jumlah proses yang hidup di dalam container;webmemiliki 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:
$ docker run --name lapar -m 100m polinux/stress --vm 1 --vm-bytes 150M --vm-hang 0Output yang Dihasilkan:
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:
- 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.
dispatching hogs: 1 vm— stress melapor membentuk satu pekerja memori;--vm-bytes 150Mmenugaskan pekerja itu mengalokasikan dan menyentuh halaman-halaman memori sebesar 150 MB.worker 6 got signal 9— pekerja berhasil mengalokasikan sampai sekitar 100 MB lalu menabrak langit-langit cgroups; kernel menjatuhkanSIGKILL(sinyal 9) tepat pada pekerja itu — inilah OOM killer yang sedang bekerja di dalam cgrouplapar.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....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:
$ docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' laparOutput yang Dihasilkan:
true 137Jejak ketiga bersembunyi di log kernel host — pengakuan tertulis dari algojonya sendiri:
$ sudo dmesg | grep --ignore-case "out of memory"Output yang Dihasilkan:
[ 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:0Mari kita telaah ketiga jejak yang kini terbaring lengkap:
true 137— field.State.OOMKilledmenyatakan resmi bahwa kematian ini adalah karya OOM killer, dan exit code137alias128 + 9mencatat bahwa senjatanyaSIGKILL; keduanya bersesuaian dengan apa yang kita prediksi pada Chapter Memahami Exit Code dan janjikan pada Resource Limit.Memory cgroup out of memory— baris daridmesgini adalah suara kernel itu sendiri: cgroups milik containerlaparkehabisan memori, dan kernel memilih membunuh proses bernamastressdengan nomor 8421.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.- Korban terkurung rapi — inilah perbedaan besar dibanding OOM di level host yang kita bahas pada chapter sebelumnya: hanya proses di dalam cgroup
laparyang ikut dinilai dan dibunuh;web,terpasang, danringansama sekali tidak tersentuh. - Bila
lapardiberi--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:
$ docker ps -aOutput yang Dihasilkan:
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 webSeluruh aktor uji hari ini kita pamitkan dalam satu ayunan:
$ docker rm -f web terpasang ringan laparOutput yang Dihasilkan:
web
terpasang
ringan
laparAda 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 #
- Runtime options with Memory, CPUs, and GPUs
— bedah resmi
--cpus,--cpuset-cpus,--cpu-shares,--memory, dan--memory-swap. - docker container stats — arti kedelapan kolom pemantauan resource secara langsung.
- docker container inspect
— membaca
HostConfig.NanoCpus,HostConfig.Memory, dan.State.OOMKilledlewat template.