node.js使用cluster實現多進程,node.jscluster
首先鄭重聲明:
nodeJS 是一門單線程!非同步!非阻塞語言!
nodeJS 是一門單線程!非同步!非阻塞語言!
nodeJS 是一門單線程!非同步!非阻塞語言!
重要的事情說3遍。 因為nodeJS天生內建buff, 所以從一出生就受到 萬千 粉絲的追捧(俺,也是它的死忠). 但是,傻逼php 竟然嘲笑 我大NodeJS 的效能。 說不穩定,不可靠,只能利用單核CPU。 辣雞 nodeJS.
艸!艸!艸!
搞mo shi~
但,大哥就是大哥,nodeJS在v0.8 的時候就已經加入了cluster的模組。 完全打臉php. 雖然,現在php 也開始抄襲nodeJS, 退出php7, 但是,渣渣,你就只會抄...
233333
對不起啊,上面是我自已意淫的一段~ 以上內容,純屬調侃,如果雷同,純屬巧合。
Ok~ 我們來正式介紹一下nodeJS的多進程吧~
cluster的前世今生
以前,由於cluster 本身的不完善,可能由於多方面原因吧,實現效能不好。 結果是,pm2 包的 崛起。 便於使用一個pm2 就可以開啟多進程,實現負載平衡的效果。
pm2 start app.js
pm2的內部和cluster內部實現其實是一個道理,都是封裝了一層child_process--fork. 而child_process--fork 則是封裝了unix 系統的fork 方法。 既然,都到這了,我們來看看官方給出的解釋吧。
fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect the other.
俺來翻譯一下,fork其實就是建立子進程的方法,新建立的進程被認為是子進程,而調用fork的進程則是父進程。 子進程和父進程本來是在獨立的記憶體空間中的。但當你使用了fork之後,兩者就處在同一個範圍內了。 但是,記憶體的讀寫,檔案的map,都不會影響對方。
上面那段的意思就是,你建立的進程其實可以相互連信,並且被master進程 管理。
看圖~~~
其實就是這個意思。
Ok~ 這隻是系統建立子進程的模型。那麼在NodeJs中是怎樣實現進程之間的互動的呢?
很簡單監聽連接埠唄。。。
但是,實現通訊不是很難,關鍵在於如果分配請求,這一點nodeJS 踩的坑確實很大。
nodeJS 實現進程分配的黑曆史
long time ago
nodeJS的master 開始並不是上帝, 他只是一個小小的太監,每次請求(妃子)來的時候,他只會默默的看著幾個worker小皇帝相互爭奪,如果某個worker勝出,則其他的worker也就草草了事,等下一個請求過來。所以說,每來一次請求,都會引起一場腥風血雨。而,我們體會最深的就是驚群現象,即,CPU爆表.
借用TJ大神的一幅圖,說明一下。
這裡,master只是綁定連接埠,而不會對來的請求做任何處理。 通過將socket的fd給fork出來的進程。造成的結果就是4個人男人(worker)搶一個妃子(request). 那場面別提有多血腥了。
前面說過,cluster其實就是對child_process的一層封裝,那我們繼續往底層走一點。實現cluster多進程。 首先,我們需要瞭解,這幾個模組的基本用法。net,child_process.
child_process
這個應該是nodeJS 進程最核心的模組。 基本的方法,有幾個,不過我這裡,只介紹比較核心的:spawn ,fork ,exec。如果大家有興趣,可以去child_process參考.
child_process.spawn(command, args)
該方法用來運行指定的程式。比如: node app.js.他是非同步命令,但不支援callback, 不過我們可以使用process.on來監聽結果。 他內建3個參數.
command: 執行命令
args[Array]: 命令所帶的參數
options[Object]: 環境變數對象
OK~ 我們舉個一個簡單的demo: 試一試運行 touch apawn.js
const spawn = require('child_process').spawn;const touch = spawn('touch',['spawn.js']);touch.stdout.on('data', (data) => { console.log(`stdout: ${data}`);});touch.stderr.on('data', (data) => { console.log(`stderr: ${data}`);});touch.on('close', (code) => { console.log(`child process exited with code $[code]`);});
如果,正確的話,應該會輸
出child process exited with code 0. 然後運行目錄會產生pawn.js檔案。 當然,如果你需要運行多參數的命令的話這就有點蛋疼了。
所以,nodeJS 使用了exec對其進行很好的封裝,而且他支援回呼函數,這比較能夠讓我們理解。
child_process.exec(order,cb(err[,stdout,stderr]));
order: 就是你執行的命令. 比如: rm spawn.js
cb: 就是命令執行成功後的回呼函數。
const childProcess = require('child_process');const ls = childProcess.exec('rm spawn.js', function (error, stdout, stderr) { if (error) { console.log(error.stack); console.log('Error code: '+error.code); } console.log('Child Process STDOUT: '+stdout);});
正常情況下會刪除spawn.js檔案。
上面兩個只是簡單的運行進程的命令。 最後,(Boss總是最後出場的). 我們來瞧瞧fork方法的使用.
fork其實也是用來執行進程,比如,spawn("node",['app.js']),其實和fork('app.js') 是一樣的效果的。但是,fork牛逼的地方在於他在開啟一個子進程時,同時建立了一個資訊通道(雙工的哦). 倆個進程之間使用process.on("message",fn)和process.send(...)進行資訊的交流.
child_process.fork(order) //建立子進程
worker.on('message',cb) //監聽message事件
worker.send(mes) //發送資訊
他和spawn類似都是通過返回的通道進行通訊。舉一個demo, 兩個檔案master.js和worker.js 來看一下.
//master.jsconst childProcess = require('child_process');const worker = childProcess.fork('worker.js');worker.on('message',function(mes){ console.log(`from worder, message: ${mes}`);});worker.send("this is master");//worker.jsprocess.on('message',function(mes){ console.log(`from master, message: ${mes}`);});process.send("this is worker");
運行,node app.js, 會輸出一下結果:
from master, message: this is masterfrom worker, message: this is worker
現在我們已經學會了,如何使用child_process來建立一個基本的進程了。
關於net 這一模組,大家可以參考一下net模組.
ok . 現在我們正式進入,類比nodeJS cluster模組通訊的procedure了。
out of date 的cluster
這裡先介紹一下,曾經的cluster實現的一套機理。同樣,再放一次圖
我們使用net和child_process來模仿一下。
//master.jsconst net = require('net');const fork = require('child_process').fork;var handle = net._createServerHandle('0.0.0.0', 3000);for(var i=0;i<4;i++) { fork('./worker').send({}, handle);}//worker.jsconst net = require('net');//監聽master發送過來的資訊process.on('message', function(m, handle) { start(handle);});var buf = 'hello nodejs'; ///返回資訊var res = ['HTTP/1.1 200 OK','content-length:'+buf.length].join('\r\n')+'\r\n\r\n'+buf; //嵌套字function start(server) { server.listen(); var num=0; //監聽connection函數 server.onconnection = function(err,handle) { num++; console.log(`worker[${process.pid}]:${num}`); var socket = new net.Socket({ handle: handle }); socket.readable = socket.writable = true; socket.end(res); }}
ok~ 我們運行一下程式, 首先運行node master.js.
然後使用測試載入器,siege.
siege -c 100 -r 2 http://localhost:3000
OK,我們看一下,到底此時的負載是否均衡。
worker[1182]:52worker[1183]:42worker[1184]:90worker[1181]:16
發現,這樣任由worker去爭奪請求,效率真的很低呀。每一次,觸發請求,都有可能導致驚群事件的發生啊喂。所以,後來cluster改變了一種模式,使用master來控制請求的分配,官方給出的演算法其實就是round-robin 輪轉方法。
高富帥版cluster
現在具體的實現模型就變成這個.
由master來控制請求的給予。通過監聽連接埠,建立一個socket,將獲得的請求傳遞給子進程。
從tj大神那裡借鑒的代碼demo:
//masterconst net = require('net');const fork = require('child_process').fork;var workers = [];for (var i = 0; i < 4; i++) { workers.push(fork('./worker'));}var handle = net._createServerHandle('0.0.0.0', 3000);handle.listen();//將監聽事件移到master中handle.onconnection = function (err,handle) { var worker = workers.pop(); //取出一個pop worker.send({},handle); workers.unshift(worker); //再放回取出的pop}//worker.jsconst net = require('net');process.on('message', function (m, handle) { start(handle);});var buf = 'hello Node.js';var res = ['HTTP/1.1 200 OK','content-length:'+buf.length].join('\r\n')+'\r\n\r\n'+buf;function start(handle) { console.log('got a connection on worker, pid = %d', process.pid); var socket = new net.Socket({ handle: handle }); socket.readable = socket.writable = true; socket.end(res);}
這裡就經由master來掌控全域了. 當一個皇帝(worker)正在寵幸妃子的時候,master就會安排剩下的幾個皇帝排隊一個幾個的來。 其實中間的handle就會我們具體的商務邏輯. 如同:app.js.
ok~ 我們再來看一下cluster模組實現多進程的具體寫法.
cluster模組實現多進程
現在的cluster已經可以說完全做到的負載平衡。在cluster說明我已經做了闡述了。我們來看一下具體的實現吧
var cluster = require('cluster');var http = require('http');var numCPUs = require('os').cpus().length;if (cluster.isMaster) { console.log('[master] ' + "start master..."); for (var i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('listening', function (worker, address) { console.log('[master] ' + 'listening: worker' + worker.id + ',pid:' + worker.process.pid + ', Address:' + address.address + ":" + address.port); });} else if (cluster.isWorker) { console.log('[worker] ' + "start worker ..." + cluster.worker.id); var num = 0; http.createServer(function (req, res) { num++; console.log('worker'+cluster.worker.id+":"+num); res.end('worker'+cluster.worker.id+',PID:'+process.pid); }).listen(3000);}
這裡使用的是HTTP模組,當然,完全也可以替換為socket模組. 不過由於這樣書寫,將叢集和單邊給混淆了。 所以,推薦寫法是將具體商務邏輯獨立出來.
var cluster = require('cluster');var numCPUs = require('os').cpus().length;if (cluster.isMaster) { console.log('[master] ' + "start master..."); for (var i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('listening', function (worker, address) { console.log('[master] ' + 'listening: worker' + worker.id + ',pid:' + worker.process.pid + ', Address:' + address.address + ":" + address.port); });} else if (cluster.isWorker) { require('app.js');}//app.js就是開啟具體的商務邏輯了//app.js具體內容const net = require('net');//自動建立socketconst server = net.createServer(function(socket) { //'connection' listener socket.on('end', function() { console.log('server disconnected'); }); socket.on('data', function() { socket.end('hello\r\n'); });});//開啟連接埠的監聽server.listen(8124, function() { //'listening' listener console.log('working')});
接著我們開啟服務,node master.js
然後進行測試
siege -c 100 -r 2 http://localhost:8124
我這裡開啟的是長串連. 每個worker處理的長串連數是有限的。所以,當有額外的串連到來時,worker會斷開當前沒有響應的串連,去處理新的串連。
不過,平常我們都是使用HTTP開啟 短串連,快速處理大並發的請求。
這是我改成HTTP短串連之後的結果
Transactions: 200 hitsAvailability: 100.00 %Elapsed time: 2.09 secsData transferred: 0.00 MBResponse time: 0.02 secsTransaction rate: 95.69 trans/secThroughput: 0.00 MB/secConcurrency: 1.74Successful transactions: 200Failed transactions: 0Longest transaction: 0.05Shortest transaction: 0.02
那,怎麼類比大並發嘞?
e e e e e e e e e ...
自己解決啊~
開玩笑的啦~ 不然我寫blog是為了什麼呢? 就是為了傳播知識.
在介紹工具之前,我想先說幾個關於效能的基本概念
QPS(TPS),並發數,回應時間,輸送量,吞吐率
你母雞的效能測試theories
自從我們和伺服器扯上關係後,我們前端的效能測試真的很多。但這也是我們必須掌握的tip. 本來前端寶寶只需要看看控制台,瞭解一下網頁運行是否運行順暢, 看看TimeLine,Profile 就可以了。 不過,作為一名有追求,有志於改變世界的童鞋來說。。。
md~ 又要學了...
ok~ 好了,在進入正題之前,我再放一次 線上的測試結果.
Transactions: 200 hitsAvailability: 100.00 %Elapsed time: 13.46 secsData transferred: 0.15 MBResponse time: 3.64 secsTransaction rate: 14.86 trans/secThroughput: 0.01 MB/secConcurrency: 54.15Successful transactions: 200Failed transactions: 0Longest transaction: 11.27Shortest transaction: 0.01
根據上面的資料,就可以得出,你網頁的大致效能了。
恩~ let's begin
吞吐率
關於吞吐率有多種解讀,一種是:描繪web伺服器單位時間處理請求的能力。根據這個描述,其單位就為: req/sec. 另一種是: 單位時間內網路上傳輸的資料量。 而根據這個描述的話,他的單位就為: MB/sec.
而這個指標就是上面資料中的Throughput. 當然,肯定是越大越好了
輸送量
這個和上面的吞吐率很有點關係的。 輸送量是在沒有時間的限制下,你一次測試的傳輸資料總和。 所以,沒有時間條件的測試,都是耍流氓。
這個對應於上面資料中的Data transferred.
事務 && TPS
熟悉資料庫操作的童鞋,應該知道,在資料庫中常常會提到一個叫做事務的概念。 在資料庫中,一個事務,常常代表著一個具體的處理流程和結果. 比如,我現在想要的資料是 2013-2015年,數學期末考試成績排名. 這個就是一個具體的事務,那麼我們映射到資料庫中就是,取出2013-2015年的排名,然後取平均值,返回最後的排序結果。 可以看出,事務並不單單指單一的操作,他是由一個或一個以上 操作組合而成具有 實際意義的。 那,反映到前端測試,我們應該怎樣去定義呢? 首先,我們需要瞭解,前端的網路交流其實就是 要求-回應模式. 也就是說,每一次請求,我們都可以理解為一次事務(trans).
所以,TPS(transaction per second)就可以理解為1sec內,系統能夠處理的請求數目.他的單位也就是: trans/sec . 你當然也可以理解為seq/sec.
所以說,TPS 應該是衡量一個系統承載力最優的一個標識.
TPS的計算公式很容易的出來就是: Transactions / Elapsed time.
不過, 凡事無絕對。 大家以後遇到測試的時候,應該就會知道的.
並發數
就是伺服器能夠並發處理的串連數,具體我也母雞他的單位是什麼。 官方給出的解釋是:
Concurrency is average number of simultaneous connections, a number which rises as server performance decreases.
這裡我們就理解為,這就是一個衡量系統的承載力的一個標準吧。 當Concurrency 越高,表示 系統承載的越多,但效能也越低。
ok~ 但是我們如何利用這些資料,來確定我們的並發策略呢? e e e e e e e ...
當然, 一兩次測試的結果真的沒有什麼卵用. 所以實際上,我們需要進行多次測試,然後畫圖才行。 當然,一些大公司,早就有一套完整的系統來計算你web伺服器的瓶頸,以及 給出 最優的並發策略.
廢話不多說,我們來看看,如何分析,才能得出 比較好的 並發策略。
探究並發策略
首先,我們這裡的並發需要進行區分. 一個是並發的請求數,一個是並發的使用者數. 這兩個對於伺服器是完全不同的需求。
假如100個使用者同時向伺服器分別進行10次請求,與1個使用者向伺服器連續進行1000次請求。兩個的效果一樣嗎?
一個使用者向伺服器連續進行1000次請求的過程中,任何時刻伺服器的網卡接受緩衝區中只有來自該使用者的1個請求,而100個使用者同時向伺服器分別進行10次請求的過程中,伺服器網卡接收緩衝區中最多有100個等待處理的請求,顯然這時候伺服器的壓力更大。
所以上面所說的 並發使用者數和吞吐率 是完全不一樣的.
不過通常來說,我們更看重的是Concurrency(並發使用者數). 因為這樣更能反映出系統的 能力。 一般,我們都會對並發使用者數進行一些限制,比如apache的maxClients參數.
ok~ 我們來執行個體分析一下吧.
首先,我們拿到一份測試資料.
接著,我們進行資料分析.
根據並發數和吞吐率的關係得出下列的圖.
OK~ 我們會發現從大約130並發數的地方開始,吞吐率開始下降,而且越多下降的越厲害。 主要是因為,在前面部分隨著使用者數的上升,閒置系統資源得到充分的利用,當然就和正太曲線一樣,總會有個頂點。 當到達一定值後,頂點就會出現了. 這就我們的系統的一個瓶頸.
接著,我們細化分析,回應時間和並發使用者數的相關性
同樣額道理,當並發數到達130左右,正對每個req的回應時間開始增加,越大越抖,這適合吞吐率是相關的。 所以,我們可以得出一個結論,該次串連 並發數 最好設定為100~150之間。 當然,這樣的分析很膚淺,不過,對於我們這些前端寶寶來說瞭解一下就足夠了。
接下來,我們使用工具來武裝自己的頭腦.
這裡主要介紹一個測試載入器,siege.
並發測試載入器
事實上並發測試載入器主要有3個siege,ab,還有webbench. 我這裡之所以沒介紹webbench的原因,因為,我在嘗試安裝他時,老子,電腦差點就掛了(我的MAC pro)... 不過後面,被聰明的我 巧妙的挽回~ 所以,如果有其他大神在MAC x11 上成功安裝,可以私信小弟。讓我學習學習。
ok~ 吐槽完了。我們正式說一下siege吧
siege
安裝siege利用MAC神器 homebrew, 就是就和js前端世界的npm一樣.
安裝ing:
brew install siege
安裝成功--bingo
接著,我們來看一下文法吧.
-c NUM 設定並發的使用者數量.eg: -c 100;
-r NUM 設定發送幾輪的請求,即,總的請求數為: -cNum*-rNum但是, -r不能和-t一起使用(為什麼呢?你猜).eg: -r 20
-t NUM 測試期間,指你運行一次測試需要的時間,在timeout後,結束測試.
-f file. 用來測試file裡面的url路徑 eg: -f girls.txt.
-b . 就是詢問開不開啟基準測試(benchmark)。 這個參數不太重要,有興趣的同學,可以下去學習一下。
關於-c -r我就不介紹了。 大家有興趣,可以參考一下,我前一篇文章讓你升級的網路知識. 這裡主要介紹一下 -f 參數.
通常,如果我們想要測試多個頁面的話,可以建立一個檔案,在檔案中建立 你想測試的所有網頁地址.
比如:
//檔案名稱為 urls.txt
www.example.comwww.example.org123.45.67.89
然後運行測試
siege -f your/file/path.txt -c 100 -t 10s
OK~ 關於進程和測試的內容就介紹到這了。
您可能感興趣的文章:
- 使用cluster 將自己的Node伺服器擴充為多線程伺服器
- Node.js中child_process實現多進程
- nodejs中使用多線程編程的方法執行個體