background

As we know, JS is single-threaded, meaning that a NodeJS process can only run on a single CPU. Nodejs is excellent in IO processing, but still has shortcomings in intensive computing applications. The solution is to take advantage of the concurrency advantage of multi-core cpus and run NodeJS processes on each CPU. Egg provides the Egg-Cluster module for multi-process management and interprocess communication.

Introduction (Egg multi-process model)

An egg – cluster is introduced

What is a cluster? To put it simply:

  • Start multiple processes simultaneously on the server.
  • Each process runs the same source code (like dividing the work of a previous process among multiple processes).
  • Even more amazing, these processes can listen on the same port at the same time.

Among them:

  • The process responsible for starting other processes is called the Master process, it is like a “contractor”, does not do the specific work, only responsible for starting other processes.
  • The other processes that are started are called Worker processes, which are literally “workers” doing the work. They receive requests and provide services externally.
  • The number of Worker processes is generally determined according to the number of CPU cores on the server, so that multi-core resources can be perfectly utilized.

Ps: Because the official website is very detailed, this part is based on the official website.

Multiprocess model

Let’s take a look at the multi-process model through the diagrams in the documentation





In fact, some tasks do not need to be done by every Worker. If they are all done, it will waste resources on the one hand and, more importantly, may lead to resource access conflicts between multiple processes

Agent In most cases, when we write business code, we do not need to consider the existence of the Agent process, but when we encounter some scenarios, we only want the code to run on a process, the Agent process comes into play. Because the Agent has only one Agent and is responsible for many dirty and tiring tasks to maintain connections, it cannot be easily hung up or restarted. Therefore, the Agent process will not exit when listening to uncaught exceptions, but will print error logs. We need to be vigilant about uncaught exceptions in logs.

Why are there no port conflicts?

Q: When the process is forked, why is the port not occupied when the code is already listening for a port? A: Working principle of Cluster recommend this article “through the source code analysis of node.js cluster module main function realization”, combined with Teacher Pu Ling “simple nodeJS” in the multi-process architecture, here is A summary:

  1. In master-worker mode, after a child process is created, the parent and child processes create an IPC channel and use message and Send to transmit messages between processes through the IPC channel. Usage:
// parent.js
var n = require('child_process').fork(__dirname + 'child.js')
n.on('message'.function(m){
    console.log('parent get msg:'+  m)  
})
n.send({hello: 'world'})
// child.js
process.on('message'.function(m){
    console.log('child get msg' + m)
})
process.send({hello: 'world'})
Copy the code
  1. A handle is a reference that can be used to identify resources. It contains file descriptors that point to objects. For example, a handle can be used to identify a socket object, a UDP socket, a pipe, etc.). Send can send data, can also send a handle.
child.send(params, [sendHandle])
Copy the code

Detailed usage:

// parent.js
var n = require('child_process').fork(__dirname + 'child.js')
var server = require('net').createServer();
server.listen(8080, function(){
    n.send('server', server)
})
// child.js
process.on('message'.function(m, server){
    server.on('connection'.function(socket){
        console.log('child get msg' + m)
        socket.end('handle by child process')})})Copy the code

By passing the TCP Server, we can see that there are no exceptions and that multiple child processes can listen on the same port. During node handle sending, multiple processes can listen on the same port without causing an EADDRINUSE exception. This is because the process we started independently has different file descriptors on the TCP server socket, causing an exception to be thrown when listening on the same port. Since independently started processes do not know the file descriptors of each other, listening on the same port will fail. However, for the service restored from the handle sent by Send (), their file descriptors are the same, so listening on the same port will not cause an exception.

To sum up, in a word: Share file descriptors by passing handles through interprocess IPC communication

Start sequence of a process

In the egg-cluster/master.js file, you need to initialize, start agent and app processes, and check their status. Taking a look at the code in the constructor, the entire process is nicely illustrated in constructor.

constructor(options) { super(); this.options = parseOptions(options); // new a Messenger instance this. Messenger = new Messenger(this); // Borrow the ready module's method ready.mixin(this); this.isProduction = isProduction(); this.isDebug = isDebug(); . . // Depending on the operating environment (local,testSet the log output levels, prod) enclosing the logger = new ConsoleLogger ({level: process. The env. EGG_MASTER_LOGGER_LEVEL | |'INFO'}); . } // The master notifies parent, app worker, and agent this.ready(() => {this.isStarted =true;
  const stickyMsg = this.options.sticky ? ' with STICKY MODE! ' : ' ';
  this.logger.info('[master] % s started on % s: / / 127.0.0.1: % s (SMS) % % s',
  frameworkPkg.name, this.options.https ? 'https' : 'http',
  this.options.port, Date.now() - startTime, stickyMsg);

  const action = 'egg-ready';
  this.messenger.send({ action, to: 'parent' });
  this.messenger.send({ action, to: 'app', data: this.options });
  this.messenger.send({ action, to: 'agent', data: this.options }); }); // Listen agent exit this.on('agent-exit', this.onAgentExit.bind(this)); // Start this. On ('agent-start', this.onAgentStart.bind(this)); // Listen to app worker exit this.on('app-exit', this.onAppExit.bind(this)); // Listen to app worker start this.on('app-start', this.onAppStart.bind(this)); // Listen to app worker restart this.on('reload-worker', this.onReload.bind(this)); // This. Once (this.once)'agent-start', this.forkAppWorkers.bind(this)); // The master is listening to its own exit and processing after exit //kill(2) Ctrl-c monitor SIGINT signal process.once('SIGINT', this.onSignal.bind(this, 'SIGINT'));
// kill(3) Ctrl-\ listen SIGQUIT signal process.once('SIGQUIT', this.onSignal.bind(this, 'SIGQUIT'));
// kill(15) Default listen to SIGTERM signal process.once('SIGTERM', this.onSignal.bind(this, 'SIGTERM')); / / to monitorexitEvents in the process. Once ('exit', this.onExit.bind(this)); DetectPort ((err, port) => {/* Istanbul ignoreif* /if (err) {
    err.name = 'ClusterPortConflictError';
    err.message = '[master] try get free port error, ' + err.message;
    this.logger.error(err);
    process.exit(1);
    return; } this.options.clusterPort = port; this.forkAgentWorker(); // If there is no conflict, execute this method});Copy the code

Master inherits eventEmitter and listens for messages using publish-subscribe mode. The flow in the constructor is as follows:

  1. usedetect-portTo get a free port
  2. forkAgentWorkerUse child_process.fork() to start the agent process and passprocess.sendNotifies the Master Agent that the agent is started
agent.ready(() => {
 agent.removeListener('error', startErrorHandler);
 process.send({ action: 'agent-start', to: 'master' });
});
Copy the code
  1. forkAppWorkers: Passes after the agent is startedcluster.fork()Start the app_worker process.
// fork app workers after agent started
this.once('agent-start', this.forkAppWorkers.bind(this));
Copy the code

Here, the cfork module is used, which is also cluster.fork() in essence. The default number of processes started is os.cpu.length. The number of worker processes can also be specified by the start parameter.

cfork({
  exec: this.getAppWorkerFile(),
  args,
  silent: false,
  count: this.options.workers,
  // don't refork in local env refork: this.isProduction, windowsHide: process.platform === 'win32'});Copy the code

After the startup is successful, the master is informed through Messenger that the worker process is ready

this.messenger.send({
    action: 'app-start',
    data: {
      workerPid: worker.process.pid,
      address,
    },
    to: 'master',
    from: 'app'});Copy the code
  1. onAppStart: Notifies the agent when the app worker starts successfully. Tell the parent, egg-ready and bring itport,address,protocolParameters such as
    this.ready(() => {
      this.isStarted = true;
      const stickyMsg = this.options.sticky ? ' with STICKY MODE! ' : ' ';
      this.logger.info('[master] %s started on %s (%sms)%s',
        frameworkPkg.name, this[APP_ADDRESS], Date.now() - startTime, stickyMsg);

      const action = 'egg-ready';
      this.messenger.send({
        action,
        to: 'parent',
        data: {
          port: this[REAL_PORT],
          address: this[APP_ADDRESS],
          protocol: this[PROTOCOL],
        },
      });
      this.messenger.send({
        action,
        to: 'app',
        data: this.options,
      });
      this.messenger.send({
        action,
        to: 'agent',
        data: this.options,
      });

      // start check agent and worker status
      if(this.isProduction) { this.workerManager.startCheck(); }});Copy the code
  1. startCheck: In the production environment, check agent and worker every 10 seconds and report exceptions.
  // check agent and worker must both alive
  // if exception appear 3 times, emit an exception event
  startCheck() {
    this.exception = 0;
    this.timer = setInterval(() => {
      const count = this.count();
      if (count.agent && count.worker) {
        this.exception = 0;
        return;
      }
      this.exception++;
      if (this.exception >= 3) {
        this.emit('exception', count); clearInterval(this.timer); }}, 10000); }Copy the code

The flowchart on the Egg document sums up the process nicely:

Interprocess message communication

Principles of Interprocess Communication (IPC)

IPC stands for inter-process Communication. The purpose of interprocess communication is to allow different processes to access each other’s resources and coordinate their work. There are many technologies to realize inter-process communication, such as named pipe, anonymous pipe, socket, semaphore, shared memory, message queue, Domain socket, etc. Pipe is used to implement IPC channel in Node. A pipe is an abstract name in Node. The details are provided by Libuv, named pipe in Win, and Unix Domain Socket in * NIx.

Q: How do processes link through the IPC channel?

The parent process creates the IPC channel and listens for it before actually creating the child process and then tells the child process the file descriptor for IPC communication through the environment variable (NODE_CHANNEL_FD). During the startup process, the child process connects to the existing IPC channel according to the file descriptor, thus completing the connection between the parent process.

Interprocess communication in egg-cluster

The IPC channel of cluster only exists between Master and Worker/Agent, and Worker and Agent processes do not exist between each other. So what should workers do to communicate with each other? Yes, through Master

Code portal

show the code

/** * master messenger,provide communication between parent, master, Agent and app. * ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐ * │ parent │ * / └ ─ ─ ─ ─ ─ ─ ─ ─ ┘ / | \ \ * * / ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐ \ * / │ master │ \ * / └ ─ ─ ─ ─ ─ ─ ─ ─ ┘ \ * / / \ \ * ┌ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ * │ agent │ -- -- -- -- -- -- -- │ │ app * └ ─ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ─ ┘ * / send (data) {if(! data.from) { data.from ='master';
    }

    // recognise receiverPid is to who
    if (data.receiverPid) {
      if (data.receiverPid === String(process.pid)) {
        data.to = 'master';
      } else if (data.receiverPid === String(this.master.agentWorker.pid)) {
        data.to = 'agent';
      } else {
        data.to = 'app';
      }
    }

    // default from -> to rules
    if(! data.to) {if (data.from === 'agent') data.to = 'app';
      if (data.from === 'app') data.to = 'agent';
      if (data.from === 'parent') data.to = 'master';
    }

    // app -> master
    // agent -> master
    if (data.to === 'master') {
      debug('%s -> master, data: %j', data.from, data);
      // app/agent to master
      this.sendToMaster(data);
      return;
    }

    // master -> parent
    // app -> parent
    // agent -> parent
    if (data.to === 'parent') {
      debug('%s -> parent, data: %j', data.from, data);
      this.sendToParent(data);
      return;
    }

    // parent -> master -> app
    // agent -> master -> app
    if (data.to === 'app') {
      debug('%s -> %s, data: %j', data.from, data.to, data);
      this.sendToAppWorker(data);
      return; } // parent -> master -> agent // app -> master -> agentif (data.to === 'agent') {
      debug('%s -> %s, data: %j', data.from, data.to, data);
      this.sendToAgentWorker(data);
      return; }}Copy the code
  1. App /agent -> master: Passmaster.emit(data.action, data.data)(Master inherited from EventEmitter)
  2. app/master/agent -> parent: process.send(data)
  3. parent/agent -> master -> app: sendmessage(worker, data)
  4. parent/agent -> master -> agent: sendmessage(this.master.agentWorker, data)

Note: [sendMessage] is a module (Send a cross process message if Message channel is connected.) written for interprocess communication. Interested students can see the source code for https://github.com/node-modules/sendmessage/blob/master/index.js

Some students might wonder, why is there a parent?

Parent is the parent process of the master process, usually the CLI, such as egg-script start and egg-bin, the egg-script master process created by (‘child_process’).spawn. The child_process.spawn() method spawns a new process with the given command and takes command line arguments from args. Also, by passing the detached parameter, you can make the child process continue executing after the parent process exits. Spawn document portal

(Thanks for @tianzhu’s answer, source link)

Ref

  • Nodejs by Ling Park
  • Egg docs – Multi-process models and inter-process communication
  • Egg source code parsing egg-cluster