Kubernetes ortamlarında zaman zaman açıklaması zor performans problemleriyle karşılaşabiliyoruz.
Dashboard’a baktığınızda CPU kullanımı %30–40 seviyelerinde görünür, node kaynakları yeterlidir, pod’larda belirgin bir problem yoktur. Buna rağmen özellikle p95 ve p99 latency değerleri beklenmedik şekilde yükselmeye başlar.
Böyle bir durumda problemin kaynağı her zaman CPU yetersizliği olmayabilir.
Asıl sorun, uygulamanın CPU kullanamaması olabilir.
Bunun arkasındaki en önemli nedenlerden biri ise Kubernetes üzerinde tanımladığımız CPU Limitleri ve CPU Throttling mekanizmasıdır.
CPU Limiti Aslında Nasıl Çalışıyor?
Kubernetes üzerinde bir container için aşağıdaki gibi bir CPU limiti tanımladığımızı düşünelim:
resources:
requests:
cpu: "1"
limits:
cpu: "1"
Buradaki 1 CPU, container’ın fiziksel olarak yalnızca tek bir CPU çekirdeğine bağlandığı anlamına gelmez.
Linux tarafında CPU kullanımı cgroup ve scheduler mekanizmaları üzerinden belirli bir zaman kotası ile sınırlandırılır.
Basitleştirilmiş bir örnek üzerinden ilerleyelim.
CPU quota periyodunun 100 ms olduğunu düşünürsek, container’a 1 CPU limiti verildiğinde uygulama bu 100 ms’lik zaman dilimi içerisinde toplamda yaklaşık 100 ms CPU zamanı kullanabilir.
İlk bakışta oldukça mantıklı görünüyor.
Ancak uygulama çok thread’li çalışıyorsa işler değişmeye başlıyor.
Örneğin JVM üzerinde çalışan bir servisin aynı anda 8 thread üzerinden yoğun CPU kullandığını düşünelim.
Bu thread’ler paralel şekilde CPU tüketebiliyorsa, kendilerine ayrılan toplam CPU kotasını teorik olarak çok kısa bir zaman içerisinde tüketebilirler.
Örneğin:
CPU Limit : 1 CPU
CPU Quota : 100 ms
Aktif Thread : 8
100 ms / 8 ≈ 12,5 ms
Uygulama, periyodun ilk bölümünde CPU kotasını tükettiğinde scheduler kalan süre boyunca container’ın CPU kullanımını sınırlar.
İşte bu duruma CPU throttling diyoruz.
Asıl Problem Burada Başlıyor
Container CPU kotasını tükettiğinde uygulama tamamen çökmeyebilir.
Pod çalışmaya devam eder.
Health check’ler başarılı olabilir.
CPU grafiği de gayet normal görünebilir.
Fakat uygulama belirli aralıklarla CPU kullanamadığı için gelen istekler beklemeye başlar.
Sonuçta özellikle:
- API response süreleri uzar,
- request queue büyüyebilir,
- p95 ve p99 latency yükselir,
- JVM thread’leri beklemeye başlar,
- GC süreleri etkilenebilir,
- uygulamada zaman zaman açıklanamayan performans sıçramaları görülebilir.
İşin yanıltıcı tarafı ise CPU dashboard’una baktığınızda ortalama kullanımın hâlâ düşük görünmesidir.
Örneğin Grafana üzerinde:
CPU Usage: %35
görürken aynı serviste:
p99 Latency: 2.5 saniye
gibi değerlerle karşılaşabilirsiniz.
Burada sorun CPU’nun tamamen dolu olması değil, uygulamanın ihtiyaç duyduğu anda CPU’ya erişememesidir.
CPU Throttling Nasıl Tespit Edilir?
Prometheus kullanıyorsanız bakmanız gereken önemli metriklerden biri:
container_cpu_cfs_throttled_periods_total
Bunun yanında toplam CPU scheduling period sayısını gösteren:
container_cpu_cfs_periods_total
metriğini de takip edebilirsiniz.
Kabaca aşağıdaki oran bize throttling hakkında fikir verir:
Throttling Ratio =
container_cpu_cfs_throttled_periods_total
/
container_cpu_cfs_periods_total
PromQL tarafında örnek olarak:
rate(container_cpu_cfs_throttled_periods_total[5m])
/
rate(container_cpu_cfs_periods_total[5m])
kullanılabilir.
Buradaki oran yükselmeye başladığında container’ın CPU limiti nedeniyle düzenli olarak throttle edildiğini anlayabiliriz.
Ancak burada tek başına sabit bir eşik üzerinden karar vermek doğru olmayabilir.
Örneğin %1–2 seviyesinde throttling bazı uygulamalar için önemsizken, latency konusunda çok hassas bir API servisi için ciddi sonuçlar doğurabilir.
Bu nedenle metriği mutlaka aşağıdaki değerlerle birlikte değerlendirmek gerekir:
CPU Throttling
+
p95 / p99 Latency
+
Request Rate
+
CPU Usage
+
Application Thread Count
+
GC süreleri
Özellikle throttling artışı ile p99 latency artışı aynı zaman aralığında gerçekleşiyorsa CPU limitlerini incelemek oldukça anlamlıdır.
Çözüm CPU Limitini Kaldırmak mı?
Latency konusunda hassas servislerde kullanılan yaklaşımlardan biri CPU limitini kaldırmak ve yalnızca doğru bir CPU request değeri tanımlamaktır.
Örneğin:
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
memory: "4Gi"
Burada CPU için bir request tanımlanmış ancak CPU limit kaldırılmıştır.
Bu yaklaşım sayesinde scheduler pod’u yerleştirirken ihtiyaç duyduğu CPU miktarını dikkate alır ancak uygulama gerektiğinde node üzerindeki boş CPU kapasitesinden kısa süreli olarak faydalanabilir.
Bu durum özellikle burst karakteristiğine sahip uygulamalarda ciddi performans avantajı sağlayabilir.
Örneğin:
Normal CPU Kullanımı
↓
%30
↓
Ani trafik geliyor
↓
Uygulama kısa süreli %150–200 CPU kullanıyor
↓
İstek kuyruğu hızlı şekilde işleniyor
↓
CPU tekrar normal seviyeye geliyor
CPU limiti bulunduğunda ise bu kısa süreli burst engellenebilir.
Fakat CPU Limitini Kaldırmanın da Bir Riski Var
Burada özellikle Java uygulamalarında dikkat edilmesi gereken başka bir konu ortaya çıkıyor.
JVM çalışırken kendisine kullanılabilir görünen CPU sayısına göre bazı internal yapıları otomatik olarak boyutlandırabilir.
Örneğin:
- Garbage Collector thread sayısı,
- ForkJoinPool,
- parallel stream işlemleri,
- bazı framework thread pool’ları
kullanılabilir CPU sayısından etkilenebilir.
Eğer container üzerinde CPU limiti kaldırılırsa ve runtime ortamında JVM çok fazla işlemci görebiliyorsa uygulama beklediğinizden çok daha fazla thread oluşturabilir.
Örneğin 64 çekirdekli bir Kubernetes node üzerinde çalışan küçük bir servis düşünelim.
Servisin gerçek ihtiyacı:
2 CPU
olmasına rağmen JVM ortamı çok daha yüksek bir CPU kapasitesi olarak değerlendirebilir.
Bu durumda GC veya ForkJoinPool gibi bileşenler gereğinden fazla paralel çalışabilir.
Sonuç olarak aynı node üzerindeki diğer container’larla CPU rekabeti artabilir.
Yani bir tarafta throttling problemini çözerken diğer tarafta noisy neighbor problemine neden olabiliriz.
Java Tarafında ActiveProcessorCount Kullanımı
Bunun önüne geçebilmek için JVM’e uygulamanın kullanmasını istediğimiz CPU sayısını açıkça belirtebiliriz.
Örneğin:
-XX:ActiveProcessorCount=2
Bu durumda JVM kendi iç mekanizmalarını yaklaşık olarak 2 işlemcili bir sistem üzerinde çalışıyormuş gibi boyutlandırabilir.
Kubernetes tarafında:
resources:
requests:
cpu: "2"
kullanıyorsak JVM tarafında da:
-XX:ActiveProcessorCount=2
kullanılması değerlendirilebilir.
Buradaki sayı elbette her zaman doğrudan request değeriyle birebir olmak zorunda değildir. Uygulamanın workload karakteristiği, thread modeli ve JVM davranışı test edilerek doğru değer belirlenmelidir.
Memory Limitlerinde Aynı Yaklaşımı Uygulamayın
CPU ve memory kaynaklarını aynı şekilde değerlendirmemek gerekir.
CPU tükenebilir bir kaynak değildir. CPU yetersiz olduğunda uygulama çoğunlukla yavaşlar veya scheduler tarafından bekletilir.
Memory tarafında ise durum çok daha farklıdır.
Uygulama node üzerindeki belleği kontrolsüz şekilde tüketirse:
Memory Pressure
↓
OOM
↓
Container Kill
↓
Pod Restart
gibi çok daha ciddi sonuçlarla karşılaşabiliriz.
Bu nedenle CPU limitinin kaldırılması değerlendirilebilirken memory limitlerinin kontrolsüz şekilde kaldırılması aynı mantıkla ele alınmamalıdır.
Memory request ve limit değerleri uygulamanın gerçek ihtiyacına göre dikkatli şekilde belirlenmelidir.
Sadece CPU Grafiğine Bakmak Yetmez
Kubernetes performans problemlerinde yaptığımız en büyük hatalardan biri yalnızca CPU kullanım yüzdesine bakmaktır.
Aslında şu soruyu sormamız gerekir:
Uygulama ne kadar CPU kullanıyor?
yerine:
Uygulama CPU’ya ihtiyaç duyduğu anda CPU kullanabiliyor mu?
Bu iki soru birbirinden oldukça farklıdır.
Özellikle production ortamlarında aşağıdaki metriklerin birlikte izlenmesi çok daha anlamlı sonuç verir:
CPU Usage
CPU Request
CPU Limit
CPU Throttling
p95 Latency
p99 Latency
Request Rate
Thread Count
GC Duration
Pod Restart
Node CPU Pressure
Bunları aynı dashboard üzerinde korelasyonlu şekilde değerlendirmek sorunun kaynağını bulmayı ciddi şekilde kolaylaştırır.
Kubernetes üzerinde yaptığımız her kaynak sınırlaması aslında uygulamanın çalışma davranışına doğrudan müdahale eder.
CPU limitleri ilk bakışta son derece mantıklı bir güvenlik önlemi gibi görünür. Bir container’ın node üzerindeki tüm CPU kaynaklarını tüketmesini engellemek isteriz.
Ancak özellikle yüksek trafikli, çok thread’li ve latency konusunda hassas uygulamalarda fazla agresif CPU limitleri beklenmeyen performans sorunlarına neden olabilir.
Üstelik problem çoğu zaman CPU kullanım grafiğinde açık şekilde görünmez.
Dashboard üzerinde %30 CPU görürken arka planda container sürekli throttle oluyor olabilir.
Bu nedenle Kubernetes kaynak planlamasını yalnızca:
CPU Request
CPU Limit
Memory Request
Memory Limit
değerlerinden ibaret düşünmemek gerekir.
Uygulamanın;
- çalışma modeli,
- thread mimarisi,
- JVM davranışı,
- trafik karakteristiği,
- burst ihtiyacı,
- latency hedefleri
ile Kubernetes kaynak yönetimi birlikte değerlendirilmelidir.
Çünkü Kubernetes tarafında oldukça masum görünen küçük bir konfigürasyon değişikliği, uygulama katmanında p99 latency değerlerinin neden bir anda yükseldiğinin cevabı olabilir.
Peki siz production Kubernetes ortamlarında CPU throttling problemlerini nasıl yönetiyorsunuz? CPU limitlerini kullanmaya devam ediyor musunuz, yoksa latency hassas servislerde yalnızca request tanımlamayı mı tercih ediyorsunuz?

