Node.js高級編程cluster環(huán)境及源碼調(diào)試詳解
前言
日常工作中,對 Node.js 的使用都比較粗淺,趁未羊之際,來學點稍微高級的,那就先從 cluster 開始吧。
尼古拉斯張三說過,“帶著問題去學習是一個比較好的方法”,所以我們也來試一試。
當初使用 cluster 時,一直好奇它是怎么做到多個子進程監(jiān)聽同一個端口而不沖突的,比如下面這段代碼:
const cluster = require('cluster')
const net = require('net')
const cpus = require('os').cpus()
if (cluster.isPrimary) {
for (let i = 0; i < cpus.length; i++) {
cluster.fork()
}
} else {
net
.createServer(function (socket) {
socket.on('data', function (data) {
socket.write(`Reply from ${process.pid}: ` + data.toString())
})
socket.on('end', function () {
console.log('Close')
})
socket.write('Hello!\n')
})
.listen(9999)
}
該段代碼通過父進程 fork 出了多個子進程,且這些子進程都監(jiān)聽了 9999 這個端口并能正常提供服務(wù),這是如何做到的呢?我們來研究一下。
準備調(diào)試環(huán)境
學習 Node.js 官方提供庫最好的方式當然是調(diào)試一下,所以,我們先來準備一下環(huán)境。注:本文的操作系統(tǒng)為 macOS Big Sur 11.6.6,其他系統(tǒng)請自行準備相應(yīng)環(huán)境。
編譯 Node.js
- 下載 Node.js 源碼
git clone https://github.com/nodejs/node.git
然后在下面這兩個地方加入斷點,方便后面調(diào)試用:
// lib/internal/cluster/primary.js
function queryServer(worker, message) {
debugger;
// Stop processing if worker already disconnecting
if (worker.exitedAfterDisconnect) return;
...
}
// lib/internal/cluster/child.js
send(message, (reply, handle) => {
debugger
if (typeof obj._setServerData === 'function') obj._setServerData(reply.data)
if (handle) {
// Shared listen socket
shared(reply, {handle, indexesKey, index}, cb)
} else {
// Round-robin.
rr(reply, {indexesKey, index}, cb)
}
})
- 進入目錄,執(zhí)行
./configure --debug make -j4
之后會生成 out/Debug/node
準備 IDE 環(huán)境
使用 vscode 調(diào)試,配置好 launch.json 就可以了(其他 IDE 類似,請自行解決):
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug C++",
"type": "cppdbg",
"program": "/Users/youxingzhi/ayou/node/out/Debug/node",
"request": "launch",
"args": ["/Users/youxingzhi/ayou/node/index.js"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "lldb"
},
{
"name": "Debug Node",
"type": "node",
"runtimeExecutable": "/Users/youxingzhi/ayou/node/out/Debug/node",
"request": "launch",
"args": ["--expose-internals", "--nolazy"],
"skipFiles": [],
"program": "${workspaceFolder}/index.js"
}
]
}
其中第一個是用于調(diào)式 C++ 代碼(需要安裝 C/C++ 插件),第二個用于調(diào)式 JS 代碼。接下來就可以開始調(diào)試了,我們暫時用調(diào)式 JS 代碼的那個配置就好了。
Cluster 源碼調(diào)試
準備好調(diào)試代碼(為了調(diào)試而已,這里啟動一個子進程就夠了):
debugger
const cluster = require('cluster')
const net = require('net')
if (cluster.isPrimary) {
debugger
cluster.fork()
} else {
const server = net.createServer(function (socket) {
socket.on('data', function (data) {
socket.write(`Reply from ${process.pid}: ` + data.toString())
})
socket.on('end', function () {
console.log('Close')
})
socket.write('Hello!\n')
})
debugger
server.listen(9999)
}
很明顯,我們的程序可以分父進程和子進程這兩部分來進行分析。
首先進入的是父進程:
執(zhí)行 require('cluster') 時,會進入 lib/cluster.js 這個文件:
const childOrPrimary = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'primary'
module.exports = require(`internal/cluster/${childOrPrimary}`)
會根據(jù)當前 process.env 上是否有 NODE_UNIQUE_ID 來引入不同的模塊,此時是沒有的,所以會引入 internal/cluster/primary.js 這個模塊:
...
const cluster = new EventEmitter();
...
module.exports = cluster
const handles = new SafeMap()
cluster.isWorker = false
cluster.isMaster = true // Deprecated alias. Must be same as isPrimary.
cluster.isPrimary = true
cluster.Worker = Worker
cluster.workers = {}
cluster.settings = {}
cluster.SCHED_NONE = SCHED_NONE // Leave it to the operating system.
cluster.SCHED_RR = SCHED_RR // Primary distributes connections.
...
cluster.schedulingPolicy = schedulingPolicy
cluster.setupPrimary = function (options) {
...
}
// Deprecated alias must be same as setupPrimary
cluster.setupMaster = cluster.setupPrimary
function setupSettingsNT(settings) {
...
}
function createWorkerProcess(id, env) {
...
}
function removeWorker(worker) {
...
}
function removeHandlesForWorker(worker) {
...
}
cluster.fork = function (env) {
...
}
該模塊主要是在 cluster 對象上掛載了一些屬性和方法,并導出,這些后面回過頭再看,我們繼續(xù)往下調(diào)試。往下調(diào)試會進入 if (cluster.isPrimary) 分支,代碼很簡單,僅僅是 fork 出了一個新的子進程而已:
// lib/internal/cluster/primary.js
cluster.fork = function (env) {
cluster.setupPrimary()
const id = ++ids
const workerProcess = createWorkerProcess(id, env)
const worker = new Worker({
id: id,
process: workerProcess,
})
...
worker.process.on('internalMessage', internal(worker, onmessage))
process.nextTick(emitForkNT, worker)
cluster.workers[worker.id] = worker
return worker
}
cluster.setupPrimary():比較簡單,初始化一些參數(shù)啥的。
createWorkerProcess(id, env):
// lib/internal/cluster/primary.js
function createWorkerProcess(id, env) {
const workerEnv = {...process.env, ...env, NODE_UNIQUE_ID: `${id}`}
const execArgv = [...cluster.settings.execArgv]
...
return fork(cluster.settings.exec, cluster.settings.args, {
cwd: cluster.settings.cwd,
env: workerEnv,
serialization: cluster.settings.serialization,
silent: cluster.settings.silent,
windowsHide: cluster.settings.windowsHide,
execArgv: execArgv,
stdio: cluster.settings.stdio,
gid: cluster.settings.gid,
uid: cluster.settings.uid,
})
}
可以看到,該方法主要是通過 fork 啟動了一個子進程來執(zhí)行我們的 index.js,且啟動子進程的時候設(shè)置了環(huán)境變量 NODE_UNIQUE_ID,這樣 index.js 中 require('cluster') 的時候,引入的就是 internal/cluster/child.js 模塊了。
worker.process.on('internalMessage', internal(worker, onmessage)):監(jiān)聽子進程傳遞過來的消息并處理。
接下來就進入了子進程的邏輯:
前面說了,此時引入的是 internal/cluster/child.js 模塊,我們先跳過,繼續(xù)往下,執(zhí)行 server.listen(9999) 時實際上是調(diào)用了 Server 上的方法:
// lib/net.js
Server.prototype.listen = function (...args) {
...
listenInCluster(
this,
null,
options.port | 0,
4,
backlog,
undefined,
options.exclusive
);
}
可以看到,最終是調(diào)用了 listenInCluster:
// lib/net.js
function listenInCluster(
server,
address,
port,
addressType,
backlog,
fd,
exclusive,
flags,
options
) {
exclusive = !!exclusive
if (cluster === undefined) cluster = require('cluster')
if (cluster.isPrimary || exclusive) {
// Will create a new handle
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
server._listen2(address, port, addressType, backlog, fd, flags)
return
}
const serverQuery = {
address: address,
port: port,
addressType: addressType,
fd: fd,
flags,
backlog,
...options,
}
// Get the primary's server handle, and listen on it
cluster._getServer(server, serverQuery, listenOnPrimaryHandle)
function listenOnPrimaryHandle(err, handle) {
err = checkBindError(err, port, handle)
if (err) {
const ex = exceptionWithHostPort(err, 'bind', address, port)
return server.emit('error', ex)
}
// Reuse primary's server handle
server._handle = handle
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
server._listen2(address, port, addressType, backlog, fd, flags)
}
}
由于是在子進程中執(zhí)行,所以最后會調(diào)用 cluster._getServer(server, serverQuery, listenOnPrimaryHandle):
// lib/internal/cluster/child.js
// 這里的 cb 就是上面的 listenOnPrimaryHandle
cluster._getServer = function (obj, options, cb) {
...
send(message, (reply, handle) => {
debugger
if (typeof obj._setServerData === 'function') obj._setServerData(reply.data)
if (handle) {
// Shared listen socket
shared(reply, {handle, indexesKey, index}, cb)
} else {
// Round-robin.
rr(reply, {indexesKey, index}, cb)
}
})
...
}
該函數(shù)最終會向父進程發(fā)送 queryServer 的消息,父進程處理完后會調(diào)用回調(diào)函數(shù),回調(diào)函數(shù)中會調(diào)用 cb 即 listenOnPrimaryHandle??磥恚?code>listen 的邏輯是在父進程中進行的了。
接下來進入父進程:
父進程收到 queryServer 的消息后,最終會調(diào)用 queryServer 這個方法:
// lib/internal/cluster/primary.js
function queryServer(worker, message) {
// Stop processing if worker already disconnecting
if (worker.exitedAfterDisconnect) return
const key =
`${message.address}:${message.port}:${message.addressType}:` +
`${message.fd}:${message.index}`
let handle = handles.get(key)
if (handle === undefined) {
let address = message.address
// Find shortest path for unix sockets because of the ~100 byte limit
if (
message.port < 0 &&
typeof address === 'string' &&
process.platform !== 'win32'
) {
address = path.relative(process.cwd(), address)
if (message.address.length < address.length) address = message.address
}
// UDP is exempt from round-robin connection balancing for what should
// be obvious reasons: it's connectionless. There is nothing to send to
// the workers except raw datagrams and that's pointless.
if (
schedulingPolicy !== SCHED_RR ||
message.addressType === 'udp4' ||
message.addressType === 'udp6'
) {
handle = new SharedHandle(key, address, message)
} else {
handle = new RoundRobinHandle(key, address, message)
}
handles.set(key, handle)
}
...
}
可以看到,這里主要是對 handle 的處理,這里的 handle 指的是調(diào)度策略,分為 SharedHandle 和 RoundRobinHandle,分別對應(yīng)搶占式和輪詢兩種策略(文章最后補充部分有關(guān)于兩者對比的例子)。
Node.js 中默認是 RoundRobinHandle 策略,可通過環(huán)境變量 NODE_CLUSTER_SCHED_POLICY 來修改,取值可以為 none(SharedHandle) 或 rr(RoundRobinHandle)。
SharedHandle
首先,我們來看一下 SharedHandle,由于我們這里是 TCP 協(xié)議,所以最后會通過 net._createServerHandle 創(chuàng)建一個 TCP 對象掛載在 handle 屬性上(注意這里又有一個 handle,別搞混了):
// lib/internal/cluster/shared_handle.js
function SharedHandle(key, address, {port, addressType, fd, flags}) {
this.key = key
this.workers = new SafeMap()
this.handle = null
this.errno = 0
let rval
if (addressType === 'udp4' || addressType === 'udp6')
rval = dgram._createSocketHandle(address, port, addressType, fd, flags)
else rval = net._createServerHandle(address, port, addressType, fd, flags)
if (typeof rval === 'number') this.errno = rval
else this.handle = rval
}
在 createServerHandle 中除了創(chuàng)建 TCP 對象外,還綁定了端口和地址:
// lib/net.js
function createServerHandle(address, port, addressType, fd, flags) {
...
} else {
handle = new TCP(TCPConstants.SERVER);
isTCP = true;
}
if (address || port || isTCP) {
...
err = handle.bind6(address, port, flags);
} else {
err = handle.bind(address, port);
}
}
...
return handle;
}
然后,queryServer 中繼續(xù)執(zhí)行,會調(diào)用 add 方法,最終會將 handle 也就是 TCP 對象傳遞給子進程:
// lib/internal/cluster/primary.js
function queryServer(worker, message) {
...
if (!handle.data) handle.data = message.data
// Set custom server data
handle.add(worker, (errno, reply, handle) => {
const {data} = handles.get(key)
if (errno) handles.delete(key) // Gives other workers a chance to retry.
send(
worker,
{
errno,
key,
ack: message.seq,
data,
...reply,
},
handle // TCP 對象
)
})
...
}
之后進入子進程:
子進程收到父進程對于 queryServer 的回復后,會調(diào)用 shared:
// lib/internal/cluster/child.js
// `obj` is a net#Server or a dgram#Socket object.
cluster._getServer = function (obj, options, cb) {
...
send(message, (reply, handle) => {
if (typeof obj._setServerData === 'function') obj._setServerData(reply.data)
if (handle) {
// Shared listen socket
shared(reply, {handle, indexesKey, index}, cb)
} else {
// Round-robin.
rr(reply, {indexesKey, index}, cb) // cb 是 listenOnPrimaryHandle
}
})
...
}
shared 中最后會調(diào)用 cb 也就是 listenOnPrimaryHandle:
// lib/net.js
function listenOnPrimaryHandle(err, handle) {
err = checkBindError(err, port, handle)
if (err) {
const ex = exceptionWithHostPort(err, 'bind', address, port)
return server.emit('error', ex)
}
// Reuse primary's server handle 這里的 server 是 index.js 中 net.createServer 返回的那個對象
server._handle = handle
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
server._listen2(address, port, addressType, backlog, fd, flags)
}
這里會把 handle 賦值給 server._handle,這里的 server 是 index.js 中 net.createServer 返回的那個對象,并調(diào)用 server._listen2,也就是 setupListenHandle:
// lib/net.js
function setupListenHandle(address, port, addressType, backlog, fd, flags) {
debug('setupListenHandle', address, port, addressType, backlog, fd)
// If there is not yet a handle, we need to create one and bind.
// In the case of a server sent via IPC, we don't need to do this.
if (this._handle) {
debug('setupListenHandle: have a handle already')
} else {
...
}
this[async_id_symbol] = getNewAsyncId(this._handle)
this._handle.onconnection = onconnection
this._handle[owner_symbol] = this
// Use a backlog of 512 entries. We pass 511 to the listen() call because
// the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1);
// which will thus give us a backlog of 512 entries.
const err = this._handle.listen(backlog || 511)
if (err) {
const ex = uvExceptionWithHostPort(err, 'listen', address, port)
this._handle.close()
this._handle = null
defaultTriggerAsyncIdScope(
this[async_id_symbol],
process.nextTick,
emitErrorNT,
this,
ex
)
return
}
}
首先會執(zhí)行 this._handle.onconnection = onconnection,由于客戶端請求過來時會調(diào)用 this._handle(也就是 TCP 對象)上的 onconnection 方法,也就是會執(zhí)行 lib/net.js 中的 onconnection 方法建立連接,之后就可以通信了。為了控制篇幅,該方法就不繼續(xù)往下了。
然后調(diào)用 listen 監(jiān)聽,注意這里參數(shù) backlog 跟之前不同,不是表示端口,而是表示在拒絕連接之前,操作系統(tǒng)可以掛起的最大連接數(shù)量,也就是連接請求的排隊數(shù)量。我們平時遇到的 listen EADDRINUSE: address already in use 錯誤就是因為這行代碼返回了非 0 的錯誤。
如果還有其他子進程,也會同樣走一遍上述的步驟,不同之處是在主進程中 queryServer 時,由于已經(jīng)有 handle 了,不需要再重新創(chuàng)建了:
function queryServer(worker, message) {
debugger;
// Stop processing if worker already disconnecting
if (worker.exitedAfterDisconnect) return;
const key =
`${message.address}:${message.port}:${message.addressType}:` +
`${message.fd}:${message.index}`;
let handle = handles.get(key);
...
}
以上內(nèi)容整理成流程圖如下:

所謂的 SharedHandle,其實是在多個子進程中共享 TCP 對象的句柄,當客戶端請求過來時,多個進程會去競爭該請求的處理權(quán),會導致任務(wù)分配不均的問題,這也是為什么需要 RoundRobinHandle 的原因。接下來繼續(xù)看看這種調(diào)度方式。
RoundRobinHandle
// lib/internal/cluster/round_robin_handle.js
function RoundRobinHandle(
key,
address,
{port, fd, flags, backlog, readableAll, writableAll}
) {
...
this.server = net.createServer(assert.fail)
...
else if (port >= 0) {
this.server.listen({
port,
host: address,
// Currently, net module only supports `ipv6Only` option in `flags`.
ipv6Only: Boolean(flags & constants.UV_TCP_IPV6ONLY),
backlog,
})
}
...
this.server.once('listening', () => {
this.handle = this.server._handle
this.handle.onconnection = (err, handle) => {
this.distribute(err, handle)
}
this.server._handle = null
this.server = null
})
}
如上所示,RoundRobinHandle 會調(diào)用 net.createServer() 創(chuàng)建一個 server,然后調(diào)用 listen 方法,最終會來到 setupListenHandle:
// lib/net.js
function setupListenHandle(address, port, addressType, backlog, fd, flags) {
debug('setupListenHandle', address, port, addressType, backlog, fd)
// If there is not yet a handle, we need to create one and bind.
// In the case of a server sent via IPC, we don't need to do this.
if (this._handle) {
debug('setupListenHandle: have a handle already')
} else {
debug('setupListenHandle: create a handle')
let rval = null
// Try to bind to the unspecified IPv6 address, see if IPv6 is available
if (!address && typeof fd !== 'number') {
rval = createServerHandle(DEFAULT_IPV6_ADDR, port, 6, fd, flags)
if (typeof rval === 'number') {
rval = null
address = DEFAULT_IPV4_ADDR
addressType = 4
} else {
address = DEFAULT_IPV6_ADDR
addressType = 6
}
}
if (rval === null)
rval = createServerHandle(address, port, addressType, fd, flags)
if (typeof rval === 'number') {
const error = uvExceptionWithHostPort(rval, 'listen', address, port)
process.nextTick(emitErrorNT, this, error)
return
}
this._handle = rval
}
this[async_id_symbol] = getNewAsyncId(this._handle)
this._handle.onconnection = onconnection
this._handle[owner_symbol] = this
...
}
且由于此時 this._handle 為空,會調(diào)用 createServerHandle() 生成一個 TCP 對象作為 _handle。之后就跟 SharedHandle 一樣了,最后也會回到子進程:
// lib/internal/cluster/child.js
// `obj` is a net#Server or a dgram#Socket object.
cluster._getServer = function (obj, options, cb) {
...
send(message, (reply, handle) => {
if (typeof obj._setServerData === 'function') obj._setServerData(reply.data)
if (handle) {
// Shared listen socket
shared(reply, {handle, indexesKey, index}, cb)
} else {
// Round-robin.
rr(reply, {indexesKey, index}, cb) // cb 是 listenOnPrimaryHandle
}
})
...
}
不過由于 RoundRobinHandle 不會傳遞 handle 給子進程,所以此時會執(zhí)行 rr:
function rr(message, {indexesKey, index}, cb) {
...
// Faux handle. Mimics a TCPWrap with just enough fidelity to get away
// with it. Fools net.Server into thinking that it's backed by a real
// handle. Use a noop function for ref() and unref() because the control
// channel is going to keep the worker alive anyway.
const handle = {close, listen, ref: noop, unref: noop}
if (message.sockname) {
handle.getsockname = getsockname // TCP handles only.
}
assert(handles.has(key) === false)
handles.set(key, handle)
debugger
cb(0, handle)
}
可以看到,這里構(gòu)造了一個假的 handle,然后執(zhí)行 cb 也就是 listenOnPrimaryHandle。最終跟 SharedHandle 一樣會調(diào)用 setupListenHandle 執(zhí)行 this._handle.onconnection = onconnection。
RoundRobinHandle 邏輯到此就結(jié)束了,好像缺了點什么的樣子。回顧下,我們給每個子進程中的 server 上都掛載了一個假的 handle,但它跟綁定了端口的 TCP 對象沒有任何關(guān)系,如果客戶端請求過來了,是不會執(zhí)行它上面的 onconnection 方法的。之所以要這樣寫,估計是為了保持跟之前 SharedHandle 代碼邏輯的統(tǒng)一。
此時,我們需要回到 RoundRobinHandle,有這樣一段代碼:
// lib/internal/cluster/round_robin_handle.js
this.server.once('listening', () => {
this.handle = this.server._handle
this.handle.onconnection = (err, handle) => {
this.distribute(err, handle)
}
this.server._handle = null
this.server = null
})
在 listen 執(zhí)行完后,會觸發(fā) listening 事件的回調(diào),這里重寫了 handle 上面的 onconnection。
所以,當客戶端請求過來時,會調(diào)用 distribute 在多個子進程中輪詢分發(fā),這里又有一個 handle,這里的 handle 姑且理解為 clientHandle,即客戶端連接的 handle,別搞混了。總之,最后會將這個 clientHandle 發(fā)送給子進程:
// lib/internal/cluster/round_robin_handle.js
RoundRobinHandle.prototype.handoff = function (worker) {
...
const message = { act: 'newconn', key: this.key };
// 這里的 handle 是 clientHandle
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);
});
};
而子進程在 require('cluster') 時,已經(jīng)監(jiān)聽了該事件:
// lib/internal/cluster/child.js
process.on('internalMessage', internal(worker, onmessage))
send({act: 'online'})
function onmessage(message, handle) {
if (message.act === 'newconn') onconnection(message, handle)
else if (message.act === 'disconnect')
ReflectApply(_disconnect, worker, [true])
}
最終也同樣會走到 net.js 中的 function onconnection(err, clientHandle) 方法。這個方法第二個參數(shù)名就叫 clientHandle,這也是為什么前面的 handle 我想叫這個名字的原因。
還是用圖來總結(jié)下:

跟 SharedHandle 不同的是,該調(diào)度策略中 onconnection 最開始是在主進程中觸發(fā)的,然后通過輪詢算法挑選一個子進程,將 clientHandle 傳遞給它。
為什么端口不沖突
cluster 模塊的調(diào)試就到此告一段落了,接下來我們來回答一下一開始的問題,為什么多個進程監(jiān)聽同一個端口沒有報錯?
網(wǎng)上有些文章說是因為設(shè)置了 SO_REUSEADDR,但其實跟這個沒關(guān)系。通過上面的分析知道,不管什么調(diào)度策略,最終都只會在主進程中對 TCP 對象 bind 一次。
我們可以修改一下源代碼來測試一下:
// deps/uv/src/unix/tcp.c 下面的 SO_REUSEADDR 改成 SO_DEBUG if (setsockopt(tcp->io_watcher.fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)))
編譯后執(zhí)行發(fā)現(xiàn),我們?nèi)匀豢梢哉J褂?cluster 模塊。
那這個 SO_REUSEADDR 到底影響的是啥呢?我們繼續(xù)來研究一下。
SO_REUSEADDR
首先,我們我們知道,下面的代碼是會報錯的:
const net = require('net')
const server1 = net.createServer()
const server2 = net.createServer()
server1.listen(9999)
server2.listen(9999)
但是,如果我稍微修改一下,就不會報錯了:
const net = require('net')
const server1 = net.createServer()
const server2 = net.createServer()
server1.listen(9999, '127.0.0.1')
server2.listen(9999, '10.53.48.67')
原因在于 listen 時,如果不指定 address,則相當于綁定了所有地址,當兩個 server 都這樣做時,請求到來就不知道要給誰處理了。
我們可以類比成找對象,port 是對外貌的要求,address 是對城市的要求。現(xiàn)在甲乙都想要一個 port 是 1米7以上 不限城市的對象,那如果有一個 1米7以上 來自 深圳 的對象,就不知道介紹給誰了。而如果兩者都指定了城市就好辦多了。
那如果一個指定了 address,一個沒有呢?就像下面這樣:
const net = require('net')
const server1 = net.createServer()
const server2 = net.createServer()
server1.listen(9999, '127.0.0.1')
server2.listen(9999)
結(jié)果是:設(shè)置了 SO_REUSEADDR 可以正常運行,而修改成 SO_DEBUG 的會報錯。
還是上面的例子,甲對城市沒有限制,乙需要是來自 深圳 的,那當一個對象來自 深圳,我們可以選擇優(yōu)先介紹給乙,非 深圳 的就選擇介紹給甲,這個就是 SO_REUSEADDR 的作用。
補充
SharedHandle 和 RoundRobinHandle 兩種模式的對比
先準備下測試代碼:
// cluster.js
const cluster = require('cluster')
const net = require('net')
if (cluster.isMaster) {
for (let i = 0; i < 4; i++) {
cluster.fork()
}
} else {
const server = net.createServer()
server.on('connection', (socket) => {
console.log(`PID: ${process.pid}!`)
})
server.listen(9997)
}
// client.js
const net = require('net')
for (let i = 0; i < 20; i++) {
net.connect({port: 9997})
}
RoundRobin 先執(zhí)行 node cluster.js,然后執(zhí)行 node client.js,會看到如下輸出,可以看到?jīng)]有任何一個進程的 PID 是緊挨著的。至于為什么沒有一直按照一樣的順序,后面再研究一下。
PID: 42904! PID: 42906! PID: 42905! PID: 42904! PID: 42907! PID: 42905! PID: 42906! PID: 42907! PID: 42904! PID: 42905! PID: 42906! PID: 42907! PID: 42904! PID: 42905! PID: 42906! PID: 42907! PID: 42904! PID: 42905! PID: 42906! PID: 42904!
Shared
先執(zhí)行 NODE_CLUSTER_SCHED_POLICY=none node cluster.js,則 Node.js 會使用 SharedHandle,然后執(zhí)行 node client.js,會看到如下輸出,可以看到同一個 PID 連續(xù)輸出了多次,所以這種策略會導致進程任務(wù)分配不均的現(xiàn)象。就像公司里有些人忙到 996,有些人天天摸魚,這顯然不是老板愿意看到的現(xiàn)象,所以不推薦使用。
PID: 42561! PID: 42562! PID: 42561! PID: 42562! PID: 42564! PID: 42561! PID: 42562! PID: 42563! PID: 42561! PID: 42562! PID: 42563! PID: 42564! PID: 42564! PID: 42564! PID: 42564! PID: 42564! PID: 42563! PID: 42563! PID: 42564! PID: 42563!
以上就是Node.js高級編程cluster環(huán)境及源碼調(diào)試詳解的詳細內(nèi)容,更多關(guān)于Node.js高級編程cluster的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Node.js中的文件系統(tǒng)(file system)模塊詳解
Node.js文件系統(tǒng)模塊提供了豐富的方法,用于讀取、寫入、操作文件和目錄,文件系統(tǒng)模塊是Node.js強大而靈活的一部分,為文件操作提供了方便的API,本文給大家介紹Node.js中的文件系統(tǒng)(file system)模塊,感興趣的朋友一起看看吧2023-11-11
node.js入門教程之querystring模塊的使用方法
querystring模塊主要用來解析查詢字符串,下面這篇文章主要介紹了關(guān)于node.js中querystring模塊使用方法的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-02-02
詳解express使用vue-router的history踩坑
這篇文章主要介紹了express 使用 vue-router 的 history 踩坑,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-06-06
利用n 升級工具升級Node.js版本及在mac環(huán)境下的坑
這篇文章主要介紹了利用n 升級工具升級Node.js的方法,以及通過網(wǎng)友的測試發(fā)現(xiàn)在mac環(huán)境下利用n工具升級不成功導致node.js不可用的解決方法,有需要的朋友可以參考借鑒,下面來一起看看吧。2017-02-02

