1 cluster多進程
cluster經過好幾代的發展,現在已經比較好使了。利用cluster,可以自動完成子進程worker分配request的事情,就不再需要自己寫代碼在master進程中robin式給每個worker分配任務了。
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { // Fork workers. for (var i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { // Workers can share any TCP connection // In this case it is an HTTP server http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(80); }
上述簡單的代碼,就實現了根據CPU個數,創建多個worker。至於實際項目中,是正好一個核對應一個worker呢,還是一個核對應2、3個worker呢,就視情況而定了。如果項目中,等待其他服務器(例如數據庫)響應特別長時間,設置2個以上worker應該會更好。
不過一般而言,一個CPU對一個worker就挺好的了。
那么,整個架構就類似這樣:
Master進程,需要做的就是監控worker的生命周期,如果發現worker掛掉了,就重啟worker,並做好相應的log。
整個架構沒有太大的難點,重點就是做好一些細節處理,例如重啟、日志、5秒心跳包等。
多進程的架構,相對原始的單進程+pm2重啟好處肯定多很多,整個node服務會更穩定,不會突然徹底掛了。
另外,對比pm2多進程,也有優勢,主要是master的邏輯掌握在開發自己手中,可以做好自定義的log和郵件、短信告警。
為了整個nodejs服務管理方便,在master進程中,我們一般開啟管理端口的監聽,例如12701,通過命令行curl 127.0.0.1:12701:xxx發起一個簡單的http get請求,輕松管理。
例如xxx傳入reload,可以作為服務器重啟的指令。
2 負載均衡
說到多進程,目的肯定是盡可能利用多核CPU,提高單機的負載能力。
但往往在實際項目中,受到業務邏輯的處理時間長短和系統CPU調度影響,導致實際上所有進程的負載並不是理想的徹底均衡。
官方也說了:
In practice however, distribution tends to be very unbalanced due to operating system scheduler vagaries. Loads have been observed where over 70% of all connections ended up in just two processes, out of a total of eight.
翻譯一下:70%的請求最終都落到2個worker身上,而這2個worker占用更多的CPU資源。
那么在實際項目部署,我們可以嘗試更進一步的措施:綁定CPU。4核CPU,我們fork出4個worker,每個worker分別綁定到#1-#4 CPU。
node並沒有給我們提供現成的接口,不過我們可以使用linux的命令:taskset
在node中,我們可以使用child_process執行shell。
cp.exec('taskset -cp ' + (cpu) + ' ' + process.pid,{ timeout: 5000 },function(err,data,errData){ if(err){ logger.error(err.stack); } if(data.length){ logger.info('\n' + data.toString('UTF-8')); } if(errData.length){ logger.error('\n' + errData.toString('UTF-8')); } });
按實際情況來看,效果是不錯的。
3 平滑重啟
每次發布新版本,服務器必然需要重啟。
簡單粗暴的,殺掉主進程,全部重啟,必然會有一段時間的服務中斷。
對於小企業還好,可以安排在凌晨重啟,但對於大公司大產品來說,就不能這么粗暴了。
那么我們需要平滑重啟,實現重啟過程中,服務不中斷。
策略並不復雜,但非常有效:
1、worker進程輪流重啟,間隔時間;
2、worker進程並不是直接重啟,而是先關閉新請求監聽,等當前請求都返回了,再重啟。
try { // make sure we close down within 30 seconds var killtimer = setTimeout(() => { process.exit(1); }, 30000); // stop taking new requests. server.close(); // Let the master know we're dead. This will trigger a // 'disconnect' in the cluster master, and then it will fork // a new worker. cluster.worker.disconnect(); } catch (er2) { }
實施了平滑重啟后,服務器的吞吐率會平滑很多。