Node.js stream.pipelineで大容量ファイルを安全に処理する方法
問題
数GBのログファイルをfs.readFileで一度に読み込もうとして、メモリが溢れてしまいました。当然の結果ですが、つい忘れてしまいがちです。
// これだとファイル全体がメモリに載ってしまう
const data = await fs.promises.readFile('huge.log', 'utf-8');
解決方法
stream.pipelineを使えば、チャンク単位で読み込み・変換・書き込みを行うパイプラインを構築できます。エラー処理とストリームのクリーンアップも自動です。
const { pipeline } = require('stream/promises');
const fs = require('fs');
const zlib = require('zlib');
// 大容量ファイルをgzip圧縮しながらコピー
await pipeline(
fs.createReadStream('huge.log'),
zlib.createGzip(),
fs.createWriteStream('huge.log.gz')
);
行単位の処理が必要な場合は、Transformストリームを挟みます。
const { Transform } = require('stream');
const lineFilter = new Transform({
transform(chunk, encoding, callback) {
const lines = chunk.toString().split('\n');
const errors = lines
.filter(line => line.includes('ERROR'))
.join('\n');
callback(null, errors ? errors + '\n' : '');
}
});
await pipeline(
fs.createReadStream('huge.log'),
lineFilter,
fs.createWriteStream('errors-only.log')
);
ポイント
stream/promisesのpipelineはasync/awaitと自然に組み合わせることができます- エラー発生時にパイプライン内のすべてのストリームを自動でdestroyしてくれます。
.pipe()チェーンとの最大の違いです - メモリ使用量がファイルサイズに関係なく一定です。10GBでも100GBでも問題ありません