cgroups ile Kaynak Kotalaması: CPU ve Bellek Limitlerini Uygulama Rehberi

Sunucu üzerinde çalışan bir process’in aniden tüm CPU’yu yemesi ya da belleği bitirmesi, klasik bir sysadmin kabusu. Özellikle çok kiracılı sistemlerde ya da microservice mimarilerinde bir servisin gidişatı diğerlerini direkt etkiliyor. İşte tam bu noktada cgroups (control groups) devreye giriyor. Linux çekirdeğinin sunduğu bu mekanizma, process’lere ve process gruplarına kaynak limitleri koymanı, öncelik atamanı ve kaynak kullanımını izlemeni sağlıyor. Bu yazıda cgroups’u sıfırdan anlatıp, gerçek dünya senaryolarıyla CPU ve bellek kotalamasını nasıl yapacağını göstereceğim.

cgroups Nedir ve Neden Önemlidir

cgroups, Linux 2.6.24 sürümüyle çekirdeğe giren, Google mühendisleri tarafından geliştirilen bir kaynak yönetim altyapısı. Temelde process’leri hiyerarşik gruplar halinde organize edip bu gruplara kaynak kısıtlamaları uygulamana yarıyor.

Şu anda iki major versiyon var: cgroups v1 ve cgroups v2. Modern sistemlerde (RHEL 9, Ubuntu 22.04+, Debian 11+) artık v2 varsayılan olarak geliyor. Ama pek çok production ortamında hâlâ v1 görüyoruz. Bu yazıda her ikisini de ele alacağız.

Hangi sistemi kullandığını anlamak için:

# cgroups versiyonunu kontrol et
cat /proc/filesystems | grep cgroup

# Mount noktalarını gör
mount | grep cgroup

# systemd üzerinden kontrol
systemctl status | head -5

Eğer /sys/fs/cgroup altında cgroup2 mount’u görüyorsan v2, memory, cpu gibi ayrı dizinler görüyorsan v1 kullanıyorsun demektir.

cgroups v1 ile Temel Kaynak Kotalaması

v1’de her kaynak tipi (CPU, memory, blkio vs.) ayrı bir subsystem olarak bağımsız hiyerarşide yönetiliyor. Dağınık ama yaygın, bu yüzden bilmek şart.

CPU Limitlemesi (v1)

# cpu cgroup'unu oluştur
mkdir /sys/fs/cgroup/cpu/myapp

# CPU quota'yı ayarla (100ms periyotta 50ms CPU kullanımı = %50 limit)
echo 50000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us

# Mevcut shell process'ini bu gruba ekle
echo $$ > /sys/fs/cgroup/cpu/myapp/cgroup.procs

# Limiti doğrula
cat /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us

Buradaki mantık şu: cpu.cfs_period_us her kaç mikrosaniyede bir hesaplama yapılacağını belirliyor. cpu.cfs_quota_us ise bu periyot içinde grubun kaç mikrosaniye CPU alacağını. 100ms periyotta 50ms quota = tek çekirdek üzerinde %50 kullanım.

Birden fazla çekirdek var ve %200 (2 core) vermek istiyorsan:

# 4 core'lu sistemde 2 core = %200 CPU
echo 200000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us

Bellek Limitlemesi (v1)

# memory cgroup'unu oluştur
mkdir /sys/fs/cgroup/memory/myapp

# Bellek limitini 512MB olarak ayarla
echo 536870912 > /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes

# Swap dahil toplam limiti ayarla (swap kullanmasın istiyorsan eşit değer ver)
echo 536870912 > /sys/fs/cgroup/memory/myapp/memory.memsw.limit_in_bytes

# OOM Killer'ı etkinleştir (limit aşılınca process'i öldür, sistemi çökertme)
echo 1 > /sys/fs/cgroup/memory/myapp/memory.oom_control

# Process'i gruba ekle
echo <PID> > /sys/fs/cgroup/memory/myapp/cgroup.procs

# Anlık bellek kullanımını izle
cat /sys/fs/cgroup/memory/myapp/memory.usage_in_bytes

memory.oom_control ayarı kritik. Eğer 1 verirsen ve limit aşılırsa sistem OOM Killer çalıştırıp process’i öldürür. 0 verirsen process askıya alınır ve bu çoğu zaman daha kötü bir sonuç doğurur.

cgroups v2 ile Modern Kaynak Yönetimi

v2, v1’in dağınık yapısını birleştiriyor. Tek bir unified hiyerarşi var ve her process yalnızca bir cgroup’a üye olabiliyor. Systemd’nin modern versiyonları tamamen v2 üzerine kurulu.

v2 Dosya Sistemi Yapısı

# v2 hiyerarşisini incele
ls /sys/fs/cgroup/

# Mevcut process'in hangi cgroup'ta olduğunu gör
cat /proc/self/cgroup

# Sistemdeki tüm cgroup'ları listele
systemd-cgls

v2 ile CPU Limitlemesi

# Yeni cgroup oluştur
mkdir /sys/fs/cgroup/myapp

# CPU controller'ı aktif et (üst cgroup'ta etkinleştirmek gerekiyor)
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control

# v2'de cpu.max kullanıyoruz: "quota period" formatında
# 50ms/100ms = %50 CPU
echo "50000 100000" > /sys/fs/cgroup/myapp/cpu.max

# CPU weight ayarla (nice value gibi, 1-10000 arası, default 100)
echo 200 > /sys/fs/cgroup/myapp/cpu.weight

# Process ekle
echo <PID> > /sys/fs/cgroup/myapp/cgroup.procs

# CPU istatistiklerini izle
cat /sys/fs/cgroup/myapp/cpu.stat

v2 ile Bellek Limitlemesi

# Memory controller'ı aktif et
echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control

# Hard limit: kesinlikle aşılamaz
echo "536870912" > /sys/fs/cgroup/myapp/memory.max

# Soft limit: sistem baskı altındaysa bu noktadan itibaren geri adım atar
echo "402653184" > /sys/fs/cgroup/myapp/memory.high

# Swap limiti
echo "536870912" > /sys/fs/cgroup/myapp/memory.swap.max

# Anlık kullanım
cat /sys/fs/cgroup/myapp/memory.current

# OOM durumlarını izle
cat /sys/fs/cgroup/myapp/memory.events

v2’deki memory.high ayarı v1’de yoktu ve son derece kullanışlı. Hard limit gibi ani bir kesilme yapmak yerine, bellek kullanımı bu değere ulaşınca kernel yavaş yavaş process’i kısıtlamaya başlıyor. Böylece daha öngörülebilir bir davranış elde ediyorsun.

systemd ile cgroup Entegrasyonu

Gerçek dünyada cgroup dosyalarına elle yazmak tercih edilmiyor. Systemd unit dosyaları üzerinden bu işi çok daha temiz yapabilirsin. Production’da da önerilen yol bu.

Servis için CPU ve Bellek Limiti Tanımlama

# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Servisi
After=network.target

[Service]
Type=simple
User=myapp
ExecStart=/usr/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure

# CPU Limitleri
CPUQuota=50%
CPUWeight=100

# Bellek Limitleri
MemoryMax=512M
MemoryHigh=400M
MemorySwapMax=0

# OOM davranisi
OOMScoreAdjust=500

[Install]
WantedBy=multi-user.target
# Unit dosyasını yükle ve başlat
systemctl daemon-reload
systemctl start myapp

# cgroup durumunu kontrol et
systemctl status myapp
systemd-cgtop

# Runtime'da limit güncelle (kalıcı değil)
systemctl set-property myapp.service CPUQuota=75%
systemctl set-property myapp.service MemoryMax=768M

# Kalıcı olarak güncelle
systemctl set-property --permanent myapp.service MemoryMax=768M

systemd-cgtop komutu, top gibi cgroup bazlı anlık kaynak kullanımını gösteriyor. Hangi servis ne kadar CPU ve bellek tüketiyor, bunu hızlıca görmenin en kolay yolu.

Gerçek Dünya Senaryosu: Web Sunucusu Kaynak Yönetimi

Diyelim ki bir sunucuda Nginx, PHP-FPM ve PostgreSQL çalıştırıyorsun. Bu senaryoda her servise belirli bir kaynak payı ayırmak istiyorsun. Sunucu 8 core, 16GB RAM olsun.

# Sistemdeki mevcut cgroup konfigürasyonunu gör
systemctl show nginx.service | grep -E "CPU|Memory"
systemctl show php-fpm.service | grep -E "CPU|Memory"
systemctl show postgresql.service | grep -E "CPU|Memory"

Nginx için (statik dosya servisi, az kaynak yeterli):

# /etc/systemd/system/nginx.service.d/limits.conf oluştur (override)
mkdir -p /etc/systemd/system/nginx.service.d/
cat > /etc/systemd/system/nginx.service.d/limits.conf << 'EOF'
[Service]
CPUQuota=100%
CPUWeight=100
MemoryMax=1G
MemoryHigh=800M
MemorySwapMax=0
EOF

PHP-FPM için (CPU yoğun uygulama):

cat > /etc/systemd/system/php-fpm.service.d/limits.conf << 'EOF'
[Service]
CPUQuota=300%
CPUWeight=200
MemoryMax=4G
MemoryHigh=3G
MemorySwapMax=512M
EOF

PostgreSQL için (bellek kritik, CPU önemli):

cat > /etc/systemd/system/postgresql.service.d/limits.conf << 'EOF'
[Service]
CPUQuota=400%
CPUWeight=300
MemoryMax=8G
MemoryHigh=7G
MemorySwapMax=0
EOF
# Tüm değişiklikleri uygula
systemctl daemon-reload
systemctl restart nginx php-fpm postgresql

# Doğrulama
systemd-cgtop -d 2

Kaynak Kullanımını İzleme ve Debug Etme

Limit koyunca iş bitmiyor. Düzgün çalışıp çalışmadığını ve OOM durumlarını izlemek kritik.

# Belirli bir cgroup'un detaylı istatistiklerini al
cat /sys/fs/cgroup/system.slice/nginx.service/memory.stat

# OOM kill olaylarını izle
cat /sys/fs/cgroup/system.slice/php-fpm.service/memory.events

# CPU throttling durumunu kontrol et (ne kadar kısıtlandı?)
cat /sys/fs/cgroup/system.slice/php-fpm.service/cpu.stat
# throttled_usec değerine bak, yüksekse quota artırman gerekiyor

# Kernel log'larında OOM mesajlarını izle
dmesg | grep -i "oom|killed process" | tail -20

# Sistemdeki tüm servislerin kaynak kullanımını periyodik izle
watch -n 2 'systemd-cgtop -n 1 -b | head -20'

cpu.stat içindeki throttled_usec değeri çok önemli. Bu değer yüksekse o servis CPU kotasıyla sık sık sınırlanıyor demektir. Uygulamanın yavaş çalışmasının sebebi bu olabilir ve kotayı artırman ya da uygulamayı optimize etmen gerekiyor.

Geçici Process’ler için systemd-run Kullanımı

Bir script ya da komut çalıştırırken anlık kaynak limiti koymak istiyorsan systemd-run ideal çözüm:

# Tek seferlik komut çalıştır, max %50 CPU ve 256MB bellek
systemd-run --scope 
  -p CPUQuota=50% 
  -p MemoryMax=256M 
  bash -c "python3 /opt/scripts/heavy_analysis.py"

# Arka planda servis olarak çalıştır
systemd-run 
  --unit=temp-backup 
  -p CPUQuota=25% 
  -p MemoryMax=512M 
  -p IOWeight=50 
  /opt/backup/run_backup.sh

# Çalışan geçici servisi izle
systemctl status temp-backup
journalctl -u temp-backup -f

Bu yöntem özellikle batch işler, veri migrasyon scriptleri ve geçici ağır yükler için mükemmel. Hem sistemi korursun hem de işin bitmesini bekleyebilirsin.

Pratik İpuçları ve Sık Yapılan Hatalar

Swap’ı sıfırlamayı unutma. MemoryMax koyuyorsun ama swap serbest bırakıyorsan process gerçekte daha fazla sanal bellek kullanıyor. Her zaman MemorySwapMax=0 ekle, ya da en azından kontrollü bir değer ver.

CPU quota’yı çok düşük koymaktan kaçın. %10’nun altındaki kotalar bazen beklenmedik latency spike’larına yol açıyor. Özellikle Java uygulamalarda GC cycle’ları bu noktada sorun çıkarıyor.

memory.high değerini iyi ayarla. memory.high değeri memory.max’in yaklaşık %80’i olmalı. Aralarında fark çok azsa kernel sürekli kısıtlama yapıp uygulamayı yavaşlatır.

Limit değerlerini önceden test et. Production’a almadan önce aynı yükü laboratuvarda simüle edip ne kadar CPU ve bellek gerektiğini ölç:

# Process'in gerçek kaynak kullanımını ölç (30 saniye boyunca)
PID=<uygulama_pid>
for i in $(seq 1 30); do
  CPU=$(cat /proc/$PID/stat | awk '{print $14+$15}')
  MEM=$(cat /proc/$PID/status | grep VmRSS | awk '{print $2}')
  echo "$(date +%s) CPU_ticks:$CPU RSS_kB:$MEM"
  sleep 1
done

Konteyner ortamlarında dikkatli ol. Docker ve Kubernetes zaten cgroup kullanıyor. Docker container’larına el ile cgroup müdahalesi yapma, Docker’ın kendi limitleme mekanizmalarını kullan (--cpus, --memory flagleri). Aksi halde çakışmalar olur.

# Docker container'ı için CPU ve bellek limiti
docker run -d 
  --name myapp 
  --cpus="1.5" 
  --memory="512m" 
  --memory-swap="512m" 
  myapp:latest

# Çalışan container'ın kaynak kullanımını izle
docker stats myapp

cgroup limitlerini monitoring sistemine bağla. Prometheus’ta node_exporter cgroup metriklerini toplayabiliyor. --collector.cgroups flag’ini aktif edersen Grafana dashboard’larında servis bazlı kaynak kullanımını görebilirsin.

OOM Durumlarını Yönetmek

Bellek limiti aşıldığında ne olacağını kontrol etmek kritik. Default davranış OOM Kill, ama bunu biraz daha rafine edebilirsin:

# OOM score'unu ayarla (yüksek değer = önce bu öldürülür, -1000 ile 1000 arası)
# Kritik servislerin öldürülme önceliğini düşür
echo -500 > /proc/$(pidof postgresql)/oom_score_adj

# Systemd üzerinden
cat > /etc/systemd/system/postgresql.service.d/oom.conf << 'EOF'
[Service]
OOMScoreAdjust=-500
EOF

# OOM kill loglarını real-time izle
journalctl -k -f | grep -i oom

OOM olaylarından sonra sistem neden o process’i seçti anlamak için:

# OOM killer'ın kararını logdan incele
dmesg | grep -A 20 "Out of memory"

# Her process'in mevcut OOM score'unu gör
for pid in $(ls /proc | grep '^[0-9]*$'); do
  score=$(cat /proc/$pid/oom_score 2>/dev/null)
  name=$(cat /proc/$pid/comm 2>/dev/null)
  [ -n "$score" ] && echo "$score $name ($pid)"
done | sort -rn | head -10

Sonuç

cgroups, Linux sistemlerde kaynak yönetiminin temel taşı. Doğru kullanıldığında bir process’in çıldırıp tüm sistemi mahvetmesinin önüne geçiyor, servislerin birbirinden izole çalışmasını sağlıyor ve kapasite planlamasını çok daha öngörülebilir hale getiriyor.

Başlangıç için tavsiyem şu: Mevcut sistemindeki kritik servislerin gerçek kaynak kullanımını önce ölç, bir hafta izle. Sonra bu verilerle makul limitler belirle ve systemd unit override dosyalarıyla uygula. İlk aşamada sadece MemoryMax ile başlayabilirsin, en kritik koruma o.

CPU throttling ve OOM eventlerini düzenli izlemeli, limitlerini uygulamanın büyümesiyle paralel güncellemelisin. Bu bir kez yapılıp unutulan bir iş değil, sürekli takip gerektiren bir operasyonel pratik. Ama bir kez oturup düzgün kurduğunda, “bir servis diğerini öldürdü” şikayetlerinden bir daha duymayacaksın.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir