Node.jsはシングルスレッド(イベントループ)で動作するため、CPU負荷の高い処理(暗号化、画像・動画解析、大規模なデータ集計など)を実行すると、イベントループがブロックされ、APIのレスポンス遅延やシステム全体のパフォーマンス低下を引き起こします。
この記事では、重い処理をメインプロセスから分離するための2つの主要なアプローチである「Worker Threads」と「メッセージキュー(BullMQ)」について、具体的な選定基準、実装コード例、および実務での注意点を解説します。
この記事で分かること
- Worker Threadsとメッセージキューの使い分け基準(比較表)
- Worker Threadsを用いたCPU負荷処理の分離実装
- BullMQ(Redis)を用いた非同期ジョブキューの実装
- 実務導入時の注意点とトラブルシューティング
対象読者・前提条件
- Node.js(Express, NestJSなど)でバックエンドAPIを開発しているエンジニア
- サービスのスケールに伴い、重い処理のバックグラウンド化を検討している方
- Node.js v18以降、Redisが利用可能な環境を想定しています。
Worker Threads と メッセージキューの比較
どちらの手法を採用すべきかは、処理の性質やインフラ構成によって異なります。
| 比較項目 | Worker Threads | メッセージキュー (例: BullMQ + Redis) |
|---|---|---|
| 主な目的 | 同一インスタンス内でのマルチスレッド化 | プロセス・サーバー間での非同期分散処理 |
| 適した処理 | 短時間で終わるCPU集中的な計算(画像リサイズ、暗号化など) | 時間がかかる処理、外部API連携、リトライが必要なバッチ処理 |
| リソース共有 | メモリ(ArrayBufferなど)を共有可能 | メモリ共有不可(シリアライズされたデータ転送) |
| スケーラビリティ | 単一サーバーのCPUコア数に依存 | 複数サーバー(Workerプロセス)への水平分散が可能 |
| 耐障害性 | プロセスがクラッシュするとWorkerも停止 | ジョブがRedisに永続化され、再起動後にリトライ可能 |
| 導入コスト | 低(外部ミドルウェア不要) | 中〜高(Redisの構築・運用が必要) |
パターン1:Worker ThreadsによるCPU負荷処理の分離
同一サーバー内でCPUリソースを効率的に活用したい場合、標準モジュールの worker_threads を使用します。
実装例:重い計算処理(素数判定など)の分離
以下は、メインプロセスからWorkerを起動し、処理を委譲する実装例です。
1. worker.js(ワーカースレッド側)
const { parentPort, workerData } = require('worker_threads');
// 重い計算処理の例
function heavyComputation(limit) {
let count = 0;
for (let i = 2; i <= limit; i++) {
let isPrime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
}
return count;
}
// メインプロセスからのデータを受け取り処理を実行
const result = heavyComputation(workerData.limit);
// 結果をメインプロセスに送信
parentPort.postMessage({ success: true, result });
2. main.js(メインプロセス側)
const { Worker } = require('worker_threads');
const path = require('path');
function runWorker(limit) {
return new Promise((resolve, reject) => {
const worker = new Worker(path.resolve(__dirname, 'worker.js'), {
workerData: { limit }
});
worker.on('message', (message) => {
resolve(message.result);
});
worker.on('error', (error) => {
reject(error);
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
}
// Expressのハンドラーなどでの利用イメージ
async function handleRequest(req, res) {
try {
const limit = parseInt(req.query.limit, 10) || 1000000;
const result = await runWorker(limit);
res.json({ result });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
パターン2:BullMQによる非同期ジョブキュー処理
処理時間が数秒〜数分以上に及ぶ場合や、リトライ制御、レートリミット、複数サーバーへの負荷分散が必要な場合は、Redisをバックエンドとした BullMQ などのメッセージキューを採用します。
実装例:非同期タスクのキュー登録と処理
事前に npm install bullmq ioredis でライブラリをインストールし、Redisサーバーを起動しておいてください。
1. queue.js(ジョブの登録側)
const { Queue } = require('bullmq');
const IORedis = require('ioredis');
const connection = new IORedis({ host: 'localhost', port: 6379 });
const reportQueue = new Queue('report-generation', { connection });
async function addReportJob(userId, reportType) {
// ジョブをキューに追加(即座にレスポンスを返す)
const job = await reportQueue.add('generate', {
userId,
reportType,
createdAt: new Date().toISOString()
}, {
attempts: 3, // 失敗時のリトライ回数
backoff: {
type: 'exponential',
delay: 5000 // リトライ間隔(5秒から指数関数的に増加)
}
});
return job.id;
}
module.exports = { addReportJob };
2. worker-process.js(ジョブの処理側 / 別プロセス)
const { Worker } = require('bullmq');
const IORedis = require('ioredis');
const connection = new IORedis({ host: 'localhost', port: 6379 });
const worker = new Worker('report-generation', async (job) => {
console.log(`Job ${job.id} processing for user ${job.data.userId}...`);
// 実際の重い処理(PDF生成や外部API連携など)をシミュレート
await new Promise((resolve) => setTimeout(resolve, 10000));
console.log(`Job ${job.id} completed successfully.`);
return { downloadUrl: `https://example.com/reports/${job.id}.pdf` };
}, { connection });
worker.on('failed', (job, err) => {
console.error(`Job ${job.id} failed with error: ${err.message}`);
});
導入時の注意点とトラブルシューティング
Worker Threadsの注意点
-
スレッド生成のオーバーヘッド
リクエストのたびにnew Worker()を実行すると、スレッド起動のオーバーヘッドにより逆にパフォーマンスが低下します。実務では、スレッドを再利用する「スレッドプール(例:piscinaライブラリなど)」の導入を検討してください。 -
I/O処理の混在
Worker ThreadsはCPU負荷処理に特化しています。データベースアクセスや外部API呼び出しなどのI/O待ちが多い処理は、Worker Threadsを使ってもパフォーマンス向上は限定的です。非同期I/O(async/await)を適切に使用するか、メッセージキューに逃がす方が適しています。
メッセージキュー(BullMQ)の注意点
-
データのシリアライズ制限
キューに渡す引数(job.data)は、Redisに保存するためにJSON文字列化されます。クラスのインスタンスや関数、循環参照を含むオブジェクトは直接渡せません。必要なIDやプリミティブな値のみを渡すように設計してください。 -
ジョブの重複実行への考慮
ネットワークの一時的な瞬断やプロセスの再起動により、同一のジョブが複数回実行される可能性があります。処理対象のステータスチェックを行うなど、冪等性(べきとうせい)を担保した実装にしてください。
まとめと選定チェックリスト
どちらの手法を採用すべきか迷った際は、以下のチェックリストを基準に判断してください。
-
処理時間は1秒未満か?
- Yes → Worker Threads(同一サーバー内で高速に処理を完了させる)
- No → メッセージキュー(ユーザーを待たせずにバックグラウンドで非同期処理)
-
処理が失敗した際に自動リトライさせたいか?
- Yes → メッセージキュー(指数バックオフなどの高度なリトライ制御が可能)
-
インフラ構成をシンプルに保ちたいか(外部ミドルウェアを増やしたくないか)?
- Yes → Worker Threads(Node.js標準機能のみで完結)
- No(Redisの導入・運用が可能) → メッセージキュー
-
将来的に処理専用のサーバーをスケールアウト(水平分散)させたいか?
- Yes → メッセージキュー(Workerプロセスを別サーバーに分離可能)
システムの要件と運用コストのバランスを考慮し、最適な分離手法を選択してください。