![692fa68df4a160a460aed9d7c6abb4d4.png](https://i-blog.csdnimg.cn/blog_migrate/4bbe3bd2a4a75865f9794a713cd59574.jpeg)
字节跳动内推啦
![3af67526c0784d52772de0d34bb3a3ec.png](https://i-blog.csdnimg.cn/blog_migrate/d09ced0187e1493811bc7d29f0ffd0dc.jpeg)
常见性能问题
内存泄露
- 作用域未释放:闭包及时释放
- 缓存空间没有限制:利用lru-cache模块
- 队列消费不及时:限制队列长度
线程阻塞
- 采用单进程方案+线程池方案实现对大并发的支持
- node 模块中IO密集型任务、CPU密集型任务都是采用线程池完成
- 自定义任务也可以通过C++模块箱libuv提交任务
- 事件轮询线程本身并不维护队列,而是一大堆文件描述符
![6f82064b789753666f007e19972105c1.png](https://i-blog.csdnimg.cn/blog_migrate/2166297b7b58d4cbdccc8126aeabcaca.jpeg)
同步IO调用(fs.xxxSync)
直接占用事件循环线程
- 使用异步调用
- 调用函数warp后自动检测并告警和统计
CPU密集型计算占用线程池、或事件循环线程
- 避免在事件循环线程处理CPU密集型任务
- 最小化任务执行时间
- 使用worker线程
- 拆分任务,分片执行任务
正则表达式攻击 REDOS
- 避免使用嵌套量词 /(a+)*b/
- 避免使用或的重复量词 /(a|ab)*/
- 避免使用回溯
- 简单匹配使用indexOf
- 使用safe-regex校验
JSON DOS
- JSON.parse/stringify 有高开销的操作,本身复杂度是O(n)
- 限制客户端json对象大小
代码上常见的优化方法
- 避免使用:debugger,eval,with
- 避免使用 delete,替换为 undefined
- 数据处理考虑使用lodash的异步计算,避免使用多次循环
- ....
常见有哪些性指标
- CPU占用
- 内存占用
- 请求成功率
- qps 请求处理能力
- TPS 事务处理能力
常见监控
- 容器负载监控
- 服务特定指标监控
- 服务日志监控
- CPU、内存监控
性能评估
压力测试工具
常见工具
httpload、wrk、apache bench、vegeta
wrk工具使用
- 安装
brew install wrk
- 运行压力测试
wrk -t12 -c400 -d30s https://cs.snssdk.com
#
参数解释:
-c, --connections # 总的http并发数
-d, --duration # 持续压测时间, 比如: 2s, 2m, 2h
-t, --threads # 总线程数
-s, --script # luajit脚本,使用方法往下看
-H, --header # 添加http header, 比如. "User-Agent: wrk"
--latency # 在控制台打印出延迟统计情况
--timeout # http超时时间
- 压力测试输出结果
Running 30s test @ https://cs.snssdk.com
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 520.26ms 402.04ms 2.00s 77.85%
Req/Sec 29.02 16.52 110.00 61.88%
10046 requests in 30.10s, 117.07MB read
Socket errors: connect 0, read 0, write 0, timeout 1446
Requests/sec: 333.78
Transfer/sec: 3.89MB
分析工具
【CPU分析】Flame Graph 火焰图
火焰图(Flame Graph) 用于 CPU 的使用情况可视化,可以直观地了解到程序的性能瓶颈。我们通常要结合操作系统的性能分析工具(profiling tracer)使用火焰图,常见的性能分析工具如下:
- Linux: perf
- windows: xpref.exe
- Mac OS: DTrace and Instruments
- npm: 0x
perf使用
perf_events(简称 perf)是 Linux Kernal 自带的系统性能分析工具,能够进行函数级与指令级的热点查找。它基于事件采样原理,以性能事件为基础,支持针对处理器相关性能指标与操作系统相关性能指标的性能剖析,常用于查找性能瓶颈及定位热点代码。
- 安装centos 虚拟机
- 直接使用DevBox机器,上面已经安装了Perf软件
- mac上面推荐使用Parallels软件,安装 centos 虚拟机
- 安装软件
sudo yum install perf # centos
npm i -g stackvis # 火焰图生成工具,不太好使
FlameGraph clone仓库 + 添加path
3. 创建Nodejs测试项目,生成性能文件
开发阶段使用
perf record -e cycles:u -g -- node --perf-basic-prof lib/dispatch.js -w 1 -e development
#启动服务
生产环境使用
perf record -F 99 -p PID -g -- sleep 60
# -F 采样率99HZ, -p 采样进程ID, -g 表示记录调用栈 持续时间 60S
- 压力测试
执行命令
wrk -t12 -c400 -d30s http://127.0.0.1:8890/
# ip换成你的服务器IP
2. 生成火焰图
推荐使用FlameGraph生成
运行命令 perf script > perfs.out 导出堆栈信息
利用stackvis生成
stackvis perf < perfs.out > flamegraph.htm
利用FlameGraph 生成
stackcollapse-perf.pl --kernel < ./perfs.out | flamegraph.pl --color=js --hash > ./flamegraph.svg
其他问题:关闭 centos 防火墙关闭方式
0x使用
安装软件
npm i -g 0x # 安装软件
启动服务
0x lib/dispatch.js # 使用0x启动Node服务
启动压力测试
./wrk.sh
自动生成flamegraph文件
【内存分析】heapdump
Make a dump of the V8 heap for later inspection.
Heapdump
安装软件
npm i heapdump
dispatch.js
require('heapdump');# 可以仅仅引用即可,通过信号产生快照
执行快照生成
kill -USR2 `pgrep -n node`
小技巧:可以导出多个快照,通过对比快照之前的差异发现GC异常,切多个快照的时间尽可能长一些,这样存在问题的差异会更大,比较容易看出问题
Chrome DevTools(Memory)
通过DevTools -> Memory -> Profiles -> Load 导入生成的快照,导入后如下图:
![6ca654429a762d87e5ba043a80fa80e1.png](https://i-blog.csdnimg.cn/blog_migrate/1d08ce200a8f43f4af05be616aaa145a.jpeg)
快照展示方式有4种:
- Summary:以构造函数名分类显示
- Comparison:比较多个快照之间的差异
- Containment:查看整个 GC 路径
- Statistics:以饼状图显示内存占用信息
切换到Summary视图
![ffca753deb3890410cd12a557e910c2d.png](https://i-blog.csdnimg.cn/blog_migrate/4cfc7e5dc101b76e78e0fd533195a12c.jpeg)
相关信息解释:
- Constructor: 构造函数名称,带有括号的表示内置对象,比如:(string),(array)
- Distance: 距离GC Root对象的距离,距离越远就越可能存在内存泄露可能
- Shallow Size:对象自身占用内存大小
- Retained Size: 对象+对象引用的占用内存总和
如何分析内存泄露?
这是一份经典的内存泄露代码演示代码
let leakObject = {};
let count = 0;
async memory(ctx: RouterContext) {
setInterval(() => {
const originLeakObject = leakObject;
const unused = function() {
if (originLeakObject) {
console.log('originLeakObject');
}
};
leakObject = {
count: String(count++),
leakStr: new Array(1e7).join('*'),
leakMethod: function() {
console.log('leakMessage');
},
};
}, 100);
ctx.body = 'ok';
}
这份在Demo仓库中,项目运行后,访问http://127.0.0.1:8890/memory/,我们导出一份内存快照,过1-2分钟导出第二份快照,并导入ChromeDevTools中进行分析。
- 对比快照区别,首先选择第二份快照,然后在视图上面切换为Comparison,在后面选择第一份快照
![277be8d24e058232fddffb50d59787ca.png](https://i-blog.csdnimg.cn/blog_migrate/6b4113257eebc02f733f63132476ad4a.jpeg)
对比时,可以关注SizeDelta 和FreedSize,SizeDelta记录增加或者减少,FreedSize则是内存释放大小。对于这个例子来说,对比大小增加了244134字节,释放了0字节。内存没有释放,但是一直在增加,说明这个 string 对象有问题,
![3439e53851421343a9b26f45575ad1bf.png](https://i-blog.csdnimg.cn/blog_migrate/83bb01575f458c1eb2d104cd4f7689a4.jpeg)
展开对象,可以看到一层一层的嵌套引用,其中会显示变量名称,可以通过变量名称去找到代码位置进行进一步分析排查。
开箱即用的工具node-clinic
Clinic.js diagnoses your Node.js performance issues
- 可视化的CPU占用+内存占用图
- 生成CPU 火焰图
- 生成特有的bubbleprof 图
安装
npm install -g clinic
生成诊断图
启动服务
clinic docker -- node lib/dispatch.js # 启动服务
wrk -t12 -c400 -d30s http://127.0.0.1:8890/cpu
# 启动压力测试
停止服务
CTRL+C 停止服务,自动生成诊断文件
![e5cb98d6be90d3a4cc9936a5748d5777.png](https://i-blog.csdnimg.cn/blog_migrate/00c9ebc7bebde4a5b83a8c1f589b3f30.jpeg)
Event Loop 被阻塞,CPU 也居高不下,一定是有 CPU 密集计算,接下来可以使用提供的火焰图来进行分析。
火焰图
启动服务
clinic flame -- node lib/dispatch.js # 启动服务
wrk -t12 -c400 -d30s http://127.0.0.1:8890/cpu
# 启动压力测试
停止服务
CTRL+C 停止服务,自动生成诊断文件
![f8cb67572c512340e6d89bcfc6015293.png](https://i-blog.csdnimg.cn/blog_migrate/24a717efba14913ee98868abb54f86d9.jpeg)
Bubbleprof图
启动服务
clinic bubbleprof -- node lib/dispatch.js # 启动服务
wrk -t12 -c400 -d30s http://127.0.0.1:8890/cpu
# 启动压力测试
停止服务
CTRL+C 停止服务,自动生成诊断文件
![0941e0703e5fcfafe3121f1b18be09c5.png](https://i-blog.csdnimg.cn/blog_migrate/ecac2e8e74551fd217cf11c2167c07ec.jpeg)
其他分析工具
- v8-profiler HEAP Snapshot API + CPU Profile
- memwatch GC回收监控
- benchmark 测试函数性能
Node.js 提供的性能测量模块
- 跟踪事件:提供集中接口管理性能事件、异步钩子事件、启动性能等信息监控的能力
- 性能钩子:用于性能测量,例如打点、函数性能测试
import { PerformanceObserver, performance, monitorEventLoopDelay } from 'perf_hooks';
import crypto from 'crypto';
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach((item) => {
console.log(item.name, item.duration);
});
});
obs.observe({ entryTypes: ['function', 'measure'], buffered: true });
function jobA() {
const salt = crypto.randomBytes(128).toString();
const encryptedPassword = crypto.pbkdf2Sync('test', salt, 100000, 64, 'sha512').toString();
return encryptedPassword;
}
function jobB() {
let x = 0;
for (let i = 0; i < 10000; i++) {
x += i;
}
}
const el = monitorEventLoopDelay({ resolution: 10 });
el.enable();
performance.mark('1');
jobA();
performance.mark('2');
jobB();
performance.mark('3');
performance.measure('jobA', '1', '2');
performance.measure('jobB', '2', '3');
function someFunction() {
jobA();
jobB();
}
const wrapped = performance.timerify(someFunction);
wrapped();
const mod = require('module');
// mod.Module.prototype.require = performance.timerify(mod.Module.prototype.require);
// require = performance.timerify(require);
el.disable();
setImmediate(() => {
console.log(el);
});
异步钩子:跟踪异步操作,例如socket链接、文件操作等
import fs from 'fs';
import asyncHooks from 'async_hooks';
let indent = 0;
const eid = asyncHooks.executionAsyncId();
const tid = asyncHooks.triggerAsyncId();
console.log(eid, tid);
asyncHooks
.createHook({
init(asyncId, type, triggerId) {
const cId = asyncHooks.executionAsyncId();
print(`${getIndent(indent)}${type}(${asyncId}): trigger: ${triggerId} scope: ${cId}`);
},
before(asyncId) {
print(`${getIndent(indent)}before: ${asyncId}`);
indent += 2;
},
after(asyncId) {
indent -= 2;
print(`${getIndent(indent)}after: ${asyncId}`);
},
destroy(asyncId) {
print(`${getIndent(indent)}destroy: ${asyncId}`);
},
})
.enable();
function print(str: string) {
fs.writeSync(1, str + 'n');
}
function getIndent(n: number) {
return ' '.repeat(n);
}
Debug
Node.js 的调试方式也有很多,常见的有:
- 打印日志:console.log
- 手动断点:debugger
- 使用繁琐,需手动打点。
- 若忘记删除 debugger,还会引起性能问题
- Chrome DevTools Debug
- VSCode Debug
- ndb
Chrome DevTools Debug
- 启动Nodejs
node --inspect lib/dispatch.js # 开启deubg端口默认是9229端口,当然你可以自定义
可以通过--inspect-brk启动立即断点
- Chrome打开chrome://inspect 点击inspect
![d0969e748a236388f1cbc8d321a3ffd8.png](https://i-blog.csdnimg.cn/blog_migrate/e3f96230d05972e2e03ae06d4a650e6f.png)
- 添加工作目录,定位源代码
![1d3e654a76b9a7823ccaa56256c49495.png](https://i-blog.csdnimg.cn/blog_migrate/d3487b3deb1fa62669f86cf1a2ef0f95.jpeg)
- 调试技巧(在断点上面右键编辑)
- 支持表达式断点
- 支持打印消息
VSCode Debug
- 配置./vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
"args": ["-e","development"],
"program": "${workspaceFolder}/lib/dispatch",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
- 如果是typescript 项目,需要在编译时添加sourceMap选项,这样才能在源代码上面断点调试
- 按下F5启动调试,可直接在源代码上面断点
- 断点调试技巧(在断点上面右键编辑)
- 表达式:a.x===true
- 次数断点: >=2
- 记录消息: 打印消息到控制台
其他方案,例如:使用ndb进行调试
定位问题技巧
- 最小化代码复杂度,不断的测试,找到存在问题的模块
- 断点调试
- 打印日志
- 工具分析定位问题代码
参考文章
- HTTP性能测试
- Nodejs火焰图
- Nodejs调试指南
- 关于火焰图的解读
- 安装perfTool
- Perf工具参数介绍