Egg框架深入
egg启动自定义https://eggjs.org/zh-CN/basics/app-start多进程模型和进程间通讯来源https://eggjs.org/zh-CN/core/cluster-and-ipc进程 vs 线程先厘清操作系统层面的通用关系这是理解 Egg 多进程模型的基础。进程process一次程序运行的实例是操作系统资源分配的单位。每个进程有独立的内存空间堆、栈、代码段进程之间内存隔离A 进程改不了 B 进程的变量。线程thread进程内的执行单位是 CPU调度的单位。一个进程里可以有多个线程它们共享同一份进程内存。一句话进程是容器线程是容器里干活的人。一个进程至少有 1 个线程主线程线程活在进程里不能脱离进程存在。进程 A独立内存 进程 B独立内存和 A 隔离 ├─ 线程1共享 A 的内存 ├─ 线程1 ├─ 线程2共享 A 的内存 └─ 线程2 └─ 线程3维度进程线程是什么的单位资源分配CPU 调度内存各自独立、隔离同进程内共享崩溃影响进程间互不影响可能拖垮整个进程Egg 里的对应Master / Agent / Worker 都是进程worker 内部的主线程 libuv/V8 后台线程关键结论Egg 扩展多核用的是多进程不是多线程。因此进程之间内存不共享——这也是为什么SingletonProto单例是每个 worker 进程一份而非全局唯一。进程 vs 程序程序你写的代码。进程 一个正在运行的程序实例OS 为它分配独立内存 一整套隔离资源并且里面有线程在执行指令。一个更贴切的类比程序 菜谱纸上的静态步骤进程 按菜谱正在炒的这道菜——占了灶台、锅、食材资源含内存而且有厨师在动手线程在执行程序实例是什么程序实例 程序被运行起来的那一份运行体。用类 vs 对象类比最贴切程序 类class硬盘上静态的可执行文件node、app.js是模板。实例 对象类被new出来后内存里那个活的东西即运行中的进程。核心在实例二字同一个程序可以同时跑出多个互相独立的实例跑 3 次node app.js程序只有一个那个文件却有 3 个运行实例各有各的 PID 和独立内存互不干扰。Egg 的多进程模型Master、Agent 和 WorkerEgg 启动后是一组进程而非一堆线程Master 进程1 个总管 ├── Agent 进程1 个 └── Worker 进程N 个处理 HTTP 请求Master守护进程不处理请求。worker 崩了它负责拉起来做优雅重启。Agent一个特殊进程用来做那些多个 worker 都需要、但只该执行一份的事比如定时任务避免 N 个 worker 重复跑。它全局只有 1 个且不监听端口、不处理外部请求。典型职责定时任务schedule、唯一长连接 / 订阅、监听配置中心变更等。自定义 Agent 逻辑写在应用根目录的agent.ts与 worker 的app.ts对应。Worker业务进程。默认数量 CPU 核数这样多核都能吃满。每个 worker 是独立的 OS 进程有各自独立的内存空间。请求由谁处理连接由操作系统内核 / master 分发一个请求只落到一个 worker其整个生命周期Controller → Service → 返回都在该 worker 内跑完。多 worker 的价值在于不同请求并行分摊到不同 worker而不是多个 worker 合处理同一个请求。同一 worker 内的多个请求则靠单主线程的 event loop并发非并行处理。cluster是什么前面说过一个 Node 进程的 JS 是单主线程吃不满多核。cluster 的解法是 fork 出多个 worker 进程大家共享同一个监听端口importclusterfromnode:cluster;importhttpfromnode:http;import{cpus}fromnode:os;if(cluster.isPrimary){// 主进程(master)for(leti0;icpus().length;i){cluster.fork();// ← fork 出 worker 子进程}}else{// 每个 worker 子进程http.createServer((req,res)res.end(hi)).listen(3000);// 多个 worker 都 listen(3000),不会端口冲突}关键点fork()由主进程调用派生出子进程这就是 Master fork 出 worker 的底层。多个 worker 监听同一端口不冲突正常情况下,一个端口只能被一个进程绑定监听(否则报 EADDRINUSE 端口占用错误)。那 Egg 有多个worker它们怎么能 都监听 3000 端口 还不冲突答案其实不是每个 worker 各自独立去 listen(3000),而是 master 统一持有那个监听 socket再把连接分给 worker。这就是一个请求只落到一个 worker 的机制来源。worker 崩了master 能收到exit事件再fork()一个补上 → 进程守护。Egg 的 Master/Worker 模型本质就是在 cluster 之上封装的还加了 Agent 进程、优雅重启、日志切割等。所以你可以理解为cluster 是地基Egg 的多进程是盖在上面的房子。对主进程的理解主进程这个词有两层含义① 操作系统层面任何一个node xxx.js启动的进程它自己就是一个进程。你在终端敲node app.js操作系统就创建了一个进程来跑它。这个进程有没有主/从之分取决于你代码里用不用 cluster。② cluster 语境下主进程primary/master是相对子进程worker而言的角色。只有当你调用了cluster.fork()才会分裂出主进程 子进程的父子关系。这时最初那个进程 主进程primary负责 fork 和管理被 fork 出来的 子进程worker。cluster.isPrimary这个判断就是用来区分我现在是不是那个最初的主进程。推论如果代码里没有任何cluster.fork()直接http.createServer(...).listen(3000)那就是一个普通单进程程序——它是进程操作系统层面但不存在主/从之分所有请求由这唯一进程的单主线程处理。主进程这个称呼要等有了 fork、有了子进程才成立。Node.js 的单线程模型Node 的 JS 代码默认单线程执行但进程内有底层多线程libuv / V8在支撑异步而且需要时可以用worker_threads显式开真线程做并行计算。JS 代码跑在单个主线程上基于事件循环 event loop。一个 Node 进程内部的线程构成Node 进程1 个独立内存 ├─ 主线程 ← 跑你的 JS、event loop。业务代码只在这根线程上执行 ├─ libuv 线程池 ← 默认 4 根,后台干文件 I/O、DNS、crypto 等阻塞活 ├─ V8 后台线程 ← GC、JIT 编译 └─可选) worker_threads 开的线程 ← 显式开来做 CPU 密集计算所以 Node 是单线程指的是主线程JS 执行单线程不是整个进程只有一根线程。⚠️ 别把worker_threads同进程内的线程用于 CPU 密集计算和 Egg 的worker独立进程用 cluster fork搞混——名字都叫 worker一个是线程、一个是进程。Node 单线程事件循环 vs Java 多线程Node和Java的设计哲学不同——Node默认用单线程 事件循环处理业务靠异步非阻塞而非多线程来抗并发。为什么 Node 敢用单线程这就要区分IO密集型还是CPU密集型任务了。Web 业务大多是 IO 密集型(查DB、调接口、读文件)CPU大部分时间在等而不是在算。用咖啡店例子来理解来了 3 个顾客各要一杯手冲咖啡每杯要等 5 分钟滴滤——这就是「IO 等待」。Java(多线程 多个店员)每来一个顾客雇一个专属店员,店员在等咖啡滴滤时干站着(线程阻塞)。3 杯 5 分钟搞定,但雇了 3 个店员;来 1000 个顾客就得雇 1000 个(线程爆炸)。Node(单线程 一个不干等的店员)全店 1 个店员,接单后立刻架上水壶就转身接下一个,哪壶好了「叮」一声再去交付。3 杯也是约 5 分钟,却只用 1 个店员;来 1000 个也是这 1 个。Java:靠「多雇人」同时处理任务,每人可以在自己的活上干等。Node:靠「一个人不干等、快速切换」,把耗时等待(滴滤 / IO)交给水壶(操作系统)去完成。事件循环就是那个「盯着所有水壶、哪个好了就叫店员处理」的调度机制——一个永不停止的循环,不停问「有没有完成的事要处理?有就执行对应回调」:console.log(1. 接单);setTimeout(()console.log(3. 叮!咖啡好了),5000);// 只登记回调,不卡住console.log(2. 转身接下一个);// 输出:1 → 2(立刻)→ 3(5秒后事件循环捞回回调才执行)真正的等待不是店员在等是水壶在等——即操作系统和底层libuv 线程池在幕后完成 IO主线程只负责派活和处理结果。弱点若店员要「亲手磨豆 1 小时」(CPU 密集),没法交给水壶,只能埋头磨,期间全店卡死。补救就是前文的worker_threads(真线程)和 cluster / 多进程。补充知识点IPC 通信IPC Inter-Process Communication进程间通信。由于进程之间内存隔离Worker#1 改不了 Worker#2 的变量。它们要交换信息就靠 IPC。在 Node/cluster 里IPC 主要通过process.send()/message事件这条消息管道实现——fork出子进程时父子之间自动建立了一条通信通道// 主进程 → 子进程// ↓↓↓ 这两行在【主进程 master】里执行constworkercluster.fork();// 变量 worker 本身在主进程里,它不是子进程,而是主进程手里握着的「子进程的遥控器/句柄」worker.send({type:reload-config,data:{}});// 子进程接收// ↓↓↓ 这段在【子进程 worker】里执行process.on(message,(msg){if(msg.typereload-config){/* 处理 */}});特点传的是消息会被序列化成结构化拷贝不是共享内存。发过去的是数据的拷贝不是同一个对象引用。双向父子都能send和监听message。在 Egg 里Agent 和 Worker 不直接通信消息都经 Master 转发Master 是 IPC 的中枢。Egg 在 cluster 的 IPC 之上封装了更好用的 API比如app.messenger// 某个进程广播消息给所有进程app.messenger.broadcast(some-event,data);// 别的进程监听app.messenger.on(some-event,(data){/* ... */});这就是 Agent 进程跑完定时任务 / 拿到配置变更后通知所有 worker 的实现方式。┌─────────── Master ───────────┐ │ fork 守护 消息中转 │ └───┬──────────────────┬────────┘ │ IPC │ IPC ┌───▼───┐ ┌─────▼─────┬──────────┐ │ Agent │◄──────►│ Worker#1 │ Worker#2 │ ... └───────┘ 经Master└───────────┴──────────┘ (1个,后台) 转发消息 (N个,处理请求)CPU 核数和进程数量关系进程数 ≠ 核数进程和核不是一对一。关键点一个 CPU 核可以跑很多个进程。操作系统的调度器会在多个进程之间快速轮流切换时间片轮转让它们看起来同时在跑。所以你可以在 12 核机器上 fork 出 4 个、12 个、100 个 worker 都行不会报错。核数只是决定了同一瞬间最多有几个进程真正在并行执行的上限12 核 → 同一时刻最多 12 个进程 / 线程真正并行超出的部分靠调度器切换。为什么 worker 数 ≈ 核数是常见推荐因为 Node 的 worker 是独立进程、JS 单主线程worker 数 核数有核闲着浪费。worker 数 核数每个核大致对应一个 worker并行度拉满是对 CPU 密集型较优的选择。worker 数 核数进程比核多得靠切换分时上下文切换开销上升但对 I/O 密集型大部分时间在等数据库 / 网络CPU 空闲有时略超核数也无妨。它是经验推荐值不是硬限制。此外cluster 的主进程几乎不占 CPU只管 fork 和守护所以常见 / Egg 默认策略是worker 数 核数而非核数 - 1无需专门留一个核给主进程。本项目实例config.workers 1——即便在多核机器上也只 fork 1 个 worker。这是主动放弃多核并行换取SingletonProto单例全局唯一的语义历史上曾有进程内会话状态多 worker 会导致请求落到不同 worker 拿不到同一份内存。可见进程数完全由配置决定与核数无关。

相关新闻