当前位置: 首页 > 科技观察

说说最近提交给Node.js的几个PR

时间:2023-03-22 11:02:08 科技观察

最近因为工作中遇到的一些问题,希望提交一些代码给Node.js,解决我遇到的问题。一共提交了4个PR。目前,一个已经在17.8.0发布,一个刚刚加入主干,一个在等待审稿人的回复,一个在等待31号的tsc会议讨论。总的来说,提交的代码并不复杂,但是确实解决了我的问题,我想也是开发者需要的一些功能。下面介绍一下这些PR是干什么的。1、通过perf_hooks收集HTTP模块的耗时了解HTTPServer处理一个请求的耗时,以及发送一个HTTPRequest到收到一个response所需的时间,是很多开发者需要的。这些数据可以帮助我们了解我们的服务性能和网络链接状况。如果没有Node.js的支持,开发者收集这些数据会很麻烦。每个开发者都需要在开始请求前记录开始时间,在收到响应后记录结束时间。这些侵入业务逻辑的重复代码,而SDK提供方,需要劫持http模块才能实现这样的功能。Node.js12.7.0版本已经支持收集HTTPServer处理请求的时间。在此基础上,我做的主要是支持收集从发送HTTPRequest到收到响应的耗时。具体实现如下。ClientRequest.prototype._finish=function_finish(){if(hasObserver('http')){this[kClientRequestStatistics]={startTime:process.hrtime(),type:'HttpClient',};}};首先在请求发送完成后,开始计时。如果开发者通过perf_hooks收集了http模块的数据,那么就开始记录请求的开始时间。然后记录接收到HTTP响应并解析请求行和标头的结束时间。functionparserOnIncomingClient(res,shouldKeepAlive){emitStatistics(req[kClientRequestStatistics]);}functionemitStatistics(statistics){if(!hasObserver('http')||statistics==null)return;conststartTime=statistics.startTime;constdiff=process.hrtime(startTime);constentry=newInternalPerformanceEntry(statistics.type,'http',startTime[0]*1000+startTime[1]/1e6,diff[0]*1000+diff[1]/1e6,undefined,);enqueue(entry);}最后通过perf_hooks机制通知用户。下面我们通过一个例子看看如何使用。const{PerformanceObserver}=require('perf_hooks');consthttp=require('http');constobs=newPerformanceObserver((items)=>{items.getEntries().forEach((item)=>{console.log(item);});});obs.observe({entryTypes:['http']});constPORT=8080;http.createServer((req,res)=>{res.end('ok');}).listen(PORT,()=>{http.get(`http://127.0.0.1:${PORT}`);});在上面的例子中我们可以收集到两个数据的分布,分别是服务端处理请求所花费的时间和客户端所花费的时间。2.通过perf_hooks收集TCP连接和DNS解析耗时第一次PR集成后,我觉得应该有很多地方可以通过perf_hooks机制收集数据。接下来要做的就是通过perf_hooks收集TCP连接和DNS解析的耗时。在做性能分析和监控的时候,这部分数据也是开发者感兴趣的。这次实现了两个通用的方法来处理一些通用的逻辑,希望在其他地方采集数据的时候可以复用。functionstartPerf(target,key,context={}){if(hasObserver(context.type)){target[key]={...context,startTime:process.hrtime(),};}}functionstopPerf(target,key,context={}){constctx=target[key];if(ctx&&hasObserver(ctx.type)){conststartTime=ctx.startTime;constdiff=process.hrtime(startTime);constentry=newInternalPerformanceEntry(ctx.name,ctx.type,startTime[0]*1000+startTime[1]/1e6,diff[0]*1000+diff[1]/1e6,{...ctx.detail,...上下文。细节},);入队(进入);}}这两个方法的逻辑很简单,startPerf记录了操作的开始时间,并记录了一些context,然后在stopPerf中记录了操作的结束时间并合并了context,最后通过perf_hooks机制通知开发者.另外,这些逻辑只有在开发者注册了perf_hooks的观察者时才会执行,所以对于不开启该功能的开发者来说,不会有性能损失。有了这两种方式,采集数据就变得简单了,只需要找到起点和终点,并添加相应的函数即可。functioninternalConnect(){if(addressType===6||addressType===4){startPerf(self,kPerfHooksNetConnectContext,{type:'net',name:'connect',detail:{host:address,port}});}}internalConnect是发起TCP请求的函数。目前只收集TCP连接的耗时。我们知道net模块也包含了IPC的实现,但是IPC是基于机器的,连接的耗时通常很快。收集这些数据意义不大。另外,只有在连接成功时才会被收集。具体在afterConnect函数中。functionafterConnect(status,handle,req,readable,writable){if(status===0){stopPerf(self,kPerfHooksNetConnectContext);}}接下来,让我们看一个用法示例。const{PerformanceObserver}=require('perf_hooks');constnet=require('net');constobs=newPerformanceObserver((items)=>{items.getEntries().forEach((item)=>{console.log(item);});});obs.observe({entryTypes:['net']});constPORT=8080;net.createServer((socket)=>{socket.destroy();})。listen(PORT,()=>{net.connect(PORT);});通过上面的代码,我们可以收集到TCP连接的耗时。除了支持TCP的耗时收集,这个PR还支持DNS解析的耗时,包括基于Promise的API。原理类似,就不详细介绍了,看一个使用例子。const{PerformanceObserver}=require('perf_hooks');constdns=require('dns');constobs=newPerformanceObserver((items)=>{items.getEntries().forEach((item)=>{console.log(item);});});obs.observe({entryTypes:['dns']});dns.lookup('localhost',()=>{});dns.promises.resolve('localhost');这个PR已经合并进trunk了,应该会在下一个版本中提供。3.通过perf_hooks收集同步文件API比较耗时这个PR一开始我是不想提的。同步API有很多,我要做的就是记录这些API开始和结束的具体时间。审稿人也提出了这个问题。虽然我对此没有特别的异议,但我确实想知道这提供了什么,而将同步调用包装在performance.timerify中不会提供?不过这个PR不只是提一下,我们经常收到业务反馈事件循环延迟报警,具体原因我也不清楚。我们可以很容易地监控事件循环延迟,但很难知道具体原因,因为情况太多了。同步文件操作就是这样一种情况,所以我们要收集这些数据。但我们发现很难在开发人员层面做到这一点。如果我是业务同学,那我需要在每个同步API前加上统计代码。如果我是监控SDK提供者,那我只能劫持文件模块的API。无论哪种方式都不是一个优雅的解决方案。但是如何在Node.js内核中支持它,情况就不一样了。虽然只是简单的把代码搬到了Node.js上,但是可以很好的解决问题。用户每次调用同步API,无论是业务生还是监控SDK提供者,都可以通过perf_hooks机制轻松获取到这些API的耗时数据。于是我回复了审稿人我提这个PR的理由。我认为如果Node.js核心提供这个,用户只需要通过perf_hooks新的观察者来收集fssyncapi的成本时间。否则,所有用户都需要编写一些相同的代码来执行此操作。如果我想提供一个sdk来做到这一点,我需要劫持fsapi。用法也很简单。const{PerformanceObserver}=require('perf_hooks');constfs=require('fs');constobs=newPerformanceObserver((items)=>{items.getEntries().forEach((item)=>{console.log(item);});});obs.observe({entryTypes:['fs']});fs.readFileSync(__filename);这样我们就可以知道readFileSync的耗时了。但是这个PR还没有通过。我仍然希望Node.js能够支持这种能力。4.暴露V8的traceAPINode.js目前实现了trace_events模块,其中的一些trace数据是通过V8提供的traceAPI实现的,但Node.js目前没有暴露。我所做的是公开这个API。这样做的好处是开发者可以利用V8的trace机制生成trace数据,并通过Node.js的trace_events模块和inspector模块收集这些数据。因为我之前开发了一个tracepoint库来做这个,但是Node.js中的支持会比那个好很多。但是Node.js同学说,在使用这个API的时候,他和V8团队达成了协议,不会暴露给开发者。Tsc需要讨论这个问题。以下为回复。不确定我们应该这样做。我的意思是,我通常更喜欢这个,但是当我们添加那个跟踪API时,我们与v8团队达成了协议,我们不会为它公开一个公共API,因为他们可能仍然想改变底层机制并公开api会使这变得更加困难。最近tsc会讨论这个事情,希望能支持这个功能。总结向大型开源项目提交PR是一个很酷但并不容易的过程。首先需要了解提PR的整个过程,然后需要了解相关模块的具体实现逻辑。比如我最近研究了trace模块和perf_hooks模块,然后就可以确定怎么修改代码了。修改后,我需要编写相应的测试。而且它不会影响其他逻辑。完成后需要根据审稿人的建议反复修改。你的想法要得到审稿人的认可并不容易。比如我之前也提到了keepalive和so_resueport的PR,但是因为平台兼容性问题没有收录,因为Libuv是针对跨平台库的。但是,如果一次提交成功,了解整个过程,以后会节省很多时间。最后附上四个PR的地址,有兴趣的同学也可以看看。https://img.ydisp.cn/news/20220902/35swhxbv31f