公司使用了阿里云的服務(wù),其中可以在項(xiàng)目中使用全鏈路監(jiān)測(cè),最近要排查慢響應(yīng),所以就在 Node 項(xiàng)目中接了一下 SkyWalking。
本文還會(huì)記錄在使用時(shí)遇到的問(wèn)題,以及解決思路。
一、初始化
1)參數(shù)配置
SkyWalking支持自動(dòng)埋點(diǎn)和手動(dòng)埋點(diǎn),自動(dòng)埋點(diǎn)只要初始化后,就可以開(kāi)始工作,很便捷。

2)下載依賴
下載 SkyWalking Node.js Agent
npm install --save skywalking-backend-js
3)初始化
在項(xiàng)目的 app.js 中配置和啟用 SkyWalking。
const {default: agent} = require("skywalking-backend-js");
agent.start({
serviceName: 'web-api-pro',
collectorAddress: 'xxx',
authorization: 'xxx'
});
二、分析
1)應(yīng)用概覽
在應(yīng)用列表,選擇web-api進(jìn)入后,就能看到如下的分析頁(yè)面。

SkyWalking默認(rèn)會(huì)上報(bào)項(xiàng)目?jī)?nèi)的所有接口通信、MySQL查詢、MongoDB查詢等。
但這樣會(huì)增加存儲(chǔ)成本,所以我需要將不相關(guān)的接口過(guò)濾去除。
2)過(guò)濾接口
翻閱官方文檔,發(fā)現(xiàn)有個(gè)參數(shù)有這個(gè)過(guò)濾作用,字符串類型,默認(rèn)是空字符串。
| SW_TRACE_IGNORE_PATH | The paths of endpoints that will be ignored (not traced), comma separated | `` |
而跳轉(zhuǎn)到源碼中,也發(fā)現(xiàn)了對(duì)應(yīng)的字段:traceIgnorePath。
export declare type AgentConfig = { serviceName?: string; collectorAddress?: string; authorization?: string; ignoreSuffix?: string; traceIgnorePath?: string; reIgnoreOperation?: RegExp; };
在 deepseek 上提問(wèn),AI 給了我如何使用參數(shù)的示例,通配符的作用也詳細(xì)的說(shuō)明了。
traceIgnorePath: "/healthcheck/*,/static/**"
但是,提交到測(cè)試環(huán)境后,并沒(méi)有像預(yù)想的那樣,將指定路徑的接口過(guò)濾掉。
在將配置路徑,翻來(lái)覆去的更改后,仍然不見(jiàn)效,遂去查看源碼,在源碼中的確包含 traceIgnorePath 參數(shù)。
3)求助阿里云
由于這是阿里云提供的可選類型,所以就去阿里云上創(chuàng)建工單。

馬上就自動(dòng)創(chuàng)建了一個(gè)小群,與對(duì)方的人員語(yǔ)音溝通了下,并且共享了屏幕代碼。
他表示需要花點(diǎn)時(shí)間,自己操作一下,在此期間,我自己也繼續(xù)查看源碼,最終發(fā)現(xiàn)了端倪。
阿里云的響應(yīng)還是很快的,特別及時(shí)。
4)源碼分析
在 node_modules 目錄中的文件,也可以打印日志,我將傳入的參數(shù)都打印了出來(lái)。
serviceName: 'web-api', serviceInstance: 'MacBook-Pro.local', collectorAddress: 'xxxx', authorization: 'xxxx', ignoreSuffix: '.gif', traceIgnorePath: '/audiostream/audit/callback', reIgnoreOperation: /^.+(?:\.gif)$|^(?:\/audiostream\/audit\/callback)$/,
看到 reIgnoreOperation 參數(shù)被賦值了,一段正則,這個(gè)很關(guān)鍵,過(guò)濾接口,其實(shí)就是匹配正則。
用 reIgnoreOperation 搜索,搜到了被使用的一段代碼,operation 是一個(gè)傳遞進(jìn)來(lái)的參數(shù)。
SpanContext.prototype.ignoreCheck = function (operation, type, carrier) { if (operation.match(AgentConfig_1.default.reIgnoreOperation) || (carrier && !carrier.isValid())) return DummySpan_1.default.create(); return undefined; };
然后再用用 traceIgnorePath 去搜索代碼,并沒(méi)有得到有用的信息,于是將關(guān)鍵字改成 Ignore。

果然找到了合適的代碼,在 HttpPlugin.prototype.interceptServerRequest 方法中,找到一段創(chuàng)建 span 的代碼。
var operation = reqMethod + ':' + (req.url || '/').replace(/\?.*/g, ''); var span = AgentConfig_1.ignoreHttpMethodCheck(reqMethod) ? DummySpan_1.default.create() : ContextManager_1.default.current.newEntrySpan(operation, carrier);
鏈路(即鏈路追蹤)可深入了解請(qǐng)求路徑、性能瓶頸和系統(tǒng)依賴關(guān)系,多個(gè)處理數(shù)據(jù)的片段(也叫 span,跨度)通過(guò)鏈路 ID 進(jìn)行串聯(lián),組成一條鏈路追蹤。
span 中有個(gè)三目運(yùn)算,經(jīng)過(guò)測(cè)試發(fā)現(xiàn),如果沒(méi)有配置要過(guò)濾的請(qǐng)求方法,那么就是 false。
所以會(huì)進(jìn)入到 newEntrySpan() 方法中,而在此方法中,恰恰會(huì)調(diào)用 ignoreCheck() 方法。
那么其傳入的 operation,其實(shí)就是要匹配的路徑值,原來(lái)我配錯(cuò)了,官方需要帶請(qǐng)求方法,如下所示。
traceIgnorePath: 'POST:/audiostream/audit/callback',
不要過(guò)渡依賴 AI,我這次就非常相信 AI 給的示例,結(jié)果繞了大彎。
5)運(yùn)行原理
在執(zhí)行 start() 方法時(shí),會(huì)進(jìn)行參數(shù)合并,參數(shù)修改等操作。
Agent.prototype.start = function (options) { // 傳入?yún)?shù)和默認(rèn)參數(shù)合并 Object.assign(AgentConfig_1.default, options); // 初始化參數(shù),例如拼接正則等 AgentConfig_1.finalizeConfig(AgentConfig_1.default); // 掛載插件,就是注入鏈路代碼 new PluginInstaller_1.default().install(); // 上報(bào) this.protocol = new GrpcProtocol_1.default().heartbeat().report(); this.started = true; };
其中在 report() 中,會(huì)創(chuàng)建一個(gè)定時(shí)任務(wù),每秒運(yùn)行一次。
setTimeout(this.reportFunction.bind(this), 1000).unref();
.unref() 告訴 Node.js 事件循環(huán):“此定時(shí)器不重要,如果它是唯一剩余的任務(wù),可以忽略它并退出進(jìn)程”。
優(yōu)化進(jìn)程生命周期管理,避免無(wú)關(guān)任務(wù)阻塞退出。
最核心的插件有HttpPlugin、IORedisPlugin、MongoosePlugin、AxiosPlugin、MySQLPlugin 等。
以 HttpPlugin 為例,在 install() 時(shí),會(huì)調(diào)用 interceptServerRequest() 方法注入鏈路操作。
HttpPlugin.prototype.install = function () { var http = require('http'); this.interceptServerRequest(http, 'http'); };
在 interceptServerRequest() 中,會(huì)修改 addListener()、on() 方法,并且會(huì)包裝響應(yīng)。
HttpPlugin.prototype.interceptServerRequest = function (module, protocol) { var plugin = this; var _addListener = module.Server.prototype.addListener; module.Server.prototype.addListener = module.Server.prototype.on = function (event, handler) { var addArgs = []; // 復(fù)制參數(shù) for (var _i = 2; _i < arguments.length; _i++) { addArgs[_i - 2] = arguments[_i]; } // 執(zhí)行事件 return _addListener.call.apply( _addListener, tslib_1.__spreadArrays([this, event, event === 'request' ? _sw_request : handler ], addArgs) ); function _sw_request(req, res) { var _this = this; var _a; var reqArgs = []; // 復(fù)制參數(shù) for (var _i = 2; _i < arguments.length; _i++) { reqArgs[_i - 2] = arguments[_i]; } var carrier = ContextCarrier_1.ContextCarrier.from(req.headers || {}); var reqMethod = (_a = req.method) !== null && _a !== void 0 ? _a : 'GET'; // 拼接請(qǐng)求方法和接口路徑 var operation = reqMethod + ':' + (req.url || '/').replace(/\?.*/g, ''); var span = AgentConfig_1.ignoreHttpMethodCheck(reqMethod) ? DummySpan_1.default.create() : ContextManager_1.default.current.newEntrySpan(operation, carrier); span.component = Component_1.Component.HTTP_SERVER; span.tag(Tag_1.default.httpURL(protocol + '://' + (req.headers.host || '') + req.url)); // 包裝響應(yīng)信息 return plugin.wrapHttpResponse(span, req, res, function () { return handler.call.apply( handler, tslib_1.__spreadArrays([_this, req, res], reqArgs) ); }); } }; };
不過(guò)在上線后,發(fā)生了意想不到的意外,就是原先可以鏈?zhǔn)秸{(diào)用的 Mongoose 的方法:
this.liveApplyRecord.find({ userId }).sort({ createTime: -1 });
在調(diào)用時(shí)會(huì)出現(xiàn)報(bào)錯(cuò):
this.liveApplyRecord.find(...).sort is not a function
posted on
浙公網(wǎng)安備 33010602011771號(hào)