接着前篇关于 egg-core 源码分析的文章 Egg 源码分析之egg-core ,今天来看一下 egg-cluster 的源码实现逻辑。
NodeJs 中 javascript 的执行是单线程的,所以一个进程只能使用一个 CPU,为了最大可能的使用服务器资源,一般我们可以使用下面三种方式实现:
- 同一台机器上部署多个 Node 服务,使用不同的端口,然后用 Nginx 做负载均衡将请求转发到不同的 Node 实例;
- 使用 PM2 进程管理工具,多个进程共用一个端口,PM2 负责进程重启工作;
- 利用 Node 自带 child_process 和 cluster 模块可以方便实现多进程之间的通信;
egg-cluster 是什么
egg-cluster 是 Egg 的多进程模型的启动模式,在使用 cluster 模式启动 Egg 应用时,我们只需要配置相关启动参数, egg-cluster 会自动创建相关进程,并管理各个进程之间的通信以及异常处理等问题。主进程 Master 通过 child_process 模块的 fork 函数创建 Agent 子进程,并通过 cluster 模块创建 Worker 子进程。Master/Agent/Worker 三者各司其职,共同保证 Egg 应用的正常运行:
- Master:
Master 进程只有一个且第一个启动,主要负责进程管理的工作,包括 Worker、Agent 进程的初始化和重启以及进程之间的通信工作。Master 不运行任何业务代码,它的稳定性特别重要,一旦挂掉整个 Node 服务就挂掉了;
- Agent
Agent 进程也只有一个,一般在业务开发时我们不太会用到 Agent 进程,它的用处主要有两方面:(1)如果想让你的代码只在一个进程上运行(2)Agent 进程可以将某个消息同时广播到所有 Worker 进程进行处理;
- Worker
Worker 进程根据用户自己的设定可以有多个,主要负责处理业务逻辑和用户的请求。当 Worker 进程异常退出时,Master 进程会重启一个新的 Worker 进程;
Agent 子进程是 Egg.Agent 类的实例,Worker 子进程是 Egg.Application 的实例,而 Egg.Agent 和 Egg.Application 都是 EggApplication 的子类,而 EggApplication 类又是 EggCore 的子类,关于 EggCore 的源码实现可以看一下我前面的文章 Egg 源码分析之egg-core,所以类与实例之间的关系图如下:
+--------------+ 实例 +--------------+
| Agent 子进程 | --------> | Agent 类 |
+--------------+ +---------------+
/ \
child_process.fork / \ 继承
/ \
+---------------+ +-------------------+ 继承 +------------+
| Master 主进程 | | EggApplication 类 | ------> | EggCore 类 |
+-------------- + +------------------ + +------------+
\ /
\ / 继承
cluster.fork \ /
+---------------+ 实例 +----------------+
| Worker 子进程 | -------> | Application 类 |
+---------------+ +-----------------+
egg-cluster 源码分析
egg-cluster 整个模块的入口是 master.js,它的初始化流程如下:
- workerManager 实例和 messenger 实例的初始化
- 自动探测及获取可用的 clusterPort,使用 cluster-client 让 Agent 和 Worker 直接通信变为可能
- 启动 Agent 子进程
- 启动 Worker 子进程
- 启动完毕,实时监测各个进程服务状态
// egg-cluster 源码 -> 启动流程(为了更容易看清楚初始化流程,constructor函数中有些代码先后顺序做了调整)
// Master 继承了 EventEmitter模块,通过事件的监听和订阅方式,非常方便的进行事务处理
class Master extends EventEmitter {
constructor(options) {
super();
//步骤1
this.workerManager = new Manager(); // workManager 是一个进程管理的工具,记录当前各个进程的状态信息
this.messenger = new Messenger(this); // messenger 主要负责进程之间的通信工作
//步骤2:自动探测及获取可用的 clusterPort
detectPort((err, port) => {
this.options.clusterPort = port;
//步骤3. 启动 Agent 子进程
this.forkAgentWorker();
});
//步骤4. 启动 Worker 子进程
this.once('agent-start', this.forkAppWorkers.bind(this));
//步骤5:通知启动完毕,实时监测子进程服务状态
this.ready(() => {
this.isStarted = true;
const action = 'egg-ready';
//通过 messenger 的 send 函数通知服务已经启动就绪
this.messenger.send({
action, to: 'parent', data: {
port: this[REALPORT], address: this[APP_ADDRESS] } });
this.messenger.send({
action, to: 'app', data: this.options });
this.messenger.send({
action, to: 'agent', data: this.options });
//使用 workerManager.startCheck 函数定时监控进程状态
if (this.isProduction) {
this.workerManager.startCheck();
}
});
}
}
步骤1:子进程的管理和进程间的通信(manager 和 messenger)
- manager
manager 实现比较简单,主要通过两个属性 workers 和 agent 来维护进程的状态信息,提供了多个函数用于获取,删除,设置相关进程。这里主要看一下监听进程存活状态的 startCheck 函数的实现:
// egg-cluster 源码 -> startCheck实现
class Manager extends EventEmitter {
startCheck() {
this.exception = 0;
// 每隔 10s 钟监听 Worker 和 Agent 的数量
this.timer = setInterval(() => {
const count = this.count();
// Agent 只有一个且必须处于存活状态,Worker 至少要有一个处于存活状态服务才可用
if (count.agent && count.worker) {
this.exception = 0;
return;
}
this.exception++;
// 如果连续三次检查发现服务都不可用就触发 exception 事件,master.workerManager 监听到该事件后会退出服务
if (this.exception >= 3) {
this.emit('exception', count);
clearInterval(this.timer);
}
}, 10000);
}
}
- messenger
Worker 子进程与 Agent 子进程都可以通过 IPC 与 Master 进程进行通信,而 Worker 子进程与 Agent 子进程之间是无法直接通信的,必须通过 Master 进程作为中间桥梁进行通信。每个 Master/Agent/Worker 进程都有一个 messenger 实例,该 messenger 实例用于管理与其它进程的通信工作。
这里需要注意的是 Master.messenger 实例对应的 Messenger 类的定义是在 egg-cluster 源码 中,而 Agent.messenger 和 Worker.messenger 实例对应的 Messenger 类的定义是在 egg 源码 中。前者定义了主进程如何给子进程发