[Node.js] Cluster,把多核用起来

时间:2023-03-09 17:32:35
[Node.js] Cluster,把多核用起来

原文地址: http://www.moye.me/?p=496

引子

众所周知,虽然Node的底层有一个IO线程池,但其应用层默认是单线程运行的,对于多核CPU环境来说,是一种资源的浪费。

所幸Node提供了child_process 模块,让开发者得以开多个进程,实现每个进程各自利用一个CPU,以实现多核的利用。

child_process 模块给予Node 可以随意创建子进程的能力。因为 child_process 类本身是一个 EventEmitter,所以进程间通信很容易;且父子进程间通信并不通过网络层,而是在内核中完成,高效。

但 child_process 对于开发者来说,编程模型还是过于复杂,需要操心的细节过多,比如:父进程崩溃了,子进程回收是需要开发者提供代码来处理的——如果开发者只是想单纯利用多核模型,对具体工作进程的控制粒度并没有太多设想,那这种开发模型无疑是令人头疼的。

针对这个问题,Node 提供了 Cluster 模块。Cluster 简化了父子模型编程模型,只区分:当前进程是不是 Master,是 Master 就可以fork子进程,不是那就请行使Worker 职责。至于什么资源的回收,负载的调配,uncaughtException的处理,它自有安排。

本质上, Cluster 是 child_process 和 net 模块的组合应用。它不仅简化了编程模型,还使得共享监听同一端口成为可能。

更多关于Cluster的原理这里不表,感兴趣可以移步 → 解读Nodejs多核处理模块cluster

场景

用Node快速搭站呢,当然就用 express 咯 [Node.js] Cluster,把多核用起来 所以,需求就是用 Cluster 做个单机集群,多进程跑 express,提升站点的吞吐量。

实验

先用 express-generator 搭个框子,模板引擎还是使 ejs 吧:

express -e myapp2

express-generator 默认会把 app.js 生成好,一行代码不写,一个完备的 http server 是已经实现的了。So,我们可以实现一个 Cluster 单机集群,clusterMaster.js:

//CPU几核?
var cpus = require('os').cpus().length; //子进程监听消息处理函数
var workerListener = function (msg) {
if (msg.access)
console.log('user access %s, worker [%d]',
msg.access, msg.workerid);
};
//fork新的子进程函数
var forkWorker = function(listener){
var worker = cluster.fork();
console.log('worker [%d] has been created',
worker.process.pid);
worker.on('message', listener);
return worker;
}; //Cluster处理
var cluster = require('cluster');
if (cluster.isMaster) {
for (var i = 0; i < cpus; i++) {
forkWorker(workerListener);
}
} else {
var app = require('./app');
return app.listen(3000);
} //Cluster收到子进程退出消息
cluster.on('exit', function (worker, code, signal) {
console.log('worker [%d] died %s, fork a new one.',
worker.process.pid, code || signal);
forkWorker(workerListener);
});
//Cluster收到子进程运行消息
cluster.on('online', function(worker){
console.log('worker [%d] is running.', worker.process.pid);
});

可以看到:

  • Cluster 是通过 isMaster 来区分父子进程的。父进程中处理创建逻辑:根据 CPU的核数,创建相应数量的子进程;子进程中运行具体的 Server 创建代码。够简单的模型~
  • 在 Cluster 父进程端,是始终能获知 当前worker 的。而当前 worker 的 pid 呢,在有worker 句柄情况下,对应的是 woker.process.pid;在子进程运行的上下文中,就是 process.pid。这不难理解~
  • 我们为新fork出来的子进程添加了消息处理函数,使得任何子进程运行代码,都可以随时利用消息触发它:用途挺广泛的,稍后会演示一个例子
  • 在子进程退出时,Cluster 父进程会收到 exit 消息,这时会重fork一个新子进程来补缺

假设用户访问这个站点: localhost:3000/,我希望告诉他,是哪个子进程在为他渲染页面。那么,在 /routes/ 添加一个express 路由 index.js:

var express = require('express');
var router = express.Router(); /* GET home page. */
router.get('/', function (req, res) {
res.render('index', { title: 'Express',
workerid: process.pid });
process.send({access: '/', workerid: process.pid});
}); module.exports = router;

可以看到,除了路由,还使用 process.send 发送了消息,它实际上会触发之前代码中子进程注册的 workerListener,向它汇报路径和进程号;由于使用了 render,那自然是少不了 ejs 模板, /views/index.js:

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
<p>[<%= workerid %>] worker is serving for you.</p>
</body>
</html>

模板会将 render 携带的子进程 pid 渲染到页面上; 当然少不了在 app.js 中添加路由映射:

var routes = require('./routes/index');

app.use('/', routes);

运行程序,在 shell 下 看看进程生成情况:

[Node.js] Cluster,把多核用起来

一父四子,试着用 kill 64163杀死一个子进程。可以在console看到,主进程收到了 exit消息,并重新fork一个新的进程:

[Node.js] Cluster,把多核用起来

测试一下负载,用 Chrome 重复访问几次站点,似乎 worker 子进程 一直未变:

[Node.js] Cluster,把多核用起来

用 ab 来模拟一下并发,让它一个核忙不过来:

[Node.js] Cluster,把多核用起来
换 Safari 浏览器访问一下站点,这次换了一个 worker 进程来服务,速度还不慢 [Node.js] Cluster,把多核用起来

[Node.js] Cluster,把多核用起来

更多文章请移步我的blog新地址: http://www.moye.me/