您好,登錄后才能下訂單哦!
預(yù)備知識(shí)
在如今機(jī)器的CPU都是多核的背景下,Node的單線(xiàn)程設(shè)計(jì)已經(jīng)沒(méi)法更充分的"壓榨"機(jī)器性能了。所以從v0.8開(kāi)始,Node新增了一個(gè)內(nèi)置模塊——“cluster”,故名思議,它可以通過(guò)一個(gè)父進(jìn)程管理一坨子進(jìn)程的方式來(lái)實(shí)現(xiàn)集群的功能。
學(xué)習(xí)cluster之前,需要了解process相關(guān)的知識(shí),如果不了解的話(huà)建議先閱讀process模塊、child_process模塊。
cluster借助child_process模塊的fork()方法來(lái)創(chuàng)建子進(jìn)程,通過(guò)fork方式創(chuàng)建的子進(jìn)程與父進(jìn)程之間建立了IPC通道,支持雙向通信。
cluster模塊最早出現(xiàn)在node.js v0.8版本中
為什么會(huì)存在cluster模塊?
Node.js是單線(xiàn)程的,那么如果希望利用服務(wù)器的多核的資源的話(huà),就應(yīng)該多創(chuàng)建幾個(gè)進(jìn)程,由多個(gè)進(jìn)程共同提供服務(wù)。如果直接采用下列方式啟動(dòng)多個(gè)服務(wù)的話(huà),會(huì)提示端口占用。
const http = require('http'); http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(8000); // 啟動(dòng)第一個(gè)服務(wù) node index.js & // 啟動(dòng)第二個(gè)服務(wù) 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的話(huà)就沒(méi)有問(wèn)題
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í)行完啟動(dòng)了一個(gè)主進(jìn)程和8個(gè)子進(jìn)程(子進(jìn)程數(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是如何實(shí)現(xiàn)多進(jìn)程共享端口的?
cluster創(chuàng)建的進(jìn)程分兩種,父進(jìn)程和子進(jìn)程,父進(jìn)程只有一個(gè),子進(jìn)程有多個(gè)(一般根據(jù)cpu核數(shù)創(chuàng)建)
有三個(gè)問(wèn)題需要回答:
子進(jìn)程為何調(diào)用listen不會(huì)綁定端口?
net.js源碼中的listen方法通過(guò)listenInCluster方法來(lái)區(qū)分是父進(jìn)程還是子進(jìn)程,不同進(jìn)程的差異在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); } }
上面是精簡(jiǎn)過(guò)的代碼,當(dāng)子進(jìn)程調(diào)用listen方法時(shí),會(huì)先執(zhí)行_getServer,然后通過(guò)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方法會(huì)向主進(jìn)程發(fā)送queryServer的message,父進(jìn)程執(zhí)行完會(huì)調(diào)用回調(diào)函數(shù),根據(jù)是否返回handle來(lái)區(qū)分是調(diào)用shared方法還是rr方法,這里其實(shí)是會(huì)調(diào)用rr方法。而rr方法的主要作用就是偽造了TCPWrapper來(lái)調(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); }
由于子進(jìn)程的server拿到的是圍繞的TCPWrapper,當(dāng)調(diào)用listen方法時(shí)并不會(huì)執(zhí)行任何操作,所以在子進(jìn)程中調(diào)用listen方法并不會(huì)綁定端口,因而也并不會(huì)報(bào)錯(cuò)。
父進(jìn)程何時(shí)創(chuàng)建的TCP Server
在子進(jìn)程發(fā)送給父進(jìn)程的queryServer message時(shí),父進(jìn)程會(huì)檢測(cè)是否創(chuàng)建了TCP Server,如果沒(méi)有的話(huà)就會(huì)創(chuàng)建TCP Server并綁定端口,然后再把子進(jìn)程記錄下來(lái),方便后續(xù)的用戶(hù)請(qǐng)求worker分發(fā)。
父進(jìn)程是如何完成分發(fā)的
父進(jìn)程由于綁定了端口號(hào),所以可以捕獲連接請(qǐng)求,父進(jìn)程的onconnection方法會(huì)被觸發(fā),onconnection方法觸發(fā)時(shí)會(huì)傳遞TCP對(duì)象參數(shù),由于之前父進(jìn)程記錄了所有的worker,所以父進(jìn)程可以選擇要處理請(qǐng)求的worker,然后通過(guò)向worker發(fā)送act為newconn的消息,并傳遞TCP對(duì)象,子進(jìn)程監(jiān)聽(tīng)到消息后,對(duì)傳遞過(guò)來(lái)的TCP對(duì)象進(jìn)行封裝,封裝成socket,然后觸發(fā)connection事件。這樣就實(shí)現(xiàn)了子進(jìn)程雖然不監(jiān)聽(tīng)端口,但是依然可以處理用戶(hù)請(qǐng)求的目的。
cluster如何實(shí)現(xiàn)負(fù)載均衡
負(fù)載均衡直接依賴(lài)cluster的請(qǐng)求調(diào)度策略,在v6.0版本之前,cluster的調(diào)用策略采用的是cluster.SCHED_NONE(依賴(lài)于操作系統(tǒng)),SCHED_NODE理論上來(lái)說(shuō)性能最好(Ferando Micalli寫(xiě)過(guò)一篇Node.js 6.0版本的cluster和iptables以及nginx性能對(duì)比的文章)但是從實(shí)際角度發(fā)現(xiàn),在請(qǐng)求調(diào)度方面會(huì)出現(xiàn)不太均勻的情況(可能出現(xiàn)8個(gè)子進(jìn)程中的其中2到3個(gè)處理了70%的連接請(qǐng)求)。因此在6.0版本中Node.js增加了cluster.SCHED_RR(round-robin),目前已成為默認(rèn)的調(diào)度策略(除了windows環(huán)境)
可以通過(guò)設(shè)置NODE_CLUSTER_SCHED_POLICY環(huán)境變量來(lái)修改調(diào)度策略
NODE_CLUSTER_SCHED_POLICY='rr' NODE_CLUSTER_SCHED_POLICY='none'
或者設(shè)置cluster的schedulingPolicy屬性
cluster.schedulingPolicy = cluster.SCHED_NONE; cluster.schedulingPolicy = cluster.SCHED_RR;
Node.js實(shí)現(xiàn)round-robin
Node.js內(nèi)部維護(hù)了兩個(gè)隊(duì)列:
當(dāng)新請(qǐng)求到達(dá)的時(shí)候父進(jìn)程將請(qǐng)求暫存handles隊(duì)列,從free隊(duì)列中出隊(duì)一個(gè)worker,進(jìn)入worker處理(handoff)階段,關(guān)鍵邏輯實(shí)現(xiàn)如下:
RoundRobinHandle.prototype.distribute = function(err, handle) { this.handles.push(handle); const worker = this.free.shift(); if (worker) { this.handoff(worker); } };
worker處理階段首先從handles隊(duì)列出隊(duì)一個(gè)請(qǐng)求,然后通過(guò)進(jìn)程通信的方式通知子worker進(jìn)行請(qǐng)求處理,當(dāng)worker接收到通信消息后發(fā)送ack信息,繼續(xù)響應(yīng)handles隊(duì)列中的請(qǐng)求任務(wù),當(dāng)worker無(wú)法接受請(qǐng)求時(shí),父進(jìn)程負(fù)責(zé)重新調(diào)度worker進(jìn)行處理。關(guān)鍵邏輯如下:
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); }); };
注意:主進(jìn)程與子進(jìn)程之間建立了IPC,因此主進(jìn)程與子進(jìn)程之間可以通信,但是各個(gè)子進(jìn)程之間是相互獨(dú)立的(無(wú)法通信)
參考資料
https://medium.com/@fermads/node-js-process-load-balancing-comparing-cluster-iptables-and-nginx-6746aaf38272
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。