原文地址: 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 下 看看进程生成情况:
一父四子,试着用 kill 64163杀死一个子进程。可以在console看到,主进程收到了 exit消息,并重新fork一个新的进程:
测试一下负载,用 Chrome 重复访问几次站点,似乎 worker 子进程 一直未变:
用 ab 来模拟一下并发,让它一个核忙不过来:
换 Safari 浏览器访问一下站点,这次换了一个 worker 进程来服务,速度还不慢
更多文章请移步我的blog新地址: http://www.moye.me/