# PM2工作机制

pm2 (opens new window) 是我们在使用 Node 开发时常用的服务托管工具,功能很强大。所以我们需要了解其工作机制

# 前置知识

# Node Cluster

熟悉 js 的人都知道,js 是单线程的,在 Node 中,采用的是 多进程单线程 的模型。由于单线程的限制,在多核服务器上,我们需要启动多个进程才能最大化利用服务器性能。

Node 在 V0.8 版本之后引入了 cluster模块 (opens new window),通过一个主进程 (master) 管理多个子进程 (worker) 的方式实现集群。

以下是官网上的一个示例,运行代码,则工作进程会共享 8000 端口:

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

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);

  console.log(`工作进程 ${process.pid} 已启动`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 通信

Node中主进程和子进程之间通过进程间通信 (IPC) 实现进程间的通信,进程间通过 send 方法发送消息,监听 message 事件收取信息,这是 cluster模块 通过集成 EventEmitter 实现的。还是一个简单的官网的进程间通信例子

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

if (cluster.isMaster) {

  // Keep track of http requests
  let numReqs = 0;
  setInterval(() => {
    console.log(`numReqs = ${numReqs}`);
  }, 1000);

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

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

  for (const id in cluster.workers) {
    cluster.workers[id].on('message', messageHandler);
  }

} else {

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

    // Notify master about the request
    process.send({ cmd: 'notifyRequest' });
  }).listen(8000);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 负载均衡

了解 cluster 的话会知道,子进程是通过 cluster.fork() 创建的。在 linux 中,系统原生提供了 fork 方法,那么为什么 Node 选择自己实现 cluster模块 ,而不是直接使用系统原生的方法?主要的原因是以下两点:

  1. fork的进程监听同一端口会导致端口占用错误

  2. fork的进程之间没有负载均衡,容易导致惊群现象

惊群现象:所有的工作进程都在等待一个socket,当socket客户端连接时,所有工作线程都被唤醒,但最终有且仅有一个工作线程去处理该连接,其他进程又要进入睡眠状态。

cluster模块 中,针对第一个问题,通过判断当前进程是否为 master进程,若是,则监听端口,若不是则表示为 fork 的 worker进程,不监听端口。

针对第二个问题,cluster模块 内置了负载均衡功能,master进程 负责监听端口接收请求,然后通过调度算法(默认为 Round-Robin (opens new window),可以通过环境变量 NODE_CLUSTER_SCHED_POLICY 修改调度算法)分配给对应的 worker进程

实现原理如下:

由于net.Server实例的listen方法,最终会调用自身_handle属性下listen方法来完成监听动作,故在代码中修改之:此时的listen方法已经被hack ,每次调用只能发挥return 0 ,并不会监听端口

// lib/net.js
// ...
function listen(self, address, port, addressType, backlog, fd, exclusive) {
  // ...
  
  if (cluster.isMaster || exclusive) {
    self._listen2(address, port, addressType, backlog, fd);
    return; // 仅在worker环境下改变
  }

  cluster._getServer(self, {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags: 0
  }, cb);

  function cb(err, handle) {
    // ...
    self._handle = handle;
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

img

# pm2的实现

pm2 (opens new window) 基于 cluster模块 进行了封装,它能自动监控进程状态、重启进程、停止不稳定进程、日志存储等。利用 pm2 时,可以在不修改代码的情况下实现负载均衡集群。

# 架构

pm2架构图

这篇文章我们要关注的是 pm2Satan进程God Deamon守护进程 以及 两者之间的 进程间远程调用RPC

如果不知道点西方文化,还真搞不清他的文件名为啥是 SatanGod

撒旦(Satan),主要指《圣经》中的堕天使(也称堕天使撒旦),被看作与上帝的力量相对的邪恶、黑暗之源,是God的对立面。

1、其中 Satan.js 提供程序的退出、杀死等方法,God.js 负责维持进程的正常运行,God进程启动后一直运行,相当于 cluster 中的 Master进程,维持 worker 进程的正常运行。

rpc(Remote Procedure Call Protocol)是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。同一机器不同进程间的方法调用也属于rpc的作用范畴。

PM2代码中采用了axon-rpc 和 axon 两个库,基本原理是提供服务的server绑定到一个域名和端口下,调用服务的client连接端口实现rpc连接。后续新版本采用了pm2-axon-rpc 和 pm2-axon两个库,绑定的方法也由端口变成.sock文件,因为采用port可能会和现有进程的端口产生冲突。

# 执行流程

pm2执行流程图

每次命令行的输入都会执行一次satan程序。如果God进程不在运行,首先需要启动God进程。然后根据指令,satan通过rpc调用God中对应的方法执行相应的逻辑。

pm2 start app.js -i 4为例,God在初次执行时会配置cluster,同时监听cluster中的事件

// 配置cluster
cluster.setupMaster({
  exec : path.resolve(path.dirname(module.filename), 'ProcessContainer.js')
});

// 监听cluster事件
(function initEngine() {
  cluster.on('online', function(clu) {
    // worker进程在执行
    God.clusters_db[clu.pm_id].status = 'online';
  });

  // 命令行中 kill pid 会触发exit事件,process.kill不会触发exit
  cluster.on('exit', function(clu, code, signal) {
    // 重启进程 如果重启次数过于频繁直接标注为stopped
    God.clusters_db[clu.pm_id].status = 'starting';

    // 逻辑
    ...
  });
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在God启动后, 会建立Satan和God的rpc链接,然后调用prepare方法。prepare方法会调用cluster.fork,完成集群的启动

God.prepare = function(opts, cb) {
  ...
  return execute(opts, cb);
};
function execute(env, cb) {
  ...
  var clu = cluster.fork(env);
  ...
  God.clusters_db[id] = clu;

  clu.once('online', function() {
    God.clusters_db[id].status = 'online';
    if (cb) return cb(null, clu);
    return true;
  });

  return clu;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 参考

https://cloud.tencent.com/developer/article/1625697

更新时间: 5/5/2023, 11:19:52 AM