nodeJS多進程

首先鄭重聲明:
nodeJS 是一門單線程!異步!非阻塞語言!
nodeJS 是一門單線程!異步!非阻塞語言!
nodeJS 是一門單線程!異步!非阻塞語言!php

重要的事情說3遍。 由於nodeJS天生自帶buff, 因此從一出生就受到 萬千 粉絲的追捧(俺,也是它的死忠). 可是,傻逼php 居然嘲笑 我大NodeJS 的性能。 說不穩定,不可靠,只能利用單核CPU。 辣雞 nodeJS.
艹!艹!艹!
搞mo shi~
但,大哥就是大哥,nodeJS在v0.8 的時候就已經加入了cluster的模塊。 徹底打臉php. 雖然,如今php 也開始抄襲nodeJS, 退出php7, 可是,渣渣,你就只會抄...
233333
對不起啊,上面是我自已意淫的一段~ 以上內容,純屬調侃,若是雷同,純屬巧合。
Ok~ 咱們來正式介紹一下nodeJS的多進程吧~html

cluster的前世此生

之前,因爲cluster 自己的不完善,可能因爲多方面緣由吧,實現性能很差。 結果是,pm2 包的 崛起。 輕鬆使用一個pm2 就能夠開啓多進程,實現負載均衡的效果。前端

pm2 start app.js

pm2的內部和cluster內部實現實際上是一個道理,都是封裝了一層child_process--fork. 而child_process--fork 則是封裝了unix 系統的fork 方法。 既然,都到這了,咱們來看看官方給出的解釋吧。node

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.web

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進程 管理。
看圖~~~
此處輸入圖片的描述apache

其實就是這個意思。
Ok~ 這只是系統建立子進程的模型。那麼在NodeJs中是怎樣實現進程之間的交互的呢?
很簡單監聽端口唄。。。
可是,實現通訊不是很難,關鍵在於若是分配請求,這一點nodeJS 踩的坑確實很大。npm

nodeJS 實現進程分配的黑歷史

long time agosegmentfault

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.js
const 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.js
process.on('message',function(mes){
    console.log(`from master, message: ${mes}`);
});
process.send("this is worker");

運行,node app.js, 會輸出一下結果:

from master, message: this is master
from worker, message: this is worker
  1. 如今咱們已經學會了,如何使用child_process來建立一個基本的進程了。
    關於net 這一模塊,你們能夠參考一下net模塊.

ok . 如今咱們正式進入,模擬nodeJS cluster模塊通訊的procedure了。

out of date 的cluster

這裏先介紹一下,曾經的cluster實現的一套機理。一樣,再放一次圖
此處輸入圖片的描述
咱們使用net和child_process來模仿一下。

//master.js
const 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.js
const 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]:52
worker[1183]:42
worker[1184]:90
worker[1181]:16

發現,這樣任由worker去爭奪請求,效率真的很低呀。每一次,觸發請求,都有可能致使驚羣事件的發生啊喂。因此,後來cluster改變了一種模式,使用master來控制請求的分配,官方給出的算法其實就是round-robin 輪轉方法。

高富帥版cluster

如今具體的實現模型就變成這個.
此處輸入圖片的描述
由master來控制請求的給予。經過監聽端口,建立一個socket,將得到的請求傳遞給子進程。
從tj大神那裏借鑑的代碼demo:

//master
const 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.js
const 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');
//自動建立socket
const 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 hits
Availability:              100.00 %
Elapsed time:                2.09 secs
Data transferred:            0.00 MB
Response time:                0.02 secs
Transaction rate:           95.69 trans/sec
Throughput:                0.00 MB/sec
Concurrency:                1.74
Successful transactions:         200
Failed transactions:               0
Longest transaction:            0.05
Shortest transaction:            0.02

那,怎麼模擬大併發嘞?
e e e e e e e e e ...
本身解決啊~
開玩笑的啦~ 否則我寫blog是爲了什麼呢? 就是爲了傳播知識.
在介紹工具以前,我想先說幾個關於性能的基本概念
QPS(TPS),併發數,響應時間,吞吐量,吞吐率

你母雞的性能測試theories

自從咱們和服務器扯上關係後,咱們前端的性能測試真的不少。但這也是咱們必須掌握的tip. 原本前端寶寶只須要看看控制檯,瞭解一下網頁運行是否運行順暢, 看看TimeLine,Profile 就能夠了。 不過,做爲一名有追求,有志於改變世界的童鞋來講。。。
md~ 又要學了...
ok~ 好了,在進入正題以前,我再放一次 線上的測試結果.

Transactions:                 200 hits
Availability:              100.00 %
Elapsed time:               13.46 secs
Data transferred:            0.15 MB
Response time:                3.64 secs
Transaction rate:           14.86 trans/sec
Throughput:                0.01 MB/sec
Concurrency:               54.15
Successful transactions:         200
Failed transactions:               0
Longest transaction:           11.27
Shortest 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.com
www.example.org
123.45.67.89

而後運行測試
siege -f your/file/path.txt -c 100 -t 10s
OK~ 關於進程和測試的內容就介紹到這了。

若是你們以爲,嘿, 這哥們寫的文章不錯呀~
能請我喝杯coffee,勉勵寫出更優質的文章嗎?
此處輸入圖片的描述

轉載請註明出處和做者:http://www.javashuo.com/article/p-nzwmqumv-x.html

相關文章
相關標籤/搜索