log4js pm2 模式工作原理分析

160 阅读3分钟

一、问题背景

在实际开发中,我们经常引入 pm2 进行 Node 服务进程管理;由于引入 Node 多进程,当它们同时对某个日志文件进行写入时,是无法保证内容完整性和正确性的。

二、解决办法

  1. 朴素想法:每个进程写单独的日志文件,比如在 pid 维度进行划分,文件数多了一点而已;当然也有可能影响到运维对日志的采集,需要沟通协商。
  2. 看看 pm2 的解法:让 master 进程来写,worker 进程与 master 通信即可

三、源码解析(log4js@6.9.1)

代码仓库:github.com/log4js-node…

  1. 核心文件lib/clustering.js(节选)

    L21 判断是否 master 进程

    const isMaster = () => disabled || (cluster && cluster.isMaster) || isPM2Master();
    

    L69 开始这段,决定只有 master 才会监听 message 事件:

    if (disabled || config.disableClustering) {
      debug('Not listening for cluster messages, because clustering disabled.');
    } else if (isPM2Master()) {
      // PM2 cluster support
      // PM2 runs everything as workers - install pm2-intercom for this to work.
      // we only want one of the app instances to write logs
      debug('listening for PM2 broadcast messages');
      process.on('message', receiver);
    } else if (cluster && cluster.isMaster) {
      debug('listening for cluster messages');
      cluster.on('message', receiver); 
    } else {
      debug('not listening for messages, because we are not a master process');
    }
    

    L28 接收到信息做什么呢,看看 receiver 定义,这里的注释就很好,说明 worker logger 会使用 process.send 来通信,这里是消息收到后的处理:

    // in a multi-process node environment, worker loggers will use
    // process.send
    const receiver = (worker, message) => {
      // prior to node v6, the worker parameter was not passed (args were message, handle)
      debug('cluster message received from worker ', worker, ': ', message);
      if (worker.topic && worker.data) {
        message = worker;
        worker = undefined;
      }
      if (message && message.topic && message.topic === 'log4js:message') {
        debug('received message: ', message.data);
        const logEvent = LoggingEvent.deserialise(message.data);
        sendToListeners(logEvent);
      }
    };
    

    L24 回来可以看到 sendToListeners 执行:

    const sendToListeners = (logEvent) => {
      listeners.forEach((l) => l(logEvent));
    };
    

    接下来该问了,listener 哪里来?

    让我们先看 L86 module.exports,逐行分析注释:

    module.exports = {
      // 只在 master 进程执行 fn
      onlyOnMaster: (fn, notMaster) => (isMaster() ? fn() : notMaster),
      // 是否 master 进程
      isMaster,
      // 暴露 send 方法给外面
      send: (msg) => {
        if (isMaster()) {
          // 当进程是 master 则将消息发送给 listener
          sendToListeners(msg);
        } else {
          // worker 进程则通过 process.send 发送消息到 master 进程
          if (!pm2) { // config 是否开启 pm2
            msg.cluster = {
              workerId: cluster.worker.id,
              worker: process.pid,
            };
          }
          process.send({ topic: 'log4js:message', data: msg.serialise() });
        }
      },
      // 暴露 onMessage,方便注册 log event 监听者
      // 这里可以看到 listener 从哪里来
      onMessage: (listener) => {
        listeners.push(listener);
      },
    };
    

    我们根据 module.exports 有理由推测,日志写入模块作为 listener 通过 onMessage 注册进来,当接收到新的 log event,则会调用 listener 处理该 event;

    send 则暴露给使用者来发送 log event;当收到 log event 后,如果是 master 则直接调用sendToListeners 方法处理;如果是 worker,则调用 process.send 发送消息给 master,master 监听 message 事件,然后交由 receiver 进行处理,内部依然是 sendToListeners,遍历监听者处理日志消息。

    到此,流程就梳理完成了。

  2. 根据其他文件如何使用 lib/clustering 来验证我们的推测,得益于 github Symbols 功能,很方便知道 lib/logger.js 使用了 send 方法:

    log(level, ...args) {
       // ......
       // 省略一堆 if...else...
       this._log(logLevel, args);
    }
    
    _log(level, data) {
        debug(`sending log data (${level}) to appenders`);
        // .....
        // 省略其他处理
        const loggingEvent = new LoggingEvent(
          this.category,
          level,
          data,
          this.context,
          callStack,
          error
        );
        // 重点
        clustering.send(loggingEvent);
    }
    

    果然通过 send 方法来发送日志信息!

  3. 再看另一个文件 lib/log4js.js,该文件使用了 onMessage,那我们推测是日志处理模块被注册进来:

    function configure(configurationFileOrObject) {
      // ......
      // 省略其他逻辑
      // 重点:此处将日志配置的 appender,比如 Console 等注册进来,最终实现将日志消息转化为写文件或输出控制台
      clustering.onMessage(sendLogEventToAppender);
      return log4js;
    }
    
  4. 总结:核心就两个 API process.on('message')process.send 实现跨进程通信。