压力测试
ab压测
QPS、Time per request、Transfer rate都是关注的指标。
服务器性能瓶颈要分析:
- CPU限制
- I/O限制,比如吞吐量大致等于网卡带宽,那瓶颈大致就是网络问题
- 程序
查看计算机性能:linux一些命令
- top
cpu、内存的分析
- iostat
I/O设备的分析(比如硬盘之类的)
不过,一般情况下大概率在node的计算性能上,也就是node中有代码性能不好。
分析Node程序的性能问题
node —prof 工具
使用chrome devtool调测node
clinic.js
内存管理的注意事项(GC)
关键词:代际假说、新生代、老生代 (这里自己整的挺明白:JS: V8知识)
检测内存泄漏
类似上文中,利用chrome dev tools,思路就是内存快照:
启动server进行ab测试。
测试之前照一个快照, 测试之后再照一个快照。理论上没有内存泄漏的情况下,两次快照得到的内存占用大小应该差不多。但是如果发现有较大差别,说明存在内存泄漏!
举个例子:
伪造一个内存泄漏:
const leak = []
app.use(
mount('/', async (ctx) => {
ctx.body = html
leak.push(html)
})
);
对比前后快照,可以看到,压测前后内存差了很多:
可以看到:
- 启动ab测试前,快照内存623MB
- 结束ab测试之后,快照内存1728MB
典型的内存侧漏了~
dev tool 可以选择「对比」能很清晰的看到,下图所示,分配了多少内存,释放了多少内存:
上图显示,分配了1159 169….释放了468 856….
点击一个string可以在下面看到,这个内存的持有者就是leak变量,就是刚才伪造的内存泄漏数组。
减少内存使用
这里主要介绍了“池”的思想:
池的思想就是最大限度的减小内存申请释放的开销,做到尽量复用的手段~
举个例子:
Node Buffer的内存管理策略:Buffer是Node定义的结构,所以和V8没有关系。
Buffer的内存管理就是利用“池”的思想。
Buffer对应C++里面的 char[]。
对于小于8KB的buffer,node认为,每一次都new char[]的分配内存,其开销是很大的
所以,node是怎么做的呢?
- 当node第一次发现程序申请了小于8kb大小的buffer,它就会new 一个8kb大小的空间作为池
- 从这个空间中切出一小块供程序使用
- 下一次再有需要使用的小于8kb的空间,那就继续切,当需有8kb却又不够切的时候,重新再new 一个8kb的空间;
在运行的时候,一旦释放掉其中某一块空间,那么这块空间会在下一次可以被复用的时候被复用;
利用c++优化
因为c++,快!
不过也可能是一把双刃剑:收益:C++是很快,比js快得多
- 成本:C++变量和V8转换
还是要考量收益是不是能抵的过成本,选择合适的场景,没有银弹~
参考:
整体流程大致就是:
- 编写C++脚本
- node-gyp
- binding.gyp
- configure
- build,完成之后生成 .node文件
- 在JS中引用C++插件
利用多进程优化
多进程简介
进程是操作系统挂载运行程序的担忧,拥有独立的资源~线程是运算调度的最小单元,进程内的线程对进程资源是共享的。(node在v10之后支持了多线程)
Node的主线程中运行v8,v8负责解析和执行js。这其中的限制在于:主线程只能利用一个cpu核心的能力。
不过node 支持子进程:可以理解为在一个node中又开了一个node。
举个例子,现在我们有两个js文件,一个是master.js,一个是child.js,期望在master中开启子进程去执行child.js的代码:
// child_process
const cp = require('child_process')
const process = cp.fork(_dirname + 'child.js')
父子进程通信的话,都用process.on() \ process.send() 接受和发送消息。
父进程开启子进程后,子进程的出现其实就是利用了多核能力,子进程被开启后默认不销毁。
此外,node也支持子线程(v10 + 支持 ),叫 Worker Threads
const worker = require('worker_threads')
cluster模块以及性能优化
cluster是node的一个内置模块,自动创建子进程,同时,程序员不需要理会master和child的通信过程,这些都是封装的。(参考以前总结的文章:Node cluster)
cluster基本使用的方法:
需要isMaster检查当前进程在子进程还是主进程,因为文件是脚本,子进程也会执行。如果是主进程,那么就fork出子进程,下面fork了三个子进程
如果是子进程,就执行./app中的工作:
const cluster = require('cluster')
if (cluster.isMaster) {
cluster.fork()
cluster.fork()
cluster.fork()
} else {
require('./app')
}
开启多进程后,理论性能要比单进程强。
经过ab测试,单进程的qps是290,fork了三个子进程,qps是770,不到三倍,但是也远远优化了。
那么到底要启动多少个子进程?(最多可以fork和cpu数量相等的子进程)
for (let i = 0, n = os.cpus().length; i < n; i += 1){
cluster.fork();
}
不过一般情况下不会fork那么多,因为时间循环等本身要占用一些资源
存在一个疑问,这么多子进程是怎么监听相同的端口的?
大致原理:
真正的监听端口是在主进程监听的,存在一个子进程空闲池,主进程从池中拉一个子进程,然后把任务交给子进程叫它执行,执行完毕后,子进程通知主进程,自身回到空闲池中。
cluster的进程守护问题
当程序出现全局错误(未被catch到),这时候没有其他机制的情况下,node server将因为异常而终止。
这时候可能需要再把这个服务拉起来,这个操作就是进程守护。
在处理异常时候,相关的策略有:
- 子进程捕获uncaughtException异常,并主动退出;
- 父进程监听子进程退出事件,并延迟创建子进程;
- 子进程监控内存占用过大,主动退出;
- 防止僵尸进程:父进程监听子进程心跳,3次没心跳,杀掉子进程;
详细的策略已总结在这里:cluster进程守护问题