カテゴリー別アーカイブ: 佐藤寛之

Nodeでストリームをpipeするときにリソースリークを起こさないために

Nodeでファイルやネットワークを扱うときにはストリームを用いることが多いと思います。例えばネットワーク越しにデータを取得してファイルに保存する、といった処理ではpipeを使って入力側と出力側のストリームをつなげるというのをよくやります。

このpipeにはあまり知られていない落とし穴があります。Nodeのリファレンスのstream.Readableの項には以下の記述があります。

One important caveat is that if the Readable stream emits an error during processing, the Writable destination is not closed automatically. If an error occurs, it will be necessary to manually close each stream in order to prevent memory leaks.

https://nodejs.org/dist/latest-v12.x/docs/api/stream.html#stream_readable_pipe_destination_options

つまり、入力側のストリームで何かエラーが発生した場合、出力側のストリームは自動的には閉じられないので明示的に閉じる必要があるということです。

これを忘れるとどうなるかというと、例えば出力側がファイルだとするとエラーのたびに開きっぱなしのファイルが増えていき、Linuxやmacではファイルディスクリプタの上限に引っかかってプロセスが死にます。先日実際に死にました。Windowsでどうなるかは確認していませんがあまり好ましいことにはならないでしょう。これがバッチのように実行の都度プロセスが起動→終了する場合であれば大きな問題にはなりにくいですが、プロセスがサーバーとして動き続ける場合は危険です。

例を見てみましょう。S3にあるファイルをローカルに保存するためにpipeで2つのストリームをつなげる場合です。

const s3 = new AWS.S3();
const writeStream = fs.createWriteStream('foo.txt');
const readStream = s3.getObject({Bucket: 'BUCKET_NAME', Key: 'foo.txt'}).createReadStream();
readStream.pipe(writeStream);

readStream.on('error', (err) => {

// Writable is not closed automatically when Readable emits 'error'
// so we have to close Writable manually as below:

writeStream.destroy(err); // this will emit 'error' and 'close'
// or
writeStream.destroy(); // this will emit 'close'
// or
writeStream.end(); // this will emit 'finish'

console.error(err);
});

readStream.on('end', () => {

// Writable automatically close when Readable ends by default
// unless specify pipe option like `readStream.pipe(writeStream, {end:false});`

console.log('ok');
});

入力側でerrorイベントが発生した場合、リファレンスに従い出力側をdestroyなりendなりで閉じてあげる必要があるようです。コメントに書いたように閉じ方によって発生するイベントが変化するので、そのあたりは使い分けかなと思います。エラーなくendイベントが発生した場合は自動的に閉じられるので何もする必要はありません。ただし、pipeの第2引数に{end:false}を指定した場合は自動的には閉じられなくなります。

Web上で調べてもこの件に触れられている情報源は数少なかったので、意外と知られていないのかなと考えて今回軽く整理してみました。

CやC++でfreedeleteを忘れると厄介なことになりますが、比較的新しめの言語ではメモリやリソースの解放に神経を使うことも少なくなりました。それでもときどきこういうことがあるので、リファレンスはきちんと目を通しましょうというお話でした。