土法炼钢兴趣小组的算法知识备份

【网络工程】Nginx 架构深度剖析:事件模型、Worker 与 Upstream

文章导航

分类入口
network
标签入口
#nginx#reverse-proxy#event-driven#epoll#upstream

目录

Nginx 是全球使用最广泛的反向代理和 Web 服务器。截至 2024 年,它服务了互联网上超过 34% 的活跃网站。但大多数工程师对 Nginx 的理解停留在配置文件层面——知道怎么配 locationupstream,但不知道一个请求进入 Nginx 后经历了什么。

这篇文章从架构层面剖析 Nginx:它为什么快?Master/Worker 模型的设计动机是什么?epoll 在其中扮演什么角色?Upstream 连接池怎么管理?理解这些才能在生产环境中正确调优 Nginx。

一、Master-Worker 进程模型

1.1 进程架构

Nginx 采用经典的 Master-Worker 多进程模型:

                    ┌──────────────────┐
                    │  Master Process  │
                    │  (PID 1)         │
                    │  - 读取配置      │
                    │  - 管理 Worker    │
                    │  - 绑定端口      │
                    │  - 信号处理      │
                    └────────┬─────────┘
                             │ fork()
              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────┴──────┐ ┌────┴──────┐ ┌─────┴───────┐
     │  Worker #0    │ │ Worker #1 │ │  Worker #2  │
     │  - epoll      │ │ - epoll   │ │  - epoll    │
     │  - 处理请求    │ │ - 处理请求 │ │  - 处理请求  │
     │  - 独立事件循环│ │ - 独立事件 │ │  - 独立事件  │
     └───────────────┘ └───────────┘ └─────────────┘
              │              │              │
              └──────────────┴──────────────┘
                     共享 listen socket

Master 进程的职责

  1. 读取并验证配置文件
  2. 创建、绑定、关闭 listen socket
  3. 启动、终止、维护 Worker 进程
  4. 处理信号(HUP/QUIT/TERM/USR1/USR2)
  5. 执行热升级(binary upgrade)

Worker 进程的职责

  1. 接受新连接(accept)
  2. 读取请求、处理请求、发送响应
  3. 与上游服务器通信(proxy_pass)
  4. 缓存管理
  5. 日志写入
# 查看 Nginx 进程结构
ps aux --forest | grep nginx
# root      1234  0.0  0.1  master process /usr/sbin/nginx
# www-data  1235  0.5  0.8   \_ worker process
# www-data  1236  0.5  0.8   \_ worker process
# www-data  1237  0.5  0.8   \_ worker process
# www-data  1238  0.5  0.8   \_ worker process

# Worker 数量通常设置为 CPU 核心数
cat /proc/cpuinfo | grep processor | wc -l
# 4

# nginx.conf
worker_processes auto;    # 自动设置为 CPU 核心数
# 等价于: worker_processes 4;

1.2 Worker 的 CPU 亲和

每个 Worker 进程可以绑定到特定的 CPU 核心,避免上下文切换和缓存失效:

# 手动绑定
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;
# Worker 0 → CPU 0, Worker 1 → CPU 1, ...

# 自动绑定(推荐)
worker_processes auto;
worker_cpu_affinity auto;
# 验证 CPU 亲和是否生效
for pid in $(pgrep -P $(cat /run/nginx.pid)); do
    echo "Worker PID $pid: CPU $(taskset -p $pid | awk '{print $NF}')"
done
# Worker PID 1235: CPU 1
# Worker PID 1236: CPU 2
# Worker PID 1237: CPU 4
# Worker PID 1238: CPU 8

1.3 信号与热更新

Nginx 使用 Unix 信号进行进程间通信:

信号 效果 命令
HUP 重新加载配置 nginx -s reload
QUIT 优雅关闭(处理完当前请求) nginx -s quit
TERM/INT 立即关闭 nginx -s stop
USR1 重新打开日志文件 nginx -s reopen
USR2 热升级二进制文件 kill -USR2 $(cat /run/nginx.pid)
# reload 的内部过程
nginx -s reload
# 1. Master 读取新配置并验证语法
# 2. Master fork 新的 Worker 进程(使用新配置)
# 3. Master 向旧 Worker 发送 QUIT 信号
# 4. 旧 Worker 停止接受新连接
# 5. 旧 Worker 处理完当前请求后退出
# 6. 整个过程不中断任何连接

# 验证 reload 过程
watch -n 0.5 'ps aux | grep "nginx: worker"'
# 可以看到新旧 Worker 共存的短暂窗口

热升级(Binary Upgrade)的完整流程:

# Step 1: 编译新版本 Nginx(不停止旧版本)
./configure --prefix=/usr/local/nginx --with-...
make
# 不执行 make install

# Step 2: 备份旧二进制
cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old

# Step 3: 替换二进制(旧进程仍在运行)
cp objs/nginx /usr/local/nginx/sbin/nginx

# Step 4: 向旧 Master 发送 USR2
kill -USR2 $(cat /run/nginx.pid)
# 旧 Master 将 PID 文件重命名为 nginx.pid.oldbin
# 旧 Master 启动新 Master(使用新二进制)
# 新 Master 启动新 Worker

# Step 5: 此时新旧 Master 和 Worker 同时运行
ps aux | grep nginx
# 旧 Master + 旧 Worker + 新 Master + 新 Worker

# Step 6: 向旧 Master 发送 QUIT,优雅关闭旧 Worker
kill -QUIT $(cat /run/nginx.pid.oldbin)

# Step 7: 如果新版本有问题,回滚
kill -HUP $(cat /run/nginx.pid.oldbin)   # 恢复旧 Master 的 Worker
kill -QUIT $(cat /run/nginx.pid)          # 关闭新 Master

二、事件驱动与 epoll

2.1 为什么用事件驱动

传统的多进程/多线程模型(如 Apache prefork)为每个连接分配一个进程或线程:

Apache prefork 模型:
  连接 1 → 进程 1 (占用 2-10MB 内存)
  连接 2 → 进程 2 (占用 2-10MB 内存)
  ...
  连接 1000 → 进程 1000 (占用 2-10GB 内存)
  
  问题: 10000 个并发连接 = 20-100GB 内存 + 大量上下文切换

Nginx 事件驱动模型:
  Worker 0 → 同时处理 5000 个连接 (事件循环)
  Worker 1 → 同时处理 5000 个连接 (事件循环)
  
  优势: 10000 个并发连接 ≈ 几百 MB 内存,几乎无上下文切换

Nginx 的 Worker 进程运行一个事件循环(Event Loop),使用 epoll(Linux)/ kqueue(FreeBSD)/ IOCP(Windows)等多路复用机制,在单线程内高效处理数千个并发连接。

2.2 事件循环的工作流程

Worker 事件循环:

while (true) {
    // 1. 调用 epoll_wait,阻塞等待事件
    events = epoll_wait(epfd, events, max_events, timeout);
    
    // 2. 处理定时器事件(超时连接、keepalive 超时等)
    process_timers();
    
    // 3. 遍历就绪事件
    for (event in events) {
        if (event.is_accept) {
            // 新连接到达
            new_fd = accept(listen_fd);
            epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, ...);
        }
        else if (event.is_readable) {
            // 可读:读取客户端请求或上游响应
            read_handler(event.connection);
        }
        else if (event.is_writable) {
            // 可写:发送响应给客户端或请求给上游
            write_handler(event.connection);
        }
    }
}

关键设计:单个 Worker 进程中所有操作都是非阻塞的。读取文件、发送网络数据、连接上游——任何操作如果不能立即完成,Worker 不会等待,而是注册一个事件回调,继续处理其他连接。当数据准备好时,epoll 会通知 Worker。

2.3 epoll 配置

events {
    worker_connections 65535;     # 每个 Worker 最大连接数
    use epoll;                   # Linux 使用 epoll(通常自动选择)
    multi_accept on;             # Worker 一次 accept 多个连接
    accept_mutex off;            # 关闭 accept 互斥锁(Linux 3.9+ 推荐)
}
参数 默认值 推荐值 说明
worker_connections 512 65535 每 Worker 最大连接数(含上游连接)
multi_accept off on 一次系统调用 accept 多个连接
accept_mutex on off Linux 3.9+ 有 EPOLLEXCLUSIVE,不需要互斥
# 验证 epoll 是否生效
strace -e epoll_wait -p $(pgrep -f "nginx: worker" | head -1) 2>&1 | head
# epoll_wait(8, [{EPOLLIN, {u32=...}}], 512, 10000) = 1
# 看到 epoll_wait 调用即确认使用了 epoll

2.4 惊群问题(Thundering Herd)

当多个 Worker 同时监听同一个 listen socket 时,新连接到达会唤醒所有 Worker,但只有一个能成功 accept——其他 Worker 白白唤醒:

新连接到达 listen socket
→ 唤醒 Worker 0 (accept 成功 ✓)
→ 唤醒 Worker 1 (accept 失败,EAGAIN ✗)
→ 唤醒 Worker 2 (accept 失败,EAGAIN ✗)
→ 唤醒 Worker 3 (accept 失败,EAGAIN ✗)

Nginx 的解决方案演进:

  1. accept_mutex(早期):Worker 竞争互斥锁,拿到锁的才能 accept。减少惊群,但引入锁竞争。
  2. EPOLLEXCLUSIVE(Linux 4.5+):内核级解决,只唤醒一个 Worker。
  3. SO_REUSEPORT(Linux 3.9+):每个 Worker 有独立的 listen socket,内核自动分发连接。
# SO_REUSEPORT 配置(推荐)
server {
    listen 80 reuseport;     # 每个 Worker 独立的 listen socket
    listen 443 ssl reuseport;
}
# 验证 reuseport 生效
ss -tlnp | grep ':80'
# LISTEN  0  511  *:80  *:*  users:(("nginx",pid=1235,fd=6))
# LISTEN  0  511  *:80  *:*  users:(("nginx",pid=1236,fd=6))
# LISTEN  0  511  *:80  *:*  users:(("nginx",pid=1237,fd=6))
# 每个 Worker 有自己的 listen socket

SO_REUSEPORT 的性能提升:Cloudflare 的测试显示,开启 reuseport 后,请求延迟的 P99 降低了 3 倍。因为不再有 accept 锁竞争,且内核的连接分发更均匀。

三、请求处理的完整路径

3.1 请求的 11 个阶段

Nginx 将 HTTP 请求处理分为 11 个阶段(Phase),模块可以挂在不同阶段执行:

POST_READ          ← 读取请求头后(realip 模块)
SERVER_REWRITE     ← server 块的 rewrite
FIND_CONFIG        ← 匹配 location(内部)
REWRITE            ← location 块的 rewrite
POST_REWRITE       ← 重写后检查(内部)
PREACCESS          ← 访问控制预处理(limit_conn, limit_req)
ACCESS             ← 访问控制(auth_basic, auth_request)
POST_ACCESS        ← 访问控制后处理(内部)
PRECONTENT         ← 内容生成前(try_files, mirror)
CONTENT            ← 内容生成(proxy_pass, fastcgi, static files)
LOG                ← 日志记录(access_log)
# 一个典型配置中各阶段模块的分布
server {
    listen 80;
    server_name app.example.com;

    # POST_READ: 获取真实 IP
    set_real_ip_from 10.0.0.0/8;
    real_ip_header X-Forwarded-For;

    # SERVER_REWRITE: server 级重写
    rewrite ^/old-api/(.*) /api/v2/$1 permanent;

    location /api/ {
        # REWRITE: location 级重写
        rewrite ^/api/v1/(.*) /api/v2/$1 break;

        # PREACCESS: 限流
        limit_req zone=api burst=20 nodelay;
        limit_conn conn_zone 10;

        # ACCESS: 认证
        auth_request /auth;

        # CONTENT: 代理到上游
        proxy_pass http://backend;
    }

    # LOG: 日志
    access_log /var/log/nginx/access.log main;
}

3.2 一个请求的完整生命周期

客户端 → TCP SYN → Nginx (listen socket)
  │
  ├── accept(): 创建连接对象(ngx_connection_t)
  ├── 分配内存池(connection pool)
  ├── 添加读事件到 epoll
  │
  ├── [等待可读]
  │
  ├── read(): 读取请求行和请求头
  ├── 解析 HTTP 请求(状态机解析器)
  ├── 创建请求对象(ngx_http_request_t)
  ├── 匹配 server_name → 选择 server 块
  ├── 匹配 location → 选择 location 块
  │
  ├── 执行 11 阶段处理链
  │   ├── rewrite / access / rate-limit...
  │   └── content: proxy_pass
  │
  ├── [代理模式] 连接上游:
  │   ├── 从连接池获取或新建上游连接
  │   ├── 发送请求到上游
  │   ├── 读取上游响应头
  │   ├── 转发响应头给客户端
  │   ├── 流式转发响应体
  │   └── 归还上游连接到连接池
  │
  ├── 写入访问日志
  │
  ├── [Keep-Alive] 保持连接,等待下一个请求
  └── [Close] 释放内存池,关闭 socket

3.3 内存池设计

Nginx 为每个连接和请求分配独立的内存池(Memory Pool),避免频繁的 malloc/free 系统调用:

连接内存池 (connection_pool_size):
  ├── 连接对象 (ngx_connection_t)
  ├── 读/写缓冲区
  └── 生命周期: 连接建立 → 连接关闭

请求内存池 (request_pool_size):
  ├── 请求对象 (ngx_http_request_t)
  ├── 请求头解析结果
  ├── 变量值
  └── 生命周期: 请求开始 → 请求结束
# 内存池大小配置
http {
    connection_pool_size 512;     # 连接内存池初始大小
    request_pool_size 4k;         # 请求内存池初始大小
    client_header_buffer_size 1k; # 请求头缓冲区
    large_client_header_buffers 4 8k; # 大请求头缓冲区
}
# 监控 Nginx 内存使用
ps -o pid,rss,vsz,comm -C nginx
# PID     RSS   VSZ   COMMAND
# 1234   3456  5678  nginx: master
# 1235  45678 67890  nginx: worker  ← 关注 RSS

# 如果 Worker RSS 持续增长,可能存在内存泄漏
# 使用 nginx -V 确认编译了 debug 模块后:
# error_log /var/log/nginx/debug.log debug;

四、Upstream 与连接管理

4.1 Upstream 配置模型

Upstream 定义了一组后端服务器及其负载均衡策略:

upstream app_backend {
    # 负载均衡算法
    # 默认: 加权轮询 (Weighted Round Robin)
    # least_conn;         # 最少连接
    # ip_hash;            # 源 IP 哈希
    # hash $request_uri consistent;  # 一致性哈希

    # 后端服务器
    server 10.0.1.1:8080 weight=5 max_conns=100 max_fails=3 fail_timeout=30s;
    server 10.0.1.2:8080 weight=3 max_conns=100 max_fails=3 fail_timeout=30s;
    server 10.0.1.3:8080 weight=2 max_conns=100 max_fails=3 fail_timeout=30s;

    # 备用服务器(所有主服务器不可用时启用)
    server 10.0.1.100:8080 backup;

    # Keepalive 连接池
    keepalive 32;              # 每个 Worker 保持 32 个空闲长连接
    keepalive_time 1h;         # 连接最大存活时间
    keepalive_timeout 60s;     # 空闲超时
    keepalive_requests 1000;   # 单个连接最大请求数
}

server {
    location / {
        proxy_pass http://app_backend;

        # 必须配合 Keepalive 的关键参数
        proxy_http_version 1.1;
        proxy_set_header Connection "";   # 清除 Connection: close
    }
}

4.2 Keepalive 连接池

Upstream Keepalive 是 Nginx 性能优化的关键——复用已建立的 TCP 连接,避免每次请求都三次握手:

没有 Keepalive:
  请求 1: TCP 握手 → 发送 → 接收 → TCP 关闭
  请求 2: TCP 握手 → 发送 → 接收 → TCP 关闭   ← 每次都要握手
  请求 3: TCP 握手 → 发送 → 接收 → TCP 关闭

有 Keepalive:
  请求 1: TCP 握手 → 发送 → 接收
  请求 2:            发送 → 接收   ← 复用连接
  请求 3:            发送 → 接收   ← 复用连接
  ...
  空闲超时 → TCP 关闭
# 验证 Keepalive 是否生效
# 在 Nginx 机器上观察与后端的连接状态
ss -tnp | grep '10.0.1.1:8080'
# ESTAB  0  0  10.0.0.1:52134  10.0.1.1:8080  users:(("nginx",pid=1235,fd=12))
# ESTAB  0  0  10.0.0.1:52135  10.0.1.1:8080  users:(("nginx",pid=1235,fd=13))
# 看到 ESTABLISHED 状态的连接 = Keepalive 连接池中的连接

# 如果看到大量 TIME_WAIT,说明 Keepalive 没有生效
ss -tn state time-wait | grep '10.0.1.1:8080' | wc -l
# 如果数量很大,检查:
# 1. proxy_http_version 是否设置为 1.1
# 2. proxy_set_header Connection 是否清除了
# 3. 后端是否支持 Keep-Alive

Keepalive 参数计算

keepalive 数量 = (峰值 QPS / Worker 数) × 平均响应时间 × 1.5

例如:
  峰值 QPS: 10000
  Worker 数: 4
  平均响应时间: 50ms
  每 Worker QPS: 2500
  每 Worker 并发连接: 2500 × 0.05 = 125
  keepalive = 125 × 1.5 ≈ 192

  配置: keepalive 192;

4.3 上游健康检查

Nginx 开源版只支持被动健康检查(请求失败后标记不健康)。主动健康检查需要 Nginx Plus 或第三方模块:

# 被动健康检查(开源版)
upstream backend {
    server 10.0.1.1:8080 max_fails=3 fail_timeout=30s;
    # max_fails=3: 30 秒内失败 3 次,标记为不健康
    # fail_timeout=30s: 30 秒后重新尝试
}

# 主动健康检查(Nginx Plus)
upstream backend {
    zone backend_zone 64k;    # 共享内存(Worker 间共享状态)
    server 10.0.1.1:8080;
    server 10.0.1.2:8080;
}

server {
    location / {
        proxy_pass http://backend;
        health_check interval=5s fails=3 passes=2 uri=/health;
        # interval=5s: 每 5 秒检查一次
        # fails=3: 连续失败 3 次标记不健康
        # passes=2: 连续成功 2 次标记恢复
    }
}
# 使用 nginx_upstream_check_module(第三方模块,用于开源版)
# 需要编译时添加模块

upstream backend {
    server 10.0.1.1:8080;
    server 10.0.1.2:8080;

    check interval=3000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "GET /health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

# 查看健康状态
# curl http://localhost/upstream_status

4.4 重试与超时

location / {
    proxy_pass http://backend;

    # 连接超时
    proxy_connect_timeout 5s;      # 连接上游的超时(TCP 握手)
    proxy_send_timeout 60s;        # 发送请求到上游的超时
    proxy_read_timeout 60s;        # 读取上游响应的超时

    # 重试策略
    proxy_next_upstream error timeout http_502 http_503;
    # 在以下情况下尝试下一个上游:
    # error: 连接错误
    # timeout: 超时
    # http_502: 上游返回 502
    # http_503: 上游返回 503

    proxy_next_upstream_tries 2;    # 最多重试 2 次
    proxy_next_upstream_timeout 10s; # 重试的总超时
}

重试的危险

场景: POST 请求的重试

Client → Nginx → app1 (处理超时)
                      ↓
               Nginx 认为超时,重试到 app2
                      ↓
               app2 再次处理 POST 请求
                      ↓
               结果: 同一个订单被提交两次!

解决方案:
1. proxy_next_upstream 不包含非幂等方法
   proxy_next_upstream error timeout;
   # 不加 http_502/503 对 POST 请求

2. 或者使用 non_idempotent 标志
   proxy_next_upstream error timeout non_idempotent;
   # 只在显式声明时重试非幂等请求

3. 更好的方案: 后端实现幂等性(幂等 key)

五、缓冲区与代理行为

5.1 Proxy Buffer

Nginx 在代理模式下使用缓冲区暂存上游响应,这个设计至关重要:

无缓冲:
  Client ←(慢)── Nginx ←(快)── Backend
  Backend 连接被慢客户端占用,直到客户端接收完毕
  Backend 的连接资源被浪费

有缓冲:
  Client ←(慢)── Nginx ←(快)── Backend
  Nginx 快速读取 Backend 响应到内存/磁盘缓冲
  释放 Backend 连接
  Nginx 按客户端速度慢慢发送

  效果: Backend 连接被快速释放,提高 Backend 并发能力
location / {
    proxy_pass http://backend;

    # 缓冲开关
    proxy_buffering on;              # 开启缓冲(默认)

    # 响应头缓冲
    proxy_buffer_size 4k;            # 响应头缓冲区(第一个 buffer)

    # 响应体缓冲
    proxy_buffers 8 16k;             # 8 个 16k 的缓冲区
    proxy_busy_buffers_size 32k;     # 正在发送的缓冲区最大值

    # 磁盘缓冲(内存不够时写磁盘)
    proxy_max_temp_file_size 1024m;  # 临时文件最大值
    proxy_temp_file_write_size 32k;  # 每次写磁盘的大小
    proxy_temp_path /var/cache/nginx/temp; # 临时文件目录
}

什么时候关闭缓冲

# SSE (Server-Sent Events) / 流式响应
location /events {
    proxy_pass http://backend;
    proxy_buffering off;        # 关闭缓冲,实时转发
    proxy_cache off;            # 关闭缓存
    proxy_read_timeout 3600s;   # 长连接超时设长
}

# WebSocket
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_buffering off;
    proxy_read_timeout 3600s;
}

5.2 客户端请求体缓冲

大文件上传时,Nginx 的请求体缓冲行为也需要注意:

http {
    # 客户端请求体
    client_max_body_size 100m;       # 最大请求体(超过返回 413)
    client_body_buffer_size 16k;     # 请求体内存缓冲区
    client_body_temp_path /var/cache/nginx/client_temp;

    # 如果请求体 > client_body_buffer_size:
    # 1. 写入 client_body_temp_path 的临时文件
    # 2. 然后再转发给上游
    # 这意味着: 上传 100MB 文件 → 先写 100MB 到 Nginx 磁盘 → 再传给后端
    # 延迟翻倍!

    # 关闭请求体缓冲(直接透传给上游)
    # proxy_request_buffering off;   # 适用于大文件上传场景
}

六、共享内存与 Worker 间通信

6.1 共享内存区域

Worker 进程是独立的——它们不共享内存。但某些功能需要跨 Worker 协调(如限流、健康检查状态)。Nginx 使用共享内存(Shared Memory Zone)解决这个问题:

# 限流的共享内存
http {
    # 创建名为 "req_limit" 的共享内存区,大小 10MB
    limit_req_zone $binary_remote_addr zone=req_limit:10m rate=100r/s;
    # $binary_remote_addr 占 16 字节/IP
    # 10MB ≈ 160,000 个 IP 的限流状态

    limit_conn_zone $binary_remote_addr zone=conn_limit:5m;

    server {
        location /api/ {
            limit_req zone=req_limit burst=20 nodelay;
            limit_conn conn_limit 10;
        }
    }
}

# Upstream 的共享内存(Nginx Plus)
upstream backend {
    zone backend_zone 64k;    # Worker 间共享后端状态
    server 10.0.1.1:8080;
    server 10.0.1.2:8080;
}
# 查看共享内存使用情况
# 如果使用了 Nginx Plus 或有 status 模块
curl http://localhost/api/status/shared_zones

# 开源版可以通过 /proc 查看
cat /proc/$(cat /run/nginx.pid)/maps | grep -i nginx

6.2 限流的精确性

因为使用共享内存,Nginx 的限流是全局精确的(跨所有 Worker):

# 不使用共享内存的限流(如果存在这种实现):
# Worker 0: 50 r/s
# Worker 1: 50 r/s
# 总实际: 100 r/s(超过预期的 50 r/s)

# 使用共享内存的限流(Nginx 的实际实现):
# Worker 0: 检查共享计数器
# Worker 1: 检查共享计数器
# 总实际: 精确 50 r/s ✓

# 限流测试
ab -n 1000 -c 50 http://localhost/api/test
# 观察返回 503 的比例

七、性能调优清单

7.1 系统级调优

# /etc/sysctl.conf — 内核网络参数
# 文件描述符
fs.file-max = 1000000

# TCP 连接
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# TIME_WAIT
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# TCP 缓冲区
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# 端口范围(Nginx 连接上游使用的源端口)
net.ipv4.ip_local_port_range = 1024 65535

# 应用
sysctl -p
# /etc/security/limits.conf
# Nginx 用户的文件描述符限制
www-data soft nofile 65535
www-data hard nofile 65535

7.2 Nginx 配置调优

# nginx.conf 完整调优示例
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;

events {
    worker_connections 65535;
    multi_accept on;
    accept_mutex off;
}

http {
    # 基础
    sendfile on;                  # 零拷贝发送文件
    tcp_nopush on;                # 优化 sendfile 的包大小
    tcp_nodelay on;               # 禁用 Nagle 算法
    types_hash_max_size 2048;

    # Keep-Alive
    keepalive_timeout 65;
    keepalive_requests 1000;

    # 缓冲区
    client_header_buffer_size 4k;
    large_client_header_buffers 4 16k;
    client_body_buffer_size 16k;
    client_max_body_size 50m;

    # 压缩
    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 1000;
    gzip_types text/plain text/css application/json
               application/javascript text/xml;

    # 日志
    access_log /var/log/nginx/access.log main buffer=32k flush=5s;
    # buffer: 缓冲日志,减少磁盘 I/O
    # flush: 最多 5 秒刷新一次

    # 开启日志条件写入(减少无用日志)
    map $status $loggable {
        ~^[23]  0;     # 2xx/3xx 不记录(可选)
        default 1;
    }
    access_log /var/log/nginx/error_access.log main if=$loggable;
}

7.3 性能基准测试

# 使用 wrk 进行基准测试
wrk -t 4 -c 200 -d 30s http://localhost/
# -t 4: 4 个线程
# -c 200: 200 个并发连接
# -d 30s: 测试 30 秒

# 结果解读
# Requests/sec: 请求吞吐量
# Latency (Avg/Stdev/Max): 延迟分布
# Transfer/sec: 数据传输速率

# 测试不同 Worker 数的性能差异
for workers in 1 2 4 8; do
    sed -i "s/worker_processes .*/worker_processes $workers;/" /etc/nginx/nginx.conf
    nginx -s reload
    sleep 2
    echo "=== Workers: $workers ==="
    wrk -t 4 -c 200 -d 10s http://localhost/ 2>&1 | grep "Requests/sec"
done

# 测试 Keepalive 对性能的影响
# 关闭 Keepalive
wrk -t 4 -c 200 -d 10s -H "Connection: close" http://localhost/
# 开启 Keepalive(wrk 默认)
wrk -t 4 -c 200 -d 10s http://localhost/

八、总结

  1. Master-Worker 模型是 Nginx 高性能的基础。Master 负责管理,Worker 负责处理请求。每个 Worker 独立运行事件循环,互不干扰。Worker 数设为 CPU 核心数,配合 CPU 亲和绑定。

  2. epoll 事件驱动让单 Worker 处理数千并发连接。非阻塞 I/O + epoll 多路复用,一个 Worker 进程可以同时处理数千个连接而不需要线程切换。reuseport 选项解决惊群问题,是 Linux 3.9+ 环境的推荐配置。

  3. Upstream Keepalive 是最重要的性能优化之一。没有 Keepalive,每个请求都需要 TCP 三次握手——延迟增加 1-2ms。配置 proxy_http_version 1.1proxy_set_header Connection "" 是 Keepalive 生效的前提。

  4. 缓冲区设计保护了后端服务。Proxy Buffer 让 Nginx 快速读取后端响应、释放后端连接,再按客户端速度发送。这对后端的并发能力至关重要——没有缓冲,慢客户端会长时间占用后端连接。

  5. 调优是系统工程,不是改一个参数。从内核参数(somaxconn、tcp_tw_reuse)到 Nginx 配置(worker_connections、keepalive),再到应用层(缓冲区大小、日志策略)——需要从上到下一起调。


参考文献


上一篇:负载均衡工程实践:会话保持、灰度发布与容量规划

下一篇:HAProxy 工程:高级配置、ACL 与运维

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .