Node.jsを使い始めた当初、一つ気になるところがありました:
どうして単一スレッドのシステムが同時に数千のリクエストを処理できるのか?
矛盾しているように聞こえます。しかし、イベントループを正しく理解した後(特に公式ドキュメントから)、すべてがピンときました。このブログは、深みを損なわず、できるだけ簡単に説明する私の試みです。
「シングルスレッド」の実際の意味とは
Node.jsはしばしばシングルスレッドと呼ばれますが、その記述は不完全です。
- JavaScriptの実行は一つのメインスレッド
- 上で行われますが、Node.js自体は一度に一つの操作に限定されません
-
それには以下のものを使用します:
- OSカーネル
- バックグラウンドスレッド(libuv)
- 非同期I/O
→ 正しい表現は以下の通りです:
Node.jsはJavaScriptの実行において単スレッドですが、I/Oの処理において
マルチシステムです
この区別がすべてです。__JHSNS_SEG_92f3a4cf_21__ 核心アイデア:イベント駆動型、非ブロッキングアーキテクチャ
Node.jsはタスクの完了を待たないです
代わりに、このパターンに従います
- リクエストを受け取る
- タスクを開始する(DBコール、ファイル読み込み、APIコール)
- 待たない
- 次のリクエストに移る
- 結果が準備できたときに後で戻ってくる
これを呼びます非同期I/O.
→ 官方ドキュメントより
Node.jsは可能な限り作業をシステムにオフロードするため、メインタスクリプトは解放されます
こんな感じで考えてみてください(簡単な類推)
想像してみてください
- あなたはウェイター(イベントループ)です
- キッチン=OS/バックグラウンドワーカー
あなた:
- 注文を取る
- 厨房に渡す
- 完成した料理を出す
あなたは:
- 自分で調理しない
- 注文を待って無駄になる
これがNode.jsがスケールする正確な方法です
深掘り:イベントループのフェーズ
イベントループは単なるキューではありません。フェーズで動作します はそれぞれ特定のタイプのコールバックを処理しています。
アーキテクチャ:
メインフェーズ:
- タイマー
setTimeout()とsetInterval()
- からのコールバックを実行します。
- システムレベルのコールバック(TCPエラーなど)を処理します。
- アイドル / 準備
- 内部使用(私たちが扱うものではありません)
- アンケートフェーズ(最も重要)
- 新しいI/Oイベントを取得
- I/Oコールバックを実行
- 何もしない場合は待機
- チェックフェーズ
setImmediate()のコールバックを実行
- クローズコールバック
- クリーンアップコールバックを実行(
socket.on('close')など)
┌───────────────────────────┐
│ timers │
└─────────────┬─────────────┘
│
v
┌───────────────────────────┐
┌─>│ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ timers │
└───────────────────────────┘
→ このループはこれらを継続的に巡回します.
ポーリングフェーズが心臓である理由
魔法が起こる場所です.
- 入力リクエストがここで処理されます
- 完了した非同期タスクがここに戻ります
- 保留がない場合 → ノードは効率的に待機します
→ それがNode.jsがCPUサイクルを無駄にしない理由で、非常にスケーラブルであるためです。
process.nextTick() と setImmediate()
これは小さいけれど重要な詳細をほとんどの人が無視していることです。
process.nextTick()
- 実行します現在の関数の直後に
- 走りますイベントループが続行する前に
- 悪用されると I/O をブロックできる
setImmediate()
- はの次のイテレーション(チェックフェーズ)で実行されます
- より安全で予測可能です
→ 公式ドキュメントでは推奨されています:
ほとんどの実世界のシナリオで
setImmediate()を使用することをお勧めします
Node.jsが数千のリクエストを処理する方法
本質的な質問です
伝統的なサーバー(リクエストごとにスレッド)
- 各リクエストは新しいスレッドとして作成
- メモリ使用量が重い
- コンテキストスイッチングのオーバーヘッド
Node.jsのアプローチ
- 単一のスレッドがすべてのリクエストを処理
- リクエストごとにスレッドを作成しない
- 非同期コールバックを使用
実際に起こること:
- 1000人のユーザーがサーバーにアクセス
-
Node.js:
- すべてのリクエストを登録します
- 非同期操作を開始します
- イベントループを解放します
-
レスポンスが返ってくるたびに:
- コールバックがキューに入ります
- イベントループがそれらを実行します
→ 結果:
高い並行性と低いリソース使用
重要な洞察:Node.jsはI/Oバウンド作業に最適です
Node.jsは次のような場面で光ります:
- データベースクエリ
- APIコール
- ファイルシステム操作
- ストリーミング
- リアルタイムアプリ(チャット、ソケット)
しかし…
適さないのは:
- 重いCPU計算
- 大規模な同期ループ
その理由は:
イベントループをブロックする = すべてをブロックする
パフォーマンスを低下させる一般的なミス
1. ブロッキングコード
while(true) {}
→ サーバー全体がフリーズする
2. process.nextTick()
- を誤使用する
- イベントループを飢らせる__JHSNS_SEG_92f3a4cf_134__I/O実行を防止する
3. APIで同期コードを書く
fs.readFileSync()
→ 本番環境では避ける
もっとパワーが必要な場合(1つのコアを超えてスケールする場合)
Node.jsはプロセスごとに単一スレッドですが、以下を使用してスケールできます:
- Clusterモジュール
- ワーカースレッド
- ロードバランサー
→ これにより:
- マルチコアの利用
- 横方向のスケーリング
私の最終的な理解
公式のNode.jsドキュメントを確認し、実際にアプリケーションを構築した後、私はそれについてこんな風に考える:
- Node.jsは一度にすべてをやろうとしていない
- それはブロックしないようにしようとしている
イベントループはただの賢い調整役です:
- 準備ができているものを実行
- 待っているものをスキップ
- システムを動かし続けます
→ そのためです:
Node.jsは数千の並行リクエストを処理できます — 並列実行ではなく、効率的なスケジューリングと非同期設計
結論
一つの文でまとめると:
Node.jsは速いから拡張できるわけではなく、不必要な待ちを非常に上手くやるから拡張できるこの考え方が理解できるようになると、Node.jsのアーキテクチャに関するすべてが意味をなすようになる













