Mark Ku's Blog
首頁 關於我
解決 Node.js 單執行緒效能瓶頸的幾種實戰解法
解決 Node.js 單執行緒效能瓶頸的幾種實戰解法
Mark Ku
Mark Ku
December 29, 2025
1 min

前言

在幫 Uptime Kuma 擴充負載平衡(Load Balancing)與節點切換(Failover)功能的過程中,我意外踩到了一個Node.Js的效能瓶頸:

同一套程式,在家裡的機器可以穩穩跑 1000 個監控,但是放到一台 13 年前的測試機 上時,大概 單一節點跑到 800 個監控就開始頓、反應變慢

這也逼得我回頭重新檢查,底是我寫壞,還是 Node.js 天生的限制?


為什麼 800 個監控就開始頓?

Uptime Kuma 本身就是一個高頻輪詢(polling)的系統:每個監控 item 都要定期發出請求、判斷成功或失敗、更新狀態,現在又多了一層:

在新一點的 CPU 上,這些重計算還撐得住;但在那台 13 年前的測試機上,大約 800 個監控 時,以下現象開始出現:

  • 後台 UI 操作有明顯「卡一下」的感覺
  • 定時任務排程延遲,比原本設定的時間晚觸發
  • 整體 CPU 使用率接近單核心吃滿,但其他核心卻閒著

在談關鍵原因前,我們先了解 JS 的Event loop

JavaScript 本身是單執行緒,所有同步程式碼都必須依序在 call stack 中執行,一旦在主執行緒裡執行了耗時的同步工作(例如大量計算或死迴圈),整個執行緒就會被 block,其他任務完全無法插隊。

相對地,像 setTimeout、HTTP 請求這類需要等待的操作,並不會直接佔用 call stack。執行環境會先將它們交給底層 API(libuv)處理,等任務完成後,再把對應的 callback 放進佇列中,等待 event loop 在 call stack 空閒時依序取回並執行。

正因如此,event loop 讓 JavaScript 即使只有一條主執行緒,也能透過非同步機制同時安排大量 I/O 工作,而不會因為等待外部資源就讓整個程式停擺。

關鍵原因

當監控數量很多、輪詢頻率又很高時**,就會出現一堆「需要重複計算」的邏輯一起擠在同一條 event loop 上。這些計算又不是在等 I/O,而是實打實吃 CPU,久了自然就變成效能瓶頸。

因此在不升級單核 CPU 的前提下,我這邊整理出幾種實際可以提昇效能的解法:

解法一:降低輪詢頻率(調整 interval)

在高頻輪詢的架構裡,很多「重計算」其實是被輪詢驅動的。直接降低輪詢頻率(interval)可以讓 CPU bound 的邏輯觸發次數下降,進而讓 event loop 壓力減輕。

以 Uptime Kuma 為例,每個監控都是一條輪詢任務:例如有 1000 個監控,每 60 秒就要發一次請求、判斷成功/失敗、更新狀態與儀表板。這些動作本身就是吃 CPU 的邏輯,如果 interval 設太短,就會在同一段時間內把一大堆「重計算」一起塞進同一條 event loop。適度拉長輪詢頻率(例如從 15 秒調成 30 秒,或把不那麼關鍵的監控改成更長間隔),可以直接減少這些 CPU-bound 邏輯被觸發的次數,讓 event loop 壓力下降、整體卡頓感也會跟著改善。


解法二:改成多節點(水平擴充 Uptime Kuma)

當單機資源有限時,將監控分散到多台節點是更穩健的做法。每個節點只負責一部分監控,狀態透過共用儲存(DB/Redis)統一,前面可加反向代理或任務分配層。

參考:我對多節點/Cluster 的思路與做法整理在這篇文章,可作為延伸閱讀:Uptime Kuma Cluster 實作筆記

實作要點

  • 共用儲存:資料庫/Redis 作為單一事實來源,避免節點只放記憶體狀態。
  • 任務分片:用標籤、哈希或佇列將監控分配到不同節點;避免重複監控。
  • 健康監測與接手:節點故障時,任務能被其他節點自動接手(Failover)。
  • 可觀測性:集中化日誌、指標與告警,利於維運與問題定位。

效果

  • 單機壓力顯著降低,整體吞吐與可用性提升。
  • 故障隔離更好,節點出問題不會拖垮整體服務。

解法三:引入 Bun 作為高效能 Runtime

Bun 是什麼?

在和同事討論 Uptime Kuma 的時候,他建議我可以試試 Bun,所以我就順勢往下研究了一下。Bun 本質上是一個主打高效能的 JavaScript Runtime,特色大致有:

  • JIT / runtime 設計偏向高效能,對啟動與執行效率都有優化
  • 內建打包器、測試工具、套件管理(bun install)等等
  • 對 Node.js API 有一定程度的相容,但不是 100%(這點要特別注意)

解法四:把重計算丟給 Worker Threads

註:此段為我目前的研究紀錄,尚未在專案中完成實作;內容著重思路與示意,非落地方案。

我研究的做法是 針對「重計算」本身下手,把最吃 CPU 的那一塊抽出去,丟給 worker_threads 處理。

適合丟給 Worker Threads 的東西

  • 根據監控結果重新計算各節點的 權重與健康度
  • 執行 複雜的規則判斷(例如多條件 failover 策略)
  • 對大量監控做 批次分析 / 排序 / 統計

概念上就是:主執行緒負責:

  • 接收監控結果
  • 排隊 / 分派任務給 worker
  • 接收 worker 算完的結果,更新狀態

而真正重的邏輯,搬去 worker 檔案裡去跑。

程式範例

一個簡化版的程式骨架大概像這樣(示意,不是完整程式):

// main.js
const { Worker } = require("worker_threads");

function recalcLoadBalancing(monitors) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./recalc-worker.js", {
      workerData: { monitors },
    });

    worker.on("message", (result) => resolve(result));
    worker.on("error", reject);
    worker.on("exit", (code) => {
      if (code !== 0) reject(new Error(`Worker exited: ${code}`));
    });
  });
}
// recalc-worker.js
const { parentPort, workerData } = require("worker_threads");

function heavyRecalc(monitors) {
  // 在這裡做複雜、吃 CPU 的演算法
  // 回傳新的節點權重 / 排序結果等等
  return { /* ... */ };
}

const result = heavyRecalc(workerData.monitors);
parentPort.postMessage(result);

這樣一來:

  • 主執行緒不再被「一次算 800 個監控的負載」卡死
  • 即使在老舊 CPU 上,UI 頓的感覺會明顯改善
  • 要多吃幾顆核心,只要開多幾個 worker 就好(當然要控制數量)

解法五:Cluster / 多個 Node process 吃滿多核心

註:此段為研究方向與可行性分析,尚未實作於現有服務。

如果你的服務本身是 多使用者、多請求的 Web API,除了把重計算抽出去,其實也可以再用 Cluster / 多 process 來分散負載。

概念類似:

  • cluster 或 PM2 把同一個 Node.js 應用跑成多個 process
  • 例如一台 4 核 CPU,就開 4 個 worker process
  • 前面再用一層反向代理(Nginx / HAProxy / Traefik)做負載平衡

Cluster 範例

const cluster = require("cluster");
const os = require("os");

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;
  console.log(`Master process ${process.pid} is running`);
  console.log(`Forking ${numCPUs} workers...`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died, restarting...`);
    cluster.fork();
  });
} else {
  // Worker process - 執行實際的應用邏輯
  require("./app.js");
  console.log(`Worker ${process.pid} started`);
}

總結

方案適用場景優點缺點
降低輪詢頻率(interval)高頻輪詢導致壓力減少重計算次數、降低主迴圈負載反應時間變慢、可能漏短暫故障
多節點(水平擴充)單機資源有限、需要擴展分散負載、故障隔離、易於橫向擴張跨節點同步與配置複雜度提高
Bun新腳本、獨立服務執行速度快、啟動快相容性還不是 100%
Worker ThreadsCPU bound 重計算不阻塞主執行緒、可利用多核心需要處理資料序列化
Cluster多請求 Web 服務多 process 分散負載、容錯性高狀態需共用儲存

如果你現在也在幫其他 Node.js 處理效能問題,又剛好卡在老機器跑不動,可以試試以上方法。


Tags

Uptime KumaNode.jsBunWorker ThreadsClusterPerformanceMonitoring
Mark Ku

Mark Ku

Software Developer

10年以上豐富網站開發經驗,開發過各種網站,電子商務、平台網站、直播系統、POS系統、SEO 優化、金流串接、AI 串接,Infra 出身,帶過幾次團隊,也加入過大團隊一起開發。

Expertise

前端(React)
後端(C#)
網路管理
DevOps
溝通
領導

Social Media

facebook github website

Quick Links

關於我

Social Media