概述
現行的軟件架構主要有兩種:單進程多線程(如: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的線程和進程