Penutup section sebelumnya meninggalkan satu kesimpulan yang pantas ditempel di dinding tim: diagnosis yang baik sering berakhir di resource — OOM lahir dari memori tanpa langit-langit, latensi memuncak dari CPU tanpa jatah, disk penuh dari IO tanpa anggaran. Section Observability dan Troubleshooting melatih kita membaca gejala setelah kejadian; section ini membalik arah dan mencegah kejadian itu lahir, dimulai dari CPU. Fondasinya sebenarnya sudah kita pegang: --cpus, --cpuset-cpus, dan --cpu-shares telah kita praktikkan satu per satu pada chapter CPU dan Memory Limit di section Konfigurasi Container. Chapter ini tidak mengulang latihan itu, melainkan menaikkan satu tingkat: membongkar mesin penjadwal CFS yang bersembunyi di balik angka desimal --cpus, menyemat container ke core tertentu untuk isolasi yang benar-benar fisikal, memahami --cpu-shares sebagai reservasi yang hanya berbicara saat host senuh, lalu membuktikan semuanya lewat satu eksperimen perebutan core yang kita sengaja pancing.
Recap Tiga Pintu CPU dari Chapter Sebelumnya #
Sebelum masuk ke mesinnya, mari kita segarkan ingatan tiga pintu yang sudah terbuka — tabel ringkasnya sebagai berikut:
| Flag | Pertanyaan yang dijawab | Contoh |
|---|---|---|
--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 |
Sepanjang chapter CPU dan Memory Limit, ketiga pintu kita pakai sebagai kotak hitam: angka masuk ke docker run, container terbatang keluar, dan docker inspect membuktiannya — --cpus 1.5 terbaca sebagai NanoCpus = 1500000000, satu setengah core dalam satuan nanodetik. Kotak hitam itulah yang kita buka sekarang, dimulai dari pasangan flag yang menjadi tulang punggung --cpus.
Kuota dan Periode CFS di Balik –cpus #
Kernel Linux tidak mengenal satuan “1,5 core”. Yang kernel kenal adalah dua angka yang bekerja seperti anggaran mingguan: periode, panjang jendela waktu penjadwalan, dan kuota, berapa banyak waktu CPU yang boleh dihabiskan sebuah container dalam satu jendela itu. Keduanya diatur sepasang flag berikut:
| Flag | Satuan | Bawaan | Peran |
|---|---|---|---|
--cpu-period | mikrodetik | 100000 (100 ms) | panjang satu jendela penjadwalan |
--cpu-quota | mikrodetik | tanpa kuota | anggaran waktu CPU per jendela |
Penjadwal yang menegakkan pasangan ini bernama CFS (Completely Fair Scheduler), dan --cpus sebenarnya hanyalah kalkulator ringkas di atasnya. Padanan kedua bentuk itu tampak seperti ini:
| Bentuk yang ditulis | Padanan kuota/period | Makna |
|---|---|---|
--cpus 1.5 | --cpu-period 100000 --cpu-quota 150000 | rata-rata 1,5 core |
--cpus 0.5 | --cpu-period 100000 --cpu-quota 50000 | rata-rata setengah core |
--cpu-quota -1 | kuota dihapus | kembali tanpa batas CFS |
Kita buktikan bahwa kedua bentuk menuju mesin yang sama. Container pertama kita pasang lewat jalur klasik kuota-periode:
$ docker run -d --name web-cfs --cpu-period 100000 --cpu-quota 150000 nginxOutput yang Dihasilkan:
d4a8b2c1e9f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1$ docker inspect --format '{{.HostConfig.NanoCpus}}' web-cfsOutput yang Dihasilkan:
1500000000Mari kita telaah angka yang muncul:
1500000000identik dengan containerwebber-flag--cpus 1.5pada chapter CPU dan Memory Limit — dua bentuk penulisan yang berbeda menghasilkan jejak metadata yang persis sama; inilah bukti bahwa--cpusdan pasangan kuota-periode menuju satu mesin penegak.- Daemon menghitung nanodetik dari pembagian —
NanoCpus = kuota × 10⁹ ÷ period, sehingga150000 × 10⁹ ÷ 100000 = 1500000000; kernel mencatat kuota CPU dalam satuan waktu absolut, bukan jumlah core. - Pada host dengan cgroups v2, nilai ini mendarat di berkas
cpu.maxsebagai satu baris150000 100000— kuota disambung period, persis dua angka yang kita tulis lewat flag.
Lalu bagaimana mesin ini menegakkan batas? Jawabannya: dengan menunda, bukan membunuh. Dalam setiap jendela 100 milidetik, kernel menghitung total waktu CPU yang dihabiskan seluruh task container; begitu angka itu menyentuh 150 milidetik, task-task container di-throttle — ditahan, tidak dijadwalkan sama sekali — sampai jendela berikutnya dibuka. Aplikasi tidak mati dan tidak menerima galat apa pun; aplikasi hanya merasakan dunia yang berhenti bergerak sesaat, berulang dengan pola berkala. Inilah penegakan yang kita janjikan pada chapter Resource Limit: batas CPU membuat aplikasi melambat, bukan mati.
Baca diagram dari atas ke bawah sebagai satu siklus tanpa akhir: selama kuota belum habis, task terus berjalan; begitu kuota jendela tersentuh, seluruh task container menunggu sampai jendela berikutnya. Konsekuensi praktisnya khas: container yang di-throttle menunjukkan latensi yang naik berkala dengan periode 100 milidetik — grafik bergigi yang stabil, bukan kegagalan mendadak. Pola bergigi semacam itu di dashboard adalah tanda tangan CFS yang sedang bekerja.
Menyemat Core dengan –cpuset-cpus #
Kita pernah menyemat container terpasang pada core 0-1 sebagai latihan singkat di chapter CPU dan Memory Limit; sekarang kita memakai pintu ini dengan niat yang lebih serius, karena sematan core menyelesaikan dua masalah yang tidak bisa dijawab kuota. Masalah pertama adalah latensi yang stabil: task yang tidak berpindah-pindah core menjaga isi cache prosesor tetap hangat, sehingga jitter waktu respons turun dan persentil ke-99 lebih rapi — alasan klasik service latensi-sensitif dipatok pada core tertentu. Masalah kedua adalah isolasi noisy neighbor yang benar-benar fisikal: kuota membagi waktu, tetapi sematan membagi alamat — dua kumpulan core yang tidak pernah bersinggungan tidak mungkin saling rebut.
Kita buktikan isolasi fisikal itu dengan membangkitkan kembali skenario Budi dari chapter Resource Limit: pekerja berat (konversi video dalam cerita aslinya; di sini kita gantikan dengan empat pekerja stress) kita kurung di core 0-3, sementara web menempati core 4-7:
$ docker run -d --name pekerja-berat --cpuset-cpus 0-3 polinux/stress --cpu 4 --timeout 120sOutput yang Dihasilkan:
c2d94e17a3b8e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0$ docker run -d --name web --cpuset-cpus 4-7 nginxOutput yang Dihasilkan:
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a$ docker stats --no-stream pekerja-berat webOutput yang Dihasilkan:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
c2d94e17a3b8 pekerja-berat 398.12% 7.91MiB / 7.63GiB 0.10% 0B / 0B 0B / 0B 5
f7c2a91b3e55 web 0.07% 5.281MiB / 7.63GiB 0.07% 656B / 0B 9.22MB / 0B 3Mari kita telaah tabel di atas:
398.12%berarti empat core bekerja nyaris penuh — persentase dihitung terhadap satu core sebagaimana kita bedah di chapter Docker Stats, dan empat pekerjastressmemang melahap empat core utuh.- Kekacauan itu terkurung di core 0-3 —
--cpuset-cpusmemastikan kernel tidak pernah menjadwalkan taskpekerja-beratdi core 4-7, seberapa pun core itu menganggur. websama sekali tidak tersentuh — CPU 0.07% adalah angka nginx yang santai; tanpa sematan, penjadwal bebas menaruh taskwebdi core yang sedang dipenuhi pekerja, dan keduanya akan saling menggerus.PIDSbernilai 5 pada pekerja — satu proses indukstressbeserta empat pekerja CPU-nya; kolom ini akan menjadi pokok bahasan chapter PIDs Limit dan Ulimit.
Catatan singkat untuk server besar: pada mesin multi-NUMA, RAM terbagi ke dalam bank yang menempel pada node tertentu, dan core yang jauh dari bank memori miliknya menambahkan latensi akses. Perintah lscpu memperlihatkan pemetaan core ke node NUMA, dan --cpuset-cpus membantu menjaga container berjalan pada core yang satu wilayah dengan datanya — meskipun kebijakan alokasi memori NUMA sendiri berada di luar cakupan flag ini. Pada laptop dan server kecil yang hanya memiliki satu node NUMA, pertimbangan ini tidak berlaku.
Bekas latihan kita bereskan sebelum melangkah:
$ docker rm -f pekerja-berat web-cfs webOutput yang Dihasilkan:
pekerja-berat
web-cfs
webReservasi Bobot dengan –cpu-shares #
--cpu-shares adalah anggota keluarga “reservasi” di dunia CPU, tetapi reservasi ini punya tabiat yang perlu dipahami betul: ia adalah bobot relatif, bukan jaminan. Nilai bawaannya 1024, dan angka ini sama sekali tidak berpengaruh selama host masih lapang — container dengan shares 512 sekalipun bebas melaju pada seluruh core yang tersedia, persis yang kita saksikan pada container ringan di chapter sebelumnya. Baru ketika semua core senuh dan perebutan sungguhan terjadi, kernel membagi waktu CPU menurut rasio bobot: container 1024 lawan 512 akan berbagi dengan proporsi dua banding satu. Pada host cgroups v2, daemon menerjemahkan shares menjadi bobot penjadwal cpu.weight dengan rasio yang dipertahankan.
Karena tabiatnya itu, shares tidak pernah menahan container yang sendirian dan tidak pernah menyimpan kursi untuk siapa pun — ia hanya menjadi hakim saat pertengkaran. Proporsi dua banding satu itu sendiri belum pernah kita buktikan dengan angka, dan membuktikannya adalah tugas eksperimen berikutnya.
Eksperimen Perebutan Satu Core #
Kita paksa perebutan terjadi: dua container sama-sama dipaku di core 0 — satu-satunya core yang boleh mereka pakai — dengan bobot 1024 lawan 512. Beban keduanya identik: sha256sum mencerna 4 GiB data dari /dev/zero, pekerjaan murni CPU yang mudah diukur. Jalankan keduanya hampir bersamaan di dua terminal terpisah.
Terminal kiri — container dengan shares 1024:
$ time docker run --rm --cpuset-cpus 0 --cpu-shares 1024 alpine:3.20 \
sh -c 'head -c 4294967296 /dev/zero | sha256sum > /dev/null'Output yang Dihasilkan (angka ilustratif):
real 0m 8.7s
user 0m 0.07s
sys 0m 0.02sTerminal kanan — container dengan shares 512, dimulai bersamaan:
$ time docker run --rm --cpuset-cpus 0 --cpu-shares 512 alpine:3.20 \
sh -c 'head -c 4294967296 /dev/zero | sha256sum > /dev/null'Output yang Dihasilkan (angka ilustratif):
real 0m 17.4s
user 0m 0.06s
sys 0m 0.03sMari kita telaah sepasang keluaran itu:
- Waktu dinding kanan hampir tepat dua kali kiri — 17,4 detik berbanding 8,7 detik; proporsi 2:1 inilah rasio 1024:512 yang dijanjikan shares, terkurung pada satu core yang sama-sama diperebutkan.
- Hanya baris
realyang boleh dibaca —userdansysmengukur proses kliendockerdi host, bukan proses di dalam container; klien hanya menunggu jawaban daemon, sehingga waktunya nyaris nol. - Angka absolutnya ilustratif, proporsinya tidak — kecepatan
sha256sumberbeda tiap prosesor, tetapi selama dua container dengan bobot 2:1 berebut satu core, rasio durasi mereka akan mengikuti rasio bobot itu. - Sematan core adalah alat pancing, bukan syarat shares — kita memakai
--cpuset-cpus 0untuk memaksa perebutan; pada host delapan core yang lapang, kedua container ini akan mendapat core masing-masing dan selesai sama cepat, karena shares tidak pernah memperlambat yang tidak bersaing.
Penjadwal Realtime dengan –cpu-rt-runtime #
Dunia penjadwal sesungguhnya tidak berhenti di CFS. Kernel Linux juga menyediakan kelas penjadwal realtime untuk proses dengan tenggat waktu keras — pemrosesan audio, sistem kontrol industri — yang dijanjikan jatah CPU tanpa bisa diprempit proses biasa. Docker menyediakan pintunya lewat --cpu-rt-runtime (mikrodetik runtime realtime per periode --cpu-rt-period) yang menyisakan anggaran waktu realtime untuk container. Flag ini termasuk wilayah niche: hampir seluruh beban container modern — web server, API, worker antrean — berjalan di penjadwal normal CFS, dan anggaran realtime yang salah pasang justru berisiko membuat satu task mengunci core dari semua tetangganya. Kecuali aplikasi di dalam container benar-benar menuntut jaminan waktu tingkat mikrodetik, biarkan pasangan flag ini tidak tersentuh.
Batas Bobot dan Sematan dalam Satu Tabel #
Ketiga pintu CPU kini terbuka tuntas, dan pilihan di antaranya bergantung pada pertanyaan yang ingin kita jawab:
| Cara | Flag | Sifat penegakan | Kapan dipakai |
|---|---|---|---|
| Batas jumlah | --cpus atau --cpu-quota+--cpu-period | langit-langit rata-rata; pelanggar di-throttle | melindungi host dan tetangga dari container rakus |
| Bobot relatif | --cpu-shares | tanpa langit-langit; hanya berlaku saat host senuh | membagi prioritas saat sesak tanpa memperlambat saat lapang |
| Sematan core | --cpuset-cpus | alamat fisik; task tidak bisa pindah core | latensi yang stabil, isolasi fisikal, kesadaran NUMA |
Ketiganya bukan pilihan eksklusif — satu container boleh memakai ketiganya sekaligus, dan kombinasi itulah yang kita rangkai sekarang.
Membaca Ketiganya di docker inspect #
Satu container dengan tiga pintu terpasang:
$ docker run -d --name web-lengkap --cpus 1.5 --cpu-shares 512 --cpuset-cpus 0-1 nginxOutput yang Dihasilkan:
8c1f4a2b6d9e0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c$ docker inspect --format '{{.HostConfig.NanoCpus}} {{.HostConfig.CpuShares}} {{.HostConfig.CpusetCpus}}' web-lengkapOutput yang Dihasilkan:
1500000000 512 0-1Mari kita telaah satu baris berisi tiga jejak itu:
1500000000— kuota 1,5 core dalam nanodetik; angka yang sama sepertiweb-cfsdi awal chapter, karena memang mesin yang sama.512— bobot relatif setengah dari bawaan; berlaku hanya bila host menjerit.0-1— sematan dua core pertama, tersimpan sebagai string persis seperti yang kita tulis.
Kita tutup panggung dengan bersih:
$ docker rm -f web-lengkapOutput yang Dihasilkan:
web-lengkapCPU ternyata punya tiga pintu, dan ketiganya ditegakkan dengan cara yang sama-sama sopan: pelanggar ditunda, tidak pernah dibunuh. Memori tidak diberi kelembutan semacam itu — halaman yang sudah dialokasikan tidak bisa “dikembalikan sedikit demi sedikit”, sehingga penegaknya bekerja dengan senjata yang jauh lebih dingin. Ditambah lagi, ada satu knob yang belum kita bedah tuntas di chapter dasar: --memory-swap, flag yang menentukan apakah container boleh berlindung ke disk saat RAM-nya penuh. Chapter berikutnya, Memory Limit dan Swap, membedah semantiknya sampai ke tabel keputusan, membuka peristiwa OOM dari dalam cgroup, lalu mengujinya dengan satu eksperimen beruntun: container yang sama mati di 128 MB, lalu bertahan sampai 256 MB.
Referensi #
- Runtime options with Memory, CPUs, and GPUs
— semantik
--cpus, kuota/periode CFS,--cpu-shares, dan--cpuset-cpus. - docker container run — daftar lengkap flag CPU beserta nilai bawaannya.
- docker container inspect
— pembacaan
HostConfig.NanoCpus,CpuShares, danCpusetCpuslewat template.