Node.js Bellek Sızıntısı Tespiti ve Giderme
Üretim ortamında çalışan bir Node.js uygulamasının haftalarca sorunsuz çalıştıktan sonra yavaşlamaya başladığını, bellek kullanımının giderek arttığını ve sonunda process’in çöktüğünü görmek gerçekten sinir bozucu bir deneyimdir. Bellek sızıntısı (memory leak) sorunları, Node.js ekosisteminde en sık karşılaşılan ve en zor teşhis edilen problemlerin başında gelir. Bu yazıda, gerçek dünya senaryoları üzerinden bellek sızıntısını nasıl tespit edeceğinizi ve nasıl gidereceğinizi adım adım ele alacağız.
Bellek Sızıntısı Nedir ve Neden Olur?
Node.js, V8 JavaScript motoru üzerinde çalışır ve çöp toplama (garbage collection) mekanizmasına sahiptir. Teoride, kullanılmayan nesneler otomatik olarak temizlenir. Ancak pratikte, bir nesneye olan referans yanlışlıkla korunduğunda, çöp toplayıcı o nesneyi temizleyemez. Zamanla bu “unutulmuş” nesneler birikir ve heap belleği şişmeye başlar.
Bellek sızıntısının en yaygın kaynakları şunlardır:
- Global değişkenler: Yanlışlıkla global scope’a atanan büyük nesneler
- Kapatılmayan event listener’lar:
on()ile eklenen amaoff()ile kaldırılmayan dinleyiciler - Kapatılmayan timer’lar:
setInterval()ile başlatılıp hiç temizlenmeyen zamanlayıcılar - Closure tuzakları: İç fonksiyonların dış scope’taki büyük nesnelere referans tutması
- Cache yönetimi hataları: Sınırsız büyüyen in-memory cache yapıları
- Promise zincirleri: Çözülmemiş promise’ler ve callback birikimi
Geliştirme Ortamının Hazırlanması
Bellek sızıntısını tespit etmek için öncelikle gerekli araçları kurmanız gerekir. Node.js’in kendi bünyesindeki araçların yanı sıra harici paketlerden de yararlanacağız.
# Node.js'in güncel LTS sürümünü kurun
nvm install --lts
nvm use --lts
# Bellek analizi için gerekli paketler
npm install -g clinic
npm install -g 0x
npm install heapdump --save-dev
npm install memwatch-next --save-dev
# Üretim ortamı için hafif bir seçenek
npm install node-memwatch --save
clinic paketi, Nearform tarafından geliştirilen ve Node.js performans sorunlarını görselleştiren mükemmel bir araç setidir. heapdump ise heap snapshot almamızı sağlar.
İlk Teşhis: Basit İzleme ile Başlayın
Büyük araçlara geçmeden önce, uygulamanızın bellek kullanımını basit yollarla izleyebilirsiniz. Aşağıdaki kod, bellek kullanımını düzenli aralıklarla loglar:
// memory-monitor.js
const formatMemoryUsage = (data) => {
return `${Math.round(data / 1024 / 1024 * 100) / 100} MB`;
};
const logMemoryUsage = () => {
const memData = process.memoryUsage();
console.log({
timestamp: new Date().toISOString(),
rss: formatMemoryUsage(memData.rss), // Toplam process belleği
heapTotal: formatMemoryUsage(memData.heapTotal), // Ayrılan heap
heapUsed: formatMemoryUsage(memData.heapUsed), // Kullanılan heap
external: formatMemoryUsage(memData.external), // C++ nesneleri
arrayBuffers: formatMemoryUsage(memData.arrayBuffers)
});
};
// Her 5 saniyede bir bellek durumunu logla
setInterval(logMemoryUsage, 5000);
// Uygulamanızı buraya import edin
require('./app');
Bu basit scripti çalıştırdığınızda, heapUsed değerinin sürekli artıp artmadığını gözlemleyin. Eğer belirli bir istek yükü altında bu değer durmaksızın büyüyorsa, bir sızıntı söz konusu demektir.
Gerçek Dünya Senaryosu: Event Listener Sızıntısı
Bir Express.js uygulamasında karşılaşılan yaygın bir senaryoyu inceleyelim. Aşağıdaki kod, her istek geldiğinde yeni bir event listener ekliyor ama hiç kaldırmıyor:
// YANLIS - bellek sizintisina yol acar
const EventEmitter = require('events');
const emitter = new EventEmitter();
app.get('/api/data', (req, res) => {
// Her istekte yeni bir listener ekleniyor!
emitter.on('dataReady', (data) => {
res.json(data);
});
fetchDataFromDB().then(data => {
emitter.emit('dataReady', data);
});
});
// DOGRU - listener'i temizle
app.get('/api/data', (req, res) => {
const handler = (data) => {
res.json(data);
emitter.off('dataReady', handler); // Temizleme!
};
emitter.once('dataReady', handler); // once() kullan
fetchDataFromDB().then(data => {
emitter.emit('dataReady', data);
});
});
Node.js, bir emitter’a 10’dan fazla listener eklendiğinde sizi uyarır. Bu uyarıyı MaxListenersExceededWarning olarak görürseniz, hemen harekete geçin. emitter.setMaxListeners(0) ile bu uyarıyı susturmak kesinlikle yanlış bir yaklaşımdır.
Heap Snapshot Alma ve Analiz Etme
Belleği izlemeye başladıktan sonra, heap snapshot almak en güçlü teşhis yöntemlerinden biridir. İki farklı zaman diliminde alınan snapshot’ları karşılaştırarak hangi nesnelerin biriktiğini görebilirsiniz.
# Heap snapshot için V8 inspector'ı etkinleştirin
node --inspect app.js
# Veya belirli bir sinyal geldiğinde snapshot alın
node --heapsnapshot-signal=SIGUSR2 app.js
# Çalışan process'e sinyal gönderin
kill -USR2 <PID>
Programatik olarak snapshot almak istiyorsanız:
// heapdump kullanarak snapshot alma
const heapdump = require('heapdump');
const path = require('path');
// HTTP endpoint üzerinden tetikleme (sadece geliştirme ortamında!)
if (process.env.NODE_ENV !== 'production') {
app.get('/debug/heapdump', (req, res) => {
const filename = path.join(
'/tmp',
`heapdump-${Date.now()}.heapsnapshot`
);
heapdump.writeSnapshot(filename, (err, filename) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.json({
message: 'Snapshot alindi',
file: filename,
hint: 'Chrome DevTools > Memory sekmesinde acin'
});
});
});
}
Alınan .heapsnapshot dosyasını Chrome DevTools’un Memory sekmesine yükleyerek analiz edebilirsiniz. Comparison (karşılaştırma) görünümünde iki snapshot arasındaki farkı incelediğinizde, hangi nesne türlerinin sayısının arttığını kolayca görebilirsiniz.
Clinic.js ile Görsel Analiz
clinic aracı, bellek sızıntısını görselleştirmek için son derece etkili bir yöntem sunar:
# Clinic Doctor ile genel sağlık taraması
clinic doctor -- node app.js
# Ayrı bir terminal'de yük testi yapın
npx autocannon -c 100 -d 30 http://localhost:3000/api/endpoint
# Clinic Heapprofiler ile heap analizi
clinic heapprofiler -- node app.js
clinic doctor çalıştırıldıktan sonra bir HTML raporu oluşturur. Bu raporda bellek kullanımı, CPU kullanımı ve event loop gecikmesi grafiklerini görebilirsiniz. Eğer bellek grafiği düzenli aralıklarla yükselmeden önce kısa düşüşler gösteriyorsa (testere dişi deseni), bu garbage collector’ın çalıştığını ama tam temizleyemediğini gösterir. Grafiğin genel eğimi yukarı doğruysa, sızıntı kesinleşmiş demektir.
Gerçek Dünya Senaryosu: Kontrolsüz Cache Büyümesi
Birçok Node.js uygulamasında, sorgu sonuçlarını cache’lemek için basit bir Map veya obje kullanılır. Ancak bu cache’in büyümesi kontrol altına alınmazsa, ciddi bir bellek sorununa yol açar:
// YANLIS - sinirsiz buyuyen cache
const cache = new Map();
app.get('/user/:id', async (req, res) => {
const { id } = req.params;
if (cache.has(id)) {
return res.json(cache.get(id));
}
const user = await User.findById(id);
cache.set(id, user); // Cache hic temizlenmiyor!
res.json(user);
});
// DOGRU - LRU cache kullan
const LRU = require('lru-cache');
const cache = new LRU({
max: 500, // Maksimum 500 item
maxSize: 50 * 1024 * 1024, // 50MB maksimum boyut
sizeCalculation: (value) => {
return Buffer.byteLength(JSON.stringify(value));
},
ttl: 1000 * 60 * 10 // 10 dakika TTL
});
app.get('/user/:id', async (req, res) => {
const { id } = req.params;
const cached = cache.get(id);
if (cached) {
return res.json(cached);
}
const user = await User.findById(id);
cache.set(id, user);
res.json(user);
});
lru-cache paketi, bu sorunu çözmek için ideal bir araçtır. LRU (Least Recently Used) algoritması, en az kullanılan öğeleri otomatik olarak silerek cache’in boyutunu kontrol altında tutar.
memwatch ile Otomatik Sızıntı Tespiti
memwatch-next veya @airbnb/node-memwatch paketi, heap boyutundaki anormal artışları otomatik olarak algılar:
// memwatch entegrasyonu
const memwatch = require('@airbnb/node-memwatch');
memwatch.on('leak', (info) => {
console.error('Bellek sizintisi tespit edildi!', {
start: info.start,
end: info.end,
growth: info.growth,
reason: info.reason
});
// Slack veya PagerDuty'e bildirim gonder
alertingService.notify({
severity: 'critical',
message: `Node.js bellek sizintisi: ${info.growth} MB artis`,
details: info
});
});
memwatch.on('stats', (stats) => {
console.log('GC istatistikleri:', {
num_full_gc: stats.num_full_gc,
num_inc_gc: stats.num_inc_gc,
heap_compactions: stats.heap_compactions,
estimated_base: stats.estimated_base,
current_base: stats.current_base,
min: stats.min,
max: stats.max
});
});
Bu kütüphane, ardışık birkaç garbage collection döngüsünde heap boyutu büyümeye devam ederse leak event’ini tetikler. Üretim ortamında alarm mekanizmasıyla entegre edildiğinde, sorunlar kritik hale gelmeden müdahale edebilirsiniz.
V8 Heap İstatistiklerini Prometheus ile İzleme
Üretim ortamında sürekli izleme için Prometheus metriklerini kullanmak en sağlıklı yaklaşımdır:
// prometheus-metrics.js
const client = require('prom-client');
const v8 = require('v8');
const heapUsedGauge = new client.Gauge({
name: 'nodejs_heap_used_bytes',
help: 'Kullanilan heap miktari (bytes)'
});
const heapTotalGauge = new client.Gauge({
name: 'nodejs_heap_total_bytes',
help: 'Toplam heap miktari (bytes)'
});
const heapSpaceGauge = new client.Gauge({
name: 'nodejs_heap_space_bytes',
help: 'V8 heap uzayi bilgileri',
labelNames: ['space']
});
const collectHeapMetrics = () => {
const memUsage = process.memoryUsage();
heapUsedGauge.set(memUsage.heapUsed);
heapTotalGauge.set(memUsage.heapTotal);
// V8 heap uzaylarini ayri ayri izle
const heapSpaces = v8.getHeapSpaceStatistics();
heapSpaces.forEach(space => {
heapSpaceGauge
.labels(space.space_name)
.set(space.space_used_size);
});
};
// Her 15 saniyede bir metrikleri guncelle
setInterval(collectHeapMetrics, 15000);
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
Grafana’da bu metrikleri görselleştirip alert kuralları tanımladığınızda, heapUsed değeri belirli bir eşiği aştığında otomatik bildirim alabilirsiniz.
Closure Tuzağını Anlamak ve Önlemek
Closure’lar JavaScript’in güçlü özelliklerinden biridir, ancak dikkatli kullanılmazsa ciddi sızıntılara yol açar:
// YANLIS - closure büyük veriyi tutuklu tutuyor
function processLargeDataset(data) {
const hugeArray = new Array(1000000).fill('veri'); // 1M item
return function() {
// hugeArray burada hic kullanilmiyor
// ama closure onu hafizada tutuyor!
return data.length;
};
}
// DOGRU - gereksiz referansi kes
function processLargeDataset(data) {
const hugeArray = new Array(1000000).fill('veri');
// Ihtiyacin olan degeri cikart, buyuk array'i birak
const result = hugeArray.length;
return function() {
return data.length + result; // hugeArray referansi yok
};
}
// YANLIS - timer closure'i
class DataProcessor {
constructor() {
this.data = new Array(100000).fill('buyuk-veri');
// Bu timer, DataProcessor instance'ini sonsuza kadar yasar tutar
this.timer = setInterval(() => {
console.log('islem devam ediyor', this.data.length);
}, 1000);
}
}
// DOGRU - temizleme metodu ekle
class DataProcessor {
constructor() {
this.data = new Array(100000).fill('buyuk-veri');
this.timer = setInterval(() => {
console.log('islem devam ediyor', this.data.length);
}, 1000);
}
destroy() {
clearInterval(this.timer);
this.data = null; // GC'ye birak
}
}
Bellek Sızıntısı Düzeltme Kontrol Listesi
Bir sızıntı tespit ettiğinizde sistematik bir yaklaşım benimsemeniz gerekir. Kodunuzu incelerken şu noktalara özellikle dikkat edin:
- Global değişken denetimi:
varyerinelet/constkullanın, yanlışlıkla global atama yapmaktan kaçının. Strict mode ('use strict') bu tür hataları yakalamaya yardımcı olur - Event listener dengesi: Her
on()için karşılık gelenoff()veyaremoveListener()olduğundan emin olun. Tek seferlik işlemler içinonce()kullanın - Timer temizliği:
setIntervalvesetTimeoutdönüş değerlerini saklayın ve kullanım sonundaclearInterval/clearTimeoutçağırın - Stream kapatma: Readable ve Writable stream’ler tamamlandığında
destroy()çağırın - Database bağlantı havuzu: Bağlantı havuzunun boyutunu sınırlandırın ve bağlantı sızıntılarını izleyin
- WeakMap ve WeakSet kullanımı: Nesneleri metadata ile ilişkilendirmek gerektiğinde, güçlü referans yerine
WeakMapkullanın
# Node.js'i garbage collection loglama ile calistirin
node --trace-gc app.js 2>&1 | grep -E "(Scavenge|Mark-sweep)"
# Heap limitini artirma (gecici cozum, koku bulmak icin)
node --max-old-space-size=4096 app.js
# V8 flag'lerini listele
node --v8-options | grep -i heap
Üretim Ortamında Acil Müdahale
Üretim ortamında bir bellek sızıntısı tespit ettiğinizde ve düzeltmeyi hemen deploy edemediğinizde, kısa vadeli çözümler için PM2’nin özelliklerinden yararlanabilirsiniz:
# PM2 ile bellek limitine gore otomatik yeniden baslatma
pm2 start app.js --max-memory-restart 500M
# ecosystem.config.js ile ayarla
# max_memory_restart: '1G'
# Mevcut uygulamanin bellek durumunu goster
pm2 monit
# Hafiza kullanimi detayli log
pm2 describe app_name
Bu yaklaşım sızıntıyı gidermez, sadece process’in çökmesini önler. Gerçek düzeltmeyi yapmak için zaman kazandırır. Process her yeniden başladığında, loglarınıza kayıt düşün ve bu kayıtları izleyin; yeniden başlatma sıklığı sızıntının ne kadar hızlı ilerlediğini gösterir.
Sonuç
Node.js bellek sızıntıları, sabırlı ve sistematik bir yaklaşım gerektiren sorunlardır. Önce izleme kurarak trendin varlığını doğrulayın, ardından heap snapshot ve clinic gibi araçlarla daraltın, sonra kod seviyesinde kaynağı bulun ve giderin.
En önemli nokta şudur: Bellek sızıntısını önlemenin en iyi yolu, baştan önleyici tedbirler almaktır. Code review süreçlerinde event listener yönetimine, timer temizliğine ve cache boyutu sınırlarına özellikle dikkat edin. Grafana-Prometheus gibi bir izleme altyapısı kurarak heap trendini sürekli görünür kılın. Bir sorun üretimde patlak vermeden önce, geliştirme ortamında yük testleri sırasında ortaya çıkması için clinic doctor veya memwatch gibi araçları CI pipeline’ınıza entegre edin.
Bellek yönetimi, Node.js uygulamalarının uzun vadeli sağlığı için temel bir konudur. Bu konuya gereken özeni gösterdiğinizde, hem kullanıcı deneyiminiz hem de operasyonel maliyetleriniz ciddi ölçüde iyileşir.
