概述
现行的软件架构主要有两种:单进程多线程(如:memcached、redis、mongodb等)和多进程单线程(nginx、node)。
单进程多线程的主要特点:
- 快:线程比进程轻量,它的切换开销要少很多。进程相当于函数间切换,每个函数拥有自己的变量;线程相当于一个函数内的子函数切换,它们拥有相同的全局变量。
- 灵活: 程序逻辑和控制方式简单,但是锁和全局变量同步比较麻烦。
- 稳定性不高: 由于只有一个进程,其内部任何线程出现问题都有可能造成进程挂掉,造成不可用。
- 性能天花板:线程和主程序受限2G地址空间;当线程到一定数量后,即使增加cpu也不能提升性能。
多进程单线程的主要特点:
- 高性能:没有频繁创建和切换线程的开销,可以在高并发的情况下保持低内存占用;可以根据CPU的数量增加进程数。
- 线程安全:没有必要对变量进行加锁解锁的操作
- 异步非阻塞:通过异步I/O可以让cpu在I/O等待的时间内去执行其他操作,实现程序运行的非阻塞
- 性能天花板:进程间的调度开销大、控制复杂;如果需要跨进程通信,传输数据不能太大。
事实上异步通过信号量、消息等方式早就存在操作系统底层,但是一直没有能在高级语言中推广使用。
Linux Unix提供了epoll方便了高级语言的异步设计。epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll只会把哪个流发生了怎样的I/O事件通知我们;libevent和libev都是对epoll的封装,nginx自己实现了对epoll的封装。
浏览器
在支持html5的浏览器里,可以使用webworker来将一些耗时的计算丢入worker进程中执行,这样主进程就不会阻塞,用户也就不会有卡顿的感觉了。
<!DOCTYPE html>
<head>
<title>worker</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script>
function init(){
//创建一个Worker对象,并向它传递将在新进程中执行的脚本url
var worker = new Worker('./webworker.js');
//接收worker传递过来的数据
worker.onmessage = function(event){
document.getElementById('result').innerHTML+=event.data+"<br/>" ;
};
};
</script>
</head>
<body onload = "init()">
<div id="result"></div>
</body>
</html>
// webworker.js
var i = 0;
function timedCount(){
for(var j = 0, sum = 0; j < 100; j++){
for(var i = 0; i < 100000000; i++){
sum+=i;
};
};
//将得到的sum发送回主进程
postMessage(sum);
};
//将执行timedCount前的时间,通过postMessage发送回主进程
postMessage('Before computing, '+new Date());
timedCount();
//结束timedCount后,将结束时间发送回主进程
postMessage('After computing, ' +new Date());
Node
Nodejs通过其内置的cluster
模块实现多进程。cluster是对child_process进行了封装,目的是发挥多核服务器的性能;pm2 是当下最热门的带有负载均衡功能的 Node.js 应用进程管理器。实际开发时,我们不需要关注多进程环境。
进程模型
Node的多进程模型是一个主master多个从worker模式,master的职责如下:
- 接收外界信号并向各worker进程发送信号
- 监控woker进程的运行状态,当woker进程退出后(异常情况下),会自动重新启动新的woker进程(进程守护)。
如果只是简单的fork几个进程,多个进程之间会竞争 accpet 一个连接,产生惊群现象,效率比较低。同时由于无法控制一个新的连接由哪个进程来处理,必然导致各 worker 进程之间的负载非常不均衡。
IPC
注: 这部分是从当我们谈论 cluster 时我们在谈论什么(下)copy而来
Node.js 中父进程调用 fork 产生子进程时,会事先构造一个 pipe 用于进程通信。
new process.binding('pipe_wrap').Pipe(true);
构造出的 pipe 最初还是关闭的状态,或者说底层还并没有创建一个真实的 pipe,直至调用到 libuv 底层的uv_spawn
, 利用 socketpair 创建的全双工通信管道绑定到最初 Node.js 层创建的 pipe 上。
管道此时已经真实的存在了,父进程保留对一端的操作,通过环境变量将管道的另一端文件描述符 fd 传递到子进程。
options.envPairs.push('NODE_CHANNEL_FD=' + ipcFd);
子进程启动后通过环境变量拿到 fd
var fd = parseInt(process.env.NODE_CHANNEL_FD, 10);
并将 fd 绑定到一个新构造的 pipe 上
var p = new Pipe(true);
p.open(fd);
于是父子进程间用于双向通信的所有基础设施都已经准备好了。
总结下,Nodejs通过pipe实现IPC,主要包括以下几个步骤:
- 主进程在fork产生子进程前生成一个pipe占位符,提示后续会有pipe创建。
- 通过系统的socketpair把双工通道绑定到此pipe占位符上。
- 通过环境变量把文件描述符fd传给子进程。
- 子进程通过fd创建pipe,此pipe替代占位符进行通信。
例子:
// master
const WriteWrap = process.binding('stream_wrap').WriteWrap;
var cp = require('child_process');
var worker = cp.fork(__dirname + '/ipc_worker.js');
var channel = worker._channel;
channel.onread = function (len, buf, handle) {
if (buf) {
console.log(buf.toString())
channel.close()
} else {
channel.close()
console.log('channel closed');
}
}
var message = { hello: 'worker', pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '\n';
channel.writeUtf8String(req, string, null);
// worker
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;
channel.ref();
channel.onread = function (len, buf, handle) {
if (buf) {
console.log(buf.toString())
}else{
process._channel.close()
console.log('channel closed');
}
}
var message = { hello: 'master', pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '\n';
channel.writeUtf8String(req, string, null);
进程失联
进程失联是在子进程退出前通知主进程,主进程fork一个新的子进程,然后原来的子进程退出;主进程通过是子进程的disconnect事件监听其状态。
例子:
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const net = require('net');
const fork = require('child_process').fork;
var workers = [];
for (var i = 0; i < 4; i++) {
var worker = fork(__dirname + '/multi_worker.js');
worker.on('disconnect', function () {
console.log('[%s] worker %s is disconnected', process.pid, worker.pid);
});
workers.push(worker);
}
var handle = net._createServerHandle('0.0.0.0', 3000);
handle.listen();
handle.onconnection = function (err,handle) {
var worker = workers.pop();
var channel = worker._channel;
var req = new WriteWrap();
channel.writeUtf8String(req, 'dispatch handle', handle);
workers.unshift(worker);
}
const net = require('net');
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;
var buf = 'hello Node.js';
var res = ['HTTP/1.1 200 OK','content-length:' + buf.length].join('\r\n') + '\r\n\r\n' + buf;
channel.ref(); //防止进程退出
channel.onread = function (len, buf, handle) {
console.log('[%s] worker %s got a connection', process.pid, process.pid);
var socket = new net.Socket({
handle: handle
});
socket.readable = socket.writable = true;
socket.end(res);
console.log('[%s] worker %s is going to disconnect', process.pid, process.pid);
channel.close();
}
参考文章
当我们谈论 cluster 时我们在谈论什么(上)
当我们谈论 cluster 时我们在谈论什么(下)
多进程单线程模型与单进程多线程模型之争
多进程和多线程的优缺点
Node.js的线程和进程