Merhaba arkadaşlar, bugün uzun süreli çalışan bir Node.js sunucumda karşılaştığım ve RAM'in yavaş yavaş şişip sunucuyu çökerten bir memory leak sorununu nasıl çözdüğümü anlatacağım. Bu hatayı ilk gördüğümde, uygulama başlangıçta gayet stabilken, birkaç gün sonra "JavaScript heap out of memory" hatası verip kapanıyordu. İşin köküne indiğimde, sorunun düzgün temizlenmeyen closure'lar ve event listener'lar olduğunu gördüm.
Sorunun Kökeni: Görünmez Bağlar
Node.js'in güçlü hafıza yönetimi (garbage collection) var, ama bir nesneye referans varsa onu temizleyemez. Özellikle event dinleyicileri (listener) veya bir closure içinde yakalanan (captured) değişkenler, beklenmedik referanslar oluşturabilir. En yaygın senaryo, bir olayı dinleyen bir sınıf örneğinin (instance), kendisini dinleyen olaydan kaldırmadan (remove) bellekten silinmesi. Bu durumda garbage collector, olay dinleyicisi hala aktif olduğu için o örneği temizleyemez ve bellek sürekli büyür.
Çözüm: Temizlik Rutinleri Oluşturmak
Çözüm, nesnelerin ömrü bittiğinde tüm dış bağlantılarını (external references) kendi elleriyle koparması. İşte benim kullandığım en temiz çözüm yapıları.
İlk olarak, bir EventEmitter kullanan bir sınıfınız varsa, mutlaka bir destroy() veya cleanup() metodu implemente edin ve bu metodu, örneği kullanmayı bırakmadan hemen önce çağırın.
İkinci Katman: WeakRef ve FinalizationRegistry (Node 14+)
Eğer durum daha karmaşıksa ve destroy() metodunu çağırmayı unutma riskiniz varsa, WeakRef ve FinalizationRegistry ile bir güvenlik ağı oluşturabilirsiniz. Bu, son çare olarak düşünülmeli, çünkü garbage collector'un ne zaman çalışacağı belli olmaz.
Sonuç olarak, Node.js'te memory leak'i engellemenin anahtarı disiplinli bir kaynak yönetimi. Her "bağlanma" işleminin (on, addEventListener, setInterval) karşılığında bir "ayrılma" işlemi (off, removeListener, clearInterval) planlamalısınız. Bu, özellikle uzun ömürlü (long-lived) sunucu uygulamalarında hayati önem taşıyor.
Siz de Node.js projelerinizde benzer memory leak sorunları yaşadınız mı? Hangi araçlarla (örneğin heap snapshot) tespit ettiniz? Sizin kullandığınız farklı bir temizlik pattern'i var mı? Yorumlarda paylaşalım!
Node.js'in güçlü hafıza yönetimi (garbage collection) var, ama bir nesneye referans varsa onu temizleyemez. Özellikle event dinleyicileri (listener) veya bir closure içinde yakalanan (captured) değişkenler, beklenmedik referanslar oluşturabilir. En yaygın senaryo, bir olayı dinleyen bir sınıf örneğinin (instance), kendisini dinleyen olaydan kaldırmadan (remove) bellekten silinmesi. Bu durumda garbage collector, olay dinleyicisi hala aktif olduğu için o örneği temizleyemez ve bellek sürekli büyür.
Çözüm, nesnelerin ömrü bittiğinde tüm dış bağlantılarını (external references) kendi elleriyle koparması. İşte benim kullandığım en temiz çözüm yapıları.
İlk olarak, bir EventEmitter kullanan bir sınıfınız varsa, mutlaka bir destroy() veya cleanup() metodu implemente edin ve bu metodu, örneği kullanmayı bırakmadan hemen önce çağırın.
JavaScript:
const EventEmitter = require('events');
class ConnectionManager extends EventEmitter {
constructor(databaseUrl) {
super();
this.dbUrl = databaseUrl;
this.internalTimer = null;
this.externalEventListener = null;
// ⚠️ DİKKAT: Bu listener, 'this' context'ini yakalar (capture).
this.on('dataReceived', this.handleData);
// ⚠️ DİKKAT: Bir dış modülün event'ine dinleyici ekliyoruz.
someExternalModule.on('update', this.externalUpdateHandler.bind(this));
this.startInternalProcess();
}
handleData(data) {
console.log('İşlenen veri:', data);
}
externalUpdateHandler(update) {
console.log('Dış güncelleme:', update, 'Bağlantı:', this.dbUrl);
}
startInternalProcess() {
this.internalTimer = setInterval(() => {
this.emit('heartbeat');
}, 5000);
}
// 🧹 İŞTE KRİTİK TEMİZLİK METODU
destroy() {
// 1. Kendi üzerindeki event listener'ları kaldır.
this.removeAllListeners('dataReceived');
// Veya spesifik listener'ı kaldır:
// this.off('dataReceived', this.handleData);
// 2. Dış modüllere eklediğimiz listener'ları kaldır.
if (someExternalModule && this.externalUpdateHandler) {
someExternalModule.off('update', this.externalUpdateHandler);
}
// 3. Interval ve timeout'ları temizle.
if (this.internalTimer) {
clearInterval(this.internalTimer);
this.internalTimer = null;
}
// 4. Büyük veri yapılarını boşalt.
this.dbUrl = null;
console.log('ConnectionManager temizlendi.');
}
}
// Kullanım
const manager = new ConnectionManager('mysql://localhost:3306/db');
// ... işlemler yap ...
manager.destroy(); // Artık garbage collector bu örneği güvenle silebilir.
Eğer durum daha karmaşıksa ve destroy() metodunu çağırmayı unutma riskiniz varsa, WeakRef ve FinalizationRegistry ile bir güvenlik ağı oluşturabilirsiniz. Bu, son çare olarak düşünülmeli, çünkü garbage collector'un ne zaman çalışacağı belli olmaz.
JavaScript:
// Örnek: Harici bir kaynağa zayıf referans tutma ve temizlik kaydı
class ResourceHolder {
constructor(externalResource) {
// Asıl kaynağa ZAYIF bir referans tutuyoruz.
this.weakResource = new WeakRef(externalResource);
this.resourceId = Math.random();
// FinalizationRegistry, nesne garbage collect edildiğinde bir callback çalıştırır.
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Resource ${heldValue} için temizlik yapılıyor...`);
// Burada, weakResource üzerinden hala erişilebilirse temizlik yapmaya ÇALIŞABİLİRİZ.
// Ancak %100 güvenilir değildir, sadece yardımcıdır.
});
// Bu örneği (this) kayıt defterine kaydediyoruz.
// Örnek garbage collect edildiğinde, id değeri callback'e parametre olarak gider.
registry.register(this, this.resourceId);
}
// Tercih edilen yöntem hala manuel destroy()
manualCleanup() {
const resource = this.weakResource.deref();
if (resource) {
// Hala erişilebilirse, manuel temizlik yap.
resource.removeListeners?.();
}
}
}
Sonuç olarak, Node.js'te memory leak'i engellemenin anahtarı disiplinli bir kaynak yönetimi. Her "bağlanma" işleminin (on, addEventListener, setInterval) karşılığında bir "ayrılma" işlemi (off, removeListener, clearInterval) planlamalısınız. Bu, özellikle uzun ömürlü (long-lived) sunucu uygulamalarında hayati önem taşıyor.
Siz de Node.js projelerinizde benzer memory leak sorunları yaşadınız mı? Hangi araçlarla (örneğin heap snapshot) tespit ettiniz? Sizin kullandığınız farklı bir temizlik pattern'i var mı? Yorumlarda paylaşalım!