Nodejs搭建多進程Web服務器實現過程
前言
上節我們講到,通過 fork()
或者其他API,創建子進程之後,可以通過 send()
和 process.on('message')
進行父子進程間的通信。這樣就實現瞭主進程代理請求到工作進程,實現瞭 Nodejs集群
:
父子進程間通信
負載均衡
通過代理,可以避免端口不能重復監聽的問題,甚至可以在代理進程上做適當的負載均衡,使得每個子進程可以較為均衡地執行任務。下面我們構建瞭一個簡單的 Web 服務器,並實現在兩個工作進程之間做簡單的負載均衡。
主進程,負責代理到對應進程中:
// main.js const { fork } = require('child_process'); const normal = fork('subprocess.js', ['normal']); const special = fork('subprocess.js', ['special']); // Open up the server and send sockets to child. Use pauseOnConnect to prevent // 套接字在發送給子進程之前不會被讀取 const server = require('net').createServer({ pauseOnConnect: true }); let flag = 0; server.on('connection', (socket) => { flag++; // this is special priority. if (flag % 2 === 0) { special.send('socket', socket); return; } // This is normal priority. normal.send('socket', socket); }); server.listen(1337);
這是工作進程,接收socket對象並做出響應:
// subprocess.js process.on('message', (m, socket) => { if (m === 'socket') { // Check that the client socket exists. // It is possible for the socket to be closed between the time it is if (socket) { // console.log(`Request handled with ${process.argv[2]} priority`); socket.end(`Request handled with ${process.argv[2]} priority, running on ${process.pid}`); } } });
然後我又編寫瞭一個 Nodejs 腳本,來發出十個 HTTP 請求:
const cp = require("child_process"); for (let i = 0; i < 10; i++) { cp.exec(`curl --http0.9 "http://127.0.0.1:1337"`, (err, stdout, stderr) => { console.log(`finished: ${i}, and received: `, stdout); }) }
最後運行結果如下:
句柄傳遞
在使用 send()
方法時,我們註意到,除瞭能通過IPC發送數據外,還能發送句柄。第二個可選參數就是一個句柄:
child.send(message, [sendHandle]);
💡 句柄是一種可以用來標識資源的引用,它的內部包含瞭指向對象的文件描述符。比如句柄可以用來標識一個服務器端socket對象、一個客戶端socket對象、一個UDP套接字、一個管道等。
在主進程將句柄發送給子進程之後,工作模型就從主進程響應用戶請求變成瞭子進程監聽用戶活動:
進程對象send()方法可以發送的句柄類型包括如下幾種:
- net.Socket。TCP套接字。
- net.Server。TCP服務器,任意建立在TCP服務上的應用層服務都可以享受到它帶來的好處。
- net.Native。C++層面的TCP套接字或IPC管道。
- dgram.Socket。UDP套接字。
- dgram.Native。C++層面的UDP套接字。
💡 另外要註意,send()方法能發送消息和句柄並不意味著它能發送任意對象,message
參數和文件句柄都要先通過 JSON.stringfy()
進行序列化後再放入IPC通道中:
集群
通過 child_process模塊
,我們完成瞭父子進程的創建和通信,已經初步搭建瞭一個Node集群。還有一些問題需要考慮:
- 性能問題。
- 多個工作進程的存活狀態管理。
- 工作進程的平滑重啟。
- 配置或者靜態數據的動態重新載入。
- 其他細節。
這其中最重要的便是集群的穩定性,這決定瞭該服務模型能否真正用於實踐生成中。雖然我們創建瞭很多工作進程,但每個工作進程依然是在單線程上執行的,它的穩定性還不能得到完全的保障。我們需要建立起一個健全的機制來保障Node應用的健壯性。
子進程事件
父進程能監聽到的,與子進程相關的事件:
- error:當子進程無法被復制創建、無法被殺死、無法發送消息時會觸發該事件。
- exit:子進程退出時觸發該事件。如果是正常退出,這個事件的第一個參數為退出碼,否則為null。如果進程是通過kill()方法被殺死的,會得到第二個參數,它表示殺死進程時的信號。
- close:在子進程的標準輸入輸出流中止時觸發該事件,參數與exit相同。
- disconnect:在父進程或子進程中調用disconnect()方法時觸發該事件,在調用該方法時將關閉監聽IPC通道。
除瞭 send()
外,還能通過 kill()
方法給子進程發送消息。kill() 方法並不能真正地將通過IPC相連的子進程殺死,它隻是給子進程發送瞭一個系統信號。默認情況下,父進程將通過 kill() 方法給子進程發送一個 SIGTERM信號
。
// 子進程 child.kill([signal]); // 當前進程 process.kill(pid, [signal]); // 監聽 process.on(signal, callback)
💡 在POSIX標準中,有一套完備的信號系統,在命令行中執行kill -l可以看到詳細的信號列表,如下所示:
而 Node 提供瞭這些信號對應的信號事件,每個進程都可以監聽這些信號事件。這些信號事件是用來通知進程的,每個信號事件有不同的含義,進程在收到響應信號時,應當做出約定的行為:
process.on('SIGTERM', () => { console.log("got sigterm, exiting..."); process.exit(1); }); console.log("process running on: ", process.pid); process.kill(process.pid, "SIGTERM");
自動重啟
有瞭父子進程之間的相關事件之後,就可以在這些關系之間創建出需要的機制瞭,至少我們能夠通過監聽子進程的 exit事件
來獲知其退出的信息。接著前文的多進程架構,我們在主進程上要加入一些子進程管理的機制,比如重新啟動一個工作進程來繼續服務:
主進程代碼:
// master.js // master.js const { fork } = require('child_process'); const cpus = require('os').cpus(); const server = require('net').createServer(); server.listen(1337); const workers = {}; // process.on('uncaughtException', function (err) { // console.log(`Master uncaughtException:\r\n`); // console.log(err); // }); const createWorker = () => { const worker = fork('./worker.js'); // 收到信號後立即重啟新進程 worker.on('message', function (message) { if (message.act === 'suicide') { createWorker(); } }); // 某個進程終止時重新啟動新的進程 worker.on('exit', () => { console.log('Worker ' + worker.pid + ' exited.'); delete workers[worker.pid]; // createWorker(); }); // 句柄轉發 worker.send('server', server); workers[worker.pid] = worker; console.log('Create worker. pid: ' + worker.pid); }; for (let i = 0; i < cpus.length; i++) { createWorker(); } // server.close(); // 進程自己退出時,讓所有工作進程退出 process.on('exit', () => { for (let pid in workers) { workers[pid].kill(); } });
子進程代碼:
// worker.js const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('handled by child, pid is ' + process.pid + '\n'); // 拋出異常,捕獲後終止進程 throw new Error('throw exception'); }); var worker; process.on('message', (m, tcp) => { if (m === 'server') { worker = tcp; worker.on('connection', (socket) => { server.emit('connection', socket); }); } }); // 捕獲異常後終止進程 process.on('uncaughtException', (err) => { // 主動發出信號,避免等待連接斷開時收到新請求而缺少進程無法響應 process.send({ act: 'suicide' }); // 停止接收新的連接 worker.close(function () { // 所有已有連接斷開後,退出進程 process.exit(1); }); // 避免長連接請求長時間無法終止,5s後自動終止 setTimeout(() => { process.exit(1); }, 5000) });
運行父進程 master.js
,控制臺中會打印出開啟的進程 PID
:
在 Linux 中,你可以直接使用 kill -9 [pid]
來終止進程。在 Windows 中,你需要打開任務管理器,找到 node.exe 的進程,終止其中某個。此時命令行會顯示該進程被終止瞭,然後重新開啟一個新的進程。
當然,你也可以使用我們之前寫的 run.js
腳本,每發起一個請求,子進程響應請求之後會拋出一個異常,異常在捕獲之後會終止該進程。
💡 我們之前寫的 run.js 腳本是並行執行的,此時會存在多個請求被分配到同一個 socket ,即分配到同一個進程中執行。那麼就會存在互斥的問題,即某個請求結束後就終止該進程,導致其他請求無法獲得響應而終止。此時你需要將 exec 方法改為同步方法:
const cp = require("child_process"); const cpus = require("os").cpus(); const sleep = (delay) => { const now = Date.now(); while (Date.now() - now < delay); return; } for (let i = 0; i < cpus.length; i++) { const out = cp.execSync(`curl --http0.9 "http://127.0.0.1:1337"`); sleep(1000); console.log(out.toString()); }
該模型一旦有異常出現,主進程會創建新的工作進程來為用戶服務,舊的進程一旦處理完已有連接就自動斷開。整個過程使得我們的應用的穩定性和健壯性大大提高:
總結
至此,我們完成瞭一個簡單的基於父子進程通信、具備異常重啟進程功能的 Web服務器 就已經搭建完成瞭。對於 Nodejs 多進程編程你也有瞭初步的瞭解。接下來我們將介紹 cluster模塊
,並介紹一下在 Nodejs 中進行多線程編程。
以上就是Nodejs搭建多進程Web服務器實現過程的詳細內容,更多關於Nodejs搭建多進程Web服務器的資料請關註WalkonNet其它相關文章!