當我第一次開始使用Node.js時,有一件事讓我覺得不對勁:
一個單線程的系統如何能同時處理成千上萬的請求?
聽起來很矛盾。但一旦我正確理解了事件迴圈(特別是從官方文件中),一切就豁然開朗了。這篇博客是我嘗試以最簡單的方式解釋它,同時不失去深度。
「單執行緒」其實是什麼意思
Node.js 常常被稱為 單執行緒,但這個說法是不完整的.
- JavaScript 執行在 一個主執行緒
- 但 Node.js 本身是 不僅限於一次一個操作
-
它使用:
- 作業系統核心
- 背景執行緒 (libuv)
- 非同步 I/O
→ 所以正確的陳述是:
Node.js 是 單執行緒的,用於 JavaScript 執行,但 多系統的,用於處理 I/O
這個區別至關重要.
核心概念:事件驅動、非阻塞性架構
Node.js不會等待任務完成
相反,它遵循這個模式:
- 接收請求
- 開始任務(DB呼叫、檔案讀取、API呼叫)
- 不等待
- 移至下一個請求
- 結果準備好時再回來
這被稱為非阻擋 I/O.
→ 從官方文件:
Node.js 在可能時將作業委派給系統,所以主執行緒保持自由.
考慮這樣想(簡單類比)
想像:
- 你是一位服務生(事件迴圈)
- 廚房 = 作業系統 / 背景工作員
你:
- 接收訂單
- 將其傳遞給廚房
- 上傳完成的菜餚
你不需要:
- 親自下廚
- 空等單一訂單
這正是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() vs 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()
- 可能使事件迴圈匱乏資源
- 阻止I/O執行
3. 在 API 中撰寫同步程式碼
fs.readFileSync()
→ 避免在生產環境中使用
如果您需要更多效能(擴展超過單一核心)
Node.js 是單線程的每個進程,但您可以透過以下方式進行擴展:
- Cluster 模組
- 工作線程
- 負載均衡器
→ 這允許:
- 多核心利用
- 水平擴展
我的最終理解
經過官方 Node.js 文檔和實際開發應用後,這是我對它的理解:
- Node.js 並非試圖一次做所有事情
- 它試圖 永不阻塞
事件迴圈只是一個聰明的協調者:
- 它執行已準備好的任務
- 跳過正在等待的任務
- 保持系統運行
→ 這就是原因:
Node.js 可以處理成千上萬的並發請求 — 不是透過平行執行,而是透過 高效的排程和非阻塞性設計
結論
如果我必須用一句話總結:
Node.js之所以可擴展,不是因為它處理工作的速度快,而是因為它在避免不必要的等待
一旦這種思維模式啟動,Node.js架構的各個方面就開始顯得合理。













