背景
最近在项目中使用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的多进程机制后再进行业务开发,避免遇到奇怪的坑,浪费时间。