注:文章整理自腾讯云高级前端工程师陈家兴在Hello Serverless 沙龙深圳站上的演讲,演讲主题为《NodeJS Runtime监控》,感兴趣的读者可关注公众号,后台回复「Serverless 深圳」领取讲师演讲PDF。
根据统计数据,SCF的用户中,NodeJs和Python的用户是最多的,而相信在座的各位应该有很多就是NodeJS的开发者,大家对监控方面有过实践或者感兴趣的话应该能有自己的收获,而如果你不是Node的开发者,那也没关系,其中的很多原理都是相通的,也希望各位能从不同的角度看这个话题,应该能碰撞出更多火花。我这次分享的主题是node JS runtime监控,我这里就先花一点点时间说两句为啥要做监控。
监控的作用
相信在座的各位都是有相当开发经验的开发者了,我们日常是否需要监控?肯定需要!但实际监控都能做到什么,可能有些同学其实并没有太明确的概念。
实际上,监控的作用很简单,只有两个,第一个就是发现问题,监控一般跟数据跟图表挂钩,当数据和图表表现出跟往常不一样的特征的时候,我们马上就能知道,可能是哪里出问题。
而另一个作用就是解决问题,一般来说,不同的问题会呈现不一样的数据特征,比如说,内存呈不断上升的,到了顶峰突然又断崖式地降下来,明显就是内存泄露到最后内存耗尽,最后服务自动重启的特征,通过数据的不同特征,就可以根据经验或者推理,找到问题的原因,进而验证和解决问题。
简单讲完为什么需要监控,就来讲这次的分享的重点,Node Runtiem级监控。
我们先来看看常规的监控,常规的都能监控到什么呢。
- CPU使用率 —— CPU使用的百分比
- 内存使用量
- 出包入包量和网卡流量 —— 互联网基本上所有应用都会跟网络沟通
而runtime级别的监控都能监控到什么呢,CPU使用时间,其中包括系统时间和用户时间,Node程序内内存使用情况,里面包了程序内存消耗总量,实际内存使用量
,空闲内存量,等等。下面还有一个Event loop Lag,我后面再详细说一下。
很明显的对比就是,常规监控都是一个概览额总值,而Runtime级别的监控下是更详细的数据,包括内部使用上面的各方面的细节,而更详细的数据,对开发者无疑就意味着更容易发现问题和解决问题。
大文豪鲁迅曾经说过,Talk is cheap, show me the code。这里先看看API,对于CPU来说,有两个API可以获取到相关的信息,一方面是cpuUsage,另一方面是cpus。
明明都是CPU信息,为什么会在两个不同的库下面呢,其实理解里面的内容就会发现挺有道理的,后者在os库下面,给出的其实是系统CPU的信息,前者放在process库中,是当前进程使用CPU的信息。所以,从runtime级别监控的观点来看,后者其实不是rumtime级别需要关注的,我们需要关注的是前者,前者返回的是类似{ user: 65655, system: 24929 }的结构。
对于内存来说,这里也是主要有两个API可以获取内存相关的信息,先是memoryUsage接口,能获取到。
- rss:node进程总内存占用量
- heapTotal:总堆内存占用量(已申请下来的)
- heapUsed:实际堆内存使用量
- external:扩展等外部程序的内存占用量
而getHeapStatistics获取到的信息跟memoryUsage是有重合的,这里就不详细介绍了,就说两个点,一个是getHeapStatistics能获取到最大可用堆内存,这是memoryUsage没有返回的,至于node可用的最大内存,比较久之前的版本会有即使调整—max-old-space-size也只能到1.7G(64bit)的内存,目前使用的node的版本也都去掉了这个限制。
另一个是,在node10的getHeapStatistics 返回的数据多了两个值。number_of_native_contexts native_context 的值是当前活动的顶层上下文的数量。 随着时间的推移,此数字的增加表示内存泄漏。number_of_detached_contexts detached_context 的值是已分离但尚未回收垃圾的上下文数。 该数字不为零表示潜在的内存泄漏。
关于Event loop lag,Event loop lag直译就是事件循环延迟,我觉得可能叫异步调用延迟会比较合适。
这里我先放一张阮一峰老师用过的@busyrich的一张图,这张图说的是NodeJS的事件循环是怎样运作的,众所周知,NodeJS是单线程的,异步任务的调度在nodeJS的环境下是由LibUV库运作的,我也不再这里长篇大论地解释Event loop了,如果对此还不太清楚的同学可以到阮一峰老师的博客学习一下。这里只说一下这个延时到底是怎么来的,简单点来说,我们设定一个异步任务,在同步队列执行完之后就是马上执行,但是如果同步队列一直被阻塞的话,就是出现异步任务延时执行的现象,这种现象在一些CPU密集型的服务中会比较常见,如果你的服务的异步任务执行延时忽然不正常了,很可能就在某个地方出现了类似死循环的问题,同步任务把队列占满了,当然,死循环往往也会伴随内存泄露出现。
Event loop lag没有API能直接获取,不过测量起来也非常简单,这里放上简单的原理代码。直接利用setTimeout的实际执行时间和设定时间直接的差值,这份代码是我随手撸的用以说明原理的demo,实际使用上还要稍作加工,npm也有库能直接使用,也写得非常简单实用。
监控性能消耗
Runtime级别监控对比外部监控还有一个不一样,就是需要介入到Runtime中,不难想象,做数据的收集肯定是会对性能有一些影响的,可能我们就会担心会不会大幅影响性能呢,为此我特意在云函数上做了一些测试。
在耗时上,在一个比较简单的服务上,添加监控后发现,耗时的确是高了蛮多,但随着测试样本增大,平均耗时有一直下降的趋势,说明添加监控初始化时可能会消耗大概几十ms左右的时间,后面其实每次增加的耗时也就不到1毫秒。
在内存消耗上这种趋势会更加明显一些,需要一点点内存来缓存数据,次数一多起来,差别基本就趋于0了,所以综上所述,rumtime里面添加监控的性能消耗其实都是非常小的。
Runtime Profile
很多时候,监控数据更多用于发现问题,有些更复杂的问题,还得需要更详细的信息,这里就涉及到做Runtime的profile了。
从现实的场景出发,我们往往就像这个狼叔一样,后面的服务器都炸了,可是我们还是一脸懵逼的状态,我们往往发现问题是源于接口调用成功率降低,或者调用时间不正常地变长了,那出现这些问题的原因其实可能有很多,有可能是出现了逻辑死循环,有可能是内存泄漏,频繁触发GC,node的GC其实是非常消耗系统资源的,甚至GC也解决不了问题,导致内存耗尽,需要重启服务,那么出现这些问题的时候除了监控数据,还有什么东西能帮助我排查到底是哪里的问题呢?
这里就需要profile出场了,我会主要介绍两种profile,一个是heap dump,也就是内存快照,另外就是CPU profile。
估计很多开发者都会接触过heap dump,一般遇到疑似内存泄漏问题,第一反应就是打快照,这里就是快照导入到chrome devtool后的效果。
V8-profiler(v8-profiler-node8)
Heapdump
这两个包都可以做内存的heapdump,原理都是一样的,具体用法大家可以到对应的包下面看看用法。
得出的包导入到chrome后可以查看里面的内容了,主要有这几种模式:
- Summary
- Comparison
- Containment
- Statistics
summary就是一个总览,可以看到不同constructor创建的对象占用内存的细节。
Comparison 是调试时候最常用的一栏,用于对比不同时间点的快照之间的异同,一般来说,在问题发生前,和问题发生后进行快照,然后对比两者之间新增了哪些内容,就比较容易找出问题的原因。
Containment 视图允许您探索堆内容。此视图提供了一种更好的对象结构视图,有助于分析全局命名空间 (window) 中引用的对象以找出是什么让它们始终如影随形。使用此视图可以分析闭包以及在较低级别深入了解您的对象。
Statistics 就是以饼图的形式给出不同内容的占比。
CPU profile可以通过查看不同函数的运行占用CPU时间,看出什么地方占用了大量CPU时间,这就就是看cpu profile的入口,在调试工具窗口的右上角更多栏打开,more tools,javascript profile栏下,嗯,没错,我之前也在吐槽,为啥这个玩意要藏这么深,其实是有原因的,chrome的devtool也是不断在进化,而这个js profile的功能其实已经很大程度上整合到了performance tab之下了,performance tab下面的除了jsprofile外,还会dom渲染,图片加载等页面性能的细节,可惜这些特性对node也是没用的,所以我们目前还是在用的原来的这个工具
同样,通过V8-profiler可以获取CPU profile。
同样有几个视图:
- Tree 则是通过调用树的顺序,列出不同函数的消耗时间。
- 这个就是charts视图下的时序火焰图,能更好地以时间为轴看到整个代码执行的过程。
- Chart 通过时序火焰图的形式展示不同函数的时间占用。
能更好地以时间为轴看到整个代码执行的过程。
通过flamegraph 这个npm包也能根据CPU profile生成火焰图的SVG进而进行查看,通过火焰图,能非常直观地看出,到底是哪个进程消耗了大量的时间。
值得留意的是,这个火焰图跟前面说的charts视图下的时序火焰图是不一样的,这个火焰图会根据相同的函数进行归类,能比较直观的看出其中耗时最长的步骤
说了这么多,跟severless有啥关系呢,这些东西如何在SCF上实现呢?
云函数下的NodeJS Runtime监控
serverless的程序也需要做监控嘛,原理上面都讲了,收集的数据放到DB,profile放到cos,然后慢慢分析就好啦。
SCF一键开启吼不吼啊,在SCF上,性能分析都能自动化,惊不惊喜,意不意外,目前SCF上一键开启分析功能已经在开发中,具体会做些什么呢?
Serverless 虽然是不用关心服务器等基础设施,但是程序的性能还是要关注的,毕竟128m比256m便宜,更快的处理 & 响应速度也非常重要。
其次在云函数上,每次调用的确是相互独立的,可是容器和实例都是必须复用的,关于冷启动和热启动的话题也总是云函数的热门话题,如果函数频繁遇到冷启动,除了调度问题外,也有可能是代码本身的问题,比如函数调用多次后出现内存泄漏导致out of memory,也就是内存耗尽了,自然会触发重启,出现冷启动。
所以,根据云函数的特性,我们对收集的信息也会进行定制化,首先收集快照的时机会在函数执前跟执行后,这样就能很直观地看到函数执行过程中内存的变化,而cpu profile则会全过程收集,详细记录函数执行的全过程。
另外,我们还会输出函数调用过程中的GC log,以便进行进一步的收集。
总而言之,无论常规监控还是runtime级别的监控,都是帮助我们更好地把握程序的健康状况,对我们日常开发运维都非常重要。同时,经过测试,即使性能消耗比较高的runtime级别监控,其实其性能消耗也在控制范围以内,在不添加太多工作量的前提下,我们都应该尽量全方位地监控我们的程序。
另一方面,如何在发现除了问题的时候更好地解决问题,做Profile是最高效的方法。
而云函数,或者说Severless,都在尽己所能为所有的开发者服务,降低接入和分析门槛,提供最方便的监控和profile服务。