主题
Egg多进程
1. Nodejs多进程【Cluster】
我们常说js是单线的,其实指的是js的主进程(Master)是单线程,默认情况下,Nodejs进程只会运行在一个cpu上,那么若是 Node.js 来做 Web Server,就无法享受到多核运算的好处。
对于Nodejs多进程, Nodejs官方提供了Cluster模块
单个 Node.js 实例在单线程环境下运行。为了更好地利用多核环境,用户有时希望启动一批 Node.js 进程用于加载。 集群化模块使得你很方便地创建子进程,以便于在服务端口之间共享。
1.1 cluster是什么?
借助Egg官方的描述:
简单的说,
- 在服务器上同时启动多个进程。
- 每个进程里都跑的是同一份源代码(好比把以前一个进程的工作分给多个进程去做)。
- 更神奇的是,这些进程可以同时监听一个端口(具体原理推荐阅读 @DavidCai1993 这篇 Cluster 实现原理)。
其中:
- 负责启动其他进程的叫做 Master 进程,他好比是个『包工头』,不做具体的工作,只负责启动其他进程。
- 其他被启动的叫 Worker 进程,顾名思义就是干活的『工人』。它们接收请求,对外提供服务。
- Worker 进程的数量一般根据服务器的 CPU 核数来定,这样就可以完美利用多核资源。
1.2 用Cluster模块创建一个Nodejs多进程应用
js
const cluster = require("cluster");
const http = require("http");
const numCPUs = require("os").cpus().length; // 获取CPU核数 (你的电脑是几核的)
if (cluster.isMaster) {
// 如果进程是主进程,就根据cpu核数,从主进程产生同等数量的新的工作进程。
// 以利用多核的优势,每一个进程上可以跑一套代码
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 主进程监听进程退出事件
cluster.on("exit", function (worker, code, signal) {
console.log("worker " + worker.process.pid + " died");
});
} else {
// 工作进程可以共享任何TCP连接
// 在这里,它是一个HTTP服务器
http
.createServer(function (req, res) {
res.writeHead(200);
res.end("hello world\n");
})
.listen(8000);
console.log("serve runing port 8000");
}
这仅仅是最基本的多进程应用例子,它没有进程之间的通信,非常简陋,非常基础
2. Egg多进程
Egg作为一个企业级的解决方案,当然考虑的东西也很全面,下面来详细介绍
2.1 egg多进程模型
一个egg应用中,有这几种进程: Master
进程:一个应用仅一个 Agent
进程:一个应用仅一个 Worker
进程:每个应用根据Cpu核数来,几核就会有几个 Worker 进程
他们之间的关系如下:
+--------+ +-------+
| Master |<-------->| Agent |
+--------+ +-------+
^ ^ ^
/ | \
/ | \
/ | \
v v v
+----------+ +----------+ +----------+
| Worker 1 | | Worker 2 | | Worker 3 | ....
+----------+ +----------+ +----------+
2.1.1 Master进程
Master 进程承担了进程管理的工作(类似 pm2),不运行任何业务代码,我们只需要运行起一个 Master 进程它就会帮我们搞定所有的 Worker、Agent 进程的初始化以及重启等工作了。
Master 进程的稳定性是极高的,线上运行时我们只需要通过 egg-scripts
后台运行通过 egg.startCluster
启动的 Master
进程就可以了,不再需要使用 pm2
等进程守护模块。
sh
egg-scripts start --daemon
2.1.2 Agent进程
若是仅仅只是Master
fork
出一些workers进程
,那么会出现一个问题,所有的工作,所有的worker进程都会参与,这显然是由问题的,例如:生产环境的日志文件我们一般会按照日期进行归档,这只需要一各进程来完成就行,若多个worker参与,不仅浪费资源,还很可能会乱套,所以,对于这一类后台运行的逻辑,我们希望将它们放到一个单独的进程上去执行,这个进程就叫 Agent Worker
,简称 Agent
。
Agent进程
在egg应用中充当的角色是Master
给其他 Worker
请的一个『秘书』,它不对外提供服务,只给 App Worker
打工,专门处理一些公共事务。
在大部分情况下,我们在写业务代码的时候完全不用考虑 Agent
进程的存在,但是当我们遇到一些场景,只想让代码运行在一个进程上的时候,Agent
进程就到了发挥作用的时候了
由于 Agent
只有一个,而且会负责许多维持连接的脏活累活,因此它不能轻易挂掉和重启,所以 Agent
进程在监听到未捕获异常时不会退出,但是会打印出错误日志,我们需要对日志中的未捕获异常提高警惕。
可以在应用
或插件
根目录下的 agent.js
中实现你自己的逻辑(和启动自定义 用法类似,只是入口参数是 agent 对象)
js
// agent.js
module.exports = agent => {
// 在这里写你的初始化逻辑
// 也可以通过 messenger 对象发送消息给 App Worker
// 但需要等待 App Worker 启动成功后才能发送,不然很可能丢失
agent.messenger.on('egg-ready', () => {
const data = { ... };
agent.messenger.sendToApp('xxx_action', data);
});
};
js
// app.js
module.exports = (app) => {
app.messenger.on('xxx_action', (data) => {
// ...
});
};
2.1.3 Worker进程
在egg中,Worker进程就是用来处理真正的用户请求
和定时任务
的进程,我们的业务代码都是跑在一个个Worker进程上面。
而 Egg 的定时任务也提供了只让一个 Worker 进程运行的能力,所以能够通过定时任务解决的问题就不要放到 Agent 上执行。
Worker 运行的是业务代码,相对会比 Agent 和 Master 进程上运行的代码复杂度更高,稳定性也低一点,当 Worker 进程异常退出时,Master 进程会重启一个 Worker 进程。
2.1.4 三种进程对比
类型 | 进程数量 | 作用 | 稳定性 | 是否运行业务代码 |
---|---|---|---|---|
Master | 1 | 进程管理,进程间消息转发 | 非常高 | 否 |
Agent | 1 | 后台运行工作(长连接客户端) | 高 | 少量 |
Worker | 一般设置为 CPU 核数 | 执行业务代码 | 一般 | 是 |
2.2 Egg进程启动顺序
首先来看一下官方给出的框架的启动时序图:
+---------+ +---------+ +---------+
| Master | | Agent | | Worker |
+---------+ +----+----+ +----+----+
| fork agent | |
+-------------------->| |
| agent ready | |
|<--------------------+ |
| | fork worker |
+----------------------------------------->|
| worker ready | |
|<-----------------------------------------+
| Egg ready | |
+-------------------->| |
| Egg ready | |
+----------------------------------------->|
- Master 启动后先 fork Agent 进程
- Agent 初始化成功后,通过 IPC 通道通知 Master
- Master 再 fork 多个 App Worker
- App Worker 初始化成功,通知 Master
- 所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功
另外,关于 Agent Worker 还有几点需要注意的是:
- 由于 App Worker 依赖于 Agent,所以必须等 Agent 初始化完成后才能 fork App Worker
- Agent 虽然是 App Worker 的『小秘』,但是业务相关的工作不应该放到 Agent 上去做,不然把她累垮了就不好了
- 由于 Agent 的特殊定位,我们应该保证它相对稳定。当它发生未捕获异常,框架不会像 App Worker 一样让他退出重启,而是记录异常日志、报警等待人工处理
- Agent 和普通 App Worker 挂载的 API 不完全一样,如何识别差异可查看框架文档
2.3 Egg怎么做进程守护的?
在Nodejs中,进程守护用的最多的恐怕就是pm2
了,进程守护,也就是说当进程因为一些原因挂掉后,进程守护的工具可以自动帮助我们重启进程,这样可以提高应用的稳定性,健壮性。
健壮性(又叫鲁棒性)是企业级应用必须考虑的问题,除了程序本身代码质量要保证,框架层面也需要提供相应的『兜底』机制保证极端情况下应用的可用性。
2.3.1 nodejs进程退出分类
nodejs进程退出可以分为两类:
未捕获异常
导致进程退出OOM、系统异常
导致进程退出
1)未捕获异常导致进程退出
当代码抛出了异常没有被捕获到时,进程将会退出,此时 Node.js 提供了 process.on('uncaughtException', handler)
接口来捕获它。
uncaughtException 事件为未捕获异常事件,uncaughtexception事件说明
但是当一个 Worker
进程遇到 未捕获的异常 时,它已经处于一个不确定状态,此时我们应该让这个进程优雅退出:
- 关闭异常
Worker
进程所有的TCP Server
(将已有的连接快速断开,且不再接收新的连接),断开和Master
的IPC
通道,不再接受新的用户请求。 Master
立刻fork
一个新的Worker
进程,保证在线的『工人(Worker进程)』总数不变。- 异常
Worker
等待一段时间,处理完已经接受的请求后退出。
未捕获异常进程守护流程图如下:
+---------+ +---------+
| Worker | | Master |
+---------+ +----+----+
| uncaughtException |
+------------+ |
| | | +---------+
| <----------+ | | Worker |
| | +----+----+
| disconnect | fork a new worker |
+-------------------------> + ---------------------> |
| wait... | |
| exit | |
+-------------------------> | |
| | |
die | |
| |
2)OOM、系统异常 导致进程退出
而当一个进程出现异常导致 crash 或者 OOM 被系统杀死时,不像未捕获异常发生时我们还有机会让进程继续执行,只能够让当前进程直接退出,Master 立刻 fork 一个新的 Worker。
在egg框架里,我们采用 graceful
和 egg-cluster
两个模块配合实现上面的逻辑。这套方案已在阿里巴巴和蚂蚁金服的生产环境广泛部署,且经受过『双 11』大促的考验,所以是相对稳定和靠谱的。
3. 进程之间的通信
3.1 Nodejs进程之间通信
虽然每个 Worker 进程是相对独立的,但是它们之间始终还是需要通讯的,叫进程间通讯(IPC)。
下面是 Node.js 官方提供的一段示例代码
js
const cluster = require('cluster');
if (cluster.isMaster) {
const worker = cluster.fork(); // 1. 主进程fork出一个worker进程
worker.send('hi there'); // 2. fork出的worke进程通过send发送一个消息给其他进程
worker.on('message', (msg) => {
// 4. 最终worker进程监听到了msg
console.log(`msg: ${msg} from worker#${worker.id}`);
});
} else if (cluster.isWorker) {
// 3. worker进程监听message事件,后,再将msg转发
process.on('message', (msg) => {
process.send(msg);
});
}
cluster
的 IPC
通道只存在于 Master
和 Worker/Agent
之间,Worker
与 Agent
进程互相间是没有的。那么 Worker
之间想通讯该怎么办呢?是的,通过 Master
来转发,如下图所示:
广播消息: agent => all workers
+--------+ +-------+
| Master |<---------| Agent |
+--------+ +-------+
/ | \
/ | \
/ | \
/ | \
v v v
+----------+ +----------+ +----------+
| Worker 1 | | Worker 2 | | Worker 3 |
+----------+ +----------+ +----------+
指定接收方: one worker => another worker
+--------+ +-------+
| Master |----------| Agent |
+--------+ +-------+
^ |
send to / |
worker 2 / |
/ |
/ v
+----------+ +----------+ +----------+
| Worker 1 | | Worker 2 | | Worker 3 |
+----------+ +----------+ +----------+
3.2 Egg进程通信方式一
为了方便调用,egg官方封装了一个 messenger
对象挂在 app
/ agent
实例上,提供一系列友好的 API。
前提: 需要等 egg-ready
消息之后才能发送消息。只有在 Master
确认所有的 Agent
进程和 Worker
进程都已经成功启动(并 ready)之后,才会通过 messenger
发送 egg-ready
消息给所有的 Agent
和 Worker
,告知一切准备就绪,IPC
通道可以开始使用了。
1)发送API
app.messenger.broadcast(action, data)
:发送给所有的 agent / app 进程(包括自己)app.messenger.sendToApp(action, data)
: 发送给所有的 app 进程 -- 在 app 上调用该方法会发送给自己和其他的 app 进程 -- 在 agent 上调用该方法会发送给所有的 app 进程app.messenger.sendToAgent(action, data)
: 发送给 agent 进程 -- 在 app 上调用该方法会发送给 agent 进程 -- 在 agent 上调用该方法会发送给 agent 自己agent.messenger.sendRandom(action, data)
: -- app 上没有该方法(现在 Egg 的实现是等同于 sentToAgent) -- agent 会随机发送消息给一个 app 进程(由 master 来控制发送给谁)app.messenger.sendTo(pid, action, data)
: 发送给指定进程
js
// app.js
module.exports = (app) => {
// 注意,只有在 egg-ready 事件拿到之后才能发送消息
app.messenger.once('egg-ready', () => {
app.messenger.sendToAgent('agent-event', { foo: 'bar' });
app.messenger.sendToApp('app-event', { foo: 'bar' });
});
};
注意:上面所有 app.messenger
上的方法都可以在 agent.messenger
上使用。
2)接收API
在 messenger
上监听对应的 action
事件,就可以收到其他进程发送来的信息了。
js
app.messenger.on(action, (data) => {
// process data
});
app.messenger.once(action, (data) => {
// process data
});
agent 上的 messenger 接收消息的用法和 app 上一致。
3) messenger 传递数据的缺点:
- 为了尽可能的复用长连接,(因为它们对于服务端来说是非常宝贵的资源),我们会把它放到 Agent 进程里维护,然后通过 messenger 将数据传递给各个 Worker。这种做法是可行的,但是往往需要写大量代码去封装接口和实现数据的传递,非常麻烦。
- messenger 传递数据效率是比较低的,因为它会通过 Master 来做中转;万一 IPC 通道出现问题还可能将 Master 进程搞挂。
3.2 egg进程间通信方式二
参见Egg官方文档:多进程研发模式增强
Egg官方提供一种新的模式来降低这类客户端封装的复杂度。通过建立 Agent
和 Worker
的 socket
直连跳过 Master
的中转。Agent
作为对外的门面维持多个 Worker
进程的共享连接。
新的模式下,客户端的通信方式如下:
+-------+
| start |
+---+---+
|
+--------+---------+
__| port competition |__
win / +------------------+ \ lose
/ \
+---------------+ tcp conn +-------------------+
| Leader(Agent) |<---------------->| Follower(Worker1) |
+---------------+ +-------------------+
| \ tcp conn
| \
+--------+ +-------------------+
| Client | | Follower(Worker2) |
+--------+ +-------------------+