Docker’da CPU ve Bellek Kısıtlama: Kaynak Limitleri Yönetimi

Bir production ortamında konteynerinizin aniden tüm CPU kaynaklarını yemesi ve sunucuyu çökertmesi kadar sinir bozucu bir şey yoktur. Bu tam olarak kaynak limiti yönetiminin neden bu kadar kritik olduğunu anlatıyor. Docker varsayılan olarak konteynerlere neredeyse sınırsız kaynak erişimi tanır. Bu, geliştirme ortamında kullanışlıdır ama production’da felakete davet çıkarmaktır.

Bu yazıda Docker konteynerlerinde CPU ve bellek kısıtlamalarını nasıl uygulayacağınızı, neden uygulamanız gerektiğini ve gerçek dünya senaryolarında nasıl optimize edeceğinizi ele alacağız.

Neden Kaynak Limiti Uygulamalısınız?

İlk soru şu: “Zaten izleme sistemim var, neden uğraşayım?” Bu soruyu kendinize sorduysanız, muhtemelen henüz kötü bir gece geçirmediniz.

Düşünün: Bir sunucuda 10 konteyner çalışıyor. Bunlardan biri bir bellek sızıntısı yaşıyor. Limit yoksa bu konteyner tüm RAM’i doldurana kadar büyümeye devam eder. Sonuç? Linux kernel’i OOM Killer’ı devreye sokar ve rastgele süreçleri öldürmeye başlar. Bu süreçlerden biri kritik bir veritabanı konteyneri de olabilir.

Kaynak limitlerinin temel faydaları şunlardır:

  • Izolasyon: Bir konteynerin sorunları diğerlerini etkilemez
  • Öngörülebilirlik: Sunucu kapasitesi planlaması yapabilirsiniz
  • Güvenlik: Potansiyel bir fork bomb saldırısının sistemin tamamını çökertmesini önler
  • Maliyet kontrolü: Özellikle cloud ortamlarında kaynak israfını önler
  • SLA uyumu: Kritik servislere her zaman yeterli kaynak garantisi

Docker Bellek Yönetimi

Temel Bellek Limiti

En basit kullanım --memory veya kısaca -m flag’iyle yapılır:

# 512 MB bellek limiti ile konteyner başlatma
docker run -d 
  --name web-app 
  --memory="512m" 
  nginx:latest

# 2 GB bellek limiti
docker run -d 
  --name db-container 
  --memory="2g" 
  postgres:14

Burada dikkat edilmesi gereken nokta: Bu limit sadece RAM için geçerlidir, swap dahil değildir. Konteyner bu limiti aştığında OOM (Out of Memory) hatası alır ve Docker konteyneri durdurur.

Swap Bellek Yönetimi

--memory-swap parametresi toplam bellek ve swap miktarını birlikte tanımlar:

# RAM: 512m, Swap: 256m (toplam 768m)
docker run -d 
  --name app 
  --memory="512m" 
  --memory-swap="768m" 
  myapp:latest

# Swap'ı tamamen devre dışı bırakma
docker run -d 
  --name no-swap-app 
  --memory="512m" 
  --memory-swap="512m" 
  myapp:latest

# Sınırsız swap (önerilmez, ama bazen gerekli)
docker run -d 
  --name unlimited-swap 
  --memory="512m" 
  --memory-swap="-1" 
  myapp:latest

–memory-swap değeri her zaman –memory değerinden büyük veya eşit olmalıdır. Eğer eşit olursa swap kullanılmaz.

Memory Swappiness

Kernel’in belleği swap’a ne sıklıkla taşıyacağını kontrol eder. 0-100 arası bir değer alır:

# Swap kullanımını minimize et (0 = hiç kullanma, 100 = agresif kullan)
docker run -d 
  --name low-swap-app 
  --memory="1g" 
  --memory-swappiness=10 
  myapp:latest

Bellek Rezervasyonu (Soft Limit)

--memory-reservation bir yumuşak limit tanımlar. Konteyner bu değeri aşabilir ama sistem baskı altındayken Docker bu değere düşürmeye çalışır:

docker run -d 
  --name flexible-app 
  --memory="1g" 
  --memory-reservation="512m" 
  myapp:latest

Bu özellikle değişken iş yükü olan uygulamalar için idealdir. Normal zamanda 512 MB kullanır, yoğun zamanda 1 GB’a kadar çıkabilir.

Docker CPU Yönetimi

CPU Paylaşım Ağırlıkları

--cpu-shares görece bir öncelik sistemi kurar. Varsayılan değer 1024’tür:

# Yüksek öncelikli servis (varsayılanın 2 katı CPU erişimi)
docker run -d 
  --name high-priority-api 
  --cpu-shares=2048 
  api-service:latest

# Düşük öncelikli arka plan işi
docker run -d 
  --name background-worker 
  --cpu-shares=256 
  worker:latest

Önemli bir nokta: CPU paylaşımları sadece rekabet durumunda devreye girer. Sistem boştayken düşük öncelikli konteyner de tüm CPU’yu kullanabilir.

CPU Çekirdek Limiti

--cpus flag’i ile konteynerin kullanabileceği maksimum CPU sayısını belirleyebilirsiniz:

# Maksimum 1.5 CPU çekirdeği kullan
docker run -d 
  --name limited-app 
  --cpus="1.5" 
  myapp:latest

# Sadece yarım çekirdek
docker run -d 
  --name micro-service 
  --cpus="0.5" 
  microservice:latest

CPU Set ile Belirli Çekirdeklere Sabitleme

--cpuset-cpus ile konteynerin hangi fiziksel çekirdekleri kullanacağını belirtebilirsiniz:

# Sadece 0 ve 1 numaralı çekirdekleri kullan
docker run -d 
  --name pinned-app 
  --cpuset-cpus="0,1" 
  myapp:latest

# 0'dan 3'e kadar çekirdekler (0,1,2,3)
docker run -d 
  --name multi-core-app 
  --cpuset-cpus="0-3" 
  myapp:latest

Bu özellik NUMA mimarisine sahip sunucularda veya CPU cache locality önemli olan yüksek performanslı uygulamalarda çok işe yarar.

CPU Period ve Quota

Daha ince taneli kontrol için --cpu-period ve --cpu-quota kullanabilirsiniz:

# Her 100ms'de 50ms CPU kullanabilir = %50 CPU
docker run -d 
  --name throttled-app 
  --cpu-period=100000 
  --cpu-quota=50000 
  myapp:latest

# Her 100ms'de 25ms CPU = %25 CPU
docker run -d 
  --name low-cpu-app 
  --cpu-period=100000 
  --cpu-quota=25000 
  myapp:latest

Docker Compose ile Kaynak Yönetimi

Gerçek dünya senaryolarında tek tek docker run komutları yerine Docker Compose kullanırsınız. İşte kapsamlı bir örnek:

version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M
    restart: unless-stopped

  api:
    image: myapi:latest
    ports:
      - "8080:8080"
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M
    environment:
      - NODE_ENV=production
    restart: unless-stopped

  database:
    image: postgres:14
    volumes:
      - pgdata:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 2G
    environment:
      POSTGRES_PASSWORD: secret
    restart: unless-stopped

  redis:
    image: redis:alpine
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.1'
          memory: 64M
    restart: unless-stopped

volumes:
  pgdata:

Dikkat: deploy.resources sözdizimi Docker Compose v3’te Swarm modu için tasarlanmıştır. Eğer Swarm olmadan çalışıyorsanız, docker-compose up ile bu limitler uygulanmaz. Bunun yerine şu sözdizimini kullanın:

version: '3.8'

services:
  api:
    image: myapi:latest
    mem_limit: 1g
    mem_reservation: 512m
    cpus: 2.0
    restart: unless-stopped

Mevcut Konteynerleri Güncelleme

Zaten çalışan bir konteynerin limitlerini yeniden başlatmadan güncelleyebilirsiniz:

# Çalışan konteynerin bellek limitini güncelle
docker update --memory="1g" --memory-swap="1g" web-app

# CPU limitini güncelle
docker update --cpus="2.0" api-container

# Birden fazla konteyneri aynı anda güncelle
docker update --memory="512m" container1 container2 container3

# Kapsamlı güncelleme
docker update 
  --memory="2g" 
  --memory-swap="2g" 
  --memory-reservation="1g" 
  --cpus="1.5" 
  --cpu-shares=1024 
  production-api

docker update komutu cgroups’u doğrudan günceller, bu yüzden anlık etki eder.

Kaynak Kullanımını İzleme

Limit koyduktan sonra izleme yapmazsanız, ne kadar işe yaradığını bilemezsiniz.

# Tüm konteynerlerin anlık kaynak kullanımı
docker stats

# Belirli konteyner(ler)i izle
docker stats web-app api-container database

# Tek seferlik snapshot (sürekli güncelleme olmadan)
docker stats --no-stream

# JSON formatında çıktı (monitoring sistemleri için)
docker stats --no-stream --format "{{json .}}" | jq .

# Özelleştirilmiş format
docker stats --format "table {{.Container}}t{{.CPUPerc}}t{{.MemUsage}}t{{.MemPerc}}"

Konteynerin limit bilgilerini görmek için:

# Konteynerin tüm ayarlarını incele
docker inspect web-app | grep -A 20 '"HostConfig"'

# Sadece bellek limitini gör
docker inspect --format='{{.HostConfig.Memory}}' web-app

# CPU limiti
docker inspect --format='{{.HostConfig.NanoCpus}}' web-app

# Daha okunabilir format (bytes'tan MB'a çevirme)
docker inspect --format='Memory Limit: {{.HostConfig.Memory}} bytes' web-app

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

Diyelim ki küçük bir e-ticaret platformu yönetiyorsunuz. 8 çekirdekli, 16 GB RAM’li bir sunucunuz var ve şu servisler çalışıyor: frontend, API, veritabanı, cache, mesaj kuyruğu ve arka plan işçileri.

İşte önerilen kaynak dağılımı:

version: '3.8'

services:
  frontend:
    image: shop-frontend:latest
    mem_limit: 512m
    mem_reservation: 256m
    cpus: 0.5
    ports:
      - "80:80"
      - "443:443"

  api:
    image: shop-api:latest
    mem_limit: 2g
    mem_reservation: 1g
    cpus: 2.0
    ports:
      - "8080:8080"
    depends_on:
      - database
      - redis

  database:
    image: postgres:14
    mem_limit: 6g
    mem_reservation: 4g
    cpus: 3.0
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_SHARED_BUFFERS: "1536MB"
      POSTGRES_EFFECTIVE_CACHE_SIZE: "4608MB"

  redis:
    image: redis:7-alpine
    mem_limit: 1g
    mem_reservation: 512m
    cpus: 0.5
    command: redis-server --maxmemory 900mb --maxmemory-policy allkeys-lru

  rabbitmq:
    image: rabbitmq:3-management
    mem_limit: 1g
    mem_reservation: 512m
    cpus: 0.5

  worker:
    image: shop-worker:latest
    mem_limit: 1g
    mem_reservation: 256m
    cpus: 1.0
    deploy:
      replicas: 2

volumes:
  pgdata:

Bu konfigürasyonda toplam bellek dağılımı: 0.5GB + 2GB + 6GB + 1GB + 1GB + 2GB = 13GB (limit), rezervasyonlar ise yaklaşık 7GB. Bu, 16GB RAM için makul bir dağılım.

OOM Killer Davranışını Kontrol Etme

Konteyner bellek limitini aştığında ne olacağını belirleyebilirsiniz:

# Konteyner bellek limitini aştığında öldürülsün (varsayılan)
docker run -d 
  --name killable-app 
  --memory="512m" 
  --oom-kill-disable=false 
  myapp:latest

# OOM kill'i devre dışı bırak (tehlikeli, dikkatli kullanın)
docker run -d 
  --name protected-app 
  --memory="512m" 
  --oom-kill-disable=true 
  critical-service:latest

# OOM score ayarla (-1000 ile 1000 arası, düşük = önce öldürülmez)
docker run -d 
  --name important-app 
  --memory="512m" 
  --oom-score-adj=-500 
  critical-db:latest

Uyarı: --oom-kill-disable kullanırken mutlaka bellek limiti de belirleyin. Aksi takdirde konteyner tüm sistem belleğini tüketebilir ve bu da kernel seviyesinde OOM’a yol açar.

Konteyner Başlangıç Sorunlarını Giderme

Kaynak limiti uygulayan konteynerlerde karşılaşılan yaygın sorunlar:

# Konteyner neden durdu? Exit code kontrol et
docker inspect --format='{{.State.ExitCode}}' container-name
# Exit code 137 = OOM kill

# Sistem loglarından OOM olaylarını ara
dmesg | grep -i "oom|killed process"
journalctl -k | grep -i oom

# Konteyner olaylarını izle
docker events --filter container=my-app --filter event=oom

# Bellek kullanım geçmişini konteyner içinden gör
docker exec container-name cat /sys/fs/cgroup/memory/memory.usage_in_bytes
docker exec container-name cat /sys/fs/cgroup/memory/memory.max_usage_in_bytes
docker exec container-name cat /sys/fs/cgroup/memory/memory.failcnt

memory.failcnt değeri sıfırdan büyükse, konteyner daha önce bellek limitine ulaşmış demektir. Bu, limitinizi artırmanız veya uygulamanızı optimize etmeniz gerektiğinin işaretidir.

Performans Testi ile Limit Doğrulama

Belirlediğiniz limitlerin gerçekten çalışıp çalışmadığını test edin:

# stress-ng ile bellek testi (konteyner içinde)
docker run --rm 
  --memory="256m" 
  --name stress-test 
  ubuntu:20.04 
  bash -c "apt-get install -y stress-ng && stress-ng --vm 1 --vm-bytes 300M --timeout 30s"

# CPU stres testi
docker run --rm 
  --cpus="0.5" 
  --name cpu-stress 
  ubuntu:20.04 
  bash -c "apt-get install -y stress-ng && stress-ng --cpu 4 --timeout 30s"

# Ayrı terminal'de izle
docker stats stress-test
docker stats cpu-stress

256 MB limitli konteyner 300 MB talep ettiğinde OOM kill göreceksiniz. Bu beklenen davranıştır.

cgroups v2 ve Modern Kernel Farkları

Docker, Linux kernel’inin cgroups mekanizmasını kullanır. cgroups v2 (systemd 243+ ve kernel 4.5+ ile geldi) biraz farklı davranır:

# Sisteminizdeki cgroups versiyonunu kontrol edin
mount | grep cgroup
# veya
cat /proc/mounts | grep cgroup2

# Docker'ın hangi cgroups versiyonunu kullandığını kontrol
docker info | grep "Cgroup"

cgroups v2’de bazı ek özellikler geldi:

  • Memory QoS: Daha hassas bellek bant genişliği kontrolü
  • CPU weight: --cpu-shares yerine daha tutarlı CPU ağırlık sistemi
  • IO limitleri: Bellek baskısı altında daha iyi IO yönetimi

Ubuntu 22.04, Fedora 34+ ve diğer modern dağıtımlar artık varsayılan olarak cgroups v2 kullanıyor. Eski docker-compose sürümleriyle sorun yaşıyorsanız, bu uyumsuzluktan kaynaklanıyor olabilir.

İzleme ve Uyarı Sistemi Entegrasyonu

Kaynak limitleri koyduktan sonra bunları izlemek için Prometheus + cAdvisor kombinasyonu kullanabilirsiniz:

# cAdvisor'ı Docker ile başlat
docker run -d 
  --name cadvisor 
  --volume=/:/rootfs:ro 
  --volume=/var/run:/var/run:ro 
  --volume=/sys:/sys:ro 
  --volume=/var/lib/docker/:/var/lib/docker:ro 
  --publish=8080:8080 
  --memory="256m" 
  --cpus="0.3" 
  gcr.io/cadvisor/cadvisor:latest

cAdvisor container_memory_usage_bytes, container_cpu_usage_seconds_total gibi metrikleri Prometheus formatında sunar. Bu metrikleri kullanarak Grafana’da dashboard oluşturabilir ve limitin %80’ine ulaşıldığında uyarı alabilirsiniz.

En İyi Uygulamalar

Yıllar içinde edindiğim deneyimlerden öneriler:

  • Başlangıçta konservatif ol: Uygulamanın gerçek kullanımını ölçtükten sonra limitleri ayarla
  • Limit ile rezervasyon arasında 2:1 oran bırak: Bu esneklik sağlar
  • Bellek limitlerini swap ile birlikte düşün: Özellikle SSD’si olmayan sistemlerde swap yavaşlatır
  • Her zaman belge tut: Neden o limiti seçtiğini docker-compose.yml yorumlarına ekle
  • Staging ortamında mutlaka test et: Production’a uygulmadan önce yük testi yap
  • OOM eventlerini izle: Haftalık kontrol et, sürekli OOM yaşayan servis optimize edilmeli
  • CPU paylaşımlarını dikkatli kullan: Paylaşım sistemi sadece rekabette devreye girer, mutlak limit için --cpus kullan
  • Java uygulamaları için özel dikkat: JVM heap boyutunu -Xmx ile konteyner limitinin %75-80’ine ayarla, aksi takdirde JVM tüm RAM’i görerek dev heap açar

Sonuç

Docker’da CPU ve bellek kısıtlamaları, production ortamlarında bir lüks değil zorunluluktur. Tek bir konteynerin tüm sistemi çökertmesini önlemek, kaynak kullanımını tahmin edilebilir hale getirmek ve uygulamalarınızın SLA gereksinimlerini karşılamasını sağlamak için bu konuya gereken önemi vermeniz gerekiyor.

Özetle yapmanız gerekenler: Önce uygulamanızı limitsiz çalıştırıp gerçek kaynak kullanımını ölçün, sonra bu değerlerin biraz üzerinde limitler belirleyin, ardından OOM olaylarını ve kaynak tüketimini düzenli izleyin. Limitler statik değildir; uygulamanız büyüdükçe, kod değiştikçe bunları da gözden geçirmeniz gerekir.

En sonunda şunu unutmayın: İyi bir kaynak yönetimi stratejisi, sizi saat 3’te telefonla uyandıracak olayların büyük bir kısmını engeller. Ve bu, tek başına yeterince değerli bir neden.

Yorum yapın