node开启子线程_多进程 amp; Node.js web 实现
更好閱讀體驗(yàn):
多進(jìn)程 & Node.js 實(shí)現(xiàn) · 語雀?www.yuque.com進(jìn)程與線程
進(jìn)程和線程的誕生要從多任務(wù)談起,多任務(wù)是指操作系統(tǒng)可以在同一時(shí)間內(nèi)運(yùn)行多個(gè)應(yīng)用程序,CPU 按順序執(zhí)行代碼,在同一時(shí)間內(nèi)只能處理一個(gè)任務(wù),而在單核時(shí)代主流操作系統(tǒng)都有了多任務(wù)能力,主要靠快速在多個(gè)任務(wù)之間切換,讓人感覺多個(gè)任務(wù)同時(shí)執(zhí)行
進(jìn)程是指操作系統(tǒng)正在運(yùn)行的應(yīng)用程序,而一個(gè)進(jìn)程內(nèi)部可能有多個(gè)并發(fā)的子任務(wù),這就是線程
Web 服務(wù)器模型
Web 服務(wù)器需要同時(shí)處理多個(gè)用戶的請求,返回給用戶響應(yīng)內(nèi)容,有幾種不同的服務(wù)器模型實(shí)現(xiàn)多任務(wù)
多進(jìn)程單線程
這種服務(wù)模型通過進(jìn)程復(fù)制實(shí)現(xiàn)同時(shí)響應(yīng)多個(gè)請求,每個(gè)請求使用一個(gè)單獨(dú)的進(jìn)程處理,但操作系統(tǒng)復(fù)制進(jìn)程需要復(fù)制進(jìn)程內(nèi)部狀態(tài),這樣相同的狀態(tài)在內(nèi)存中存在多份,對內(nèi)存有一定的開銷,可以同時(shí)處理的請求數(shù)和內(nèi)存大小正相關(guān)
單進(jìn)程多線程
為了避免復(fù)制多進(jìn)程帶來的內(nèi)存浪費(fèi)問題,多線程被引入 Web 服務(wù)器模型,一個(gè)線程響應(yīng)一個(gè)用戶請求,線程可以共享進(jìn)程的內(nèi)存,不會造成內(nèi)存浪費(fèi),同時(shí)線程相對于進(jìn)程的內(nèi)存開銷要小得多。但每個(gè)線程有自己的獨(dú)立堆棧,需要占據(jù)一定的內(nèi)存空間,因此只是緩解了多進(jìn)程帶來的資源浪費(fèi)問題
另外操作系統(tǒng)在切換線程的同時(shí)需要切換線程的 context,當(dāng)線程數(shù)量過多時(shí) CPU 會被耗在 context 切換中。同時(shí)一個(gè)線程的崩潰可能會導(dǎo)致整個(gè)進(jìn)程 crash,為服務(wù)器帶來了相當(dāng)程度的穩(wěn)定性風(fēng)險(xiǎn)
多進(jìn)程多線程
顧名思義多進(jìn)程多線程模型就是啟用多個(gè)進(jìn)程,在每個(gè)進(jìn)程內(nèi)啟用多個(gè)線程來解決高并發(fā)問題,集成了多進(jìn)程和多線程模型的好處,但當(dāng)用戶量足夠大的時(shí)候也同時(shí)擁有了另外兩種模型的缺陷
當(dāng)并發(fā)數(shù)達(dá)到千萬級內(nèi)存好用問題就會暴露出來,這就是著名的 C10k 問題,C10k 問題的本質(zhì)在于:為了處理高并發(fā)創(chuàng)建的進(jìn)程線程太多,數(shù)據(jù)拷貝頻繁、進(jìn)程/線程上下文切換消耗大, 導(dǎo)致操作系統(tǒng)崩潰
事件驅(qū)動
為了解決 Web 高并發(fā)問題 Nginx 使用了事件驅(qū)動的模型,在一個(gè) CPU 上使用單進(jìn)程、單線程來響應(yīng)用戶請求,把最耗時(shí)的阻塞任務(wù) I/O 任務(wù)異步化,處理完成后通過事件通知主進(jìn)程給用戶響應(yīng),在等待 I/O 任務(wù)的時(shí)候處理下一個(gè)請求
這樣的模型性能取決于 CPU 的運(yùn)算能力,但不受多進(jìn)程、多線程模式中資源上限的影響,非常適合 Web I/O 密集的特征,成了現(xiàn)在 Web 服務(wù)器的主流模型
master-worker 模式
Node.js 本身就使用的事件驅(qū)動模型,為了解決單進(jìn)程單線程對多核使用不足問題,可以按照 CPU 數(shù)目多進(jìn)程啟動,理想情況下一個(gè)每個(gè)進(jìn)程利用一個(gè) CPU
Node.js 提供了 child_process 模塊支持多進(jìn)程,通過 child_process.fork(modulePath) 方法可以調(diào)用指定模塊,衍生新的 Node.js 進(jìn)程 worker.js
const http = require('http'); const randomPort = parseInt(Math.random() * 10000); http.createServer((req, res) => {res.end('Hello world') }).listen(randomPort);master.js
const { fork } = require('child_process'); const os = require('os');for (let i = 0, len = os.cpus().length; i < len; i++) {fork('./worker.js'); }使用 node master.js 啟動,會復(fù)制 CPU 數(shù)量的進(jìn)程數(shù)執(zhí)行 worker.js,使用 ps aux | grep worker.js 可以看到對應(yīng)的進(jìn)程
undefined 5271 4931720 21584 0:00.13 /usr/local/bin/node ./worker.js undefined 5270 4931720 21624 0:00.13 /usr/local/bin/node ./worker.js undefined 5269 4931720 21640 0:00.13 /usr/local/bin/node ./worker.js undefined 5268 4931720 21636 0:00.12 /usr/local/bin/node ./worker.js undefined 5267 4931720 21616 0:00.13 /usr/local/bin/node ./worker.js undefined 5266 4931720 21696 0:00.12 /usr/local/bin/node ./worker.js undefined 5265 4931720 21648 0:00.13 /usr/local/bin/node ./worker.js undefined 5264 4931720 21640 0:00.12 /usr/local/bin/node ./worker.js這就是 Master-Worker 模式,主進(jìn)程負(fù)責(zé)調(diào)度和管理工作進(jìn)程,工作進(jìn)程負(fù)責(zé)具體業(yè)務(wù)邏輯處理
進(jìn)程通信
主進(jìn)程管理工作進(jìn)程,經(jīng)常需要和工作進(jìn)程通信,通過 child_process 復(fù)制的進(jìn)程和主進(jìn)程通信可以使用 WebWorker API worker.js
const http = require('http'); const randomPort = parseInt(Math.random() * 10000);http.createServer((req, res) => {res.end('Hello world') }).listen(randomPort);process.on('message', msg => {console.log(`worker get message: ${msg}`); });process.send(`${randomPort} ready`);master.js
const { fork } = require('child_process'); const os = require('os');for (let i = 0, len = os.cpus().length; i < len; i++) {const worker = fork('./worker.js');worker.on('message', msg => {console.log(`master get message: ${msg}`);});worker.send('ok'); }句柄傳遞
在上面的例子中每個(gè)工作進(jìn)程都使用了一個(gè)隨機(jī)端口,如果設(shè)置成一樣的會出現(xiàn)端口號被占用的錯(cuò)誤
Error: listen EADDRINUSE :::9527at Server.setupListenHandle [as _listen2] (net.js:1360:14)at listenInCluster (net.js:1401:12)at Server.listen (net.js:1485:7)這個(gè)問題可以通過 master 監(jiān)聽 80 端口,分發(fā)請求給工作進(jìn)程,工作進(jìn)程使用不同的端口號解決,所以上面例子使用了隨機(jī)端口號
但進(jìn)程每接收一個(gè)連接會使用一個(gè)文件描述符,上面的模型因?yàn)槭褂昧舜矸?wù),每次連接需要消耗兩個(gè)文件描述符,而操作系統(tǒng)的文件描述符是有限的,代理方案浪費(fèi)了一倍的文件描述符影響了系統(tǒng)吞吐量
為了解決這個(gè)問題 master 可以把句柄(標(biāo)識資源的引用,內(nèi)部包含了指向?qū)ο蟮奈募枋龇?#xff09;發(fā)送給工作進(jìn)程
send(message, handler);也就是說 master 進(jìn)程接收到請求后把 socket 直接發(fā)送給 worker,不用為了和 worker 連接重新創(chuàng)建一個(gè) socket master.js
const { fork } = require('child_process'); const net = require('net'); const os = require('os');const workers = []; for (let i = 0, len = os.cpus().length; i < len; i++) {const worker = fork('./worker.js');workers.push(worker); }const server = net.createServer(); server.listen(9527, () => {workers.forEach(worker => {worker.send('SERVER', server);});server.close(); });在主進(jìn)程中創(chuàng)建一個(gè) tcp server,監(jiān)聽 9527 端口后把 tcp server 發(fā)送給所有 worker,然后關(guān)閉 tcp server,所有監(jiān)聽交給 worker 處理 worker.js
const http = require('http');// 創(chuàng)建 http 服務(wù)器,不監(jiān)聽任何端口號 const httpServer = http.createServer((req, res) => {res.end(`Hello world by ${process.pid}n`); });process.on('message', (msg, tcpServer) => {// 如果是 master 傳遞來的 tcp serverif (msg === 'SERVER') {// 新連接建立的時(shí)候觸發(fā)tcpServer.on('connection', socket => {// 把 tcp server 的連接轉(zhuǎn)給 http server 處理httpServer.emit('connection', socket);});} });這樣寫之后為什么多個(gè)進(jìn)程可以監(jiān)聽同樣的端口號,不報(bào) EADDRINUSE 錯(cuò)誤了呢?
Node.js 對每個(gè)端口監(jiān)聽的時(shí)候設(shè)置了 SO_REUSEADDR 選項(xiàng),允許不同的進(jìn)程對相同的端口號監(jiān)聽
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));獨(dú)立啟動的進(jìn)程服務(wù)器 socket 的文件描述符(listenfd)不同,所以監(jiān)聽相同的端口號會失敗,而上面代碼 socket 都使用 master 發(fā)送的 socket,所以可以監(jiān)聽成功
多個(gè)應(yīng)用監(jiān)聽相同的端口號時(shí)文件描述符同一時(shí)間只能被一個(gè)進(jìn)程占用,也就是說網(wǎng)絡(luò)請求向服務(wù)器發(fā)送的時(shí)候只有一個(gè)進(jìn)程可以搶占到對請求提供服務(wù)
穩(wěn)定性
利用 master 和 worker 的通信機(jī)制可以讓 master 對 worker 進(jìn)行管理
worker 自動重啟
master.js
const { fork } = require('child_process'); const net = require('net'); const os = require('os');const workers = {};function createWorker(server) {const worker = fork('./worker.js');worker.send('SERVER', server);workers[worker.pid] = worker;console.log(`worker ${worker.pid} created`);worker.on('exit', () => {// worker 進(jìn)程退出,自動重新創(chuàng)建console.log(`worker ${worker.pid} exited`);delete workers[worker.pid];createWorker(server);}); }const server = net.createServer(); server.listen(9527);for (let i = 0, len = os.cpus().length; i < len; i++) {createWorker(server); }master 關(guān)閉自動關(guān)閉 worker
master.js
process.on('exit', () => {for (const pid in workers) {workers[pid].kill();} });cluster 模塊
上面講的內(nèi)容可以通過 Node.js 的內(nèi)置模塊 cluster 實(shí)現(xiàn)
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length;if (cluster.isMaster) {console.log(`主進(jìn)程 ${process.pid} 正在運(yùn)行`);// 衍生工作進(jìn)程for (let i = 0; i < numCPUs; i++) {cluster.fork();}cluster.on('exit', (worker, code, signal) => {console.log(`工作進(jìn)程 ${worker.process.pid} 已退出`);}); } else {// 工作進(jìn)程可以共享任何 TCP 連接,在本例子中,共享的是 HTTP 服務(wù)器。http.createServer((req, res) => {res.writeHead(200);res.end('你好世界n');}).listen(8000);console.log(`工作進(jìn)程 ${process.pid} 已啟動`); }cluster 事件
cluster 模塊也暴露了一些事件給開發(fā)者更多的定制性
總結(jié)
以上是生活随笔為你收集整理的node开启子线程_多进程 amp; Node.js web 实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: arcgis 分区 属性值_如何使用Ar
- 下一篇: 阮一峰es6电子书_ES6理解进阶【大前