一文聊聊Node.js中的cluster(集群)

一文聊聊Node.js中的cluster(集群)

日常工作中,对 Node.js 的使用都比较粗浅,趁未羊之际,来学点稍微高级的,那就先从 cluster 开始吧。

尼古拉斯张三说过,“带着问题去学习是一个比较好的方法”,所以我们也来试一试。

当初使用 cluster 时,一直好奇它是怎么做到多个子进程监听同一个端口而不冲突的,比如下面这段代码:

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 出了多个子进程,且这些子进程都监听了 9999 这个端口并能正常提供服务,这是如何做到的呢?我们来研究一下。【相关教程推荐:nodejs视频教程、编程教学】

准备调试环境

学习 Node.js 官方提供库最好的方式当然是调试一下,所以,我们先来准备一下环境。注:本文的操作系统为 macOS Big Sur 11.6.6,其他系统请自行准备相应环境。

编译 Node.js

下载 Node.js 源码

git clone https://github.com/nodejs/node.git

然后在下面这两个地方加入断点,方便后面调试用:

// lib/internal/cluster/primary.jsfunction queryServer(worker, message) {  debugger;  // Stop processing if worker already disconnecting  if (worker.exitedAfterDisconnect) return;  ...}
// lib/internal/cluster/child.jssend(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)  }})

进入目录,执行

./configure --debugmake -j4

之后会生成 out/Debug/node

准备 IDE 环境

使用 vscode 调试,配置好 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"    }  ]}

其中第一个是用于调式 C++ 代码(需要安装 C/C++ 插件),第二个用于调式 JS 代码。接下来就可以开始调试了,我们暂时用调式 JS 代码的那个配置就好了。

Cluster 源码调试

准备好调试代码(为了调试而已,这里启动一个子进程就够了):

debuggerconst 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)}

很明显,我们的程序可以分父进程和子进程这两部分来进行分析。

首先进入的是父进程:

执行 require('cluster') 时,会进入 lib/cluster.js 这个文件:

const childOrPrimary = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'primary'module.exports = require(`internal/cluster/${childOrPrimary}`)

会根据当前 process.env 上是否有 NODE_UNIQUE_ID 来引入不同的模块,此时是没有的,所以会引入 internal/cluster/primary.js 这个模块:

...const cluster = new EventEmitter();...module.exports = clusterconst handles = new SafeMap()cluster.isWorker = falsecluster.isMaster = true // Deprecated alias. Must be same as isPrimary.cluster.isPrimary = truecluster.Worker = Workercluster.workers = {}cluster.settings = {}cluster.SCHED_NONE = SCHED_NONE // Leave it to the operating system.cluster.SCHED_RR = SCHED_RR // Primary distributes connections....cluster.schedulingPolicy = schedulingPolicycluster.setupPrimary = function (options) {...}// Deprecated alias must be same as setupPrimarycluster.setupMaster = cluster.setupPrimaryfunction setupSettingsNT(settings) {...}function createWorkerProcess(id, env) {  ...}function removeWorker(worker) { ...}function removeHandlesForWorker(worker) { ...}cluster.fork = function (env) {  ...}

该模块主要是在 cluster 对象上挂载了一些属性和方法,并导出,这些后面回过头再看,我们继续往下调试。往下调试会进入 if (cluster.isPrimary) 分支,代码很简单,仅仅是 fork 出了一个新的子进程而已:

// lib/internal/cluster/primary.jscluster.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():比较简单,初始化一些参数啥的。

createWorkerProcess(id, env)

// lib/internal/cluster/primary.jsfunction 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 启动了一个子进程来执行我们的 index.js,且启动子进程的时候设置了环境变量 NODE_UNIQUE_ID,这样 index.jsrequire('cluster') 的时候,引入的就是 internal/cluster/child.js 模块了。

worker.process.on('internalMessage', internal(worker, onmessage)):监听子进程传递过来的消息并处理。

接下来就进入了子进程的逻辑:

前面说了,此时引入的是 internal/cluster/child.js 模块,我们先跳过,继续往下,执行 server.listen(9999) 时实际上是调用了 Server 上的方法:

// lib/net.jsServer.prototype.listen = function (...args) {  ...      listenInCluster(        this,        null,        options.port | 0,        4,        backlog,        undefined,        options.exclusive      );}

可以看到,最终是调用了 listenInCluster

// lib/net.jsfunction 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)  }}

由于是在子进程中执行,所以最后会调用 cluster._getServer(server, serverQuery, listenOnPrimaryHandle)

// lib/internal/cluster/child.js// 这里的 cb 就是上面的 listenOnPrimaryHandlecluster._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)    }  })  ...}

该函数最终会向父进程发送 queryServer 的消息,父进程处理完后会调用回调函数,回调函数中会调用 cblistenOnPrimaryHandle。看来,listen 的逻辑是在父进程中进行的了。

接下来进入父进程:

父进程收到 queryServer 的消息后,最终会调用 queryServer 这个方法:

// lib/internal/cluster/primary.jsfunction 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 指的是调度策略,分为 SharedHandleRoundRobinHandle,分别对应抢占式和轮询两种策略(文章最后补充部分有关于两者对比的例子)。

Node.js 中默认是 RoundRobinHandle 策略,可通过环境变量 NODE_CLUSTER_SCHED_POLICY 来修改,取值可以为 noneSharedHandle) 或 rrRoundRobinHandle)。

SharedHandle

首先,我们来看一下 SharedHandle,由于我们这里是 TCP 协议,所以最后会通过 net._createServerHandle 创建一个 TCP 对象挂载在 handle 属性上(注意这里又有一个 handle,别搞混了):

// lib/internal/cluster/shared_handle.jsfunction 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 中除了创建 TCP 对象外,还绑定了端口和地址:

// lib/net.jsfunction 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 中继续执行,会调用 add 方法,最终会将 handle 也就是 TCP 对象传递给子进程:

// lib/internal/cluster/primary.jsfunction 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 的回复后,会调用 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 中最后会调用 cb 也就是 listenOnPrimaryHandle

// lib/net.jsfunction 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,这里的 serverindex.jsnet.createServer 返回的那个对象,并调用 server._listen2,也就是 setupListenHandle

文心一言 文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

文心一言 1008 查看详情 文心一言

// lib/net.jsfunction 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  }}

首先会执行 this._handle.onconnection = onconnection,由于客户端请求过来时会调用 this._handle(也就是 TCP 对象)上的 onconnection 方法,也就是会执行lib/net.js 中的 onconnection 方法建立连接,之后就可以通信了。为了控制篇幅,该方法就不继续往下了。

然后调用 listen 监听,注意这里参数 backlog 跟之前不同,不是表示端口,而是表示在拒绝连接之前,操作系统可以挂起的最大连接数量,也就是连接请求的排队数量。我们平时遇到的 listen EADDRINUSE: address already in use 错误就是因为这行代码返回了非 0 的错误。

如果还有其他子进程,也会同样走一遍上述的步骤,不同之处是在主进程中 queryServer 时,由于已经有 handle 了,不需要再重新创建了:

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);  ...}

以上内容整理成流程图如下:

sharedhandle.png

所谓的 SharedHandle,其实是在多个子进程中共享 TCP 对象的句柄,当客户端请求过来时,多个进程会去竞争该请求的处理权,会导致任务分配不均的问题,这也是为什么需要 RoundRobinHandle 的原因。接下来继续看看这种调度方式。

RoundRobinHandle

// lib/internal/cluster/round_robin_handle.jsfunction 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 会调用 net.createServer() 创建一个 server,然后调用 listen 方法,最终会来到 setupListenHandle

// lib/net.jsfunction 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 为空,会调用 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 给子进程,所以此时会执行 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)}

可以看到,这里构造了一个假的 handle,然后执行 cb 也就是 listenOnPrimaryHandle。最终跟 SharedHandle 一样会调用 setupListenHandle 执行 this._handle.onconnection = onconnection

RoundRobinHandle 逻辑到此就结束了,好像缺了点什么的样子。回顾下,我们给每个子进程中的 server 上都挂载了一个假的 handle,但它跟绑定了端口的 TCP 对象没有任何关系,如果客户端请求过来了,是不会执行它上面的 onconnection 方法的。之所以要这样写,估计是为了保持跟之前 SharedHandle 代码逻辑的统一。

此时,我们需要回到 RoundRobinHandle,有这样一段代码:

// lib/internal/cluster/round_robin_handle.jsthis.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 执行完后,会触发 listening 事件的回调,这里重写了 handle 上面的 onconnection

所以,当客户端请求过来时,会调用 distribute 在多个子进程中轮询分发,这里又有一个 handle,这里的 handle 姑且理解为 clientHandle,即客户端连接的 handle,别搞混了。总之,最后会将这个 clientHandle 发送给子进程:

// lib/internal/cluster/round_robin_handle.jsRoundRobinHandle.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') 时,已经监听了该事件:

// lib/internal/cluster/child.jsprocess.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) 方法。这个方法第二个参数名就叫 clientHandle,这也是为什么前面的 handle 我想叫这个名字的原因。

还是用图来总结下:

roundrobinhandle.png

SharedHandle 不同的是,该调度策略中 onconnection 最开始是在主进程中触发的,然后通过轮询算法挑选一个子进程,将 clientHandle 传递给它。

为什么端口不冲突

cluster 模块的调试就到此告一段落了,接下来我们来回答一下一开始的问题,为什么多个进程监听同一个端口没有报错?

网上有些文章说是因为设置了 SO_REUSEADDR,但其实跟这个没关系。通过上面的分析知道,不管什么调度策略,最终都只会在主进程中对 TCP 对象 bind 一次。

我们可以修改一下源代码来测试一下:

// deps/uv/src/unix/tcp.c 下面的 SO_REUSEADDR 改成 SO_DEBUGif (setsockopt(tcp->io_watcher.fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)))

编译后执行发现,我们仍然可以正常使用 cluster 模块。

那这个 SO_REUSEADDR 到底影响的是啥呢?我们继续来研究一下。

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 是对城市的要求。现在甲乙都想要一个 port1米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)

结果是:设置了 SO_REUSEADDR 可以正常运行,而修改成 SO_DEBUG 的会报错。

还是上面的例子,甲对城市没有限制,乙需要是来自 深圳 的,那当一个对象来自 深圳,我们可以选择优先介绍给乙,非 深圳 的就选择介绍给甲,这个就是 SO_REUSEADDR 的作用。

补充

SharedHandleRoundRobinHandle 两种模式的对比

先准备下测试代码:

// cluster.jsconst cluster = require('cluster')const net = require('net')if (cluster.isMaster) {  for (let i = 0; i  {    console.log(`PID: ${process.pid}!`)  })  server.listen(9997)}
// client.jsconst net = require('net')for (let i = 0; i < 20; i++) {  net.connect({port: 9997})}

RoundRobin先执行 node cluster.js,然后执行 node client.js,会看到如下输出,可以看到没有任何一个进程的 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

先执行 NODE_CLUSTER_SCHED_POLICY=none node cluster.js,则 Node.js 会使用 SharedHandle,然后执行 node client.js,会看到如下输出,可以看到同一个 PID 连续输出了多次,所以这种策略会导致进程任务分配不均的现象。就像公司里有些人忙到 996,有些人天天摸鱼,这显然不是老板愿意看到的现象,所以不推荐使用。

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相关知识,请访问:nodejs 教程!

以上就是一文聊聊Node.js中的cluster(集群)的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/544582.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
java前端后端怎么对接
上一篇 2025年11月9日 16:48:57
opera欧朋浏览器官网入口
下一篇 2025年11月9日 16:48:59

相关推荐

  • VS Code微服务开发:Docker与Kubernetes集成

    VS Code通过Docker扩展实现本地容器化开发,支持自动生成Dockerfile、一键构建镜像及devcontainer环境一致性;2. Kubernetes扩展可连接集群并管理资源,结合Bridge to Kubernetes实现本地调试与集群网络集成;3. 使用Skaffold自动化构建部…

    2026年9月24日
    100
  • VSCode主题开发:创建动态色彩主题的进阶技术解析

    动态主题需通过外部插件监听系统事件实现,核心是利用vscode.themeColor API响应主题切换,结合语义化作用域与Semantic Highlighting精准控制配色逻辑,实现智能自适应视觉体验。 想让VSCode主题随环境自动切换色彩?动态主题不只是换个配色那么简单。核心在于理解VSC…

    2026年9月23日
    400
  • 如何在mysql中使用连接池提升并发

    连接池通过复用数据库连接减少开销,提升高并发下系统性能;需根据语言选择HikariCP、SQLAlchemy等组件,合理配置最大连接数、空闲连接等参数,并结合数据库优化与监控调优以充分发挥效果。 在高并发场景下,频繁创建和销毁数据库连接会带来显著的性能开销。MySQL本身不直接提供连接池功能,但可以…

    2026年9月23日
    300
  • 嵌入式Linux开发-根文件系统本地挂载

    嵌入式Linux开发-根文件系统本地挂载嵌入式Linux开发-根文件系统本地挂载嵌入式Linux开发-根文件系统本地挂载嵌入式Linux开发-根文件系统本地挂载

    引言 前一篇文章介绍了根文件系统的制作与nfs网络挂载,本文将探讨如何通过本地挂载根文件系统来完成系统启动。本地挂载通常用于产品发布阶段,并且分为两种操作方式。 第一种方式:在PC机上制作好文件映像rootfs.img,然后通过uboot加载并直接烧写到EMMC中。这种方法最便捷,适用于产品批量生产…

    2026年9月23日 用户投稿
    300
  • 构建VSCode多媒体编程界面与实时音视频处理

    答案:VSCode通过配置Node.js、Python扩展及FFmpeg等工具,结合OpenCV、PyAudio等框架,可构建高效音视频处理环境。1. 安装Python和Node.js支持,启用Pylance、Jupyter插件提升数据处理体验;2. 配置终端与Code Runner实现脚本一键执行…

    2026年9月22日
    100
  • rm -rf 误删文件?别急,或许有救!

    rm -rf 误删文件?别急,或许有救!rm -rf 误删文件?别急,或许有救!rm -rf 误删文件?别急,或许有救!rm -rf 误删文件?别急,或许有救!

    立即采取行动! 在生产环境中,我们应尽量避免进行风险操作。但如果不慎犯错,如何挽救呢?让我分享一个故事:上周我为了打包一个应用,需要整理Ubuntu 16.04上的线上数据,不小心删除了一个数据文件,幸而最终有惊无险,现记录如下。 extundelete 我的恢复计划主要依赖于一个工具——extun…

    2026年9月22日 用户投稿
    000
  • Linux系统中文件属性和权限实战操作

    Linux系统中文件属性和权限实战操作Linux系统中文件属性和权限实战操作Linux系统中文件属性和权限实战操作Linux系统中文件属性和权限实战操作

    —–原本今天的文章是昨天晚上就要更新的,但是由于昨天晚上下班回到住的地方,发现停电了,所以就没写成。今天是在上一篇文章–linux系统中文件类型的基础上,继续进行深入的学习。好了,直接开干。 一、文件的操作权限: 1、在这之前我想还是很有必要介绍对文件的操作权限(…

    2026年9月22日 用户投稿
    300
  • 使用本地HTML文件运行JavaScript脚本失败的原因及解决方案

    本文旨在帮助开发者理解在没有Web服务器的情况下,直接通过浏览器打开本地HTML文件时,JavaScript脚本可能无法正常运行的原因,并提供相应的解决方案。文章将深入探讨浏览器安全策略、相对路径问题以及如何正确引入和执行JavaScript脚本,确保你的HTML、CSS和JavaScript代码能…

    2026年9月21日
    100
  • PHPComposer怎么安装_PHPComposer依赖管理工具安装与使用指南

    PHPComposer是PHP的依赖管理工具,类似npm或pip。需先安装PHP,再下载并验证composer-setup.php,执行安装生成composer.phar,推荐全局安装至/usr/local/bin/composer,运行composer –version验证。使用com…

    2026年9月21日
    100
  • mysql如何实现后台管理系统

    答案:基于MySQL的%ignore_a_1%需设计用户、权限、日志等表结构,通过后端语言实现安全的CRUD接口与JWT认证,前端展示数据并控制权限,确保系统安全稳定。 实现一个基于 MySQL 的后台管理系统,核心是构建一个安全、稳定、可扩展的系统架构,将数据库作为数据存储层,配合后端语言和前端界…

    2026年9月21日
    200
  • VSCode的扩展推荐是怎么工作的?

    VSCode的扩展推荐基于用户行为和项目环境智能生成,当你打开.py文件时会推荐Python相关工具,打开.ts、.vue等文件则触发对应语言插件;系统通过分析package.json、requirements.txt等依赖文件识别技术栈,推荐Docker、ESLint等匹配扩展;同时记录常用操作如…

    2026年9月20日
    000
  • VSCode的侧边栏图标代表什么?

    资源管理器(文件夹图标)用于管理项目文件结构,支持新建、重命名、删除和拖拽操作;2. 搜索(放大镜图标)实现全局文本查找与替换,支持正则表达式及范围筛选;3. 源代码管理(分支图标)集成Git功能,可查看变更、提交代码并同步远程仓库;4. 运行和调试(虫子图标)支持断点调试、变量监控及多语言启动配置…

    2026年9月20日
    000
  • 如何为VSCode添加自定义主题?

    可通过安装扩展、手动添加JSON文件或使用Yeoman生成器创建主题。首先安装现成主题扩展并从颜色主题列表中选择应用;其次将自定义主题JSON文件放入用户themes目录后在命令面板中启用;最后可用yo code生成项目开发发布主题,需编辑配色文件并预览效果。 为 VSCode 添加自定义主题有几种…

    2026年9月20日
    000
  • edge浏览器无法打开本地HTML文件或显示空白怎么办_Edge浏览器打开本地HTML文件失败解决方法

    1、检查文件路径并选择Edge打开,确保路径为纯英文;2、在edge://flags中启用“Allow file access from files”;3、使用开发者工具排查资源加载错误;4、通过命令行启动Edge绕过安全限制;5、推荐使用npx live-server搭建本地服务器运行HTML文件…

    2026年9月20日
    000
  • VSCode的扩展推荐是基于什么?

    答案是VSCode根据文件类型、项目配置、用户行为和社区流行度智能推荐扩展。打开.py文件时提示Python扩展,存在package.json则推荐JS工具,频繁格式化代码会推荐Prettier,安装ESLint后可能推荐Prettier,高评分和官方扩展优先展示。 VSCode的扩展推荐主要基于你…

    2026年9月20日
    000
  • mysql如何实现用户注册功能

    答案:通过MySQL创建用户表并结合后端逻辑实现注册功能。首先在MySQL中创建包含用户名、密码、邮箱等字段的users表,确保唯一性约束;后端接收%ignore_a_1%提交的注册数据,对密码加密(如SHA256或bcrypt),使用参数化查询将数据安全存入数据库;前端通过表单或AJAX发送注册请…

    2026年9月13日
    200
  • 如何利用VSCode的配置文件(Launch.json)环境变量来管理不同环境的配置?

    通过配置launch.json中的多个调试环境并结合.env文件,可高效管理开发、测试、生产等不同环境的运行参数。 在使用 VSCode 进行开发时,launch.json 文件是调试配置的核心。通过合理利用环境变量和配置文件分离,可以轻松管理不同环境(如开发、测试、生产)的运行参数。 理解 lau…

    2026年9月13日
    000
  • 如何用ChatGPT来重构优化一段旧代码_提升代码质量与性能的步骤

    明确目标并提供上下文,分步引导ChatGPT优化代码结构、算法和语言特性,最后验证建议并本地测试,确保重构有效且安全。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ 用ChatGPT来重构和优化旧代码是一种高效提升代码质量与性能的方式。关键…

    2026年9月12日
    200
  • 如何让VSCode自动识别项目类型?

    VSCode通过项目中的文件和配置自动识别类型,需提供线索如package.json、requirements.txt等标志性文件,结合安装对应语言扩展、配置.vscode/settings.json及使用.code-workspace文件,即可让编辑器正确识别并支持Node.js、Python、R…

    2026年9月12日
    300
  • 如何配置VSCode以附加(Attach)模式调试一个正在运行的Node.js或Python进程?

    要使用VSCode附加模式调试Node.js或Python进程,需先以可调试方式启动程序。对于Node.js,通过–inspect系列参数开启调试端口(如9229),并在launch.json中配置”request”: “attach”及对…

    2026年9月12日
    100

发表回复

登录后才能评论
关注微信