Node.jsの実行モデルと前提
Node.jsの実行モデルと前提
Section titled “Node.jsの実行モデルと前提”Node.jsの実行モデルと、実務で事故を防ぐための前提条件を詳しく解説します。
実行モデルとリソースの物理的制約
Section titled “実行モデルとリソースの物理的制約”コンピュータ資源は有限であり、性能ではなく制約を前提に設計することが基本です。
主な物理的制約
Section titled “主な物理的制約”CPU・メモリよりも先に枯渇するリソース:
-
DB・外部APIのコネクション数
- 接続プールの上限(例:
pgのmax: 10) - 接続リークは数時間後にシステム全体を停止させる
- 接続プールの上限(例:
-
イベントループのブロック
- 同期I/O操作により、イベントループがブロックされる
- すべてのリクエストが処理できなくなる
-
ファイル記述子
- OSレベルの制限(通常1024〜65536)
- ファイルやソケットを適切にクローズしないと枯渇
-
メモリリーク
- イベントリスナーが削除されない
- クロージャーで参照が保持される
実際の事故例:
10:00:00 - アプリケーション起動(イベントリスナー: 0)10:00:01 - リクエスト1受信(イベントリスナー1を追加)10:00:02 - リクエスト2受信(イベントリスナー2を追加)...10:30:00 - リクエスト1000受信(イベントリスナー1000を追加)10:30:01 - メモリ使用量: 2GB10:30:02 - OutOfMemoryError発生10:30:03 - アプリケーションがクラッシュNode.jsの実行モデル
Section titled “Node.jsの実行モデル”イベントループとシングルスレッド
Section titled “イベントループとシングルスレッド”実行モデル:
Node.jsプロセス├─ イベントループ(シングルスレッド)│ ├─ タイマー│ ├─ ペンディングコールバック│ ├─ ポール(I/O操作)│ ├─ チェック│ └─ クローズコールバック└─ Worker Threads(オプション)重要な特徴:
- シングルスレッド: メインスレッドは1つだけ(イベントループがブロックされると全体が停止)
- 非ブロッキングI/O: I/O操作は非ブロッキング(同期I/Oは避ける)
- イベント駆動: イベントループによる非同期処理
- メモリ管理: ガベージコレクションがあるが、参照が保持されている場合は動作しない
トランザクション境界
Section titled “トランザクション境界”Node.jsのトランザクション管理:
// Prismaでのトランザクション管理async function createOrder(orderData: OrderData) { return await prisma.$transaction(async (tx) => { // トランザクション内の処理 const order = await tx.order.create({ data: orderData }); await tx.inventory.updateMany({ where: { productId: { in: orderData.productIds } }, data: { stock: { decrement: 1 } }, }); // すべて成功するか、すべてロールバック return order; });}特徴:
- 明示的なトランザクション境界:
prisma.$transactionで明示 - コールバック形式: トランザクション内の処理をコールバックで記述
- 自動ロールバック: エラー時に自動的にロールバック
他言語との比較:
// Java: 宣言的トランザクション管理@Transactionalpublic Order createOrder(OrderData orderData) { // アノテーションでトランザクションを管理 Order order = orderRepository.save(new Order(orderData)); return order;}Node.jsの非同期処理:
// Promise/async-awaitを使用async function fetchData() { try { const data = await fetch('https://api.example.com/data'); return await data.json(); } catch (error) { // エラーハンドリング throw error; }}特徴:
- 信頼できる非同期: Promise/async-awaitによる制御
- エラーハンドリング: try-catchでエラーを処理
- 再実行: 手動で実装する必要がある
他言語との比較:
// Java: CompletableFutureを使用CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return externalApi.call();});実行環境による特性
Section titled “実行環境による特性”| 環境 | 特徴 | 主なリスク |
|---|---|---|
| Serverless (Lambda/Vercel) | 短寿命・自動スケール | コールドスタート、接続バースト、DBパンク、実行時間制限(Lambda: 15分、Vercel: 300秒) |
| 常駐プロセス (Express/Next.js) | 長寿命・安定動作 | メモリリーク、イベントリスナーの蓄積、イベントループのブロック |
Serverless環境での実行
Section titled “Serverless環境での実行”制約:
// ❌ 悪い例: Serverless環境で問題のあるコードexport default async function handler(req: Request) { // 問題: 長時間実行される可能性がある // 問題: トランザクションが長時間保持される // 問題: 接続プールが適切に管理されない const order = await createOrder(req.body); return Response.json(order);}問題点:
- 実行時間の制限: Lambdaは最大15分、Vercelは最大300秒
- コールドスタート: Node.jsの起動に時間がかかる(100-500ms)
- メモリ制限: メモリ使用量に制限がある(Lambda: 128MB〜10GB)
- 接続バースト: スケールアウト時に接続プールが急増し、DBがパンクする可能性
解決策:
// ✅ 良い例: Serverless環境に適したコードexport default async function handler(req: Request) { // 1. バリデーション(短時間) validateOrderData(req.body);
// 2. 注文を作成(短時間) const order = await createOrder(req.body);
// 3. 非同期処理をキューに投入 await messageQueue.send('order.created', { orderId: order.id });
// 4. 即座にレスポンスを返す return Response.json({ orderId: order.id, status: 'PROCESSING' });}常駐プロセス環境での実行
Section titled “常駐プロセス環境での実行”Express.jsアプリケーション
Section titled “Express.jsアプリケーション”特徴:
// 常駐プロセス環境での実行const express = require('express');const app = express();
app.listen(3000, () => { console.log('Server is running on port 3000');});メリット:
- 長時間実行可能: 実行時間の制限がない
- 接続プール: データベース接続プールを保持
- キャッシュ: メモリキャッシュを保持
- バックグラウンド処理: Worker Threadsによる処理
実装例:
// ✅ 良い例: 常駐プロセス環境に適したコードimport { Worker } from 'worker_threads';
app.post('/orders', async (req, res) => { // トランザクション内で処理 const order = await createOrder(req.body);
// バックグラウンド処理をWorker Threadで実行 const worker = new Worker('./process-order.js', { workerData: { orderId: order.id }, });
res.json({ orderId: order.id, status: 'PROCESSING' });});Node.jsの実行モデルと前提のポイント:
- リソースの物理的制約: CPU・メモリよりも先に枯渇するのは、DB接続数・イベントループのブロック・ファイル記述子・メモリリーク
- イベントループ: シングルスレッド、非ブロッキングI/O(同期I/Oは避ける)
- トランザクション境界:
prisma.$transactionで明示、コールバック形式 - 非同期処理: Promise/async-awaitによる制御、エラーハンドリングが必要
- Serverless環境: 実行時間制限、コールドスタート、メモリ制限、接続バースト
- 常駐プロセス環境: 長時間実行可能、接続プール、キャッシュ、Worker Threads(メモリリーク・イベントリスナーの蓄積に注意)
重要な原則: 性能ではなく制約を前提に設計する。イベントループのブロックやメモリリークは数時間後にシステム全体を停止させる。