nodejs性能优化——多进程
1.引言
现在在准备毕设,打算还是使用nodejs作为后端,遇到了一些知识上的瓶颈,主要是想要写出一个高性能点的爬虫,由于每次请求的http数量上万,经常挂了,要么是他人的服务器终止了连接,要么是node经不起密集CPU,毕竟请求完成之后还需要对数据进行处理,特别是我毕设里面需要的数据涉及到cheerio依赖对于页面dom的操作,因此更加怀疑的是nodejs的算力不够导致的。因此,想要看看是否可以通过多线程和多进程来解决一下,恰好之前有一些知识落下了,刚好补一补。
2.ab测试
提及这个主要是为了验证node多线程和多进程到底是做什么的,能够提供那些优化参考。首先借鉴一写文章说一下概念,ab是apache bench命令的缩写,ab命令会创建多个并发访问线程,模拟多个访问者同时对某一URL地址进行访问。它既可以用来测试apache的负载压力,也可以测试nginx、tomcat等其它Web服务器的压力。ab命令对发出负载的计算机要求很低,它既不会占用很高CPU,也不会占用很多内存。但却会给目标服务器造成巨大的负载,其原理类似CC攻击。
2.1环境配置
mac内置Apache,但是windows是没有的,所以需要自己下一个Apache。刚好自己经常用小皮面板
来下载数据库或者Apache或者是Nginx来配置本地开发环境,因此直接配个环境变量就行,比如我电脑里面的路径:
D:\phpstudy_pro\Extensions\Apache2.4.39\bin
如果没有下载过小皮面板,推荐安装一个,毕竟用这个能够快速切换数据库版本,快速搭建本地环境,配一个Navicat能够轻松搞一个系统出来,比直接单独安装Apache和MySQL好的多。
3.1ab测试演示
我们先主要说一下ab测试的流程,翻译一些常用参数,以及指示性能的参数:
//多进程测试
let cluster = require("cluster");
let http = require("http");
let CPUNum = require("os").cpus().length;//获取CPU的核数
if (cluster.isMaster) {
for (let i = 0; i < CPUNum; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log("进程消亡:", worker, code, signal)
})
} else {
http.createServer((req, res) => {
res.end('hello world\n');
}).listen(8000, () => {
console.log("localhost:8000");
});
}
对于电脑的CPU核数也可以利用任务管理器查看:
好直接测试,cmd中输入(如果提示ab不是内部指令那就是Apache的环境变量没有配置好):
ab -c 20 -n 100 http://127.0.0.1:8000/
参数说明:由于ab测试需要拿到一个平均值,因此,会进行多次测试取平均结果:
(1)-c
表示的是并发请求;
(2)-n
表示的是ab测试的次数;
(3)后面的就是测试的站点的接口了,注意最后的/
不能少;
(4)还有一些常用参数,-p
post请求提交的文件数据,-w
以html表格的形式输出结果(估计测试岗狂喜),-t
测试的总时间,默认是50000s,其他的可以看看英文提示。
结果的参数说明:
验证cluster的多进程优化效果,直接使用10000并发测试:
3.node的多进程
其实ab测试只是引子,主要是为了看看cluster配置多进程的有点,但是这个显然不是我们想要的,在爬虫上,我们是请求者,不是服务提供者。因此还需要进一步探究子进程相关内容。
nodejs实现多进程有如下几种常用的方式:
(1)spawn: 启动一个子进程来执行命令;
(2)exec: 启动一个子进程来执行命令,与 spawn 不同的是,它有一个回调函数获知子进程的状况;
(3)execFile: 启动一个子进程来执行可执行文件;
(4)fork: 与 spawn 类似,不同点在于它创建 Node 的子进程只需指定要执行的 JavaScript 文件模块即可;
基本使用如下:
四种构建子进程方法的区别如下:
综合考虑下,execFile
算是比较良好的,nodejs里面的异常处理相当重要。既然是父子进程,少不了探究它们之间的通信原理,其实通过查阅源码可以发现子进程继承了EventEmitter
类:
借助于EventEmitter
对象,常用的方法如下:
(1)close 事件:子进程的 stdio 流关闭时触发;
(2)disconnect 事件:事件在父进程手动调用 child.disconnect 函数时触发;
(3)error 事件:产生错误时会触发;
(4)exit 事件:子进程自行退出时触发;
(5)message 事件:它在子进程使用 process.send() 函数来传递消息时触发;
需要进一步说明的是这里的send方法并不是基于eventEmitter
,而是基于管道通信实现的,具体实现细节由 libuv
实现,相信背过考研IO发展流程的友友们应该对管道通信不陌生。
// 父进程
const child = fork('.test.js');
child.on('message', (m)=>{
console.log('message from child: ' + JSON.stringify(m));
});
child.send({data: '父进程发送的消息'});
// 子进程
process.on('message', function(m){
console.log('来自父进程的消息: ' + JSON.stringify(m));
});
process.send({data: "来自子进程的消息"});
4.node多进程的应用思路
怎么在实际中使用呢?首先基于上面父子进程的通信,我们可以让每一个子进程执行完毕之后,向父进程发送消息提示父进程根据当前任务的分派情况分发最新的任务,这样子进程就能够一直处于工作状态了。比如首先通过一些接口获取有效的文章的id信息,存储到数据库(没错只需要id,别的信息暂时不需要管,不然会影响处理速度甚至会使程序崩溃),接下来从数据库中读取id信息,然后交给子进程处理,子进程处理完了再分配新的任务,一般子进程的数量设置为CPU的核数。
守护进程,虽然理论上上面的处理方法挺好,但是子进程难免会由于网络或者第三方拦截等原因崩溃。我们可以利用回调函数或者父子进程之间的监听机制,如果子进程崩溃了,那么再新建一个子进程,保证内部总有固定数量的进程处于运行中。