Egg 框架多进程模型与 IPC 进程间通信完全指南:Master / Agent / Worker 架构原理与实战

发布时间:2026/9/21 3:28:31
Egg 框架多进程模型与 IPC 进程间通信完全指南:Master / Agent / Worker 架构原理与实战 后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载本文深入解析 Egg 框架的 Cluster 多进程模型与基于 Messenger 的进程间通信IPC机制涵盖 Master、Agent、Worker 三类进程的分工、守护与重启策略、启动时序以及messenger对象提供的全部消息收发 API。读完本文你将掌握如何用agent.js承载单实例后台任务、如何通过app.js与定时任务、IPC 协作实现内存缓存的多方案更新并在复杂场景下正确选择 Agent 与 Worker 的通信模式。为什么 Node.js 服务需要多进程模型JavaScript 代码运行在单线程上一个 Node.js 进程同一时刻只能利用一个 CPU 核心。当用 Node.js 构建 Web 服务器时如果不引入多进程就无法利用服务器上的多核资源。作为企业级解决方案Egg 必须回答这样一个问题如何榨干服务器资源、充分利用多核Node.js 官方给出的答案是 Cluster 模块单实例 Node.js 运行在单线程中为了利用多核系统用户需要启动一组 Node.js 进程来分担负载Cluster 模块允许你轻松创建共享服务器端口的子进程。在 packages/cluster/src/master.ts 中Egg 的 Master 正是基于这一能力构建它负责 fork 出 Agent 与多个 App Worker并监听agent-start、app-start、agent-exit、app-exit等事件来管理整个进程集群的生命周期。什么是 Cluster多进程并发与端口共享简单来说Cluster 做了三件事在服务器上并发 fork 出多个进程每个进程运行同一份源码相当于把原来由一个进程完成的工作分派给多个进程所有这些进程可以监听同一个端口底层依赖 socket 句柄的共享与分发机制。其中负责 fork 其他进程的进程称为Master 进程它像是一个包工头自身除了 fork 进程外几乎不干活被 fork 出来的进程称为Worker 进程顾名思义它们是真正干活的工人负责接收请求、提供服务Worker 进程的数量通常与 CPU 核心数一致只有这样才能充分发挥多核资源。Node.js 官方 Cluster 示例代码如下const cluster require(cluster); const http require(http); const numCPUs require(os).cpus().length; if (cluster.isMaster) { // Fork workers. for (let i 0; i numCPUs; i) { cluster.fork(); } cluster.on(exit, function (worker, code, signal) { console.log(worker worker.process.pid died); }); } else { // Workers can share any TCP connection // In this case it is an HTTP server http .createServer(function (req, res) { res.writeHead(200); res.end(hello world\n); }) .listen(8000); }示例本身很简单但作为企业级方案还有更多问题需要考虑Worker 进程意外退出时如何处理异常多个 Worker 进程之间如何共享资源如何调度多个 Worker 进程……Egg 在packages/cluster包中给出了完整的答案其启动流程的核心编排逻辑位于 packages/cluster/src/master.ts 的#start方法中。框架的多进程模型守护进程Daemon Process企业级应用必须考虑健壮性。除了程序本身要有高质量的代码框架层面还应提供兜底机制确保极端情况下服务的可用性。一般来说Node.js 进程退出有两种原因。未捕获异常Uncaught Exception代码抛出异常却未被捕获时进程会退出。Node.js 提供了process.on(uncaughtException, handler)接口来捕获异常。但如果 Worker 进程遇到未捕获异常它就进入了不确定状态此时应该让它优雅退出关闭该异常 Worker 启动的所有 TCP Server关闭所有连接、停止接收新请求同时关闭 Master 与它之间的 IPC 通道不再接受用户请求Master 立即 fork 一个新的 Worker 进程保证 Worker 总数不变异常 Worker 进程等待一段时间再退出以便把已接受的请求处理完。--------- --------- | Worker | | Master | --------- -------- | uncaughtException | ------------ | | | | --------- | ---------- | | Worker | | | -------- | disconnect | fork a new worker | ------------------------- --------------------- | | wait... | | | exit | | ------------------------- | | | | | die | | | | | |在 packages/cluster/src/master.ts 的onAppExit处理中可以看到Master 会先记录AppWorkerDiedError日志、清理该 Worker 监听器并从workerManager中移除然后向 Agent 同步存活 Worker 的 pid 列表egg-pids。当应用已经启动isAllWorkerStarted后Worker 退出会交由 cfork 在线上模式自动重新 fork 补足如果 Worker 在启动阶段就退出Master 会直接以退出码 1 结束避免带病启动。OOM 与系统级异常当进程因异常崩溃或因内存溢出OOM被操作系统直接杀掉时我们不可能像未捕获异常那样去挽救进程唯一的选择是让当前进程直接退出然后由 Master 立即 fork 一个新的 Worker 顶替。Egg 分别使用graceful与egg-cluster两个模块来实现上述两类守护逻辑。该方案在阿里巴巴集团与蚂蚁集团的生产环境被广泛部署并经历了双 11大促的长期考验稳定可靠。相关实现可进一步阅读本仓库的 packages/cluster 包源码其中 packages/cluster/src/agent_worker.ts 的注释明确说明Agent Worker 只会在两种情况下退出——收到SIGTERM优雅退出退出码 0或收到disconnect事件Master 意外退出退出码 110并在退出前通过gracefulExit执行agent.close()。Agent 机制单实例后台任务承载者单看上面的多进程方案似乎已经足够这也是 Egg 之前在生产环境使用的方案。但很快发现有些工作其实不应该由每个 Worker 都做一遍否则既浪费资源还可能造成多进程间的资源访问冲突。例如生产环境按日期归档日志文件在单进程模型下很容易实现凌晨 0 点把当前日志文件按日期重命名销毁旧文件句柄创建新日志文件并继续写入。试想如果有 4 个进程同时做这件事场面会非常混乱。因此对于这类后台逻辑我们希望它只运行在单个进程上这个进程被称为Agent Worker简称Agent。Agent 相当于 Master 为其他 Worker 引入的秘书它不对外提供服务只服务于 App Worker专门处理公共事务。引入 Agent 后的多进程模型如下-------- ------- | Master |--------| Agent | -------- ------- ^ ^ ^ / | \ / | \ / | \ v v v ---------- ---------- ---------- | Worker 1 | | Worker 2 | | Worker 3 | ---------- ---------- ----------框架的启动时序如下--------- --------- --------- | Master | | Agent | | Worker | --------- -------- -------- | fork agent | | --------------------| | | agent ready | | |-------------------- | | | fork worker | -----------------------------------------| | worker ready | | |----------------------------------------- | Egg ready | | --------------------| | | Egg ready | | -----------------------------------------|Master 启动后先 fork Agent 进程Agent 初始化成功后通过 IPC 通道通知 MasterMaster 再 fork 多个 App WorkerApp Worker 初始化成功后通知 Master当所有进程都初始化成功后Master 通知 Agent 和 Worker应用启动成功。这段时序与 packages/cluster/src/master.ts 中的代码完全对应this.once(agent-start, this.forkAppWorkers.bind(this))—— 只有当 Agent 启动成功agent-start事件触发后Master 才去 fork App Worker。Agent 进程由 packages/cluster/src/agent_worker.ts 启动它加载框架的Agent类对应 packages/egg/src/lib/agent.tsready 后通过{ action: agent-start, to: master }消息通知 Master。关于 Agent Worker 需要特别注意的几点由于 App Worker 依赖 AgentApp Worker 只能在 Agent 初始化完成后才能被 fork虽然 Agent 是 App Worker 的秘书但业务相关的工作不应分配给 Agent否则可能出问题考虑到 Agent 的特殊定位必须保证它相对稳定。当它抛出未捕获异常时框架不会像对待 App Worker 那样把它关闭再重启而是记录异常日志、发出告警等待人工处理Agent 上挂载的 API 与 App Worker 不同具体差异参见 Framework docs。从源码看packages/egg/src/lib/agent.ts 中 Agent 还注册了一个 keepalive 定时器防止 Agent 在没有待处理 I/O 时提前退出这进一步印证了Agent 必须长期稳定存在的设计目标。同时该文件中的_wrapMessenger会对broadcast、sendTo、sendToApp、sendToAgent、sendRandom等方法做包装在服务尚未启动完成前调用会打印agent cant call xxx before server started警告日志。Agent 的使用方式可以在应用目录或插件目录下的agent.js中实现自己的逻辑用法与自定义启动类似唯一区别是入口参数是agent对象// agent.js module.exports agent { // 在这里编写你的初始化逻辑 // 也可以使用 messenger 对象向 App Worker 发送消息 // 但必须等 App Worker 启动成功后再发否则消息可能丢失 agent.messenger.on(egg-ready, () { const data { ... }; agent.messenger.sendToApp(xxx_action, data); }); };// app.js module.exports (app) { app.messenger.on(xxx_action, (data) { // ... }); };在这个示例中agent.js的代码运行在 Agent 进程中app.js的代码运行在 Worker 进程中它们通过框架封装的messenger对象进行进程间通信IPC具体细节见下文。Master vs Agent vs Worker三类进程对比应用启动时框架会 fork 出 3 类进程类型进程数量职责稳定性是否运行业务代码Master1管理进程、在进程间转发消息极高否Agent1运行后台任务长连接客户端等高少Worker通常为 CPU 核心数运行业务代码一般是Master在这种模型下Master 进程承担了进程管理的工作类似 pm2但不运行任何业务代码。我们只需启动一个 Master 进程它会处理 Worker 与 Agent 进程的全部初始化和重启问题。Master 进程非常稳定。线上使用egg-scripts启动、后台使用egg.startCluster启动 Master 进程不再需要 pm2 或其他守护进程模块$ egg-scripts start --daemon在 packages/cluster/src/master.ts 中可以看到 Master 对信号的处理收到SIGINT/SIGQUIT/SIGTERM后会进入close()流程先向 App Worker 发送SIGTERM超时默认由EGG_APP_CLOSE_TIMEOUT控制兜底 5000ms再向 Agent 发送SIGTERMEGG_AGENT_CLOSE_TIMEOUT全部退出后以退出码 0 结束若 15 秒内未完成关闭则以退出码 2 强制退出。这一先杀 Worker 再杀 Agent的顺序保证了关闭过程的确定性。Agent多数情况下写业务代码时不需要关心 Agent 进程但在少数场景下我们希望代码运行在单进程中这时就轮到 Agent 出场了。由于 Agent 只有一个负责保持连接这类繁琐辛苦的工作它不能被随意挂掉或重启。Agent 进程遇到未捕获异常时不会退出而是输出错误日志所以我们要时刻关注日志中的未捕获异常。WorkerWorker 进程实际承担用户请求和定时任务。Egg 提供的定时任务具备只在某一个 Worker 进程中运行的能力所以凡是能用定时任务解决的问题绝不要用 Agent 去解决。Worker 运行的是业务代码比 Agent 和 Master 复杂得多稳定性也可能更低当 Worker 进程意外退出时Master 会负责重启它。进程间通信IPC虽然每个 Worker 进程独立运行但它们之间仍有必要相互通信这就是进程间通信IPC。Node.js 官方示例如下use strict; const cluster require(cluster); if (cluster.isMaster) { const worker cluster.fork(); worker.send(hi there); worker.on(message, (msg) { console.log(msg: ${msg} from worker#${worker.id}); }); } else if (cluster.isWorker) { process.on(message, (msg) { process.send(msg); }); }细看会发现Cluster 的 IPC 通道只存在于 Master 与 Worker/Agent 之间Worker 与 Agent 之间并没有直接通道。那么 Worker 之间如何通信答案是由 Master 帮忙转发。Broadcast messages: agent all workers -------- ------- | Master |---------| Agent | -------- ------- / | \ / | \ / | \ / | \ v v v ---------- ---------- ---------- | Worker 1 | | Worker 2 | | Worker 3 | ---------- ---------- ---------- Specify receivers: one worker another worker -------- ------- | Master |----------| Agent | -------- ------- ^ | send to / | worker 2 / | / | / v ---------- ---------- ---------- | Worker 1 | | Worker 2 | | Worker 3 | ---------- ---------- ----------为了简化调用框架封装了一个messenger对象并挂载到 app/agent 实例上同时提供了一套友好的 API。从源码看messenger 的封装位于 packages/egg/src/lib/core/messenger 目录index.ts 会根据egg.options.mode决定使用 IPC 版还是本地版single模式使用 local.ts默认多进程场景使用 ipc.ts。IPC 版 Messenger 继承自EventEmitterbase.ts在onMessage中解析收到的消息并触发对应的 action 事件所有消息通过sendmessage库经process.send发出。而 Master 侧的中转逻辑在 packages/cluster/src/utils/messenger.ts它根据to字段agent/app/master/parent把消息路由到 Agent 或对应 Worker支持通过receiverWorkerId精确投递给指定进程。发送消息app.messenger.broadcast(action, data)向所有 agent/app 进程发送消息包括自己app.messenger.sendToApp(action, data)向所有 app 进程发送消息在 app 上调用时会发给自身及其他 app 进程在 agent 上调用时会发给所有 app 进程app.messenger.sendToAgent(action, data)向 agent 进程发送消息在 app 上调用时发送给 agent 进程在 agent 上调用时发送给 agent 自身agent.messenger.sendRandom(action, data)app 没有这个方法当前 Egg 将其实现为sendToAgentagent 向某个 app 进程随机发送一条消息由 master 决定发给谁app.messenger.sendTo(pid, action, data)向指定进程发送消息。这些 API 的实现与 packages/egg/src/lib/core/messenger/ipc.ts 一一对应broadcast会同时发送to: app与to: agent两条消息sendTo会带上receiverWorkerId兼容旧的receiverPid由 Master 精确路由sendRandom会在 Master 下发的进程 pid 列表egg-pids消息维护的opids中随机挑选一个目标。// app.js module.exports (app) { // 注意只有 egg-ready 事件发生后才能发送消息 app.messenger.once(egg-ready, () { app.messenger.sendToAgent(agent-event, { foo: bar }); app.messenger.sendToApp(app-event, { foo: bar }); }); };上面所有在app.messenger上调用的方法都可以在agent.messenger上调用。egg-ready事件上面的示例提到只有egg-ready事件发生后才能发送消息。这是因为只有 Master 确认所有 Agent 进程和 Worker 进程都已成功启动ready后才会通过 messenger 向所有 Agent 和 Worker 发送egg-ready消息通知一切就绪、IPC 通道可以使用。在 packages/cluster/src/master.ts 中可以看到Master 的ready()回调里会向parent、app、agent三个方向同时发送egg-ready消息而 packages/egg/src/lib/start.ts 显示在单进程single模式下则由应用自身application.messenger.broadcast(egg-ready)广播。packages/egg/src/lib/egg.ts 也通过this.messenger.once(egg-ready, ...)在应用侧监听该事件。接收消息在 messenger 上监听 action 事件即可收到其他进程发来的消息。app.messenger.on(action, (data) { // 处理数据 }); app.messenger.once(action, (data) { // 处理数据 });在 agent 中使用 messenger 接收消息的方式与 app 相同。IPC 实战内存缓存的三方案更新下面通过一个简单例子展示 IPC 如何配合框架的多进程模型解决实际问题。需求我们有一个 API需要从远端数据源拉取数据对外提供服务。由于数据源的数据变化不大我们希望把它缓存在内存中以加快服务响应、降低 RT。现在需要一个更新内存缓存的机制。定期从远端数据源拉取数据并更新内存缓存。为减轻数据源压力更新周期可以设得比较长远端数据源提供一个检查数据是否更新的 API。我们的服务更频繁地调用该 API仅在数据有更新时才拉取数据远端数据源通过消息中间件推送数据变更我们的服务监听消息来更新数据。真实项目中我们以方案一兜底再结合方案二或三来加快数据更新的实时性。下面的例子用 IPC 定时任务 同时实现这三种缓存更新方案。实现把与远端数据源交互的逻辑都放进一个 Service对外暴露get方法供 Controller 调用// app/service/source.js let memoryCache {}; class SourceService extends Service { get(key) { return memoryCache[key]; } async checkUpdate() { // 检查远端数据源是否变化 const updated await mockCheck(); this.ctx.logger.info(check update response %s, updated); return updated; } async update() { // 从远端更新内存缓存 memoryCache await mockFetch(); this.ctx.logger.info(update memory cache from remote: %j, memoryCache); } }编写定时任务实现方案一每 10 分钟从远端数据源拉取一次数据更新缓存作为兜底。// app/schedule/force_refresh.js exports.schedule { interval: 10m, type: all, // run in all workers }; exports.task async (ctx) { await ctx.service.source.update(); ctx.app.lastUpdateBy force; };再编写一个定时任务实现方案二的检查逻辑让一个 Worker 每 10 秒调用一次检查 API发现数据变化后用 messenger 的方法通知所有 Worker。// app/schedule/pull_refresh.js exports.schedule { interval: 10s, type: worker, // only run in one worker }; exports.task async (ctx) { const needRefresh await ctx.service.source.checkUpdate(); if (!needRefresh) return; // 通知所有 worker 从远端更新内存缓存 ctx.app.messenger.sendToApp(refresh, pull); };在自定义启动文件中监听refresh事件并更新数据。所有 Worker 进程都会收到这条消息、触发更新方案二就此实现。// app.js module.exports (app) { app.messenger.on(refresh, (by) { app.logger.info(start update by %s, by); // 创建一个匿名 context 以访问 service const ctx app.createAnonymousContext(); ctx.runInBackground(async () { await ctx.service.source.update(); app.lastUpdateBy by; }); }); };再来看方案三如何实现。我们需要一个与消息中间件保持长连接的客户端。这类长连接非常适合放在 Agent 进程中维护能有效减少连接数、降低两端成本。因此我们把消息监听放在 Agent 进程上。// agent.js const Subscriber require(./lib/subscriber); module.exports (agent) { const subscriber new Subscriber(); // 监听 changed 事件广播给所有 worker subscriber.on(changed, () agent.messenger.sendToApp(refresh, push)); };通过巧妙地组合使用 Agent 进程、定时任务与 IPC我们可以轻松实现这类需求并减轻数据源的压力。结合源码看消息流转这条refresh消息的完整链路是Workerpull_refresh 定时任务→sendToApp发出{ action: refresh, to: app }→ Master 侧 packages/cluster/src/utils/messenger.ts 的sendToAppWorker遍历workerManager中所有未断开的 Worker 并逐个worker.send(data)→ 各 Worker 内 ipc.ts 的onMessage解析出action refresh并emit(refresh, data)→ 触发app.js中注册的监听回调。Agent 侧sendToApp的路径类似只是消息先经to: agent路由到 Agent 进程再转发给 Master 分发到所有 app Worker。至此三种缓存更新方案的完整数据流便串联起来了。更复杂的场景上面的例子中我们在 Agent 进程上运行了一个 subscriber 来监听消息中间件的消息。如果 Worker 进程也需要监听消息呢如何由 Agent 进程创建连接、再转发消息给 Worker 进程这些问题的答案可以在多进程开发进阶模式中找到。总结Egg 的多进程模型可以概括为一个稳定的 Master 负责调度一个专职的 Agent 承载单实例后台任务一组 Worker 处理业务。掌握以下要点即可在生产中游刃有余Worker 负责业务异常退出由 Master 自动补足Agent 负责长连接等公共事务异常时只记录日志、等待人工处理所有跨进程消息都经由 Master 中转Worker 与 Agent 之间没有直连通道messenger提供broadcast、sendToApp、sendToAgent、sendTo、sendRandom五类发送 API发送前务必等待egg-ready定时任务的type: worker天然只在一个 Worker 中运行能替代 Agent 完成的任务优先交给定时任务涉及长连接、需在 Agent 建连再转发给 Worker 的复杂模式继续阅读 cluster-client 文档。赞分享后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载相关推荐Vim插件备份恢复终极指南Vundle.vim确保配置永不丢失Vim插件备份恢复终极指南Vundle.vim确保配置永不丢失 Vundle.vim作为Vim的插件管理器不仅能帮助用户轻松管理各类插件更能通过简单的配置后端Web框架i茅台自动预约落地完整指南从账号池到门店匹配Docker 一键部署i茅台自动预约落地完整指南从账号池到门店匹配Docker 一键部署 每天 9:00i茅台申购通道开启热门品类的额度在头几秒内被消耗殆尽。对同时管理 10后端Web框架Egg 框架 FAQ 实战指南反馈、排障与多进程原理深度解析Egg 框架 FAQ 实战指南反馈、排障与多进程原理深度解析 本指南以 Egg 官方社区 FAQ site/docs/community/faq.md ht后端Web框架上一篇UI-TARS 坐标定位总打偏3 步找到根源并完成修复下一篇aligo常见问题解答解决你使用过程中的99%问题创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考