思考一下node在服务上的代码调试和性能调优

前言

在服务器上调试node,是一件救火行为,因为,如果是本地开发,通过IDE提供的调试平台我们可以进行一步一步的调试,在远程服务器上调试,会中断服务进程,造成其他错误,推荐利用单元测试工具和做好合理的日志进程调试,万不得已,后院着火了,再使用服务器上的调试方法。

Debugger

V8提供了标准的调试API,这个工具在node中是通过在语句中添加debugger的形式进行的,同时,启动的时候也需要带上debug参数才可以实现。详见下方

node debug xxx.js
x = 5;
setTimeout(function () {
debugger;
console.log("world");
}, 1000);
console.log("hello");

$ node debug examples/B/myscript.js
< debugger listening on port 5858
connecting... ok
break in examples/B/myscript.js:2
1 // myscript.js
2 x = 5;
3 setTimeout(function () {
4 debugger;
debug>

代码中遇到debugger;后会中断,可以通过以下的命令进行debug。

命令 说明
cont 或者c,继续执行
next 或者n,执行到下一个断点
step 或者s,步进到函数内部
out 或者o,从函数内部跳出
pause 暂停执行
setBreakpoint() 或者sb(),在当前行设置断点
setBreakpoint(lineNo) 或者sb(lineNo),在指定行设置断点
setBreakpoint('fn()') 或者sb('fn()'),在函数体的第一个声明处设置断点
setBreakpoint('xxx.js') 或者sb('xxx.js'),在脚本文件的第一行设置断点
clearBreakpoint 或者cb(),清除断点
backtrace 或者bt,打印当前执行情况下的堆栈信息
list(5) 列出当前上下文前后5行的源代码
watch(expr) 添加表达式到观察者列表,进行观察
unwatch(expr) 从观察列表中移除对表达式的观察
watchers 列出所有观察的表达式和值
repl 打开调试的交互,用于执行调试脚本的上下文

对于已经启动的程序如何debug

通过向程序发送SIGUSR1信号开启调试,kill -s USR1 10093,可以在浏览器上开启调试,使用工具Node Inspector。

安装:npm install -g node-inspector

启动的时候,直接node-inspector即可.....

当然,使用node Inspector必须先启用node进程的调试模式,也就是使用debug或者发送SIGUSR1给node进程来启用调试模式,启动进程后,调用Inspector工具,工具就与node进程建立了调试代理:

$ node-inspector
Node Inspector v0.5.0
info - socket.io started
Visit http://127.0.0.1:8080/debug?port=5858 to start debugging

剩下的就跟在IDE中调试代码一样了。

node调试的原理

在打开调试的时候,我们会看到这样的输出:

Debugger listening on port 5858

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

看到如下信息:

Type:connect
V8-Version:4.2.15.19
Protocol-Version:1
Embedding-Host:node v4.2.15
Content-Length:0

这个其实就是node调用呢内建调试功能,并监听端口5858来使用调试命令。我们还可以使用

node debug 
node debug -p 

来调用

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

node --debug xxxx.js

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

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

可以在本地远程调试 nodejs 代码。不过这里需要区分下 --debug 和 --debug-brk,前者会执行完所有的代码,一般是在监听事件的时候使用,而后者,不会执行代码,需要等到外部调试接入后,进入代码区。语言表述不会那么生动,读者可以自行测试下。

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

node  node --debug-brk=8787 xxxx.js
Debugger listening on port 8787

不过在最新的node中,调试的原理以及有所改变:

思考一下node在服务上的代码调试和性能调优_第1张图片
调试

可以看到直接推荐使用Inspector了,因此,大家也就不必太过于在乎调试了,还是那句话:服务器上的debug比较麻烦,还是通过良好的单元测试服务器日志进行错误排查。

性能调试

我们可以使用pm2这样的检测工具来检测node,现在我们想先分享一下产生问题的原因。或者说是什么问题造成了cpu或内存达到瓶颈,从而导致系统奔溃的呢?在菜谱负载过高,内存突然飙升的时候又如何发现这些问题呢?

分析原因

cpu负载过重的原因

1.垃圾回收频率过高、量过大,也给也是因为内存飙升造成的后遗症
2.计算密集型的长循环,比如遍历大量的文件、文件夹、大量的计算等

内存飙升的原因

1.缓存,很多人把内存当缓存用,比如session存在内存中
2.闭包,作用域没有被及时释放,造成常驻内存占用过多
3.生成者的生产能力和消费者的消费能力不匹配,例如数据库已经忙不过来了,但是Query队列中还有大量请求堆积。

解决手段

一般情况下,我们可以通过分析GC日志来查找问题:

问题 解释
内存飙升 特别是old space内存的飙升,会直接导致GC的次数和时间增长
缓存增加 导致GC的时间增加,无用遍历过多(如果缓存增加(比如使用对象缓存了很多用户信息),GC 是不知道这些缓存死了还是活着的,他们会不停地查看这个对象,以及这个对象中的子对象是否还存活,如果这个对象数特别大,那么 GC 遍历的时间也会特别长。当我们进行密集型计算的时候,会产生很多中间变量,这些变量往往在 New Space 中就死掉了,那么 GC 也会在这里多次地进行 New Space 区域的垃圾回收。
密集型计算 导致GC new space次数增加。(也就是前边说过的scavenage操作的cheney算法)

分析GC日志

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

node --trace_gc aa.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 日志,对于排查隐晦问题比较有用

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

$ node --prof test.js
//执行命令之后,会在该目录下产生一个 *-v8.log 的日志文件,我们可以安装一个日志分析工具 tick:
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: ~ 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
...

解决问题

对于内存当做缓存的,我们可以引入外部缓存如redis等,对于闭包,我们可以人为控制闭包的释放,对于大文件,我们可以将其分解为小文件迭代的去读取,总之,方法多多,大家可以继续探讨

你可能感兴趣的:(思考一下node在服务上的代码调试和性能调优)