404 Not Found

404 Not Found


nginx

Node.jsのストリーム — 4つのタイプとバックプレッシャー機構の詳細な解説

チャーリーには、サーバー上の2GBのアクセスログを分析するという緊急の任務が課せられました。彼がfs.readFileを使ってログを読み込んだ途端、メモリがすぐに不足し、OOMクラッシュが発生してしまいました。チームリーダーは、「ファイルがどんなに大きかろうと、一度に全部処理しようとしないで、Streamを使って少しずつ処理しなさい!」とアドバイスした。チャーリーはcreateReadStreamに切り替えてログを1行ずつ処理したところ、メモリ使用量は50MBで安定した。それ以来、彼は出会う人すべてにこう言うようになった。「Streamは、Node.jsでビッグデータを処理するためのスイスアーミーナイフのような存在だ。」

1. ストリームの基本概念

Streamは、Node.jsにおけるストリーミングデータを処理するための抽象インターフェースです。ストリーミングデータとは、データが一度にメモリに読み込まれるのではなく、水の流れのようにチャンク単位で送信されるものです。

(1) 4種類のフロー

タイプ 説明 代表的なシナリオ 入力 出力
読み取り可能 読み取り可能なストリーム、データソース fs.createReadStream, process.stdin なし はい
書き込み可能 書き込み可能なストリーム、データエンドポイント fs.createWriteStream, process.stdout はい いいえ
デュプレックス デュプレックス(読み取り/書き込み独立) net.Socket, tls.Socket はい はい
変換 変換ストリーム。出力は入力の変換結果である zlib.createGzip, crypto.createCipheriv はい はい

(2) 2種類の流れ

Readableストリームには、2つの動作モードがあります:

機能 再生モード 一時停止モード
データの取得 自動プッシュ;data イベントを介して消費 取得するには read() を手動で呼び出す必要がある
トリガー方法 data リスナーの追加 / pipe() の呼び出し / resume() の呼び出し 初期デフォルト状態 / pause() の呼び出し
背圧処理 pipe() 自動処理;手動操作の場合は監視が必要 drain 消費者がペースを制御、自然な背圧
ユースケース 高スループットの連続データソース 読み取りタイミングの精密な制御が必要

2. ストリームイベントの詳細な解説

すべてのストリームは EventEmitter をベースとしており、イベント駆動型の仕組みを用いてデータや状態を伝達しています。

(1) 一般的なイベント

イベント トリガー条件 適用されるフロー コールバックパラメータ
data 新しいチャンクを受信 読み取り可能 chunk
end データ読み取り 読み取り可能 なし
error エラー すべてのストリーム Error
close 低レベルリソースを閉じる すべてのストリーム なし
finish end() すべてのデータがフラッシュされた後 書き込み可能 なし

▶ 例:Readableストリームのイベントをリッスンする

JAVASCRIPT
const fs = require('fs');
const rs = fs.createReadStream('./access.log', { highWaterMark: 64 * 1024 });

rs.on('data', (chunk) => {
  console.log(`Received ${chunk.length} Byte`);
});

rs.on('end', () => {
  console.log('Finished reading');
});

rs.on('error', (err) => {
  console.error('Output error:', err.message);
});

▶ 例:書き込み可能ストリームのイベントを監視する

JAVASCRIPT
const ws = fs.createWriteStream('./output.txt');

ws.on('finish', () => {
  console.log('All data has been written');
});

ws.on('error', (err) => {
  console.error('Write error:', err.message);
});

ws.write('Hello Stream');
ws.end();

3. pipe() との連鎖

pipe() は Stream の最も強力なメソッドであり、読み取り可能なストリームの出力を書き込み可能なストリームの入力に自動的に接続し、バックプレッシャーを自動的に処理します。

(1) 「pipe」の基本的な使い方

JAVASCRIPT
readable.pipe(writable);

pipe() ターゲットストリームを返すため、チェーン処理が可能です:

JAVASCRIPT
readable.pipe(transform1).pipe(transform2).pipe(writable);

▶ 例:ファイルのコピー

JAVASCRIPT
const fs = require('fs');
fs.createReadStream('./source.txt')
  .pipe(fs.createWriteStream('./dest.txt'));

▶ 例:HTTPレスポンスのストリーミング

JAVASCRIPT
const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  fs.createReadStream('./large.txt').pipe(res);
}).listen(3000);

(2) パイプデータのフロー図

100%
flowchart LR
    A[Readable<br/>Data Source] -->|chunk| B[Transform<br/>Data Transformation]
    B -->|chunk| C[Writable<br/>Data Endpoint]
    C -.->|backpressure| A
    B -.->|backpressure| A

    style A fill:#4CAF50,color:#fff
    style B fill:#FF9800,color:#fff
    style C fill:#2196F3,color:#fff

4. fs ストリームとファイル操作

fs モジュールは、ファイルシステムに関連する読み取りおよび書き込みストリームを提供します。

(1) createReadStream / createWriteStream

オプション 説明 既定値
highWaterMark バッファサイズ(バイト) 読み取り可能:64KB / 書き込み可能:16KB
encoding エンコーディング null (バッファ)
start 開始バイト位置 0
end 終了バイトの位置(そのバイトを含む) 無限大
flags ファイルのアクセス権 読み取り可能:r / 書き込み可能:w

▶ 例:指定した範囲内のファイルセグメントを読み込む

JAVASCRIPT
const fs = require('fs');
const rs = fs.createReadStream('./big.bin', {
  start: 100,
  end: 199,
  highWaterMark: 32
});

rs.on('data', (chunk) => {
  console.log(chunk.length);
});

(2) readFile と createReadStream の比較

比較 readFile createReadStream
メモリ使用量 メモリに読み込まれたすべてのファイル highWaterMark サイズのみを使用
起動の遅延 すべてのファイルの読み込みが完了するまで待機してからコールバックを呼び出す ストリームを直ちに返し、読み込みながら処理する
適切なファイルサイズ 小さなファイル(10 MB未満) 大きなファイルまたは連続データ
エラー処理 コールバックでのエラーの取得 error イベントのリスニング
一時停止・再開が可能 非対応 pause() / resume()

▶ 例:大容量ファイルをブロック単位で処理する

JAVASCRIPT
const fs = require('fs');
let totalBytes = 0;

const rs = fs.createReadStream('./2gb.log');
rs.on('data', (chunk) => {
  totalBytes += chunk.length;
});
rs.on('end', () => {
  console.log(`Total ${totalBytes} Byte`);
});

5. 背圧メカニズム

バックプレッシャーはフロー制御の中核をなすものです。書き込み側が読み出し側のプッシュレートに追いつけない場合、読み出し側を一時停止させ、メモリのバックログを防ぐために、書き込み側に「バックプレッシャー」がかけられます。

(1) pipe() はバックプレッシャーを自動的に処理する

pipe() を使用する場合、背圧は Node.js によって内部で自動的に管理されるため、手動での操作は不要です。

(2) 手動背圧制御

pipe() を使用しない場合は、write() の戻り値を手動で評価する必要があります:

JAVASCRIPT
const fs = require('fs');
const rs = fs.createReadStream('./source.txt');
const ws = fs.createWriteStream('./dest.txt');

rs.on('data', (chunk) => {
  const canContinue = ws.write(chunk);
  if (!canContinue) {
    rs.pause();
    ws.once('drain', () => {
      rs.resume();
    });
  }
});

rs.on('end', () => {
  ws.end();
});

▶ 例:背圧の影響の観察

JAVASCRIPT
const rs = fs.createReadStream('./big.log', { highWaterMark: 1024 });
const ws = fs.createWriteStream('./out.log', { highWaterMark: 512 });

let paused = 0;
rs.on('data', (chunk) => {
  const ok = ws.write(chunk);
  if (!ok) {
    paused++;
    rs.pause();
    ws.once('drain', () => rs.resume());
  }
});
rs.on('end', () => {
  console.log(`Backpressure triggered ${paused} times`);
  ws.end();
});

6. ストリームの変換

TransformストリームはDuplexのサブクラスであり、その出力は入力の変換結果となります。一般的に、データの圧縮、暗号化、および形式変換に使用されます。

(1) 「トランスフォーム」と「デュプレックス」の違い

比較項目 デュプレックス 変換
入力と出力の関係 独立しており、互いに影響を与えない 入力を変換することで出力が生成される
必須メソッド _read() + _write() _transform()
代表的な用途 ネットワークソケット 圧縮、暗号化、データ変換
内部バッファ 読み取り用と書き込み用がそれぞれ1つずつ 変換の中間状態

▶ 例:カスタム変換ストリーム(大文字・小文字の変換)

JAVASCRIPT
const { Transform } = require('stream');

const upper = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  }
});

process.stdin.pipe(upper).pipe(process.stdout);

▶ 例:zlibで圧縮されたファイル

JAVASCRIPT
const fs = require('fs');
const zlib = require('zlib');

fs.createReadStream('./access.log')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('./access.log.gz'));

▶ 例:zlibファイルの解凍

JAVASCRIPT
const fs = require('fs');
const zlib = require('zlib');

fs.createReadStream('./access.log.gz')
  .pipe(zlib.createGunzip())
  .pipe(fs.createWriteStream('./access_restored.log'));

7. ストリームの停止と再開

「Readable」ストリームはデフォルトで一時停止モードになっており、pause() および resume() を使用して切り替えることができます。

▶ 例:一時停止と再開の操作

JAVASCRIPT
const fs = require('fs');
const rs = fs.createReadStream('./big.log');
let count = 0;

rs.on('data', (chunk) => {
  count++;
  if (count % 10 === 0) {
    rs.pause();
    console.log(`Processed ${count} chunks, pause 1 second`);
    setTimeout(() => rs.resume(), 1000);
  }
});

▶ 例:ファイルを行単位で読み込む(readline)

JAVASCRIPT
const fs = require('fs');
const readline = require('readline');

const rl = readline.createInterface({
  input: fs.createReadStream('./access.log'),
  crlfDelay: Infinity
});

rl.on('line', (line) => {
  if (line.includes('ERROR')) {
    console.log(line);
  }
});

rl.on('close', () => {
  console.log('The file has been read.');
});

8. 包括的な例:ログ処理パイプライン

完全なデータ処理パイプラインを構築する:大規模なログファイルを読み込む → 行ごとに解析する → エラー行を除外する → 出力を圧縮する。

JAVASCRIPT
const fs = require('fs');
const zlib = require('zlib');
const { Transform } = require('stream');

const filterError = new Transform({
  transform(chunk, encoding, callback) {
    const lines = chunk.toString().split('\n');
    const errors = lines.filter(l => l.includes('ERROR')).join('\n');
    callback(null, errors ? errors + '\n' : '');
  }
});

const src = fs.createReadStream('./app.log');
const dest = fs.createWriteStream('./errors.log.gz');

src
  .pipe(filterError)
  .pipe(zlib.createGzip())
  .pipe(dest);

dest.on('finish', () => {
  console.log('The error log has been written in compressed form.');
});

src.on('error', (err) => {
  console.error('Read failed:', err.message);
});

❓ よくある質問

Q ストリームとは何ですか?
A ストリームとは、データを処理するための抽象的なインターフェースです。データを一度にメモリに読み込むことなく、チャンク単位で読み書きすることができます。
Q 読み取り可能ストリームと書き込み可能ストリームの違いは何ですか?
A 読み取り可能ストリームは、データを読み取ることができるデータソースであり、書き込み可能ストリームは、データを書き込むことができるデータの宛先です。
Q ストリームはどのような場合に使用すべきですか?
A 大容量ファイルの処理、ネットワーク通信、リアルタイムデータ処理など、データ量が多い場合や、データを一度にメモリに読み込めない場合などに使用します。
Q pipe メソッドはどのような働きをしますか?
A pipe は、読み取り可能なストリームの出力を書き込み可能なストリームの入力に接続し、データフローとバックプレッシャーを自動的に管理します。
Q バックプレッシャーとは何ですか?
A 書き込みストリームのデータ出力レートが読み取りストリームのデータ入力レートよりも遅い場合、書き込みストリームは読み取りストリームに対して読み取りを一時停止するよう信号を送ります。これがバックプレッシャー機構です。

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%