1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スケーリング Node.js:知っておくべきマルチスレッド処理

Posted at

表紙

Node.js はシングルスレッドの特性を持っており、メインスレッドはノンブロッキング I/O 操作を実行するために使用されます。しかし、CPU 負荷の高いタスクを実行する場合、シングルスレッドに依存するとパフォーマンスのボトルネックが発生する可能性があります。幸いなことに、Node.js は複数の方法でスレッドを作成・管理し、マルチコア CPU の利点を活用することができます。

サブスレッドを作成する理由

Node.js でサブスレッドを作成する主な目的は、並行タスクの処理とアプリケーションのパフォーマンス向上です。Node.js はイベントループをベースとしたシングルスレッドモデルであるため、すべての I/O 操作(ファイルの読み書きやネットワークリクエストなど)はノンブロッキングですが、CPU 負荷の高いタスク(大量の計算など)はイベントループをブロックし、アプリケーション全体のパフォーマンスに影響を与える可能性があります。

サブスレッドを作成することで、以下の問題を解決できます。

  • ノンブロッキング操作の維持: Node.js の設計哲学はノンブロッキング I/O ですが、メインスレッドで外部コマンドを直接実行すると、そのプロセスが完了するまでメインスレッドがブロックされ、アプリケーションの応答性が低下します。サブスレッドを使用してコマンドを実行すれば、メインスレッドのノンブロッキング特性を維持し、他の並行操作に影響を与えません。
  • システムリソースの有効活用: サブプロセスやワーカースレッドを利用することで、Node.js アプリケーションはマルチコア CPU の計算能力をより効果的に活用できます。特に、CPU 負荷の高い外部コマンドを実行する場合、それらを別の CPU コアで処理することで、Node.js のメインイベントループの負担を軽減できます。
  • 分離と安全性の向上: 外部コマンドをサブスレッドで実行することで、アプリケーションの安全性を高めることができます。例えば、外部コマンドがクラッシュした場合でも、メインの Node.js プロセスには影響を与えず、アプリケーションの安定性を確保できます。
  • 柔軟なデータ処理と通信: サブスレッドを使用すると、外部コマンドからの出力を効率的に処理し、結果をメインプロセスに渡すことができます。Node.js にはプロセス間通信(IPC)を実現するための多くの方法が用意されており、データ交換をスムーズに行うことができます。

Node.js におけるサブスレッドの作成方法

ここからは、Node.js でサブスレッドを作成するいくつかの方法を紹介します。

Child Processes(子プロセス)

Node.js の child_process モジュールを使用すると、システムコマンドや他のプログラムを実行するためのサブプロセスを作成できます。これにより、CPU 負荷の高いタスクを処理したり、外部プログラムを実行したりできます。

spawn

child_process.spawn() メソッドは、新しい子プロセスを作成して指定されたコマンドを実行するために使用されます。このメソッドは stdoutstderr のストリームを返し、それらを使用してプロセスと対話できます。特に、大量の出力データを処理する長時間実行のプロセスに適しています。なぜなら、データをバッファに保持するのではなく、ストリームとして逐次処理できるためです。

spawn() の基本的な構文は次のとおりです。

const { spawn } = require('child_process');
const child = spawn(command, [args], [options]);
  • command: 実行するコマンドの文字列
  • args: コマンドライン引数の配列
  • options: 子プロセスの作成方法を指定するオプションオブジェクト
    • cwd: 子プロセスのカレントディレクトリ
    • env: 環境変数のオブジェクト
    • stdio: 標準入出力の設定(パイプ処理やファイルリダイレクトに使用)
    • shell: true にすると、シェル内でコマンドを実行(Unix では /bin/sh、Windows では cmd.exe
    • detached: true にすると、親プロセスとは独立して実行

以下は spawn() を使用した簡単な例です。

const { spawn } = require('child_process');
const path = require('path');

// touch コマンドを使用して moment.txt を作成
const touch = spawn('touch', ['moment.txt'], {
  cwd: path.join(process.cwd(), './m'),
});

touch.on('close', (code) => {
  if (code === 0) {
    console.log('ファイルの作成に成功しました');
  } else {
    console.error(`ファイル作成時にエラーが発生しました。終了コード: ${code}`);
  }
});

このコードは、カレントディレクトリの m サブディレクトリ内に moment.txt という空のファイルを作成します。作成に成功すると、コンソールに「ファイルの作成に成功しました」と表示されます。ディレクトリ m が存在しない場合など、エラーが発生すると適切なエラーメッセージが表示されます。

exec

Node.js の child_process.exec() メソッドは、指定されたコマンドを実行し、その出力をバッファに保存するために使用されます。spawn() とは異なり、exec() はコマンドの出力をバッファに格納するため、大量の出力を伴う処理には適していません。

exec() の基本的な構文は次のとおりです。

const { exec } = require('child_process');

exec(command, [options], callback);
  • command: 実行するコマンドの文字列
  • options: 実行環境をカスタマイズするオプション
  • callback: コマンドの実行完了後に呼び出される関数 (error, stdout, stderr)

exec()options オブジェクトには、次のような設定を含めることができます。

  • cwd: 子プロセスのカレントディレクトリ
  • env: 環境変数オブジェクト
  • encoding: 出力のエンコーディング(デフォルトは utf8
  • shell: 使用するシェル(デフォルトは Unix では /bin/sh、Windows では cmd.exe
  • timeout: タイムアウト時間(ミリ秒単位)。この時間を超えると子プロセスが強制終了される
  • maxBuffer: stdoutstderr の最大バッファサイズ(デフォルトは 1MB
  • killSignal: 子プロセスを終了する際に送信するシグナル(デフォルトは SIGTERM

exec() の使用例

const { exec } = require('child_process');
const path = require('path');

// コマンドの指定(touch を使用して moment.txt を作成)
const command = `touch ${path.join('./m', 'moment.txt')}`;

exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => {
  if (error) {
    console.error(`コマンド実行中にエラーが発生しました: ${error}`);
    return;
  }
  if (stderr) {
    console.error(`標準エラー出力: ${stderr}`);
    return;
  }
  console.log('ファイルの作成に成功しました');
});

このコードを実行すると、指定されたディレクトリ内に moment.txt が作成されます。

fork()

child_process.fork() メソッドは、Node.js の child_process モジュールの一部であり、新しい Node.js プロセスを作成し、親プロセスとの IPC(プロセス間通信)チャンネルを提供します。fork() は特に Node.js モジュールを実行する用途に適しており、マルチコア CPU を活用した並列処理を実現するのに便利です。

fork() の基本構文

const { fork } = require('child_process');

const child = fork(modulePath, [args], [options]);
  • modulePath: 子プロセスで実行するモジュールのパス
  • args: 子プロセスに渡す引数の配列
  • options: 子プロセスの作成方法を指定するオプションオブジェクト

オプション options の主なプロパティ:

  • cwd: 子プロセスのカレントディレクトリ
  • env: 環境変数オブジェクト
  • execPath: 実行する Node.js のパス
  • execArgv: Node.js の起動オプション
  • silent: true にすると、stdoutstderr を親プロセスで取得可能
  • stdio: 標準入出力の設定
  • ipc: IPC(プロセス間通信)チャンネルを作成するかどうか(デフォルトは true

fork() を使った親子プロセスの通信

親プロセス (index.js)

const { fork } = require('child_process');

const child = fork('./child.js');

child.on('message', (message) => {
  console.log('子プロセスからのメッセージ:', message);
});

child.send({ hello: 'world' });

setInterval(() => {
  child.send({ hello: 'world' });
}, 1000);

子プロセス (child.js)

process.on('message', (message) => {
  console.log('親プロセスからのメッセージ:', message);
});

process.send({ foo: 'bar' });

setInterval(() => {
  process.send({ hello: 'world' });
}, 1000);

この例では、親プロセス index.jschild.js をフォークして、新しい Node.js インスタンスを作成しています。親プロセスと子プロセスは message イベントを使って通信し、定期的にメッセージを送信します。

fork() で作成される各子プロセスは、独立した V8 インスタンスとイベントループを持つため、大量の子プロセスを生成するとシステムリソースを大きく消費する可能性があります。

Worker Threads(ワーカースレッド)

worker_threads モジュールは、Node.js のシングルスレッドの制約を克服し、CPU 負荷の高いタスクをマルチスレッドで処理するために導入されました。これにより、Node.js アプリケーションは複数の CPU コアを活用し、計算集約型の処理を並行して実行できます。

Worker Threads の基本概念

  • Worker(ワーカー): 独立したスレッドで JavaScript コードを実行する単位です。それぞれのワーカーは独自の V8 インスタンスを持ち、メインスレッドとは分離されています。
  • メインスレッド: ワーカーを作成するスレッドであり、通常の Node.js 実行環境を指します。
  • 通信: メインスレッドとワーカーはメッセージを介してデータを交換します。これにより、異なるスレッド間でデータのやり取りが可能になります。

Worker Threads の基本的な使い方

以下は、worker_threads を使用してワーカースレッドを作成し、メインスレッドとメッセージをやり取りする例です。

const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  // メインスレッド側の処理
  const worker = new Worker(__filename);

  worker.on('message', (message) => {
    console.log('ワーカーからのメッセージ:', message);
  });

  worker.postMessage('Hello Worker!');
} else {
  // ワーカー側の処理
  parentPort.on('message', (message) => {
    console.log('メインスレッドからのメッセージ:', message);
    parentPort.postMessage('Hello Main Thread!');
  });
}

このコードでは、isMainThread を使ってスクリプトがメインスレッドで実行されているかどうかを判断し、それに応じて異なる処理を実行します。

worker_threads と fork の違い

基本概念の違い

  • worker_threads: worker_threads モジュールに属し、Node.js 内で複数のスレッドを作成して並列処理を可能にする仕組みです。スレッド間でメモリを共有でき、特に CPU 集約型の処理に適しています。
  • fork: child_process.fork() によって作成される子プロセスは、親プロセスとは独立した V8 インスタンスを持ち、完全に分離された環境で動作します。プロセス間のデータ通信は IPC(プロセス間通信)を使用します。

通信の違い

  • worker_threads: メインスレッドとワーカーは MessagePort を通じてデータをやり取りします。JavaScript のオブジェクトや ArrayBuffer などを転送できます。
  • fork: fork() で作成された子プロセスは process.send() メソッドを使用して親プロセスとメッセージをやり取りします。通信には JSON 形式のデータを使用します。

メモリとパフォーマンスの違い

  • worker_threads: スレッド間で SharedArrayBuffer を使用してメモリを共有できるため、メモリ使用量が少なくなります。CPU 集約型の処理に適しています。
  • fork: 子プロセスごとに独立した V8 インスタンスが作成されるため、メモリ消費が大きくなりがちです。

適用シナリオ

  • worker_threads: 計算負荷の高い処理や、大量のデータを処理するタスクに適しています。例えば、画像処理やデータ圧縮、機械学習の計算などに向いています。
  • fork: 独立したプロセスが必要な場合や、完全に分離された環境で Node.js アプリを実行する場合に適しています。例えば、複数の API サーバーを並行して動作させる場合などです。

Cluster(クラスタ)

Node.js の cluster モジュールを使用すると、1 つのサーバープロセスを複数の子プロセスに分割し、負荷分散を実現できます。これにより、マルチコア CPU を活用し、サーバーのスループットを向上させることが可能です。

Cluster モジュールの基本原理

cluster モジュールを使用すると、メインプロセス(マスタープロセス)が複数の ワーカープロセス を生成し、それぞれが同じサーバーをリッスンできます。これにより、Node.js のシングルスレッドモデルの制約を克服し、リクエストの処理能力を向上させることができます。

内部的には、child_process.fork() を使用してワーカープロセスを作成します。ワーカープロセスは、親プロセスとの IPC を介してメッセージを送受信しながら動作します。

Cluster の使用例

以下の例では、CPU コアの数に応じて複数のワーカープロセスを生成し、それぞれが HTTP サーバーを動作させます。

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`マスタープロセス ${process.pid} が実行中`);

  // CPU コアの数だけワーカープロセスを生成
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker) => {
    console.log(`ワーカープロセス ${worker.process.pid} が終了しました`);
  });
} else {
  // ワーカープロセスごとに HTTP サーバーを作成
  http
    .createServer((req, res) => {
      res.writeHead(200);
      res.end('Hello World\n');
    })
    .listen(8000);

  console.log(`ワーカープロセス ${process.pid} が起動しました`);
}

このコードを実行すると、複数のワーカープロセスが作成され、それぞれが同じポート 8000 で HTTP リクエストを受け付けます。リクエストはワーカープロセスに均等に分配され、並行処理が可能になります。

Cluster の利点と注意点

  • メリット:

    • マルチコア CPU を活用し、Node.js のスループットを向上させる
    • プロセスごとに独立した環境を持ち、メモリリークなどがあっても影響を最小限に抑えられる
    • 1 つのプロセスがクラッシュしても、他のワーカープロセスが正常に動作するため、安定性が向上する
  • デメリット:

    • ワーカープロセス間の通信やデータ共有には工夫が必要
    • プロセス管理が複雑になり、開発コストが増加する
    • ワーカープロセスごとにメモリを消費するため、大量のワーカープロセスを作成するとリソース不足になる可能性がある

まとめ

Node.js では、child_processspawn, exec, fork)、worker_threadscluster などの機能を活用することで、並列処理を実現し、アプリケーションのパフォーマンスを向上させることができます。

  • CPU 負荷の高いタスクworker_threads を使用
  • 完全に独立したプロセスfork() を使用
  • シンプルな外部コマンド実行exec() または spawn() を使用
  • マルチコア CPU を活用した負荷分散cluster を使用

適切な手法を選択することで、Node.js のパフォーマンスを最大限に引き出すことができます。


私たちはLeapcell、Node.jsプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?