Skip to content

Node.jsの実行モデルと前提

Node.jsの実行モデルと、実務で事故を防ぐための前提条件を詳しく解説します。

実行モデルとリソースの物理的制約

Section titled “実行モデルとリソースの物理的制約”

コンピュータ資源は有限であり、性能ではなく制約を前提に設計することが基本です。

CPU・メモリよりも先に枯渇するリソース:

  1. DB・外部APIのコネクション数

    • 接続プールの上限(例: pgmax: 10
    • 接続リークは数時間後にシステム全体を停止させる
  2. イベントループのブロック

    • 同期I/O操作により、イベントループがブロックされる
    • すべてのリクエストが処理できなくなる
  3. ファイル記述子

    • OSレベルの制限(通常1024〜65536)
    • ファイルやソケットを適切にクローズしないと枯渇
  4. メモリリーク

    • イベントリスナーが削除されない
    • クロージャーで参照が保持される

実際の事故例:

10:00:00 - アプリケーション起動(イベントリスナー: 0)
10:00:01 - リクエスト1受信(イベントリスナー1を追加)
10:00:02 - リクエスト2受信(イベントリスナー2を追加)
...
10:30:00 - リクエスト1000受信(イベントリスナー1000を追加)
10:30:01 - メモリ使用量: 2GB
10:30:02 - OutOfMemoryError発生
10:30:03 - アプリケーションがクラッシュ

イベントループとシングルスレッド

Section titled “イベントループとシングルスレッド”

実行モデル:

Node.jsプロセス
├─ イベントループ(シングルスレッド)
│ ├─ タイマー
│ ├─ ペンディングコールバック
│ ├─ ポール(I/O操作)
│ ├─ チェック
│ └─ クローズコールバック
└─ Worker Threads(オプション)

重要な特徴:

  1. シングルスレッド: メインスレッドは1つだけ(イベントループがブロックされると全体が停止)
  2. 非ブロッキングI/O: I/O操作は非ブロッキング(同期I/Oは避ける)
  3. イベント駆動: イベントループによる非同期処理
  4. メモリ管理: ガベージコレクションがあるが、参照が保持されている場合は動作しない

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: 宣言的トランザクション管理
@Transactional
public 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();
});
環境特徴主なリスク
Serverless (Lambda/Vercel)短寿命・自動スケールコールドスタート、接続バースト、DBパンク、実行時間制限(Lambda: 15分、Vercel: 300秒)
常駐プロセス (Express/Next.js)長寿命・安定動作メモリリーク、イベントリスナーの蓄積、イベントループのブロック

制約:

// ❌ 悪い例: 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' });
}

特徴:

// 常駐プロセス環境での実行
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(メモリリーク・イベントリスナーの蓄積に注意)

重要な原則: 性能ではなく制約を前提に設計する。イベントループのブロックやメモリリークは数時間後にシステム全体を停止させる。