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

AGI時代に向けたプログラマーの未来:役割変化とキャリア戦略

AGI時代に向けたプログラマーの未来:役割変化とキャリア戦略

はじめに 私がはじめてコードを書いたのは1989年です。 当時NECのPC88というパソコンを中古でかってもらい N-88 Basic というBASIC言語のコードをみようみまねで書いて動かしたあの日から何年経つのでしょうか。 当時、電波新聞社のマイコンBASICマガジンという雑誌があり、ベーマガにはいろんなパソコン向けのプログラムコードが掲載されていました。 そんなわけでもう35年以上趣味や仕事でプログラミングに従事していますが、開発環境、情報流通の仕組みには革命といっていいほどの変化、進化がおこりました。 しかしながら、そんな中でも、あくまでコードを書くのは「私」という生身の人間でした。 そうしたある種の古き良き時代は、いよいよ本格的に終わりを告げようとしています。 2023年ごろからのLLM技術の飛躍的進歩により、プログラミング業界は大きな転換期を迎えています。 特に、OpenAI o3,o1やClaude 3.5、Gemini2.0などの大規模言語モデル(LLM)の進化や、その先にある将来的な汎用人工知能(AGI)の出現は、プログラマーやAIエンジニアの役割に根

By Tomonori Misawa / CEO
PythonとWSL開発のトラブルシューティング: PyCharmとCondaの環境不一致問題

PythonとWSL開発のトラブルシューティング: PyCharmとCondaの環境不一致問題

こんにちは! 今回は、WSL上のConda環境をPyCharmから利用する際に発生した「同じ環境なのにパッケージリストが一致しない」という問題に遭遇したため、その原因と対策について書いてみたいとおもいます 問題の状況 開発の流れは以下のようなものでした 1. WSL環境でConda仮想環境を作成 2. その環境をPyCharmのプロジェクトインタプリタとして設定 3. 開発を進める中で奇妙な現象に気づく 具体的には、次のような不一致が発生していました * PyCharmのプロジェクト設定で表示されるpipパッケージのリスト * WSLでConda環境をアクティベートした後にpip listコマンドで表示されるパッケージのリスト これらが一致せず、「WSL側のシェルから直接インストールしたパッケージがPyCharmで認識されない」という問題が生じていました。 この手の問題でよくある原因は、PyCharm側がWSL側の更新を得るのに少し時間がかかったり、 Indexing が遅れているなどなのですが、今回はそれが原因ではありませんでした。 危険な「静かな

By Qualiteg プロダクト開発部
人気ゲーム「ヒット&ブロー」で学ぶ情報理論

人気ゲーム「ヒット&ブロー」で学ぶ情報理論

こんにちは! Qualiteg研究部です! 今日はAIにおいても非常に重要な情報理論について、Nintendo Switchの人気ゲーム「世界のアソビ大全51」にも収録されている「ヒット&ブロー」というゲームを題材に解説いたします! はじめに 論理的思考力を鍛える定番パズルゲームとして長年親しまれている「ヒット&ブロー」(海外では「Mastermind」として知られています)。 このゲームは一見シンプルながらも、その攻略には深い論理的アプローチが必要とされております。 本稿では、このゲームについて情報理論という数学的概念を用いてゲームの素性を分析する方法について掘り下げてみたいとおもいます。 さらに、この情報理論が現代の人工知能(AI)技術においてどのように活用されているかについても触れていきます。 ヒット&ブローのルール説明 ヒット&ブローは、相手が秘密に設定した色や数字の組み合わせを推測するゲームです。日本では主に数字を使った「数当てゲーム」として親しまれていますが、本記事では色を使ったバージョン(マスターマインド)に焦点を当てます。 Nintendo Sw

By Qualiteg 研究部
Model Context Protocol(MCP)入門:いよいよセマンティックWebの世界へ

Model Context Protocol(MCP)入門:いよいよセマンティックWebの世界へ

こんにちは! きょうは話題のMCPについて解説いたします! はじめに 「AIが便利なのはわかるけど、自分のデータにアクセスさせたり、他のアプリと連携させたりするのは難しそう...」 このような悩みを持っている方は多いのではないでしょうか。 実際、従来のAIには大きな壁がありました。トレーニングデータの範囲でしか回答できない、リアルタイム情報にアクセスできない、外部アプリケーションを操作できないなどの制約です。 トレーニングデータの外側にあるデータをうまく検索する技術としてLLM黎明期からRAGとよばれる技術が発展してきました。 データ検索だけではなく、あらゆる分野でAIが半ば自動で連携してくれる技術が登場しました。 それが「Model Context Protocol(MCP)」です。 本記事では、AIと外部ツールの連携を革新的に簡単にするMCPについて、基本から実用まで詳しく解説します。 MCPの本質:AIのための標準インターフェース MCPは、AIモデルと外部ツール・アプリケーションの間の通信を標準化するプロトコルです。これはインターネットの世界でいえば、

By Qualiteg プロダクト開発部