NUMA Mimarisi ve Çok İşlemcili Sistemlerde Performans Optimizasyonu
Büyük sunucularda performans sorunlarıyla uğraşırken çoğu zaman CPU kullanımı, disk I/O veya ağ bant genişliğine bakılır. Ama bazı durumlarda tüm bu metrikler normal görünür, yük dengeli dağılmıştır, yine de uygulama beklediğiniz gibi performans vermez. İşte bu noktada NUMA mimarisi devreye giriyor ve çoğu sysadmin için kör nokta olmaya devam ediyor.
NUMA Nedir ve Neden Önemlidir?
NUMA, Non-Uniform Memory Access kelimelerinin kısaltmasıdır. Türkçesiyle “Tekdüze Olmayan Bellek Erişimi” diyebiliriz. Modern çok işlemcili sunucularda her fiziksel CPU’nun (soket) kendi yerel bellek bankası vardır. Bir CPU kendi yerel belleğine eriştiğinde bu işlem hızlıdır. Ama başka bir CPU’nun belleğine erişmesi gerektiğinde bu erişim bir köprü (interconnect) üzerinden geçer ve belirgin biçimde daha yavaştır.
Bu ayrımı şöyle somutlaştıralım: 2 soketli bir sunucuda CPU0 kendi belleğine ~70 nanosaniyede erişirken, CPU1’in belleğine erişmesi ~120 nanosaniye sürebilir. Bu fark küçük gibi görünse de yoğun bellek operasyonlarında, özellikle veritabanları ve in-memory uygulamalarda ciddi bir etki yaratır.
NUMA’nın temel bileşenleri şunlardır:
- NUMA Node: Bir veya birden fazla fiziksel CPU soketi ve ona bağlı bellek bankalarından oluşan grup
- Local Memory: Bir NUMA node’un kendi bellek bankası
- Remote Memory: Başka bir NUMA node’un bellek bankası
- NUMA Distance: Node’lar arası erişim gecikmesinin göreceli ölçüsü
- Interconnect: Node’ları birbirine bağlayan donanım yolu (AMD’de Infinity Fabric, Intel’de QPI/UPI)
Sisteminizin NUMA Topolojisini Anlamak
Bir sistemi optimize etmeden önce mevcut yapıyı tam olarak anlamak gerekir. Linux’ta bunu yapmak için birkaç araç mevcuttur.
# NUMA topolojisine genel bakış
numactl --hardware
# Örnek çıktı:
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
# node 0 size: 64301 MB
# node 0 free: 48123 MB
# node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
# node 1 size: 64508 MB
# node 1 free: 51204 MB
# node distances:
# node 0 1
# 0: 10 21
# 1: 21 10
Burada node distances kısmı kritik. Node’un kendi belleğine erişim maliyeti 10 (referans değer), diğer node’un belleğine erişim maliyeti 21 olarak görünüyor. Yani remote erişim, local erişimin iki katından fazla maliyetli.
# Daha detaylı topoloji için lstopo veya hwloc-ls
lstopo --of ascii
# NUMA istatistiklerini gerçek zamanlı izleme
numastat
# Belirli bir process için NUMA istatistikleri
numastat -p <pid>
# Tüm process'ler için özet
numastat -s
# /sys üzerinden NUMA bilgilerine erişim
cat /sys/devices/system/node/node0/meminfo
cat /sys/devices/system/node/node1/meminfo
# CPU'ların hangi NUMA node'una ait olduğunu görmek
cat /sys/devices/system/cpu/cpu0/topology/physical_package_id
# Tüm CPU'lar için döngüsel kontrol
for cpu in /sys/devices/system/cpu/cpu[0-9]*/topology/physical_package_id; do
echo "$(basename $(dirname $cpu)): Node $(cat $cpu)"
done
Gerçek Dünya Senaryosu: PostgreSQL ve NUMA
Tipik bir senaryo düşünelim. 2 soketli, her sokette 16 core, toplamda 128GB RAM olan bir sunucuda PostgreSQL çalıştırıyorsunuz. Veritabanı sorguları tutarsız gecikmeler gösteriyor, bazıları çok hızlı tamamlanırken bazıları yavaş. CPU kullanımı %40 civarında, bellek baskısı yok.
Önce NUMA istatistiklerine bakalım:
# numastat çıktısını izleyin
watch -n 1 numastat
# postgresql process'i için özel izleme
PG_PID=$(pgrep -x postgres | head -1)
numastat -p $PG_PID
# Beklenen sorunlu çıktı:
# Per-node process memory usage (in MBs) for PID 1234 (postgres)
# Node 0 Node 1 Total
# --------------- --------------- ---------------
# Huge 0.00 0.00 0.00
# Heap 8234.12 6123.45 14357.57
# Stack 0.23 0.18 0.41
# Private 1205.34 2341.22 3546.56
Bu çıktıda belleğin iki node arasında dağıldığını görüyoruz. PostgreSQL kah Node 0’daki CPU’larda, kah Node 1’dekinde çalışıyor ve sürekli remote memory erişimi yapıyor.
Çözüm, PostgreSQL’i belirli bir NUMA node’una sabitlemek:
# PostgreSQL'i Node 0'a bağla, sadece Node 0'ın belleğini kullan
numactl --cpunodebind=0 --membind=0 /usr/bin/postgres -D /var/lib/postgresql/data
# Systemd service dosyasına eklemek için
# /etc/systemd/system/postgresql.service.d/numa.conf
[Service]
ExecStart=
ExecStart=numactl --cpunodebind=0 --membind=0 /usr/bin/postgres -D /var/lib/postgresql/data
Bu ayarın ardından yeniden test ettiğinizde sorgu gecikmelerinin önemli ölçüde düzeldiğini göreceksiniz. Ancak bu yaklaşımın bir dezavantajı var: Tüm sisteminizin yarısını bir uygulamaya hasrediyorsunuz. Daha büyük sistemlerde ve birden fazla servis çalışırken planlamayı buna göre yapmanız gerekiyor.
NUMA Politikaları ve numactl Kullanımı
numactl aracı NUMA politikalarını yönetmenin temel yoludur. Farklı senaryolar için farklı politikalar mevcuttur.
# --localalloc: Her zaman mevcut CPU'nun bulunduğu node'dan bellek al
numactl --localalloc ./uygulamam
# --preferred: Tercih edilen node'u belirt, doluysa diğerine geç
numactl --preferred=0 ./uygulamam
# --membind: Sadece belirtilen node'lardan bellek kullan
numactl --membind=0,1 ./uygulamam
# --cpunodebind: Sadece belirtilen node'ların CPU'larını kullan
numactl --cpunodebind=1 ./uygulamam
# --physcpubind: Belirli fiziksel CPU'lara bağla
numactl --physcpubind=0,1,2,3 ./uygulamam
# --interleave: Belleği node'lar arasında interleave et (throughput odaklı)
numactl --interleave=all ./uygulamam
Interleave politikası özellikle dikkat çekiyor. Tek bir büyük bellek alanına erişen ve hem okuma hem yazma bant genişliğini maksimize etmek isteyen uygulamalar için ideal. Örneğin in-memory analitik iş yüklerinde gecikme değil throughput önemliyse interleave mükemmel çalışır. Ancak gecikme kritikse ve bellek erişim paterni lokalite gösteriyorsa --membind daha iyi sonuç verir.
Linux Kernel’ın NUMA Dengeleme Mekanizması
Linux kernel 3.8’den itibaren Automatic NUMA Balancing özelliği geldi. Bu özellik, çalışan process’lerin bellek erişim paternlerini izleyerek belleği ve thread’leri daha optimal node’lara taşıyor.
# Automatic NUMA balancing durumunu kontrol et
cat /proc/sys/kernel/numa_balancing
# 1 = aktif, 0 = pasif
# Aktif/pasif yap
echo 1 > /proc/sys/kernel/numa_balancing
echo 0 > /proc/sys/kernel/numa_balancing
# Kalıcı hale getirmek için sysctl.conf
echo "kernel.numa_balancing = 1" >> /etc/sysctl.conf
sysctl -p
Otomatik dengeleme her zaman iyi bir fikir değildir. Belleği taşımak için kernel process’leri durdurmak zorunda kalabilir (page fault mekanizması aracılığıyla). Bazı iş yüklerinde bu küçük duraklamalar latency spike’larına yol açar. Redis, Memcached gibi düşük latency gerektiren uygulamalarda otomatik dengelemeyi kapatıp manuel politika uygulamak daha iyi olabilir.
# NUMA balancing istatistiklerini izleme
cat /proc/vmstat | grep numa
# numa_hit: Local node'dan başarılı bellek tahsisi
# numa_miss: Remote node'dan yapılan bellek tahsisi
# numa_page_migrate: Taşınan page sayısı
# Sürekli izleme
watch -n 2 'cat /proc/vmstat | grep -E "numa_hit|numa_miss|numa_migrate"'
numa_miss değeri yüksekse process’leriniz remote memory’e sürekli erişiyor demektir. Bu değerin numa_hite oranı yükseldikçe NUMA optimizasyonu yapmanız gerektiğini gösterir.
cgroups ile NUMA Yönetimi
Kubernetes veya benzeri orkestrasyon platformu kullanıyorsanız ya da birden fazla uygulamayı aynı sunucuda izole etmeniz gerekiyorsa cgroups’un cpuset kontrolörü işinize yarar.
# cpuset cgroup oluştur
mkdir /sys/fs/cgroup/cpuset/uygulama1
# CPU ve bellek node'larını ata
echo "0-7" > /sys/fs/cgroup/cpuset/uygulama1/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/uygulama1/cpuset.mems
# Process'i bu cgroup'a ekle
echo $PID > /sys/fs/cgroup/cpuset/uygulama1/cgroup.procs
# Bellek migrasyonunu aktif et (mevcut belleği de taşıt)
echo 1 > /sys/fs/cgroup/cpuset/uygulama1/cpuset.memory_migrate
Kubernetes ortamında bu biraz daha karmaşık ama CPU Manager politikası olarak static kullanarak benzer sonuçlar elde edebilirsiniz. Guaranteed QoS sınıfındaki pod’lar için Kubernetes otomatik olarak exclusive CPU ve NUMA ataması yapabilir.
Huge Pages ve NUMA
Huge pages, TLB (Translation Lookaside Buffer) basıncını azaltarak bellek yoğun uygulamalarda ciddi performans artışı sağlar. NUMA ile birlikte kullanıldığında her node için ayrı huge page havuzu yönetmek önemlidir.
# Her node için huge page durumunu kontrol et
cat /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
cat /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
# Node başına huge page tahsis et
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
# Transparent Huge Pages durumunu kontrol et
cat /sys/kernel/mm/transparent_hugepage/enabled
# Veritabanları için THP'yi kapat (genellikle önerilir)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
Oracle DB, PostgreSQL ve Redis gibi uygulamalar için her NUMA node’unda yeterli huge page olduğundan emin olun. Aksi takdirde uygulama local huge page bulamazsa remote node’daki huge page’i kullanır ve NUMA optimizasyonunuzun etkisi azalır.
Performans Metrikleri ve İzleme Araçları
NUMA optimizasyonlarının etkisini ölçmek için doğru araçları kullanmak şart.
# perf ile NUMA miss oranını ölç
perf stat -e cache-misses,cache-references,LLC-load-misses
-e numa:numa_hit,numa:numa_miss
-p $PID -- sleep 10
# turbostat ile CPU frekansı ve enerji kullanımını node bazında izle
turbostat --interval 5
# mpstat ile CPU kullanımını core bazında izle
mpstat -P ALL 1
# numatop ile gerçek zamanlı NUMA analizi (ayrıca kurulum gerektirir)
# Ubuntu/Debian
apt-get install numatop
# RHEL/CentOS
yum install numatop
numatop
# Kapsamlı NUMA raporu için özel script
#!/bin/bash
echo "=== NUMA Topology ==="
numactl --hardware
echo ""
echo "=== NUMA Statistics ==="
numastat
echo ""
echo "=== Per-Process NUMA Usage ==="
for pid in $(pgrep -f "postgres|java|redis"); do
pname=$(cat /proc/$pid/comm 2>/dev/null)
echo "--- Process: $pname (PID: $pid) ---"
numastat -p $pid 2>/dev/null
done
echo ""
echo "=== NUMA Balancing Stats ==="
grep -E "numa_hit|numa_miss|numa_migrate|numa_foreign" /proc/vmstat
Bu scripti cron’a alıp çıktıları loglayarak zaman içinde NUMA davranışını takip edebilirsiniz. Ani artışlar genellikle yeni deploy edilen bir servisin NUMA’ya duyarsız şekilde çalıştığını gösterir.
JVM Tabanlı Uygulamalar için NUMA Optimizasyonu
Java uygulamaları NUMA konusunda özellikle sorunlu olabilir çünkü JVM kendi bellek yönetimini yapar. HotSpot JVM’in NUMA-aware heap yönetimi için özel parametreleri vardır.
# JVM'i NUMA-aware modda başlat
java -XX:+UseNUMA
-XX:+UseParallelGC
-Xms16g -Xmx16g
-jar uygulamam.jar
# UseNUMA ile birlikte G1GC kullanımı (JDK 14+)
java -XX:+UseNUMA
-XX:+UseG1GC
-XX:G1HeapRegionSize=16m
-Xms32g -Xmx32g
-jar uygulamam.jar
# Ek olarak numactl ile de kısıtlama uygulayın
numactl --cpunodebind=0 --membind=0
java -XX:+UseNUMA -Xms16g -Xmx16g -jar uygulamam.jar
-XX:+UseNUMA parametresi JVM’in heap’i NUMA node’larına göre bölmesini sağlar. Her thread mümkün olduğunda kendi NUMA node’undaki heap bölümünden nesne oluşturur. Bu, özellikle thread başına izole işlem yapan uygulamalarda (web sunucuları, uygulama sunucuları) belirgin fark yaratır.
IRQ Affinity ve Ağ Kartı Optimizasyonu
NUMA optimizasyonu sadece işlemci ve bellek ile sınırlı değil. Ağ trafiği yoğun sistemlerde NIC interrupt’larının doğru NUMA node’una yönlendirilmesi kritik önem taşır.
# Ağ kartının hangi NUMA node'unda olduğunu bul
cat /sys/class/net/eth0/device/numa_node
# Çıktı: 0 veya 1
# IRQ'ları ilgili NUMA node'una bağla
# Önce eth0'ın IRQ numaralarını bul
grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'
# IRQ affinity ayarla (örnek IRQ numarası 34 için)
# Node 0 CPU'ları 0-7 ise (hex: 0xff)
echo ff > /proc/irq/34/smp_affinity
# irqbalance'ı durdurup manuel yönetim yapıyorsanız
systemctl stop irqbalance
# set_irq_affinity scripti ile otomatik yapılandırma
# (Intel'in set_irq_affinity.sh scripti ile)
./set_irq_affinity.sh eth0
10GbE veya 25GbE kartlarda bu optimizasyon önemli. NIC, PCI bus üzerinden Node 1’deki CPU’ya bağlıyken Node 0’daki bir process’e paket gönderiyorsa hem CPU hem de bellek için remote hop yapıyorsunuz demektir. IRQ’ları NIC’in fiziksel olarak bağlı olduğu node’un CPU’larına bağlamak bu masrafı azaltır.
BIOS Ayarları ve Donanım Düzeyi Optimizasyon
Yazılım tarafında ne yaparsanız yapın, BIOS ayarları NUMA performansını doğrudan etkiler. Üretim sunucularında kontrol etmeniz gereken kritik ayarlar şunlardır:
- NUMA Enabled: BIOS’ta NUMA mutlaka aktif olmalıdır. Bazı sistemlerde varsayılan olarak kapalı gelir ve Linux tüm belleği tek düz adres alanı olarak görür.
- Memory Interleaving: Kapalı olmalıdır; açıkken hardware otomatik interleave yapar ve yazılım tarafındaki NUMA farkındalığını anlamsız kılar.
- Node Interleaving: Kapalı olmalıdır; Memory Interleaving ile aynı etkiyi yapar.
- Sub-NUMA Clustering (SNC): Intel Cascade Lake ve sonrası için her soket birden fazla NUMA node’u olarak görünebilir. SNC2 modunda bir soket 2 node, SNC4 modunda 4 node gibi görünür. Yazılımınız buna hazır değilse kapalı tutun.
- C-States: Derin uyku durumları NUMA gecikmesini artırabilir. Latency kritik iş yüklerinde C-State sınırlaması yapın.
Gerçek Dünya Senaryosu: Redis ve NUMA
Bir Redis sunucusunda yüksek latency spike’ları yaşadığınızı düşünelim. Redis single-threaded çalışır (komut işleme açısından) ve bellek yoğun bir uygulamadır.
# Redis PID'ini bul ve NUMA istatistiklerine bak
REDIS_PID=$(pgrep redis-server)
numastat -p $REDIS_PID
# Eğer numa_miss yüksekse ve Redis Node 1'de çalışırken
# Node 0'dan bellek kullanıyorsa şunu yapın:
# Redis'i tek node'a sabitle
numactl --cpunodebind=0 --membind=0 redis-server /etc/redis/redis.conf
# Systemd için
# /etc/systemd/system/redis.service.d/override.conf
[Service]
ExecStart=
ExecStart=numactl --cpunodebind=0 --membind=0 /usr/bin/redis-server /etc/redis/redis.conf
systemctl daemon-reload
systemctl restart redis
# Optimizasyon sonrası latency testi
redis-cli --latency -h 127.0.0.1 -p 6379
redis-cli --latency-history -h 127.0.0.1 -p 6379
Pratikte Redis’i tek NUMA node’una sabitledikten sonra P99 latency’nin %30-50 düşmesi nadir değil. Özellikle 64GB üzeri bellek kullanan büyük Redis instance’larında bu etki çok belirgin olur.
Sürekli İzleme için Prometheus ve Node Exporter
Tek seferlik ölçümler yerine sürekli izleme kurmak daha değerlidir. Node Exporter varsayılan olarak NUMA istatistiklerini toplar.
# Node Exporter NUMA metriklerini kontrol et
curl -s http://localhost:9100/metrics | grep numa
# Önemli metrikler:
# node_memory_numa_hit_total
# node_memory_numa_miss_total
# node_memory_numa_foreign_total
# node_memory_numa_interleave_total
# Prometheus'ta NUMA miss oranı sorgusu
# rate(node_memory_numa_miss_total[5m]) /
# (rate(node_memory_numa_hit_total[5m]) + rate(node_memory_numa_miss_total[5m]))
Grafana’da bu metriği görselleştirin ve NUMA miss oranı belirli bir eşiğin üzerine çıktığında alert tetikleyin. %5’in üzerindeki miss oranları genellikle müdahale gerektiren bir duruma işaret eder.
Sonuç
NUMA optimizasyonu, modern çok işlemcili sunucularda göz ardı edilen ama ciddi performans kazanımları sağlayan bir alandır. Özetlemek gerekirse:
- Önce ölç, sonra optimize et.
numactl --hardware,numastatve/proc/vmstatile mevcut durumu belgeleyin. - Her iş yükü farklı politika ister. Gecikme odaklı uygulamalarda
--membind, throughput odaklılarda--interleavedaha iyi çalışır. - Otomatik dengelemeyi körü körüne aktif bırakmayın. Kritik uygulamalar için NUMA politikasını manuel yönetin ve otomatik dengelemeyi kapatın.
- Ağ kartlarını unutmayın. NIC’in hangi NUMA node’unda olduğunu bilin ve IRQ affinity’yi buna göre ayarlayın.
- JVM uygulamalarında
-XX:+UseNUMAekleyin. Bu parametre çoğu zaman sıfır risk, pozitif kazanım sağlar. - BIOS ayarlarını doğrulayın. Yazılım optimizasyonu yapmadan önce NUMA’nın donanım düzeyinde doğru yapılandırıldığından emin olun.
NUMA, yüzde hesaplarına sığmayan türden bir performans sorunudur. Bir sunucu %40 CPU kullanımında boğulurken diğeri aynı iş yükünü %20 ile kaldırıyorsa, aralarındaki fark çoğu zaman NUMA farkındalığından kaynaklanıyor olabilir. Bu konuya zaman ayırmak, özellikle veritabanı sunucuları ve in-memory sistemler için yatırımın çok üzerinde geri dönüş sağlar.
