Node.js Worker 新特性,你知道多少?
Node.js 單線程架構對開發者來說是比較友好的,但是單線程也存在一定的限制,比如單個任務同步執行耗時過長會阻塞后面的任務,所以后面 Node.js 開發了 Worker 模塊,Worker 模塊用于創建子線程,比如進行一些 CPU 密集型任務的處理。本文介紹最近給 Node.js Worker 貢獻了幾個新特性,主要是和 APM 相關的能力。
之前做 Node.js APM 時,我們實現了無侵入式的子線程監控和診斷方案,業務只需要在主線程引入 SDK,SDK 會自動感知子線程,并進行 CPU 負載、內存等監控,還可以實時采集子線程的 Profile 或堆快照。這個方案本質上還是借助了 Node.js 本身的能力,但是需要理解一些 Node.js 的實現細節,有一定的成本,但是的確非常強大。最近嘗試提供一些簡單的替換方案,下面是具體的內容。
Worker 名稱
Worker 模塊提供了一個 threadId 字段來區分不同的 Worker。
ounter(lineounter(lineounter(line
const { Worker } = require('worker_threads');
const worker = new Worker(`setInterval(() => {}, 1000)`, { eval: true });
console.log(worker.threadId);threadId 是一個數字,通過這個數字我們無法區分這個線程是哪個線程,最近有個開發者實現了給 Worker 設置名字的能力,但是主要用于調試時顯示,并沒有把這個能力暴露到應用層,所以我基于能力提交了一個 PR,讓上層的開發者尤其是 APM 開發者不僅可以感知到線程,而且還可以區分線程。其用法和 threadId 一樣。
ounter(lineounter(lineounter(line
const { Worker } = require('worker_threads');
const worker = new Worker(`setInterval(() => {}, 1000)`, { eval: true });
console.log(worker.threadName);具體可以參考這個 PR:https://github.com/nodejs/node/pull/59213。
Worker CPU 負載
操作系統底層是有記錄每個線程 CPU 負載的,但是這個系統兼容性不是很好,所以最近不久才加入到 Libuv 和 Node.js 中,不過在 Worker 代碼中才能使用該 API。而 APM 代碼是運行在主線程中,所以無法獲取到 Worker 的 CPU 負載,最近提交了一個 PR 給 Worker 添加了一個新 API,從而實現在主線程獲取 Worker 的 CPU 負載。使用方式如下。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.on('message', () => {});
`, { eval: true });
worker.on('online', () => {
const usage = await worker.cpuUsage();
console.log(usage);
worker.terminate();
});這樣就可以非常簡單地在主線程中獲取所有 Worker 的 CPU 負載。具體可以參考這個 PR:https://github.com/nodejs/node/pull/59177。
Worker CPU Profile
和 CPU 負載一樣,在主線程中無法獲取到 Worker 的 CPU Profile。所以類似的方式,給 Worker 增加一個新的 API 來實現這個能力。使用方式如下。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.on('message', () => {});
`, { eval: true });
worker.on('online', () => {
const handle = await worker.startCpuProfile("demo");
const profile = await handle.stop();
console.log(profile);
worker.terminate();
});實現 Worker 的 APM 能力
在上面的基礎能力上,我們就可以實現 Worker 的 APM 能力,比如下面的代碼實現了對 Worker 進行 CPU 負載監控,并在必要時采集 CPU Profile 幫助診斷問題的原因。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
const { Worker } = require('worker_threads');
process.on('worker', async (worker) => {
console.log(worker.threadName);
let prevUsage = await worker.cpuUsage();
let profiling = false;
setInterval(async () => {
const { user, system } = await worker.cpuUsage(prevUsage);
// 計算 CPU 負載
CPU 負載 = xxx
// 上報 CPU 負載
// 采集 CPU Profile
if (CPU 負載 > yyy && !profiling) {
profiling = true;
const handle = await worker.startCpuProfile("demo");
const profile = await handle.stop();
console.log(profile);
profiling = false;
}
}, 1000)
});
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.on('message', () => {});
`, { eval: true });具體可以參考這個 PR:https://github.com/nodejs/node/pull/59428。
總結
在新特性的基礎上,我們可以以簡單的方式實現對 Worker 的 APM 能力,提高了應用的可觀測性。另外,這種方案可以在 Worker 執行 JS 耗時代碼甚至死循環時正常工作。這對 APM 來說是非常基礎且核心的能力,也是 APM 需要解決的問題,否則 APM 能力將大打折扣,這也是我們之前一直在探索的一些事情,比如保證 APM 在死循環時正常工作或采集堆快照不阻塞目的線程等。

























