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

システムとcondaのC++標準ライブラリ(libstdc++)のバージョン違い問題による事象と対処法解説

システムとcondaのC++標準ライブラリ(libstdc++)のバージョン違い問題による事象と対処法解説

こんにちは! 先日、dlibをつかったPythonアプリケーション(conda環境で動作する)作っていたところ、以下のようなエラーに遭遇しました。 ImportError: /home/mlu/anaconda3/envs/example_env/bin/../lib/libstdc++.so.6: version `GLIBCXX_3.4.32' not found (required by /home/mlu/anaconda3/envs/example_env/lib/python3.10/site-packages/_dlib_pybind11.cpython-310-x86_64-linux-gnu.so) 「dlib_pybind11モジュールがGLIBCXX_3.4.32を要求してるけど、みつからない!」という感じのエラーですね。

By Qualiteg プロダクト開発部
LLM推論基盤プロビジョニング講座 第2回 LLMサービスのリクエスト数を見積もる

LLM推論基盤プロビジョニング講座 第2回 LLMサービスのリクエスト数を見積もる

こんにちは! 今回はLLM推論基盤プロビジョニング講座 第2回です! STEP2 LLMサービスへのリクエスト数見積もり それでは、早速、LLM推論基盤プロビジョニングの第2ステップである「リクエスト数見積もり」の重要性と方法を解説いたします。 LLMサービスを構築する際に必要となるGPUノード数を適切に見積もるためには、まずサービスに対して想定されるリクエスト数を正確に予測する必要があります。 リクエスト数見積もりの基本的な考え方 LLMサービスへの想定リクエスト数から必要なGPUノード数を算出するプロセスは、サービス設計において非常に重要です。過小評価すればサービス品質が低下し、過大評価すれば無駄なコストが発生します。このバランスを適切に取るための基礎となるのがリクエスト数の見積もりです。 想定リクエスト数の諸元 リクエスト数を見積もるための5つの重要な要素(諸元)をみてみましょう。 1. DAU(Daily Active Users): 1日あたりの実際にサービスを利用するユーザー数です。これはサービスの規模を示す最も基本的な指標となります。 2. 1日

By Qualiteg コンサルティング
Zoom会議で肩が踊る?自動フレーミング映像安定化とAIによる性能向上の可能性

Zoom会議で肩が踊る?自動フレーミング映像安定化とAIによる性能向上の可能性

こんにちは! 本日は、自動フレーミング映像の安定化に関するアルゴリズム・ノウハウを解説いたします 第1章 問題の背景と目的 バストアップ映像を撮影する際、特にオンラインミーティングやYouTubeなどのトーク映像では、人物がうなずく、首を振るなどの自然な動作をした際に「首まわりや肩がフレーム内で上下に移動してしまう」という現象がしばしば起こります。これは、多くの場合カメラや撮影ソフトウェアが人物の「目や顔を画面中央に保とう」とする自動フレーミング機能の働きに起因します。 撮影対象の人物が頭を下げた際に、映像のフレーム全体が相対的に上方向へシフトし、その結果、本来動いていないはずの肩の部分が映像内で持ち上がっているように見えてしまう現象です。 本稿では、この問題を撮影後の後処理(ポストプロセッシング)のみを用いて、高速、高い精度かつロバストに解決する手法をご紹介します。 前半では、従来のCV(コンピュータービジョン)の手法を使い高速に処理する方法をご紹介します。後半では、AIを使用してより安定性の高い性能を実現する方法について考察します。 第2章 古典手法による肩の上下

By Qualiteg 研究部
LLM推論基盤プロビジョニング講座 第1回 基本概念と推論速度

LLM推論基盤プロビジョニング講座 第1回 基本概念と推論速度

こんにちは! 本日は LLMサービスの自社構築する際の推論基盤プロビジョニング、GPUプロビジョニングについて数回にわけて解説いたします。 はじめに LLMの進化に伴い、ChatGPTやClaudeといったパブリックなLLMの活用は企業においても急速に広がってきました。しかし先進的な企業はこれらの汎用LLMに加えて、「領域特化型」「ドメイン特化型」といった専用LLMの構築へと歩みを進めています。こうした動きの背景には、企業固有の専門知識への対応力強化と情報セキュリティの確保という二つの重要なニーズがあります。 一般的なパブリックLLMでは対応できない企業固有の専門知識や機密情報の取り扱いが必要なケースが増えているため、自社LLMの構築や自社サーバーでの運用を検討する企業が急増しています。特に金融、医療、製造、法務といった専門性の高い領域では、業界特化型の独自LLMが競争優位性をもたらすと認識されています。 しかし、業界特化型のLLMを自社で運用することは簡単ではありません。自社運用を決断した場合、まず最初に取り組むべきは適切な推論環境の整備です。オンプレミス環境を構築するに

By Qualiteg コンサルティング