Nginx Mimarisi: Worker Process ve Event Loop Derinlemesine İnceleme

Bir web sunucusunun altında ne döndüğünü anlamadan onu doğru yapılandırmak neredeyse imkansız. Nginx’i yıllarca kullanan sysadmin’lerin bile “neden bu kadar hızlı?” sorusuna yüzeysel cevaplar verdiğini görüyorum. Bu yazıda Nginx’in motor kısmını, yani worker process yapısını ve event loop mekanizmasını gerçekten derinlemesine inceleyeceğiz. Konfigürasyon dosyalarına bakıp “worker_processes auto yaz geç” demek yerine, bu direktiflerin arkasında ne olduğunu anlayacaksınız.

Nginx Neden Farklı: Apache ile Temel Fark

Apache, gelen her HTTP bağlantısı için ya yeni bir process ya da yeni bir thread açar. Bu model “prefork” ya da “worker” MPM olarak bilinir. 1000 eş zamanlı bağlantınız varsa Apache’nin 1000 ayrı thread ya da process yönetmesi gerekir. Thread başına ortalama 8MB bellek düşünürseniz, bu ciddi bir kaynak tüketimi demektir. İşin kötüsü, bu thread’lerin büyük çoğunluğu aslında I/O bekleyerek, yani ağdan veri gelmesini ya da diskten okuma tamamlanmasını bekleyerek zaman harcar.

Nginx’in yaklaşımı köklü biçimde farklıdır. Az sayıda worker process çalıştırır ve her worker binlerce bağlantıyı eş zamanlı yönetir. Bunu mümkün kılan şey ise non-blocking I/O ve event-driven mimari kombinasyonudur.

Master Process: Orkestranın Şefi

Nginx başlatıldığında önce bir master process devreye girer. Bu process root yetkisiyle çalışır ve aşağıdaki sorumlulukları üstlenir:

  • Konfigürasyon dosyalarını okur ve parse eder
  • 80 ve 443 gibi privileged portları açar
  • Worker process’leri fork eder ve yönetir
  • Log dosyalarını açık tutar
  • Zero-downtime reload işlemlerini yönetir
  • Sinyalleri (SIGHUP, SIGTERM vs.) işler

Master process’in PID’ini görmek için:

# Master process'i bul
ps aux | grep "nginx: master"

# Ya da nginx pid dosyasından oku
cat /run/nginx.pid

# Konfigürasyonu yeniden yükle (worker'ları öldürmez)
kill -HUP $(cat /run/nginx.pid)

# Ya da nginx komutunu kullan
nginx -s reload

Master process, SIGHUP aldığında mevcut worker’ları hemen öldürmez. Yeni konfigürasyonla yeni worker’lar başlatır, eski worker’ların ellerindeki aktif bağlantıları tamamlamalarını bekler, sonra eski worker’ları temiz şekilde kapatır. Bu mekanizma sayesinde canlı sistemlerde sıfır kesinti ile konfigürasyon güncellemesi yapılabilir.

Worker Process: Gerçek İşi Yapan Kısım

Worker process’ler master’ın fork ettiği alt process’lerdir. Genellikle düşük yetkili bir kullanıcı (www-data, nginx) olarak çalışırlar ve gerçek bağlantı işleme işini bunlar yapar.

# Kaç worker process çalıştığını kontrol et
ps aux | grep "nginx: worker"

# Worker process sayısını konfigürasyonda ayarla
# /etc/nginx/nginx.conf
# nginx.conf - Worker process temel konfigürasyonu
worker_processes auto;           # CPU çekirdek sayısı kadar worker aç
worker_cpu_affinity auto;        # Her worker'ı bir CPU core'a bağla
worker_priority -5;              # Worker'lara daha yüksek OS önceliği ver
worker_rlimit_nofile 65535;      # Her worker'ın açabileceği max dosya sayısı

events {
    worker_connections 4096;     # Her worker'ın aynı anda yönetebileceği bağlantı
    use epoll;                   # Linux için en verimli event mekanizması
    multi_accept on;             # Bir event'te birden fazla bağlantı kabul et
    accept_mutex off;            # Yüksek yük altında mutex'i kapat
}

worker_processes auto direktifi, Nginx’in /proc/cpuinfo dosyasını okuyarak sistemdeki mantıksal CPU sayısını tespit etmesi ve o kadar worker açması anlamına gelir. 8 core’lu bir sunucuda 8 worker process olur.

worker_cpu_affinity direktifi ise her worker’ı belirli bir CPU core’una “pin’ler”. Bu, CPU cache locality açısından önemlidir. Bir worker her zaman aynı core’da çalışırsa, o core’un L1/L2 cache’i worker’ın sık kullandığı veriyi sıcak tutabilir.

Event Loop: Non-Blocking I/O’nun Kalbi

Bir worker process başladığında sonsuz bir döngüye girer. Bu döngü “event loop” olarak adlandırılır. Her iterasyonda worker şunları yapar:

  • OS’tan bekleyen olayları toplar (yeni bağlantı, okunacak veri, yazılacak veri)
  • Her olay için ilgili handler’ı çağırır
  • Timer’ları kontrol eder
  • Tekrar başa döner

Buradaki kritik nokta şudur: Worker hiçbir zaman bir I/O operasyonunun tamamlanmasını beklemez. Bir bağlantıdan veri okumaya başladığında ve veri henüz hazır değilse, o bağlantıyı “bekleme listesine” koyar ve diğer olaylarla ilgilenmeye devam eder. Veri hazır olduğunda OS bir event üretir, worker bu event’i alır ve o bağlantıyla ilgilenmeye devam eder.

epoll: Linux’un Event Mekanizması

Linux kernel’i 2.6’dan itibaren epoll API’sini sunuyor. Nginx bu API’yi kullanarak binlerce file descriptor’ı aynı anda izleyebilir.

# Sistemde epoll desteğini kontrol et
cat /boot/config-$(uname -r) | grep CONFIG_EPOLL

# Nginx'in hangi event mekanizmasını kullandığını görmek için
nginx -V 2>&1 | grep event

# Aktif bağlantıları ve socket durumlarını izle
ss -s

# Worker başına bağlantı sayısını görüntüle
cat /proc/$(pgrep -o nginx)/net/sockstat

epoll’un eski select ve poll mekanizmalarından farkı önemlidir. select ve poll, her çağrıda tüm izlenen file descriptor’ları tarar. 10000 bağlantı izliyorsanız, her event kontrolünde 10000 descriptor’a bakılır. epoll ise sadece durumu değişen descriptor’ları döndürür. 10000 bağlantıdan sadece 5’i aktifse, epoll bu 5’i döndürür.

Bu fark O(n) yerine O(1) amortized complexity anlamına gelir ve büyük bağlantı sayılarında performans farkı devasa boyutlara ulaşır.

Connection State Machine

Nginx her HTTP bağlantısını bir durum makinesi (state machine) olarak modeller. Bir bağlantı şu durumlar arasında geçiş yapar:

  • Accepting: Yeni TCP bağlantısı kabul ediliyor
  • Reading headers: HTTP istek başlıkları okunuyor
  • Processing: İstek işleniyor, upstream’e proxy yapılıyor ya da dosya okunuyor
  • Sending: Yanıt istemciye gönderiliyor
  • Keepalive wait: Bağlantı açık tutuluyor, yeni istek bekleniyor
  • Closing: Bağlantı kapatılıyor

Worker process bu state machine’leri eş zamanlı yürütür. Bir bağlantı “Processing” durumundayken ve upstream sunucudan yanıt bekleniyorsa, worker diğer bağlantıların “Sending” ya da “Reading” adımlarını ilerletir.

# nginx.conf - Bağlantı yönetimi ayarları
http {
    # Keepalive bağlantı timeout'u
    keepalive_timeout 65s;
    
    # Tek bağlantıda kaç istek yapılabilir
    keepalive_requests 1000;
    
    # İstek başlığı okuma timeout'u
    client_header_timeout 15s;
    
    # İstek body okuma timeout'u
    client_body_timeout 15s;
    
    # Yanıt gönderme timeout'u
    send_timeout 30s;
    
    # TCP optimizasyonları
    tcp_nopush on;      # Yanıt başlık ve body'yi tek pakette gönder
    tcp_nodelay on;     # Küçük paketleri geciktirmeden gönder
    sendfile on;        # Kernel-space dosya transferi
}

sendfile on direktifi özellikle önemlidir. Normal şartlarda bir dosya sunulurken şu adımlar gerçekleşir: dosya kernelden user space’e kopyalanır, sonra user space’den tekrar kernele ve socket buffer’a kopyalanır. sendfile ile bu kopyalama kernel içinde gerçekleşir, CPU müdahalesi minimuma iner.

Worker Connection Limiti ve C10K Problemi

1999’da ortaya atılan C10K problemi, tek bir sunucuda 10000 eş zamanlı bağlantı nasıl yönetilir sorusunu soruyordu. Nginx bu problemi event-driven mimarisiyle çözdü. Bugün ise C100K ya da C1M konuşuyoruz.

# Teorik maksimum eş zamanlı bağlantı hesabı
# max_connections = worker_processes * worker_connections
# 4 worker * 4096 connection = 16384 eş zamanlı bağlantı

# OS limiti kontrol et
ulimit -n

# nginx kullanıcısı için limiti artır
# /etc/security/limits.conf
echo "nginx soft nofile 65535" >> /etc/security/limits.conf
echo "nginx hard nofile 65535" >> /etc/security/limits.conf

# Sistem geneli dosya limiti
sysctl fs.file-max
sysctl -w fs.file-max=2097152

# Kalıcı hale getir
echo "fs.file-max = 2097152" >> /etc/sysctl.conf

Gerçek dünya senaryosunda bir e-ticaret sitesi Black Friday trafiğine hazırlanıyorsa bu ayarlar kritik önem taşır. Ani trafik artışlarında worker_connections limitine takılmak, sunucunun yeni bağlantıları reddetmesi anlamına gelir.

Upstream Bağlantıları ve Keepalive

Nginx bir reverse proxy olarak çalışırken, backend sunucularla (uygulama sunucuları, veritabanı proxy’leri) bağlantı yönetimi ayrıca önemlidir. Her istek için yeni TCP bağlantısı açmak pahalıdır.

# nginx.conf - Upstream keepalive konfigürasyonu
upstream backend_app {
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
    
    # Her worker için upstream'e açık tutulacak bağlantı sayısı
    keepalive 32;
    
    # Keepalive bağlantı timeout'u
    keepalive_timeout 60s;
    
    # Tek keepalive bağlantıda kaç istek
    keepalive_requests 1000;
}

server {
    location /api/ {
        proxy_pass http://backend_app;
        
        # Keepalive için HTTP/1.1 şart
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        
        proxy_connect_timeout 5s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }
}

keepalive 32 direktifi, her worker process’in upstream’e 32 adet bağlantıyı açık tutacağı anlamına gelir. 4 worker varsa toplam 128 kalıcı bağlantı upstream’e hazır bekler. Yoğun trafik altında bu TCP handshake maliyetini ciddi ölçüde azaltır.

Shared Memory ve Worker’lar Arası İletişim

Worker process’ler birbirinden izole çalışsa da bazı verileri paylaşmaları gerekir. Rate limiting sayaçları, upstream’in sağlık durumu, SSL session cache bunların başında gelir. Nginx bu amaçla shared memory zone kullanır.

# nginx.conf - Shared memory zone örnekleri
http {
    # Rate limiting için shared zone
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
    
    # Bağlantı limiti için shared zone
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
    
    # SSL session cache shared memory'de tutulur
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    
    server {
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_conn conn_limit 10;
            proxy_pass http://backend_app;
        }
    }
}

zone=api_limit:10m ifadesindeki 10m, bu zone için 10 megabyte shared memory ayrılacağı anlamına gelir. 1 byte’lık bir IP adresi kaydı yaklaşık 64 byte yer kaplar, yani 10MB yaklaşık 160000 farklı IP adresini takip edebilir demektir.

Tüm worker’lar bu shared memory’yi okuyup yazabilir. Mutex koruması altında yapılan bu operasyonlar minimize edilmiştir çünkü her mutex lock/unlock event loop’u geçici olarak bloke eder.

Gerçek Dünya: Nginx Performans Profili Çıkarma

Bir production sunucusunda Nginx’in gerçekte ne yaptığını anlamak için şu araçları kullanıyorum:

# stub_status modülünü aktive et (nginx.conf)
server {
    listen 127.0.0.1:8080;
    location /nginx_status {
        stub_status on;
        allow 127.0.0.1;
        deny all;
    }
}

# Status çıktısını oku
curl http://127.0.0.1:8080/nginx_status

# Çıktı şöyle görünür:
# Active connections: 847
# server accepts handled requests
#  1234567 1234567 9876543
# Reading: 2 Writing: 48 Waiting: 797

# Anlık bağlantı durumu
watch -n 1 'curl -s http://127.0.0.1:8080/nginx_status'

# Worker process başına CPU kullanımı
pidstat -u -p $(pgrep -d',' nginx) 2 5

# Worker process'lerin sistem çağrılarını izle
strace -p $(pgrep -n "nginx: worker") -e trace=epoll_wait,accept4,read,write -c

stub_status çıktısındaki Waiting sayısı keepalive bağlantıları gösterir. Reading aktif istek okuma, Writing ise aktif yanıt yazma sayısıdır. Yüksek bir Writing/Active oranı, sunucunun ağ çıkışı bandwith’iyle sınırlı olduğuna işaret edebilir.

Worker Process Crash ve Otomatik Yeniden Başlatma

Worker process bir hata nedeniyle çökerse, master process bunu fark eder ve hemen yeni bir worker başlatır. Bu mekanizma Nginx’in yüksek erişilebilirliğinin önemli bir parçasıdır.

# Master process'in worker'ları nasıl izlediğini simüle etmek için
# Bir worker'ı zorla öldür ve master'ın tepkisini izle
WORKER_PID=$(ps aux | grep "nginx: worker" | head -1 | awk '{print $2}')
kill -9 $WORKER_PID

# Hemen ardından kontrol et - master yeni worker başlatmış olmalı
sleep 1
ps aux | grep nginx

# /var/log/nginx/error.log'u kontrol et
tail -f /var/log/nginx/error.log

Log dosyasında şuna benzer bir kayıt görürsünüz:

2024/01/15 14:23:45 [alert] 1234#1234: worker process 5678 exited on signal 9
2024/01/15 14:23:45 [notice] 1234#1234: start worker process 5679

Master process saniyeler içinde yeni worker’ı devreye alır. Bu sürede diğer worker’lar bağlantı almaya devam eder, dolayısıyla kullanıcı tarafında herhangi bir kesinti yaşanmaz.

Kernel Parametreleri ve Nginx Uyumu

Nginx’in event loop’u ne kadar iyi olursa olsun, OS kernel parametreleri yanlış yapılandırılmışsa performans tavanı düşük kalır.

# /etc/sysctl.conf - Nginx için optimize edilmiş kernel parametreleri
cat >> /etc/sysctl.conf << 'EOF'
# Bağlantı kuyruğu boyutu
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# TIME_WAIT soket yeniden kullanımı
net.ipv4.tcp_tw_reuse = 1

# Yerel port aralığı genişletme
net.ipv4.ip_local_port_range = 1024 65535

# TCP read/write buffer'ları
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# epoll için dosya limiti
fs.file-max = 2097152
EOF

# Parametreleri uygula
sysctl -p

# nginx'in dinlediği socket backlog'unu da artır
# nginx.conf
# listen 80 backlog=65535;

net.core.somaxconn parametresi özellikle kritiktir. Bu değer, kernel’in accept kuyruğuna alabileceği maksimum bağlantı sayısını belirler. Eğer Nginx’in worker_connections değeri bu limitin üzerindeyse, kernel bağlantıları düşürmeye başlar ve nginx.conf‘taki tüm optimizasyonlar anlamsız kalır.

Debug ve Troubleshooting: Event Loop’u İzlemek

Production’da aniden performans düşüşü yaşandığında event loop’da ne olduğunu anlamak gerekir.

# Nginx debug log'unu geçici olarak aç (dikkatli - disk I/O artar!)
# nginx.conf'a ekle:
# error_log /var/log/nginx/debug.log debug;
# Ardından: nginx -s reload

# perf ile CPU hotspot'larını bul
perf top -p $(pgrep -o "nginx: worker")

# gdb ile worker process'e bak (production'da dikkatli kullan)
gdb -p $(pgrep -o "nginx: worker") -ex "bt" -ex "quit" 2>/dev/null

# Ağ buffer doluluk durumunu izle
ss -tnp | grep nginx | awk '{print $2, $3}' | sort | uniq -c | sort -rn

# epoll_wait syscall süresini ölç - uzun süreler I/O beklemeye işaret eder
strace -T -p $(pgrep -o "nginx: worker") -e trace=epoll_wait 2>&1 | head -20

strace çıktısında epoll_wait çağrısının çok kısa sürelerde (0.001s altı) dönmesi, sürekli event geldiği ve worker’ın meşgul olduğu anlamına gelir. Uzun wait süreleri ise worker’ın boşta beklediğini gösterir.

Eğer bir worker’ın CPU kullanımı sürekli %100’de kalıyorsa ve epoll_wait hızlı dönüyorsa, ya çok yoğun trafik var ya da worker bir döngüye girmiştir. İkinci durum oldukça nadir olmakla birlikte, belirli upstream bağlantı hatalarında gözlemlenebilir.

Sonuç

Nginx’in worker process ve event loop mimarisi, modern web sunucusu tasarımının en başarılı örneklerinden biridir. Az sayıda process, non-blocking I/O ve kernel’in epoll mekanizmasının akıllıca kullanımı bir araya geldiğinde, mütevazı donanımda on binlerce eş zamanlı bağlantıyı sorunsuz yönetebilen bir sistem ortaya çıkar.

Buradan çıkarılacak pratik dersler şunlardır:

  • worker_processes değerini CPU sayısıyla eşleştirin, fazlası context switch maliyeti yaratır
  • worker_connections değerini OS file descriptor limitiyle uyumlu tutun
  • Upstream bağlantılarda keepalive kullanmak, yoğun trafik altında ciddi fark yaratır
  • Kernel parametreleri Nginx konfigürasyonu kadar önemlidir, birini optimize edip diğerini unutmayın
  • stub_status modülünü aktif tutun ve metriklerini monitoring sisteminize dahil edin

Bu mimariyi kavradığınızda, Nginx konfigürasyon belgelerindeki her direktif çok daha anlamlı hale gelir. Bir sayıyı körü körüne kopyalamak yerine, o sayının event loop üzerindeki etkisini anlayarak bilinçli kararlar verebilirsiniz.

Yorum yapın