Kubernetes’te Kaynak Limitleri: Request ve Limit Tanımlama

Kubernetes cluster’ında bir pod’un tüm node kaynaklarını tükettiğini ve yanındaki kritik servislerin çöktüğünü gece 2’de fark etmek… Bu senaryoyu yaşadıysanız, kaynak limitlerinin ne kadar önemli olduğunu zaten biliyorsunuzdur. Yaşamadıysanız, bu yazıyı okuduktan sonra yarın sabah production’a uygulayacaksınız.

Kubernetes’te kaynak yönetimi, stabil ve öngörülebilir bir cluster işletmenin temel taşıdır. Request ve limit kavramlarını doğru anlamadan yapılan deploymentlar, er ya da geç ciddi problemlere yol açar. Bu yazıda bu iki kavramı derinlemesine inceleyeceğiz, gerçek dünya senaryolarıyla pekiştireceğiz.

Request ve Limit Nedir?

Kubernetes’te her container için iki temel kaynak parametresi tanımlarsınız: requests ve limits.

Requests (İstek): Container’ın çalışması için garantilenmiş minimum kaynak miktarıdır. Kubernetes scheduler, pod’u yerleştireceği node’u seçerken bu değeri baz alır. Yani “bu container en az bu kadar kaynağa ihtiyaç duyar” diyorsunuz.

Limits (Sınır): Container’ın kullanabileceği maksimum kaynak miktarıdır. Container bu sınırı aşmaya çalıştığında Kubernetes müdahale eder. CPU için throttling uygulanır, memory için ise container sonlandırılır (OOMKilled).

Bu iki değer arasındaki fark kritiktir. Request, scheduler için önemlidir. Limit ise runtime davranışını belirler.

CPU ve Memory Birimleri

CPU değerleri millicore cinsinden ifade edilir:

  • 1000m = 1 CPU core
  • 500m = 0.5 CPU core
  • 250m = 0.25 CPU core
  • 100m = 0.1 CPU core

Memory değerleri ise şu birimlerle ifade edilir:

  • Ki = Kibibyte (1024 bytes)
  • Mi = Mebibyte (1024 Ki)
  • Gi = Gibibyte (1024 Mi)
  • M = Megabyte (1000 bytes) – Ki/Mi ile karıştırmayın!

Temel Kaynak Tanımlaması

En basit haliyle bir deployment’a kaynak tanımlamak şu şekilde görünür:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: nginx:1.25
        resources:
          requests:
            memory: "128Mi"
            cpu: "250m"
          limits:
            memory: "256Mi"
            cpu: "500m"

Bu örnekte container başına minimum 128Mi memory ve 250m CPU garantilenmiş, maksimum ise 256Mi memory ve 500m CPU ile sınırlandırılmıştır.

Scheduler Nasıl Karar Verir?

Diyelim ki 3 node’lu bir cluster’ınız var ve her node’da 4Gi memory mevcut. 2 Gi memory request’i olan bir pod deploy etmek istiyorsunuz. Scheduler bu pod’u yerleştirmek için hangi node’da 2Gi boş memory olduğuna bakar. Buradaki “boş” kavramı önemli: Scheduler, fiziksel kullanımı değil, mevcut pod’ların request toplamını baz alır.

Mevcut durumu görmek için şu komutları kullanabilirsiniz:

# Node'lardaki allocatable kaynakları göster
kubectl describe nodes | grep -A 5 "Allocatable:"

# Her node için kaynak kullanımını özetle
kubectl get nodes -o custom-columns=
"NAME:.metadata.name,
CPU-ALLOCATABLE:.status.allocatable.cpu,
MEM-ALLOCATABLE:.status.allocatable.memory"

# Namespace bazında kaynak kullanımını gör
kubectl top pods -n production --sort-by=memory

QoS Sınıfları: Gözardı Edilmemesi Gereken Konu

Kubernetes, kaynak tanımlamalarınıza göre pod’larınıza otomatik olarak bir QoS (Quality of Service) sınıfı atar. Bu sınıf, sistemde bellek baskısı oluştuğunda hangi pod’ların önce öldürüleceğini belirler.

Guaranteed

Request ve limit değerleri eşit olduğunda atanır. En yüksek önceliğe sahiptir, sistem baskı altındayken en son öldürülür.

apiVersion: v1
kind: Pod
metadata:
  name: kritik-veritabani
spec:
  containers:
  - name: postgres
    image: postgres:15
    resources:
      requests:
        memory: "512Mi"
        cpu: "500m"
      limits:
        memory: "512Mi"
        cpu: "500m"

Burstable

Request tanımlanmış ama limit farklı (veya sadece biri tanımlı) olduğunda atanır. Orta önceliğe sahiptir.

apiVersion: v1
kind: Pod
metadata:
  name: api-server
spec:
  containers:
  - name: api
    image: myapp:latest
    resources:
      requests:
        memory: "256Mi"
        cpu: "200m"
      limits:
        memory: "1Gi"
        cpu: "1000m"

BestEffort

Hiç kaynak tanımlanmadığında atanır. En düşük önceliğe sahiptir, sistem baskı altındayken ilk öldürülen bunlar olur. Production’da hiçbir zaman bu sınıfta pod bırakmayın.

LimitRange ile Namespace Bazlı Varsayılanlar

Her deployment’a tek tek kaynak tanımlamak hem zahmetlidir hem de gözden kaçabilir. LimitRange objesi, namespace seviyesinde varsayılan değerler ve sınırlar belirlemenizi sağlar.

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limitrange
  namespace: development
spec:
  limits:
  - default:
      cpu: "500m"
      memory: "256Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    max:
      cpu: "2000m"
      memory: "2Gi"
    min:
      cpu: "50m"
      memory: "64Mi"
    type: Container

Bu konfigürasyonla development namespace’ine deploy edilen herhangi bir container, kaynak tanımlaması olmasa bile otomatik olarak varsayılan değerleri alır. Aynı zamanda hiçbir container 2Gi memory veya 2 CPU’dan fazla talep edemez.

LimitRange’i uyguladıktan sonra test etmek için:

# LimitRange'i uygula
kubectl apply -f limitrange.yaml

# Kaynak tanımı olmayan bir pod deploy et
kubectl run test-pod --image=nginx -n development

# Pod'un aldığı kaynakları kontrol et
kubectl get pod test-pod -n development -o yaml | grep -A 10 "resources:"

# LimitRange detaylarını gör
kubectl describe limitrange default-limitrange -n development

ResourceQuota ile Namespace Toplam Sınırları

LimitRange container başına sınır koyarken, ResourceQuota tüm namespace için toplam kaynak sınırı belirler. Çok tenant’lı (multi-tenant) cluster’larda ekipler arası adil kaynak dağılımı için vazgeçilmezdir.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: production-quota
  namespace: production
spec:
  hard:
    requests.cpu: "10"
    requests.memory: "20Gi"
    limits.cpu: "20"
    limits.memory: "40Gi"
    pods: "50"
    services: "20"
    persistentvolumeclaims: "30"

Bu konfigürasyonla production namespace’indeki tüm pod’ların toplam CPU request’i 10 core’u, toplam memory request’i 20Gi’yi geçemez. Aynı zamanda namespace’de maksimum 50 pod çalışabilir.

Mevcut quota kullanımını kontrol etmek için:

kubectl describe resourcequota production-quota -n production

Çıktı şuna benzer:

Name:                   production-quota
Namespace:              production
Resource                Used    Hard
--------                ----    ----
limits.cpu              8500m   20
limits.memory           16Gi    40Gi
pods                    23      50
requests.cpu            4200m   10
requests.memory         8Gi     20Gi

Gerçek Dünya Senaryosu: E-ticaret Platformu

Bir e-ticaret platformu yönettiğinizi düşünün. Frontend, backend API, cache ve veritabanından oluşan bir stack’iniz var. Her servis için farklı kaynak profili gerekir.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: ecommerce
spec:
  replicas: 3
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: ecommerce-frontend:v2.1
        ports:
        - containerPort: 3000
        resources:
          requests:
            memory: "256Mi"
            cpu: "200m"
          limits:
            memory: "512Mi"
            cpu: "1000m"
        env:
        - name: NODE_ENV
          value: "production"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-api
  namespace: ecommerce
spec:
  replicas: 5
  selector:
    matchLabels:
      app: backend-api
  template:
    metadata:
      labels:
        app: backend-api
    spec:
      containers:
      - name: api
        image: ecommerce-api:v3.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "2000m"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
  namespace: ecommerce
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis-cache
  template:
    metadata:
      labels:
        app: redis-cache
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        resources:
          requests:
            memory: "256Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Bu senaryoda frontend container’ları burst edebilir (yoğun trafik anlarda CPU limitine kadar çıkabilir), backend API’ler ise daha geniş bir burst aralığına sahiptir. Redis için Guaranteed QoS sağlamak mantıklı olabilir çünkü cache’in aniden ölmesi tüm sistemi etkiler.

Kaynak Değerlerini Nasıl Belirlemeliyim?

En sık sorulan soru budur ve maalesef “şu kadar koy” diye bir cevabı yoktur. Ancak sistematik bir yaklaşım izleyebilirsiniz.

İlk deployment için tahmini değerlerle başlayın: Uygulamanın türüne göre başlangıç değerleri belirleyin. Bir Node.js API için 128Mi-256Mi memory request makul bir başlangıçtır.

Metrics toplamak için Vertical Pod Autoscaler (VPA) recommendation modunu kullanın:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: backend-api-vpa
  namespace: ecommerce
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: backend-api
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
    - containerName: api
      minAllowed:
        cpu: "100m"
        memory: "128Mi"
      maxAllowed:
        cpu: "4"
        memory: "4Gi"

updateMode: "Off" ile VPA sadece tavsiye verir, değişiklik yapmaz. Bir süre sonra önerileri görmek için:

kubectl describe vpa backend-api-vpa -n ecommerce

kubectl top ile gerçek zamanlı kullanımı izleyin:

# Pod bazında CPU ve memory kullanımı
kubectl top pods -n ecommerce

# Container bazında detaylı gösterim
kubectl top pods -n ecommerce --containers

# Sürekli izlemek için watch ile
watch -n 5 kubectl top pods -n ecommerce

Yaygın Hatalar ve Çözümleri

Limit koymadan request koymak

Request tanımlanmış ama limit yok ise container teorik olarak tüm node kaynaklarını tüketebilir. Her zaman ikisini birlikte tanımlayın.

Çok düşük memory limit (OOMKilled)

# OOMKilled olan pod'ları tespit et
kubectl get pods -n production | grep OOMKilled

# Pod'un sonlanma nedenini detaylı gör
kubectl describe pod <pod-name> -n production | grep -A 5 "Last State:"

# Önceki container'ın çıkış kodunu kontrol et
# Exit code 137 = OOMKilled
kubectl get pod <pod-name> -n production -o jsonpath=
'{.status.containerStatuses[0].lastState.terminated.exitCode}'

OOMKilled görüyorsanız memory limit’iniz gerçek kullanımın altındadır. Yüzde 20-30 headroom bırakarak limit değerini artırın.

CPU throttling gözardı etmek

CPU throttling, OOMKilled gibi dramatik değildir; uygulamanız yavaşlar ama çalışmaya devam eder. Bu yüzden fark edilmesi daha zordur. Prometheus ve Grafana kullanıyorsanız şu metriği izleyin: container_cpu_cfs_throttled_seconds_total

Throttling oranı yüzde 25’i geçiyorsa CPU limitinizi artırmayı düşünün.

Request/Limit oranını çok açık tutmak

Bazı ekipler çok küçük request, çok büyük limit tanımlar. Örneğin 100m request, 4000m limit. Bu durumda pod kolayca yerleştirilir ama çalışırken node’u boğabilir. Genel kural olarak limit/request oranını 2-3x içinde tutmaya çalışın. Kritik servisler için 1x (Guaranteed) tercih edin.

Namespace Başlangıç Konfigürasyonu: Production Ready Template

Yeni bir namespace açtığınızda uygulamanız gereken minimum konfigürasyon:

apiVersion: v1
kind: Namespace
metadata:
  name: new-project
  labels:
    environment: production
---
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: new-project
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: "256Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    max:
      cpu: "4"
      memory: "4Gi"
    min:
      cpu: "50m"
      memory: "64Mi"
  - type: Pod
    max:
      cpu: "8"
      memory: "8Gi"
  - type: PersistentVolumeClaim
    max:
      storage: "50Gi"
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: namespace-quota
  namespace: new-project
spec:
  hard:
    requests.cpu: "8"
    requests.memory: "16Gi"
    limits.cpu: "16"
    limits.memory: "32Gi"
    pods: "40"
    services: "15"
    secrets: "30"
    configmaps: "30"
    persistentvolumeclaims: "20"

Bu template’i bir baseline olarak kullanın ve projenin ihtiyaçlarına göre değerleri ayarlayın. Kaynaklara ihtiyaç duydukça quota’yı artırmak, baştan sınırsız bırakmaktan çok daha güvenlidir.

Monitoring ve Alerting

Kaynak limitlerini tanımlamak yetmez; sürekli izlemek gerekir. Prometheus kullanıyorsanız şu alertleri tanımlamanızı öneririm:

# Prometheus alert kuralları örneği
groups:
- name: kubernetes-resources
  rules:
  - alert: PodMemoryUsageHigh
    expr: |
      (container_memory_working_set_bytes / 
       container_spec_memory_limit_bytes) > 0.85
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Pod memory kullanimi limit'in %85'ini asti"
      description: "{{ $labels.pod }} pod'u memory limit'inin %85'ini kullaniyor"

  - alert: CPUThrottlingHigh
    expr: |
      rate(container_cpu_cfs_throttled_seconds_total[5m]) /
      rate(container_cpu_cfs_periods_total[5m]) > 0.25
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "CPU throttling yuksek"
      description: "{{ $labels.pod }} %25'in uzerinde CPU throttling yasaniyor"

  - alert: NamespaceQuotaAlmostFull
    expr: |
      kube_resourcequota{type="used"} / 
      kube_resourcequota{type="hard"} > 0.80
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Namespace quota %80'i asti"

Sonuç

Kubernetes’te kaynak yönetimi, cluster sağlığının ve uygulama güvenilirliğinin temel belirleyicisidir. Özetleyecek olursak:

Requests, scheduler’a rehberlik eder ve container’a garanti edilmiş kaynağı ifade eder. Limits ise runtime’da uygulanan üst sınırdır ve aşılması durumunda CPU throttling veya memory OOMKill ile sonuçlanır.

QoS sınıflarını anlayarak kritik servislerinizi Guaranteed, normal servislerinizi Burstable olarak yapılandırın. BestEffort pod’larını production’da bırakmayın. LimitRange ile namespace’e varsayılan değerler atayın, kimse kaynak tanımını unutmasın. ResourceQuota ile ekipler arası adil kaynak dağılımı sağlayın.

Değerleri belirlerken önce gözlemleyin, VPA recommendation modunu ve kubectl top’u aktif kullanın, sonra bilinçli kararlar alın. Throttling ve OOMKilled metriklerini Prometheus’ta izleyin ve alert kuralları tanımlayın.

En önemli kural: Hiçbir production pod’unuzu kaynak tanımı olmadan çalıştırmayın. Bugün aldığınız bu önlem, sizi gece 2’deki o acı verici telefon görüşmesinden kurtarabilir.

Yorum yapın