共计 2405 个字符,预计需要花费 7 分钟才能阅读完成。
背景
最近在项目中使用 egg 进行服务端开发,在开发过程中遇到了比较诡异的问题,具体表现为 mq 在监听到信息时,其回调函数会被多次执行,那么这会导致某个文件被同时操作等问题。
问题成因
这边梳理 egg 文档时,重点过了一下 egg 多进程的设计模式,了解到 egg 的 master-agent-worker 模式,那么这里面有些问题是需要我们在开发时去注意的了。
首先介绍下 egg 的多进程实现方式
egg 通过 node 提供的 cluster 实现了多进程模式,为了更好地利用多核环境,egg 一般会启用相当于 cpu 核数的 worker,以此来最大化利用 cpu 能力。
在 egg 启动时,master,agent,worker 的关系如图所示
+---------+ +---------+ +---------+
| Master | | Agent | | Worker |
+---------+ +----+----+ +----+----+
| fork agent | |
+-------------------->| |
| agent ready | |
|<--------------------+ |
| | fork worker |
+----------------------------------------->|
| worker ready | |
|<-----------------------------------------+
| Egg ready | |
+-------------------->| |
| Egg ready | |
+----------------------------------------->|
在这种模式下,master、agent、worker 各司其职,主要制作分配如下:
master: 负责维护整个应用稳定性,当有 worker 因异常而退出时,master 负责拉起新的 worker,以确保应用正常运行。
agent: 由于 egg 的多进程模型会在每个进程中运行一份我们的应用实例,那么在某些情况下,这种机制会导致问题。比如,保存日志的逻辑如果在每个进程中都执行的话,那么在触发日志保存操作的时候,会有多个进程同时操作日志文件,那么此时就会导致文件读写问题。所以 egg 设计了 agent 进程,agent 进程只会有一个,不会出现上述问题,这样,对于类似上述的后台运行的逻辑就统一放到 agent 中去处理了。
worker: 负责执行业务代码,处理用户请求和定时任务,egg 在框架层保证了定时任务只会在单个 worker 中执行,所以可以放心使用。
分析 egg 多进程导致的问题
上面我们分析过了 egg 的多进程机制,所以我们知道了问题成因,出现我们最开始说的问题的原因是我们把 mq 的监听和处理逻辑放到了 worker 中,那么这样的话在实际运行过程中,就会导致 mq 收到消息时,回调函数被执行多次。
到这里我们已经知道如何优化了,那就是把 mq 的处理逻辑放到 agent 中,以确保 mq 消息的回调仅执行一次。但是细心地你肯定发现了,这里有个问题,agent 只有一个实例,如果事情在 agent 里面做,那么不是无法利用多核性能了吗?
agent 与 worker 通信
的确,我们可以在 agent 中处理仅需要单次执行的逻辑,但是这样做就没法利用多核性能了。那么有什么办法吗?没错,就是进程间通信,具体思路就是,agent 还是负责 mq 的连接和监听逻辑,但是回调函数不在 agent 中执行,而是写在 worker 里面。那么 worker 什么时候执行这个逻辑呢?答案是,agent 通过进程间通信通知 worker。egg 内部实现了一个进程间通信机制,我们直接调用即可,主要实现方式如下:
广播消息: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 |
+----------+ +----------+ +----------+
这里我们可以看出来,进程间通信都是基于 master 转发的,所以我们可以利用 egg 提供的机制,解决我们的问题。
解决办法
如上文分析,我们把 mq 的连接和监听逻辑放到 agent 中,当接收到消息时,通过进程间通信把通知发送给 worker,然后由 worker 执行具体的业务逻辑即可。具体代码其实可以参考 vue 的事件机制,在 worker 中监听指定事件:
app.messenger.on(action, data => {// 执行业务逻辑});
在 agent 中建立 mq 连接并监听消息,收到消息后触发事件:
exports.task = async ctx => {
...// 收到 mq 消息的逻辑此处省略
// 准备发送通知
ctx.app.messenger.sendRandom(action);
};
注意,需要单次执行的任务要调用 sendRandom 方法,这个是发送给一个 worker 的方法。当然,如果要执行多次的,可以调用 app.messenger.sendToApp() 方法,这个方法会把消息发送给所有 worker,并执行多次处理逻辑。
总结
egg 在多进程模型中的使用还是需要有一些技巧的,所以需要我们先熟悉 egg 的多进程机制后再进行业务开发,避免遇到奇怪的坑,浪费时间。