Kubernetes'te Scaling: HPA, VPA, In-Place Resize, KEDA ve PodDisruptionBudget
HPA CPU yüzdesini node'a değil container'ın request'ine göre hesaplar; request yoksa hiç ölçeklemez. HPA v2 behavior ayarlarını, in-place resize sonrası .NET GC'nin yeni limiti görmemesini ve GC.RefreshMemoryLimit çözümünü, KEDA ile RabbitMQ queue'suna göre scale-to-zero'yu, drain'i kilitleyen PodDisruptionBudget'ı ve topology spread tuzağını gerçek kind çıktılarıyla görüyoruz.
Sürüm notu: Bu yazıdaki örnekler kind 0.33 ile Kubernetes 1.37, kubectl 1.37, metrics-server 0.9.1, KEDA (Kubernetes Event-driven Autoscaling) 2.21, RabbitMQ 4.4 ve .NET 10 ile denenmiştir.
Önce Tahmin Et
Bookstore API'sinin (Application Programming Interface) bir pod'u limits.memory: 256Mi ile çalışıyor. Kampanya günü bellek yetmiyor ve pod'u yeniden başlatmadan, Kubernetes 1.35'te GA (General Availability) olan in-place resize ile limiti 512Mi'ye çıkarıyorsun. Kubernetes "resize completed" diyor, restart sayısı hâlâ 0. Soru: içerideki .NET runtime'ının GC'si (Garbage Collector) artık 512Mi'ye göre mi çalışıyor, yoksa hâlâ 256Mi'ye göre mi? İkinci soru: CPU (Central Processing Unit) request'i tanımlanmamış bir Deployment'a CPU hedefli bir HPA (Horizontal Pod Autoscaler) bağlarsan ne olur, pod sayısı hiç artmaz mı, sonsuza kadar mı artar? Cevaplar yazının içinde, gerçek cluster çıktılarıyla.
HPA Neyi Ölçer: metrics-server ve Request'e Göre Yüzde
HPA bir controller'dır: belirli aralıklarla (varsayılan 15 saniye) hedef Deployment'ın pod'larının metriklerini okur, istenen replica sayısını hesaplar ve Deployment'ın scale subresource'unu günceller. CPU ve memory metrikleri için cluster'da metrics-server olması gerekir; managed cluster'larda genelde hazır gelir, kind'da biz kuruyoruz. kind'daki kubelet sertifikaları cluster CA (Certificate Authority) tarafından imzalanmadığı için metrics-server'ı yalnız test ortamında --kubelet-insecure-tls argümanıyla çalıştırdık:
kubectl apply -f metrics-server.yaml
kubectl top pods -n bookstore
NAME CPU(cores) MEMORY(bytes)
api-79544c7fc7-m95j8 4m 45Mi
api-79544c7fc7-rqdgf 3m 52Mi
HPA'nın "yüzde" dediği şey node'un CPU'su değil, container'ın requests.cpu değeridir. Request'i 100m olan pod 50m kullanıyorsa kullanım %50'dir. Bu yüzden request tanımlanmamış bir pod için yüzde hesaplanamaz. Aynı API'yi request'siz bir Deployment olarak (api-norequest) açıp ona da HPA bağladığımızda kubectl get hpa şunu gösterdi:
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
api Deployment/api cpu: 3%/60% 2 6 2 45s
api-norequest Deployment/api-norequest cpu: <unknown>/60% 2 6 2 45s
describe sebebi açıkça yazıyor:
ScalingActive False FailedGetResourceMetric the HPA was unable to compute the replica count: failed to get cpu utilization: missing request for cpu in container api-norequest of Pod api-norequest-5d9865c7b5-h7jd5
Yani ikinci sorunun cevabı: HPA ne artırır ne azaltır, minReplicas'ta donup kalır ve bunu yalnız event'lerde söyler. Dashboard'da "HPA var" görüp rahatlayan ekip yük geldiğinde iki pod'la kalır. Request değerlerinin nasıl seçileceğini serinin dokuzuncu yazısı (Resource Request ve Limit'ler) anlattı; HPA'nın doğru çalışması o değerlerin gerçekçi olmasına bağlı.
HPA v2 ve behavior: Ne Kadar Hızlı Büyüsün, Ne Kadar Yavaş Küçülsün
autoscaling/v2 HPA hem birden çok metriği hem de büyüme ve küçülme hızını tarif etmeyi destekler. Bookstore API için yazdığımız HPA:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
namespace: bookstore
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 2
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 120
policies:
- type: Pods
value: 1
periodSeconds: 60
Hesap basit: desiredReplicas = ceil(currentReplicas × currentMetric / targetMetric). Oran 1'e %10'dan yakınsa (varsayılan tolerance) hiçbir şey yapılmaz. behavior bu ham sonucu terbiye eder: scale-up'ta stabilization yok, ama 30 saniyede en fazla 2 pod eklenir; scale-down'da son 120 saniyedeki en yüksek öneri esas alınır ve dakikada en fazla 1 pod silinir. Yükü bir busybox pod'undan dört paralel döngüyle /api/diag/burn?seconds=1 çağırarak ürettik. kubectl get hpa api -w çıktısının önüne saat ekledik:
06:28:45 api Deployment/api cpu: 3%/60% 2 6 2 60s
06:29:00 api Deployment/api cpu: 135%/60% 2 6 2 75s
06:29:15 api Deployment/api cpu: 1914%/60% 2 6 4 90s
06:29:45 api Deployment/api cpu: 814%/60% 2 6 6 2m
06:31:00 api Deployment/api cpu: 1%/60% 2 6 6 3m15s
06:33:01 api Deployment/api cpu: 1%/60% 2 6 5 5m16s
06:34:01 api Deployment/api cpu: 1%/60% 2 6 4 6m16s
06:35:01 api Deployment/api cpu: 1%/60% 2 6 3 7m16s
06:36:01 api Deployment/api cpu: 1%/60% 2 6 2 8m16s
Üç şeye dikkat et. %1914 bir hata değil: container'da CPU limit'i yok, request 100m, pod node'da boşta duran iki çekirdeği kullanabiliyor. Ham formül 2 × 1914 / 60 ≈ 64 pod isterdi; policy 2'şer ekledi, maxReplicas 6'da durdurdu. Load generator 06:30:24'te durdu, metrik 06:31:00'da %1'e indi, ama ilk scale-down 06:33:01'de geldi: 120 saniyelik pencere. Sonra her dakika bir pod. Ani bir düşüşte pod'ları hemen silmemek, birkaç dakika sonra gelecek ikinci dalgada cold start yaşamamak demek. .NET uygulamalarında cold start (JIT, connection pool, EF (Entity Framework) Core model'i) birkaç saniye sürdüğü için scale-down'ı yavaş tutmak genelde doğru tercih.
Kubernetes 1.37 ile GA olan bir ayrıntı daha var: behavior.scaleUp.tolerance ve behavior.scaleDown.tolerance. Cluster genelindeki %10'luk tolerance'ı HPA başına değiştirmeye yarar; 50 pod'luk bir Deployment'ta %10, beş pod'luk bir sapmayı yok saymak demektir. kubectl explain hpa.spec.behavior.scaleUp.tolerance alanın açıklamasını gösterir: ayarlanmazsa "the default cluster-wide tolerance is applied (by default 10%)".
VPA ve In-Place Resize: Pod'u Yatay Değil Dikey Büyütmek
HPA pod sayısını değiştirir; VPA (Vertical Pod Autoscaler) ise pod'un request ve limit değerlerini. VPA cluster'a ayrı kurulan bir projedir (kubernetes/autoscaler deposunda, Ekim 2026 itibarıyla 1.8.1). Geçmiş kullanımı izleyip öneri üretir, updateMode'a göre uygular: Off yalnız öneri yazar, Initial yalnız yeni pod'lara uygular, Recreate pod'u evict edip yeniden yaratır, InPlaceOrRecreate (VPA 1.6'da GA) önce pod'u yerinde büyütmeyi dener. Auto modu deprecated; bugün Recreate gibi davranıyor. Aynı Deployment'ta CPU üzerinden hem HPA hem VPA çalıştırmak klasik bir çatışmadır: biri pod ekler, öteki request'i büyütür, ikisi de birbirinin metriğini değiştirir.
VPA'nın yerinde büyütme yapabilmesini sağlayan çekirdek özellik in-place pod resize. Kubernetes 1.33'te beta olarak varsayılan açık geldi, 1.35'te GA oldu. Pod spec'indeki resources artık resize subresource'u üzerinden değiştirilebiliyor:
apiVersion: v1
kind: Pod
metadata:
name: api-resize
namespace: bookstore
spec:
containers:
- name: api
image: bookstore-api:1.2.0
ports:
- name: http
containerPort: 8080
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired
- resourceName: memory
restartPolicy: NotRequired
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: "1"
memory: 256Mi
kubectl -n bookstore patch pod api-resize --subresource resize --patch \
'{"spec":{"containers":[{"name":"api","resources":{"requests":{"cpu":"1","memory":"512Mi"},"limits":{"cpu":"2","memory":"512Mi"}}}]}}'
Normal ResizeStarted 5s kubelet Pod resize started: {"containers":[{"name":"api","resources":{"limits":{"cpu":"2","memory":"512Mi"},"requests":{"cpu":"1","memory":"512Mi"}}}],"generation":2}
Normal ResizeCompleted 5s kubelet Pod resize completed: {"containers":[{"name":"api","resources":{"limits":{"cpu":"2","memory":"512Mi"},"requests":{"cpu":"1","memory":"512Mi"}}}],"generation":2}
Restart sayısı 0. Şimdi baştaki soruya geldik. Bookstore API'nin /api/diag/runtime endpoint'i Environment.ProcessorCount ve GC.GetGCMemoryInfo().TotalAvailableMemoryBytes değerlerini döndürüyor. Resize'dan önce ve sonra:
{"processorCount":1,"isServerGC":false,"totalAvailableMemoryMB":192,"heapSizeMB":0,"gcHeapHardLimitMB":0,"workingSetMB":67}
{"processorCount":1,"isServerGC":false,"totalAvailableMemoryMB":192,"heapSizeMB":0,"gcHeapHardLimitMB":0,"workingSetMB":68}
Kubernetes cgroup limitini 512Mi'ye çıkardı, ama .NET hâlâ 192 MB görüyor (256Mi'nin %75'i, dokuzuncu yazıdaki varsayılan heap hard limit). Runtime bu değerleri process başlarken okur; CPU limiti 2 çekirdeğe çıktığı halde ProcessorCount da 1'de kaldı. .NET 8 ile gelen GC.RefreshMemoryLimit() GC'ye limitleri yeniden okutur. Bunu çağıran küçük bir endpoint ekledik:
// in-place resize sonrası cgroup memory limitini yeniden okur (.NET 8+)
app.MapPost("/api/diag/refresh-memory-limit", () =>
{
long before = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 1024 / 1024;
GC.RefreshMemoryLimit();
long after = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 1024 / 1024;
return Results.Ok(new { beforeMB = before, afterMB = after, processorCount = Environment.ProcessorCount });
});
{"beforeMB":192,"afterMB":384,"processorCount":1}
Memory tarafı düzeldi, ProcessorCount yine 1: thread pool ve Server GC heap sayısı gibi çekirdek sayısına bağlı kararlar process ömrü boyunca sabit kalıyor. Pratik sonuç şu: .NET servisinde memory resize'ın gerçekten işe yaraması için ya uygulama RefreshMemoryLimit çağırmalı ya da resizePolicy'de memory için RestartContainer seçilmeli. İkincisini denedik; container yeniden başladı (RESTARTS 1) ve yeni process 384 MB gördü:
NAME READY STATUS RESTARTS AGE
api-resize2 1/1 Running 1 (8s ago) 8s
{"processorCount":1,"isServerGC":false,"totalAvailableMemoryMB":384,"heapSizeMB":0,"gcHeapHardLimitMB":0,"workingSetMB":67}
Container restart'ı pod restart'ından ucuzdur: pod aynı node'da, aynı IP (Internet Protocol) adresiyle kalır, scheduling ve image pull yoktur. Resize'ın iki sınırı da gerçek çıktıyla görüldü. QoS (Quality of Service) class'ı değiştirecek bir resize reddedilir (Pod QOS Class may not change as a result of resizing), node'un kapasitesini aşan bir istek de bizim 1.37 cluster'ımızda API server tarafından anında geri çevrildi: node didn't have enough allocatable resources: cpu, requested: 32000, allocatable: 10000.
KEDA: Queue Uzunluğuna Göre Worker ve Scale-to-Zero
CPU, HTTP (Hypertext Transfer Protocol) API'si için iyi bir sinyaldir; queue tüketen bir worker için değildir. Sipariş onay e-postası gönderen Bookstore.Worker zamanının çoğunu SMTP (Simple Mail Transfer Protocol) beklemekle geçirir, CPU'su düşük kalır, queue ise büyür. KEDA bu boşluğu doldurur: queue, stream, cron gibi dış kaynakları okuyan scaler'ları vardır ve arka planda kendi yönettiği bir HPA yaratır. 0 ile 1 arasındaki geçişi ise HPA'ya bırakmadan kendisi yapar. Worker'ın tüketim kodu Message Queue'larla Çalışmak serisindeki RabbitMQ.Client 7 yazısıyla aynı; burada önemli olan iki satır prefetchCount: 1 ve manual ack:
public sealed class OrderEmailConsumer(IConfiguration configuration, ILogger<OrderEmailConsumer> logger)
: BackgroundService
{
private const string QueueName = "order-emails";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
ConnectionFactory factory = new() { Uri = new Uri(configuration["RabbitMq:Uri"]!) };
await using IConnection connection = await factory.CreateConnectionAsync(stoppingToken);
await using IChannel channel = await connection.CreateChannelAsync(cancellationToken: stoppingToken);
await channel.QueueDeclareAsync(QueueName, durable: true, exclusive: false, autoDelete: false,
cancellationToken: stoppingToken);
await channel.BasicQosAsync(prefetchSize: 0, prefetchCount: 1, global: false, stoppingToken);
AsyncEventingBasicConsumer consumer = new(channel);
consumer.ReceivedAsync += async (_, ea) =>
{
string orderId = Encoding.UTF8.GetString(ea.Body.Span);
// bir sipariş onay e-postasını hazırlayıp göndermeyi taklit eder
await Task.Delay(TimeSpan.FromMilliseconds(500), stoppingToken);
await channel.BasicAckAsync(ea.DeliveryTag, multiple: false, stoppingToken);
logger.LogInformation("Sent confirmation e-mail for order {OrderId}", orderId);
};
await channel.BasicConsumeAsync(QueueName, autoAck: false, consumer, stoppingToken);
logger.LogInformation("Consuming {Queue} on {Host}", QueueName, Environment.MachineName);
await Task.Delay(Timeout.Infinite, stoppingToken).ContinueWith(_ => { }, TaskScheduler.Default);
}
}
KEDA'yı Helm ile kurduk (helm install keda kedacore/keda --version 2.21.0 -n keda --create-namespace; Helm'in kendisi ilerideki Helm serisinin konusu). KEDA 2.21'in resmi test listesinde Kubernetes 1.34–1.36 var; 1.37 üzerinde bu yazıdaki senaryo sorunsuz çalıştı, ama production'da sürüm uyumluluk tablosuna bakmadan upgrade yapma. Worker Deployment'ı replicas: 0 ile başlıyor, ölçeği KEDA belirliyor:
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: rabbitmq
namespace: bookstore
spec:
secretTargetRef:
- parameter: host
name: rabbitmq
key: uri
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker
namespace: bookstore
spec:
scaleTargetRef:
name: worker
minReplicaCount: 0
maxReplicaCount: 5
pollingInterval: 5
cooldownPeriod: 30
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: order-emails
mode: QueueLength
value: "20"
authenticationRef:
name: rabbitmq
value: "20" "pod başına 20 mesaj" demek. Bağlantı adresi Secret'tan geliyor; AMQP (Advanced Message Queuing Protocol) URI'si (Uniform Resource Identifier) kullanıcı adı ve şifre içerdiği için manifest'e yazılmaz. Queue'ya 300 mesaj bastık:
06:33:45 keda-hpa-worker Deployment/worker <unknown>/20 (avg) 1 5 0 24s
06:33:52 keda-hpa-worker Deployment/worker 300/20 (avg) 1 5 1 31s
06:34:07 keda-hpa-worker Deployment/worker 75250m/20 (avg) 1 5 4 46s
06:34:22 keda-hpa-worker Deployment/worker 55800m/20 (avg) 1 5 5 61s
06:34:52 keda-hpa-worker Deployment/worker 0/20 (avg) 1 5 5 91s
06:35:22 keda-hpa-worker Deployment/worker <unknown>/20 (avg) 1 5 0 2m1s
Normal KEDAScaleTargetActivated 2m36s keda-operator Scaled apps/v1.Deployment bookstore/worker from 0 to 1, triggered by s0-rabbitmq-order-emails
Normal KEDAScaleTargetDeactivated 67s keda-operator Deactivated apps/v1.Deployment bookstore/worker from 5 to 0
KEDA 0'dan 1'e kendisi çıkardı, 1'den 5'e HPA taşıdı (75250m, pod başına 75,25 mesaj demek). Queue boşaldıktan sonra cooldownPeriod dolunca Deployment yeniden 0'a indi. Scale-to-zero'nun bedeli ilk mesajın gecikmesidir: pod'un schedule edilmesi, image'ın (yoksa) çekilmesi ve .NET host'unun açılması. Bizim denemede worker image'ı node'a yüklenmediği için ilk pod'lar ImagePullBackOff'a düştü; KEDA ölçeklemeyi doğru yaptı ama mesajlar ancak image yüklendikten sonra işlendi. Sipariş e-postası birkaç saniye gecikebilir; ödeme onayı gecikemiyorsa minReplicaCount: 1 bırak. Kubernetes 1.37'de HPA'nın kendi scale-to-zero desteği (HPAScaleToZero) varsayılan açık beta oldu, ama yalnız Object ve External metriklerle çalışıyor ve dış kaynaktan metrik sağlayacak bir adapter yine gerekiyor; queue senaryosunda KEDA hâlâ en kısa yol.
PodDisruptionBudget ve Topology Spread: Ölçeklerken Ayakta Kalmak
Pod sayısını artırmak, pod'ların aynı node'a yığılmadığı sürece işe yarar. topologySpreadConstraints pod'ları node'lara (ya da zone'lara) dengeli dağıtır. İlk denememiz kind'da beklenmedik bir şekilde takıldı:
Warning FailedScheduling 2m7s default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint(s), 2 node(s) didn't match pod topology spread constraints. preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption victims found for incoming pod.
Sebep: nodeTaintsPolicy'nin varsayılanı Ignore. Taint'li control-plane node'u da bir topology domain'i sayılıyor ve orada 0 pod var; worker'lardan birine üçüncü pod konursa skew 2 olur, maxSkew: 1 izin vermez. Rolling update sırasında eski ve yeni ReplicaSet'in pod'ları da birlikte sayıldığı için yeni pod hiç yer bulamadı. Düzeltilmiş hali:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
nodeTaintsPolicy: Honor
matchLabelKeys:
- pod-template-hash
labelSelector:
matchLabels:
app.kubernetes.io/name: api
matchLabelKeys: [pod-template-hash] yalnız aynı revision'ın pod'larını sayar. Production'da topology key genelde topology.kubernetes.io/zone olur; amaç bir zone kaybında servisin yarısının ayakta kalması.
PDB (PodDisruptionBudget) ise gönüllü kesintileri sınırlar: kubectl drain, cluster upgrade'i, cluster autoscaler'ın node silmesi gibi eviction API'si üzerinden gelen işlemleri. Pod'un çökmesi veya node'un kendiliğinden ölmesi PDB'yi umursamaz. İlk denemede minAvailable: 2 yazdık, replica sayısı da 2'ydi:
evicting pod bookstore/api-6c84998f86-9wqzq
error when evicting pods/"api-6c84998f86-9wqzq" -n "bookstore" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
...
error: unable to drain node "k8swork2-1-worker" due to error: error when evicting pods/"api-6c84998f86-9wqzq" -n "bookstore": global timeout reached: 30s, continuing command...
Replica sayısına eşit bir minAvailable "hiçbir pod gönüllü olarak gidemez" demektir; managed cluster'ın node upgrade'i saatlerce takılır ya da sağlayıcı belirli bir süre sonra PDB'yi yok sayar. Doğrusu maxUnavailable: 1:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
namespace: bookstore
spec:
maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/name: api
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
api N/A 1 1 3m19s
evicting pod bookstore/api-6c84998f86-9wqzq
pod/api-6c84998f86-9wqzq evicted
node/k8swork2-1-worker drained
maxUnavailable HPA ile büyüyen Deployment'larda daha iyi davranır: 2 replica'da da 6 replica'da da bir pod'a izin verir. Drain bir de cannot delete Pods that declare no controller hatasıyla durdu: controller'sız tek başına pod'lar (bizim test için açtığımız curl pod'u) drain'i bloklar. Production'da controller'sız pod olmamalı.
Node'lar Bitince: Cluster Autoscaler ve Karpenter
HPA'nın ekleyeceği pod için node'da yer yoksa pod Pending kalır. Node sayısını artırmak cluster seviyesinde bir iştir. Cluster Autoscaler, Pending pod'ları görüp bulut sağlayıcısının node group'unu büyütür: AKS (Azure Kubernetes Service) ve GKE (Google Kubernetes Engine) tarafında node pool'u, EKS (Amazon Elastic Kubernetes Service) tarafında managed node group'u; boş kalan node'ları da boşaltıp siler. Karpenter (AWS, yani Amazon Web Services kaynaklı; artık AKS'te de "node auto provisioning" adıyla var) önceden tanımlı node group yerine pod'ların ihtiyacına göre uygun VM (Virtual Machine) tipini seçip doğrudan açar. İkisi de node silerken eviction kullanır, yani PDB'ye uyar; PDB'si hiç izin vermeyen bir uygulama node'un silinmesini ve dolayısıyla maliyet düşüşünü engeller. Bu araçlar kind'da çalışmaz; managed cluster'a geçişi serinin on yedinci yazısında konuşacağız.
Üretimde Dikkat
Scaling zinciri en zayıf halkası kadar hızlıdır: metrics-server 15 saniyede bir ölçer, HPA 15 saniyede bir karar verir, yeni pod schedule edilir, image çekilir, .NET uygulaması açılır, readiness probe geçer. Bizim ölçümde ilk ölçek kararı yük başladıktan yaklaşık 30 saniye sonra geldi. Trafik saniyeler içinde patlıyorsa HPA yetişmez; minReplicas'ı normal trafiğin taşıyabileceği kadar yüksek tut, HPA'yı ani zirvelerin değil yavaş büyümenin aracı olarak gör. Yeni pod'un hızlı hazır olması için image'ı küçük tut, readiness'ı gerçek hazır olma anına bağla (serinin sekizinci yazısı). CPU hedefini %60–70 civarında seç: %90 hedef yeni pod gelene kadar mevcutları boğar. Ölçmek için kubectl describe hpa event'leri ve HPA'nın kendi metrikleri yeterli bir başlangıç; replica sayısını, queue derinliğini ve p95 gecikmeyi aynı grafikte görmek ilerideki Observability serisinin konusu. Son olarak, HPA bir Deployment'ı yönetiyorsa manifest'te replicas alanını sabit tutma; her kubectl apply HPA'nın kararını ezip replica sayısını geri çeker. Alanı manifest'ten kaldırmak en temiz yol.
Sık Düşülen Tuzaklar
Request'siz pod'a CPU hedefli HPA bağlamak: <unknown> hedef, sıfır ölçekleme. Memory'ye göre HPA kurmak: .NET'te GC belleği hemen iade etmediği için memory kullanımı yük kalksa da düşmeyebilir ve HPA hiç küçülmez; memory metriğini ancak gerçekten iş yüküyle orantılıysa kullan. In-place resize sonrası .NET'in yeni limiti göreceğini varsaymak: yukarıdaki çıktılar görmediğini gösteriyor. Aynı CPU metriği üzerinden HPA ve VPA'yı birlikte çalıştırmak. minAvailable'ı replica sayısına eşitlemek ve node upgrade'lerini kilitlemek. KEDA'da minReplicaCount: 0 seçip ilk mesajın gecikmesini hesaba katmamak. Topology spread'de control-plane'in ya da cordon'lanmış node'ların domain olarak sayıldığını unutmak. Son olarak scale-down'ın uzun süren isteği kesmesi: HPA'nın sildiği pod da SIGTERM (Unix'in sonlandırma sinyali) alır, graceful shutdown'ın (serinin onuncu yazısı) doğru kurulmamışsa her scale-down bir avuç 502 üretir.
Küçük Bir Deneme
★☆☆ HPA'daki scaleDown.stabilizationWindowSeconds değerini 0 yap ve yükü 30 saniye açıp kapat. kubectl get hpa -w çıktısında replica sayısının nasıl dalgalandığını önceki çıktıyla karşılaştır.
★★☆ Bookstore API pod'unu limits.memory: 256Mi ile çalıştır, POST /api/diag/alloc-managed?mb=150 ile belleği doldur, sonra pod'u 512Mi'ye resize et ve RefreshMemoryLimit çağırmadan önce ve sonra 150 MB'lık ikinci bir allocation dene. Hangisinde OutOfMemoryException görüyorsun?
★★★ KEDA ScaledObject'e ikinci bir trigger olarak cron scaler ekle: hafta içi 09:00–18:00 arasında en az bir worker ayakta kalsın, gece queue boşsa sıfıra insin. Hangi trigger'ın hangi anda belirleyici olduğunu kubectl describe hpa keda-hpa-worker ile izle.
Ölçeklenen pod'ların hepsi stateless'tı: hangisi silinse veri kaybolmuyordu. Bir sonraki yazıda işin zor tarafına geçiyoruz: volume, PersistentVolumeClaim ve StatefulSet ile durum tutan uygulamalar ve "veritabanını cluster'a koyalım mı" sorusu.