1. 程式人生 > >node.js多程序架構

node.js多程序架構

node.js是單程序應用,要充分利用多核cpu的效能,就需要用到多程序架構。

作為web伺服器,不能多個程序建立不同的socket檔案描述符去accept網路請求, 有經驗的同學知道,如果埠被佔用了,再跑一個監聽該埠的服務就會報EADDRINUSE異常。那麼問題來了,多程序架構如何去解決這個問題?

 

我們把多程序架構設計成典型的master-workers架構, 一個master, 多個worker。

 

master-workers架構如下圖所示:

 

 

我們可以在master程序代理accept請求然後分配給worker處理。但客戶端程序連線到master程序,master程序連線到worker程序需要用掉兩個檔案描述符,會浪費掉一倍數量的檔案描述符。

所以交由worker來accept請求會是更好的方案。

 

master先建立一個server監聽埠,然後通過程序間通訊,把socket檔案描述符傳遞給所有的worker程序, worker程序用傳遞過來的socket檔案描述符封裝成server(感官上好像是把一個server物件傳送給另一個程序,其實是把相應的控制代碼封裝後,通過JSON.stringify()序列化再發送, 接收端程序還原成相應的控制代碼。)

 

然後,還有一個問題,假如其中一個worker程序異常退出了怎麼辦, 這個時候,worker程序應該要通知到master程序,然後master程序重新fork一個worker程序。

 

先上master的程式碼:

 1 "use strict"
 2 
 3 const fork = require('child_process').fork;
 4 const cpus = require('os').cpus();
 5 let server = require('net').createServer((socket)=>{
 6     // ‘connection’ 監聽器
 7     socket.end('Handled by master \n');
 8     console.error('Handled by master \n');   //不應該在master accept請求
 9 });
10 
11 
12 server.listen(8001);
13 
14 let workers = {};
15 
16 function createWorker(ser) {
17     let worker = fork('./worker.js');
18 
19     worker.on('message', function(msg, handle) {
20         // 收到子程序通知需要建立新的worker(子程序退出前通知父程序)
21         if(msg ==='new_worker') {
22             let ser = handle;
23             createWorker(ser);
24             // 關掉
25             ser.close();
26         }
27     })
28 
29 
30     worker.on('exit', function(code, signal){
31         delete workers[worker.pid];
32     });
33 
34     // 控制代碼轉發 
35     let result = worker.send('server', ser, (err)=> {err&&console.error(err)});
36     console.info('send server to child result:', result);
37     workers[worker.pid] = worker;
38 }
39 
40 for(let i=0; i<cpus.length; i++) {
41     createWorker(server);
42 }
43 
44 // 關掉,不再accept埠請求
45 server.close();
46 
47 /*
48 code <number> The exit code if the child exited on its own.
49 signal <string> The signal by which the child process was terminated.
50  */
51 process.on('exit', function(code, signal) {
52     console.log(`master exit, code:${code}, signal:${signal}`);
53     for(let pid in workers) {
54         workers[pid].kill();
55     }
56 })
57 
58 process.on('uncaughtException', function(error) {
59     console.error('master | uncaughtException, error:', error);
60     process.exit(1);
61 })
62 
63 //一些常用的退出訊號的處理:
64 // kill pid 預設是SIGTERM訊號
65 // 控制檯 ctrl-c 是SIGINT訊號
66 const killSignalList = ['SIGTERM', 'SIGINT'];
67 killSignalList.forEach((SIGNAL)=>{
68     process.on(SIGNAL, function(){
69         console.log(`${SIGNAL} signal`);
70         process.exit(1);
71     })  
72 })

 

 

master程序根據cpu核數fork相應數量的worker程序, fork成功後馬上把server控制代碼傳送給worker程序, fork所有worker程序後, 就把server關掉,不再接收請求。 master程序退出前會呼叫worker的kill()方法殺掉所有worker程序。

 

 

worker程式碼如下:

 1 const http = require('http');
 2 
 3 
 4 const server = http.createServer(function(req, res) {
 5      // ‘request’ 監聽器
 6     res.end('handled by worker \n');
 7     // throw new Error('error');
 8 })
 9 
10 let worker;
11 process.on('message', function(msg, handle){
12     if(msg === 'server') {
13         worker = handle;
14         worker.on('connection', function(socket){
15             server.emit('connection', socket);
16         })
17     }
18 
19 })
20 
21 
22 process.on('uncaughtException', function(err) {
23     console.error('uncaughtException err:', err.message, ', worker程序將重啟');
24     // 通知master建立新的worker
25     process.send('new_worker', worker);
26     // 停止接收新的連線
27     worker.close(function() {
28         // 所有已有連線斷開後,退出程序
29         process.exit(1);
30     });
31 });

 

 

 worker程序有個細節處理的地方: 異常退出前,先通知master程序建立新的worker, 然後等待所有已有連線斷開後再退出程序。

 

關於程序間的控制代碼傳送功能, 有興趣的同學可以再去了解一下, 子程序物件send(message,[sendHandle])方法可以傳送的控制代碼型別有:

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

 

多個worker程序監聽同一個套接字,會導致驚群現象, 有請求過來時cpu會喚醒所有的worker程序, 最終只有一個程序accept到請求, 其它程序accept請求失敗,這種情況會產生一些不必要的開銷。 如何避免驚群現象,我另外寫一篇文章具體說一下。

&n