Node.js性能调优

上篇文章:《Node.js断言方案》介绍了Node.js断言技术,现在我们再来说说有关性能调优相关技术。

性能调优

说完了Node.js调试方面需要掌握的基本技能,下面说一下性能调优相关的技巧。
Node.js相比Java、PHP这些老牌语言,基础设施还是有所欠缺的:比如性能分析和监控工具等,加上它的单线程运行特性,在大型应用中,很容易让系统的CPU、内存或者事件队列太满达到瓶颈,从而导致程序崩溃。一旦发现程序警报CPU负载过高,或者内存飙高时,我们该如何排查Node.js代码存在的问题呢?首先先分析一下问题。

内存持高存在的因素:

  • 缓存,很多人在code的时候把内存当缓存用,e.g. 使用js对象储存用户的session信息
  • 闭包,作用域长时间不能被释放掉
  • 生产者和消费者存在速度延迟,e.g. 数据库忙不过来,query队列堆积

CPU负载过高可能因素:

  • 垃圾回收频率过高、量太大,这一般是因为内存或者缓存暴涨导致的
  • 密集型的长循环计算,e.g. 大量遍历文件夹、大量计算等
  • 客户端并发数太大,e.g. 利用node作为socket服务端时,客户端连接数太大时,心跳处理也属于一次网络io,最终libuv转换成事件,严重影响业务消息发送

这些问题都是让人头疼的,一个项目几十上百个文件,收到这些警报如果没有经验,根本无从下手排查。

最直接的手段就是分析 GC 日志,因为程序的一举一动都会反馈到 GC 上,而上述问题也会一一指向 GC,如:
内存暴涨,尤其是 Old Space 内存的暴涨,会直接导致 GC 的次数和时间增长
缓存增加,导致 GC 的时间增加,无用遍历过多
密集型计算,导致 GC Now Space次数增加

ps:阿里出了一套调优工具alinode,看起来比较不错,感兴趣的童鞋可以试试,顺便反馈一下情况。😄

内存管理

有关Node.js内存管理和垃圾回收机制,这里简单介绍一下,用心的同学可以去搜索一下这方面的资料。
V8把内存分为 New Space 和 Old Space,New Space 的大小默认为 8M,Old Space 的大小默认为 0.7G,64位系统这两个数值翻倍。
对象的生命周期是:首先进入 New Space,然而 New Space 被平均分为两份,当一次GC运行后都会将其中一份的活着的对象复制到另一份,所以它的空间使用率是50%,这个算法叫做 Cheney 算法,这个操作叫做 Scavenge。再过一段时间,如果 New Space 中的对象还活着,这些对象会被挪到 Old Space 中去,GC 会每隔一段时间遍历 Old Space 中死掉的对象,然后整理碎片(这里有两种模式 mark-sweep 和 mark-compact,不赘述)。上面提到的死掉的对象指的是对象已经没有被引用了,或者说被引用的次数为零了。

分析GC日志

上面说完Node.js内存管理后,下面我们开始分析具体问题了,e.g. 假设缓存增加(比如使用对象缓存了很多用户信息),GC 是不知道这些缓存死了还是活着的,他们会不停地查看这个对象,以及这个对象中的子对象是否还存活,如果这个对象数特别大,那么GC遍历的时间也会特别长。
当我们进行密集型计算的时候,会产生很多中间变量,这些变量往往在New Space中就死掉了,那么GC也会在这里多次地进行New Space区域的垃圾回收。
如何分析?
Node在启动程序的时候添加--trace_gc参数,当V8在进行垃圾回收的时候,会将垃圾回收的信息打印出来:

➜  $ node --trace_gc test.js
                
...
[94036] 68 ms: Scavenge 8.4 (42.5) -> 8.2 (43.5) MB, 2.4 ms [allocation failure].
[94036] 74 ms: Scavenge 8.9 (43.5) -> 8.9 (46.5) MB, 5.1 ms [allocation failure].
[94036] Increasing marking speed to 3 due to high promotion rate
[94036] 85 ms: Scavenge 16.1 (46.5) -> 15.7 (47.5) MB, 3.8 ms (+ 5.0 ms in 106 steps since last GC) [allocation failure].
[94036] 95 ms: Scavenge 16.7 (47.5) -> 16.6 (54.5) MB, 7.2 ms (+ 1.3 ms in 14 steps since last GC) [allocation failure].
[94036] 111 ms: Mark-sweep 23.6 (54.5) -> 23.2 (54.5) MB, 6.2 ms (+ 15.3 ms in 222 steps since start of marking, biggest step 0.3 ms) [GC interrupt] [GC in old space requested].
...

V8也提供了很多程序启动选项:

选项 含义
–max-stack-size 设置栈大小
–v8-options 打印V8相关命令
–trace-bailout 查找不能被优化的函数,重写
–trace-deopt 查找不能优化的函数

这些选项可以让我们查看V8在执行时的各种log日志,对于排查隐晦问题比较有用。

然而这堆日志看起来很糟糕,可以先将日志输出来之后交给专业的工具帮我们分析,相信大部分人都用Chrome DevTools的JavaScript CPU Profile:

通过Profile工具可以找到具体函数在整个程序中的执行时间和执行时间占比,从而分析到具体的代码问题,V8 也提供了Profile日志导出:

node --prof test.js

执行命令之后,会在该目录下产生一个 *-v8.log 的日志文件,我们可以安装一个日志分析工具 tick:

➜  $ sudo npm install tick -g
➜ $ node-tick-processor *-v8.log
[Top down (heavy) profile]:
Note: callees occupying less than 0.1% are not shown.
inclusive self name
ticks total ticks total
426 36.7% 0 0.0% Function: ~<anonymous> node.js:27:10
426 36.7% 0 0.0% LazyCompile: ~startup node.js:30:19
410 35.3% 0 0.0% LazyCompile: ~Module.runMain module.js:499:26
409 35.2% 0 0.0% LazyCompile: Module._load module.js:273:24
407 35.1% 0 0.0% LazyCompile: ~Module.load module.js:345:33
406 35.0% 0 0.0% LazyCompile: ~Module._extensions..js module.js:476:37
405 34.9% 0 0.0% LazyCompile: ~Module._compile module.js:378:37
...

也可以使用headdump之类的工具将日志导出,然后放到Chrome的Profile中去分析,如果看过深入浅出node.js的童鞋可以参考内存调优的章节。

总结

对于任何一门程序而已,内存调优都是比较难的,这方面是需要智慧和经验,然而并不要被困难吓到,不断去折腾,相信每一次都会有新的收获。

参考:
https://nodejs.org
https://iojs.org
http://www.barretlee.com/blog/2015/10/07/debug-nodejs-in-command-line/


博客:https://vectorho.github.io
作者:Vector Ho