內容概括
-
進程重啓
-
處理HTTP服務
-
cluster模塊
-
負載均衡
1.進程重啓
worker進程可能因爲某些異常情況而退出,爲了提高集羣的穩定性,master進程需要監聽子進程的存活狀態。
當子進程退出之後,master進程要及時重啓新的子進程。在Node中,子進程退出時,會在父進程中觸發exit事件。
父進程只需通過監聽該事件便可知道子進程是否退出,並在退出的時候做出相應的處理。
以下是master進程代碼,文件名爲master.js
const childProcess = require('child_process')
const net = require('net')
const cpuNum = require('os').cpus().length - 1
// 創建工作進程
let workers = []
let cur = 0
for (let i = 0; i < cpuNum; ++i) {
workers.push(childProcess.fork('./worker.js'))
console.log('Create worker-' + workers[i].pid)
}
// 創建TCP服務器
const server = net.createServer()
// 由於master進程也會監聽端口。因此需要對請求做出處理
server.on('connection', (socket) => {
// 利用setTimeout模擬處理請求時的操作耗時
setTimeout(() => {
socket.end('Request handled by master')
}, 10)
})
server.listen(8080, () => {
console.log('TCP server: 127.0.0.1:8080')
// 監聽端口後將服務器句柄發送給工作進程
for (let i = 0; i < cpuNum; ++i) {
workers[i].send('server', server)
// 工作進程退出後重啓
workers[i].on('exit', ((i) => {
return () => {
console.log('Worker-' + workers[i].pid + ' exited')
workers[i] = childProcess.fork('./worker.js')
console.log('Create worker-' + workers[i].pid)
workers[i].send('server', server)
}
})(i))
}
// 關閉主線程服務器的端口監聽
// server.close()
})
以下是worker進程的代碼,文件名爲worker.js
process.on('message', (msg, server) => {
if (msg === 'server' && server) {
server.on('connection', (socket) => {
// 利用setTimeout模擬處理請求時的操作耗時
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid)
}, 10)
})
}
})
執行node master.js啓動服務器後,可以通過任務管理器直接殺掉進程來模擬進程異常退出。
可以看到worker進程退出後,master能夠發現並及時創建新的worker進程。任務管理器中的Node進程數量恢復原樣。
執行node tcp_client.js啓動客戶端,客戶端發出的連接請求被處理的情況如下,同樣地,由於監聽同一端口,進程之間採取搶佔式服務,不一定保障負載均衡。
2.處理HTTP服務
前面的示例所使用的是TCP服務器,如果要處理HTTP請求,需要使用HTTP服務器。而HTTP其實是基於TCP的,發送HTTP請求的時候同樣也會發起TCP連接。
只需要對前面的TCP服務器進行一點小改動便可以支持HTTP了。在進程中新增HTTP服務器,當TCP服務器收到請求時,把請求提交給HTTP服務器處理即可。
以下是worker進程的代碼,文件名爲worker.js
const http = require('http')
const httpServer = http.createServer((req, res) => {
// 利用setTimeout模擬處理請求時的操作耗時
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Request handled by worker-' + process.pid)
}, 10)
})
process.on('message', (msg, server) => {
if (msg === 'server' && server) {
server.on('connection', (socket) => {
// 提交給HTTP服務器處理
httpServer.emit('connection', socket)
})
}
})
3.cluster模塊
前面簡單描述了使用child_process實現單機Node集羣的做法,需要處理挺多的細節。
Node提供了cluster模塊,該模塊提供了更完善的API,除了能夠實現多進程充分利用CPU資源以外,還能夠幫助我們更好地進行進程管理和處理進程。
下面是簡單示例if條件語句判斷當前進程是master還是worker,master進程會執行if語句塊包含的代碼,而worker進程則執行else語句塊包含的代碼。
master進程中,利用cluster模塊創建了與CPU數量相應的worker進程,並通過監聽cluster的online事件來判斷worker的創建成功。
在worker進程退出後,會觸發master進程中cluster模塊上的exit事件,通過監聽該事件可以瞭解worker進程的退出情況並及時fork新的worker。
最後,worker進程中只需創建服務器監聽端口,對客戶端請求做出處理即可。(這裏設置相同端口8080之後,所有worker都將監聽同一個端口)
以下是master進程代碼,文件名爲master.js
const cluster = require('cluster')
if (cluster.isMaster) {
const cpuNum = require('os').cpus().length
for (let i = 0; i < cpuNum; ++i) {
cluster.fork()
}
// 創建進程完成後輸出提示信息
cluster.on('online', (worker) => {
console.log('Create worker-' + worker.process.pid)
})
// 子進程退出後重啓
cluster.on('exit', (worker, code, signal) => {
console.log('[Master] worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal)
cluster.fork()
})
} else {
const net = require('net')
net.createServer().on('connection', (socket) => {
// 利用setTimeout模擬處理請求時的操作耗時
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid)
}, 10)
}).listen(8080)
}
執行node server.js啓動服務器,繼續按照之前的做法,利用任務管理器殺死進程,可以看到在進程被殺後master能夠及時啓動新的worker。
繼續運行tcp_client,可以看到服務器能夠正常處理請求。
4.負載均衡
說到多進程,目的肯定是儘可能利用多核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資源。