Node.js 進(jìn)程、線程調(diào)試和診斷的設(shè)計(jì)和實(shí)現(xiàn)
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
前言:本文介紹 Node.js 中,關(guān)于進(jìn)程、線程調(diào)試和診斷的相關(guān)內(nèi)容。進(jìn)程和線程的方案類似,但是也有一些不一樣的地方,本文將會(huì)分開介紹,另外本文介紹的是對(duì)業(yè)務(wù)代碼無(wú)侵入的方案,通過命令行開啟 Inspector 端口或者在代碼里通過 Inspector 模塊打開端口在很多場(chǎng)景下并不適用,我們需要的是一種動(dòng)態(tài)控制的能力。
1. 背景
隨著前端的快速發(fā)展,Node.js 在業(yè)務(wù)中的使用場(chǎng)景也越來越多,如何保證 Node.js 服務(wù)的穩(wěn)定也逐漸成為一個(gè)非常重要事情,傳統(tǒng)的服務(wù)器架構(gòu)大多數(shù)基于多進(jìn)程、多線程的,任務(wù)的執(zhí)行是隔離的,一個(gè)任務(wù)出現(xiàn)問題通常不會(huì)影響其他任務(wù),比如在一個(gè)請(qǐng)求中執(zhí)行一個(gè)死循環(huán),服務(wù)器還能處理其他的請(qǐng)求。
但是 Node.js 不一樣,從整體來看,Node.js 是單線程的,單個(gè)任務(wù)出現(xiàn)問題有可能會(huì)影響其他任務(wù),比如在一個(gè)請(qǐng)求中執(zhí)行了死循環(huán),那么整個(gè)服務(wù)就沒法繼續(xù)工作了。所以在 Node.js 中,我們更加需要方便的調(diào)試和診斷工具,以便遇到問題時(shí)可以快速找到問題,解決問題,另外,工具不僅可以幫我們排查問題,還可以找出我們服務(wù)中的性能瓶頸,方便我們進(jìn)行性能優(yōu)化。
2. 目標(biāo)
我們基于 Node.js 本身提供的調(diào)試和診斷能力,提供一個(gè)調(diào)試和診斷平臺(tái),使用方只需要引入 SDK,然后通過調(diào)試和診斷平臺(tái)就可以對(duì)服務(wù)的進(jìn)程和線程進(jìn)行調(diào)試和診斷。
3. 實(shí)現(xiàn)
目前支持了多進(jìn)程和多線程的調(diào)試和診斷,下面按照進(jìn)程和線程兩個(gè)方面介紹一下原理和具體實(shí)現(xiàn)。
3.1. 單進(jìn)程
3.1.1 調(diào)試和診斷基礎(chǔ)
在 Node.js 中,可以通過以下方式收集進(jìn)程的數(shù)據(jù)。
const inspector = require('inspector');
const session = new inspector.Session();
session.connect();
// 發(fā)送命令
session.post('Profiler.enable', () => {});
使用方式很簡(jiǎn)單,通過新建一個(gè)和 V8 Inspector 通信的 Session 就可以對(duì)進(jìn)程進(jìn)行數(shù)據(jù)的收集,比如抓取進(jìn)程的堆快照和 Profile 數(shù)據(jù)。有了這個(gè)基礎(chǔ)后,我們就可以封裝這個(gè)能力。
const http = require('http');
const inspector = require('inspector');
const fs = require('fs');
// 打開一個(gè)和 V8 Inspector 的會(huì)話
const session = new inspector.Session();
session.connect();
function getCpuprofile(req, res) {
// 向V8 Inspector 提交命令,開啟 CPU Profile 并收集數(shù)據(jù)
session.post('Profiler.enable', () = >{
session.post('Profiler.start', () = >{
// 收集一段時(shí)間后提交停止收集命令
setTimeout(() = >{
session.post('Profiler.stop', (err, { profile }) = >{
// 把數(shù)據(jù)寫入文件
if (!err && profile) {
fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile));
}
// 回復(fù)客戶端
res.end('ok');
});
},
3000);
})
});
}
http.createServer((req, res) = >{
if (req.url == '/debug/getCpuprofile') {
getCpuprofile(req, res);
} else {
res.end('ok');
}
}).listen(80);
但是這種方式不能調(diào)試進(jìn)程,調(diào)試進(jìn)程需要使用另外的 API,可以通過以下方式啟動(dòng)調(diào)試進(jìn)程的服務(wù)。
const inspector = require('inspector');
inspector.open();
console.log(inspector.url());
這時(shí)候 Node.js 進(jìn)程中就會(huì)啟動(dòng)一個(gè) WebSocket Server,我們可以通過 Chrome Dev Tools 連上這個(gè) Server 進(jìn)行調(diào)試,我們看看如何封裝。
const inspector = require('inspector');
const http = require('http');
let isOpend = false;
function getHTML() {
return `<html>
<meta charset="utf-8" />
<body>
復(fù)制到新 Tab 打開該 URL 開始調(diào)試 devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=${inspector.url().replace("ws://", '')}
</body>
</html>`;
}
http.createServer((req, res) = >{
if (req.url == '/debug/open') {
// 還沒開啟則開啟
if (!isOpend) {
isOpend = true;
// 打開調(diào)試器
inspector.open();
}
// 返回給前端的內(nèi)容
const html = getHTML();
res.end(html);
} else if (req.url == '/debug/close') {
// 如果開啟了則關(guān)閉
if (isOpend) {
inspector.close();
isOpend = false;
}
res.end('ok');
} else {
res.end('ok');
}
}).listen(80);
我們以 API 的方式對(duì)外提供動(dòng)態(tài)控制進(jìn)程調(diào)試和診斷的能力,具體的實(shí)現(xiàn)可以根據(jù)場(chǎng)景去修改,比如給前端返回一個(gè)不帶 Inspector 端口的 URL,前端再通過 URL 訪問服務(wù),服務(wù)代理請(qǐng)求 Websocket 請(qǐng)求到 Inspector 對(duì)應(yīng)的 WebSocket 服務(wù)。比如把收集的數(shù)據(jù)上傳到云上,給前端返回一個(gè) URL。
3.1.2 具體實(shí)現(xiàn)
我們通過 API 的方式提供功能,設(shè)計(jì)上采用插件化的思想,主框架負(fù)責(zé)接收請(qǐng)求和路由處理,具體的邏輯交給具體的插件去做,結(jié)構(gòu)如下所示。
數(shù)據(jù)收集的實(shí)現(xiàn)和上面的例子中類似,收到請(qǐng)求路由到對(duì)應(yīng)的插件,插件通過 Session 和 V8 Inspector 通信完成數(shù)據(jù)的收集。調(diào)試的實(shí)現(xiàn)就稍微復(fù)雜些,主要的原因是我們不能把端口返回給前端,讓前端直接連接該端口。這個(gè)不是因?yàn)榘踩珕栴},因?yàn)檎{(diào)試的 URL 是一個(gè)帶有一個(gè)復(fù)雜隨機(jī)值的字符串,就算端口暴露了,攻擊者也很難猜對(duì)隨機(jī)值,相比來說,通過提供 API 的方式更加不安全,因?yàn)橹灰婪?wù)的地址,就可以通過 API 去調(diào)試進(jìn)程了,所以嚴(yán)格來說,這里還需要加一些校驗(yàn)機(jī)制。言歸正傳,不暴露端口的原因是通常前端無(wú)法直接連接到這個(gè)端口,原因可能有很多,比如我們的服務(wù)運(yùn)行在容器中,容器只對(duì)外暴露有限的端口,我們不能期待在進(jìn)程中隨便起一個(gè)端口,在前端就可以直接訪問,但是有一個(gè)可以肯定的是,服務(wù)至少會(huì)對(duì)外提供一個(gè)端口,那就意味著我們可以通過某個(gè)對(duì)外的端口把非業(yè)務(wù)相關(guān)的請(qǐng)求傳遞到進(jìn)程內(nèi),基于上面的情況,當(dāng)我們打開 Inspector 端口時(shí),我們只會(huì)告訴前端打開成功或者失敗,當(dāng)前端通過調(diào)試 API 訪問服務(wù)器時(shí),我們會(huì)判斷端口是否已經(jīng)打開,是的話代理請(qǐng)求到 WebSocket Server。結(jié)構(gòu)設(shè)計(jì)如下:
大致實(shí)現(xiàn)如下:
const client = connect(WebSocket Server地址);
client.on('connect', () = >{
// 轉(zhuǎn)發(fā)協(xié)議升級(jí)的 HTTP 請(qǐng)求給 WebSocket Server
client.write(`GET ${req.path} HTTP/1.1\r\n` + buildHeaders(req.headers) + '\r\n');
// 透?jìng)?/span>
socket.pipe(client);
client.pipe(socket);
});
收到客戶的的請(qǐng)求后,首先連接到 WebSocket Server,然后透?jìng)骺蛻舳说恼?qǐng)求,接著通過管道讓 WebSocket Server 和客戶端通信就行。
3.2 多進(jìn)程
為了利用多核,Node.js 服務(wù)通常會(huì)啟動(dòng)多個(gè)進(jìn)程,所以支持多進(jìn)程的調(diào)試和診斷也是非常必要的。但是單進(jìn)程的調(diào)試診斷方案無(wú)法通過橫行拓展來支持多進(jìn)程的場(chǎng)景。
3.2.1 單進(jìn)程方案的限制
前面提到的方式看起來工作得不錯(cuò),但是如果服務(wù)是單實(shí)例上多進(jìn)程部署,就會(huì)存在一些限制。我們來看看這時(shí)候的結(jié)構(gòu):
假如我們只有一個(gè)對(duì)外端口:
-
基于 Node.js Cluster 模塊的多進(jìn)程管理機(jī)制,多個(gè)進(jìn)程監(jiān)聽同一個(gè)端口是沒問題的,但是請(qǐng)求的分發(fā)上會(huì)存在問題,比如請(qǐng)求 1 被分發(fā)到進(jìn)程 1,打開了進(jìn)程 1 的 Inspector 端口,接著請(qǐng)求 2 想關(guān)閉這個(gè)端口,但是請(qǐng)求被分發(fā)到了進(jìn)程 2,但是進(jìn)程 2 并沒有打開 Inspector 端口。 -
基于 child_process 的 fork 創(chuàng)建多進(jìn)程,則在重復(fù)監(jiān)聽端口時(shí)會(huì)報(bào)錯(cuò),導(dǎo)致只有一個(gè)進(jìn)程可以使用提供的功能。
3.2.1 Agent 進(jìn)程
一種解決方案是每個(gè)進(jìn)程監(jiān)聽不同的端口,這樣又回到了前面討論到問題,但是這種方案也不是完全不可行,只需要基于這個(gè)方案做一下改進(jìn),那就是引入 Agent 進(jìn)程,這時(shí)候結(jié)構(gòu)如下:
Agent 進(jìn)程負(fù)責(zé)收集和管理工作進(jìn)程的信息(如 pid、監(jiān)聽地址),并接管所有調(diào)試和診斷相關(guān)的請(qǐng)求,收到請(qǐng)求后根據(jù)參數(shù)進(jìn)行請(qǐng)求分發(fā)。具體流程如下:
-
Agent 啟動(dòng)一個(gè)服務(wù)器。 -
子進(jìn)程啟動(dòng)后,把自己的 pid 和監(jiān)聽的隨機(jī)服務(wù)地址注冊(cè)到 Agent。 -
客戶端通過 Agent 獲取進(jìn)程的 pid 列表,并選擇需要操作的進(jìn)程。 -
Agent 收到客戶的請(qǐng)求,根據(jù)入?yún)⒅械?pid 把請(qǐng)求發(fā)送給對(duì)應(yīng)的子進(jìn)程。 -
子進(jìn)程處理完畢后返回給 Agent,Agent返回給客戶端。
3.2.2 如何創(chuàng)建 Agent 進(jìn)程
確定了 Agent 進(jìn)程的方案后,如何創(chuàng)建 Agent 進(jìn)程成為一個(gè)需要解決的問題。在 Node.js 里啟動(dòng)多個(gè)服務(wù)器的方式是通過 Cluster 或者直接通過 child_process 模塊 fork 出多個(gè)子進(jìn)程,Node.js 框架/工具通常都會(huì)封裝這些邏輯,但是框架不一定會(huì)提供創(chuàng)建 Agent 進(jìn)程的方式。為了通用,我們不能假設(shè)運(yùn)行在某種框架/工具中,所以我們只能尋找一種獨(dú)立于框架/工具的方案。我們?cè)诿總€(gè) Worker 進(jìn)程里都創(chuàng)建一個(gè) Agent 進(jìn)程,然后多個(gè) Agent 進(jìn)程競(jìng)爭(zhēng)監(jiān)聽一個(gè)端口,監(jiān)聽成功的進(jìn)程繼續(xù)運(yùn)行,監(jiān)聽失敗的退出,最終剩下一個(gè) Agent 進(jìn)程。
3.3 多線程
線程和進(jìn)程的調(diào)試、診斷類似,下面主要講一下不一樣的地方。
3.3.1 調(diào)試和診斷基礎(chǔ)
可以通過以下方式收集線程的數(shù)據(jù)。
const { Worker, workerData } = require('worker_threads');
const { Session } = require('inspector');
const session = new Session();
session.connect();
let id = 1;
// 給子線程發(fā)送消息
function post(sessionId, method, params, callback) {
session.post('NodeWorker.sendMessageToWorker', {
sessionId,
message: JSON.stringify({
id: id++,
method,
params
})
},
callback);
}
// 子線程連接上 V8 Inspector 后觸發(fā)
session.on('NodeWorker.attachedToWorker', (data) = >{
post(data.params.sessionId, 'Profiler.enable');
post(data.params.sessionId, 'Profiler.start');
// 收集一段時(shí)間后提交停止收集命令
setTimeout(() = >{
post(data.params.sessionId, 'Profiler.stop');
},
10000)
});
// 收到子線程消息時(shí)觸發(fā)
session.on('NodeWorker.receivedMessageFromWorker', ({ params: { message } }) = >{});
const worker = new Worker('./httpServer.js', { workerData: { port: 80 } });
worker.on('online', () = >{
session.post("NodeWorker.enable", { waitForDebuggerOnStart: false }, (err) = >{
console.log(err, "NodeWorker.enable");
});
});
setInterval(() = >{},100000);
類似通過 Agent 進(jìn)程管理多個(gè) Worker 進(jìn)程一樣,因?yàn)橐粋€(gè)進(jìn)程中可能存在多個(gè)線程,所以需要對(duì)多個(gè)線程進(jìn)行管理。首先通過 NodeWorker.enable 命令開啟子線程的 Inspector 能力,然后通過 NodeWorker.attachedToWorker 事件拿到線程對(duì)應(yīng)的 sessionId,后續(xù)通過 sessionId 和線程進(jìn)行通信。接著看一下調(diào)試的實(shí)現(xiàn):
const { Worker, workerData } = require('worker_threads');
const { Session } = require('inspector');
const session = new Session();
session.connect();
let workerSessionId;
let id = 1;
function post(method, params) {
session.post('NodeWorker.sendMessageToWorker', {
sessionId: workerSessionId,
message: JSON.stringify({
id: id++,
method,
params
})
});
}
session.on('NodeWorker.receivedMessageFromWorker', ({ params: { message } }) = >{
const data = JSON.parse(message);
console.log(data);
});
session.on('NodeWorker.attachedToWorker', (data) = >{
workerSessionId = data.params.sessionId;
post("Runtime.evaluate", {
includeCommandLineAPI: true,
expression: `const inspector = process.binding('inspector');
inspector.open();
inspector.url();
`
});
});
const worker = new Worker('./httpServer.js', { workerData: { port: 80 } });
worker.on('online', () = >{
session.post("NodeWorker.enable", { waitForDebuggerOnStart: false }, (err) = >{
err && console.log("NodeWorker.enable", err);
});
});
setInterval(() = >{}, 100000);
線程的調(diào)試主要利用 Runtime.evaluate 在子線程里動(dòng)態(tài)執(zhí)行代碼來打開子線程的 Inspector 端口。了解了基礎(chǔ)使用后,我們看一下具體實(shí)現(xiàn)。
3.3.2 具體實(shí)現(xiàn)
首先我們提供一個(gè) API 獲取線程列表,這樣我們后續(xù)就可以選擇操作某個(gè)線程,后續(xù)的每個(gè)請(qǐng)求都需要帶上 線程對(duì)應(yīng)的 id,這里以獲取 Profile 為例講一下處理過程。
const {
sessionId,
interval = INTERVAL,
duration = DURATION
} = req.query;
// 向V8 Inspector 提交命令,開啟 CPU Profile 并收集數(shù)據(jù)
this.post(sessionId, { method: 'Profiler.enable' }, (err) = >{
this.post(sessionId, {
method: 'Profiler.setSamplingInterval',
params: { interval }
});
this.post(sessionId, { method: 'Profiler.start' }, (err) = >{
// 收集一段時(shí)間后提交停止收集命令
setTimeout(() = >{
this.post(sessionId, { method: 'Profiler.stop' }, (err, { profile }) => {});
}, duration);
});
})
我們看到每一個(gè)操作都需要 sessionId。通過 sessionId,我們把請(qǐng)求轉(zhuǎn)發(fā)到對(duì)應(yīng)的線程。但是和進(jìn)程不一樣,進(jìn)程發(fā)送一個(gè)請(qǐng)求時(shí)傳入一個(gè)回調(diào),請(qǐng)求成功后就會(huì)執(zhí)行對(duì)應(yīng)的回調(diào),我們不需要保存請(qǐng)求上下文,Node.js 會(huì)幫我們處理,但是線程不一樣,存在一個(gè)嵌套的過程,因?yàn)?Inspector 命令的執(zhí)行模式是一個(gè)請(qǐng)求命令對(duì)應(yīng)一個(gè)回調(diào),但是和線程通信時(shí),是首選通過 NodeWorker.sendMessageToWorker 命令和主線程通信,主線程會(huì)解析出 NodeWorker.sendMessageToWorker 的參數(shù),參數(shù)里包含了給子線程發(fā)送的命令,接著主線程通過 sessionId 把請(qǐng)求轉(zhuǎn)發(fā)到子線程,然后這時(shí)候 NodeWorker.sendMessageToWorker 就會(huì)返回并執(zhí)行對(duì)應(yīng)的回調(diào),這時(shí)候意味著 NodeWorker.sendMessageToWorker 執(zhí)行結(jié)束了,但是我們請(qǐng)求子線程的命令還沒有完成,也就是說我們需要自己維護(hù)請(qǐng)求子線程對(duì)應(yīng)的回調(diào)。我們看看 post 的具體實(shí)現(xiàn):
post(sessionId, message, callback ? ) {
// 請(qǐng)求對(duì)應(yīng)的 id
const requestId = ++this.id;
this.session.post('NodeWorker.sendMessageToWorker', {
sessionId,
message: JSON.stringify({ ...message, id: requestId })
},
(err) = >{
/*
回調(diào)說明 NodeWorker.sendMessageToWorker 請(qǐng)求完成
err非空說明請(qǐng)求失敗,直接執(zhí)行回調(diào)
err為空說明請(qǐng)求成功,記錄 post 調(diào)用方的請(qǐng)求回調(diào),通過 id 關(guān)聯(lián)
*/
if (typeof callback === 'function') {
// 發(fā)送失敗則直接執(zhí)行回調(diào),成功則記錄回調(diào)
if (err) {
callback(err);
} else {
this.sessionMap[sessionId]['requests'][requestId] = callback;
}
}
});
}
我們看到在 NodeWorker.sendMessageToWorker 回調(diào)里保存了請(qǐng)求子線程的回調(diào)。接下來我們看一下線程執(zhí)行完命令后的回調(diào)。
this.session.on('NodeWorker.receivedMessageFromWorker', ({
params: {
sessionId,
message
}
}) = >{
const ctx = this.sessionMap[sessionId];
try {
const data = JSON.parse(message);
/**
* data 的內(nèi)容格式如下:
* {
* method: string,
* params: Object
* }
* 或者
* {
* id: number,
* result: { result: Object }
* }
*/
const {
id,
method,
result
} = data;
// 有 id 說明是請(qǐng)求對(duì)應(yīng)的響應(yīng),沒有 id 說明是 Inspector 異步觸發(fā)的事件
if (id) {
if (typeof ctx.requests[id] === 'function') {
const fn = ctx.requests[id];
delete ctx.requests[id];
fn(null, result);
}
} else {
ctx.emit(method, data);
}
} catch(e) {
console.warn(e);
}
});
通過 NodeWorker.receivedMessageFromWorker 事件可以接收到線程返回的請(qǐng)求結(jié)果,從響應(yīng)的數(shù)據(jù)中我們可以知道這個(gè)響應(yīng)來自的線程和請(qǐng)求 id,根據(jù)這些信息我們就可以從維護(hù)的上下文中找到對(duì)應(yīng)的回調(diào)(某些請(qǐng)求在收到響應(yīng)前會(huì)觸發(fā)一些事件,這種情況下響應(yīng)里是沒有請(qǐng)求 id 的)。
接著看一下如何調(diào)試子線程,調(diào)試端口默認(rèn)是 9229,因?yàn)榇嬖诙嗑€程,如果我們要同時(shí)調(diào)試多個(gè)線程的話,則會(huì)失敗,所以我們要允許前端來控制打開的端口,接著給子線程發(fā)送一個(gè)命令。
this.post(query.sessionId, {
method: "Runtime.evaluate",
params: {
includeCommandLineAPI: true,
expression: `let inspector;
try {
inspector = require('inspector');
inspector.open(${port}, ${host});
} catch(e) {
inspector = process.binding('inspector');
inspector.open(${port}, ${host});
}
inspector.url();`
}
},
(err, result) = >{
});
我們通過在子線程里動(dòng)態(tài)執(zhí)行代碼來打開 Inspector 端口,這里需要處理一下不同 Node.js 版本的兼容問題,高版本(比如 16)中增加了一個(gè)判斷邏輯,如果存在 session 就無(wú)法動(dòng)態(tài)打開 Inspector 端口了,比如以下代碼在 16 中會(huì)報(bào)錯(cuò)(換一下 connect 和 open 的位置就可以執(zhí)行)。
const inspector = require('inspector');
const session = new inspector.Session();
session.connect();
inspector.open()
這里需要繞過 JS 層的判斷,通過 C++ 模塊提供的接口直接打開 Inspector 端口,這樣就可以保證任何時(shí)候我們都可以動(dòng)態(tài)打開 Inspector 端口。最后通過 inspector.url() 讓子線程返回調(diào)試的 URL 并保存到上下文中,和進(jìn)程一樣,前端也是通過 API 的方式連接子線程的 WebSocket Server。最后形成的結(jié)構(gòu)如下。
4. 使用方式
目前支持了多個(gè)子進(jìn)程和多個(gè)線程的調(diào)試、獲取 CPU Profile、獲取 Heap Profile、獲取 Heap Snapshot、獲取內(nèi)存信息(RSS、堆外內(nèi)存、ArrayBuffer等信息)能力。使用方首先在業(yè)務(wù)代碼里加載 SDK,部署服務(wù)后,進(jìn)入調(diào)試診斷平臺(tái)頁(yè)面,按照以下步驟操作:
-
選擇調(diào)試進(jìn)程還是線程 -
輸入服務(wù)地址(Agent 進(jìn)程監(jiān)聽的地址)和選擇對(duì)應(yīng)的操作類型,如果是收集數(shù)據(jù)則還需要輸入收集的持續(xù)時(shí)間。 -
獲取進(jìn)程列表,并從中選擇你想操作的進(jìn)程,每個(gè)選項(xiàng) hover 時(shí)會(huì)提示進(jìn)程對(duì)應(yīng)的信息,比如文件路徑。 -
如果操作線程的話,在選擇進(jìn)程后,還需要獲取該進(jìn)程下的線程列表,并選擇你想操作的線程。 -
點(diǎn)擊執(zhí)行就可以獲得你想收集的數(shù)據(jù)或者在線調(diào)試的 URL。
進(jìn)程:
線程:
5. 總結(jié)
進(jìn)程、線程的調(diào)試和診斷在 Node.js 中的實(shí)現(xiàn)非常復(fù)雜,了解了 Node.js 的實(shí)現(xiàn)和使用方式后,具體應(yīng)用到業(yè)務(wù)里也不容易,主要是要考慮到不同的業(yè)務(wù)場(chǎng)景,需要設(shè)計(jì)出通用的方案,另外調(diào)試是一個(gè)比較有用但是也比較危險(xiǎn)的操作,在安全方面也需要多多考慮。調(diào)試、診斷和安全一樣,平時(shí)用不上,但是有問題的時(shí)候,能幫助我們更好地解決問題。
更多內(nèi)容參考:
-
深入理解 Node.js 的 Inspector:https://mp.weixin.qq.com/s/GLIlhURSrCYQ-8Bqg7i1kA -
Node.js子線程調(diào)試和診斷指南:https://zhuanlan.zhihu.com/p/402855448
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點(diǎn)贊、在看” 支持一波??
