最近因为工作中碰到的一些问题,希望给 Node.js 提交一些代码来解决我碰到的问题,一共提交了 4 个 PR,目前一个已经在 17.8.0 中发布,一个刚合到主干,一个等 reviewer 回复,一个等 31 号 tsc 开会讨论。总的来说,提交的代码并不复杂,但是的确解决了我的问题,同时我觉得也是开发者需要的一些功能。下面介绍一下这几个 PR 做的事情。
1.通过 perf_hooks 收集 HTTP 模块的耗时
了解 HTTP Server 处理一个请求的耗时和发送一个 HTTP Request 到收到响应所需要的耗时是很多开发者都需要的,这些数据可以帮助我们了解我们服务的性能和网络链路的情况。没有 Node.js 的支持,开发者如果想收集这个数据会非常麻烦,每个开发者都需要实现开始请求前,记录开始时间,拿到响应后记录结束时间这些入侵业务逻辑的重复代码,而 SDK 的提供者,则需要劫持 http 模块来实现这样的功能。Node.js 在 12.7.0 版本里已经支持收集 HTTP Server 处理请求的耗时。在这个基础上,我做的事情主要是支持收集发送一个 HTTP Request 到收到响应所需要的耗时。具体实现如下。
ClientRequest.prototype._finish = function _finish() {
if (hasObserver('http')) {
this[kClientRequestStatistics] = {
startTime: process.hrtime(),
type: 'HttpClient',
};
}
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
首先在请求发送完毕后开始计时,如果开发者在通过 perf_hooks 收集 http 模块的数据,那么就开始记录请求的开始时间。然后收到 HTTP 响应并解析完请求行和请求头时记录结束时间。
function parserOnIncomingClient(res, shouldKeepAlive) {
emitStatistics(req[kClientRequestStatistics]);
}
function emitStatistics(statistics) {
if (!hasObserver('http') || statistics == null) return;
const startTime = statistics.startTime;
const diff = process.hrtime(startTime);
const entry = new InternalPerformanceEntry(
statistics.type,
'http',
startTime[0] * 1000 + startTime[1] / 1e6,
diff[0] * 1000 + diff[1] / 1e6,
undefined,
);
enqueue(entry);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
最后通过 perf_hooks 机制通知用户。接下来通过一个例子看看如何使用。
const { PerformanceObserver } = require('perf_hooks');
const http = require('http');
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach((item) => {
console.log(item);
});
});
obs.observe({ entryTypes: ['http'] });
const PORT = 8080;
http.createServer((req, res) => {
res.end('ok');}).listen(PORT, () => {
http.get(`http://127.0.0.1:${PORT}`);
});
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
在上面的例子中我们可以通过 perf_hooks 模块收集到两个数据,分布是 server 处理请求的耗时和 client 经历的耗时。
2.通过 perf_hooks 收集 TCP 连接和 DNS 解析的耗时
第一个 PR 合进去后,我觉得后续应该有很多地方可以通过 perf_hooks 机制进行数据的收集。接下来做的事情就是通过 perf_hooks 收集 TCP 连接和 DNS 解析的耗时。做性能分析和监控的时候,这部分的数据也是开发者感兴趣的。这次实现了两个通用的方式来处理一些公共的逻辑,希望后续其他地方收集数据的时候也可以复用。
function startPerf(target, key, context = {}) {
if (hasObserver(context.type)) {
target[key] = {
...context,
startTime: process.hrtime(),
};
}
}
function stopPerf(target, key, context = {}) {
const ctx = target[key];
if (ctx && hasObserver(ctx.type)) {
const startTime = ctx.startTime;
const diff = process.hrtime(startTime);
const entry = new InternalPerformanceEntry(
ctx.name,
ctx.type,
startTime[0] * 1000 + startTime[1] / 1e6,
diff[0] * 1000 + diff[1] / 1e6,
{ ...ctx.detail, ...context.detail },
);
enqueue(entry);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
这两个方式的逻辑很简单,startPerf 是记录操作的开始时间,并且记录一些上下文,然后在 stopPerf 中再记录操作的结束时间并合并上下文,最后通过 perf_hooks 机制通知开发者。另外只有在开发者注册了 perf_hooks 的观察者时才会执行这些逻辑,这样对不开启该功能的开发者就不会有性能的损耗。有了这两个方式后,收集数据就变得简单,只需要找到开始和结束的点,加入对应的函数就行。
function internalConnect() {
if (addressType === 6 || addressType === 4) {
startPerf(self, kPerfHooksNetConnectContext, { type: 'net', name: 'connect', detail: { host: address, port } });
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
internalConnect 是发起 TCP 请求的函数,目前只收集 TCP 连接的耗时,我们知道 net 模块还包括了 IPC 的实现,但是 IPC 是基于本机的,连接的耗时通常很快,收集这个数据没有太大意义。另外只有连接成功的时候才会被收集。具体在 afterConnect 函数。
function afterConnect(status, handle, req, readable, writable) {
if (status === 0) {
stopPerf(self, kPerfHooksNetConnectContext);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
接下来看一个使用的例子。
const { PerformanceObserver } = require('perf_hooks');
const net = require('net');
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach((item) => {
console.log(item);
});
});
obs.observe({ entryTypes: ['net'] });
const PORT = 8080;
net.createServer((socket) => {
socket.destroy();}).listen(PORT, () => {
net.connect(PORT);
});
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
通过上面的代码,我们就可以收集到 TCP 连接的耗时。这个 PR 中除了支持收集 TCP 耗时,还支持了 DNS 解析的耗时,包括 Promise 化的 API。原理类似,不再具体介绍,看一个使用例子就可以。
const { PerformanceObserver } = require('perf_hooks');
const dns = require('dns');
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach((item) => {
console.log(item);
});
});
obs.observe({ entryTypes: ['dns'] });
dns.lookup('localhost', () => {});
dns.promises.resolve('localhost');
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
这个 PR 目前已经被合到主干,下个版本应该就可以使用。
3.通过 perf_hooks 收集同步文件 API 耗时
这个 PR 一开始不是很想提,同步 API 很多,而我要做的事情就是在这些 API 开始和结束的时候记录具体的时间,reviewer 也提了这个问题。
- While I have no specific objection to this, I do wonder what this provides that wrapping the sync calls in performance.timerify wouldn't provide?
不过这个 PR 也不是为了提而提,我们经常收到业务反馈说事件循环延迟告警,但是不知道具体什么原因。我们很容易能监控到事件循环延迟,但是却很难知道具体的原因,因为情况太多了。而同步文件操作就是其中一种情况,所以我们希望收集这个数据。但是我们发现在开发者层面很难做到这个事情。如果我是业务同学,那么我需要在每个同步 API 的前面加统计代码,如果我是监控 SDK 提供者,那么我只能劫持文件模块的 API。无论哪种方式都不是优雅的解决方案。但是如何在 Node.js 内核支持的话,情况就不一样了,虽然只是简单地把代码移到 Node.js 里面,但是却能很好地解决问题,每次用户调用同步 API 的时候,无论是业务同学还是监控 SDK 提供者都能非常简单地通过 perf_hooks 机制拿到这些 API 的耗时数据。所以我回复 reviewer 我提这个 PR 的理由。
- I think if Node.js core provide this, user just need to new a observer by perf_hooks for collecting the cost time of fs sync api. otherwise, all users need to write some same code to do this. And if i want to provide a sdk to do this, i need to hijack the fs api.
使用方式也非常简单。
const { PerformanceObserver } = require('perf_hooks');
const fs = require('fs');
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach((item) => {
console.log(item);
});
});
obs.observe({ entryTypes: ['fs'] });
fs.readFileSync(__filename);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
这样我们就可以知道 readFileSync 的耗时。但是这个 PR 目前还没有 approve。我还是希望 Node.js 能支持这个能力。
4.暴露 V8 的 trace API
Node.js 目前实现了 trace_events 模块,里面有部分 trace 数据是通过 V8 提供的 trace API 实现的,但是 Node.js 目前没有暴露出来,我做的事情就是把这个 API 暴露出来,这样的好处是开发者可以利用 V8 的 trace 机制产生 trace 数据,并且可以通过 Node.js 的 trace_events 模块和 inspector 模块收集这些数据。因为我之前开发了一个 tracepoint 的库来做这个事情,但是相比来说在 Node.js 里支持会好很多。但是 Node.js 同学说在使用这个 API 时和 V8 团队达成了协议不会对外暴露给开发者,需要 tsc 讨论这个事情,下面是回复。
- Not sure we should do this. I mean, I generally prefer this, but when we added that trace API it was with the agreement with the v8 team that we would not expose a public API for it since they might still want to make changes to the underlying mechanism and exposing the public apis would make that more difficult.
最近 tsc 会讨论这个事情,希望能支持这个功能。
总结
给大型的开源项目提交 PR 是一个很 cool 但是其实也挺不容易的过程。首先需要了解提 PR 的整个流程,接着需要去了解相关模块的具体实现逻辑,比如我最近把 trace 模块和 perf_hooks 模块研究了一遍,然后才能确定怎么修改代码,修改完之后还需要写对应的测试,以及不能影响其他逻辑,都完成后还需要按照 reviewer 的建议进行反复修改,你的想法能得到 reviewer 的同意也不容易,比如我之前也提过 keepalive 和 so_resueport 的 PR,不过因为平台兼容性的问题没有被合进去,因为 Libuv 定位的是跨平台的库。但是,如果你提交成功一次,了解了整个过程之后,以后就会节约很多时间。最后附上四个 PR 的地址,有兴趣的同学也可以看下。
https://github.com/nodejs/node/pull/42345
https://github.com/nodejs/node/pull/42390
https://github.com/nodejs/node/pull/42421
https://github.com/nodejs/node/pull/42462