前言
在幫 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 Threads | CPU bound 複雜計劃 | 不阻塞主執行緒、可利用多核心 | 需要處理資料序列化 |
| Cluster | 多請求 Web 服務 | 多 process 分散負載、容錯性高 | 狀態需共用儲存 |
如果你現在也在幫其他 Node.js 處理效能問題,又剛好卡在老機器跑不動,可以試試以上方法。





















留言