[NODE JS] Stream Backpressure Kavramı
Backpressure, stream programlamanın en önemli ve en az anlaşılan kavramlarından biridir. Türkçe karşılığı olarak "geri basınç" veya "akış kontrolü" diyebiliriz. Bu kavramı anlamadan stream'lerle çalışmak, freni olmayan bir arabayla yokuş aşağı inmek gibidir. Bir süre her şey yolunda gider ama sonunda felaket kaçınılmazdır. Ben bu dersi üretim ortamında bellek kullanımının aniden fırladığı ve sunucunun çöktüğü bir gece yarısı çağrısıyla öğrendim.
Backpressure, bir stream pipeline'ında yazma tarafının okuma tarafından daha yavaş olması durumunda ortaya çıkar. Düşünün ki çok hızlı okunan bir dosyanın verileri yavaş bir ağ bağlantısı üzerinden gönderiliyor. Okuma tarafı saniyede 100 MB veri üretirken, yazma tarafı saniyede sadece 10 MB veri gönderebiliyor. Aradaki fark bir yerlerde birikmek zorundadır ve bu birikme kontrolsüz olursa bellek patlar.
Node.js stream sistemi bu sorunu dahili bir mekanizmayla ele alır. Her writable stream'in bir highWaterMark değeri vardır. Bu değer, dahili tamponda ne kadar veri birikebileceğini belirler. Write metodu çağrıldığında, tampon bu eşiği aşarsa false değeri döner. Bu, "daha fazla veri gönderme, ben henüz mevcut verileri işleyemedim" anlamına gelir. Okuma tarafı bu sinyali alarak okumayı duraklatmalıdır.
Pipe mekanizması backpressure yönetimini otomatik olarak yapar. Readable stream'den Writable stream'e pipe yaptığınızda, Node.js arka planda write false döndüğünde okumayı duraklatır ve drain olayı tetiklendiğinde okumaya devam eder. Bu yüzden kendi stream pipeline'larınızı elle yönetmek yerine pipe veya pipeline kullanmanız şiddetle önerilir.
Ancak bazen pipe kullanmanız mümkün olmayabilir. Verileri işlerken asenkron bir işlem yapmanız gerektiğinde, örneğin her veri parçasını bir API'ye gönderirken, backpressure yönetimini kendiniz yapmanız gerekir. Bu durumda write metodunun dönüş değerini kontrol etmeniz ve false döndüğünde drain olayını beklemeniz gerekir.
Pratikte backpressure sorunlarını tespit etmek her zaman kolay olmayabilir. Bellek kullanımının zamanla artması, işlem süresinin uzaması veya Node.js prosesinin aniden çökmesi belirtileri olabilir. Process.memoryUsage() fonksiyonuyla bellek kullanımını izleyerek sorunları erken tespit edebilirsiniz. Özellikle heapUsed değerinin sürekli arttığını görüyorsanız muhtemelen bir backpressure sorununuz vardır.
HighWaterMark değerini ayarlamak da önemli bir optimizasyon noktasıdır. Varsayılan değer 16 KB'tır (object mode'da 16 nesne). Hızlı I/O işlemleri için bu değeri artırabilirsiniz ama çok yüksek tutmak bellek kullanımını artırır. Yavaş tüketiciler için ise daha düşük bir değer tercih edilebilir. Doğru dengeyi bulmak için uygulamanızın profilini çıkarmanız gerekir.
Transform stream'lerde backpressure özellikle dikkat gerektirir. Transform aşamasında yoğun hesaplama yapılıyorsa veya asenkron işlemler varsa, transform fonksiyonunun callback'ini çağırmadan önce işi tamamlaması gerekir. Callback geç çağrıldığında okuma otomatik olarak yavaşlar, bu da doğal bir backpressure mekanizması oluşturur.
JavaScript:
const { Readable, Writable, Transform, pipeline } = require('stream');
const fs = require('fs');
// Backpressure sorununu gösteren YANLIŞ kullanım
function yanlisKullanim() {
const okuma = fs.createReadStream('./buyuk-dosya.dat');
const yazma = fs.createWriteStream('./hedef.dat');
// DİKKAT: Bu yaklaşım backpressure'ı yönetmez!
okuma.on('data', (parca) => {
// write() dönüş değeri kontrol edilmiyor
// Eğer yazma yavaşsa, veriler bellekte birikir
yazma.write(parca);
});
okuma.on('end', () => {
yazma.end();
console.log('İşlem tamamlandı (ama bellek sorunları olabilir!)');
});
}
// Backpressure'ı doğru yöneten elle yazılmış akış kontrolü
function dogruKullanim() {
const okuma = fs.createReadStream('./buyuk-dosya.dat');
const yazma = fs.createWriteStream('./hedef.dat');
okuma.on('data', (parca) => {
const devamEdebilirMi = yazma.write(parca);
if (!devamEdebilirMi) {
// Yazma tamponu doldu, okumayı duraklat
console.log('Backpressure algılandı! Okuma duraklatılıyor...');
okuma.pause();
// Tampon boşaldığında okumaya devam et
yazma.once('drain', () => {
console.log('Drain olayı! Okuma devam ediyor...');
okuma.resume();
});
}
});
okuma.on('end', () => {
yazma.end();
console.log('İşlem tamamlandı (backpressure yönetildi!)');
});
}
// En iyi yöntem: pipeline kullanımı
function enIyiKullanim() {
const okuma = fs.createReadStream('./buyuk-dosya.dat');
const donustur = new Transform({
transform(parca, kodlama, geriCagir) {
// İşleme simülasyonu - yavaş bir işlem
const islenmisParca = parca.toString().toUpperCase();
// Callback çağrılana kadar okuma otomatik yavaşlar
setTimeout(() => {
this.push(Buffer.from(islenmisParca));
geriCagir();
}, 10);
}
});
const yazma = fs.createWriteStream('./hedef.dat');
// pipeline otomatik backpressure yönetimi sağlar
pipeline(okuma, donustur, yazma, (hata) => {
if (hata) {
console.error('Pipeline hatası:', hata.message);
} else {
console.log('Pipeline başarıyla tamamlandı!');
}
});
}
// Bellek kullanımını izleyen yardımcı fonksiyon
function bellekIzle(aralikMs = 1000) {
const baslangic = process.memoryUsage().heapUsed;
const zamanlayici = setInterval(() => {
const bellek = process.memoryUsage();
const heapMB = (bellek.heapUsed / 1024 / 1024).toFixed(2);
const rssMB = (bellek.rss / 1024 / 1024).toFixed(2);
const artis = ((bellek.heapUsed - baslangic) / 1024 / 1024).toFixed(2);
console.log(
'Bellek - Heap: ' + heapMB + ' MB | RSS: ' + rssMB + ' MB | Artış: ' + artis + ' MB'
);
}, aralikMs);
return () => clearInterval(zamanlayici);
}
// Hız sınırlayıcı Transform stream
class HizSinırlayici extends Transform {
constructor(byteSaniye) {
super();
this.byteSaniye = byteSaniye;
this.sonGonderim = Date.now();
this.gonderilenBoyut = 0;
this.toplamBoyut = 0;
}
_transform(parca, kodlama, geriCagir) {
this.toplamBoyut += parca.length;
this.gonderilenBoyut += parca.length;
const simdi = Date.now();
const gecenSure = (simdi - this.sonGonderim) / 1000;
const mevcutHiz = this.gonderilenBoyut / gecenSure;
if (mevcutHiz > this.byteSaniye && gecenSure > 0) {
const beklemeSuresi = (this.gonderilenBoyut / this.byteSaniye - gecenSure) * 1000;
setTimeout(() => {
this.push(parca);
geriCagir();
}, Math.max(0, beklemeSuresi));
} else {
this.push(parca);
geriCagir();
}
// Her saniye sıfırla
if (gecenSure >= 1) {
this.sonGonderim = simdi;
this.gonderilenBoyut = 0;
}
}
_flush(geriCagir) {
console.log('Toplam aktarılan veri: ' + (this.toplamBoyut / 1024 / 1024).toFixed(2) + ' MB');
geriCagir();
}
}
// Kullanım örneği
const izlemeyiDurdur = bellekIzle(2000);
const sinirli = new HizSinırlayici(1024 * 1024); // 1 MB/s sınırı
pipeline(
fs.createReadStream('./buyuk-dosya.dat'),
sinirli,
fs.createWriteStream('./yavas-kopyala.dat'),
(hata) => {
izlemeyiDurdur();
if (hata) console.error('Hata:', hata.message);
else console.log('Hız sınırlı aktarım tamamlandı.');
}
);
Backpressure kavramını anlamak, Node.js'te güvenilir ve ölçeklenebilir stream uygulamaları geliştirmenin anahtarıdır. Pipeline kullanarak otomatik backpressure yönetiminden faydalanın ve bellek kullanımını düzenli olarak izleyin. Bu basit kuralları takip ettiğinizde stream tabanlı uygulamalarınız çok daha kararlı çalışacaktır.
Emeğiniz ve katkınız için teşekkür ederiz!
Konunuz gerekli incelemelerden geçerek onaylandı. Topluluğumuza katkılarınızın devamını bekliyoruz.
İyi forumlar!
Konunuz gerekli incelemelerden geçerek onaylandı. Topluluğumuza katkılarınızın devamını bekliyoruz.
İyi forumlar!

Şu an konuyu görüntüleyenler (Toplam : 0, Üye: 0, Misafir: 0)
Benzer konular
- Kilitli
- Cevaplar
- 7
- Görüntüleme
- 2K
- Kilitli
- Cevaplar
- 1
- Görüntüleme
- 488
- Cevaplar
- 1
- Görüntüleme
- 53
- Cevaplar
- 1
- Görüntüleme
- 522
