Ekim 17 2026

Kubernetes’te ‘Ghost’ Resource’ları Bulmak: Zombie Pod Avcılığı

Modern bir devops ekibinin yönettiği multi-tenant production cluster’ında her şeyin dashboard üzerinde yemyeşil görünmesi, her şeyin yolunda olduğu anlamına gelmez. Kümeler büyüdükçe, microservice mimarileri dallanıp budaklandıkça ve continuous delivery hatları günde onlarca kez release çıktıkça arka planda sessiz bir felaket birikir: “Ghost” (hayalet) resource’lar. Sağlıklı bir resource-management stratejisi ve keskin observability pratikleri kurgulamadığınızda, kubernetes worker node’larınız aslında hiçbir iş yapmayan, hiçbir trafiğe yanıt vermeyen ama faturayı kabartan “zombie pod” sürüleriyle dolar taşar.

Bu yazıda 5+ yıl deneyimli bir mühendisin bildiği o temel kavramları bir kenara bırakıyoruz. Pod nedir, replica nedir anlatmayacağım. Doğrudan sahaya inip kümedeki hayaletleri avlayacağız; neden oluştuklarını analiz edecek, gerçek komutlar, PromQL sorguları ve otomasyon script’leriyle bu yükü kümeden kazıyacağız.

Zombie Pod Nedir ve Neden Türer?

Kubernetes ekosisteminde bir kaynağa “zombie” veya “ghost” dememiz için ille de CrashLoopBackOff hatası alması gerekmez. Hatta en tehlikelisi, cluster’a göre kusursuz çalışanlardır. Bir pod şu durumlarda zombie statüsüne düşer:

  • Lich Podlar (Stuck in Terminating): Kapatılma sinyali (SIGTERM) almış fakat unmount edilemeyen volume veya takılı kalan bir finalizer yüzünden günlerdir Terminating durumunda bekleyenler.
  • Avare (Orphaned/Headless) Podlar: Bir Service selector’ı ile eşleşmeyen, ingress rotası olmayan veya Helm release’i purge edildiği halde etcd tutarsızlığı nedeniyle silinmeyenler.
  • Sıfır Yüklü “Over-provisioned” Podlar: Geliştiricinin debug amacıyla ayağa kaldırdığı, arkasında 4 vCPU / 16 GiB request unuttuğu ve haftalardır 0.001 Core tüketen test artıkları.
  • StatefulSet Kalıntıları (Unbound/Dangling PVCs): StatefulSet scale down edildiğinde Kubernetes mimari gereği PVC’leri silmez. Pod gitmiştir ama arkasında pahalı SSD block storage’lar yatmaya devam eder.

1. Adım: Terminating State’te Çakılı Kalan Podları Avlamak

En sık karşılaşılan senaryo, storage unmount işlemlerinin node üzerinde kilitlenmesi veya custom controller’ların yazdığı finalizer’ların reconcile edilememesidir. Bu podlar CPU/RAM harcamasa bile IP pool’unu (özellikle AWS-VPC CNI veya Calico kullanıyorsanız) rezerve tutar ve node drain işlemlerini bloklar.

Kümedeki tüm namespace’ler genelinde 1 saatten uzun süredir Terminating durumunda olan podları bulalım:

kubectl get pods -A --field-selector=status.phase!=Running \
  -o json | jq -r '
  .items[] | 
  select(.metadata.deletionTimestamp != null) | 
  select((now - (.metadata.deletionTimestamp | fromdateiso8601)) > 3600) | 
  "Namespace: " + .metadata.namespace + " | Pod: " + .metadata.name + " | Waiting: " + ((now - (.metadata.deletionTimestamp | fromdateiso8601))/3600 | tostring) + " hours"'

Birçok mühendis burada hemen kubectl delete pod <pod-name> --force --grace-period=0 komutuna sarılır. Yapmayın. Neden? Çünkü arkasında asılı bir CSI volume kilidi bırakırsanız, ileride o node’a schedule edilecek yeni pod Multi-Attach error for volume hatasıyla patlayacaktır.

Bunun yerine pod’un neden takıldığını görmek için metadata finalizer alanına bakın:

kubectl get pod <zombie-pod-name> -n <namespace> -o jsonpath='{.metadata.finalizers}'

Eğer storage driver çoktan node üzerinden çekildiyse ve controller yanıt vermiyorsa, finalizer’ı JSON merge patch ile söküp atın:

kubectl patch pod <zombie-pod-name> -n <namespace> \
  -p '{"metadata":{"finalizers":null}}' --type=merge

2. Adım: Arkasında Servis Olmayan “Sağır” Podları Yakalamak

CI/CD pipeline’ları çöktüğünde bazen Deployment silinir ancak pod bağımsız bir şekilde çalışmaya devam eder. Ya da deployment label’ları güncellenir fakat Service manifest’indeki selector güncellenmeyi unutulur. Sonuç: Pod CPU ve Memory tüketir ama gelen tek bir HTTP isteği dahi almaz.

Herhangi bir Service’in Endpoints veya modern Kubernetes kümelerinde EndpointSlice objesi tarafından hedeflenmeyen podları tespit etmek için bu pratik bash one-liner’ını çalıştırın:

#!/usr/bin/env bash
# Namespace bazlı hedefsiz pod kontrolü
NAMESPACE="production"

# Tüm geçerli endpoint IP'lerini al
VALID_IPS=$(kubectl get endpointslices -n "$NAMESPACE" -o jsonpath='{.items[*].endpoints[*].addresses[*]}' | tr ' ' '\n' | sort -u)

# Çalışan podların IP'lerini karşılaştır
for POD in $(kubectl get pods -n "$NAMESPACE" -o jsonpath='{range .items[?(@.status.phase=="Running")]}{.metadata.name}{":"}{.status.podIP}{"\n"}{end}'); do
    POD_NAME=$(echo "$POD" | cut -d: -f1)
    POD_IP=$(echo "$POD" | cut -d: -f2)

    if ! echo "$VALID_IPS" | grep -q "$POD_IP"; then
        # DaemonSet ve standalone jobları hariç tutmak isteyebilirsiniz
        OWNER=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.metadata.ownerReferences[0].kind}')
        if [[ "$OWNER" != "DaemonSet" && "$OWNER" != "Job" ]]; then
            echo "[ZOMBIE DETECTED] Pod: $POD_NAME (IP: $POD_IP) - Hiçbir Service arkasında değil!"
        fi
    fi
done

Bu senaryo özellikle preview environment’larda çok yaşanır. Pull Request merge edilir, ingress kalkar, service kalkar ama pod orada sonsuza dek sessizce yaşar.

3. Adım: Observability ile Kaynak İsrafını Ortaya Çıkarma

FinOps dünyasında en tehlikeli ghost pod, “iş yapıyor gibi durup hiçbir şey yapmayan” poddur. Geliştirici deployment manifest’inde cpu: "2" ve memory: "4Gi" talep etmiştir. Kubernetes scheduler bu talebi garanti altına almak için node üzerinde ilgili kaynağı rezerve eder (allocatable space düşer). Ancak pod arka planda sadece idle beklemektedir.

Bu israfı yakalamak için Prometheus ve Thanos üzerinde çalıştıracağımız PromQL sorguları devreye girer. Son 7 gün boyunca talep ettiği CPU kaynağının %1’inden azını kullanan podları listeleyelim:

# Prometheus / Thanos PromQL
(
  sum(rate(container_cpu_usage_seconds_total{container!=""}[1h])) by (namespace, pod)
  /
  sum(kube_pod_container_resource_requests{resource="cpu"}) by (namespace, pod)
) * 100 < 1

Bu sorgunun çıktısı size direkt olarak kümenizdeki “zombi rezervasyonları” verir. Pod fiziki olarak ölü değildir, ama maliyet açısından tam bir hayalettir. Node Autoscaler (Karpenter veya Cluster Autoscaler) bu pod yüzünden yeni ec2/compute instance ayağa kaldırmak zorunda kalır.

4. Adım: Sahipsiz StatefulSet Kalıntıları (Ghost PVC’ler)

Storage israfı, compute israfından daha sinsi seyreder çünkü disk saatlik değil, provizyon edildiği toplam boyut üzerinden (GB/ay) faturalandırılır. Bir StatefulSet’in replica sayısını 5’ten 2’ye indirdiğinizde Kubernetes, veri kaybını önlemek adına 3 adet PVC’yi cluster içinde Bound statüsünde tutmaya devam eder.

Peki bu diskleri kullanan aktif bir pod var mı? Görelim:

kubectl get pvc -A -o json | jq -r '
  .items[] | 
  select(.status.phase == "Bound") | 
  select(.metadata.annotations["volume.kubernetes.io/selected-node"] == null and .metadata.ownerReferences == null) | 
  "Namespace: " + .metadata.namespace + " | Unmounted PVC: " + .metadata.name + " | Size: " + .spec.resources.requests.storage'

Burada listelenen PVC’ler, Kubernetes API server düzeyinde kilitli kalmış, pod’u çoktan yok edilmiş disklere işaret eder. Eğer Retain politikasına sahip bir StorageClass kullanıyorsanız, deployment tamamen silinse bile AWS EBS veya GCP Persistent Disk konsolunda faturayı şişirmeye devam eder.

Otomasyon: Zombie Avını Sürekli Kılmak

Her hafta terminal başına geçip bu script’leri çalıştırmak sürdürülebilir bir SRE yaklaşımı değildir. Bunu engellemenin en temiz yolu, kuralları policy engine seviyesinde cluster’a enjekte etmektir.

Completed (başarıyla bitmiş) job podlarının cluster’da günlerce yer kaplamasını engellemek için manifest’lerinize mutlaka ttlSecondsAfterFinished ekleyin veya bir Kyverno policy’si ile bunu zorunlu kılın:

apiVersion: batch/v1
kind: Job
metadata:
  name: nightly-backup-sync
spec:
  # Pod tamamlandıktan 300 saniye (5 dk) sonra kendini imha eder
  ttlSecondsAfterFinished: 300
  template:
    spec:
      containers:
      - name: sync-worker
        image: custom-sync:v1.2
      restartPolicy: OnFailure

Aynı şekilde Kyverno veya OPA Gatekeeper kullanarak, her Deployment için memory/cpu limit ve request oranının belirli bir çarpanı (örneğin request limitin %50’sinden az olamaz kuralı) aşmasını admission webhook aşamasında engelleyebilirsiniz.

Sonuç: FinOps Salt Fatura İncelemek Değildir

DevOps dünyasında maliyet optimizasyonu (FinOps), ay sonunda AWS veya GCP faturasındaki grafikleri inceleyip “Compute Engine çok harcamış” demekten ibaret değildir. Asıl optimizasyon, runtime seviyesinde etcd ve scheduler üzerindeki gereksiz yükleri ayıklamaktır.

Terminating state’te kalan podlar IP tüketir, selector’ı kopmuş podlar gereksiz compute harcar, dangling PVC’ler storage faturasını patlatır. Kümelerinizde periyodik olarak bu kontrolleri pipeline’larınıza veya cron job’larınıza entegre edin. Ghost resource’ları temizlemek sadece faturayı düşürmekle kalmaz, scheduler gecikmelerini (scheduling latency) ve control plane üzerindeki memory overhead’ini doğrudan azaltır.

Etiketler: , , ,
Copyright 20254541. All rights reserved.

Posted 17 Ekim 2026 by Kerem Danış in category "Genel