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

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のモデルファイルを問題なく保存できるようになりました。

主な改善点は以下の通りです

  1. ストリーム処理
    データを小さなチャンク(1MB)に分割して処理することで、バッファサイズの制限を回避しました。
  2. 非同期処理
    async/awaitを使用することで、ファイル処理中もサーバーが他のリクエストに応答できるようになりました。
  3. プログレス表示の実装
    大きなファイルの転送過程を監視するために、チャンク単位のプログレス表示も組み込みました(コード例では省略)。

ということで、巨大ファイルを扱い、安定性を向上するためには、キャッシュ・ストリーミング・非同期での処理が非常に重要となります。

最新の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モジュールを組み合わせることで、さらに効率的な処理が可能になります

  1. CPUコア数の最適利用
    clusterモジュールでCPUコア数分のプロセスを起動
  2. ストリーム処理
    各ワーカープロセス内でチャンク単位のストリーム処理を実装
  3. 負荷分散
    大きなファイルをワーカー間で分割処理(例: 範囲ごとに担当を分ける)

まとめ

AIモデルのような大容量ファイルを扱うNode.jsアプリケーションのストリーム処理についてご紹介しました。巨大ファイルはストリーム処理と非同期I/O操作を組み合わせることで効率的に扱うことができます

それではまた次回おあいしましょう!

Read more

AIがよく間違える「クロージャ問題」の本質と対策

AIがよく間違える「クロージャ問題」の本質と対策

こんにちは! 本日は「クロージャ問題」に関する話題となります。 Pythonでループ内に関数を定義したことはありますか? もしあるなら、あれれ?な挙動に遭遇したことがあるかもしれません。 本稿では、Pythonプログラマーなら一度は経験する「クロージャ問題」について、初心者にもわかりやすく解説してみたいとおもいます クロージャとは何か? そもそも ”クロージャ” とは何でしょうか。 クロージャ(closure)とは、関数が自分の定義されたスコープの変数を覚えて持ち運ぶ仕組み のことです。 もう少し分解すると、次の2つがポイントとなります 1. 内側の関数が、外側の関数の変数を使える 2. 外側の関数が終了しても、その変数は生き続ける 普通の関数とクロージャ―を使った関数を比較してみましょう 普通の関数との比較 まずは普通の関数から、 def add(x, y): return x + y print(add(3, 5)) # 8 print(add(3, 7)

By Qualiteg プロダクト開発部
フリーランスHub様にQualiteg Blogをご紹介いただきました

フリーランスHub様にQualiteg Blogをご紹介いただきました

この度、フリーランス向け案件検索サービス「フリーランスHub」様の特集記事「トレンドをキャッチアップ!AIに関する情報が得られるメディア・ブログまとめ」にて、弊社が運営する「Qualiteg Blog」をご紹介いただきました。 掲載記事について フリーランスHub様の記事では、AI技術の最前線で活躍するエンジニアや開発者の方々に向けて、価値ある情報源となるメディア・ブログが厳選して紹介されています。 その中で、Qualiteg Blogを「AI技術の専門知識を実践的なビジネス活用につなげる貴重な情報源」として取り上げていただきました。 特に以下の点を評価いただいております * 実践的なビジネス活用事例の提供 AI新規事業創出や事業選定方法など、経営者やビジネスリーダーが直面する課題への具体的な解決策 * 技術的な深掘りコンテンツ リップシンク技術など、実際のサービスで使用されている技術の開発現場目線での詳細な解説 * 多様な情報発信 代表執筆記事、AIトピックス、講演会動画など、幅広いフォーマットでの情報提供 今後も価値ある情報発

By Qualiteg ニュース
PyTorchの重いCUDA処理を非同期化したらメモリリークした話と、その解決策

PyTorchの重いCUDA処理を非同期化したらメモリリークした話と、その解決策

こんにちは!Qualitegプロダクト開発部です! 今回は同期メソッドを非同期メソッド(async)化しただけなのに、思わぬメモリリーク※に見舞われたお話です。 深層学習モデルを使った動画処理システムを開発していた時のことです。 「処理の進捗をリアルタイムでWebSocketで通知したい」という要件があり、「単にasync/awaitを使えばいいだけでしょ?」と軽く考えていたら、思わぬ落とし穴にはまりました。 プロ仕様のGPUを使っていたにも関わらず、メモリ不足でクラッシュしてしまいました。 この記事では、その原因と解決策、そして学んだ教訓を詳しく共有したいと思います。同じような問題に直面している方の参考になれば幸いです。 ※ 厳密には「メモリリーク」ではなく「メモリの解放遅延」ですが、 実用上の影響は同じなので、この記事では便宜上「メモリリーク」と表現します。 背景:なぜ進捗通知は非同期である必要があるのか モダンなWebアプリケーションの要求 最近のWebアプリケーション開発では、ユーザー体験を向上させるため、長時間かかる処理の進捗をリアルタイムで表示することが

By Qualiteg プロダクト開発部
ゼロトラスト時代のLLMセキュリティ完全ガイド:ガーディアンエージェントへの進化を見据えて

ゼロトラスト時代のLLMセキュリティ完全ガイド:ガーディアンエージェントへの進化を見据えて

こんにちは! 今日はセキュリティの新たな考え方「ゼロトラスト」とLLMを中心としたAIセキュリティについて解説いたします! はじめに 3つのパラダイムシフトが同時に起きている いま、企業のIT環境では3つの大きな変革が起ころうとしています。 1つ目は「境界防御からゼロトラストへ」というセキュリティモデルの転換。 2つ目は「LLMの爆発的普及」による新たなリスクの出現。 そして3つ目は「AIエージェント時代の到来」とそれに伴う「ガーディアンエージェント」という新概念の登場です。 これらは別々の出来事のように見えて、実は密接に関連しています。本記事では、この3つの変革がどのように結びつき、企業がどのような対策を取るべきかを解説いたします 目次 1. はじめに:3つのパラダイムシフトが同時に起きている 2. 第1の変革:ゼロトラストという新しいセキュリティ思想 3. 第2の変革:LLM時代の到来とその影響 4. 第3の変革:AIエージェントとガーディアンエージェント 5. 3つの変革を統合する:実践的なアプローチ 6. 実装のベストプラクティス 7. 日本

By Qualiteg コンサルティング