4. TCP 端口复用实战:Nginx 多 worker 进程端口复用、Apache 事件模型、自定义服务器实现

端口复用这个词,很多人一听就觉得是「多个进程监听同一个端口」。嗯,这个说法没错,但太笼统了。真正落地的时候,你会发现不同服务器实现方式完全不同,性能表现也天差地别。

我这些年调过不少高并发服务,踩过最多的坑就是端口复用配置不当导致的惊群效应。今天咱们就把 Nginx、Apache 和自定义服务器这三种典型实现掰开揉碎了讲清楚。

4.1 Nginx 多 worker 进程的端口复用

Nginx 的架构设计很有意思。它有一个 master 进程和多个 worker 进程。所有 worker 进程共享同一个监听套接字。这是怎么做到的?

核心就三个字:SO_REUSEPORT。

关键机制:Nginx 在 1.9.1 版本之后引入了 reuseport 参数。每个 worker 进程独立创建监听 socket,内核负责将新连接负载均衡到各个 worker 的 socket 上。

配置起来非常简单,在 listen 指令后面加上 reuseport 就行:

http {
    server {
        listen 80 reuseport;
        server_name example.com;
        ...
    }
}

我个人习惯在生产环境里配合 worker 数量一起调优。比如 4 核机器,我会这样配:

worker_processes 4;
events {
    worker_connections 10240;
    use epoll;
    multi_accept on;
}

这里有个细节要注意。我遇到过不少团队把 worker_connections 设得特别大,觉得越大越好。其实不然。每个 worker 能处理的并发连接数受限于系统文件描述符上限。你想想看,如果 ulimit -n 只有 1024,你配 65535 也没用。

调优建议:worker_connections 建议设为 ulimit -n 的 80% 左右。留点余量给其他系统操作。

4.2 惊群效应与内核负载均衡

没有 reuseport 的时候,Nginx 是怎么工作的?所有 worker 进程共享一个监听 socket。新连接到达时,内核会唤醒所有在 epoll_wait 上等待的 worker。但最终只有一个 worker 能成功 accept,其他 worker 白忙一场。

这就是经典的惊群效应(Thundering Herd)。

我曾经在一个日活千万的广告系统上做过对比测试。没有 reuseport 时,CPU 上下文切换每秒高达 8 万次。开启 reuseport 后,直接降到 1.2 万次。差距就是这么明显。

为什么会这样?因为 reuseport 让内核在 TCP 层就完成了负载均衡。内核维护一个哈希表,根据源 IP、源端口、目标 IP、目标端口四元组计算哈希值,然后分配到对应的 worker socket 上。同一个连接始终落在同一个 worker 上,CPU 缓存命中率也更高。

注意:reuseport 虽然好,但有个潜在问题——连接分配不均匀。如果某个 worker 处理慢,新连接还是会继续分配给它。Nginx 官方建议配合 worker_shutdown_timeout 使用,让慢 worker 优雅退出。

4.3 Apache 事件模型(MPM Event)

Apache 的端口复用思路和 Nginx 完全不同。Apache 用的是预派生(Prefork)或工作者(Worker)模型,每个进程或线程处理一个连接。事件模型(MPM Event)算是它的一个重大改进。

MPM Event 的核心思想是:把监听和处理的职责分开。

具体来说,Apache 有一个专门的监听线程(Listener),负责 accept 新连接。然后它把连接交给工作线程池处理。工作线程处理完请求后,监听线程继续复用这个连接处理下一个请求。

配置示例:

<IfModule mpm_event_module>
    StartServers             3
    MinSpareThreads         75
    MaxSpareThreads        250
    ThreadsPerChild         25
    MaxRequestWorkers      400
    MaxConnectionsPerChild 1000
</IfModule>

这里我特别想强调 MaxRequestWorkers 这个参数。它决定了 Apache 能同时处理的最大请求数。我见过有人把它设成 10000,结果内存直接爆了。为什么?因为每个工作线程都要占用一定的栈空间和内存池。Apache 不像 Nginx 那样是事件驱动的,它的线程开销更大。

参数 说明 建议值
StartServers 启动时创建的子进程数 CPU 核数 × 2
MinSpareThreads 最小空闲线程数 75
MaxSpareThreads 最大空闲线程数 250
ThreadsPerChild 每个子进程的线程数 25-64
MaxRequestWorkers 最大并发请求数 根据内存计算

经验之谈:Apache 的 MPM Event 适合长连接场景,比如 WebSocket。如果是短连接为主的 API 服务,Nginx 的 reuseport 方案更高效。

4.4 自定义服务器实现端口复用

如果你需要自己写一个高性能服务器,怎么实现端口复用?我推荐两种方式:

  1. SO_REUSEPORT + 多进程:每个进程独立创建 socket,设置 SO_REUSEPORT 选项。
  2. SO_REUSEADDR + 单进程多线程:一个 socket 监听,多个线程 accept。

下面是一个 C 语言的 SO_REUSEPORT 示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define WORKER_NUM 4

void worker_process() {
    int server_fd;
    struct sockaddr_in address;
    int opt = 1;

    // 创建 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 设置 SO_REUSEPORT
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 128);

    printf("Worker %d listening on port %d\n", getpid(), PORT);

    while (1) {
        int new_socket = accept(server_fd, NULL, NULL);
        // 处理连接...
        close(new_socket);
    }
}

int main() {
    for (int i = 0; i < WORKER_NUM; i++) {
        if (fork() == 0) {
            worker_process();
            exit(0);
        }
    }
    // 父进程等待
    while (1) sleep(1);
    return 0;
}

这段代码里最关键的就是 setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, ...)。每个 worker 进程都创建自己的 socket,但都绑定到同一个 IP 和端口上。内核会自动做负载均衡。

避坑指南:我曾经在 Linux 3.9 以下的内核上尝试 SO_REUSEPORT,结果编译都过不了。因为这个选项是 3.9 才引入的。生产环境一定要确认内核版本。

4.5 三种方案的对比与选型

说了这么多,到底该用哪种?我整理了一个对比表:

方案 模型 连接分配 惊群效应 适用场景
Nginx reuseport 多进程 + 事件驱动 内核哈希 无 高并发静态/反向代理
Apache MPM Event 多进程 + 多线程 监听线程分发 有(但可控) 动态内容/长连接
自定义 SO_REUSEPORT 多进程 内核哈希 无 定制化协议/游戏服务器

我个人建议:

  • 如果是标准 Web 服务,直接用 Nginx 的 reuseport,省心又高效。
  • 如果必须用 Apache,优先选 MPM Event,别用 Prefork。
  • 如果是自定义协议或特殊需求,自己实现 SO_REUSEPORT 也不难,但要注意内核版本和负载均衡的均匀性。

下面这张图展示了三种方案的架构差异:

三种端口复用方案架构对比 Nginx reuseport Worker 1 Worker 2 Worker 3 Worker 4 内核哈希分配,无惊群 Apache MPM Event 监听线程 工作线程池 连接复用 监听/处理分离,适合长连接 自定义 SO_REUSEPORT 进程 A 进程 B 进程 C 进程 D 每个进程独立 socket,内核均衡 关键差异总结 • Nginx:多进程 + 事件驱动,内核哈希分配,无惊群,适合高并发短连接 • Apache:监听线程 + 工作线程池,连接复用,适合长连接和动态内容 • 自定义:完全控制,适合特殊协议,但需注意内核版本和负载均衡均匀性 • 性能排序(同硬件):Nginx reuseport > 自定义 SO_REUSEPORT > Apache MPM Event • 开发成本:Nginx 最低(配置即可),Apache 中等,自定义最高

最后说一句,端口复用不是银弹。如果你的业务逻辑本身就很重,比如每个请求要查数据库、调外部 API,那瓶颈根本不在端口复用上。先把业务逻辑优化好,再来考虑这些底层技巧。嗯,这个顺序别搞反了。