Skip to main content

Node性能调优

内存泄露

程序运行需要内存,对于持续运行的附近进程,必须及时释放不再用到的内存,否则,内存占用会越来越高,轻则影响系统性能,重则导致进程奔溃。不再用到的内存,没有及时释放,叫做内存泄露。

具体表现

Locale Dropdown

  • 随着内存泄露的增长,V8对垃圾收集器越具有攻击性在,这使得应用运行速度变慢。

  • 内存泄漏可能触发其他类型的失败. 内存泄漏的代码可能会持续引用有限的资源,可能会耗尽文件描述符;还可能突然不能建立连接新的数据库连接。

  • 应用会因为资源耗尽奔溃。

    点击Memory收集内存快照

    内存测试寻找内存泄露

    什么是QPS

    PV:网站当日访问人数UV独立访问人数

    QPS=PV/t

    如:1000000/106060 = 27.7(100万个请求集中在10个小时,服务每秒需处理27.7个业务请求)

wrk压力测试

wrk是一个用来做HTTP benchmark测试的工具

安装wrk

brew install wrk

命令行输入wrk。查看帮助

使用方法: wrk <选项> <被测HTTP服务的URL>                            
Options:
-c, --connections <N> 跟服务器建立并保持的TCP连接数量
-d, --duration <T> 压测时间
-t, --threads <N> 使用多少个线程进行压测

-s, --script <S> 指定Lua脚本路径
-H, --header <H> 为每一个HTTP请求添加HTTP头
--latency 在压测结束后,打印延迟统计信息
--timeout <T> 超时时间
-v, --version 打印正在使用的wrk的详细版本信息

<N>代表数字参数,支持国际单位 (1k, 1M, 1G)
<T>代表时间参数,支持时间单位 (2s, 2m, 2h)

做一次简单压力测试

"scripts": {
"test": "wrk -t12 -c200 -d 60s http://127.0.0.1:3000"
}

启动命令

node index.js
node run test

压测结果如下:

Locale Dropdown

属性名称含义
Avg平均值每次测试的平均值
Stdev标准偏差结果的离散程度,越高说明越不稳定
Max最大值最大结果
+/- SStdev正负一个标准差占比结果的离散程度,越大越不稳定

Latency:响应时间

Req/Sec:每个下沉每秒钟完成的请求数,一般来说主要关注平均值和最大值,标准差如果太大说明样本本身离散程度比较高,有可能系统性能波动很大。

也可以使用autocannon

使用Memeye查看内存

Memeye是一个轻量级的NodeJS进程监控工具,它提供进程内存、V8堆空间内存、操作系统内存三大维度的数据可视化展示。

npm install memeye -D

用法如下:

const http = require('http');
const memeye = require('memeye');
memeye();
let leakArray = [];
const server = http.createServer((req, res) => {
if (req.url == '/') {
leakArray.push(Math.random());
console.log(leakArray);
res.end('hello world');
}
});
server.listen(3000);

再次启动Node服务,会在本地启动另一个服务http://localhost:2333如下:

process.memoryUsage

使用process.memoryUsage可以查看内存使用情况如下:

console.log(process.memoryUsage())

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

key = null
console.log(process.memoryUsage());

终端hi显示内存占用情况

{
rss: 18481152,
heapTotal: 4014080,
heapUsed: 2237704,
external: 764981,
arrayBuffers: 9382
}
{
rss: 61792256,
heapTotal: 46886912,
heapUsed: 44959104,
external: 933057,
arrayBuffers: 9382
}

内存泄露原因以及编码规范

内存泄露

如下针对某个接口进行统计:

console.log('statr', process.memoryUsage())
let leakArray = []
app.get('/', function (req, res) {
leakArray.push('leak' + Math.random())
console.log('end', process.memoryUsage());
res.send('hello word')
})

这时内存会不断膨胀,因为函数内的变量是随着函数执行被回收的,但全局的不行。如果实在业务需要应避免使用对象作为缓存,可移步到Redis等。

队列消费不及时

例:日志收集功能,如果日志产生速度大于文件写入速度,就容易产生内存泄露(压测工具已经收到返回,服务器log4j日志还在不停写入)。简单解决办法可以更换消费速度更快的速度,但这不治本。

根本解决办法应该是监测队列长度,一旦堆积就报警或者拒绝新的请求,还有一种是将所有的异步调用都设置超时时间,一旦达到超时时间调用未得到结果就报警。

编码不规范

闭包

function foo() {
var tem_obj = {
x: 1,
y: 2,
arr: new Array(20000)
}
// 目前闭包只引用了clone
let clone = tem_obj.x
return function () {
return clone
}
}

如上,闭包中只使用了x,那么只需要引入tem_obj.x即可,而不是返回整个tem_obj

  • 闭包不可怕,可怕的是闭包里面引用了大对象

GC无法回收

let map = new Map();
let key = new Array(5 * 1024 * 1024)
map.set(key, 1)
key = null

正确的是先移除map对key的引用,再把key设置null

map.delete(key)
key=null

频繁的垃圾回收会让GC没有机会工作

function strToArray(str, bufferView) {
let i = 0;
const len = str.length;
for (; i < len; i++) {
bufferView[i] = str.charCodeAt(i) + Math.random();
}
return bufferView;
}
function foo() {
let i = 0;
let str = 'test v8 GC';
// SharedArrayBuffer = 连续的内存
let bufferView = [];
while (i++ < 10000) {
strToArray(str, bufferView);
}
}
foo();
[8135:0x1045e7000]       43 ms: Scavenge 2.3 (3.0) -> 1.9 (4.0) MB, 0.8 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
[8135:0x1045e7000] 54 ms: Scavenge 2.4 (4.0) -> 2.0 (5.0) MB, 0.7 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[8135:0x1045e7000] 55 ms: Scavenge 3.0 (5.0) -> 2.0 (7.0) MB, 0.2 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[8135:0x1045e7000] 57 ms: Scavenge 4.0 (7.0) -> 2.0 (7.0) MB, 0.1 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
# 优化后
node --trace-gc demo4.js
[8139:0x1045e7000] 36 ms: Scavenge 2.3 (3.0) -> 1.9 (4.0) MB, 0.8 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[8139:0x1045e7000] 46 ms: Scavenge 2.4 (4.0) -> 2.0 (5.0) MB, 0.8 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[8139:0x1045e7000] 47 ms: Scavenge 3.0 (5.0) -> 2.0 (7.0) MB, 0.2 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure

Buffer

调试工具clinicjs

clinicjs

中文文档

总结

  • 准确计算QPS未雨绸缪
  • 利用压测工具发现内存是否有异常
  • 缓存 队列内存泄露 耗时较长的任务