深蓝黑背景封面:右侧大标题「master 发号,worker 干活」,副标题「管理面与数据面分离的进程设计」;左侧原创示意图——上方一个 master 进程方框,一手连着配置文件、一手向下发出信号箭头;下方三个 worker 进程方框并排,各自内部画一个事件循环小环,左侧大量连接箭头汇入 worker 层;左上标注「面试经典 · 系统设计」,右上标注「Day 04 · 11-01」 图 1:本文封面,一个 master 发号施令,一群 worker 埋头干活。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。题面是「解释 Nginx 服务器上的 Master 和 Worker 进程分别是什么」。官方文档的原话可以作为骨架:Nginx 有一个 master 进程和若干 worker 进程,master 的职责是读取并评估配置、维护 worker 进程,worker 做实际的请求处理。答题的关键是把「为什么这样分」讲出来。

master:控制面,不碰连接。

master 干的是管理活,按官方文档可列成四类:读取并校验配置;按配置拉起、监控、回收 worker;接收管理员信号并翻译成对 worker 的动作;在 worker 异常退出后补拉新的。它自己不处理任何用户连接——你在压测时打满的永远是 worker。

信号机制是 master 的工作界面。管理员发给 master 的常用信号有:TERM/INT 快速停止;QUIT 优雅停止;HUP 重新加载配置;USR1 重开日志文件(日志切割就靠它);USR2 平滑升级可执行文件;WINCH 优雅关闭 worker。以 HUP 为例,master 收到后先做新配置的语法校验,校验失败就回滚、老配置继续跑、worker 一个不动;校验通过才按新配置拉起一批新 worker,同时通知旧 worker 优雅退出——旧 worker 关掉监听 socket、不再接新连接,把手上存量请求服务完再退出。整个 reload 过程对外零断连,这就是「改配置不掉线」的机制来源。

worker:数据面,各自为战。

每个 worker 是独立进程,跑自己的事件循环(epoll 等系统机制),争抢新连接、处理请求、转发上游。worker 之间不共享内存,一个 worker 崩溃不会污染其他 worker,master 发现缺口后补拉一个即可。worker 数量由配置指定,常见做法是设为 CPU 核数(也可以配成 auto 自动按核数调整),让每个 worker 钉在一个核上,避免进程间切换的浪费。

为什么这么分?三个动机。

第一是权限与全局视角:读配置、绑定特权端口、写 pid 文件这些操作需要特权和全局状态,集中在单个 master 里做,worker 就可以用非特权用户跑。第二是水平扩展与故障隔离:数据面按进程复制,天然利用多核;进程隔离让单个 worker 的崩溃只是局部事件。第三是管理操作不打断服务:reload、日志切割、平滑升级都发生在控制面,通过信号翻译给数据面逐步执行,用户无感。如果管理面与数据面混在一个进程里,改一次配置就得断一轮连接。

用一段模拟把 master 的三类动作跑出来。

下面这段代码模拟 master/worker 的信号语义:worker 崩溃补拉、HUP 换配置的「校验-拉新-优雅退旧」、配置失败回滚。用 Node.js 22.15.0 直接运行(node master-worker.js):

'use strict';

// Nginx master/worker 信号语义的最小模拟:master 管配置与工人,worker 管连接
let nextPid = 100;
class Worker {
  constructor(config) { this.pid = nextPid++; this.config = config; this.connections = 3; this.draining = false; }
  tick(log) {
    if (this.draining && --this.connections <= 0) return false; // 优雅退出:存量请求服务完才走
    return true;
  }
}
class Master {
  constructor(config) { this.config = config; this.workers = [new Worker(config), new Worker(config)]; }
  signal(sig, log, payload) {
    if (sig === 'HUP') {
      if (!payload.valid) { log.push('HUP:新配置语法校验失败,回滚,worker 不动'); return; }
      log.push('HUP:校验通过,按新配置拉起新 worker');
      const fresh = [new Worker(payload.config), new Worker(payload.config)];
      for (const w of this.workers) w.draining = true; // 通知旧 worker 优雅退出
      this.workers.push(...fresh);
      this.config = payload.config;
    }
    if (sig === 'QUIT') { for (const w of this.workers) w.draining = true; log.push('QUIT:全体优雅退出中'); }
  }
  tick(log) {
    const alive = this.workers.filter((w) => w.tick(log));
    if (alive.length !== this.workers.length) log.push('旧 worker 存量连接服务完毕,退出');
    this.workers = alive;
  }
  crashOne(log) {
    const victim = this.workers.shift();
    log.push(`worker ${victim.pid} 崩溃`);
    this.workers.push(new Worker(this.config));
    log.push('master 检测到缺口,立即拉起新 worker');
  }
}

const log = [];
const master = new Master('v1');
master.crashOne(log);
master.signal('HUP', log, { valid: true, config: 'v2' });
for (let i = 0; i < 3; i++) master.tick(log);
master.signal('HUP', log, { valid: false, config: 'broken' });
log.push(`当前 worker 数 ${master.workers.length},配置版本 ${master.config}`);
for (const line of log) console.log(line);

实测输出:

worker 100 崩溃
master 检测到缺口,立即拉起新 worker
HUP:校验通过,按新配置拉起新 worker
旧 worker 存量连接服务完毕,退出
HUP:新配置语法校验失败,回滚,worker 不动
当前 worker 数 2,配置版本 v2

五行输出对应 master 的三类动作:崩溃补拉、HUP 的「先验后换」、失败回滚。注意中间几次 tick 里旧 worker 是排干存量连接后才退出的——优雅退出的成本由旧 worker 自己承担,新请求已经在走新 worker 了。

左右分层图:左栏 master 控制面——读取并校验配置、按配置拉起与回收 worker、接收管理员信号(重载配置、平滑退出、重开日志),自己不碰任何用户连接;右栏 worker 数据面——多 worker 各自跑独立事件循环,争抢新连接、处理请求与转发,进程间不共享内存、互不阻塞。中部信号通道:管理员命令发给 master,master 翻译后对 worker 发信号。底部两张卡:左「为什么这么分」——管理操作要特权与全局视角,数据处理要水平扩展与故障隔离,混在一起则改配置就得断连接;右「故障语义」——worker 崩溃 master 拉新的,master 崩溃 worker 继续服务存量连接但失去管理 图 2:控制面与数据面的职责划分及故障语义。

可能的追问。

「master 挂了会怎样?」worker 还能继续服务存量连接,但失去了管理面:没有进程补拉、没有 reload、日志切割停摆,应尽快恢复 master。「为什么用多进程而不是多线程?」进程隔离换来的是崩溃不扩散与地址空间独立,配合事件驱动,单 worker 内部根本不需要多线程的复杂度。「这套范式别处有吗?」遍地都是:PostgreSQL 的 postmaster 加后端进程、各类推理服务的 supervisor 加 worker,都是同一个「管理面与数据面分离」的同构设计。

回到面试现场,建议的答题结构:先用官方定义一句话分清两者职责,再展开 master 的四类管理动作与信号列表(HUP 的「校验-拉新-退旧」流程是加分项),然后讲 worker 的事件循环与进程隔离,最后收在三个分工动机上。你在线上 reload 或升级过 Nginx 吗?有没有遇到过旧 worker 迟迟不退的情况,原因是什么?欢迎聊聊。