NODE.JS STREAM VE BACKPRESSURE NEDİR?

Node.js logosu ve taşan kova metaforuyla stream backpressure mekanizmasının somut anlatımı

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.

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.

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.

Hızlı musluk yavaş kova kıyaslaması ile backpressure olmadan bellek taşmasının görsel anlatımı

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:

  1. readStream.on('data', ...) içinde write()'ın dönüşünü kontrol et.
  2. False döndüyse readStream.pause() çağır.
  3. writeStream.once('drain', () => readStream.resume()) ile devam et.
  4. 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.

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.

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.

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.

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.

pipeline zincirinde readable transform writable üç aşama flow control akış diyagramı

Üretim Kontrol Listesi

Bir Node.js servisinde stream kodunu gözden geçirirken bakılması gereken noktalar:

  • fs.readFile yerine büyük dosyalar için fs.createReadStream kullanılıyor mu?
  • pipe() yerine pipeline() 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.