Kubernetes Scheduling: Requests, Limits, Eviction
Sesi: 10:00 Technical Exploration · 15 Okt 2026 · Dif 3 (Intermediate–Advanced)
Tujuan Belajar
Setelah sesi ini Bapak memahami:
1. Bagaimana kube-scheduler memilih node (filter → score → bind) dan apa bedanya dari kubelet.
2. Bedanya requests vs limits di dua level: scheduler (scheduling) dan kubelet/cgroup (runtime).
3. Bagaimana QoS class ditentukan mesin, bukan manual, dan kenapa itu menentukan urutan eviction.
4. Node-pressure eviction: signal, threshold hard/soft, grace period, urutan victim.
5. Perubahan terkini: In-Place Pod Resize GA di v1.35 dan scheduler preemption for resize (Alpha) di v1.37 (Sep 2026).
Arsitektur & Alur Teknis
Dua "otak" berbeda
| Komponen | Peran | Keputusan |
|---|---|---|
kube-scheduler |
Placement | Pod ini jalan di node mana |
kubelet (per node) |
Enforcement | Berapa resource nyata yang diberi; siapa dibunuh kalau kurang |
Scheduler tidak pernah melihat pemakaian aktual (metrics). Ia hanya melihat jumlah requests yang sudah ter-commit di tiap node. Inilah sumber utama salah paham operasional: node "penuh" menurut scheduler artinya jumlah requests = allocatable, bukan CPU benar-benar terpakai.
Alur scheduling cycle
Pod masuk queue (unscheduled)
│
├─ Filtering → buang node yang tidak feasible
│ NodeResourcesFit (requests > allocatable = buang)
│ NodeAffinity, Taints/Tolerations, PodTopologySpread,
│ inter-pod affinity, PVC topology, node conditions
│
├─ Scoring → skor node feasible
│ NodeResourcesBalancedAllocation (biar seimbang CPU/mem)
│ NodeResourcesFit (LeastAllocated / MostAllocated / RequestedToCapacityRatio)
│ ImageLocality (image sudah ada di node = skor tinggi)
│ + plugin custom (mis. Kueue, Volcano untuk batch)
│
└─ Binding → tulis spec.nodeName
Setelah binding, kubelet yang mulai kerja:
kubelet admit pod
├─ cek requests vs allocatable (kubelet punya view sendiri, dari cAdvisor + cgroup)
├─ buat cgroup: cpu.max / cpu.weight / memory.max / memory.high
├─ jalankan container via CRI
└─ loop periodik: sinkronkan status, jalankan eviction manager
Requests vs Limits — apa yang sebenarnya terjadi
- Requests → dipakai scheduler untuk bin-packing, dan oleh kubelet untuk menentukan QoS + setting
cpu.weight(cgroup v2) /cpu.shares(v1). - Limits → diterjemahkan langsung ke
cpu.maxdanmemory.max(cgroup v2).
Konsekuensi penting:
- CPU limit = throttling, bukan pembunuhan. Container kena
cpu.maxakan distop paksa tiap periode (default 100ms) → manifestasi klasik: latency p99 naik walau CPU usage rendah. Ini CFS throttling, bukan bug aplikasi. - Memory limit = OOM kill. Melebihi
memory.max→ kernel OOM killer bunuh proses di cgroup itu (biasanya exit code 137).
# requests rendah + limits tinggi = Burstable, bisa burst tapi rapuh saat pressure
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2" # 2 core ceiling; di atas ini → throttled
memory: 512Mi # di atas ini → OOMKilled
QoS class — mesin yang menentukan, bukan kita
| QoS | Syarat (semua container di pod) | Prioritas hidup |
|---|---|---|
| Guaranteed | requests == limits untuk CPU dan memory di setiap container |
Terakhir di-evict |
| Burstable | Ada minimal satu request/limit, tapi tidak memenuhi syarat Guaranteed | Tengah |
| BestEffort | Tidak ada request maupun limit sama sekali | Pertama di-evict |
Cek cepat:
kubectl get pod <name> -o jsonpath='{.status.qosClass}{"\n"}'
Node-Pressure Eviction (kubelet)
Ini bagian yang paling sering bikin insiden produksi malam-malam. Kubelet memantau signal dan membandingkan dengan threshold:
| Signal | Sumber nilai |
|---|---|
memory.available |
node.status.capacity[memory] − node.stats.memory.workingSet (dihitung dari cgroupfs, bukan free -m) |
nodefs.available / nodefs.inodesFree |
filesystem node |
imagefs.available / containerfs.available |
filesystem image/container runtime |
pid.available |
rlimit.maxpid − curproc |
Catatan teknis: kubelet meng-exclude inactive_file dari perhitungan memory.available karena dianggap reclaimable di bawah pressure. v1.37 (Beta, default on): pada node dengan hugepages, kubelet mengurangi kapasitas hugepage dari memory.available — sebelum ini hugepage-reserved RAM menggelembungkan AvailableBytes dan bikin eviction decision keliru.
Hard vs Soft threshold
Hard eviction → kubelet langsung kill dengan grace period 0s. Tidak sopan sama sekali.
Soft eviction → kubelet tunggu eviction-max-pod-grace-period sebelum kill.
# KubeletConfiguration
evictionHard:
memory.available: "100Mi"
nodefs.available: "10%"
nodefs.inodesFree: "5%"
evictionSoft:
memory.available: "200Mi"
evictionSoftGracePeriod:
memory.available: "60s"
evictionMaxPodGracePeriod: 120
memoryThrottlingFactor: 0.9
memoryReservationPolicy: TieredReservation # cgroup v2 memory QoS
⚠️ Krusial: node-pressure eviction TIDAK menghormati PodDisruptionBudget dan TIDAK menghormati terminationGracePeriodSeconds pod. PDB hanya berlaku untuk API-initiated eviction (kubectl drain, Eviction API). Banyak orang salah paham di sini.
Urutan victim (ranking)
1. Pod dengan usage > requests (kandidat pertama)
2. Di antara kandidat → urutkan per QoS class:
BestEffort → Burstable (yang paling "melampaui requests") → Guaranteed
3. Tie-break: PodPriority → usage relatif terhadap requests
Poin penting yang sering dilewatkan: hanya pod yang melebihi requests-nya yang jadi kandidat ketika eviction dipicu resource pressure. Pod Burstable yang diam di bawah requests-nya relatif aman.
Eviction ranking pseudocode
candidates = pods where usage > requests
sort candidates by:
key1 = qos_rank(pod) # BestEffort=0, Burstable=1, Guaranteed=2
key2 = -(usage - requests) # yang paling melampaui dibunuh dulu
key3 = pod.priority # angka lebih kecil dibunuh dulu
evict first candidate
# kubelet lakukan bertahap, re-evaluate signal tiap interval
# berhenti begitu signal kembali di atas threshold
Priority & Preemption — beda mekanisme, beda pelaku
- Eviction = kubelet, karena resource pressure (node sudah kritis).
- Preemption = scheduler, karena pod baru tidak dapat tempat. Scheduler membunuh pod prioritas rendah supaya pod prioritas tinggi bisa dijadwalkan.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Latency-critical services"
⚠️ Anti-pattern fatal: system-cluster-critical (value 2000000000) dipakai untuk app biasa. Saat pressure, app sembarangan bisa membunuh kube-dns / CoreDNS karena priority-nya lebih tinggi. Ini penyebab outage DNS di banyak cluster.
Perkembangan 2025–2026 (terverifikasi)
| Fitur | Versi | Status |
|---|---|---|
| In-Place Pod Resize | v1.33 (Mei 2025) | Beta |
| In-Place Pod Resize | v1.35 (Des 2025) | GA/Stable |
| Scheduler Preemption for In-Place Resize | v1.37 (Sep 2026) | Alpha, gate InPlacePodVerticalScalingSchedulerPreemption |
Masalah yang dipecahkan fitur v1.37: setelah in-place resize GA, muncul celah — kalau pod minta scale-up melebihi headroom node, kubelet menandai resize sebagai Deferred (bukan Infeasible). Pod nyangkut di situ tanpa batas waktu, menunggu resource bebas secara natural. Sebelum v1.37, opsi operator cuma: evict manual, atau tunggu cluster autoscaler (disrupsi, melanggar janji "no restart").
Sekarang scheduler ikut memantau pod ber-Deferred dan bisa mem-preempt workload prioritas rendah di node yang sama untuk membebaskan headroom.
Resize flow (v1.37):
1. ResizeDeferred → kubelet tolak: OutOfcpu
2. scheduler preempt low-priority-pod di node yang sama
3. ResizeStarted → kubelet actuate
4. ResizeCompleted → cgroup di-update, pod TIDAK restart
# Manual edit (v1.33+)
kubectl edit pod <pod-name> --subresource resize
# Cek CPU yang benar-benar dialokasikan setelah resize
kubectl get pod high-priority-pod \
-o jsonpath='{.status.containerStatuses[0].allocatedResources.cpu}{"\n"}'
Pilihan bin-packing sekarang jadi berbeda: dengan preemption ini, Bapak bisa agresif mengisi sisa kapasitas node dengan batch/low-priority workload tanpa takut memblokir scale-up workload kritis. Trade-off-nya: preemption = restart untuk pod yang di-preempt (bukan in-place), jadi Pastikan pod batch punya restart policy/checkpoint yang benar.
Trade-off & Pendekatan Alternatif
Requests sizing — over vs under
| Strategi | Risiko | Kapan dipakai |
|---|---|---|
| Over-request | Cluster boros, bin-packing jelek, pod pending | Workload kritikal, VPA belum tersedia |
| Under-request | Throttle/OOM, sasaran empuk eviction | Batch, best-effort |
| Right-size via VPA | VPA restart pod saat update (mode Auto) |
Workload stabil |
| In-place resize / KEDA | Kompleksitas tambahan, versi cluster | Workload dengan pola trafik dinamis |
NodeResourcesFit strategy
LeastAllocated → sebar merata (default-ish). Tahan node failure.
MostAllocated → bin-pack rapat. Hemat biaya, rapuh saat spike.
RequestedToCapacityRatio → kontrol manual shapes.
Kombinasi MostAllocated + lower-priority overcommit = resep bin-packing agresif. Kalau kube ini punya lima pod replika dengan profil trafik mirip, pakai LeastAllocated biar kegagalan satu node tidak menjatuhkan semuanya sekaligus.
Alternatif cluster autoscaler
| Solusi | Model | Catatan |
|---|---|---|
| Cluster Autoscaler | Scale node saat pod pending | Lambat (menit), rebutan quota cloud |
| Karpenter | Just-in-time provisioning, instance fleksibel | Lebih cepat, lebih granular |
| VPA | Scale requests pod | Restart untuk skema lama; in-place kini tersedia |
| In-place resize + preemption | Scale tanpa restart | Alpha (v1.37), butuh desain priority yang matang |
Use Case Produksi
Skenario: API server latency-critical + batch ETL di node pool sama.
# Workload kritikal — jadikan Guaranteed, priority tinggi
apiVersion: v1
kind: Pod
spec:
priorityClassName: high-priority
containers:
- name: api
resources:
requests: { cpu: "1", memory: "1Gi" }
limits: { cpu: "1", memory: "1Gi" } # == requests → Guaranteed
---
# Workload batch — BestEffort/Burstable, priority rendah
apiVersion: batch/v1
kind: Job
spec:
template:
spec:
priorityClassName: low-priority
containers:
- name: etl
resources:
requests: { cpu: "100m" } # Burstable
# tidak ada memory limit → risiko OOM internal app, tapi tidak OOMKilled
Dengan preemption v1.37, saat API perlu scale-up > headroom, scheduler bisa membunuh job ETL untuk membebaskan CPU. ETL tadinya jalan sebagai Burstable, akan dibunuh dulu (rank 1) sebelum pod API yang Guaranteed (rank 2) tersentuh.
Failure Mode Umum
| Gejala | Penyebab | Perbaikan |
|---|---|---|
Pod Pending terus, node "kosong" di monitoring |
Requests ketat + fragmentasi. Scheduler lihat requests, bukan usage | Turunkan requests, aktifkan bundling / LeastAllocated, cek VPA rekomendasi |
| Latency p99 naik, CPU usage rendah | CPU limit → CFS throttling | Hapus CPU limit untuk service latency-sensitive, andalkan requests saja |
OOMKilled (exit 137) sporadis |
Memory limit terlalu ketat, spike alami | Naikkan limit, aktifkan memoryReservationPolicy, atau perbaiki leak |
| Pod mati tiap beberapa menit di satu node | Node-pressure eviction (memory.available) | Turunkan requests, perbaiki overcommit, cek node allocatable |
| CoreDNS mati saat malam | Priority class app > system-critical | Turunkan priority app, atau beri reservasi eksplisit untuk system pods |
Pod Deferred nyangkut (v1.35+) |
Jadi headroom penuh, preemption belum aktif | Update ke v1.37 + aktifkan gate, atau sisakan buffer |
| Pod Burstable dibunuh saat disk pressure, bukan memory | Eviction signal berbeda (nodefs), QoS yang sama tetap ranking | Cek semua signal, tidak hanya memory |
| Node "melaporkan memory penuh" padahal page cache besar | Kubelet hitung memory.available dari workingSet |
Pahami rumus; pertimbangkan memoryReservationPolicy |
Latihan Praktis Kecil
Latihan 1 — Verifikasi QoS. Ambil pod yang sedang jalan, ubah manifestnya jadi Burstable vs Guaranteed, apply, cek .status.qosClass. Konfirmasi di cgroup: cat /sys/fs/cgroup/.../cpu.weight.
Latihan 2 — Reproduksi throttling. Limit CPU 200m, jalankan beban yes > /dev/null. Cek container_cpu_cfs_throttled_periods_total di endpoints kubelet /metrics. Lalu hapus CPU limit dan bandingkan.
Latihan 3 — Peta priority. Buat tiga PriorityClass (low/medium/high), jalankan tiga pod di satu node, lalu isi node sampai penuh. Amati siapa yang di-preempt (kubelet event Preempted).
Latihan 4 — Baca eviction. Set evictionHard.memory.available: 200Mi, dorong node ke pressure, baca /var/log/pods dan event kubelet. Catat urutan eviction vs QoS yang Bapak harapkan.
Poin Kunci
- Scheduler = requests. Kubelet = limits + usage. Dua sumber kebenaran yang berbeda; kebingungan di sini pangkal 80% masalah kapasitas.
- QoS bukan pilihan, ia turunan. Turunan dari hubungan requests vs limits di seluruh container.
- Eviction ≠ Preemption. Eviction dipicu resource pressure (kubelet); preemption dipicu pod pending (scheduler). PDB hanya melindungi dari preemption/API eviction, tidak dari node-pressure eviction.
- CPU limit = throttle, memory limit = OOM. Menaikkan CPU limit tidak pernah memperbaiki throughput yang sudah cukup requests.
- Hanya pod di atas requests yang di-evict. Requests adalah "pelindung"; tidak ada cara lain.
- In-place resize kini GA (v1.35), dan preemption-nya Alpha (v1.37) — memungkinkan bin-packing agresif tanpa memblokir scale-up workload kritis.
- Priority class adalah senjata. Salah pakai = membunuh komponen sistem sendiri.
Eksplorasi Lanjut
- Descheduler — kapan pod perlu dikutip ulang dari node yang tidak lagi optimal? Bandingkan strategi
RemoveDuplicates,LowNodeUtilization,RemovePodsViolatingNodeAffinity. - Topology spread constraints — jamin replika tersebar antar zone/rack, dan bagaimana interaksinya dengan eviction satu node.
- Kueue / Volcano — scheduling antrian untuk batch/ML, perbandingan dengan PriorityClass biasa.
- cgroup v2
memory.highvsmemory.max— di mana kubelet menetapkanmemoryThrottlingFactordan implikasinya. - Node allocatable reservation —
kubeReserved,systemReserved,evictionHard— dan bagaimana ketiganya menentukan daya tampung nyata node. - Scheduler plugin framework — tulis custom scoring plugin (mis. scoring berbasis biaya listrik/idle timeout) sebagai latihan lanjutan.
Referensi terverifikasi:
- Kubernetes Docs — Pod QoS Classes, Node-pressure Eviction, Pod Priority and Preemption (updated Jul 2026)
- Kubernetes Blog — In-Place Pod Resize GA, v1.35; Scheduler Preemption for In-Place Pod Resize, v1.37, 10 Sep 2026