Resource Request & Limit #
Di dalam kluster Kubernetes yang padat, pengelolaan kapasitas komputasi merupakan pilar utama dari kestabilan sistem. Tanpa adanya pembatasan yang jelas, satu kontainer yang mengalami kebocoran memori (memory leak) atau lonjakan beban CPU dapat dengan mudah melumpuhkan seluruh kontainer lain yang berada di dalam node worker yang sama. Untuk mencegah anarki operasional ini, Kubernetes menyediakan mekanisme deklarasi resource melalui dua parameter kunci: Resource Requests dan Resource Limits.
Namun, menuliskan angka pada manifes YAML hanyalah langkah awal. Implikasi dari deklarasi tersebut sangatlah mendalam, karena memengaruhi cara kerja scheduler di control plane hingga interaksi kernel Linux tingkat rendah di dalam node worker. Artikel ini akan membahas secara mendalam perbedaan semantik antara request dan limit, bagaimana kernel Linux (melalui cgroups) mengendalikan CPU dan RAM, cara menentukan nilai yang akurat berdasarkan data empiris, serta tata kelola resource di tingkat namespace menggunakan LimitRange dan ResourceQuota.
Perbedaan Semantik: Kontrak Scheduler vs Kontrak Runtime #
Banyak tim DevOps pemula menyamakan arti dari request dan limit, padahal keduanya adalah dua kontrak yang sama sekali berbeda dan beroperasi pada fase daur hidup Pod yang berbeda pula.
Berikut adalah tabel perbandingan esensial antara Resource Requests dan Resource Limits:
| Karakteristik | Resource Requests | Resource Limits |
|---|---|---|
| Fase Utama | Penjadwalan (Scheduling) | Berjalan (Runtime) |
| Pihak yang Mengontrol | kube-scheduler di Control Plane |
Container Runtime & Kernel Linux (cgroups) |
| Fungsi Utama | Menjamin sisa kapasitas minimal pada node | Membatasi konsumsi maksimal kontainer |
| Dampak CPU Berlebih | Tidak ada pembatasan (selama CPU fisik menganggur) | CPU Throttling (proses kontainer melambat) |
| Dampak RAM Berlebih | Tidak ada pembatasan langsung | OOMKilled (proses dibunuh seketika, exit code 137) |
| Penentuan Lokasi Pod | Sangat menentukan node mana yang akan dipilih | Tidak memengaruhi keputusan penjadwalan |
Untuk memahami bagaimana kedua parameter ini bekerja secara end-to-end, mari kita perhatikan diagram siklus hidup alokasi resource berikut:
flowchart TD
PodCreate["Pod Baru Dideklarasikan"] --> SpecRead["Baca spec.resources"]
subgraph SchedulerPhase ["Tahap 1: Penjadwalan (Scheduling)"]
SpecRead --> CheckRequest["Evaluasi CPU & RAM 'requests'"]
CheckRequest --> FilterNodes["Filter Node: Sisa kapasitas allocatable >= request"]
FilterNodes --> MatchNode["Tempatkan Pod di Node Terpilih"]
end
subgraph RuntimePhase ["Tahap 2: Runtime (Cgroups & Kernel)"]
MatchNode --> RunContainer["Jalankan Kontainer via Containerd"]
RunContainer --> ApplyCgroups["Terapkan limit via Linux Cgroups"]
ApplyCgroups --> CPUMonitor{"Kontainer menggunakan\nCPU > limit?"}
CPUMonitor -- "Ya" --> CPUThrottle["Throttle CPU (CFS Quota) -. Aplikasi Melambat .-> Kontainer Tetap Hidup"]
CPUMonitor -- "Tidak" --> CPUNormal["CPU Berjalan Normal"]
ApplyCgroups --> RAMMonitor{"Kontainer menggunakan\nRAM > limit?"}
RAMMonitor -- "Ya" --> RAMKill["Kernel OOM-Killer Aktif -. Exit Code 137 .-> Kontainer Mati (OOMKilled)"]
RAMMonitor -- "Tidak" --> RAMNormal["RAM Berjalan Normal"]
end
Mekanisme CPU: Compressible Resource #
CPU di dalam Kubernetes dikategorikan sebagai compressible resource (sumber daya yang dapat ditekan). Artinya, jika kontainer kita mencoba menggunakan CPU melebihi kapasitas yang diizinkan, kernel Linux tidak akan membunuh proses kontainer tersebut. Sebaliknya, kernel akan membatasi atau “menekan” jatah waktu CPU untuk kontainer itu, membuat jalannya aplikasi melambat.
1. CPU Requests dan Linux cpu.shares
#
Di bawah kap, ketika kita menetapkan nilai request CPU (misalnya cpu: "250m" atau 0.25 core), Kubernetes menerjemahkan nilai ini menjadi unit bobot cpu.shares pada sistem cgroups Linux.
- Secara standar, 1 Core CPU (1000m) setara dengan 1024 shares.
- Maka,
cpu: "250m"akan diterjemahkan menjadi 256 shares.
cpu.shares adalah jatah proporsional yang hanya akan ditegakkan ketika seluruh CPU fisik di node worker mengalami perebutan beban kerja 100%.
- Jika kluster sedang sepi, sebuah kontainer yang hanya meminta
250mdiperbolehkan menggunakan hingga 100% CPU node jika diperlukan. - Namun, jika semua Pod di node mendadak sibuk, kernel Linux akan membagi siklus CPU secara adil berdasarkan proporsi shares masing-masing. Pod dengan CPU request
1000mdijamin akan mendapatkan jatah CPU 4 kali lipat lebih banyak daripada Pod yang meminta250m.
2. CPU Limits dan CFS Quota #
Berbeda dengan request, CPU limits diterjemahkan menggunakan mekanisme CFS (Completely Fair Scheduler) Bandwidth Control pada kernel Linux melalui parameter cpu.cfs_quota_us dan cpu.cfs_period_us.
- Period (
cfs_period_us): Durasi jendela waktu evaluasi (default: 100.000 mikrodetik atau 100 ms). - Quota (
cfs_quota_us): Jatah waktu aktif CPU yang diizinkan untuk digunakan kontainer di dalam jendela period tersebut.
Jika kita menetapkan limits.cpu: "0.5" (setengah core), cgroups akan dikonfigurasi dengan:
$$\text{period} = 100\text{ ms}$$
$$\text{quota} = 50\text{ ms}$$
Artinya, dalam setiap 100 ms, kontainer kita hanya boleh berjalan selama total 50 ms. Jika kontainer kita menggunakan multi-threading dan langsung menghabiskan jatah 50 ms tersebut dalam 10 ms pertama (misalnya dengan memakai 5 thread secara bersamaan), maka untuk sisa 90 ms berikutnya proses kontainer kita akan di-throttle (dihentikan sementara) oleh kernel.
Bahaya CPU Throttling pada Aplikasi Latency-Critical #
CPU Throttling adalah pembunuh tersembunyi bagi performa aplikasi. Gejalanya meliputi:
- Lonjakan latensi API secara tiba-tiba (latency spikes).
- Gagalnya HTTP Readiness Probe akibat kontainer terlambat merespon ping Kubelet.
- Penumpukan antrean koneksi TCP.
Kita dapat memantau tingkat throttling ini dari dashboard Prometheus menggunakan metrik:
sum(rate(container_cpu_cfs_throttled_seconds_total[5m])) by (container, pod)
Mekanisme Memori: Incompressible Resource #
Memori fisik (RAM) dikategorikan sebagai incompressible resource (sumber daya yang tidak dapat ditekan). Jika sebuah proses membutuhkan 1 byte memori tambahan untuk alokasi variabel baru, kita tidak dapat menekan memori tersebut; memori itu harus ada secara fisik. Jika memori fisik habis, kernel Linux terpaksa mengambil tindakan ekstrem.
1. Memory Requests #
Memory requests digunakan oleh kube-scheduler untuk menghitung kapasitas tersisa node worker saat fase penjadwalan.
- Di tingkat kernel node, memory requests tidak menetapkan batasan keras apa pun pada runtime cgroups (tidak ada padanan langsung untuk
cpu.sharespada memori dalam cgroups v1). - Namun, pada Kubernetes modern yang menggunakan cgroups v2, request memori diterjemahkan menjadi nilai
memory.lowataumemory.minuntuk memberikan proteksi pemadaman (page-reclaim protection) agar memori milik Pod penting tidak mudah dibersihkan oleh kernel saat terjadi memory pressure.
2. Memory Limits dan Bencana OOMKilled #
Ketika kita menetapkan limits.memory: "512Mi", cgroups akan menuliskan nilai pembatasan keras ini ke dalam parameter file memory.limit_in_bytes.
Begitu kontainer mencoba mengalokasikan RAM hingga melampaui batas 512MiB tersebut:
- Cgroup OOM Trigger: Subsistem cgroup memory controller mendeteksi pelanggaran batas.
- Kernel OOM-Killer: Kernel Linux langsung mengaktifkan modul Out of Memory (OOM) Killer khusus untuk cgroup tersebut.
- Proses Dibunuh: Kernel memilih proses utama di dalam kontainer yang mengonsumsi RAM terbesar untuk dibunuh seketika menggunakan sinyal
SIGKILL. - Exit Code 137: Kontainer mati mendadak dan Kubelet mencatat statusnya sebagai
OOMKilleddengan kode keluar137(yang merupakan hasil dari kode sinyal sistem $128 + 9 \text{ (SIGKILL)} = 137$).
# Periksa apakah ada Pod yang terkena OOMKilled baru-baru ini
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}{end}' | grep OOMKilled
[!CAUTION] Menyetel memori limit terlalu rendah pada aplikasi berbasis JVM (seperti Java Spring Boot) adalah resep instan menuju kegagalan produksi. JVM membutuhkan ruang tambahan di luar tumpukan memori utama (Java Heap) untuk menyimpan Metaspace, thread stacks, garbage collection overhead, dan memori off-heap JNI. Jika kita menyetel
-Xmx512m(maksimal heap 512MB), kita wajib menetapkan limit memori Kubernetes minimal 768MB atau 1GB agar proses JVM tidak dibunuh oleh Kernel OOM-Killer.
Tata Kelola Resource di Tingkat Namespace #
Untuk mencegah satu tim atau satu aplikasi menyabotase seluruh kapasitas kluster akibat kelalaian penulisan manifes, kita harus menetapkan pagar pembatas (guardrails) di tingkat namespace menggunakan objek LimitRange dan ResourceQuota.
1. LimitRange: Menegakkan Default dan Batasan Kontainer #
LimitRange adalah objek kebijakan yang secara otomatis menyuntikkan nilai request/limit default ke kontainer yang tidak mendefinisikannya, serta menolak pembuatan Pod jika nilainya melanggar ambang batas minimal/maksimal yang kita izinkan.
Berikut adalah manifes LimitRange standar untuk namespace produksi:
apiVersion: v1
kind: LimitRange
metadata:
name: production-limit-range
namespace: production
spec:
limits:
- type: Container
# Jika developer tidak menuliskan spec.resources, nilai di bawah ini otomatis disuntikkan:
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "200m"
memory: "256Mi"
# Menolak pembuatan Pod jika melompati batas maksimal:
max:
cpu: "2000m"
memory: "4Gi"
# Menolak pembuatan Pod jika kurang dari batas minimal:
min:
cpu: "100m"
memory: "128Mi"
2. ResourceQuota: Membatasi Akumulasi Total Namespace #
Jika LimitRange mengatur ukuran masing-masing kontainer, ResourceQuota membatasi jumlah akumulasi total konsumsi dari seluruh objek yang berjalan di dalam namespace tersebut.
Berikut adalah manifes ResourceQuota untuk mencegah satu tim memonopoli kluster:
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-resource-quota
namespace: production
spec:
hard:
# Batasi total akumulasi request dari seluruh Pod:
requests.cpu: "20"
requests.memory: "40Gi"
# Batasi total akumulasi limit dari seluruh Pod:
limits.cpu: "40"
limits.memory: "80Gi"
# Batasi jumlah objek fisik yang boleh dibuat:
pods: "50"
services: "25"
persistentvolumeclaims: "10"
Jika namespace production sudah memiliki total CPU request terakumulasi sebesar 19 Core, dan ada tim yang mencoba mendeploy Pod baru dengan request CPU 2 Core, API Server akan langsung menolak request tersebut dengan pesan error: Forbidden: exceeded quota.
Panduan Praktis Menentukan Nilai Requests & Limits #
Menentukan nilai resource tidak boleh dilakukan dengan tebakan kasar (guesswork). Kita harus menggunakan data empiris untuk mencapai titik keseimbangan antara efisiensi biaya (cost efficiency) dan keandalan operasional (reliability).
Langkah 1: Kumpulkan Data Penggunaan Aktual #
Jalankan beban kerja kita di lingkungan staging dengan load testing yang merepresentasikan trafik puncak produksi. Gunakan perintah CLI dasar untuk melihat konsumsi riil:
kubectl top pods -n production --containers
Untuk analisis jangka panjang, perhatikan grafik Prometheus untuk metrik memori:
container_memory_working_set_bytes{container="nama-kontainer", pod="nama-pod"}
[!TIP] Selalu gunakan metrik
container_memory_working_set_bytesuntuk menghitung kebutuhan RAM kontainer, bukancontainer_memory_rss. Metrik Working Set mencakup memori RSS ditambah memory cache aktif yang tidak dapat dilepas oleh kernel. Angka inilah yang dijadikan acuan oleh OOM-killer untuk membunuh kontainer.
Langkah 2: Gunakan Rumus Empiris Penentuan Resource #
Setelah kita mendapatkan profil penggunaan rata-rata (Average) dan puncak (Peak) selama load testing, terapkan rumus berikut:
A. Komputasi CPU #
CPU requests disetel rendah untuk efisiensi, namun limits disetel longgar agar aplikasi dapat melakukan proses secara cepat (bursting) saat startup atau lonjakan request: $$\text{CPU Requests} = \text{CPU Usage (Average)}$$ $$\text{CPU Limits} = \text{CPU Usage (Peak)} \times 1.5 \text{ s.d. } 3.0$$
B. Komputasi Memori #
Karena memori adalah incompressible resource yang berakibat kematian Pod jika terlampaui, kita harus bersikap konservatif: $$\text{Memory Requests} = \text{Memory Usage (Average)} \times 1.1$$ $$\text{Memory Limits} = \text{Memory Usage (Peak)} \times 1.25 \text{ s.d. } 1.5$$
Langkah 3: Gunakan Vertical Pod Autoscaler (VPA) #
Daripada menghitung manual, kita dapat memanfaatkan Vertical Pod Autoscaler (VPA) dalam mode Recommendation untuk menganalisis perilaku aplikasi secara otomatis:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa-recommender
namespace: production
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: api-app
updatePolicy:
updateMode: "Off" # Hanya memberikan rekomendasi di status, tidak mengubah Pod secara paksa
Setelah berjalan beberapa hari, jalankan kubectl describe vpa api-vpa-recommender untuk melihat rekomendasi resource yang ideal.
Anti-Pattern dalam Manajemen Resource & Solusinya #
Berikut adalah tiga kesalahan konfigurasi resource yang paling sering merusak stabilitas kluster produksi:
Anti-Pattern 1: Tidak Menetapkan Resource Requests & Limits Sama Sekali #
Mendeploy manifest Pod tanpa menyertakan blok resources karena menganggap Kubernetes akan mengatur segalanya secara ajaib.
ANTI-PATTERN: Mendeploy Pod Aplikasi Tanpa Blok resources
// KITA MELAKUKAN:
- Menulis manifes Deployment web API tanpa mendefinisikan requests maupun limits.
// KONSEKUENSI DI PRODUKSI:
- Kelas QoS Terendah: Pod otomatis dikategorikan ke dalam QoS class **BestEffort**.
- Kerentanan Eviction: Saat node worker mengalami tekanan resource dikit saja,
Pod BestEffort adalah korban pertama yang akan diusir (evicted) secara paksa oleh Kubelet.
- Monopoli Resource: Jika kontainer kita mengalami memory leak, ia akan terus memakan RAM
hingga 100% kapasitas node worker fisik, mengakibatkan matinya seluruh Pod sehat lain di node tersebut.
✓ SOLUSI YANG BENAR:
- Selalu tegakkan aturan bahwa tidak boleh ada kontainer yang meluncur tanpa deklarasi resources.
- Pasang LimitRange di setiap namespace produksi untuk menyuntikkan nilai default
jika developer lupa menuliskan spesifikasi resource.
Anti-Pattern 2: Menyamakan CPU Requests dan CPU Limits (Guaranteed QoS) Secara Membabi Buta #
Menyamakan nilai request dan limit CPU di seluruh aplikasi microservice demi kepraktisan penulisan YAML.
ANTI-PATTERN: Menyetel requests.cpu: "1" dan limits.cpu: "1" untuk Seluruh Microservice
// KITA MELAKUKAN:
- Menyamakan request dan limit CPU di angka 1 Core pada semua Deployment aplikasi stateless.
// KONSEKUENSI DI PRODUKSI:
- Utilisasi Kluster Sangat Rendah: Scheduler akan mengunci 1 Core CPU fisik secara permanen untuk
masing-masing Pod. Jika kita memiliki 50 Pod, kita butuh 50 Core fisik di kluster. Padahal penggunaan
riil rata-rata seluruh Pod tersebut mungkin hanya 0.05 Core. Kita membuang biaya cloud hingga 90% secara sia-sia.
- Hambatan Startup: Aplikasi web (terutama Node.js dan Java) membutuhkan CPU burst tinggi
saat proses bootstrap awal. Tanpa adanya ruang limit di atas request, proses startup akan
terhambat throttling parah, membuat Pod lama menyala atau hang saat startup.
✓ SOLUSI YANG BENAR:
- Berikan ruang burst yang longgar pada CPU. Setel request CPU sesuai kebutuhan rata-rata runtime
(misal `requests.cpu: "100m"`), dan berikan limit yang lebih tinggi (misal `limits.cpu: "500m"` s.d. `"1000m"`).
- Kecualikan pola Guaranteed QoS hanya untuk aplikasi database database-stateful atau aplikasi
tingkat latensi mikro (*microsecond latency*) yang tidak boleh mengalami context switching sama sekali.
Anti-Pattern 3: Menghitung Memori Limit Terlalu Mepet Tanpa Buffer Runtime #
Menetapkan batas limit memori kontainer yang sama persis dengan batasan heap internal bahasa pemrograman.
ANTI-PATTERN: Menyetel runtime Java -Xmx512m dan limits.memory: "512Mi"
// KITA MELAKUKAN:
- Kita membatasi heap maksimal Java (JVM) sebesar 512MB di variabel lingkungan.
- Kita menyetel limit memori pod Kubernetes sebesar 512MiB.
// KONSEKUENSI DI PRODUKSI:
- OOMKilled Berulang: JVM membutuhkan memori tambahan di luar heap untuk menjalankan internal engine-nya
(Class metadata, thread stacks, memory garbage collection, compilation cache). Total konsumsi memori
riil proses Java tersebut di sistem operasi akan mencapai ~650MB.
- Karena limit kita kunci di 512MB, Kernel Linux akan membunuh kontainer Spring Boot kita
seketika (OOMKilled) sebelum aplikasi berhasil melayani request HTTP pertama.
✓ SOLUSI YANG BENAR:
- Selalu berikan buffer memori minimal 25% hingga 40% di luar batas heap memory runtime bahasa pemrograman.
- Jika JVM heap diatur sebesar 512MB, maka batasan memori Kubernetes minimal diatur sebesar 768MiB atau 1GiB.
- Pantau metrik memori secara berkala dan sesuaikan buffer ini berdasarkan data riil working set bytes.
Ringkasan #
- Request untuk Penjadwalan — Nilai
requestsdigunakan secara eksklusif olehkube-scheduleruntuk menyaring dan menempatkan Pod pada node worker yang memiliki kapasitas sisa memadai.- Limit untuk Pembatasan Runtime — Nilai
limitsditegakkan secara paksa di tingkat node worker oleh cgroups kernel Linux selama kontainer berjalan.- Throttling vs OOMKilled — Pelanggaran batas CPU berakibat pada pembatasan kecepatan pemrosesan (throttling), sedangkan pelanggaran batas memori berakibat pada pembunuhan instan (OOMKilled).
- Gunakan Working Set Bytes — Saat menganalisis kebutuhan RAM aplikasi lewat Prometheus, selalu jadikan metrik
container_memory_working_set_bytessebagai acuan utama.- Tegakkan LimitRange — Gunakan
LimitRangeuntuk menjamin semua kontainer di kluster secara otomatis mendapatkan nilai resource default dan mencegah kegagalan BestEffort di produksi.- ResourceQuota untuk Proteksi Kluster — Terapkan
ResourceQuotadi tingkat namespace untuk membatasi konsumsi total tim dan mencegah monopoli resource komputasi kluster.- Beri Buffer Memori Runtime — Selalu sediakan memori buffer yang cukup di luar kapasitas heap memori runtime (seperti JVM heap atau Node.js memory limit) guna menghindari bencana OOMKilled.