IO.js Cluster

2018-11-28 22:33 更新

稳定度: 2 - 稳定

单个的io.js实例运行在单线程上。为了享受多核系统的优势,用户需要启动一个io.js集群来处理负载。

cluster模块允许你方便地创建共享服务器端口的子进程:

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  // Fork workers.
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  // Workers can share any TCP connection
  // In this case its a HTTP server
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}

启动io.js将会在工作线程中共享8000端口:

% NODE_DEBUG=cluster iojs server.js
23521,Master Worker 23524 online
23521,Master Worker 23526 online
23521,Master Worker 23523 online
23521,Master Worker 23528 online

这个特性是最近才开发的,并且可能在未来有所改变。请试用它并提供反馈。

注意,在Windows中,在工作进程中建立命名管道服务器目前是不可行的。

工作原理

工作进程通过child_process.fork方法被创建,所以它们可以与父进程通过IPC管道沟通以及相互传递服务器句柄。

集群模式支持两种分配传入连接的方式。

第一种(并且是除了Windows平台外默认的方式)是循环式。主进程监听一个端口,接受新连接,并且以轮流的方式分配给工作进程,并且以一些内建机制来避免一个工作进程过载。

第二种方式是,主进程建立监听socket并且将它发送给感兴趣的工作进程。工作进程直接接受传入的连接。

第二种方式理论上有最好的性能。但是在实践中,操作系统的调度不可预测,分配往往十分不平衡。负载曾被观察到8个进程中,超过70%的连接结束于其中的2个进程。

因为server.listen()将大部分工作交给了主进程,所以一个普通的io.js进程和一个集群工作进程会在三种情况下有所区别:

  1. server.listen({fd: 7}) 因为消息被传递给了主进程,主进程的文件描述符7会被监听,并且句柄会被传递给工作进程而不是监听工作进程中文件描述符为7的东西。

  2. server.listen(handle) 明确地监听句柄,会让工作进程使用给定的句柄,而不是与主进程通信。如果工作进程已经有了此句柄,那么将假设你知道你在做什么。

  3. server.listen(0) 通常,这会导致服务器监听一个随机端口。但是,在集群中,每次调用listen(0)时,每一个工作进程会收到同样的“随机”端口。也就是说,端口只是在第一次方法被调用时是随机的,但在之后是可预知的。如果你想监听特定的端口,则根据工作进程的PID来生成端口号。

由于在io.js或你的程序中的工作进程间没有路由逻辑也没有共享的状态。所以,请不要为你的程序设计成依赖太重的内存数据对象,如设计会话和登陆时。

因为工作进程都是独立的进程,它们可以根据你程序的需要被杀死或被创建,并且并不会影响到其他工作进程。只要有活跃的工作进程,那么服务器就会继续接收连接。但是io.js不会自动地为你管理工作进程数。所以根据你的应用需求来管理工作进程池是你的责任。

cluster.schedulingPolicy

调度策略,选择cluster.SCHED_RR来使用循环式,或选择cluster.SCHED_NONE来由操作系统处理。这是一个全局设定,并且在你第一次启动了一个工作进程或调用cluster.setupMaster()方法后就不可再更改。

SCHED_RR是除了Windows外其他操作系统中的默认值。一旦libuv能够有效地分配IOCP句柄并且没有巨大的性能损失,那么Windows下的默认值也会变为它。

cluster.schedulingPolicy也可以通过环境变量NODE_CLUSTER_SCHED_POLICY来设定。合法值为rrnone

cluster.settings

  • Object

  • execArgv Array 传递给io.js执行的字符串参数(默认为process.execArgv
  • exec String 工作进程文件的路径(默认为process.argv[1]
  • args Array 传递给工作进程的字符串参数(默认为process.argv.slice(2)
  • silent Boolean 是否将工作进程的输出传递给父进程的stdio(默认为false
  • uid Number 设置用户进程的ID
  • gid Number 设置进程组的ID

在调用.setupMaster()(或.fork())方法之后,这个settings对象会存放方法的配置,包括默认值。

因为.setupMaster()仅能被调用一次,所以这个对象被设置后便不可更改。

这个对象不应由你来手工更改或设置。

cluster.isMaster

  • Boolean

如果进程是主进程则返回true。这由process.env.NODE_UNIQUE_ID决定。如果process.env.NODE_UNIQUE_IDundefined,那么就返回true

cluster.isWorker

  • Boolean

如果进程不是主进程则返回true

Event: 'fork'

  • worker Worker object

当一个新的工作进程由cluster模块所开启时会触发fork事件。这能被用来记录工作进程活动日志,或创建自定义的超时。

var timeouts = [];
function errorMsg() {
  console.error("Something must be wrong with the connection ...");
}

cluster.on('fork', function(worker) {
  timeouts[worker.id] = setTimeout(errorMsg, 2000);
});
cluster.on('listening', function(worker, address) {
  clearTimeout(timeouts[worker.id]);
});
cluster.on('exit', function(worker, code, signal) {
  clearTimeout(timeouts[worker.id]);
  errorMsg();
});

Event: 'online'

  • worker Worker object

当创建了一个新的工作线程后,工作线程必须响应一个在线信息。当主进程接收到在线信息后它会触发这个事件。forkonline事件的区别在于:fork是主进程创建了工作进程后触发,online是工作进程开始运行时触发。

cluster.on('online', function(worker) {
  console.log("Yay, the worker responded after it was forked");
});

Event: 'listening'

  • worker Worker object
  • address Object

当工作进程调用listen()方法。服务器会触发listening事件,集群中的主进程也会触发一个listening事件。

这个事件的回调函数包含两个参数,第一个worker是一个包含工作进程的对象,address对象是一个包含以下属性的对象:addressportaddressType。当工作进程监听多个地址时,这非常有用。

cluster.on('listening', function(worker, address) {
  console.log("A worker is now connected to " + address.address + ":" + address.port);
});

addressType是以下中的一个:

  • 4 (TCPv4)
  • 6 (TCPv6)
  • -1 (unix domain socket)
  • "udp4" 或 "udp6" (UDP v4 或 v6)

Event: 'disconnect'

  • worker Worker object

当工作进程的IPC信道断开连接时触发。这个事件当工作进程优雅地退出,被杀死,或手工断开连接(如调用worker.disconnect())后触发。

disconnectexit事件之间可能存在延迟。这两个事件可以用来侦测是否进程在清理的过程中被阻塞,或者是否存在长连接。

cluster.on('disconnect', function(worker) {
  console.log('The worker #' + worker.id + ' has disconnected');
});

Event: 'exit'

  • worker Worker object
  • code Number 如果正常退出,则为退出码
  • signal String 导致进程被杀死的信号的信号名(如'SIGHUP')

当任何一个工作进程结束时,cluster模块会触发一个exit事件。

这可以被用来通过再次调用.fork()方法重启服务器。

cluster.on('exit', function(worker, code, signal) {
  console.log('worker %d died (%s). restarting...',
    worker.process.pid, signal || code);
  cluster.fork();
});

参阅child_process事件:exit

Event: 'setup'

  • settings Object

每次.setupMaster()方法被调用时触发。

这个settings对象与.setupMaster()被调用时cluster.settings对象相同,并且仅供查询,因为.setupMaster()可能在一次事件循环里被调用多次。

如果保持精确十分重要,请使用cluster.settings

cluster.setupMaster([settings])

  • settings Object

  • exec String 工作进程文件的路径(默认为process.argv[1]
  • args Array 传递给工作进程的参数字符串(默认为process.argv.slice(2)
  • silent Boolean 是否将工作进程的输出传递给父进程的stdio(默认为false)

setupMaster方法被用来改变默认的fork行为。一旦被调用,settings参数将被表现为cluster.settings

注意:

  • 任何settings的改变仅影响之后的.fork()调用,而不影响已经运行中的工作进程
  • 工作进程中唯一不同通过.setupMaster()来设置的属性是传递给.fork()方法的env参数
  • 上文中的参数的默认值仅在第一次调用时被应用,之后的调用的默认值是当前cluster.setupMaster()被调用时的值。

例子:

var cluster = require('cluster');
cluster.setupMaster({
  exec: 'worker.js',
  args: ['--use', 'https'],
  silent: true
});
cluster.fork(); // https worker
cluster.setupMaster({
  args: ['--use', 'http']
});
cluster.fork(); // http worker

这只能被主进程调用。

cluster.fork([env])

  • env Object 将添加到工作进程环境变量的键值对
  • return Worker object

创建一个新的工作进程。

这只能被主进程调用。

cluster.disconnect([callback])

  • callback Function 当所有的工作进程断开连接并且所有句柄关闭后调用

cluster.workers中的每一个工作进程中调用.disconnect()

当所有进程断开连接,所有内部的句柄都将关闭,如果没有其他的事件处于等待,将允许主进程优雅地退出。

这个方法接受一个可选的将会在结束时触发的回调函数参数。

这只能被主进程调用。

cluster.worker

  • Object

当前工作进程对象的引用。对于主进程不可用。

var cluster = require('cluster');

if (cluster.isMaster) {
  console.log('I am master');
  cluster.fork();
  cluster.fork();
} else if (cluster.isWorker) {
  console.log('I am worker #' + cluster.worker.id);
}

cluster.workers

  • Object

一个储存了所有活跃的工作进程对象的哈希表,以id字段为主键。这使得遍历所有工作进程变得容易。仅在主进程中可用。

当工作进程断开连接或退出时,它会从cluster.workers中移除。这个两个事件的触发顺序不能被提前决定。但是,能保证的是,从cluster.workers移除一定发生在这两个事件触发之后。

// Go through all workers
function eachWorker(callback) {
  for (var id in cluster.workers) {
    callback(cluster.workers[id]);
  }
}
eachWorker(function(worker) {
  worker.send('big announcement to all workers');
});

想要跨越通信信道来得到一个工作进程的引用时,使用工作进程的唯一id能简单找到工作进程。

socket.on('data', function(id) {
  var worker = cluster.workers[id];
});

Class: Worker

Worker对象包含了一个工作进程所有的公开信息和方法。在主进程中它可以通过cluster.workers取得。在工作进程中它可以通过cluster.worker取得。

worker.id

  • String

每一个新的工作进程都被给予一个独一无二的id,这个id被存储在此id属性中。

当一个工作进程活跃时,这是它被索引在cluster.workers中的主键。

worker.process

  • ChildProcess object

所有的工作进程都通过child_process.fork()被创建,返回的对象被作为.process属性存储。在一个工作进程中,全局的process被存储。

参阅Child Process module

注意,如果在进程中disconnect事件触发并且.suicide属性不为true,那么进程会调用process.exit(0)。这防止了意外的断开连接。

worker.suicide

  • Boolean

通过调用.kill().disconnect()设置,在这之前他为undefined

布尔值worker.suicide使你可以区别自发和意外的退出,主进程可以通过这个值来决定使用重新创建一个工作进程。

cluster.on('exit', function(worker, code, signal) {
  if (worker.suicide === true) {
    console.log('Oh, it was just suicide\' – no need to worry').
  }
});

// kill worker
worker.kill();

worker.send(message[, sendHandle])

  • message Object
  • sendHandle Handle object

给工作进程或主进程发生一个信息,可选得添加一个句柄。

在主进程中它将给特定的工作进程发送一个信息。它指向child.send()

在工作进程中它将给主进程发送一个信息。它指向process.send()

下面的例子将来自主进程的所有信息返回:

if (cluster.isMaster) {
  var worker = cluster.fork();
  worker.send('hi there');

} else if (cluster.isWorker) {
  process.on('message', function(msg) {
    process.send(msg);
  });
}

worker.kill([signal='SIGTERM'])

  • signal String 传递给工作进程的结束信号名

这个函数将会杀死工作进程。在主进程中,它通过断开worker.process做到,并且一旦断开,使用signal杀死进程。在工作进程中,它通过断开信道做到,然后使用退出码0退出。

会导致.suicide被设置。

为了向后兼任,这个方法的别名是worker.destroy()

注意在工作进程中,process.kill()存在,但它不是这个函数。是process.kill(pid[, signal])

worker.disconnect()

在工作进程中,这个函数会关闭所有的服务器,等待这些服务器上的close事件,然后断开IPC信道。

在主进程中,一个内部信息会被传递给工作进程,至使它们自行调用.disconnect()

会导致.suicide被设置。

注意在一个服务器被关闭后,它将不会再接受新连接,但是连接可能被其他正在监听的工作进程所接收。已存在的连接将会被允许向往常一样退出。当没有更多的连接存在时,工作进程的IPC信道会关闭并使之优雅地退出,参阅server.close()

以上说明仅应用于服务器连接,客户端连接将不会自动由工作进程关闭,并且在退出前,不会等到连接退出。

注意在工作进程中,process.disconnect存在,但它不是这个函数。是child.disconnect()

由于长连接可能会阻塞工作进程的退出,这时传递一个动作信息非常有用,应用来根据信息指定的动作来关闭它们。超时机制是上述的有用实现,在disconnect事件在指定时长后没有触发时,杀死工作进程。

if (cluster.isMaster) {
  var worker = cluster.fork();
  var timeout;

  worker.on('listening', function(address) {
    worker.send('shutdown');
    worker.disconnect();
    timeout = setTimeout(function() {
      worker.kill();
    }, 2000);
  });

  worker.on('disconnect', function() {
    clearTimeout(timeout);
  });

} else if (cluster.isWorker) {
  var net = require('net');
  var server = net.createServer(function(socket) {
    // connections never end
  });

  server.listen(8000);

  process.on('message', function(msg) {
    if(msg === 'shutdown') {
      // initiate graceful close of any connections to server
    }
  });
}

worker.isDead()

如果工作进程已经被关闭,则返回true

worker.isConnected()

如果工作进程通过它的IPC信道连接到主进程,则返回true。一个工作进程在被创建后连接到它的主进程。在disconnect事件触发后它会断开连接。

Event: 'message'

  • message Object

这个事件与child_process.fork()所提供的事件完全相同。

在工作进程中你也可以使用process.on('message')

例子,这里有一个集群,使用消息系统在主进程中统计请求的数量:

var cluster = require('cluster');
var http = require('http');

if (cluster.isMaster) {

  // Keep track of http requests
  var numReqs = 0;
  setInterval(function() {
    console.log("numReqs =", numReqs);
  }, 1000);

  // Count requestes
  function messageHandler(msg) {
    if (msg.cmd && msg.cmd == 'notifyRequest') {
      numReqs += 1;
    }
  }

  // Start workers and listen for messages containing notifyRequest
  var numCPUs = require('os').cpus().length;
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  Object.keys(cluster.workers).forEach(function(id) {
    cluster.workers[id].on('message', messageHandler);
  });

} else {

  // Worker processes have a http server.
  http.Server(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");

    // notify master about the request
    process.send({ cmd: 'notifyRequest' });
  }).listen(8000);
}

Event: 'online'

cluster.on('online')事件相似,但指向了特定的工作进程。

cluster.fork().on('online', function() {
  // Worker is online
});

这不是在工作进程中触发的。

Event: 'listening'

  • address Object

cluster.on('listening')事件相似,但指向了特定的工作进程。

cluster.fork().on('listening', function(address) {
  // Worker is listening
});

这不是在工作进程中触发的。

Event: 'disconnect'

cluster.on('disconnect')事件相似,但指向了特定的工作进程。

cluster.fork().on('disconnect', function() {
  // Worker has disconnected
});

Event: 'exit'

  • code Number 如果正常退出,则为退出码
  • signal String 导致进程被杀死的信号名(如'SIGHUP'

cluster.on('exit')事件相似,但指向了特定的工作进程。

var worker = cluster.fork();
worker.on('exit', function(code, signal) {
  if( signal ) {
    console.log("worker was killed by signal: "+signal);
  } else if( code !== 0 ) {
    console.log("worker exited with error code: "+code);
  } else {
    console.log("worker success!");
  }
});

Event: 'error'

这个事件与child_process.fork()所提供的事件完全相同。

在工作进程中你也可以使用process.on('error')

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号