Stream: backpressure

Механизм авторегуляции потока данных: если приёмник не успевает писать, источник приостанавливается. Без backpressure быстрый read в медленный write взорвёт память.

Что это

В цепочке Readable → Writable (через pipe) скорости могут не совпадать: чтение с диска идёт MB/s, запись по сети — KB/s. Без регуляции байты копятся в буфере writable и память растёт. Backpressure — обратное давление: writable говорит «стоп», readable приостанавливает чтение.

У каждого стрима в конструкторе есть два параметра:

  • highWaterMark — буфер > этого → пауза источника
  • lowWaterMark — буфер < этого → упреждающее чтение

Дефолт highWaterMark = 16 КБ (или 16 объектов в object mode).

Как срабатывает

const writable = fs.createWriteStream('out');

const ok = writable.write(chunk);
// false — буфер переполнен, перестать писать
// true  — можно дальше

if (!ok) {
  readable.pause();             // притормозить источник
  writable.once('drain', () => readable.resume); // буфер опустел → продолжить
}

При использовании pipe всё это происходит автоматически:

fs.createReadStream('big.mp4')
  .pipe(zlib.createGzip)
  .pipe(res); // pipe сам отслеживает backpressure

В цепочке pipe данные тянутся «с конца»: самый последний writable просит у предыдущего, тот у источника. Это pull-модель.

pipeline вместо ручного pipe

pipe не закрывает потоки при ошибке: если Readable бросил error, Writable остаётся открытым → утечка дескрипторов. Решение — stream.pipeline:

const { pipeline } = require('node:stream/promises');

await pipeline(
  fs.createReadStream('input'),
  zlib.createGzip,
  fs.createWriteStream('output.gz')
);
// при ошибке: все потоки закрываются автоматически

Подводные камни

  • pipe не закрывает события сам: при ручном pipe всё равно надо вешать on('end'), on('close'), on('error'). Иначе утечка
  • Ошибка без обработчикаEventEmitter бросает throw, в синхронном контексте процесс падает. В async — зависит
  • Игнор write returning false → потеря backpressure, накопление в RAM
  • Object mode: highWaterMark считается в штуках объектов, не байтах
  • req/res HTTP — duplex на одном сокете, end/close синхронизированы; allowHalfOpen влияет на закрытие

🎓 Источник: HTTP Server in Node.js — req, res, sockets, and streams

  • 📅 2020-01-16 · YouTube · PDR5hcV4a_0
  • Тезисы:
    • Стримы — ленивая обработка: ничего лишнего не читается, данные тянутся с конца цепочки
    • У каждого стрима в конструкторе highWaterMark / lowWaterMark: буфер > high → пауза источника, < low → упреждающее чтение
    • Каждый стрим в цепочке вырабатывает backpressure независимо
    • Если хочешь весь файл — fs.readFile быстрее чем стрим; стрим оправдан только при обработке/преобразовании
    • req и res — это один и тот же сокет (duplex), end и close синхронизированы
    • Необработанный error в синхронном контексте роняет процесс; в асинхронном может не свалить
  • Цитата:

    «Если количество забуферизированных данных, которые никто еще не смог прочитать, будет выше, чем high watermark, то стрим приостанавливается и не читает данные из источника дальше.»

См. также