Node.js中的cluster模塊深入解讀
預備知識
在如今機器的CPU都是多核的背景下,Node的單線程設計已經(jīng)沒法更充分的"壓榨"機器性能了。所以從v0.8開始,Node新增了一個內(nèi)置模塊——“cluster”,故名思議,它可以通過一個父進程管理一坨子進程的方式來實現(xiàn)集群的功能。
學習cluster之前,需要了解process相關的知識,如果不了解的話建議先閱讀process模塊、child_process模塊。
cluster借助child_process模塊的fork()方法來創(chuàng)建子進程,通過fork方式創(chuàng)建的子進程與父進程之間建立了IPC通道,支持雙向通信。
cluster模塊最早出現(xiàn)在node.js v0.8版本中
為什么會存在cluster模塊?
Node.js是單線程的,那么如果希望利用服務器的多核的資源的話,就應該多創(chuàng)建幾個進程,由多個進程共同提供服務。如果直接采用下列方式啟動多個服務的話,會提示端口占用。
const http = require('http'); http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(8000); // 啟動第一個服務 node index.js & // 啟動第二個服務 node index.js & throw er; // Unhandled 'error' event ^ Error: listen EADDRINUSE :::8000 at Server.setupListenHandle [as _listen2] (net.js:1330:14) at listenInCluster (net.js:1378:12) at Server.listen (net.js:1465:7) at Object.<anonymous> (/Users/xiji/workspace/learn/node-basic/cluster/simple.js:5:4) at Module._compile (internal/modules/cjs/loader.js:702:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:713:10) at Module.load (internal/modules/cjs/loader.js:612:32) at tryModuleLoad (internal/modules/cjs/loader.js:551:12) at Function.Module._load (internal/modules/cjs/loader.js:543:3) at Function.Module.runMain (internal/modules/cjs/loader.js:744:10)
如果改用cluster的話就沒有問題
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers. for (let 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(8000); console.log(`Worker ${process.pid} started`); } // node index.js 執(zhí)行完啟動了一個主進程和8個子進程(子進程數(shù)與cpu核數(shù)相一致) Master 11851 is running Worker 11852 started Worker 11854 started Worker 11853 started Worker 11855 started Worker 11857 started Worker 11858 started Worker 11856 started Worker 11859 started
cluster是如何實現(xiàn)多進程共享端口的?
cluster創(chuàng)建的進程分兩種,父進程和子進程,父進程只有一個,子進程有多個(一般根據(jù)cpu核數(shù)創(chuàng)建)
- 父進程負責監(jiān)聽端口接受請求,然后分發(fā)請求。
- 子進程負責請求的處理。
有三個問題需要回答:
- 子進程為何調(diào)用listen不會進行端口綁定
- 父進程何時創(chuàng)建的TCP Server
- 父進程是如何完成分發(fā)的
子進程為何調(diào)用listen不會綁定端口?
net.js源碼中的listen方法通過listenInCluster方法來區(qū)分是父進程還是子進程,不同進程的差異在listenInCluster方法中體現(xiàn)
function listenInCluster(server, address, port, addressType, backlog, fd, excluseive) { if (cluster.isMaster || exclusive) { server._listen2(address, port, addressType, backlog, fd); return; } const serverQuery = { address: address ......}; cluster._getServer(server, serverQuery, listenOnMasterHandle); function listenOnMasterHandle(err, handle) { server._handle = handle; server._listen2(address, port, addressType, backlog, fd); } }
上面是精簡過的代碼,當子進程調(diào)用listen方法時,會先執(zhí)行_getServer,然后通過callback的形式指定server._handle的值,之后再調(diào)用_listen2方法。
cluster._getServer = function(obj, options, cb) { ... const message = util._extend({ act: 'queryServer', index: indexes[indexesKey], data: null }, options); message.address = address; send(message, (reply, handle) => { if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. }); ... };
_getServer方法會向主進程發(fā)送queryServer的message,父進程執(zhí)行完會調(diào)用回調(diào)函數(shù),根據(jù)是否返回handle來區(qū)分是調(diào)用shared方法還是rr方法,這里其實是會調(diào)用rr方法。而rr方法的主要作用就是偽造了TCPWrapper來調(diào)用net的listenOnMasterHandle回調(diào)函數(shù)
function rr(message, indexesKey, cb) { var key = message.key; function listen(backlog) { return 0; } function close() { if (key === undefined) return; send({ act: 'close', key }); delete handles[key]; delete indexes[indexesKey]; key = undefined; } function getsockname(out) { if (key) util._extend(out, message.sockname); return 0; } const handle = { close, listen, ref: noop, unref: noop }; handles[key] = handle; cb(0, handle); }
由于子進程的server拿到的是圍繞的TCPWrapper,當調(diào)用listen方法時并不會執(zhí)行任何操作,所以在子進程中調(diào)用listen方法并不會綁定端口,因而也并不會報錯。
父進程何時創(chuàng)建的TCP Server
在子進程發(fā)送給父進程的queryServer message時,父進程會檢測是否創(chuàng)建了TCP Server,如果沒有的話就會創(chuàng)建TCP Server并綁定端口,然后再把子進程記錄下來,方便后續(xù)的用戶請求worker分發(fā)。
父進程是如何完成分發(fā)的
父進程由于綁定了端口號,所以可以捕獲連接請求,父進程的onconnection方法會被觸發(fā),onconnection方法觸發(fā)時會傳遞TCP對象參數(shù),由于之前父進程記錄了所有的worker,所以父進程可以選擇要處理請求的worker,然后通過向worker發(fā)送act為newconn的消息,并傳遞TCP對象,子進程監(jiān)聽到消息后,對傳遞過來的TCP對象進行封裝,封裝成socket,然后觸發(fā)connection事件。這樣就實現(xiàn)了子進程雖然不監(jiān)聽端口,但是依然可以處理用戶請求的目的。
cluster如何實現(xiàn)負載均衡
負載均衡直接依賴cluster的請求調(diào)度策略,在v6.0版本之前,cluster的調(diào)用策略采用的是cluster.SCHED_NONE(依賴于操作系統(tǒng)),SCHED_NODE理論上來說性能最好(Ferando Micalli寫過一篇Node.js 6.0版本的cluster和iptables以及nginx性能對比的文章)但是從實際角度發(fā)現(xiàn),在請求調(diào)度方面會出現(xiàn)不太均勻的情況(可能出現(xiàn)8個子進程中的其中2到3個處理了70%的連接請求)。因此在6.0版本中Node.js增加了cluster.SCHED_RR(round-robin),目前已成為默認的調(diào)度策略(除了windows環(huán)境)
可以通過設置NODE_CLUSTER_SCHED_POLICY環(huán)境變量來修改調(diào)度策略
NODE_CLUSTER_SCHED_POLICY='rr' NODE_CLUSTER_SCHED_POLICY='none'
或者設置cluster的schedulingPolicy屬性
cluster.schedulingPolicy = cluster.SCHED_NONE; cluster.schedulingPolicy = cluster.SCHED_RR;
Node.js實現(xiàn)round-robin
Node.js內(nèi)部維護了兩個隊列:
- free隊列記錄當前可用的worker
- handles隊列記錄需要處理的TCP請求
當新請求到達的時候父進程將請求暫存handles隊列,從free隊列中出隊一個worker,進入worker處理(handoff)階段,關鍵邏輯實現(xiàn)如下:
RoundRobinHandle.prototype.distribute = function(err, handle) { this.handles.push(handle); const worker = this.free.shift(); if (worker) { this.handoff(worker); } };
worker處理階段首先從handles隊列出隊一個請求,然后通過進程通信的方式通知子worker進行請求處理,當worker接收到通信消息后發(fā)送ack信息,繼續(xù)響應handles隊列中的請求任務,當worker無法接受請求時,父進程負責重新調(diào)度worker進行處理。關鍵邏輯如下:
RoundRobinHandle.prototype.handoff = function(worker) { const handle = this.handles.shift(); if (handle === undefined) { this.free.push(worker); // Add to ready queue again. return; } const message = { act: 'newconn', key: this.key }; sendHelper(worker.process, message, handle, (reply) => { if (reply.accepted) handle.close(); else this.distribute(0, handle); // Worker is shutting down. Send to another. this.handoff(worker); }); };
注意:主進程與子進程之間建立了IPC,因此主進程與子進程之間可以通信,但是各個子進程之間是相互獨立的(無法通信)
參考資料
https://medium.com/@fermads/node-js-process-load-balancing-comparing-cluster-iptables-and-nginx-6746aaf38272
總結
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關文章
node.js根據(jù)不同請求路徑返回不同數(shù)據(jù)詳解流程
本篇文章介紹了我在開發(fā)過程中發(fā)現(xiàn)的一個小問題,就是node.js如何能夠根據(jù)不同的請求路徑來返回得到不同數(shù)據(jù),通讀本篇對大家的學習或工作具有一定的價值,需要的朋友可以參考下2021-10-10淺談在koa2中實現(xiàn)頁面渲染的全局數(shù)據(jù)
本篇文章主要介紹了淺談在koa2中實現(xiàn)頁面渲染的全局數(shù)據(jù),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10Windows中安裝nvm進行Node版本控制與詳細使用教程
nvm和npm都是node.js版本管理工具,但是為了解決node各種不同之間版本存在不兼容的問題,因此可以通過nvm安裝和切換不同版本的node,感興趣的可以了解一下2023-09-09