Konteyner Performansı: Docker ve Kubernetes Optimizasyonu
Prodüksiyonda konteyner ortamı kurduğunuzda her şey harika görünür. Uygulamalar çalışıyor, servisler ayakta, herkes mutlu. Sonra trafik artmaya başlar ve o an gerçeklerle yüzleşirsiniz: yüksek CPU kullanımı, bellek sızıntıları, ağ gecikmeleri ve Kubernetes pod’larının sürekli yeniden başlaması. Bu yazıda Docker ve Kubernetes ortamlarında performans sorunlarını nasıl tespit edeceğinizi, izleyeceğinizi ve optimize edeceğinizi gerçek dünya senaryolarıyla anlatacağım.
Docker Konteyner Performansını Anlama
Her şeyden önce konteynerlerinizin ne kadar kaynak tükettiğini bilmeniz gerekiyor. docker stats komutu bunun için en hızlı başlangıç noktası.
# Tüm çalışan konteynerlerin anlık istatistikleri
docker stats --no-stream
# Belirli bir konteynerin detaylı izlenmesi
docker stats --format "table {{.Container}}t{{.CPUPerc}}t{{.MemUsage}}t{{.NetIO}}t{{.BlockIO}}" my-app
# JSON formatında çıktı almak (otomasyon için kullanışlı)
docker stats --no-stream --format json my-app
Burada dikkat etmeniz gereken birkaç kritik metrik var. CPUPerc değeri 80% üzerindeyse throttling başlar ve uygulamanız yavaşlar. MemUsage değeri limiti aşmaya başlıyorsa OOM Killer devreye girer ve konteyneriniz çöker. Bu değerleri düzenli olarak kayıt altına almak için basit bir script yazabilirsiniz.
#!/bin/bash
# docker-perf-monitor.sh
LOG_FILE="/var/log/docker-stats.log"
INTERVAL=30
while true; do
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "=== $TIMESTAMP ===" >> $LOG_FILE
docker stats --no-stream --format
"{{.Container}},{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}},{{.NetIO}},{{.BlockIO}}"
>> $LOG_FILE
sleep $INTERVAL
done
Bu script’i systemd servisi olarak çalıştırıp log rotasyonunu da ayarladığınızda, performans sorunlarını retrospektif olarak analiz edebilirsiniz.
Resource Limits: Konteyner Kaynak Kısıtlamaları
Prodüksiyonda kaynak limitleri olmadan çalışmak, makinenizin tüm RAM’ini tek bir uygulamanın yemesine izin vermek anlamına gelir. Bu hem güvensiz hem de tehlikeli.
# CPU ve bellek limiti ile konteyner başlatma
docker run -d
--name web-app
--cpus="2.0"
--memory="512m"
--memory-swap="512m"
--memory-reservation="256m"
--cpu-shares=1024
nginx:latest
# Çalışan bir konteynerin limitlerini güncelleme (yeniden başlatma gerekmez)
docker update
--cpus="1.5"
--memory="1g"
--memory-swap="1g"
my-existing-container
Buradaki parametreleri açıklayayım:
- –cpus: Konteyner’in kullanabileceği maksimum CPU miktarı (2.0 = 2 çekirdek eşdeğeri)
- –memory: Hard limit, bu aşılırsa OOM Killer devreye girer
- –memory-swap: RAM + swap toplamı, memory ile aynıysa swap kullanılmaz
- –memory-reservation: Soft limit, sistem baskı altındayken bu kadarı garanti edilir
- –cpu-shares: Göreli CPU ağırlığı, varsayılan 1024, rekabet durumunda öncelik belirler
Gerçek bir senaryoda bir e-ticaret sitesini yönetiyordum. Gece yarısı yapılan kampanya trafiğinde MySQL konteyner’i tüm sunucu belleğini yiyip diğer servisleri çöküşe geçiriyordu. Kaynak limitleri ekleyince sorun ortadan kalktı ama bu sefer doğru limitleri bulmak için benchmark almak gerekti.
Docker Image Optimizasyonu
Küçük image’lar hem daha hızlı başlar hem de daha az bellek tüketir. Multi-stage build bu konuda büyük fark yaratır.
# Optimize edilmemiş örnek (YAPMAYIN)
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Optimize edilmiş multi-stage build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
RUN addgroup -g 1001 -S nodejs &&
adduser -S nodeuser -u 1001
COPY --from=builder --chown=nodeuser:nodejs /app/dist ./dist
COPY --from=builder --chown=nodeuser:nodejs /app/node_modules ./node_modules
USER nodeuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
Bu yaklaşımla image boyutunuzu bazen 10 kata kadar küçültebilirsiniz. Alpine tabanlı image’lar 5-10MB’dan başlarken, tam Ubuntu tabanlı bir image 200MB+ olabilir. Build zamanı bağımlılıklarını runtime image’a taşımamak kritik önem taşır.
Kubernetes Pod Optimizasyonu
Kubernetes’te performans optimizasyonu Docker’a göre çok daha katmanlı bir konu. Başlangıç noktamız yine kaynak tanımları olacak.
# deployment.yaml - Optimize edilmiş kaynak tanımları
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-api
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: web-api
template:
metadata:
labels:
app: web-api
spec:
containers:
- name: web-api
image: my-registry/web-api:v1.2.3
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
env:
- name: GOMAXPROCS
valueFrom:
resourceFieldRef:
resource: limits.cpu
requests ile limits arasındaki fark çok önemli. Requests, pod’un schedule edilmesi için gerekli minimum kaynak, limits ise maksimum kullanabileceği miktar. requests=limits yaparsanız pod’unuz “Guaranteed” QoS class’ına girer ve OOM durumunda en son kill edilir. Bu prodüksiyon için önerilir.
Horizontal Pod Autoscaler Konfigürasyonu
Trafik dalgalanmalarını yönetmenin en etkili yolu HPA. Ama yanlış konfigüre edilmiş HPA bazen daha büyük sorunlara yol açabilir.
# hpa.yaml - Akıllı otomatik ölçeklendirme
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 50
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Max
scaleDown.stabilizationWindowSeconds: 300 değeri özellikle önemli. Bu olmadan HPA trafiğin düşmesiyle hemen scale down yapar, trafik tekrar artınca yavaşça scale up eder ve bu arada kullanıcılar yüksek latency yaşar. 5 dakikalık bekleme penceresi bu dalgalanmayı önler.
Node Affinity ve Pod Anti-Affinity
Kubernetes cluster’ınızda node’larınız arasında yük dengesini sağlamak için affinity kuralları kritik.
# anti-affinity ile pod dağılımı
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-api
topologyKey: kubernetes.io/hostname
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: node-type
operator: In
values:
- compute-optimized
Bu konfigürasyonla aynı uygulamanın pod’larının farklı node’lara dağılması zorunlu hale gelir. Bir node çökerse diğer pod’lar etkilenmez. Gerçek bir prodüksiyon felaketini önlemenin en basit yolu bu.
Kubernetes Performans Analizi Araçları
Sorun yaşandığında hızlıca analiz yapabilmek için araç setinizin hazır olması gerekiyor.
# Pod kaynak kullanımını node bazında görüntüleme
kubectl top nodes
kubectl top pods -n production --sort-by=cpu
kubectl top pods -n production --sort-by=memory
# Belirli bir pod'un detaylı event'lerini inceleme
kubectl describe pod web-api-7d9f6b8c4-xk2p9 -n production
# Pod loglarını canlı takip etme (son 100 satır)
kubectl logs -f web-api-7d9f6b8c4-xk2p9 -n production --tail=100
# Crash loop'taki pod'un önceki container loglarını görme
kubectl logs web-api-7d9f6b8c4-xk2p9 -n production --previous
# Namespace bazında tüm kaynakların durumu
kubectl get all -n production -o wide
# Pod'un hangi node'da çalıştığını ve kaynak kullanımını kontrol etme
kubectl get pod web-api-7d9f6b8c4-xk2p9 -n production -o jsonpath=
'{.spec.nodeName}{"n"}{.status.containerStatuses[0].restartCount}{"n"}'
Bir prodüksiyon ortamında sabah 3’te alarm aldığınızda bu komutları ezberden çalıştırabilmeniz gerekiyor. Her saniye önemli.
cgroups ile Sistem Düzeyinde Analiz
Konteyner performansı sorunları bazen Docker ve Kubernetes katmanının altındaki cgroups seviyesinde analiz gerektirir.
# Docker konteyner'ının cgroup bilgilerini bulma
CONTAINER_ID=$(docker inspect --format='{{.Id}}' my-container)
CGROUP_PATH="/sys/fs/cgroup/cpu/docker/${CONTAINER_ID}"
# CPU throttling istatistikleri
cat /sys/fs/cgroup/cpu/docker/${CONTAINER_ID}/cpu.stat
# nr_throttled ve throttled_time değerlerine bakın
# Bellek kullanım detayları
cat /sys/fs/cgroup/memory/docker/${CONTAINER_ID}/memory.stat
# Kubernetes pod için cgroup path
# /sys/fs/cgroup/cpu/kubepods/besteffort/pod<POD_UID>/
ls /sys/fs/cgroup/cpu/kubepods/guaranteed/
# CPU throttling yüzdesi hesaplama scripti
#!/bin/bash
CONTAINER_ID=$1
CGROUP="/sys/fs/cgroup/cpu/docker/${CONTAINER_ID}"
read NR_PERIODS NR_THROTTLED THROTTLED_TIME <<< $(awk
'/nr_periods|nr_throttled|throttled_time/ {print $2}'
${CGROUP}/cpu.stat | tr 'n' ' ')
if [ "$NR_PERIODS" -gt 0 ]; then
THROTTLE_PCT=$(echo "scale=2; $NR_THROTTLED * 100 / $NR_PERIODS" | bc)
echo "Throttling: ${THROTTLE_PCT}%"
echo "Toplam throttled süre: $((THROTTLED_TIME / 1000000)) ms"
fi
CPU throttling oranı %5’in üzerindeyse ciddi performans sorununuz var demektir. Bu analizi yapmak Kubernetes metrics-server’ın görmediği sorunları ortaya çıkarır.
Network Performansı ve Service Mesh
Konteyner ortamlarında network gecikmeleri çok kolay gözden kaçar. Servisler arası iletişim yavaşladığında ilk bakılacak yer network katmanı.
# Kubernetes servisler arası network latency testi
kubectl run nettest --image=nicolaka/netshoot -it --rm --
bash -c "for i in {1..10}; do curl -s -o /dev/null -w '%{time_total}n'
http://web-api-service.production.svc.cluster.local/health; done"
# DNS çözümleme süresini ölçme
kubectl run dnstest --image=nicolaka/netshoot -it --rm --
bash -c "for i in {1..5}; do time nslookup
web-api-service.production.svc.cluster.local; done"
# iptables kurallarının performans etkisini görme
iptables -t nat -L -n --line-numbers | grep -c "KUBE"
# Bu sayı çok yüksekse (1000+) kube-proxy IPVS moduna geçmeyi düşünün
# kube-proxy IPVS moduna geçiş kontrolü
kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode
Büyük cluster’larda iptables tabanlı kube-proxy ciddi performans sorunu yaratır. IPVS moduna geçmek network latency’yi önemli ölçüde düşürür.
Prometheus ve Grafana ile Konteyner Monitoring
Gerçek anlamda performans yönetimi için metrik toplama ve görselleştirme şart. Prometheus + Grafana stack’i bu iş için endüstri standardı haline geldi.
# prometheus-rules.yaml - Kritik uyarı kuralları
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: container-performance-alerts
namespace: monitoring
spec:
groups:
- name: container.rules
rules:
- alert: ContainerHighCPUThrottling
expr: |
rate(container_cpu_cfs_throttled_seconds_total[5m]) /
rate(container_cpu_cfs_periods_total[5m]) > 0.25
for: 5m
labels:
severity: warning
annotations:
summary: "Container CPU throttling yüksek"
description: "{{ $labels.container }} konteyneri %{{ $value | humanizePercentage }} throttling yaşıyor"
- alert: ContainerMemoryNearLimit
expr: |
container_memory_working_set_bytes /
container_spec_memory_limit_bytes > 0.90
for: 5m
labels:
severity: critical
annotations:
summary: "Konteyner bellek limiti yaklaşıyor"
description: "{{ $labels.container }} belleğinin %90'ını kullanıyor, OOM riski var"
- alert: PodRestartingFrequently
expr: |
rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 15m
labels:
severity: warning
annotations:
summary: "Pod sürekli yeniden başlıyor"
description: "{{ $labels.pod }} pod'u son 15 dakikada yeniden başladı"
Bu alertler prodüksiyon ortamında sorunları oluşmadan önce yakalamanızı sağlar. CPU throttling uyarısı özellikle değerli çünkü metrics-server CPU kullanımını olduğundan düşük gösterebilir.
Pratik Optimizasyon Senaryosu: API Servisi
Gerçek bir senaryo üzerinden gidelim. Bir microservice API’si var, response time’ları artmış, intermittent timeout’lar yaşanıyor.
Adım adım analiz:
- 1. Adım:
kubectl top podsile CPU ve memory baseline alın - 2. Adım:
kubectl describe podile event’leri kontrol edin, OOMKilled veya throttling var mı? - 3. Adım: cgroup stats ile CPU throttling oranını hesaplayın
- 4. Adım: Application metrics’e bakın, hangi endpoint yavaş?
- 5. Adım: Network latency testi yapın, DNS çözümleme sorunlu mu?
- 6. Adım: Node kaynak kullanımını kontrol edin, noisy neighbor problemi var mı?
Bu analiz sırasında en sık karşılaştığım sorun yanlış ayarlanmış CPU limitleri. CPU limit’i requests’e çok yakın ayarladığınızda normal trafik dalgalanmalarında bile throttling başlıyor. Genel kural olarak CPU limits’i requests’in 2-3 katı yapın. Memory için ise OOM riski nedeniyle limits’i requests’e daha yakın tutun, ama requests’i gerçekçi ölçümlerle belirleyin.
Init Container ve Startup Probe Optimizasyonu
Konteyner başlangıç süresi prodüksiyonda çok önemli. Yavaş başlayan pod’lar rolling update’leri uzatır, ölçeklendirme geç kalır.
spec:
initContainers:
- name: wait-for-db
image: busybox:1.35
command: ['sh', '-c',
'until nc -z postgres-service 5432; do echo waiting; sleep 2; done']
resources:
requests:
cpu: "10m"
memory: "16Mi"
limits:
cpu: "50m"
memory: "32Mi"
containers:
- name: web-api
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
StartupProbe kullanmak kritik bir optimizasyon. Bu probe başarılı olana kadar liveness probe devreye girmiyor. Yavaş başlayan uygulamalar liveness probe başarısızlığı nedeniyle sürekli yeniden başlamak yerine, başlangıç sürecini tamamlayabiliyor. failureThreshold: 30 ve periodSeconds: 10 ile uygulamaya 300 saniye başlangıç süresi tanıyorsunuz.
Garbage Collection ve JVM Konteynerleri
Java uygulamaları konteyner ortamında özel dikkat gerektirir. Eski JVM sürümleri container CPU limitlerini göremez ve tüm host CPU’larını sayar.
# JVM 17+ için container-aware ayarlar
docker run -d
--name java-app
--cpus="2.0"
--memory="2g"
-e JAVA_OPTS="-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+PrintGCDetails
-Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=5,filesize=20m"
my-java-app:latest
# Kubernetes için JVM environment
env:
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
- name: JVM_MAX_HEAP
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1Mi
-XX:+UseContainerSupport ve -XX:MaxRAMPercentage=75.0 kombinasyonu JVM’in container memory limit’inin %75’ini kullanmasını sağlar. Kalan %25 JVM overhead, metaspace ve native memory için. Bu olmadan ya OOM yaşarsınız ya da JVM’e çok az heap verip GC thrashing’e düşersiniz.
Sonuç
Konteyner performans optimizasyonu tek seferlik bir görev değil, sürekli bir süreç. Prodüksiyona aldığınız her uygulama için kaynak limitlerini gerçekçi yük testleriyle belirleyin, CPU throttling’i düzenli kontrol edin ve Prometheus alertlerini erkenden kurun.
En önemli takeaway’ler şunlar:
- Kaynak limitleri olmadan prodüksiyona çıkmayın, ama yanlış ayarlanmış limitler bazen limitsiz çalışmaktan daha kötü sonuç verir
- CPU throttling en sessiz performans katili, cgroup stats ile düzenli kontrol edin
- Multi-stage Docker build ile başlayın, küçük image her şeyi iyileştirir
- HPA scale-down davranışını stabilizasyon penceresiyle yavaşlatın
- requests ve limits arasındaki farkı anlayın, QoS class’ları önemli
- JVM uygulamaları için container-aware JVM flaglerini mutlaka kullanın
- StartupProbe kullanarak yavaş başlayan uygulamaları false positive OOM’dan koruyun
Performans optimizasyonu veri odaklı bir iş. Ölçmeden optimize etmeye çalışmak karanlıkta hedef aramak gibi. Önce ölçün, sonra optimize edin, ardından tekrar ölçün. Bu döngüyü iyi kurarsanız konteyner ortamınız hem güvenilir hem de verimli çalışır.