Node.js Uygulamalarında Graceful Shutdown ve Sinyal Yönetimi
Production ortamında Node.js uygulaması çalıştırıyorsun ve bir deployment geldi. Uygulamayı yeniden başlatman gerekiyor ama tam o sırada 50 kullanıcı aktif istek yapıyor. Ne yaparsın? Uygulamayı pat diye öldürürsen bu kullanıcıların istekleri yarım kalır, veritabanı bağlantıları kirli kapanır, dosya yazma işlemleri bozulur. İşte tam bu noktada graceful shutdown devreye giriyor. Bu yazıda Node.js uygulamalarında sinyal yönetimini, temiz kapanışı ve production ortamında karşılaşacağın gerçek senaryoları ele alacağız.
Sinyal Nedir ve Linux’ta Nasıl Çalışır
Linux’ta bir prosese mesaj göndermek için sinyaller kullanılır. Kernel veya başka bir proses, çalışan bir prosese çeşitli sinyaller iletebilir. Node.js uygulamaları da bu sinyalleri dinleyip uygun şekilde tepki verebilir.
Production’da en sık karşılaşacağın sinyaller şunlardır:
- SIGTERM: Nazik sonlandırma isteği. Systemd, Kubernetes ve Docker’ın varsayılan olarak gönderdiği sinyal budur.
- SIGINT: Terminalden Ctrl+C bastığında gönderilir.
- SIGHUP: Terminal bağlantısı koptuğunda veya yapılandırmayı yeniden yüklemek için kullanılır.
- SIGKILL: Anında öldürme. Bu sinyali uygulamanız yakalayamaz, kernel direkt devreye girer.
- SIGUSR1: Kullanıcı tanımlı sinyal. Node.js bunu debugger başlatmak için kullanır.
- SIGUSR2: İkinci kullanıcı tanımlı sinyal. Nodemon bu sinyali yeniden başlatma için kullanır.
Bir sinyalin prosese nasıl gönderildiğini görmek için:
# Proses ID'sini bul
ps aux | grep node
# SIGTERM gönder
kill -15 <PID>
# ya da
kill -SIGTERM <PID>
# SIGINT gönder
kill -2 <PID>
# Sinyalleri listele
kill -l
Kritik nokta şu: SIGTERM aldığında uygulamanın “tamam, kapanıyorum ama önce işlerimi tamamlayayım” demesi gerekir. SIGKILL aldığında ise zaten yapabileceğin hiçbir şey yoktur.
Node.js’te Sinyal Dinleme Temelleri
Node.js’te process objesi üzerinden sinyalleri dinleyebilirsin. En basit haliyle:
// signals-basic.js
process.on('SIGTERM', () => {
console.log('SIGTERM alindi, uygulama kapaniyor...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT alindi (Ctrl+C), uygulama kapaniyor...');
process.exit(0);
});
// Uygulamanin calismaya devam etmesi icin bir sey yapmali
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Merhaba Dunyan');
});
server.listen(3000, () => {
console.log('Sunucu 3000 portunda calisiyor, PID:', process.pid);
});
Bu temel yapı çalışır ama production için yeterli değildir. Gerçek dünyada aktif HTTP bağlantıları, veritabanı transaction’ları ve kuyruk işlemleri vardır. Bunların hepsini düzgün kapatman gerekir.
Gerçek Bir HTTP Sunucusunda Graceful Shutdown
Production uygulamalarında graceful shutdown’ın nasıl uygulanacağını gösteren kapsamlı bir örnek:
// graceful-server.js
const http = require('http');
const server = http.createServer((req, res) => {
// Uzun suren bir islem simule ediyoruz
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', pid: process.pid }));
}, 2000);
});
let isShuttingDown = false;
// Graceful shutdown fonksiyonu
async function gracefulShutdown(signal) {
if (isShuttingDown) {
console.log('Zaten kapanma sureci baslamis, bekleniyor...');
return;
}
isShuttingDown = true;
console.log(`${signal} sinyali alindi. Graceful shutdown basliyor...`);
// Yeni baglantilari reddet
server.close(async (err) => {
if (err) {
console.error('Sunucu kapatilirken hata:', err);
process.exit(1);
}
console.log('HTTP sunucusu kapatildi.');
try {
// Veritabani baglantisini kapat
await closeDatabase();
console.log('Veritabani baglantisi kapatildi.');
// Redis baglantisini kapat
await closeRedis();
console.log('Redis baglantisi kapatildi.');
console.log('Temiz kapanis tamamlandi.');
process.exit(0);
} catch (cleanupError) {
console.error('Temizlik sirasinda hata:', cleanupError);
process.exit(1);
}
});
// Zorla kapanma icin timeout (30 saniye)
setTimeout(() => {
console.error('Graceful shutdown zaman asimina ugradi, zorla kapatiliyor.');
process.exit(1);
}, 30000);
}
// Sinyal dinleyicileri
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
server.listen(3000, () => {
console.log(`Sunucu baslatildi. PID: ${process.pid}`);
});
Burada dikkat etmen gereken birkaç şey var. isShuttingDown flag’i, aynı anda birden fazla sinyal geldiğinde çift kapanma işlemini önler. server.close() yeni bağlantı kabul etmeyi durdurur ama mevcut bağlantılar tamamlanana kadar bekler. 30 saniyelik timeout ise sonsuz beklemeyi engeller.
Express.js ile Graceful Shutdown
Gerçek dünya projelerinin büyük çoğunluğu Express kullanıyor. İşte production’a hazır bir Express yapılandırması:
// express-graceful.js
const express = require('express');
const app = express();
app.use(express.json());
// Middleware: Kapanma sirasinda yeni istekleri reddet
app.use((req, res, next) => {
if (isShuttingDown) {
res.set('Connection', 'close');
return res.status(503).json({
error: 'Sunucu bakimda, lutfen biraz sonra tekrar deneyin.'
});
}
next();
});
app.get('/health', (req, res) => {
res.json({
status: isShuttingDown ? 'shutting_down' : 'healthy',
pid: process.pid,
uptime: process.uptime()
});
});
app.get('/api/data', async (req, res) => {
try {
const data = await fetchDataFromDatabase();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
let isShuttingDown = false;
const server = app.listen(3000);
async function shutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`[${new Date().toISOString()}] ${signal} alindi`);
const shutdownTimeout = setTimeout(() => {
console.error('Shutdown timeout! Zorla cikiliyor.');
process.exit(1);
}, 30000);
shutdownTimeout.unref(); // Bu timeout Node'un kapanmasini engellemesin
server.close(async () => {
try {
await Promise.all([
db.end(), // PostgreSQL pool
redisClient.quit(), // Redis
messageQueue.close() // RabbitMQ veya benzeri
]);
clearTimeout(shutdownTimeout);
process.exit(0);
} catch (err) {
console.error('Temizlik hatasi:', err);
process.exit(1);
}
});
}
['SIGTERM', 'SIGINT', 'SIGHUP'].forEach(signal => {
process.on(signal, () => shutdown(signal));
});
Keep-Alive Bağlantıları ile Başa Çıkmak
HTTP/1.1 keep-alive bağlantıları graceful shutdown’ı zorlaştırır. server.close() çağrıldığında aktif keep-alive bağlantıları hemen kapanmaz ve bu süreç uzayabilir. Bunu çözmek için bağlantıları takip etmek gerekir:
// connection-tracker.js
const http = require('http');
const connections = new Map();
let connectionId = 0;
const server = http.createServer((req, res) => {
res.end('OKn');
});
server.on('connection', (socket) => {
const id = connectionId++;
connections.set(id, socket);
socket.on('close', () => {
connections.delete(id);
});
});
function destroyConnections() {
console.log(`${connections.size} baglanti kapatiliyor...`);
for (const [id, socket] of connections) {
socket.destroy();
connections.delete(id);
}
}
async function gracefulShutdown() {
console.log('Graceful shutdown basliyor...');
// Once server.close() ile yeni baglantilari engelle
server.close(() => {
console.log('Sunucu kapatildi.');
process.exit(0);
});
// Keep-alive baglantilari icin 10 saniye bekle, sonra zorla kes
setTimeout(() => {
console.log('Keep-alive baglantilari zorla kesiliyor...');
destroyConnections();
}, 10000);
}
process.on('SIGTERM', gracefulShutdown);
server.listen(3000);
Kubernetes ve Docker Ortamında Sinyal Yönetimi
Kubernetes ve Docker’da bir container durdurulduğunda şu süreç yaşanır: önce SIGTERM gönderilir, ardından terminationGracePeriodSeconds (varsayılan 30 saniye) beklenir, süre dolunca SIGKILL gelir. Bunu doğru yönetmek çok önemlidir.
Docker’da PID 1 problemi yaygın bir tuzaktır. node app.js ile başlatılan uygulama PID 1 olur ve bazı Linux sinyalleri PID 1 tarafından farklı işlenir. Bunun için iki çözüm vardır:
# Dockerfile - doğru yöntem 1: exec form kullan
CMD ["node", "app.js"]
# Dockerfile - doğru yöntem 2: tini kullan
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "app.js"]
# docker-compose ile
services:
app:
image: myapp:latest
init: true # Docker'in dahili init'ini kullan
stop_grace_period: 30s
Kubernetes için deployment yapılandırması:
# kubernetes deployment ornegi (yaml formatinda gosteriyoruz)
# terminationGracePeriodSeconds: 60
# lifecycle preStop hook ekle:
# exec:
# command: ["/bin/sh", "-c", "sleep 5"]
# Bu sayede Kubernetes endpoint'i guncelleme sansı bulur
# Konteyner durdurma sirasini test etmek icin:
kubectl delete pod <pod-name> --grace-period=0 --force
# NOT: Bu production'da kullanma, sadece test icin
# Grace period ile normal kapanma:
kubectl delete pod <pod-name>
# Deployment guncelleme sirasinda graceful shutdown:
kubectl rollout restart deployment/myapp
Hata Yönetimi ve Uncaught Exception’lar
Graceful shutdown sadece sinyal yönetimi değildir. Yakalanmamış hatalar da uygulamayı düzgünce kapatmalıdır:
// error-handling.js
// Yakalanmamis exception
process.on('uncaughtException', (error) => {
console.error('Yakalanmamis exception:', error);
console.error('Stack:', error.stack);
// Burada loglama yap, alert gonder
// Ama dikkat: Bu noktada uygulama tutarsiz durumda olabilir
// Temiz kapanma yapmaya calis ama garanti yok
gracefulShutdown('uncaughtException').finally(() => {
process.exit(1);
});
});
// Yakalanmamis Promise reddi
process.on('unhandledRejection', (reason, promise) => {
console.error('Yakalanmamis Promise reddi:', promise);
console.error('Sebep:', reason);
// Node.js 15+ surumlerinde bu otomatik olarak uygulamayi cokertir
// Onceki surumlerde sadece uyari verirdi
// Explicit olarak handle etmek en iyisi:
gracefulShutdown('unhandledRejection').finally(() => {
process.exit(1);
});
});
// Memory leak uyarisi
process.on('warning', (warning) => {
console.warn('Node.js uyarisi:', warning.name, warning.message);
if (warning.name === 'MaxListenersExceededWarning') {
console.error('Olasilikla event listener leak var!');
}
});
PM2 ile Graceful Shutdown
PM2 ile çalışıyorsan, PM2 kendi sinyal mekanizmasını kullanır:
# pm2 ecosystem.config.js ornegi olustur
cat > ecosystem.config.js << 'EOF'
module.exports = {
apps: [{
name: 'myapp',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
kill_timeout: 5000, // SIGKILL gondermeden once bekle (ms)
wait_ready: true, // process.send('ready') bekle
listen_timeout: 10000, // ready sinyali icin max bekleme
shutdown_with_message: false
}]
};
EOF
# Uygulamayi baslat
pm2 start ecosystem.config.js
# Graceful reload (sifir downtime)
pm2 reload myapp
# Graceful stop
pm2 stop myapp
# Proses durumunu kontrol et
pm2 list
pm2 logs myapp --lines 50
PM2 cluster modu ile çalışırken uygulamanın hazır olduğunu bildirmesi önemlidir:
// pm2-ready.js
const server = app.listen(3000, () => {
console.log('Sunucu hazir');
// PM2'ye hazir oldugunu bildir
if (process.send) {
process.send('ready');
}
});
// PM2'nin gonderdigi shutdown mesajini dinle
process.on('message', (msg) => {
if (msg === 'shutdown') {
console.log('PM2 shutdown mesaji alindi');
gracefulShutdown('PM2_SHUTDOWN');
}
});
Gerçek Dünya Senaryosu: Veritabanı Transaction’ları
En kritik senaryo, shutdown sırasında aktif veritabanı transaction’ının bulunmasıdır:
// db-transaction-shutdown.js
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const activeTransactions = new Set();
async function withTransaction(callback) {
const client = await pool.connect();
const transactionId = Date.now() + Math.random();
try {
await client.query('BEGIN');
activeTransactions.add(transactionId);
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
activeTransactions.delete(transactionId);
client.release();
}
}
async function waitForActiveTransactions(timeoutMs = 10000) {
const start = Date.now();
while (activeTransactions.size > 0) {
if (Date.now() - start > timeoutMs) {
console.warn(`${activeTransactions.size} transaction hala aktif, devam ediliyor...`);
break;
}
console.log(`${activeTransactions.size} aktif transaction bekleniyor...`);
await new Promise(resolve => setTimeout(resolve, 500));
}
}
async function gracefulShutdown() {
console.log('Shutdown basliyor...');
isShuttingDown = true;
// Aktif transaction'larin bitmesini bekle
await waitForActiveTransactions();
// Veritabani pool'unu kapat
await pool.end();
console.log('Veritabani pool kapatildi.');
process.exit(0);
}
Monitoring ve Logging
Graceful shutdown sürecini izlemek production’da hayat kurtarır:
# Systemd ile shutdown loglarini izle
journalctl -u myapp -f
# Son 100 satir log
journalctl -u myapp -n 100
# Belirli bir zaman araliginda
journalctl -u myapp --since "2024-01-15 10:00:00" --until "2024-01-15 11:00:00"
# Prosesin ne kadar surdugunu gormek icin
systemctl status myapp
# Systemd service dosyasinda graceful shutdown ayarlari
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Node.js Application
After=network.target
[Service]
Type=simple
User=nodejs
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node app.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
KillMode=mixed
KillSignal=SIGTERM
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
TimeoutStopSec=30 ile systemd’ye 30 saniye beklemesini söylüyoruz. KillMode=mixed ise SIGTERM’den sonra SIGKILL gönderir.
Sonuç
Graceful shutdown, “isteğe bağlı güzel bir özellik” değil, production uygulamalarının olmazsa olmazıdır. Özellikle Kubernetes, Docker Swarm veya PM2 cluster modu kullandığında, deployment sırasında yaşanan her düzgün olmayan kapanış kullanıcıya hata olarak yansır.
Özetlemek gerekirse dikkat etmen gereken başlıca noktalar şunlardır:
- Her zaman SIGTERM dinle: Systemd, Kubernetes ve Docker bu sinyali gönderir.
- Timeout koy: Graceful shutdown sonsuza kadar beklememeli, 30 saniye genellikle yeterlidir.
- Keep-alive bağlantılarını takip et:
server.close()tek başına yetmeyebilir. - Uncaught exception’ları handle et: Uygulama çökse bile temiz kapanmaya çalış.
- PID 1 problemine dikkat et: Docker’da
tinikullan veya exec form ile başlat. - Aktif işlemleri bekle: Transaction’lar, dosya yazmaları ve mesaj işlemleri tamamlanmalı.
Uyguladıktan sonra mutlaka test et. kill -SIGTERM ile uygulamana sinyal gönder, logları izle ve davranışın beklediğin gibi olduğunu doğrula. Production’da sürpriz yaşamamanın en iyi yolu, bu senaryoları önceden test etmektir.
