Node.jsで大容量ファイルを扱う:AIモデルのような大きなデータ保存はストリーム処理使いましょう

こんにちは!今日はAIシステムのフロントサーバーとしてもよく使用するNode.jsについてのお話です。
AIモデルの普及に伴い、大容量のデータファイルを扱う機会が急増しています。LLMなどのモデルファイルやトレーニングデータセットは数GB、場合によっては数十、数百GBにも達することがあります。
一方、Node.jsはWebアプリケーションのフロントサーバーとして広く採用されており、データマネジメントやPythonで書かれたAIバックエンドとの橋渡し役としてもかなりお役立ちな存在です。
本記事では、Node.js v20LTSで5GB程度のファイルを処理しようとして遭遇した問題と、その解決方法について解説します。
Node.jsのバッファサイズ制限の変遷
Node.jsのバッファサイズ制限は、バージョンによって大きく変化してきました
Node.jsバージョン | サポート終了日 | バッファサイズ上限 | 備考 |
---|---|---|---|
Node.js 0.12.x | 2016年12月31日 | ~1GB | 初期のバッファサイズ制限(smalloc.kMaxLength使用) |
Node.js 4.x (Argon) | 2018年4月30日 | ~2GB | V8 4.4での書き換えにより制限が拡大 |
Node.js 6.x (Boron) | 2019年4月30日 | 32ビット符号付き整数の最大値 | |
Node.js 8.x (Carbon) | 2019年12月31日 | OpenSSL 1.0.2のEOLに合わせて早期終了 | |
Node.js 10.x (Dubnium) | 2021年4月30日 | 32ビット符号付き整数の最大値 | |
Node.js 12.x (Erbium) | 2022年4月30日 | 32ビット符号付き整数の最大値 | |
Node.js 14.x (Fermium) | 2023年4月30日 | 途中から4GBに拡大 | |
Node.js 16.x | 2023年9月11日 | ~4GB | OpenSSL 1.1.1のサポート終了に合わせてEOLが早まった |
Node.js 17.x | 2022年6月1日 | 奇数バージョンは短期サポート | |
Node.js 18.x | 2025年4月30日 | 現在メンテナンスLTSフェーズ | |
Node.js 19.x | 2023年6月1日 | 奇数バージョンは短期サポート | |
Node.js 20.x | 2026年4月30日 | 現在アクティブLTSフェーズ | |
Node.js 21.7.2 | 2024年6月1日 | ||
Node.js 21.7.3 | 2024年6月1日 | ~8TB | v21.7.3でバッファサイズ上限が大幅拡大 |
Node.js 22.x (Jod) | 2027年4月30日 | 2024年10月29日にLTSに移行 | |
Node.js 23.x | 2025年6月1日 | 奇数バージョンは短期サポート |
Node.js v20LTSでは理論上は4GBまでのバッファを扱えるようになっていますが、I/O操作(ファイルの読み書き)における制限が依然として存在します。これはNode.js自体ではなく、その下層で動作するlibuv(非同期I/Oライブラリ)の制限によるものです。
実際に遭遇した問題:5GBのAIモデルファイル
あるプロジェクトで、5GBのAIモデルファイルをモデル管理サーバーとして使っているNode.js v20 LTSを経由して保存しようとした際、以下のコードを使用しました:
save_file(target_dir, file_name, file_buffer) {
try {
// 保存先ディレクトリが存在しない場合は作成
if (!fs.existsSync(target_dir)) {
fs.mkdirSync(target_dir, { recursive: true });
}
const file_path = path.join(target_dir, file_name);
fs.writeFileSync(file_path, file_buffer);
return true;
} catch (error) {
console.error(`ファイル保存エラー: ${error.message}\n${error.stack}`);
return false;
}
}
すると、以下のようなエラーが発生しました
ファイル保存エラー: The value of "length" is out of range. It must be >= 0 && <= 4294967295. Received 5368709120
このエラーは、Node.js v20LTSのバッファ制限が4GBであるのに対し、我々が扱おうとしていたファイルは5GB(5,368,709,120バイト)だったことを示しています。
こうやって無邪気なコードをかきましたが、巨大ファイルをこのような方法で保存するのはいただけないです。
エラーのとおり、5GBのファイルを一度に処理することはできないことが分かります。
(5GBならかわいいもんですが、素人が数百GBクラスのモデルデータをあつかうと、通常のコードは何でもなかったコードが一斉に不具合に見舞われたりします。)
解決策:ストリーム処理と非同期I/O
さて、この問題を解決するために、ストリーム処理と非同期I/Oを採用したアプローチに切り替えました
async save_file(target_dir, file_name, input_data) {
try {
// 保存先ディレクトリが存在しない場合は作成(非同期版)
await fs.promises.mkdir(target_dir, { recursive: true });
const file_path = path.join(target_dir, file_name);
// ストリームを使用してファイルを書き込む
const writeStream = fs.createWriteStream(file_path);
// Bufferの場合
if (Buffer.isBuffer(input_data)) {
// チャンクに分割して書き込む
const chunkSize = 1024 * 1024; // 1MBずつ
for (let i = 0; i < input_data.length; i += chunkSize) {
const chunk = input_data.slice(i, Math.min(i + chunkSize, input_data.length));
writeStream.write(chunk);
}
writeStream.end();
}
// ストリームの場合
else if (typeof input_data.pipe === 'function') {
input_data.pipe(writeStream);
}
// その他の場合(文字列など)
else {
writeStream.write(input_data);
writeStream.end();
}
// 完了または失敗を待機する
await new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
return true;
} catch (error) {
console.error(`ファイル保存エラー: ${error.message}\n${error.stack}`);
throw error; // asyncメソッドなのでthrowを使う
}
}
この改善版コードを使って5GBのモデルファイルを問題なく保存できるようになりました。
主な改善点は以下の通りです
- ストリーム処理
データを小さなチャンク(1MB)に分割して処理することで、バッファサイズの制限を回避しました。 - 非同期処理
async/await
を使用することで、ファイル処理中もサーバーが他のリクエストに応答できるようになりました。 - プログレス表示の実装
大きなファイルの転送過程を監視するために、チャンク単位のプログレス表示も組み込みました(コード例では省略)。
ということで、巨大ファイルを扱い、安定性を向上するためには、キャッシュ・ストリーミング・非同期での処理が非常に重要となります。
最新のNode.js(2025年4月時点でv.23)でも注意が必要
さて、Node.js v22以降では理論上8TBまでのバッファを扱えるようになりますが、実際のI/O操作ではまだ制限があるため、大きなファイルを扱う際にはどのバージョンでもストリーム処理を採用することがおすすめです。
(おまけ)さらに、マルチコアを活かすことで、パフォーマンス向上・最適化
Node.jsは単一スレッドで動作するため、CPUバウンドな処理を行う場合、マルチコアのパフォーマンスを活かしきれません。これを解決するのがcluster
モジュールです。
今回のように単純なファイル保存の場合、基本的に単一ファイルへの書き込みはI/Oバウンドな処理で、OSのファイルシステムによって直列化されますので、複数のプロセスからの保存には実はそんなに意味がありません。まして、同じファイルに同時に書き込むと、ファイルシステムのロックやシークポインタの競合が発生し、むしろパフォーマンスが低下する可能性すらあります。
ただ、ファイルに対して一定の処理を行ったりする場合には、マルチコアにすることで、パフォーマンスを向上できる可能性もありますので、ご紹介します。
cluster モジュールの基本的な使い方
import cluster from 'node:cluster';
import http from 'node:http';
import { cpus } from 'node:os';
import process from 'node:process';
const numCPUs = cpus().length;
if (cluster.isPrimary) {
console.log(`メインプロセス ${process.pid} 実行中`);
// CPUコア数分のワーカーを起動
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`ワーカー ${worker.process.pid} が終了しました`);
// 必要に応じてワーカーを再起動
cluster.fork();
});
} else {
// ワーカーは同じポートでHTTPサーバーを起動
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World\n');
}).listen(8000);
console.log(`ワーカー ${process.pid} 起動完了`);
}
大容量ファイル処理での最適化の組み合わせ
大容量ファイル+何等かな処理(CPUバウンドな)を扱う場合は、ストリーム処理とclusterモジュールを組み合わせることで、さらに効率的な処理が可能になります
- CPUコア数の最適利用
cluster
モジュールでCPUコア数分のプロセスを起動 - ストリーム処理
各ワーカープロセス内でチャンク単位のストリーム処理を実装 - 負荷分散
大きなファイルをワーカー間で分割処理(例: 範囲ごとに担当を分ける)
まとめ
AIモデルのような大容量ファイルを扱うNode.jsアプリケーションのストリーム処理についてご紹介しました。巨大ファイルはストリーム処理と非同期I/O操作を組み合わせることで効率的に扱うことができます
それではまた次回おあいしましょう!