NodeJS 代码调试、性能调优

参考链接:http://www.barretlee.com/blog/2015/10/07/debug-nodejs-in-command-line/

命令行调试

NodeJS 给我们提供了 Debugger 模块,内建客户端,通过 TCP 将命令行的输入传送到内建模块以达到调试的目的。

在启动文件时,添加第二个参数 debug:

bcaring:bqxu.github.io bcaring$ node debug assets/node/2016-09/debugger.js
< Debugger listening on port 5858
connecting to 127.0.0.1:5858 ... ok
break in assets/node/2016-09/debugger.js:1
> 1 'use strict';
  2 let i = 0;
  3
debug>

调试代码的时候存在两个状态,

一个是操作调试的位置,比如下一步,进入函数,跳出函数等,此时为 debug 模式;

另一个是查看变量的值,比如进入循环中,想查看循环计数器 i 的值,此时为 repl(read-eval-per-line) 状态

在 debug 模式下输入 repl 即可进入 repl 状态:

debug> repl
Press Ctrl + C to leave debug repl
> process
{ title: '/usr/local/bin/node',
  version: 'v6.2.0',
  moduleLoadList:
...

按下 Ctrl+C 可以从 repl 状态回到 debug 状态下,

debug 状态下有多少调试命令,执行 help 即可:

debug> help
Commands: run (r), cont (c), next (n), step (s), out (o), backtrace (bt), setBreakpoint (sb), clearBreakpoint (cb),
watch, unwatch, watchers, repl, exec, restart, kill, list, scripts, breakOnException, breakpoints, version

常用的命令

命令 解释
cont, c 进入下一个断点
next, n 下一步
step, s 进入函数
out, o 跳出函数
setBreakpoint(), sb() 在当前行设置断点
setBreakpoint(line), sb(line) 在 line 行设置断点

更多命令

NodeJS的调试原理

当我们使用 debug 参数打开一个 node 文件时,会输出这样一行文案:

Debugger listening on port 5858

可以访问下 http://localhost:5858

除了在命令行中直接调试之外,我们还可以通过另外两种方式去调试这个代码:

  • node debug , 通过 URI 连接调试,如 node debug localhost:5858
  • node debug -p 通过 PID 链接调试

如果我们使用 –debug 参数打开文件:

此时,nodejs 不会进入到命令行模式,而是直接执行代码,但是依然会开启内建调试功能,这就意味着我们具备了远程调试 NodeJS 代码的能力,使用 –debug 参数打开服务器的 nodejs 文件,然后通过:

node debug <服务器IP>:<调试端口,默认5858>

可以在本地远程调试 nodejs 代码。

不过需要区分下 –debug 和 –debug-brk, 前者会执行完所有的代码,一般是在监听事件的时候使用, 而后者,不会执行代码,需要等到外部调试接入后,进入代码区。

默认端口号是 5858,如果这个端口被占用,程序会递增端口号,我们也可以指定端口:

更多的调试方式

node-inspector

NodeJS 提供的内建调试十分强大,它告诉 V8,在执行代码的时候中断程度,等待开发者操控代码的执行进度。我们熟知的 node-inspector 也是用的这个原理。

node-inspector --web-port 8080 --debug-port 5858

这里的 –web-port 是 Chrome Devtools 的调试页面地址端口,–debug-port 为 NodeJS 启动的内建 debug 端口,我们可以在 http://localhost:8080/debug?port=5858 打开页面,调试使用 –debug(-brk) 参数打开的程序。

官网

IDE调试

发现程序的问题

内存飙高

首先来分析下问题,内存飙高存在哪些方面的因素呢:

  • 缓存,很多人在敲程序的时候把缓存当内存用,比如使用一个对象储存用户的 session 信息
  • 闭包,作用域没有被释放掉
  • 生产者和消费者存在速度差,比如数据库忙不过来,Query 队列堆积

CPU 负载过高预警可能因素:

  • 垃圾回收频率过高、量太大,这一般是因为内存或者缓存暴涨导致的
  • 密集型的长循环计算,比如大量遍历文件夹、大量计算等

最直接的手段就是分析 GC 日志

  • 内存暴涨,尤其是 Old Space 内存的暴涨,会直接导致 GC 的次数和时间增长
  • 缓存增加,导致 GC 的时间增加,无用遍历过多
  • 密集型计算,导致 GC Now Space次数增加

分析 GC 日志

如何去分析 GC 的日志?

在启动程序的时候添加 –trace_gc 参数,V8 在进行垃圾回收的时候,会将垃圾回收的信息打印出来:

 node --trace_gc app.js

更多启动选项:

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

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

然而这堆日志并不太好看,我们可以将日志输出来之后交给专业的工具帮我们分析,

相比很多人都用过 Chrome DevTools 的 JavaScript CPU Profile,

通过 Profile 可以找到具体函数在整个程序中的执行时间和执行时间占比,

从而分析到具体的代码问题,V8 也提供了 Profile 日志导出:

 node --prof app.js

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

sudo npm install tick -g
node-tick-processor *-v8.log

性能优化技巧

避免使用同步代码

在设计上,Node.js是单线程的。 为了能让一个单线程处理许多并发的请求,你可以永远不要让线程等待阻塞,同步或长时间运行的操作。 Node.js的一个显著特征是:它从上到下的设计和实现都是为了实现异步。这让它非常适合用于事件型程序。 不幸的是,还是有可能会发生同步/阻塞的调用。 例如,许多文件系统操作同时拥有同步和异步的版本,比如writeFile和writeFileSync。 即使你用代码来控制同步方法,但还是有可能不注意地用到阻塞调用的外部函数库。 当你这么做时,对性能的影响是极大的。

我们的初始化log在实现时无意地包含了一个同步调用来将内容写入磁盘。 如果我们不做性能测试那么就会很容易忽略这个问题。 当以developer box中一个node.js实例来作为标准测试,这个同步调用将导致性能从每秒上千次的请求降至只有几十个。

关闭套接字池

Node.js的http客户端会自动地使用套接字池: 默认地,它会限制每台主机只能有5个套接字。 虽然套接字的重复使用可能会让资源的增加在控制之下, 但如果你需要处理许多数据来自于同一主机的并发请求时, 将会导致一系列的瓶颈。 在这种情况下,增大maxSockets 的值或关闭套接字池是个好主意:

// Disable socket pooling

var http = require('http');
var options = {.....};
options.agent = false;
var req = http.request(options)

不要让静态资源使用Node.js

使用gzip

许多服务器和客户端支持gzip来压缩请求和应答。 无论是应答客户端还是向远程服务器发送请求,请确保充分使用它。

并行化

试着让你所有的阻塞操作-向远程服务发送请求,DB调用,文件系统访问并行化。 这将能减少最慢的阻塞操作的等待时间,而不是所有阻塞操作的等待时间。 为了保持回调和错误处理的干净,我们使用Step来控制流量。

Session自由化

许多express的例子都包含如下的配置: app.use(express.session({ secret: “keyboard cat” })); 默认地,session数据是存储在内存中的,这会给服务器增加巨大的开销,特别是随着用户量的增长。

你可以使用一个外部session存储,比如MongoDB或Redis,不过每一个请求将会导致远程调用来取得session数据的开销。 在可能的情况下,最好的选择就是在服务器端存储所有的无状态数据。 通过不包含上述express配置让session自由化,你会看到更好的性能。

使用二进制模块

如果可能,用二进制模块取代JavaScript模块。

// Use built in or binary modules
var crypto = require('crypto');
var hash = crypto.createHmac("sha1",key).update(signatureBase).digest("base64");

用标准的 V8 JavaScript 取代客户端库