压力测试

ab压测

ab压测

QPS、Time per request、Transfer rate都是关注的指标。
服务器性能瓶颈要分析:

  • CPU限制
  • I/O限制,比如吞吐量大致等于网卡带宽,那瓶颈大致就是网络问题
  • 程序

查看计算机性能:linux一些命令

  • top

cpu、内存的分析

  • iostat

I/O设备的分析(比如硬盘之类的)

不过,一般情况下大概率在node的计算性能上,也就是node中有代码性能不好。

分析Node程序的性能问题

参考:

node —prof 工具

node自带工具:node —prof

使用chrome devtool调测node

使用chrome devtools调测node

clinic.js

开源 Node.js 性能分析套件,具体使用方法看这里:

内存管理的注意事项(GC)

关键词:代际假说、新生代、老生代 (这里自己整的挺明白:JS: V8知识

检测内存泄漏

类似上文中,利用chrome dev tools,思路就是内存快照:
启动server进行ab测试。
测试之前照一个快照, 测试之后再照一个快照。理论上没有内存泄漏的情况下,两次快照得到的内存占用大小应该差不多。但是如果发现有较大差别,说明存在内存泄漏!
举个例子:
伪造一个内存泄漏:

  1. const leak = []
  2. app.use(
  3. mount('/', async (ctx) => {
  4. ctx.body = html
  5. leak.push(html)
  6. })
  7. );

对比前后快照,可以看到,压测前后内存差了很多:
image.png
可以看到:

  • 启动ab测试前,快照内存623MB
  • 结束ab测试之后,快照内存1728MB

典型的内存侧漏了~

dev tool 可以选择「对比」能很清晰的看到,下图所示,分配了多少内存,释放了多少内存:
image.png
上图显示,分配了1159 169….释放了468 856….
image.png
点击一个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的代码:

  1. // child_process
  2. const cp = require('child_process')
  3. const process = cp.fork(_dirname + 'child.js')

父子进程通信的话,都用process.on() \ process.send() 接受和发送消息。
父进程开启子进程后,子进程的出现其实就是利用了多核能力,子进程被开启后默认不销毁。

此外,node也支持子线程(v10 + 支持 ),叫 Worker Threads

  1. const worker = require('worker_threads')

cluster模块以及性能优化

cluster是node的一个内置模块,自动创建子进程,同时,程序员不需要理会master和child的通信过程,这些都是封装的。(参考以前总结的文章:Node cluster
cluster基本使用的方法:
需要isMaster检查当前进程在子进程还是主进程,因为文件是脚本,子进程也会执行。如果是主进程,那么就fork出子进程,下面fork了三个子进程
如果是子进程,就执行./app中的工作:

  1. const cluster = require('cluster')
  2. if (cluster.isMaster) {
  3. cluster.fork()
  4. cluster.fork()
  5. cluster.fork()
  6. } else {
  7. require('./app')
  8. }

开启多进程后,理论性能要比单进程强。
经过ab测试,单进程的qps是290,fork了三个子进程,qps是770,不到三倍,但是也远远优化了。
那么到底要启动多少个子进程?(最多可以fork和cpu数量相等的子进程)

  1. for (let i = 0, n = os.cpus().length; i < n; i += 1){
  2. cluster.fork();
  3. }

不过一般情况下不会fork那么多,因为时间循环等本身要占用一些资源

存在一个疑问,这么多子进程是怎么监听相同的端口的?
大致原理:
真正的监听端口是在主进程监听的,存在一个子进程空闲池,主进程从池中拉一个子进程,然后把任务交给子进程叫它执行,执行完毕后,子进程通知主进程,自身回到空闲池中。

cluster的进程守护问题

当程序出现全局错误(未被catch到),这时候没有其他机制的情况下,node server将因为异常而终止。
这时候可能需要再把这个服务拉起来,这个操作就是进程守护。
在处理异常时候,相关的策略有:

  • 子进程捕获uncaughtException异常,并主动退出;
  • 父进程监听子进程退出事件,并延迟创建子进程;
  • 子进程监控内存占用过大,主动退出;
  • 防止僵尸进程:父进程监听子进程心跳,3次没心跳,杀掉子进程;

详细的策略已总结在这里:cluster进程守护问题