Node.js 實(shí)例在單線程中運(yùn)行顷窒,這意味著在多核系統(tǒng)(如今大多數(shù)計(jì)算機(jī)都是多核)上,應(yīng)用程序不會(huì)使用所有內(nèi)核兄一。要利用其他可用內(nèi)核,可以啟動(dòng) Node.js 進(jìn)程集群并在它們之間分配負(fù)載识腿。
通過(guò)多個(gè)進(jìn)程來(lái)處理請(qǐng)求可以提高服務(wù)器的吞吐量(請(qǐng)求數(shù)/秒)出革,因?yàn)榭梢酝瑫r(shí)處理多個(gè)服務(wù)。
集群
Node.js 集群模塊支持創(chuàng)建并同時(shí)運(yùn)行多個(gè)子進(jìn)程渡讼,進(jìn)程之間共享相同的端口骂束。每個(gè)生成的子進(jìn)程都擁有自己的事件循環(huán)、內(nèi)存和 V8 實(shí)例成箫。子進(jìn)程使用 IPC(進(jìn)程間通信)與主進(jìn)程進(jìn)行通信展箱。
通過(guò)多個(gè)進(jìn)程來(lái)處理傳入的請(qǐng)求意味著可以同時(shí)處理多個(gè)請(qǐng)求,如果一個(gè)工作進(jìn)程的有長(zhǎng)時(shí)間運(yùn)行/阻塞操作蹬昌,其他工作進(jìn)程可以繼續(xù)處理其他傳入請(qǐng)求混驰,不會(huì)使應(yīng)用程序阻塞。
傳入的連接有兩種方式分布在子進(jìn)程中:
- 主進(jìn)程偵聽(tīng)端口上的連接皂贩,并以循環(huán)方式將它們分配給工作進(jìn)程(這是除 Windows 之外的所有平臺(tái)上的默認(rèn)方法)
- 主進(jìn)程創(chuàng)建一個(gè)偵聽(tīng)套接字并將其發(fā)送給子進(jìn)程栖榨,然后這些工作進(jìn)程將直接接受傳入的連接
先測(cè)試沒(méi)有使用集群的情況:
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.get('/api/:n', function (req, res) {
let n = parseInt(req.params.n)
let count = 0
if (n > 5000000000) n = 5000000000
for (let i = 0; i <= n; i++) {
count += i
}
res.send(`count is ${count}`)
})
app.listen(3000, () => {
console.log(`runing...`)
})
/api/:n
為動(dòng)態(tài)路由,根據(jù)傳入的參數(shù)執(zhí)行 for 循環(huán)明刷,其時(shí)間復(fù)雜度為 O(n)
婴栽,瀏覽器訪問(wèn) http://localhost:3000/api/50)
,將快速執(zhí)行并立即返回響應(yīng)辈末。當(dāng) n 傳入很大的值時(shí)愚争,http://localhost:3000/api/5000000000
,程序需要幾秒才能完成請(qǐng)求本冲。如果同時(shí)再打開(kāi)一個(gè)瀏覽器選項(xiàng)卡并向服務(wù)器發(fā)送另一個(gè) n 為 50 的請(qǐng)求准脂,該請(qǐng)求仍要等待幾秒鐘才能完成劫扒,因?yàn)閱蝹€(gè)線程忙于處理第一個(gè)耗時(shí)的請(qǐng)求檬洞。單個(gè) CPU 內(nèi)核必須先完成第一個(gè)請(qǐng)求,才能處理另一個(gè)請(qǐng)求沟饥。
使用集群
const express = require('express')
const cluster = require('cluster')
const totalCPUs = require('os').cpus().length
if (cluster.isMaster) {
console.log(`CPU 總核數(shù): ${totalCPUs}`)
console.log(`主進(jìn)程 ${process.pid} is running`)
// Fork 與內(nèi)核數(shù)量相同的工作進(jìn)程
for (let i = 0; i < totalCPUs; i++) {
cluster.fork()
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`)
console.log("Let's fork another worker!")
cluster.fork()
})
} else {
const app = express()
console.log(`工作進(jìn)程 ${process.pid} started`)
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.get('/api/:n', function (req, res) {
let n = parseInt(req.params.n)
let count = 0
if (n > 5000000000) n = 5000000000
for (let i = 0; i <= n; i++) {
count += i
}
res.send(`count is ${count}`)
})
app.listen(3001, () => {
console.log(`runing...`)
})
}
工作進(jìn)程由主進(jìn)程創(chuàng)建和管理添怔。當(dāng)程序第一次運(yùn)行時(shí)湾戳,首先檢查是否是主進(jìn)程(isMaster),由 process.env.NODE_UNIQUE_ID
變量決定的广料。如果 process.env.NODE_UNIQUE_ID
是 undefined
砾脑,那么 isMaster
將是 true
。然后調(diào)用 cluster.fork()
生成多個(gè)工作進(jìn)程艾杏。當(dāng)工作進(jìn)程退出時(shí)韧衣,緊接著生成一個(gè)新進(jìn)程以繼續(xù)利用可用的 CPU 內(nèi)核。
工作進(jìn)程之間共享 3000 端口购桑,并且都能夠處理發(fā)送到該端口的請(qǐng)求畅铭。工作進(jìn)程使用 child_process.fork()
方法生成。該方法返回一個(gè) ChildProcess
具有內(nèi)置通信通道的對(duì)象勃蜘,該通道允許消息在子進(jìn)程和父進(jìn)程之間傳遞硕噩。
CPU 總核數(shù): 6
主進(jìn)程 id 40727 is running
工作進(jìn)程 id 40733 started
工作進(jìn)程 id 40729 started
工作進(jìn)程 id 40732 started
工作進(jìn)程 id 40730 started
工作進(jìn)程 id 40731 started
runing
runing
runing
runing
runing
工作進(jìn)程 id 40734 started
runing
測(cè)試集群效果,首先訪問(wèn) http://localhost:3000/api/100000000000000000
缭贡,緊接著在另一個(gè)選項(xiàng)卡訪問(wèn) http://localhost:3000/api/50
炉擅,發(fā)現(xiàn)后一個(gè)請(qǐng)求立即響應(yīng),第一個(gè)仍要等待幾秒完成阳惹。
由于有多個(gè)工作進(jìn)程可以處理請(qǐng)求谍失,服務(wù)器的可用性和吞吐量都得到了提高。但是通過(guò)瀏覽器的訪問(wèn)來(lái)衡量請(qǐng)求處理以及集群的優(yōu)勢(shì)并不是正確可靠的方法穆端,要更好的了解集群的性能需要通過(guò)工具測(cè)量袱贮。
性能指標(biāo)
使用 loadtest
模塊對(duì)以上兩個(gè)程序進(jìn)行負(fù)載測(cè)試,看看每個(gè)程序如何處理大量傳入的請(qǐng)求体啰。
loadtest
可模擬大量的并發(fā)連接攒巍,以便測(cè)量其性能。
安裝 loadtest
yarn add loadtest -D
基本使用:
loadtest [-n requests] [-c concurrency] [-k] URL
參數(shù)說(shuō)明:
-n requests
要發(fā)送的請(qǐng)求數(shù)量
-c concurrency
并發(fā)數(shù)
--rps requestsPerSecond
控制每秒發(fā)送的請(qǐng)求數(shù)荒勇。也可以是小數(shù)柒莉,比如 --rps 0.5
,每?jī)擅氚l(fā)送一個(gè)請(qǐng)求沽翔。
URL
可以是 http兢孝、https、ws仅偎。
打開(kāi)新的終端對(duì)第一個(gè)程序進(jìn)行負(fù)載測(cè)試:
npx loadtest http://localhost:3000/api/5000000 -n 1000 -c 100
結(jié)果:
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO Requests: 726 (73%), requests per second: 145, mean latency: 646.4 ms
INFO
INFO Target URL: http://localhost:3000/api/5000000
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 6.7272657559999995 s
INFO Requests per second: 149
INFO Mean latency: 640.3 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 626 ms
INFO 90% 631 ms
INFO 95% 770 ms
INFO 99% 1010 ms
INFO 100% 1070 ms (longest request)
? Done in 1.21s.
總耗時(shí) 6.7272657559999995 s
平均延時(shí)(完成單個(gè)請(qǐng)求所需的時(shí)間) 640.3 ms
RPS(可以處理的并發(fā)量) 149
n 再加一個(gè)數(shù)量級(jí)測(cè)試:
npx loadtest http://localhost:3000/api/50000000 -n 1000 -c 100
結(jié)果:
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO Requests: 89 (9%), requests per second: 18, mean latency: 2510.7 ms
INFO Requests: 177 (18%), requests per second: 18, mean latency: 5618.9 ms
INFO Requests: 265 (27%), requests per second: 18, mean latency: 5691.5 ms
INFO Requests: 353 (35%), requests per second: 18, mean latency: 5679.5 ms
INFO Requests: 441 (44%), requests per second: 18, mean latency: 5663.3 ms
INFO Requests: 528 (53%), requests per second: 17, mean latency: 5686.3 ms
INFO Requests: 614 (61%), requests per second: 17, mean latency: 5808.5 ms
INFO Requests: 700 (70%), requests per second: 17, mean latency: 5828.4 ms
INFO Requests: 785 (79%), requests per second: 17, mean latency: 5800.2 ms
INFO Requests: 872 (87%), requests per second: 17, mean latency: 5845.6 ms
INFO Requests: 959 (96%), requests per second: 17, mean latency: 5713.4 ms
INFO
INFO Target URL: http://localhost:3000/api/50000000
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 57.321694236000006 s
INFO Requests per second: 17
INFO Mean latency: 5446.2 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 5703 ms
INFO 90% 5842 ms
INFO 95% 5852 ms
INFO 99% 5873 ms
INFO 100% 5879 ms (longest request)
? Done in 57.94s.
總耗時(shí) 57.321694236000006 s跨蟹,平均延時(shí) 5446.2 ms,RPS 17橘沥。
下面同樣的測(cè)試在集群下測(cè)試
n = 500000:
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO
INFO Target URL: http://localhost:3000/api/5000000
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 1.288627536 s
INFO Requests per second: 776
INFO Mean latency: 120.6 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 121 ms
INFO 90% 132 ms
INFO 95% 136 ms
INFO 99% 149 ms
INFO 100% 160 ms (longest request)
總耗時(shí) 1.288627536 s窗轩,平均延時(shí) 120.6 ms,RPS 776
速度快了 5 倍多座咆。
n = 50000000:
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO Requests: 436 (44%), requests per second: 87, mean latency: 1047.2 ms
INFO Requests: 988 (99%), requests per second: 110, mean latency: 902.9 ms
INFO
INFO Target URL: http://localhost:3000/api/50000000
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 10.130369751 s
INFO Requests per second: 99
INFO Mean latency: 966.2 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 904 ms
INFO 90% 1084 ms
INFO 95% 1529 ms
INFO 99% 1882 ms
INFO 100% 1937 ms (longest request)
總耗時(shí) 10.130369751 s
平均延時(shí) 966.2 ms
RPS 99
差了將近 5.7 倍痢艺。
以上測(cè)試的是計(jì)算量很大的 CPU 密集型運(yùn)算仓洼。下面測(cè)試計(jì)算量小運(yùn)行速度快的請(qǐng)求。
測(cè)試單進(jìn)程 n = 50 :
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO
INFO Target URL: http://localhost:3000/api/50
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 0.836801992 s
INFO Requests per second: 1110
INFO Mean latency: 76.8 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 90 ms
INFO 90% 95 ms
INFO 95% 96 ms
INFO 99% 96 ms
INFO 100% 97 ms (longest request)
總耗時(shí) 0.836801992 s堤舒,平均延時(shí) 76.8 ms色建,RPS 1110
n = 5000:
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO
INFO Target URL: http://localhost:3000/api/5000
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 0.877875636 s
INFO Requests per second: 1095
INFO Mean latency: 81.3 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 89 ms
INFO 90% 94 ms
INFO 95% 94 ms
INFO 99% 95 ms
INFO 100% 99 ms (longest request)
總耗時(shí) 0.877875636 s,平均延時(shí) 81.3 ms舌缤,RPS 1095
集群模式下測(cè)試 n = 50:
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO
INFO Target URL: http://localhost:3000/api/50
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 0.9260376199999999 s
INFO Requests per second: 1080
INFO Mean latency: 86 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 90 ms
INFO 90% 96 ms
INFO 95% 97 ms
INFO 99% 99 ms
INFO 100% 99 ms (longest request)
總耗時(shí) 0.9260376199999999 s箕戳,平均延時(shí) 86 ms,RPS 1080
n = 5000
INFO Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
INFO
INFO Target URL: http://localhost:3000/api/5000
INFO Max requests: 1000
INFO Concurrency level: 100
INFO Agent: none
INFO
INFO Completed requests: 1000
INFO Total errors: 0
INFO Total time: 0.872461731 s
INFO Requests per second: 1146
INFO Mean latency: 80.9 ms
INFO
INFO Percentage of the requests served within a certain time
INFO 50% 84 ms
INFO 90% 96 ms
INFO 95% 97 ms
INFO 99% 99 ms
INFO 100% 99 ms (longest request)
總耗時(shí) 0.872461731 s国撵,平均延時(shí) 80.9 ms漂羊,RPS 1146
可以看出在非 CPU 密集型場(chǎng)景下,單進(jìn)程和集群模式相比集群跟單進(jìn)程差不多甚至不如單進(jìn)程卸留,并沒(méi)有對(duì)程序的性能有所提升走越。事實(shí)上,與不使用集群的應(yīng)用相比耻瑟,集群應(yīng)用的性能確實(shí)要差一些旨指。
以上面測(cè)試為例,當(dāng)用一個(gè)相當(dāng)小的值調(diào)用 API 時(shí)代碼中的計(jì)算量很小喳整,不會(huì)占用大量 CPU谆构,而集群模式下創(chuàng)建的每個(gè)進(jìn)程都會(huì)有自己的內(nèi)存和 V8 實(shí)例,造成額外的資源分配框都,所以在非密集型運(yùn)算場(chǎng)景下集群反而不占優(yōu)勢(shì)搬素。所以不建議總是創(chuàng)建子進(jìn)程。
但處理 CPU 密集型任務(wù)時(shí)集群是有很大優(yōu)勢(shì)的魏保。
所以熬尺,在實(shí)際項(xiàng)目中需要評(píng)估項(xiàng)目是否是 CPU 密集型的以確定是否啟用集群模式。
使用 PM2 管理 Node.js 集群
以上程序中使用 Node 的 cluster
模塊創(chuàng)建和管理子進(jìn)程谓罗。根據(jù) cpu 核數(shù)確定創(chuàng)建子進(jìn)程的數(shù)量粱哼。然后監(jiān)聽(tīng)進(jìn)程的狀態(tài),一旦 died 就立馬新創(chuàng)建一個(gè)檩咱。
PM2 是一個(gè)守護(hù)進(jìn)程管理器揭措,內(nèi)置負(fù)載均衡器,自動(dòng)在集群模式下運(yùn)行刻蚯,創(chuàng)建工作進(jìn)程并在工作進(jìn)程 died 時(shí)創(chuàng)建新工作進(jìn)程绊含。可以停止炊汹、刪除和啟動(dòng)進(jìn)程躬充,0 秒停機(jī)重載,還有一些監(jiān)控功能可以幫助監(jiān)控和調(diào)整應(yīng)用程序的性能。
全局安裝
npm i -g pm2
使用第一個(gè)程序就行麻裳,不需要開(kāi)發(fā)創(chuàng)建集群。
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.get('/api/:n', function (req, res) {
let n = parseInt(req.params.n)
let count = 0
if (n > 5000000000) n = 5000000000
for (let i = 0; i <= n; i++) {
count += i
}
res.send(`count is ${count}`)
})
app.listen(3000, () => {
console.log(`runing`)
})
運(yùn)行:
pm2 start server.js -i 0
-i
表示 PM2 在 cluster_mode(而不是 fork_mode)中啟動(dòng)應(yīng)用程序器钟。如果設(shè)置為 0津坑,PM2 將自動(dòng)生成與 CPU 內(nèi)核數(shù)量一樣多的工作線程。
終端中輸入如下表格:
設(shè)置 exec_mode
的值為 cluster
讓 PM2 在每個(gè)實(shí)例之間進(jìn)行負(fù)載平衡
instances
代表工作進(jìn)程的數(shù)量傲霸;0 代表和內(nèi)核相同數(shù)量疆瑰,-1
表示 CPU - 1
;或者指定特定值(別大于 CPU 核數(shù))
使用 pm2 stop server.js
終止程序昙啄。終端輸出所有進(jìn)程的 stopped 狀態(tài)
除了 -i
參數(shù)還有別的穆役,最好是通過(guò)配置文件管理。使用 pm2 ecosystem
生成配置文件梳凛。該文件還可以為不同的應(yīng)用程序設(shè)置特定的配置耿币。這種對(duì)微服務(wù)程序特別有用。
修改配置文件:
module.exports = {
apps: [
{ name: 'app', script: 'server.js', instances: 0, exec_mode: 'cluster' }
]
}
運(yùn)行:
pm2 start ecosystem.config.js
仍然在集群模式下運(yùn)行韧拒。
PM2 還有其他命令:
pm2 start app_name
pm2 restart app_name
pm2 reload app_name
pm2 stop app_name
pm2 delete app_name
# 使用配置文件時(shí)
pm2 [start|restart|reload|stop|delete] ecosystem.config.js
restart
命令立即終止并重新啟動(dòng)進(jìn)程淹接,實(shí)現(xiàn)了 0 秒的停機(jī)時(shí)間,工作進(jìn)程會(huì)一個(gè)接一個(gè)重新啟動(dòng)叛溢。
還可以檢查程序的狀態(tài)塑悼、日志和指標(biāo)。
狀態(tài):
pm2 ls
實(shí)時(shí)日志:
pm2 logs
終端顯示儀表盤:
pm2 monit