3

背景

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


水电
534 声望33 粉丝