Node.js Streamに入門してみた

2022.03.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

2022/3/31 17:04 追記: メモリ使用量計測方法を変更しました。


広島の吉川です。

今までNode.jsのStream APIについて、「なんか難しそう・・・」と逃げていた感があるのですが、

【S3からS3へ】Node.js の Streaming API を使って Lambda Function のみで CSVファイルを JSON Lines ファイルへ変換する | DevelopersIO

こちらの記事のような処理を組めるようになりたいと思ったため入門してみます。

Streamのメリットは大きな容量のファイルを読み取り・書き込み・加工する際に「ちょっとずつ処理」することで最大メモリ使用量を抑えることができる点と認識しているため、「大容量CSVを生成してファイルに書き出す」という処理をサンプルにやってみたいと思います。

環境

  • macOS Catalina
  • node 16.14.0
  • typescript 4.6.3
  • csv 6.0.5
  • esbuild 0.14.29
  • esbuild-register 3.3.2

最大使用メモリ量の計測

まず、最大使用メモリ量を測定する仕組みを用意します。

process.memoryUsage()+process.nextTick()

javascript - Monitor maximum memory consumption in Node.js process - Stack Overflow

メモリ使用量を監視し、逐次最大量を更新していき最後に出力するような仕組みを作ることで計測できるようです。

let maxMemory = 0

process.nextTick(() => {
  let memUsage = process.memoryUsage()
  if (memUsage.rss > maxMemory) {
    maxMemory = memUsage.rss
  }
})

process.on('exit', () => {
  console.log(`Max memory: ${maxMemory / 1024 / 1024}MB`)
})

@airbnb/node-memwatch

airbnb/node-memwatch: A NodeJS library to keep an eye on your memory usage, and discover and isolate leaks.

メモリ使用量を監視できるパッケージです。他にはnode-memwatchも有力そうでした。

import memwatch from '@airbnb/node-memwatch'

memwatch.on('stats', (stats) =>
  console.log(`${stats.used_heap_size / 1024 / 1024}MB`)
)

/usr/bin/timeコマンド (macOSの場合)

command - How to get the memory usage of a OS X/macOS process - Stack Overflow

node実行コマンドの直前に /usr/bin/time -l を付けることで実行後に使用メモリを出力することができるようです。

/usr/bin/time -l node -r esbuild-register index.ts

今回はこの方法を採用します。

Streamを使わずに大容量CSVを生成して書き込む

では、CSVデータの生成と書き込みをしてみます。csvパッケージを使って500万行のCSVを生成し、ファイルに書き出してみます。

import { generate } from 'csv/sync' // Sync APIを使う
import fs from 'fs'

fs.writeFileSync('example.csv', generate({ length: 5000000 }))

約1227.7MBを使用する結果となりました。

Streamを使って大容量CSVを生成して書き込む

続いてStreamを使って上と同じことをしてみます。

import { generate } from 'csv'
import fs from 'fs'

generate({ length: 5000000 })
  .pipe(fs.createWriteStream('example.csv'))
  .on('finish', () => console.log('finish!'))

約55.7MBの最大量使用に留まりました。

まとめ

ケース メモリ使用量
Streamを使わず大容量CSVデータの生成と書き込み 約1227.7MB
Streamを使って大容量CSVデータの生成と書き込み 約55.7MB

狙い通り、Streamを使うことでメモリ使用量を下げることができました。

今後、AWSリソースを絡めるなどもう少し複雑なケースのサンプルも検証して出せればと思います。

参考