Node.js Cluster Modülü ile Çok Çekirdekli CPU Kullanımı
Modern web uygulamalarında performans, her şeyin üzerinde tutulan bir öncelik haline geldi. Kullanıcılar sayfaların anında yüklenmesini bekliyor, API’lerin milisaniyeler içinde cevap vermesini istiyor ve herhangi bir yavaşlamayı hemen fark edip uygulamayı terk ediyor. Node.js, tek iş parçacıklı (single-threaded) yapısı sayesinde I/O yoğun işlemlerde mükemmel performans gösterse de bu mimari, çok çekirdekli modern sunucularda ciddi bir darboğaz oluşturur. Sunucunuzda 32 çekirdek varken Node.js uygulamanız sadece bir tanesini kullanıyorsa, kalan 31 çekirdeği boşa harcıyorsunuz demektir. İşte bu noktada Cluster modülü devreye girer.
Node.js’in Tek İş Parçacıklı Yapısının Sınırları
Node.js, V8 motoru üzerinde çalışır ve varsayılan olarak tek bir CPU çekirdeğinde çalışır. Event loop mekanizması sayesinde asenkron I/O işlemlerini son derece verimli yönetir, ancak CPU yoğun işlemlerde bu yapı yetersiz kalır. Bir HTTP isteği geldiğinde ve bu istek yoğun bir hesaplama gerektirdiğinde, event loop bloklanır ve diğer istekler sıraya girmek zorunda kalır.
# Sistem çekirdek sayısını öğrenelim
nproc
# ya da daha detaylı bilgi için:
lscpu | grep "CPU(s):"
# Node.js'in hangi çekirdekte çalıştığını görmek için
node -e "console.log('PID:', process.pid); setInterval(() => {}, 1000)"
# Başka terminalde:
ps aux | grep node
taskset -p <PID>
Üretim ortamında bir Node.js sunucusunun CPU kullanımını izlediğinizde, yoğun trafik altında bile tek bir çekirdeğin %100’e çıktığını, diğerlerinin ise boşta beklediğini görürsünüz. Bu durum hem kaynakların israfı hem de kullanıcı deneyimi açısından kabul edilemez.
Cluster Modülü Nedir ve Nasıl Çalışır?
Node.js’in yerleşik cluster modülü, ana sürecin (master process) birden fazla alt süreç (worker process) oluşturmasına olanak tanır. Her worker, aynı sunucu portunu dinler ve gelen istekleri işler. İşletim sistemi seviyesinde gerçekleşen bir fork() işlemi ile her worker kendi hafıza alanına sahip bağımsız bir Node.js örneği olarak çalışır.
Cluster modülünün çalışma mantığı şu şekildedir:
- Master süreç: Worker süreçleri oluşturur, izler ve yönetir, doğrudan HTTP isteği işlemez
- Worker süreçler: Gerçek uygulama kodunu çalıştırır, HTTP isteklerini karşılar
- IPC kanalı: Master ve worker süreçler arasındaki iletişimi sağlar
- Round-robin dağıtım: Linux ve macOS’ta varsayılan olarak gelen istekler sırayla worker’lara dağıtılır
- Shared port: Tüm worker’lar aynı portu dinler, işletim sistemi bağlantıları dağıtır
Basit Bir Cluster Uygulaması
Önce basit bir örnek ile başlayalım. Aşağıdaki kod, mevcut CPU çekirdek sayısı kadar worker oluşturur:
// cluster-basic.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master süreç başlatıldı. PID: ${process.pid}`);
console.log(`Toplam CPU çekirdeği: ${numCPUs}`);
// Her çekirdek için bir worker oluştur
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Bir worker çöktüğünde yenisini başlat
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} sonlandı. Yenisi başlatılıyor...`);
cluster.fork();
});
} else {
// Worker süreçler HTTP sunucusunu başlatır
http.createServer((req, res) => {
res.writeHead(200);
res.end(`Merhaba! Bu isteği Worker ${process.pid} işledi.n`);
}).listen(3000);
console.log(`Worker ${process.pid} başlatıldı, port 3000'i dinliyor.`);
}
Bu kodu çalıştırdığınızda terminalde şöyle bir çıktı görürsünüz:
node cluster-basic.js
# Çıktı:
# Master süreç başlatıldı. PID: 12345
# Toplam CPU çekirdeği: 8
# Worker 12346 başlatıldı, port 3000'i dinliyor.
# Worker 12347 başlatıldı, port 3000'i dinliyor.
# ...
Express ile Cluster Entegrasyonu
Gerçek dünyada genellikle Express veya Fastify gibi framework’ler kullanılır. Express uygulamasını cluster ile entegre etmek son derece kolaydır:
// app.js - Express uygulaması (worker tarafı)
const express = require('express');
const app = express();
app.use(express.json());
// CPU yoğun bir endpoint simülasyonu
app.get('/heavy', (req, res) => {
// Fibonacci hesaplaması - kasıtlı olarak CPU yoğun
const fib = (n) => n <= 1 ? n : fib(n - 1) + fib(n - 2);
const result = fib(40);
res.json({
result,
worker: process.pid,
timestamp: new Date().toISOString()
});
});
app.get('/health', (req, res) => {
res.json({
status: 'ok',
pid: process.pid,
memory: process.memoryUsage(),
uptime: process.uptime()
});
});
module.exports = app;
// server.js - Cluster yönetimi
const cluster = require('cluster');
const os = require('os');
const app = require('./app');
const PORT = process.env.PORT || 3000;
const numWorkers = process.env.WEB_CONCURRENCY || os.cpus().length;
if (cluster.isMaster) {
console.log(`[MASTER] PID: ${process.pid}`);
console.log(`[MASTER] ${numWorkers} worker başlatılıyor...`);
// Worker'ları oluştur
for (let i = 0; i < numWorkers; i++) {
const worker = cluster.fork();
// Worker mesajlarını dinle
worker.on('message', (msg) => {
if (msg.type === 'log') {
console.log(`[WORKER ${worker.id}] ${msg.data}`);
}
});
}
cluster.on('online', (worker) => {
console.log(`[MASTER] Worker ${worker.id} (PID: ${worker.process.pid}) aktif`);
});
cluster.on('exit', (worker, code, signal) => {
const reason = signal || code;
console.log(`[MASTER] Worker ${worker.id} kapandı (${reason}). Yenisi başlatılıyor...`);
// Sadece anormal kapanmalarda yeniden başlat
if (code !== 0 && !worker.exitedAfterDisconnect) {
cluster.fork();
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('[MASTER] SIGTERM alındı, worker'lar kapatılıyor...');
for (const id in cluster.workers) {
cluster.workers[id].send({ type: 'shutdown' });
cluster.workers[id].disconnect();
}
});
} else {
// Worker'da uygulamayı başlat
const server = app.listen(PORT, () => {
console.log(`[WORKER ${process.pid}] Port ${PORT}'de çalışıyor`);
});
// Master'dan gelen mesajları dinle
process.on('message', (msg) => {
if (msg.type === 'shutdown') {
console.log(`[WORKER ${process.pid}] Graceful shutdown başlatıldı`);
server.close(() => {
process.exit(0);
});
}
});
// İşlenmemiş hataları yakala
process.on('uncaughtException', (err) => {
console.error(`[WORKER ${process.pid}] Kritik hata:`, err);
process.exit(1);
});
}
Worker’lar Arası İletişim
Cluster modülünün güçlü özelliklerinden biri, master ve worker süreçleri arasında IPC (Inter-Process Communication) üzerinden mesaj gönderebilme yeteneğidir. Bu özellik özellikle önbellek senkronizasyonu, sayaç güncellemeleri ve durum yönetimi için kullanışlıdır:
// ipc-ornek.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const workers = {};
let requestCount = 0;
for (let i = 0; i < os.cpus().length; i++) {
const worker = cluster.fork();
workers[worker.id] = worker;
// Worker'dan gelen mesajları işle
worker.on('message', (msg) => {
if (msg.type === 'increment') {
requestCount += msg.value;
// Güncel sayacı tüm worker'lara broadcast et
for (const id in cluster.workers) {
cluster.workers[id].send({
type: 'counter_update',
count: requestCount
});
}
}
if (msg.type === 'get_stats') {
worker.send({
type: 'stats_response',
totalRequests: requestCount,
activeWorkers: Object.keys(cluster.workers).length,
uptime: process.uptime()
});
}
});
}
} else {
let localStats = { totalRequests: 0 };
const http = require('http');
http.createServer((req, res) => {
// Master'a istek sayacını artırmasını söyle
process.send({ type: 'increment', value: 1 });
if (req.url === '/stats') {
// Master'dan istatistik iste
process.send({ type: 'get_stats' });
process.once('message', (msg) => {
if (msg.type === 'stats_response') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(msg));
}
});
} else {
res.writeHead(200);
res.end(`Worker ${process.pid} - Toplam istek: ${localStats.totalRequests}`);
}
}).listen(3000);
// Master'dan gelen güncellemeleri al
process.on('message', (msg) => {
if (msg.type === 'counter_update') {
localStats.totalRequests = msg.count;
}
});
}
Sıfır Kesintili Güncelleme (Zero Downtime Reload)
Üretim ortamında en kritik ihtiyaçlardan biri, uygulamayı güncellerken mevcut bağlantıları kesmemektir. Cluster modülü bu konuda da çözüm sunar:
// zero-downtime-reload.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numWorkers = os.cpus().length;
// Tüm worker'ları başlat
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
// SIGUSR2 sinyali ile yeniden yükleme
process.on('SIGUSR2', () => {
console.log('[MASTER] Sıfır kesintili yeniden yükleme başlatıldı...');
const workerIds = Object.keys(cluster.workers);
let index = 0;
const reloadNext = () => {
if (index >= workerIds.length) {
console.log('[MASTER] Tüm worker'lar yeniden yüklendi!');
return;
}
const workerId = workerIds[index++];
const worker = cluster.workers[workerId];
if (!worker) {
reloadNext();
return;
}
// Yeni worker'ı başlat, eskisini kapat
const newWorker = cluster.fork();
newWorker.on('listening', () => {
console.log(`[MASTER] Yeni worker ${newWorker.process.pid} hazır, eski kapatılıyor...`);
worker.send({ type: 'shutdown' });
worker.disconnect();
setTimeout(reloadNext, 500); // Biraz bekle
});
};
reloadNext();
});
console.log(`[MASTER] PID: ${process.pid}`);
console.log('Yeniden yüklemek için: kill -SIGUSR2 ' + process.pid);
} else {
require('./app').listen(3000);
process.on('message', (msg) => {
if (msg.type === 'shutdown') {
process.exit(0);
}
});
}
Üretimde bu komutu kullanarak sıfır kesinti ile yeniden yükleyebilirsiniz:
# Master PID'ini bul
ps aux | grep "node zero-downtime-reload.js" | grep -v grep
# Sıfır kesintili yeniden yükle
kill -SIGUSR2 <MASTER_PID>
# Veya systemd kullanıyorsanız
sudo systemctl reload myapp
# Yük testi sırasında yeniden yüklemeyi test et
npm install -g autocannon
autocannon -c 100 -d 30 http://localhost:3000 &
kill -SIGUSR2 <MASTER_PID>
PM2 ile Cluster Modu Karşılaştırması
Cluster modülünü kendiniz yönetmek yerine PM2 gibi bir process manager kullanmak da yaygın bir tercihtir. Her iki yaklaşımın avantajları farklıdır:
PM2 cluster modunu kullanmanın avantajları şunlardır:
- Hazır izleme paneli:
pm2 monitile anlık CPU ve RAM kullanımı - Otomatik yeniden başlatma: Bellek sınırı aşıldığında otomatik restart
- Log yönetimi: Merkezi log toplama ve rotasyon
- Ecosystem dosyası: Tek bir konfigurasyon ile ortam yönetimi
- Sıfır kesinti güncellemesi:
pm2 reloadkomutu ile otomatik
Öte yandan kendi cluster kodunuzu yazmanın avantajları da mevcuttur:
- Tam kontrol: Yük dengeleme stratejisini kendiniz belirleyebilirsiniz
- Özel IPC mesajları: Worker’lar arası karmaşık iletişim senaryolarını yönetebilirsiniz
- Bağımlılık yok: Ekstra bir araç kurmadan sadece Node.js yeterlidir
- Özel sağlık kontrolleri: Uygulamaya özel restart mantığı yazabilirsiniz
# PM2 ile cluster modunda uygulama başlatma
npm install -g pm2
# Mevcut CPU sayısı kadar instance başlat
pm2 start server.js -i max --name "myapp"
# Belirli sayıda instance
pm2 start server.js -i 4 --name "myapp"
# Sıfır kesinti ile yeniden başlat
pm2 reload myapp
# Durum izle
pm2 status
pm2 monit
pm2 logs myapp
Performans Testi ve Sonuçları
Cluster modülünün gerçek etkisini görmek için basit bir yük testi yapalım:
# Apache Benchmark veya autocannon ile test
# Önce tek instance test:
node app.js &
ab -n 10000 -c 100 http://localhost:3000/heavy
# Sonra cluster ile test:
node server.js &
ab -n 10000 -c 100 http://localhost:3000/heavy
# autocannon ile daha detaylı test
npm install -g autocannon
autocannon -c 200 -d 20 -p 10 http://localhost:3000/health
# htop ile CPU kullanımını izle (başka terminalde)
htop -p $(pgrep -d',' -f "node server")
Gerçek dünya testlerinde 8 çekirdekli bir sunucuda şu sonuçlar tipik olarak gözlemlenir:
- Tek instance: saniyede yaklaşık 800-1200 istek
- 8 worker cluster: saniyede yaklaşık 5000-7000 istek
- CPU kullanımı: tek instance’ta %12, cluster’da %85-90 toplam
Bu rakamlar uygulamanın yapısına göre değişse de genel trend her zaman cluster lehine olmaktadır.
Üretim Ortamı İpuçları
Cluster modülünü üretimde kullanırken dikkat etmeniz gereken bazı önemli noktalar bulunur:
- Worker sayısını çekirdek sayısına eşit tutun: Daha fazla worker daha iyi değildir, context switching maliyeti artar
- Bellek sızıntısını izleyin: Her worker bağımsız bir süreç olduğundan bellek kullanımını ayrı ayrı takip edin
- Graceful shutdown: SIGTERM sinyalini yakalayıp mevcut bağlantıları tamamlayarak kapanın
- Health check endpoint: Her worker’ın
/healthendpoint’i döndürmesi izleme araçları için kritiktir - Paylaşımlı durum için Redis kullanın: Session, önbellek gibi paylaşılan veriler için hiçbir zaman in-memory çözüm kullanmayın
- Ortam değişkenlerini doğru yönetin: Worker’lar fork ile oluşturulduğundan master’ın ortam değişkenlerini miras alır
- Log formatını standartlaştırın: Worker PID’ini log satırlarına eklemek hata ayıklamayı kolaylaştırır
# Systemd servis dosyası örneği
sudo nano /etc/systemd/system/nodeapp.service
[Unit]
Description=Node.js Cluster Uygulaması
After=network.target
[Service]
Type=simple
User=nodeuser
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=nodeapp
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=WEB_CONCURRENCY=8
KillMode=mixed
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
Sonuç
Node.js Cluster modülü, çok çekirdekli sunucuların potansiyelini tam anlamıyla kullanmak için vazgeçilmez bir araçtır. Özellikle doğrudan Node.js uygulamalarını yönettiğiniz ve PM2 gibi araçlara bağımlı olmak istemediğiniz senaryolarda cluster modülünü anlamak ve doğru kullanmak size büyük avantaj sağlar.
Özetle şu noktaları aklınızda tutun: Tek başına çalışan bir Node.js uygulaması sunucunuzun gücünü boşa harcar. Cluster modülü ile bu gücü tam kullanabilirsiniz. Worker’lar birbirinden bağımsız süreçlerdir, bu yüzden paylaşımlı durum için Redis gibi dış çözümler şarttır. Sıfır kesintili deployment, production ortamında kullanıcı deneyimini doğrudan etkiler ve cluster bunu mümkün kılar. PM2 kullanmak çoğu senaryoda işi kolaylaştırır ama altta ne döndüğünü anlamak sorun giderme sırasında hayat kurtarır.
Node.js uygulamalarınızı cluster ile dönüştürmek aslında düşündüğünüzden çok daha az değişiklik gerektirir. Mevcut Express uygulamanızın üzerine birkaç satırlık wrapper kodu ekleyerek performansı birkaç katına çıkarabilirsiniz. Bunu okuduktan sonra hala sunucunuzun 31 çekirdeğini boşa harcıyor olmak çok yazık olur.
