Node.js中的高并发方案
Intel老大哥已经挤牙膏挤了10多年了,摩尔定律即将走到尽头,自从苹果公司发布了M1芯片后,Intel似乎从中找到了些许灵感,从12代CPU开始,Intel的部分CPU也开始使用大小核
的架构了,这样,一台普通的家用电脑也可以拥有接近20个CPU核心。因此,如何充分利用这些核心已经成为编程语言需要处理的基本问题。本篇聊聊Node.js中的并发方案。
Node.js是单进程的
Node.js之父Ryan Dahl在设计之初,借鉴了Nginx的事件 + 异步IO
的模型来达到高并发的目的,这是因为JavaScript运行在单进程
中(只有一个线程
),不像Java,Node无法使用多线程,为了实现高并发,就只能采用非阻塞IO
,这就是为什么Node的库中提供了那么多异步API,如果在Node(比如WEB服务器)中使用同步API,那么它的性能将惨不忍睹。这种架构有它的优势,一个Node应用(单进程单线程)可以充分利用单核CPU,不用像多线程编程(比如Java)那样需要处处在意线程间同步的问题,也没有线程上下文交换时带来的额外性能开销。当然,这种模式的缺点也很明显:
- 无法利用多核CPU
- 出错会导致整个应用退出
- 如果涉及大量计算会导致CPU无法继续调用异步IO
也就是说,Node使用单进程单线程就实现了传统意义上的高并发,但是这种模式有如上几个缺点,为了解决这个问题,Node在0.8版本时引入了Cluster模块。
多进程之路:Cluster
Cluster模块提供了一种运行多个
Node.js 实例的方案,为了说明问题,我们来构建一个小小的HTTP服务器,通过对比使用和不使用Cluster模块的差异,来发现其中的秘密。
新增一个worker.js文件,内容如下:
const http = require('http')
const pid = process.pid
const server = http.createServer((req, res) => {
for (let i = 1e7; i > 0; i--) {} // empty loop
console.log(`handing request from ${pid}`)
res.end(`hello from ${pid}\n`)
})
server.listen(8000, () => console.log(`started ${pid}`))
为了模拟一些实际的CPU工作,我们执行了1000万次空循环,运行该应用,并通过ab
来测试:
ab -c200 -t20 http://192.168.5.9:8000/
上面的命令将用200个并发来测试该http服务器(我这里将该脚本放在了另一台Linux服务器上,IP为192.168.5.9),一共测试20s,得到如下数据:
可以看到,在20s内一共完成了1297次请求,QPS只有64.1,50%的请求都需要2.8s的响应时间,这是运行在单核CPU的情况下。
接下来,我们使用Cluster模块,来让该HTTP服务器充分利用电脑上的多核CPU,先创建一个clustered_app.js,内容如下:
const cluster = require('cluster')
const os = require('os')
if (cluster.isMaster) {
const cpus = os.cpus().length
console.log(`clustering to ${cpus} CPUs`)
for (let i = 0; i < cpus; i++) {
cluster.fork()
}
} else {
require('./worker.js')
}
运行clustered_app.js:
node clustered_app.js
如果有多个处理器,比如,在我的测试机器上,是四核的CPU,在shell命令行中会出现如下的提示:
我们再次使用ab来进行测试,同样的条件下,得到如下结果:
可以发现,当通过Cluster开启了4个进程后,QPS从64
提升到了173
,提升了差不多2.7倍
左右,同时,通过top
命令可以看到四个CPU的负载都达到90%左右,而之前的单核模式,只有其中一个CPU有90%的负载,其他三个CPU核心基本在摸鱼的状态。
部署利器:PM2
上面的例子说明了Cluster模块能够利用多核CPU带来性能上的提升,但在生产环境上使用时,你还需要考虑更多的情况,比如,自动重启,监控,日志等等,所以,生产环境常常使用PM2
这个工具,它可以让你在不修改代码
的情况下实现应用的扩展,拿上面的例子来说,直接使用-i参数启动:
pm2 start worker.js -i max
-i 或 instances 选项可以是:
0/max: to spread the app across all CPUs
-1: to spread the app across all CPUs - 1
number: to spread the app across number CPUs
使用PM2不仅仅是扩展应用比较方便,它还提供了监控,日志等等诸多功能。在实际的部署中,一般会使用一个配置文件来启动,比如,新增一个app.config.js
,内容大致如下:
module.exports = {
apps: [
{
name: 'myapp',
script: '/srv/worker.js',
instances: 0,
autorestart: true,
restart_delay: 2000,
max_restarts: 5,
watch: false,
env: {
NODE_ENV: 'production',
PORT: 3000
},
out_file: '/srv/pm2/myapp/out.log',
error_file: '/srv/pm2/myapp/error.log',
pid_file: '/srv/pm2/myapp/app.pid'
}
]
}
然后,通过 pm2 start ./app.config.js
启动即可。
参考
- Episode 8: Interview with Ryan Dahl, Creator of Node.js
- Introduction to Node.js
- 朴灵,深入浅出Node.js
- Mario Casciaro, Luciano Mammino, Node.js 设计模式(第2版)
- PM2-Cluster Mode