代码改变世界

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

2014-09-17 22:09  莫耶  阅读(2675)  评论(0编辑  收藏  举报

原文地址: 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 咯 :) 所以,需求就是用 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 下 看看进程生成情况:

processes of Cluster

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

fork a new process

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

access via chrome

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

simulate concurrency via ab
换 Safari 浏览器访问一下站点,这次换了一个 worker 进程来服务,速度还不慢 :)

access site root via safari

 

 

 

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