NODE.JS STREAM VE BACKPRESSURE NEDİR?

Saat 03.00, üretim ortamından alarm geldi: 12 GB'lık bir log dosyasını parse edip başka bir servise aktaran Node.js worker, RSS belleği 4 GB'ı aşınca OOM ile çöküyor. Kod gözle "doğru" görünüyor — readStream.on('data', ...) ile veri okunuyor, writeStream.write(chunk) ile yazılıyor. Ama hiç kimse write()'in dönüş değerine bakmamış. Bu sessiz hata, Node.js stream'lerinin en yanlış anlaşılan tarafıdır: backpressure. Hızlı okuyup yavaş yazdığınızda, aradaki fark RAM'de birikir ve süreci yere serer.
1. Stream Nedir ve Neden Var?
Stream, veriyi parça parça (chunk) işleyen bir soyutlamadır. Dosyanın tamamını fs.readFileSync ile belleğe almak yerine, 64 KB'lık tamponlarla okur, işler, bırakırsınız. Node.js'te dört temel tür vardır:
- Readable: Veri okunan kaynak (dosya, HTTP request, stdin).
- Writable: Veri yazılan hedef (dosya, HTTP response, stdout).
- Duplex: Hem okunan hem yazılan (TCP socket).
- Transform: Veriyi dönüştüren duplex (gzip, şifreleme).
Stream'in vaadi şudur: 12 GB dosya için 12 GB RAM gerekmesin. Bu vaat sadece backpressure doğru ele alındığında geçerlidir.
2. Problem: Hızlı Üretici, Yavaş Tüketici
Tipik senaryo: Disk'ten okumak hızlı (saniyede yüzlerce MB), ağa yazmak yavaş (saniyede onlarca MB). Aradaki fark her saniye birikmeye başlar. Yanlış kod şu şekildedir:
readStream.on('data', (chunk) => { writeStream.write(chunk); });
Bu kodda write() her zaman çağrılır ve dönüş değeri yok sayılır. write() aslında bir boolean döner: true ise iç tampon henüz dolmamıştır, devam et; false ise tampon yüksek su seviyesini (highWaterMark, varsayılan 16 KB) aşmıştır, durmalısın. Durmazsan Node.js seni durdurmaz — chunk'lar iç kuyruğa eklenmeye devam eder, V8 heap'i ya da off-heap buffer alanı şişer.

3. Backpressure Semantiği: drain Olayı
Backpressure, "yavaşla, henüz hazır değilim" sinyalidir. write() false döndüğünde okumayı duraklatır, writable stream'in drain olayını beklersiniz. Bu olay tampon boşaldığında tetiklenir; o anda okumaya devam edebilirsiniz.
Doğru manuel yaklaşım:
readStream.on('data', ...)içindewrite()'ın dönüşünü kontrol et.- False döndüyse
readStream.pause()çağır. writeStream.once('drain', () => readStream.resume())ile devam et.- Okuma bitince
writeStream.end()ile kapat.
Bu döngü doğru kurulduğunda 100 GB'lık dosya bile sabit ~16 KB RAM ile akar. Tampon hiç dolmaz çünkü kaynak, hedefin hızına ayak uydurur.
4. pipe() ve pipeline(): Otomatik Çözüm
Yukarıdaki manuel dans hata yapmaya çok açıktır: error handler atlama, end olayını unutma, double-resume bug'ı... Node.js bunu otomatikleştirir. readStream.pipe(writeStream) tek satırla backpressure'ı, pause/resume'u ve veri akışını yönetir.
Ancak pipe()'ın bir zaafı vardır: hata oluştuğunda alttaki stream'leri otomatik kapatmaz, kaynak sızıntısına yol açar. Node 10+ ile gelen stream.pipeline() bunu çözer:
pipeline(readStream, gzipStream, writeStream, (err) => { if (err) console.error(err); });
pipeline tüm zincirde hata yakalar, hata anında tüm stream'leri düzgünce kapatır ve callback (ya da promisify edilmiş haliyle Promise) ile sonucu bildirir. Üretim kodunda neredeyse her zaman pipeline tercih edilmelidir; tüm stream API'sinin davranışı, seçenekleri ve sürüm farkları resmi stream dokümantasyonunda belgelenmiştir.
5. highWaterMark: Tampon Eşiğini Ayarlama
Varsayılan highWaterMark 16 KB'tır (object mode'da 16 nesne). Bu değer çoğu senaryo için iyidir ama bazı durumlarda artırılabilir:
- Yüksek throughput'lu ağ aktarımları: 64 KB - 256 KB arası test edilebilir.
- Çok küçük chunk'larla gelen kaynaklar: küçültmek event sayısını artırır, performansı düşürür.
- Object mode transform'lar: nesne sayısı, byte değil — küçük tutmak gerekir.
highWaterMark sadece bir "uyarı eşiği"dir, sert bir limit değildir. Eşik aşılınca write() false döner; siz yine de yazmaya devam edebilirsiniz — ama bu zaten problemin ta kendisidir.
6. Async Iterator ile Modern Yaklaşım
Node 10+ ile for await...of readable stream'ler üzerinde çalışır ve backpressure'ı doğal olarak korur. Çünkü await bir sonraki chunk gelene kadar döngüyü durdurur; iterator ile readable arasındaki senkronizasyon zaten geri basıncı sağlar:
for await (const chunk of readStream) { if (!writeStream.write(chunk)) await once(writeStream, 'drain'); }
Bu deyim manuel pause/resume'dan daha okunaklıdır. Yine de error handling için pipeline sarması önerilir. Node.js'in stream temelini daha geniş bir bağlamda öğrenmek için Node.js eğitimi içeriğinden yararlanabilirsiniz.
7. Transform Stream'lerde Backpressure
Pipe zincirinde araya bir Transform (örneğin zlib.createGzip()) eklediğinizde backpressure tüm zincire yayılır. Sıkıştırma yavaşsa, gzip Transform iç tamponunu doldurur; bu okuma kaynağına geri yansır ve disk okuma yavaşlar. Zincirin en yavaş elemanı tüm akışın tempo'sunu belirler — buna "flow control" denir ve doğru pipeline kurulumunda otomatik çalışır.

8. Üretim Kontrol Listesi
Bir Node.js servisinde stream kodunu gözden geçirirken bakılması gereken noktalar:
fs.readFileyerine büyük dosyalar içinfs.createReadStreamkullanılıyor mu?pipe()yerinepipeline()tercih ediliyor mu?write()'ın dönüş değeri kontrol ediliyor mu, yoksa yok mu sayılıyor?- Hata handler'ları her stream için tanımlı mı? Tek bir handler eksik kaynak sızıntısına yeter.
- HTTP response'larda
req.on('close')ile erken kapanma yakalanıyor mu? - Object mode kullanılıyorsa highWaterMark mantıklı bir değere ayarlanmış mı?
Backpressure, Node.js'in performansını ve stabilitesini belirleyen sessiz mekanizmadır. Doğru kurulduğunda gigabayt-mertebesinde verileri kilobayt-mertebesi bellekle akıtır. Yok sayıldığında ise üretimde gece yarısı sayfaya çağrılırsınız. Node.js eğitimi kapsamında pipe ve pipeline'ı pratik örneklerle inceleyebilirsiniz — stream zincirini bir kez doğru kurduğunuzda, geri kalan ölçekleme problemlerinin yarısı kendiliğinden ortadan kalkar.
Sıkça Sorulan Sorular
Backpressure tam olarak ne anlama gelir?
Backpressure, bir writable stream'in iç tamponunun dolduğunu ve daha fazla veri kabul edemeyeceğini kaynağa bildiren mekanizmadır. write() metodunun false döndürmesi ile sinyallenir ve drain olayı ile tampon boşaldığında geri kalkar. Üretici-tüketici hız farkını belleğe yansıtmadan dengeler.
pipe() ile pipeline() arasındaki fark nedir?
pipe() backpressure'ı yönetir ama hata oluştuğunda zincirdeki diğer stream'leri otomatik kapatmaz, dosya tanıtıcı sızıntısına yol açabilir. pipeline() Node 10 ile geldi ve zincirde herhangi bir hata oluştuğunda tüm stream'leri düzgünce kapatır, tek bir callback veya Promise üzerinden sonucu bildirir.
highWaterMark değerini değiştirmeli miyim?
Çoğu durumda varsayılan 16 KB yeterlidir. Yüksek bant genişlikli ağ aktarımları veya disk okumalarında 64-256 KB test edilebilir, throughput artabilir. Object mode'da değer nesne sayısıdır, küçük tutulmalıdır. Değişikliği daima benchmark ile doğrulayın, körlemesine büyütmek bellek baskısını artırır.
for await...of stream üzerinde backpressure'ı korur mu?
Evet. await bir sonraki chunk hazır olana kadar döngüyü duraklatır, bu doğal bir geri basınç yaratır. Ancak yazma tarafındaki write() yine false dönebilir; bu durumda events.once(writeStream, 'drain') ile drain'i beklemek gerekir. Hata yönetimi için yine pipeline veya try/catch önerilir.
write() false döndüğünde ne olur, veri kayboluyor mu?
Hayır, veri kaybolmaz. Yazılan chunk iç kuyruğa eklenir ve sırası gelince işlenir. False sadece bir uyarıdır: tampon yüksek su seviyesini aştı, yazmaya devam edebilirsin ama bellek büyür. Doğru davranış, pause/drain döngüsü ile yazmayı geçici olarak durdurmaktır.
Büyük dosyayı fs.readFile ile okumak neden tehlikeli?
fs.readFile dosyanın tamamını tek seferde belleğe alır. 2 GB'lık bir dosya için en az 2 GB heap gerekir; V8'in string/buffer boyut limitlerine de takılabilir. createReadStream ise 64 KB'lık chunk'larla işler, bellek sabit kalır. Büyük dosyalarda her zaman stream tercih edilmelidir.
HTTP response'ta backpressure önemli mi?
Çok önemli. Yavaş bir istemci yüksek hızla veri pompalayan sunucunun belleğini şişirebilir. res.write() dönüşünü kontrol etmek ya da kaynağı doğrudan res'e pipeline ile bağlamak gerekir. Ayrıca req.on('close') ile istemcinin erken kapatması yakalanıp kaynak stream'i destroy edilmelidir.
Transform stream eklediğimde backpressure nasıl çalışır?
Backpressure tüm pipeline boyunca yayılır. Transform'un iç tamponu dolarsa kendisinden önceki readable yavaşlatılır. Böylece zincirdeki en yavaş eleman, üst akıştaki tüm üreticilerin hızını belirler. Bu mekanizmaya flow control denir ve pipeline kurulumunda otomatik devreye girer.


