一、问题背景
在实际开发中,我们经常引入 pm2 进行 Node 服务进程管理;由于引入 Node 多进程,当它们同时对某个日志文件进行写入时,是无法保证内容完整性和正确性的。
二、解决办法
- 朴素想法:每个进程写单独的日志文件,比如在 pid 维度进行划分,文件数多了一点而已;当然也有可能影响到运维对日志的采集,需要沟通协商。
- 看看 pm2 的解法:让 master 进程来写,worker 进程与 master 通信即可
三、源码解析(log4js@6.9.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
,遍历监听者处理日志消息。到此,流程就梳理完成了。
-
根据其他文件如何使用
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
方法来发送日志信息! -
再看另一个文件
lib/log4js.js
,该文件使用了onMessage
,那我们推测是日志处理模块被注册进来:function configure(configurationFileOrObject) { // ...... // 省略其他逻辑 // 重点:此处将日志配置的 appender,比如 Console 等注册进来,最终实现将日志消息转化为写文件或输出控制台 clustering.onMessage(sendLogEventToAppender); return log4js; }
-
总结:核心就两个 API
process.on('message')
和process.send
实现跨进程通信。