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