taskset Komutu ile Süreçleri Belirli CPU Çekirdeklerine Bağlama (CPU Affinity Yönetimi)

Yıllar önce bir veritabanı sunucusunda yaşadığım bir olayı hiç unutamam. PostgreSQL instance’ı tüm çekirdekleri eşit şekilde kullanıyor gibiydi ama sorgu süreleri tutarsızdı, kimi zaman 50ms, kimi zaman 800ms. Monitoring grafikleri düzenli görünüyor, bellek tamam, disk I/O normal. Sonunda sorunun CPU scheduler’ın process’i çekirdekler arasında çok sık taşımasından kaynaklandığını fark ettim. Cache miss’ler artıyor, TLB flush’lar yaşanıyor, performans dengesi bozuluyordu. İşte o gün taskset ile gerçek anlamda tanıştım.

CPU Affinity Nedir ve Neden Önemlidir?

Linux kernel’ı bir process’i hangi CPU çekirdeğinde çalıştıracağına scheduler algoritmasına göre karar verir. Bu çoğu senaryo için yeterlidir, ancak yüksek performans gerektiren sistemlerde bu “özgürlük” bazen bir yük haline gelir.

CPU Affinity, bir process ya da thread’in hangi CPU çekirdek(leri)nde çalışabileceğini kısıtlama ya da belirleme yeteneğidir. taskset komutu, Linux üzerinde bu affinity maskesini hem okumak hem de ayarlamak için kullanılan araçtır. Kernel’ın sched_setaffinity() sistem çağrısının kullanıcı alanındaki arayüzüdür.

Neden bu kadar önemli?

  • L1/L2 Cache Locality: Bir process hep aynı çekirdekte çalışırsa, o çekirdeğin önbelleğindeki veriler “sıcak” kalır. Çekirdek değiştiğinde cache soğur, performans düşer.
  • NUMA Sistemleri: Çok soketli sunucularda bellek erişim süresi, hangi NUMA node’undan eriştiğinize göre değişir. Process’i doğru çekirdeğe sabitlemek bellek bant genişliğini artırır.
  • Öngörülebilir Gecikme: Gerçek zamanlı veya düşük gecikme gerektiren uygulamalarda scheduler’ın process’i taşıması ciddi jitter’a yol açar.
  • Gürültülü Komşu İzolasyonu: Aynı sunucuda çalışan farklı iş yüklerini farklı çekirdeklere hapsetmek, birbirlerini etkilemelerini önler.

taskset Kurulumu ve Temel Kullanım

taskset, util-linux paketinin bir parçasıdır. Çoğu modern Linux dağıtımında zaten yüklü gelir.

# Debian/Ubuntu
apt install util-linux

# RHEL/CentOS/AlmaLinux
yum install util-linux

# Mevcut versiyonu kontrol et
taskset --version

Komutun temel parametreleri şunlardır:

  • -c, –cpu-list: Çekirdekleri virgül ve tire ile insan okunabilir formatta belirtir (örn: 0,2,4-7)
  • -p, –pid: Mevcut bir process’in PID’ini belirtir
  • -a, –all-tasks: Process’in tüm thread’lerini etkiler
  • –help: Yardım mesajını gösterir

CPU maskesi iki farklı formatta belirtilebilir: hexadecimal bitmask ya da -c ile insan okunabilir liste. Bitmask kullanımı biraz kafa karıştırıcı olabilir, ben sahada neredeyse her zaman -c ile çalışırım.

Mevcut Process’in CPU Affinity’sini Okuma

Çalışan bir process’in hangi çekirdeklere atanmış olduğunu görmek için:

# PID ile affinity okuma
taskset -p 1234

# İnsan okunabilir formatta
taskset -cp 1234

# Örnek çıktı:
# pid 1234's current affinity list: 0-7
# (tüm 8 çekirdeğe erişim var demek)

# Birden fazla process için döngü
for pid in $(pgrep nginx); do
    echo "PID $pid: $(taskset -cp $pid)"
done

Sistem genelindeki tüm process’lerin affinity durumunu toplu görmek istediğinizde ps ile kombinleyebilirsiniz:

ps -eo pid,comm,psr | head -20
# psr kolonu: o an hangi çekirdekte çalıştığını gösterir

Yeni Process Başlatırken CPU Affinity Belirleme

En temiz yöntem, process’i başlatırken affinity’yi belirlemektir:

# Sadece çekirdek 0'da çalıştır
taskset -c 0 ./uygulama

# Çekirdek 2 ve 3'te çalıştır
taskset -c 2,3 python3 hesaplama_scripti.py

# Çekirdek 4'ten 7'ye kadar (4,5,6,7)
taskset -c 4-7 java -jar uygulama.jar

# Kombinasyon: 0, 2, 4'ten 6'ya kadar
taskset -c 0,2,4-6 ./worker

# Bitmask ile (hexadecimal) - çekirdek 0 ve 1 = 0x3
taskset 0x3 ./uygulama

Gerçek dünya örneği: Bir log işleme pipeline’ında birden fazla worker çalıştırıyorsunuz ve her birini farklı çekirdeklere sabitlemek istiyorsunuz:

# Worker 1: çekirdek 0-1
taskset -c 0-1 ./log_worker --input /var/log/app1.log &

# Worker 2: çekirdek 2-3
taskset -c 2-3 ./log_worker --input /var/log/app2.log &

# Worker 3: çekirdek 4-5
taskset -c 4-5 ./log_worker --input /var/log/app3.log &

echo "Tüm worker'lar başlatıldı"

Çalışan Process’in Affinity’sini Değiştirme

Production’da çalışan bir process’i durdurmadan affinity’sini değiştirebilirsiniz. Bu özellikle kritik servisler için çok değerlidir:

# Çalışan nginx master process'ini çekirdek 0-3'e sabitle
NGINX_PID=$(pgrep -x nginx | head -1)
taskset -cp 0-3 $NGINX_PID

# MySQL/MariaDB process affinity değiştirme
MYSQL_PID=$(pgrep mysqld)
taskset -cp 4-7 $MYSQL_PID

# Redis'i tek çekirdeğe sabitle (single-threaded olduğu için mantıklı)
taskset -cp 0 $(pgrep redis-server)

Önemli not: -a flag’i olmadan sadece ana process thread’i etkilenir. Tüm thread’leri etkilemek için -a kullanın:

# Java gibi çok thread kullanan uygulamalarda tüm thread'leri etkile
taskset -acp 0-3 $(pgrep -x java)

Systemd Servisleri ile CPU Affinity

Modern sistemlerde servisleri systemd ile yönetiyorsanız, affinity’yi servis unit dosyasında tanımlamak çok daha temiz bir yaklaşımdır. Böylece servis her yeniden başladığında affinity ayarı korunur.

# /etc/systemd/system/myapp.service
[Unit]
Description=Yüksek Performanslı Uygulama
After=network.target

[Service]
Type=simple
User=appuser
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
CPUAffinity=4 5 6 7
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
# Değişiklikleri uygula
systemctl daemon-reload
systemctl restart myapp

# Doğrulama
systemctl show myapp | grep CPUAffinity

Systemd’de CPUAffinity direktifi çekirdek numaralarını boşlukla ayırarak alır. Aralık belirtmek için de kullanabilirsiniz: CPUAffinity=0-3 ya da CPUAffinity=0 1 2 3.

NUMA Sistemlerde Gelişmiş Senaryo

İki soketli bir sunucuda çalışıyorsanız, numactl ile taskset‘i birlikte kullanmak çok daha etkili sonuç verir. Ancak salt taskset perspektifinden bakacak olursak:

# NUMA topolojisini önce anla
numactl --hardware
# ya da
lscpu | grep -E "NUMA|Socket|Core"

# Tipik 2 soketli sunucuda:
# Socket 0: Çekirdek 0-11
# Socket 1: Çekirdek 12-23

# Veritabanını Socket 0 çekirdeklerine sabitle
taskset -cp 0-11 $(pgrep postgres | head -1)

# Uygulama sunucusunu Socket 1'e sabitle
taskset -cp 12-23 $(pgrep java | head -1)

Bu şekilde her iş yükü kendi NUMA node’unun belleğine daha hızlı erişir ve iki iş yükü CPU cache seviyesinde birbirini “kirletmez”.

Gerçek Dünya Senaryosu: Yüksek Frekanslı Veri İşleme

Bir finansal veri şirketinde çalışırken şöyle bir senaryo ile karşılaştım. Piyasa verisi işleyen bir C++ uygulaması vardı, ortalama gecikme 2-3ms olması gerekirken 8-15ms arasında gidip geliyordu. Profiling yaptık, CPU migration’ları görüldü.

# Önce mevcut durumu incele
watch -n 1 'cat /proc/$(pgrep market_feed)/status | grep -i cpu'

# Migration sayısını izle (yüksek değer sorun işareti)
grep migration /proc/$(pgrep market_feed)/sched 2>/dev/null || 
    cat /proc/$(pgrep market_feed)/schedstat

# Çözüm: İzole çekirdeklere sabitle
# Kernel parametresi olarak isolcpus=6,7 boot'ta ayarlandı
# Sonra process bu izole çekirdeklere alındı
taskset -c 6,7 ./market_feed_processor --config prod.conf

# IRQ affinity'yi de ayarla (ağ kartı interrupt'ları için)
# Hangi IRQ numarasının ilgili NIC'e ait olduğunu bul
cat /proc/interrupts | grep eth0

# O IRQ'yu belirli çekirdeklere yönlendir
echo "3f" > /proc/irq/24/smp_affinity  # çekirdek 0-5

Sonuç: Ortalama gecikme 2.1ms’e indi, jitter neredeyse yok oldu.

Script ile Otomatik Affinity Yönetimi

Production ortamlarında bu işlemleri manuel yapmak hem riskli hem de zaman alıcıdır. Basit bir Bash script’i işleri kolaylaştırır:

#!/bin/bash
# cpu_affinity_manager.sh
# Servisleri önceden tanımlı çekirdeklere sabitleyen script

declare -A AFFINITY_MAP
AFFINITY_MAP["nginx"]="0-3"
AFFINITY_MAP["mysqld"]="4-7"
AFFINITY_MAP["redis-server"]="8"
AFFINITY_MAP["java"]="9-11"

LOG_FILE="/var/log/cpu_affinity.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

apply_affinity() {
    local process_name=$1
    local cpu_list=$2

    local pids
    pids=$(pgrep -x "$process_name" 2>/dev/null)

    if [ -z "$pids" ]; then
        log "UYARI: $process_name çalışmıyor, atlandı"
        return 1
    fi

    for pid in $pids; do
        if taskset -acp "$cpu_list" "$pid" >/dev/null 2>&1; then
            log "OK: $process_name (PID: $pid) -> CPU $cpu_list"
        else
            log "HATA: $process_name (PID: $pid) için affinity ayarlanamadı"
        fi
    done
}

log "=== CPU Affinity Ayarlama Başladı ==="

for process in "${!AFFINITY_MAP[@]}"; do
    apply_affinity "$process" "${AFFINITY_MAP[$process]}"
done

log "=== CPU Affinity Ayarlama Tamamlandı ==="

# Doğrulama
echo ""
echo "--- Mevcut Durum ---"
for process in "${!AFFINITY_MAP[@]}"; do
    pids=$(pgrep -x "$process" 2>/dev/null)
    for pid in $pids; do
        echo "$process (PID: $pid): $(taskset -cp $pid 2>/dev/null)"
    done
done
# Script'i çalıştırılabilir yap ve test et
chmod +x cpu_affinity_manager.sh
sudo ./cpu_affinity_manager.sh

# Crontab'a ekle (sistem açılışından sonra çalışsın)
echo "@reboot root /usr/local/sbin/cpu_affinity_manager.sh" >> /etc/cron.d/cpu-affinity

Docker Container’larda CPU Affinity

Container ortamında taskset doğrudan container içinde çalışmayabilir (capability gerektirir), ancak Docker’ın kendi CPU pinning mekanizması vardır:

# Container'ı belirli çekirdeklere sabitle
docker run --cpuset-cpus="0,1" nginx

# Docker Compose'da
# docker-compose.yml
# services:
#   app:
#     image: myapp
#     cpuset: "4-7"

# Çalışan container'ın CPU affinity'sini kontrol et
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' mycontainer)
taskset -cp $CONTAINER_PID

Dikkat Edilmesi Gereken Noktalar

Bu kadar güçlü bir araç kullanırken bazı tuzaklardan kaçınmak gerekir:

  • Hyperthreading farkındalığı: lscpu çıktısında “Thread(s) per core: 2” görüyorsanız, çekirdek 0 ve çekirdek 1 aslında aynı fiziksel çekirdeğin iki mantıksal thread’idir. Gerçek izolasyon için fiziksel çekirdek çiftlerini birlikte düşünün.
  • IRQ affinity ile uyum: Process’i belirli çekirdeklere sabitlediyseniz ama ağ/disk interrupt’ları hala o çekirdeklere geliyorsa, kazanç azalır. /proc/irq/*/smp_affinity dosyalarını da inceleyin.
  • Kernel isolcpus parametresi: Gerçek anlamda izolasyon istiyorsanız boot parametresine isolcpus=6,7 ekleyin. Bu çekirdeklere kernel scheduler normal process’leri koymayacaktır.
  • Process sayısı ve çekirdek dengesi: 4 çekirdeğe 8 process sabitlerseniz oversubscription olur. Affinity avantajı tersine dönebilir.
  • Root yetkisi: Kendi process’leriniz için root gerekmez, ama başka kullanıcıların process’leri için root gereklidir.
  • Monitoring ile doğrulama: Affinity ayarladıktan sonra htop‘ta F2 > Display Options > Show CPU column ile hangi çekirdekte çalıştığını takip edin.
# Fiziksel çekirdek topolojisini anlamak için
lscpu --extended
# ya da
cat /sys/devices/system/cpu/cpu*/topology/core_id

Sonuç

taskset, doğru kullanıldığında sıradan bir Linux sunucusunu performans açısından tamamen farklı bir seviyeye taşıyabilir. Özellikle çok çekirdekli sunucularda, veritabanı iş yüklerinde, düşük gecikme gerektiren uygulamalarda ve NUMA mimarili sistemlerde etkisi çok belirgin hissedilir.

Ancak şunu da söylemek gerekir: taskset bir sihir değildir. Yanlış uygulandığında, mesela az sayıda çekirdeğe çok fazla process sabitlendiğinde ya da hyperthreading gözetilmeden yapıldığında, performansı artırmak bir yana düşürebilir. Bu yüzden her değişikliği önce test ortamında deneyin, öncesi ve sonrası metriği alın, karar öyle verin.

Benim önerim şu yönde olur: Sisteminizde tutarsız gecikme, yüksek context switch sayısı ya da cache miss oranları görüyorsanız, taskset denemek için iyi bir adaydır. perf stat, mpstat ve htop ile mevcut durumu önce belgeleyin, sonra affinity uygulayın, sonra tekrar ölçün. Veri konuşur, sezgi yanıltır.

Bir yanıt yazın

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