nodejs玩兒轉進程

序言node

  1. nodejs是如何充分利用多核cup 服務器的?
  2. 如何保證進程的穩健型?

正文緩存

由於Node運行在V8引擎上,咱們的JavaScript 將會運行在單個進程的單個線程上。它帶來的好處是: 程序狀態是單一的,在沒有多線程的狀況 下沒有鎖、線程同步問題,操做系統在調度時也由於較少上下文的切換,能夠很好地提升CPU的使用率bash

從嚴格的意義上而言,Node並不是真正的單線程架構,Node自身還有 必定的I/O線程存在,這些I/O線程由底層libuv處理,這部分線程對於JavaScript開發者而言是透明 的,只在C++擴展開發時纔會關注到服務器

進程和線程的區別及優劣:
1進程是操做系統分配資源的最小單元
複製代碼

多進程的缺點主要體如今網絡

1 沒法共享內部狀態(進程池的方式能夠解決)
	2 以及建立和銷燬進程時候
複製代碼

多線程相對多進程的優勢:多線程

建立和銷燬線程相對進程來講開銷小不少,(而且線程之間能夠共享數據 ,內存浪費的問題得
以解決)而且利用線程池能夠減小建立和銷燬線程的開銷
複製代碼

多線程的缺點:架構

每一個線程都有本身獨立的堆棧,每一個堆棧都要佔用必定的內存空間
複製代碼

服務模型的變遷:

從「古」到今,Web服務器的架構已經歷了幾回變遷。從服務器處理客戶端請求的併發量這個緯度來看,每次變遷都是里程碑的見證併發

由此來看 多線程和事件驅動都有本身弊端

事件驅動:CPU的計算能力決定這類服務的性能上線
多線程模式:受資源上限的影響
複製代碼

那麼nodejs是如何充分利用多核cup 服務器的?

答案是經過fork進程的方式 ,咱們再一次將經典的示例代碼存爲worker.js文件,代碼以下:負載均衡

var http = require('http'); http.createServer(function (req, res) {

	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Hello World\n');
	
}).listen(Math.round((1 + Math.random()) * 1000), '127.0.0.1');
複製代碼

經過node worker.js啓動它,將會偵聽1000到2000之間的一個隨機端口 將如下代碼存爲master.js,並經過node master.js啓動它:dom

/** * 充分利用cup的資源同時啓動在多個進程上啓動服務 */
const cpus = require("os").cpus();
const fork = require("child_process").fork;

for (let index = 0; index < cpus.length; index++) {
  fork("./worker.js");
}
複製代碼

這段代碼將會根據當前機器上的CPU數量複製出對應Node進程數。在*nix系統下能夠經過ps aux | grep worker.js查看到進程的數量,以下所示

建立子進程

child_process 模塊賦予了node能夠隨意建立子進程的能力 ,它提供了4個方法用於建立子進程:

spawn(): 啓動一個子進程來執行命令 exec: 啓動一個子進程來執行命令,與spawn不一樣的是其接口不一樣,他有一個回掉函數來獲知子進程的情況。 execFile():啓動一個子進程來執行可執行文件。 fork():與spawn()相似,不一樣點在於它建立Node的子進程只需指定要執行的JavaScript文件模塊便可。

spawn()與exec()、execFile()的不一樣是:
後二者建立時能夠指定timeout屬性設置超時時間,一旦建立的進程運行超過設定的時間將會
被殺死。
複製代碼

exec()與execFile()不一樣的是,exec()適合執行已有的命令execFile()適合執行文件。這裏咱們以一個尋常命令爲例,node worker.js分別用上述4種方法實現,以下所示

var cp = require('child_process');
//spawn
cp.spawn('node', ['worker.js']);
//exec
cp.exec('node worker.js', function (err, stdout, stderr) {
    // some code 
});
//execFile
cp.execFile('worker.js', function (err, stdout, stderr) { 
	// some code
}); 
//fork
cp.fork('./worker.js');
複製代碼

若是是JavaScript文件經過execFile()運行,它的首行內容必須添加以下代碼

#!/usr/bin/env node
複製代碼

儘管4種建立子進程的方式有些差異,但事實上後面3種方法都是spawn()的延伸應用

進程間通訊

主線程與工做線程之間經過onmessage()和postMessage()進行通訊,子進程對象則由send() 方法實現主進程向子進程發送數據message事件實現收聽子進程發來的數據,與API在必定 程度上類似。經過消息傳遞內容,而不是共享或直接操做相關資源,這是較爲輕量和無依賴 的作法

parent.js

// 
var cp = require('child_process');
var n = cp.fork(__dirname + '/sub.js');
n.on('message', function (m) { console.log('PARENT got message:', m);
});
n.send({hello: 'world'});
複製代碼

sub.js

process.on('message', function (m) { 
	console.log('CHILD got message:', m);
});
process.send({foo: 'bar'});
複製代碼

經過fork()或者其餘API,建立子進程以後,爲了實現父子進程之間的通訊,父進程與子進程之間將會建立IPC通道。經過IPC通道,父子進程之間才能經過message和send()傳遞消息

進程間通訊原理

IPC的全稱是Inter-Process Communication,即進程間通訊 進程間通訊的目的是爲了讓不一樣的進程可以互相訪問資源並進行協調工做

實現進程間通訊的技術有不少,如 命名管道 匿名管道 socket 信號量 共享內存 消息隊列 Domain Socket

Node中實現IPC通道的是管道(pipe) 技術。但此管道非彼管道,在Node中管道是個抽象層面的稱呼,具體細節實現由libuv提供,在 Windows下由命名管道(named pipe)實現,*nix系統則採用Unix Domain Socket實現。表如今應用層上的進程間通訊只有簡單的message事件和send()方法,接口十分簡潔和消息化。下圖爲IPC 建立和實現的示意圖。

父進程在實際建立子進程以前,會建立IPC通道並監聽它,而後才真正建立出子進程並通 過環境變量(NODE_CHANNEL_FD)告訴子進程這個IPC通道的文件描述符。子進程在啓動的過程當中, 根據文件描述符去鏈接這個已存在的IPC通道,從而完成父子進程之間的鏈接

句柄傳遞

創建好進程之間的IPC後,若是僅僅只用來發送一些簡單的數據,顯然不夠咱們的實際應用 使用 若是讓服務都監聽 到相同的端口,將會有什麼樣的結果?

這時只有一個工做進程可以監聽到該端口上,其他的進程在監聽的過程當中都拋出了 EADDRINUSE異常,這是端口被佔用的狀況,新的進程不能繼續監聽該端口了。這個問題破壞了我 們將多個進程監聽同一個端口的想法。要解決這個問題,一般的作法是讓每一個進程監聽不一樣的端 口,其中主進程監聽主端口(如80),主進程對外接收全部的網絡請求,再將這些請求分別代理 到不一樣的端口的進程上。示意圖如圖9-4所示。

經過代理,能夠避免端口不能重複監聽的問題,甚至能夠在代理進程上作適當的負載均衡, 使得每一個子進程能夠較爲均衡地執行任務。因爲進程每接收到一個鏈接,將會用掉一個文件描述 符,所以代理方案中客戶端鏈接到代理進程, 代理進程鏈接到工做進程的過程須要用掉兩個文件 描述符。操做系統的文件描述符是有限的,代理方案浪費掉一倍數量的文件描述符 的作法影響了 系統的擴展能力

主進程代碼以下所示
var child = require('child_process').fork('child.js');
// Open up the server object and send the handle 
var server = require('net').createServer(); 
server.on('connection', function (socket) {
	socket.end('handled by parent\n'); 
});
server.listen(1337, function () { 
	child.send('server', server);
});
複製代碼
子進程代碼以下所示:
process.on('message', function (m, server) { 
	if (m === 'server') {
		server.on('connection', function (socket) { 
			socket.end('handled by child\n');
		}); 
	}
});
複製代碼

而後新開一個命令行窗口,用上curl工具,以下所示:

$ curl "http://127.0.0.1:1337/" 
handled by parent
$ curl "http://127.0.0.1:1337/"
handled by child
$ curl "http://127.0.0.1:1337/" 
handled by child
$ curl "http://127.0.0.1:1337/" 
handled by parent
複製代碼

命令行中的響應結果也是很難以想象的,這裏子進程和父進程都有可能處理咱們客戶端發起 的請求。 試試將服務發送給多個子進程,以下所示: parent.js

var cp = require('child_process'); 
var child1 = cp.fork('child.js'); 
var child2 = cp.fork('child.js');
// Open up the server object and send the handle 
var server = require('net').createServer(); 
server.on('connection', function (socket) {
	socket.end('handled by parent\n'); 
});
server.listen(1337, function () { 
	child1.send('server', server); 		
	child2.send('server', server);
});

複製代碼

而後在子進程中將進程ID打印出來,以下所示: // child.js

process.on('message', function (m, server) { 
	if (m === 'server') {
		server.on('connection', function (socket) {
			socket.end('handled by child, pid is ' + process.pid + '\n');
		}); 
	}
});
複製代碼

再用curl測試咱們的服務,以下所示:

$ curl "http://127.0.0.1:1337/" 
handled by child, pid is 24673 
$ curl "http://127.0.0.1:1337/" 
handled by parent
$ curl "http://127.0.0.1:1337/" 
handled by child, pid is 24672
複製代碼

測試的結果是每次出現的結果均可能不一樣,結果可能被父進程處理,也可能被不一樣的子進程 處理。 其實咱們能夠在父進程啓動以後立馬把他close掉

const cp = require("child_process")
const child1 = cp.fork("child.js");
const child2 = cp.fork('child.js');
const server = require("net").createServer();

server.on("connection",(socket)=>{
  socket.end('handled by parent\n');
})

server.listen(1337,()=>{
  child2.send('server', server);
  child1.send('server', server);
  server.close();
})
複製代碼

整個過程當中,服務的過程發生了一次改變,如

主進程發送完句柄並關閉監聽以後成爲了下圖所示的結構

咱們神奇地發現, 多個子進程能夠同時監聽相同端口,再沒有EADDRINUSE異常發生了

句柄發送與還原

句柄發送跟咱們直接將服務器對象發送給子進程有沒有差異?它是否真的將服務器對象發送給了子進程?爲何它能夠發送到多個子進程 中?發送給子進程爲何父進程中還存在這個對象? 目前子進程對象send()方法能夠發送的句柄類型包括以下幾種。

  • net.Socket。TCP套接字。
  • net.Server。TCP服務器,任意創建在TCP服務上的應用層服務均可以享受到它帶來的好處。
  • net.Native。C++層面的TCP套接字或IPC管道。
  • dgram.Socket。UDP套接字。
  • dgram.Native。C++層面的UDP套接字。

send()方法在將消息發送到IPC管道前,將消息組裝成兩個對象,一個參數是handle,另外一個 是message。message參數以下所示

{
	cmd: 'NODE_HANDLE',
	type: 'net.Server', 
	msg: message
}
複製代碼

發送到IPC管道中的其實是咱們要發送的句柄文件描述符,文件描述符其實是一個整數 值。這個message對象在寫入到IPC管道時也會經過JSON.stringify()進行序列化。因此最終發送 到IPC通道中的信息都是字符串,send()方法能發送消息和句柄並不意味着它能發送任意對象。

鏈接了IPC通道的子進程能夠讀取到父進程發來的消息,將字符串經過JSON.parse()解析還 原爲對象後,才觸發message事件將消息體傳遞給應用層使用。在這個過程當中,消息對象還要被 進行過濾處理,message.cmd的值若是以NODE_爲前綴,它將響應一個內部事件internalMessage。

若是message.cmd值爲NODE_HANDLE,它將取出message.type值和獲得的文件描述符
 一塊兒還原出一個對應的對象。這個過程的示意圖如圖所示
複製代碼

以發送的tcp服務器句柄爲例 子進程收到消息後的還原過程以下

function(message, handle, emit) {
 var self = this;
var server = new net.Server(); 
server.listen(handle, function() {
	emit(server); 4 });
}
複製代碼

上面的代碼中,子進程根據message.type建立對應TCP服務器對象,而後監聽到文件描述符上。因爲底層細節不被應用層感知,因此在子進程中,開發者會有一種服務器就是從父進程中直接傳遞過來的錯覺。值得注意的是,Node進程之間只有消息傳遞,不會真正地傳遞對象,這種錯 覺是抽象封裝的結果

端口共同監聽

多個進程能夠監聽到 相同的端口而不引發EADDRINUSE異常?

咱們獨立啓動的進程中,TCP服務器端socket套接字的文件描述符並不相同,致使監聽到相同的端口時會拋出異常。

Node底層對每一個端口監聽都設置了SO_REUSEADDR選項,這個選項的涵義是不一樣進程能夠就相 同的網卡和端口進行監聽,這個服務器端套接字能夠被不一樣的進程複用,以下所示

setsockopt(tcp->io_watcher.fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))
複製代碼

因爲獨立啓動的進程互相之間並不知道文件描述符,因此監聽相同端口時就會失敗。但對於 send()發送的句柄還原出來的服務而言,它們的文件描述符是相同的,因此監聽相同端口不會引 起異常

多個應用監聽相同端口時,文件描述符同一時間只能被某個進程所用。換言之就是網絡請求 向服務器端發送時,只有一個幸運的進程可以搶到鏈接,也就是說只有它能爲這個請求進行服務。 這些進程服務是搶佔式的。

集羣穩定之路

搭建好了集羣,充分利用了多核CPU資源,彷佛就能夠迎接客戶端大量的請求了。但請等等, 咱們還有一些細節須要考慮。

  • 性能問題。
  • 多個工做進程的存活狀態管理。
  • 工做進程的平滑重啓。
  • 配置或者靜態數據的動態從新載入。
  • 其餘細節。

進程事件

再次迴歸到子進程對象上,除了引人關注的send()方法和message事件外,子進程還有些什 麼呢?首先除了message事件外,Node還有以下這些事件:

error:當子進程沒法被複制建立、沒法被殺死、沒法發送消息時會觸發該事件 exit:子進程退出時觸發該事件,子進程若是是正常退出,這個事件的第一個參數爲退出 碼,不然爲null。若是進程是經過kill()方法被殺死的,會獲得第二個參數,它表示殺死進程時的信號。 close:在子進程的標準輸入輸出流停止時觸發該事件,參數與exit相同 disconnect:在父進程或子進程中調用disconnect()方法時觸發該事件,在調用該方法時將關閉監聽IPC通道。

自動重啓

有了父子進程之間的相關事件以後,就能夠在這些關係之間建立出須要的機制了。至少咱們 可以經過監聽子進程的exit事件來獲知其退出的信息,接着前文的多進程架構,咱們在主進程上 要加入一些子進程管理的機制,好比從新啓動一個工做進程來繼續服務。

實現代碼以下所示: master.js

// 主進程
const fork = require("child_process").fork;
const cpus = require("os").cpus();

// 建立tcp server
const server = require("net").createServer();
server.listen(1337);

const workers = {}

// 建立進程的函數
const createWorker = () =>{
  const worker = fork(__dirname+"/work.js");
  // 監聽進程退出事件 自動重啓
  worker.on("exit",()=>{
    delete workers[worker.pid];
    console.log(worker.pid+"is delete");
    createWorker();
  })
  // 發送當前進程的句柄文件描述符
  worker.send("server",server)
  workers[worker.pid]= worker
  console.log('created worker :', worker.pid);
}

for (let index = 0; index < cpus.length; index++) {
  createWorker();
}

// 進程本身退出時,讓全部工做進程退出 
process.on('exit', function () {
  for (var pid in workers) { 
    workers[pid].kill();
  } 
});

console.log('process.pid', process.pid)
複製代碼

work.js

var http = require('http');
var server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('handled by child, pid is' + process.pid + '\n'); 
});

var worker;
process.on('message', function (m, tcp) {
  if (m === 'server') {
    worker = tcp;
    worker.on('connection', function (socket) {
      server.emit('connection', socket); 
    });
  } 
});

process.on('uncaughtException', function () { // 中止接收新的鏈接
  worker.close(function () {
    // 全部已有鏈接斷開後,退出進程
    process.exit(1); 
  });
});
複製代碼

測試一下上面的代碼,以下所示:

$ node master.js
Create worker: 30504 
Create worker: 30505 
Create worker: 30506 
Create worker: 30507
複製代碼

上述代碼的處理流程是,一旦有未捕獲的異常出現,工做進程就會當即中止接收新的鏈接; 當全部鏈接斷開後,退出進程。主進程在偵聽到工做進程的exit後,將會當即啓動新的進程服務, 以此保證整個集羣中老是有進程在爲用戶服務的。 經過kill命令殺死某個進程試試,以下所示

$ kill 30506
複製代碼

結果是30506進程退出後,自動啓動了一個新的工做進程30518,整體進程數量並無發生改 變,以下所示:

Worker 30506 exited. 
Create worker. pid: 30518
複製代碼

自殺信號

固然上述代碼存在的問題是要等到已有的全部鏈接斷開後進程才退出,在極端的狀況下,所 有工做進程都中止接收新的鏈接,全處在等待退出的狀態。但在等到進程徹底退出才重啓的過程 中,全部新來的請求可能存在沒有工做進程爲新用戶服務的情景,這會丟掉大部分請求。

爲此須要改進這個過程,不能等到工做進程退出後才重啓新的工做進程。固然也不能暴力退 出進程,由於這樣會致使已鏈接的用戶直接斷開。因而咱們在退出的流程中增長一個自殺 (suicide)信號。工做進程在得知要退出時,向主進程發送一個自殺信號,而後才中止接收新的 鏈接,當全部鏈接斷開後才退出。主進程在接收到自殺信號後,當即建立新的工做進程服務。 代碼改動以下所示:

// master.js 主要是重啓進程的任務放到了 接收到suicide 事件以後
// 主進程
const fork = require("child_process").fork;
const cpus = require("os").cpus();

// 建立tcp server
const server = require("net").createServer();
server.listen(1337);

const workers = {}

// 建立進程的函數
const createWorker = () =>{
  const worker = fork(__dirname+"/work.js");
  // 啓動新的進程
  worker.on('message', function (message) {
    if (message.act === 'suicide') { 
      createWorker();
    } 
  });
  
  worker.on("exit",()=>{
    delete workers[worker.pid];
    console.log(worker.pid+"is delete");
  })
  // 發送當前進程的句柄文件描述符
  worker.send("server",server)
  workers[worker.pid]= worker
  console.log('created worker :', worker.pid);
}

for (let index = 0; index < cpus.length; index++) {
  createWorker();
}

// 進程本身退出時,讓全部工做進程退出 
process.on('exit', function () {
  for (var pid in workers) { 
    workers[pid].kill();
  } 
});

console.log('process.pid', process.pid)
複製代碼

work.js主要是再接收到未捕獲的異常以後向主進程發送事件告知子進程將要退出 此時建立新的進程爲用戶服務 ,以後子進程才退出 再回頭看重啓信息,以下所示:

created worker : 14397
14394is delete
複製代碼

與前一種方案相比,建立新工做進程在前,退出異常進程在後。在這個可憐的異常進程退出 以前,老是有新的工做進程來替上它的崗位。至此咱們完成了進程的平滑重啓,一旦有異常出現, 主進程會建立新的工做進程來爲用戶服務,舊的進程一旦處理完已有鏈接就自動斷開。整個過程 使得咱們的應用的穩定性和健壯性大大提升。示意圖如圖所示

這裏存在問題的是有可能咱們的鏈接是長鏈接,不是HTTP服務的這種短鏈接,等待長鏈接 斷開可能須要較久的時間。爲此爲已有鏈接的斷開設置一個超時時間是必要的,在限定時間裏強 制退出的設置以下所示:

process.on('uncaughtException', function (err) {
process.send({act: 'suicide'}); 2 // 中止接收新的鏈接
   worker.close(function () {
   	// 全部已有鏈接斷開後,退出進程
   	process.exit(1);
   }); // 5秒後退出進程
   setTimeout(function () {
     process.exit(1); 
   }, 5000);
});
複製代碼

進程中若是出現未能捕獲的異常,就意味着有那麼一段代碼在健壯性上是不合格的。爲此退 出進程前,經過日誌記錄下問題所在是必需要作的事情,它能夠幫咱們很好地定位和追蹤代碼異 常出現的位置,以下所示:

process.on('uncaughtException', function (err) { // 記錄日誌
	logger.error(err);
	// 發送自殺信號
	process.send({act: 'suicide'}); // 中止接收新的鏈接 
	worker.close(function () {
	// 全部已有鏈接斷開後,退出進程
		process.exit(1); 
	});
	// 5秒後退出進程 
	setTimeout(function () {
		process.exit(1);
	}, 5000);
});

複製代碼

經過自殺信號告知主進程可使得新鏈接老是有進程服務,可是依然仍是有極端的狀況。工 做進程不能無限制地被重啓,若是啓動的過程當中就發生了錯誤,或者啓動後接到鏈接就收到錯誤, 會致使工做進程被頻繁重啓,這種頻繁重啓不屬於咱們捕捉未知異常的狀況,由於這種短期內 頻繁重啓已經不符合預期的設置,極有多是程序編寫的錯誤。 爲了消除這種無心義的重啓,在知足必定規則的限制下,不該當反覆重啓。好比在單位時間 內規定只能重啓多少次,超過限制就觸發giveup事件,告知放棄重啓工做進程這個重要事件。 爲了完成限量重啓的統計,咱們引入一個隊列來作標記,在每次重啓工做進程之間進行打點 並判斷重啓是否太過頻繁,以下所示:

// 重啓次數 
var limit = 10;
// 時間單位
var during = 60000;
var restart = [];
var isTooFrequently = function () {
	// 記錄重啓時間
	var time = Date.now();
	var length = restart.push(time);
	if (length > limit) {
		// 取出最後10個記錄
		restart = restart.slice(limit * -1);
	}
	// 最後一次重啓到前10次重啓之間的時間間隔
	return restart.length >= limit && restart[restart.length - 1] - restart[0] < during; 
};
var workers = {};
var createWorker = function () {
	// 檢查是否太過頻繁
	if (isTooFrequently()) {
	// 觸發giveup事件後,再也不重啓 
		process.emit('giveup', length, during);
		return;
	}
	var worker = fork(__dirname + '/worker.js'); 
	worker.on('exit', function () {
		console.log('Worker ' + worker.pid + ' exited.');
		delete workers[worker.pid]; 
	});
	// 從新啓動新的進程
	worker.on('message', function (message) {
		if (message.act === 'suicide') {
		 createWorker();
		} 
	});
// 句柄轉發
worker.send('server', server); workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
};
複製代碼

giveup事件是比uncaughtException更嚴重的異常事件。uncaughtException只表明集羣中某個 工做進程退出,在總體性保證下,不會出現用戶得不到服務的狀況,可是這個giveup事件則表示 集羣中沒有任何進程服務了,十分危險。爲了健壯性考慮,咱們應在giveup事件中添加劇要日誌, 並讓監控系統監視到這個嚴重錯誤,進而報警等。

負載均衡

在多進程之間監聽相同的端口,使得用戶請求可以分散到多個進程上進行處理,這帶來的好 處是能夠將CPU資源都調用起來。這猶如飯店將客人的點單分發給多個廚師進行餐點製做。既然 涉及多個廚師共同處理全部菜單,那麼保證每一個廚師的工做量是一門學問,既不能讓一些廚師忙不過來,也不能讓一些廚師閒着,這種保證多個處理單元工做量公平的策略叫負載均衡Node默認提供的機制是採用操做系統的搶佔式策略。所謂的搶佔式就是在一堆工做進程中,閒着的進程對到來的請求進行爭搶,誰搶到誰服務。

通常而言,這種搶佔式策略對你們是公平的,各個進程能夠根據本身的繁忙度來進行搶佔。對於Node而言,它的繁忙是由CPU、I/O兩個部分構成的影響搶佔的是CPU 的繁忙度對不一樣的業務,可能存在I/O繁忙,而CPU較爲空閒的狀況,這可能形成某個進程能 夠搶到較多請求,造成負載不均衡的狀況

爲此Node在v0.11中提供了一種新的策略使得負載均衡更合理,這種新的策略叫 Round-Robin,又叫輪叫調度。輪叫調度的工做方式是由主進程接受鏈接,將其依次分發給工做 進程。分發的策略是在N個工做進程中,每次選擇第i = (i + 1) mod n個進程來發送鏈接。在cluster 模塊中啓用它的方式以下:

狀態共享

Node進程中不宜存放太多數據,由於它會加劇垃圾回收的負擔,進 而影響性能。同時,Node也不容許在多個進程之間共享數據。但在實際的業務中,每每須要共享 一些數據,譬如配置數據,這在多個進程中應當是一致的。爲此,在不容許共享數據的狀況下, 咱們須要一種方案和機制來實現數據在多個進程之間的共享。

  1. 第三方數據存儲 解決數據共享最直接、簡單的方式就是經過第三方來進行數據存儲,好比將數據存放到數據 庫、磁盤文件、緩存服務(如Redis)中,全部工做進程啓動時將其讀取進內存中。但這種方式 存在的問題是若是數據發生改變,還須要一種機制通知到各個子進程,使得它們的內部狀態也得 到更新。 實現狀態同步的機制有兩種,一種是各個子進程去向第三方進行定時輪詢,示意圖如圖所示。

實現狀態同步的機制有兩種:

一種是各個子進程去向第三方進行定時輪詢
複製代碼

定時輪詢帶來的問題是輪詢時間不能過密,若是子進程過多,會造成併發處理,若是數據沒 有發生改變,這些輪詢會沒有意義,白白增長查詢狀態的開銷。若是輪詢時間過長,數據發生改 變時,不能及時更新到子進程中,會有必定的延遲。 2. 主動通知 一種改進的方式是當數據發生更新時,主動通知子進程。固然,即便是主動通知,也須要一 種機制來及時獲取數據的改變。這個過程仍然不能脫離輪詢,但咱們能夠減小輪詢的進程數量, 咱們將這種用來發送通知和查詢狀態是否更改的進程叫作通知進程。爲了避免混合業務邏輯,能夠 將這個進程設計爲只進行輪詢和通知,不處理任何業務邏輯,示意圖如圖所示

這種推送機制若是按進程間信號傳遞,在跨多臺服務器時會無效,是故能夠考慮採用TCP或 UDP的方案。進程在啓動時從通知服務處除了讀取第一次數據外,還將進程信息註冊到通知服務 處。一旦經過輪詢發現有數據更新後,根據註冊信息,將更新後的數據發送給工做進程。因爲不涉及太多進程去向同一地方進行狀態查詢,狀態響應處的壓力不至於太過巨大,單一的通知服務 輪詢帶來的壓力並不大,因此能夠將輪詢時間調整得較短,一旦發現更新,就能實時地推送到各個子進程中。

Cluster 模塊 v0.8時直接引入了cluster模塊,用以解決多核CPU的利用率問題,同時也提供了較完 善的API,用以處理進程的健壯性問題 對於開頭提到的建立Node進程集羣,cluster實現起來也是很輕鬆的事情,以下所示

// 事實上cluster模塊就是child_process和net模塊的組合應用
const cluster = require("cluster");
const cpus = require('os').cpus();

cluster.setupMaster({
  exec: "worker.js"
})

for(var i = 0; i < cpus.length; i++) {
  cluster.fork();
}

複製代碼

Cluster 事件

對於健壯性處理,cluster模塊也暴露了至關多的事件。

fork:複製一個工做進程後觸發該事件。
online:複製好一個工做進程後,工做進程主動發送一條online消息給主進程,主進程收到消息後,觸發該事件。
listening:工做進程中調用listen()(共享了服務器端Socket)後,發送一條listening消息給主進程,主進程收到消息後,觸發該事件。
disconnect:主進程和工做進程之間IPC通道斷開後會觸發該事件。
exit:有工做進程退出時觸發該事件。
setup:cluster.setupMaster()執行後觸發該事件。
複製代碼

這些事件大多跟child_process模塊的事件相關,在進程間消息傳遞的基礎上完成的封裝。 這些事件對於加強應用的健壯性已經足夠了 儘管經過child_process模塊能夠大幅提高Node的穩定性,可是一旦主進程出現問題,所 有子進程將會失去管理。在Node的進程管理以外,還須要用監聽進程數量或監聽日誌的方式確 保整個系統的穩定性,即便主進程出錯退出,也能及時獲得監控警報,使得開發者能夠及時處 理故障

相關文章
相關標籤/搜索