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:

FlagPertanyaan yang dijawabContoh
--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

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:

FlagSatuanBawaanPeran
--cpu-periodmikrodetik100000 (100 ms)panjang satu jendela penjadwalan
--cpu-quotamikrodetiktanpa kuotaanggaran 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 ditulisPadanan kuota/periodMakna
--cpus 1.5--cpu-period 100000 --cpu-quota 150000rata-rata 1,5 core
--cpus 0.5--cpu-period 100000 --cpu-quota 50000rata-rata setengah core
--cpu-quota -1kuota dihapuskembali tanpa batas CFS

Kita buktikan bahwa kedua bentuk menuju mesin yang sama. Container pertama kita pasang lewat jalur klasik kuota-periode:

bash
$ docker run -d --name web-cfs --cpu-period 100000 --cpu-quota 150000 nginx

Output yang Dihasilkan:

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

Output yang Dihasilkan:

text
1500000000

Mari kita telaah angka yang muncul:

  1. 1500000000 identik dengan container web ber-flag --cpus 1.5 pada chapter CPU dan Memory Limit — dua bentuk penulisan yang berbeda menghasilkan jejak metadata yang persis sama; inilah bukti bahwa --cpus dan pasangan kuota-periode menuju satu mesin penegak.
  2. Daemon menghitung nanodetik dari pembagian — NanoCpus = kuota × 10⁹ ÷ period, sehingga 150000 × 10⁹ ÷ 100000 = 1500000000; kernel mencatat kuota CPU dalam satuan waktu absolut, bukan jumlah core.
  3. Pada host dengan cgroups v2, nilai ini mendarat di berkas cpu.max sebagai satu baris 150000 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:

bash
$ docker run -d --name pekerja-berat --cpuset-cpus 0-3 polinux/stress --cpu 4 --timeout 120s

Output yang Dihasilkan:

text
c2d94e17a3b8e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0
bash
$ docker run -d --name web --cpuset-cpus 4-7 nginx

Output yang Dihasilkan:

text
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a
bash
$ docker stats --no-stream pekerja-berat web

Output yang Dihasilkan:

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

Mari kita telaah tabel di atas:

  1. 398.12% berarti empat core bekerja nyaris penuh — persentase dihitung terhadap satu core sebagaimana kita bedah di chapter Docker Stats, dan empat pekerja stress memang melahap empat core utuh.
  2. Kekacauan itu terkurung di core 0-3 — --cpuset-cpus memastikan kernel tidak pernah menjadwalkan task pekerja-berat di core 4-7, seberapa pun core itu menganggur.
  3. web sama sekali tidak tersentuh — CPU 0.07% adalah angka nginx yang santai; tanpa sematan, penjadwal bebas menaruh task web di core yang sedang dipenuhi pekerja, dan keduanya akan saling menggerus.
  4. PIDS bernilai 5 pada pekerja — satu proses induk stress beserta 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:

bash
$ docker rm -f pekerja-berat web-cfs web

Output yang Dihasilkan:

text
pekerja-berat
web-cfs
web

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

bash
$ 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):

text
real    0m 8.7s
user    0m 0.07s
sys     0m 0.02s

Terminal kanan — container dengan shares 512, dimulai bersamaan:

bash
$ 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):

text
real    0m 17.4s
user    0m 0.06s
sys     0m 0.03s

Mari kita telaah sepasang keluaran itu:

  1. 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.
  2. Hanya baris real yang boleh dibaca — user dan sys mengukur proses klien docker di host, bukan proses di dalam container; klien hanya menunggu jawaban daemon, sehingga waktunya nyaris nol.
  3. Angka absolutnya ilustratif, proporsinya tidak — kecepatan sha256sum berbeda tiap prosesor, tetapi selama dua container dengan bobot 2:1 berebut satu core, rasio durasi mereka akan mengikuti rasio bobot itu.
  4. Sematan core adalah alat pancing, bukan syarat shares — kita memakai --cpuset-cpus 0 untuk 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:

CaraFlagSifat penegakanKapan dipakai
Batas jumlah--cpus atau --cpu-quota+--cpu-periodlangit-langit rata-rata; pelanggar di-throttlemelindungi host dan tetangga dari container rakus
Bobot relatif--cpu-sharestanpa langit-langit; hanya berlaku saat host senuhmembagi prioritas saat sesak tanpa memperlambat saat lapang
Sematan core--cpuset-cpusalamat fisik; task tidak bisa pindah corelatensi 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:

bash
$ docker run -d --name web-lengkap --cpus 1.5 --cpu-shares 512 --cpuset-cpus 0-1 nginx

Output yang Dihasilkan:

text
8c1f4a2b6d9e0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c
bash
$ docker inspect --format '{{.HostConfig.NanoCpus}} {{.HostConfig.CpuShares}} {{.HostConfig.CpusetCpus}}' web-lengkap

Output yang Dihasilkan:

text
1500000000 512 0-1

Mari kita telaah satu baris berisi tiga jejak itu:

  1. 1500000000 — kuota 1,5 core dalam nanodetik; angka yang sama seperti web-cfs di awal chapter, karena memang mesin yang sama.
  2. 512 — bobot relatif setengah dari bawaan; berlaku hanya bila host menjerit.
  3. 0-1 — sematan dua core pertama, tersimpan sebagai string persis seperti yang kita tulis.

Kita tutup panggung dengan bersih:

bash
$ docker rm -f web-lengkap

Output yang Dihasilkan:

text
web-lengkap

CPU 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 #