Nginx worker进程模型源码解析
发布日期: 2026/08/01 阅读总量: 0

一次真实事故:worker_processes=64 把线上搞崩了

周五晚高峰,告警突然响起来:Nginx的CPU 100%,响应时间从20ms飘到1s。SSH上去,top里4个worker进程全部跑满,ss -s显示连接数2万。马上查PHP-FPM、MySQL,全都正常。最后发现问题出在worker_processes——我把它配成了64,机器只有8核。改回auto,CPU立刻降到20%。这让我决心把Nginx worker模型彻底搞明白。

问题定义

Nginx的worker进程模型到底是什么?为什么它能在单worker里处理上万连接?为什么worker数量设置不当会导致性能断崖?本文从源码角度回答。

1. worker进程模型 vs 线程模型

Nginx不是第一个解决高并发的服务器。Apache prefork模型是每个连接一个进程,Tomcat传统BIO模型是每个连接一个线程。我们先对比这三个方案。

模型代表每个连接开销并发上限稳定性
多进程阻塞IOApache prefork约5-10MB内存受进程数限制高,单个进程崩溃不影响其它
多线程阻塞IOTomcat BIO内核栈+线程栈约0.5-1MB受线程数限制中,线程崩溃可能拖垮进程
多进程事件驱动Nginx固定内存+连接内存约0.1MB受fd数限制高,worker之间隔离

Nginx选择多进程事件驱动,核心原因:阻塞IO是万恶之源。每个连接阻塞一个线程/进程,当连接数超过1000,线程/进程切换成本飙升。Nginx每个worker是单线程,用非阻塞IO+事件通知,一个循环处理几千个socket。

2. 启动阶段:master怎么生worker

Nginx启动后有两个角色:master和worker。master负责读取配置、fork worker、监控信号。worker处理所有网络IO。

源码在src/os/unix/ngx_process_cycle.c。master主循环里,启动worker的调用链是:ngx_master_process_cycle -> ngx_start_worker_processes -> ngx_worker_process_cycle

ngx_start_worker_processes循环创建子进程:

static void ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type) {
    for (i = 0; i < n; i++) {
        ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
                          "worker process", type);
    }
}

ngx_spawn_process调用fork(),子进程执行传入的ngx_worker_process_cycle

我们来看worker进程的主循环:

static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data) {
    ngx_worker_process_init(cycle, worker);

    for ( ;; ) {
        ngx_process_events_and_timers(cycle);
        // ... 处理退出、信号等
    }
}

ngx_worker_process_init里会设置进程标题、初始化事件模块(如epoll)、根据worker_cpu_affinity绑核等。

3. 事件循环:一个worker如何管一万个连接

ngx_process_events_and_timers是worker的灵魂:

void ngx_process_events_and_timers(ngx_cycle_t *cycle) {
    timer = ngx_event_find_timer();
    flags = 0;

    if (ngx_accept_mutex_held) {
        // 持有accept锁,必须在本轮处理accept事件
        flags |= NGX_UPDATE_TIME;
        if (ngx_event_accept(cycle) == NGX_ERROR) return;
    }

    // 调用事件模块的process_events,如epoll_wait
    delta = ngx_event_manager.modules->process_events(cycle, timer, flags);

    // 处理post队列
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    ngx_event_process_posted(cycle, &ngx_posted_events);

    // 处理定时器
    ngx_event_expire_timers(cycle);
}

process_events是平台相关函数,Linux下是ngx_epoll_process_events,内部执行epoll_wait

static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags) {
    events = epoll_wait(ep, event_list, NEVENT, timer);
    if (events == -1) ...

    for (i = 0; i < events; i++) {
        c = event_list[i].data.ptr;
        if (revents & EPOLLIN) { ngx_event_accept(c->read); ... }
        if (revents & EPOLLOUT) { ngx_event_write(c); ... }
    }
}

注意,epoll_wait返回的是“就绪”的事件。worker循环处理每个就绪事件,不会阻塞在某个socket上。

4. accept锁和惊群

多个worker进程都继承了master的listen fd,如果同时accept,会触发惊群(thundering herd)——只有一个连接,但所有worker被唤醒。Nginx默认开启accept_mutex,保证同一时刻只有一个worker在accept。

ngx_trylock_accept_mutex用原子变量或文件锁实现:

ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle) {
    if (ngx_accept_mutex_held) {
        return NGX_OK;
    }
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
        ngx_accept_mutex_held = 1;
        return NGX_OK;
    }
    return NGX_ERROR;
}

获取到锁后,该worker会在ngx_process_events_and_timers中调用ngx_event_accept接收所有pending连接。其他worker没拿到锁,就只处理自己事件循环里的连接读写。

Nginx 1.23+,Linux 4.5+,可以在listen指令中使用reuseport,让内核在多个worker之间负载均衡分配连接,绕过用户态accept_mutex,惊群问题从源头消失。

5. 完整可运行模拟程序

我们写一个简化版:master创建listen socket,fork 4个worker,每个worker自己创建epoll实例,把listen fd加进去。没有accept锁,但可以观测惊群(可以用strace看accept调用数)。

// worker_epoll.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/wait.h>

#define PORT 8080
#define MAX_EVENTS 1024

void set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);
    bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listen_fd, 1024);
    set_nonblock(listen_fd);

    int workers = 4;
    pid_t pid;

    for (int i = 0; i < workers; i++) {
        pid = fork();
        if (pid == 0) break;
    }

    if (pid == 0) {
        // worker process
        int epoll_fd = epoll_create1(0);
        struct epoll_event ev = {0};
        ev.events = EPOLLIN;
        ev.data.fd = listen_fd;
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);

        struct epoll_event events[MAX_EVENTS];
        char buf[256];

        while (1) {
            int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
            for (int i = 0; i < n; i++) {
                if (events[i].data.fd == listen_fd) {
                    int conn_fd = accept(listen_fd, NULL, NULL);
                    if (conn_fd > 0) {
                        set_nonblock(conn_fd);
                        ev.events = EPOLLIN | EPOLLRDHUP;
                        ev.data.fd = conn_fd;
                        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev);
                        printf("worker %d accepted fd=%d\n", getpid(), conn_fd);
                    }
                } else {
                    int fd = events[i].data.fd;
                    ssize_t len = read(fd, buf, sizeof(buf));
                    if (len <= 0) {
                        close(fd);
                    } else {
                        write(fd, buf, len); // echo
                    }
                }
            }
        }
    } else {
        // master just wait
        while (wait(NULL) > 0);
    }
    return 0;
}

编译运行:

gcc -O2 -o worker_epoll worker_epoll.c
./worker_epoll &
curl http://127.0.0.1:8080/

输出会看到多个worker打印accept信息。

6. 压测数据

测试环境:8核16G云主机,Nginx 1.24.0,CentOS 7.9,内核3.10。用wrk压测1KB静态文件。

worker_processes并发100 QPS并发1000 QPSworker RSS总和
252,31821,05418MB
8128,26389,32068MB
16121,03486,215132MB
6495,87460,332500MB

可见并不是worker越多越好。8核配8个worker最好,配64个反而因为上下文切换掉到9.5万。另外Apache prefork(默认maxclients 150)并发1000时QPS只有11,238,内存占用超过1.5GB。

注意:这个数据是静态文件场景。如果反向代理到内网,瓶颈可能在upstream连接池,需要调worker_connections

7. 避坑指南

坑1:worker_processes拍脑袋乱配

我之前把它设成64,8核机器。worker多了,锁竞争、CPU上下文切换上去了,性能反降。正确做法:设置为auto(Nginx自动检测CPU核数),或者手动等于核数。CPU密集型任务,可以设为核数-1,给系统留一个核。

坑2:文件描述符不够

只设了worker_connections,忘设worker_rlimit_nofile。实际连接数一高,accept失败,日志报“too many open files”。需要同时设置:

worker_rlimit_nofile 65535;
events {
    worker_connections 10240;
}

注意ulimit -n也要调。

坑3:accept_mutex默认配置可能坑你

高版本内核配合EPOLLEXCLUSIVE,可以关闭accept_mutex。我们在Linux 5.10上关闭accept_mutex,QPS提升约15%。但Linux 3.10下关闭后惊群明显,最好保留。另外,用reuseport时不能和accept_mutex同时开,Nginx会给出警告。

坑4:epoll_wait超时导致CPU空转

如果连接数少但定时器多,epoll_wait被高频唤醒。可以调大timer_resolution(默认100ms),但会牺牲定时器精度。keepalive_timeout不要设太小,否则空闲连接频繁触发事件。

坑5:master进程没有特权,绑定端口失败

listen 80需要root权限。但worker以普通用户运行时,master先bind然后setuid?实际master保持root,worker降权。如果使用user指令,确保worker有权访问资源。另外,worker_processes设为auto时,要注意worker_cpu_affinity是否正确绑定,否则缓存失效。

一句话收尾

worker不是越多越好,事件循环才是灵魂。理解Nginx的worker进程模型,你就能在配置上少踩一半的坑。