找回密码
立即注册
搜索
发回帖 发新帖

6319

积分

0

好友

774

主题
发表于 昨天 22:57 | 查看: 1| 回复: 0

一、问题背景

Nginx 性能优化这件事,多数团队都干过,但真正能做得对的不到一半。我先后参与过几次“调优后并发翻倍”的实战,回头翻改动清单会发现,真正起作用的其实只有 6 到 8 个配置项,再加上系统层的 2 到 3 个内核参数。剩下 90% 的动作,基本是在堆配置、抄博客,既没有基准数据,也没有压测验证,更没有回滚方案。

我在 2019 年做过一次比较完整的 Nginx 调优:一台 8 核 16G 的机器,跑 1 个 Nginx 加 1 个后端 Java 应用。优化前压测是 4800 QPS,p99 顶到 60ms;优化后同样压测条件,QPS 跑到 9200,p99 降到 22ms。看着是“翻倍”,但事后拆解,实际是 4 个层面一共 12 个改动点叠加出来的结果,单独拎出任何一项,都不足以称之为翻倍。

之所以写这篇文章,是因为我把当时的调优笔记整理到团队 wiki 后,很多同学追着问“能直接把那台机器的 Nginx 配置发我一份吗”。答案是不能简单复制:我当时同时改了系统层、配置层、Keepalive 层、压缩层和缓存层。只有把这几层拆开讲清楚,读者才有可能在自己生产环境里复现类似收益。

先解释一下“并发翻倍”这个说法。并发能力不是单一指标,它至少包含 5 个维度:

  1. 峰值 QPS:同样延迟下能扛多少 QPS。
  2. 稳态 QPS:持续 1 小时、错误率低于 0.1% 时的 QPS。
  3. p99 延迟:99% 请求的响应时间。
  4. 错误率:5xx / 4xx 比例。
  5. 资源利用率:CPU、内存、连接数、fd、I/O。

真正的“翻倍”,是这 5 个维度同时改善,而不是只盯着 QPS 一个数。全文所有数据都基于同样的压测场景,例如:

wrk -t8 -c200 -d60s -H "Connection: keep-alive" http://host/

最后先给一个明确观点:优化前一定要做基线(Baseline)。没有基线的优化,说白了就是“感觉上快了”,但拿不出证据说服别人。基线怎么做?留 1 小时稳态压测,再配合关键指标快照,后面会细说。

二、适用场景

本文适合以下读者和场景:

  1. 初中级运维 / SRE:生产环境 Nginx 在跑但并发上不去,想知道还能怎么调。
  2. 后端开发:需要本地起 Nginx 反向代理做接口压测。
  3. 架构师:要给新业务从零开始上线 Nginx,需要配置清单。
  4. DevOps 工程师:在 CI/CD 里集成 Nginx 性能基线测试。

不适合的情况:

  • 已经用 CDN 解决流量峰值问题,Nginx 优化的边际收益会下降。
  • 想做“百万 QPS 集群”,本文聚焦单机/单实例优化,集群层还需要 LB、HPA、Circuit Breaker。
  • 想直接拿“配置模板”抄,本文更多是方法论,具体配置要结合业务调整。

我常用的“调优边界”判断表如下:

  • 单实例 QPS 目标 < 1 万:单机调优足够,先不集群。
  • 单实例 QPS 目标 1 万 - 5 万:单机调优 + 内核参数 + Worker 调优。
  • 单实例 QPS 目标 5 万 - 20 万:Worker 调优 + Keepalive + 上游 keepalive + 缓存。
  • 单实例 QPS 目标 > 20 万:考虑多实例、Cluster、硬件层。

本文覆盖前 3 档,第 4 档只点出方向,不做展开。

三、核心知识点

3.1 Nginx 的进程模型

Nginx 是多进程 + 异步非阻塞模型:

  • Master 进程:读配置、绑定端口、启动 Worker。
  • Worker 进程:处理实际请求,每个 Worker 是单线程事件循环。
  • Worker connections:每个 Worker 同时能处理的连接数。
  • Worker CPU affinity:Worker 与 CPU 核的绑定。

性能调优的所有事情,都围绕“Worker 数量是否合理”和“Worker connections 是否够”展开。从 Linux 视角看,Nginx 的每个连接就是一个 fd,所以 fd 上限直接决定并发上限。

3.2 性能分层

我把性能分成 4 层,优化时需要按顺序来:

  1. 系统层(OS):内核参数、fd 上限、文件句柄、SOMAXCONN、TCP 缓冲区、网卡 offload。
  2. Nginx 主配置:Worker 数量、Worker connections、sendfile、TCP_NOPUSH、TCP_NODELAY、use epoll、open_file_cache。
  3. HTTP 层:Keepalive、gzip、缓存、客户端 body 缓冲、proxy_buffer、proxy_cache、upstream keepalive。
  4. 上游层:应用层调优、连接池、慢调用治理。

只改第 2 层不调第 1 层,收益很小;只改第 3 层不调第 2 层,瓶颈会卡在事件循环。

3.3 压测工具与基线

基线压测常用工具:

  • wrk:多线程异步压测,推荐,需要编译或通过包管理器安装。
  • ab(Apache Bench):简单,单进程,不要在高并发压测时使用。
  • siege:支持 HTTP/1.1 和 HTTP/2。
  • vegeta:Go 写的,支持 rate 模型。
  • k6 / locust:脚本化压测。

示例:

# 装 wrk(Ubuntu/Debian)
apt install wrk

# CentOS / RHEL
yum install wrk

# 或者从源码编译
git clone https://github.com/wg/wrk.git
cd wrk && make && make install

# 压测:8 线程,200 并发,持续 60 秒
wrk -t8 -c200 -d60s --latency http://10.0.0.10:8080/

# 输出:
# Running 30s test @ http://10.0.0.10:8080/
#   12 threads and 200 connections
#   Thread calibration: mean lat.: 1.812ms
#   Latency distribution
#     50%   2.10ms
#     75%   3.50ms
#     99%   12.40ms
#   1789432 requests in 30.00s, 0.95GB read
# Requests/sec: 59647.21
# Transfer/sec:     32.41MB

3.4 关键的“反模式”

下面这些是“看上去在优化,实际拖后腿”的反模式:

  1. 盲目把 worker_processes 调成 32,高核机器反而可能因锁竞争性能下降。
  2. 盲目开 gzip on + gzip_min_length 0,对小文件压缩收益为负。
  3. 把 keepalive_timeout 调成 0,等于关闭 Keepalive,性能可能掉一半。
  4. proxy_buffer_size 调得超大,吃 Worker 内存,容易触发 OOM。
  5. 不对上游配 keepalive 连接池,每次请求新建 TCP 连接,又慢又吃 fd。
  6. 用 if 做 location 内的判断,经典 if is evil。
  7. 不分离 server 块,证书、路由、反代全混在一起,排查困难。
  8. access_log 直接写 off,出事故时没有日志可查。

下面 14 节会逐条展开该怎么做。

四、整体排查或实施思路

整体思路总结为“5 步法”:

  1. 基线:压测现状,记录 QPS / p99 / 错误率 / CPU / 内存 / fd / 端口。
  2. 诊断:看瓶颈在哪一层,系统、Worker、Keepalive 还是上游。
  3. 最小改动:每次只改一个点或一层,改完跑同样压测。
  4. 固化:改动有效后,落到版本管理(Ansible / Salt / Helm)。
  5. 监控:基线 + 偏差告警。

下面是简化版诊断决策树:

性能上不去
├── CPU 100%?
│   ├── us 高 -> 上游慢(后端业务) / 业务逻辑重 -> 优化业务
│   ├── sy 高 -> 锁 / 系统调用 / 中断 -> 排查内核
│   └── wa 高 -> 磁盘 I/O 慢 -> 排查点 4 已经在做
├── 内存满?
│   ├── Worker 数过多 -> 调小 worker_processes
│   └── 缓存过大 -> 调小 proxy_cache / 关闭 gzip on 大文件
├── 端口耗尽?
│   └── upstream keepalive 没开 -> 见 7.5
├── fd 耗尽?
│   └── Worker connections 太大 / fd 上限不够 -> 见 7.1
└── 看起来都不高,但 QPS 顶不上去?
    └── 业务处理慢 -> 上游问题,不在本文范围

五、实战步骤

下面按“基线 → 系统层 → Nginx 主配置 → HTTP 层 → 上游层 → 验证”的顺序展开。

5.1 第一步:压测基线

目的:拿到调优前的“真值”,后续所有改动都和它对比。

命令:

# 准备 1 台压测机(不能是待测机器自己)
# 待测 Nginx:10.0.0.10:8080

# 装 wrk
apt install -y wrk

# 压测 1(短跑,看大致水位)
wrk -t8 -c200 -d30s --latency http://10.0.0.10:8080/

# 压测 2(长跑,看稳态)
wrk -t8 -c200 -d300s --latency http://10.0.0.10:8080/

# 压测 3(高并发,看是否能顶住)
wrk -t16 -c1000 -d60s --latency http://10.0.0.10:8080/

同时记录以下系统指标:

# CPU
sar -u 1 30
# 内存
sar -r 1 30
# 磁盘 I/O
iostat -xz 1 30
# 网络
sar -n DEV 1 30
# 端口
ss -s
# fd
cat /proc/sys/fs/file-nr
# Worker 进程状态
ps -eo pid,pcpu,pmem,rss,stat,comm | grep -E 'nginx|worker'

典型基线输出:

  • QPS: 4800
  • p50: 8ms
  • p99: 60ms
  • CPU 100%(us 高)
  • 内存 8G(占 50%)
  • 端口峰值 8000
  • 错误率 0%

判断:CPU 100% 是瓶颈,但 us 高,说明 CPU 在干用户态的事,需要进一步确认是“事件循环”被太多连接打满,还是“上游太慢”。

进阶基线方法:

  1. 多场景压测:wrk 默认发 GET /,业务主链路往往是 POST,要写 Lua 脚本或用 wrk2、k6 模拟。
  2. 混合负载:线上 70% GET、20% POST、10% 其他,压测要按比例。
  3. 冷启动 vs 热启动:刚启动的 Nginx 缓存命中率低,要排除冷启动前 5-10 分钟。
  4. 错误率基线:0.1% 是经验值,业务可能 0.01% 才算正常。

基线数据保存:

# 把基线数据保存到文件,后续对比
mkdir -p /tmp/baseline
wrk -t8 -c200 -d60s --latency http://10.0.0.10:8080/ > /tmp/baseline/before.txt
# 同时记录系统指标
sar -u 1 30 > /tmp/baseline/cpu-before.txt
sar -r 1 30 > /tmp/baseline/mem-before.txt
iostat -xz 1 30 > /tmp/baseline/io-before.txt

5.2 第二步:系统层优化

目的:在 Nginx 之上,把事件循环的资源上限放大。

改动清单:

# /etc/sysctl.d/99-nginx-tuning.conf

# 1) 接受连接队列上限
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# 2) 端口范围(应对大量 TIME_WAIT 复用)
net.ipv4.ip_local_port_range = 1024 65535

# 3) 加快 TIME_WAIT 回收
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

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

# 5) 连接跟踪(高并发需要)
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 7200

# 6) 文件句柄上限
fs.file-max = 2097152
# /etc/security/limits.d/90-nginx.conf
nginx  soft  nofile  1048576
nginx  hard  nofile  1048576
nginx  soft  nproc   65535
nginx  hard  nproc   65535
root   soft  nofile  1048576
root   hard  nofile  1048576

生效:

sysctl -p /etc/sysctl.d/99-nginx-tuning.conf
# limits 需要重新登录才生效,或者用 LimitNOFILE 在 systemd unit 里覆盖

验证:

sysctl net.core.somaxconn
ulimit -n
cat /proc/sys/fs/file-nr

风险:tcp_tw_reuse=1 只对主动出方向的连接复用,不影响入站;但不要同时开 tcp_tw_recycle(新版内核已移除,概念上也不要乱用)。nf_conntrack_max 设大需要保证内存够,每项 conntrack 大约占 300-400 字节。

回滚:

# 备份
sysctl -a > /tmp/sysctl.before.txt
# 回滚
sysctl -w net.core.somaxconn=128
sysctl -w net.ipv4.tcp_max_syn_backlog=128
# 等

深入:内核参数含义详解

下面解释每个参数的真实含义,避免抄博客却不知道所以然:

  • net.core.somaxconn:系统级 accept 队列长度,listen() 第二个参数上限。调大后,listen(sock, SOMAXCONN) 才有效;但 Nginx 自己也会传 backlog,要看实际调用。
  • net.ipv4.tcp_max_syn_backlog:SYN 队列长度(没完成三次握手的连接)。syncookies 启用后会失效。
  • net.ipv4.tcp_tw_reuse:主动发起新连接时,是否复用 TIME_WAIT 状态的端口。对服务端入站连接无效。
  • net.ipv4.tcp_fin_timeout:FIN_WAIT_2 状态超时,默认 60s,调小到 15s 可以让连接更快释放。
  • net.ipv4.tcp_rmem / tcp_wmem:TCP 接收/发送缓冲区三档(min/default/max),由内核自动调节范围。
  • net.core.rmem_max / wmem_max:应用层 setsockopt(SO_RCVBUF) 的上限。
  • net.netfilter.nf_conntrack_max:连接跟踪表项上限。conntrack 不是必须,但用了 iptables/nftables 规则就必须关注。
  • fs.file-max:系统级 fd 上限。

深度:net.ipv4.tcp_mem

tcp_mem 是系统级 TCP 缓冲区页数,高内存机器也要调:

# 查看当前值
cat /proc/sys/net/ipv4/tcp_mem
# 输出:每个数字是"页数"
# 调整
sysctl -w net.ipv4.tcp_mem='384000 512000 768000'
# 16G 内存机器经验值

深度:网卡 offload

# 看网卡是否开启 LRO / GRO / TSO
ethtool -k eth0 | grep -E 'lro|gro|tso'
# 关闭 TSO(某些场景有副作用)
ethtool -K eth0 tso off
# 关闭 GRO
ethtool -K eth0 gro off

风险提示:网卡 offload 调整要看业务场景,不能一刀切。

5.3 第三步:Nginx 主配置优化

目的:让 Worker 数量和事件循环参数匹配机器资源。

改动清单:

# /etc/nginx/nginx.conf

user nginx;
worker_processes auto;          # 让 Nginx 自动按 CPU 核数设 Worker
# worker_processes 8;           # 也可以显式指定
worker_rlimit_nofile 1048576;   # 每个 Worker 的 fd 上限
worker_cpu_affinity auto;       # 自动绑定 Worker 到 CPU(需要 Nginx 1.9+)

# Worker 连接数:略大于理论值即可
events {
    worker_connections 65535;   # 每个 Worker 最大并发连接
    multi_accept on;            # 一个事件循环 accept 多个连接(高并发)
    use epoll;                  # Linux 下用 epoll
    accept_mutex off;           # 高并发下关闭,新连接分发更快
}

关键点解释:

  • worker_processes auto:Nginx 会按 CPU 核数启动 Worker。一般经验:CPU 核数 <= 8,worker = 核数;CPU 核数 > 8,可以 worker = 核数 * 0.5 ~ 1,但要实测。
  • multi_accept on:一次事件循环里 accept 多个连接,降低 accept 系统调用频率。
  • accept_mutex off:Worker 之间不再用 accept 互斥锁,减少延迟。多 Worker 高并发下推荐关闭;单 Worker 无意义。

生效:

nginx -t
nginx -s reload

验证:

ps -eo pid,pcpu,pmem,rss,comm | grep nginx
# 应该看到 1 个 master,N 个 worker
ss -s
# 看连接数

风险:worker_processes auto 在 NUMA 机器上要结合 worker_cpu_affinity 才有意义;否则 Worker 可能分布在不同 CPU 域,内存访问开销变大。

回滚:

# 备份
cp /etc/nginx/nginx.conf /tmp/nginx.conf.bak
# 改 + reload
# 回滚
cp /tmp/nginx.conf.bak /etc/nginx/nginx.conf
nginx -s reload

深度:Nginx 进程模型详细

1 个 master 进程负责:

  • 读 / 解析配置
  • bind / listen 端口
  • spawn Worker 进程
  • 处理信号(HUP / QUIT / USR1 / USR2 / WINCH)
  • 升级二进制(平滑升级)

N 个 Worker 进程负责:

  • 实际处理连接
  • 通过共享内存(ngx_shared_zone)协作
  • Worker 之间不需要进程间通信(IPC)

进程间通信共享内存:Nginx 用 mmap + atomic 实现无锁共享,典型应用是 limit_req / limit_conn 的计数器、upstream 的健康状态、proxy_cache 的元数据。

为什么 Worker 是单线程:避免锁竞争。Nginx 通过事件循环异步处理多连接。

为什么 Worker 不是越多越好:

  • 每个 Worker 占用内存(基础约 2-5MB),Worker 太多会浪费内存。
  • 多 CPU 之间共享内存有开销,NUMA 机器尤其明显。
  • 一般经验:Worker = CPU 核数(I/O 密集型可适当超)。

深度:worker_cpu_affinity 配置

# 手动绑定到 8 核
worker_processes 8;
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
# 或者更直观
worker_cpu_affinity auto;  # 自动绑定(需要 Nginx 1.9+)

深度:为什么 accept_mutex 关闭

  • accept_mutex on:Worker 之间用 accept 互斥锁,保证同一时刻只有一个 Worker 在 accept,减少“惊群”。
  • accept_mutex off:多个 Worker 同时 accept,新连接分发更快。现代 Linux 内核已经修复了 accept 惊群,所以可以关。

深度:为什么 multi_accept on

  • multi_accept off(默认):一次事件循环 accept 一个新连接。
  • multi_accept on:一次事件循环 accept 多个新连接,降低 epoll_wait 返回次数,提高吞吐。
  • 适合高并发短连接场景;低并发长连接场景意义不大。

5.4 第四步:HTTP 层优化(Keepalive + gzip + 缓存)

目的:让“长连接 + 压缩 + 缓存”三件套在 HTTP 层把流量打回去。

改动清单:

http {
    # === 基础 ===
    include             mime.types;
    default_type        application/octet-stream;
    charset             utf-8;

    # === 关键:Sendfile / 零拷贝 ===
    sendfile on;
    tcp_nopush on;       # 与 sendfile 配合,包合并
    tcp_nodelay on;      # 小包立即发送,降低延迟

    # === 客户端 Keepalive ===
    keepalive_timeout 65;
    keepalive_requests 1000;     # 单个连接最多处理 1000 个请求(避免被恶意占用)

    # === 客户端请求体缓冲 ===
    client_body_buffer_size 128k;
    client_max_body_size 16m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;

    # === 长连接 + gzip + 缓存(略) ===
    # (在 server/location 块里按需)
    ...
}

Keepalive 关键参数:

  • keepalive_timeout 65:客户端连接空闲 65 秒后关闭。0 表示禁用 Keepalive,不要用 0。
  • keepalive_requests 1000:单连接最多处理 1000 个请求后强制关闭,防止连接被“长占用”。
  • 客户端 Keepalive 必须在 http {} 块里设置。

风险:keepalive_requests 设得太小,会让长连接频繁重建,反而性能下降;太大又会被单连接占用太多资源。1000 是常用值,要结合业务实际请求量调整。

5.5 第五步:上游层优化(upstream keepalive + buffer)

目的:让 Nginx 和上游服务之间的连接复用,避免每次新建 TCP 连接。

改动清单:

http {
    # 上游连接池(关键!)
    upstream backend {
        server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
        server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
        keepalive 64;                 # 关键:与上游保持的连接数
        keepalive_timeout 60s;        # 上游空闲连接超时
        keepalive_requests 1000;      # 上游单连接最多请求数
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass         http://backend;
            proxy_http_version 1.1;
            proxy_set_header   Connection "";  # 关键:清除 close,启用 keepalive
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;

            proxy_connect_timeout 5s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;

            # buffer
            proxy_buffering on;
            proxy_buffer_size 8k;
            proxy_buffers 8 16k;
            proxy_busy_buffers_size 32k;
        }
    }
}

关键点解释:

  • keepalive 64:Nginx 与每个上游 Worker 之间保持 64 个空闲连接(总数 64,不是每上游 64)。
  • proxy_http_version 1.1:必须 HTTP/1.1,否则 Keepalive 不生效。
  • proxy_set_header Connection "":必须清掉 Connection: close,否则 Nginx 会主动关闭。

风险:keepalive 设太大,会消耗 fd 和内存;设太小,起不到连接池效果。经验值:keepalive = 上游数 * 16 ~ 64。生产调优要压测。

回滚:

# 删除 keepalive 相关行
# proxy_http_version 1.0
# proxy_set_header Connection close;
# reload

5.6 第六步:压缩层优化

目的:让 gzip 真正起作用,而不是成为负担。

改动清单:

http {
    gzip on;
    gzip_vary on;
    gzip_proxied      any;
    gzip_comp_level 5;            # 1-9,5 是经验值
    gzip_min_length 1024;         # 小于 1KB 不压缩(收益为负)
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        application/x-font-ttf
        application/vnd.ms-fontobject
        image/svg+xml
        image/x-icon
        font/opentype;
    # 不要压缩已经压缩过的类型:image/jpeg, image/png, image/gif, video/mp4
}

关键点解释:

  • gzip_min_length 1024:小于 1KB 的内容不压缩。压缩小文件的开销大于收益。
  • gzip_comp_level 5:1-9 数字越大压缩比越高,但 CPU 越费,5 是经验值。
  • gzip_types 必须显式列,*不能只写 ``**,否则会压缩图片、mp4。
  • gzip_vary on:让响应带 Vary: Accept-Encoding,让 CDN / 浏览器能正确处理。

风险:gzip 会让 Worker 进程在压缩时短暂占用 CPU,高并发下可能让 us 升高。要压测,不能照搬。

回滚:

# 备份 /etc/nginx/nginx.conf
sed -i 's/gzip on;/gzip off;/' /etc/nginx/nginx.conf
nginx -s reload

5.7 第七步:缓存层优化

目的:让“短期重复请求”直接由 Nginx 响应,不打后端。

改动清单:

http {
    proxy_cache_path /var/cache/nginx/proxy
        levels=1:2
        keys_zone=my_cache:10m        # 内存中 key 索引,10M 约 80K 个 key
        max_size=10g                  # 磁盘最大 10G
        inactive=60m                  # 60 分钟未访问清除
        use_temp_path=off;

    server {
        location / {
            proxy_pass         http://backend;
            proxy_cache        my_cache;
            proxy_cache_valid  200 302 10m;   # 200/302 缓存 10 分钟
            proxy_cache_valid  404 1m;        # 404 缓存 1 分钟
            proxy_cache_key    "$scheme$proxy_host$request_uri";
            add_header X-Cache-Status $upstream_cache_status;  # 调试用
        }
    }
}

关键点解释:

  • levels=1:2:缓存文件目录层级,避免单目录文件过多。
  • keys_zone=my_cache:10m:key 索引放在内存,10M 大约能存 80K 个 key。
  • max_size=10g:缓存磁盘最多 10G,满了按 LRU 淘汰。
  • proxy_cache_valid:按状态码分别设置缓存时间。

风险:缓存会让“用户看到的内容”和“实际后端数据”有时差,业务必须接受这个时差;支付、库存、个人中心等场景不建议开启。可以按 location 细粒度开启。

回滚:

# 关闭缓存
sed -i 's/proxy_cache my_cache;/\/\/ proxy_cache my_cache;/' /etc/nginx/nginx.conf
nginx -s reload

5.8 第八步:日志层优化(避免 log IO 拖慢请求)

目的:access log 写磁盘是阻塞 IO,在高并发下会拖慢请求。

改动清单:

http {
    # 异步日志 + 缓冲
    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'urt="$upstream_response_time"';

    # buffer 32k,flush 5s
    access_log /var/log/nginx/access.log main buffer=32k flush=5s;

    # 减少不必要字段
    open_log_file_cache max=1000 inactive=20s valid=1m min_uses=2;
}

关键点解释:

  • buffer=32k flush=5s:Nginx 把日志先写 buffer,5 秒 flush 一次,减少 IO 次数。
  • open_log_file_cache:缓存日志文件描述符,避免每次 open。

风险:开启 buffer 后,日志丢失窗口是 5 秒(进程崩溃 / kill -9 会丢)。async 模式可以异步写,但需 Nginx 1.9.5+。

回滚:

# access_log 去掉 buffer 和 flush
# access_log /var/log/nginx/access.log main;
nginx -s reload

5.9 第九步:安全 / 限制层优化

目的:让恶意流量或慢攻击被早期识别,不影响正常请求。

改动清单:

http {
    # 限制连接数和请求速率
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
    limit_req_zone  $binary_remote_addr zone=req_per_ip:10m rate=10r/s;

    server {
        limit_conn conn_per_ip 50;
        limit_req  zone=req_per_ip burst=20 nodelay;
    }
}

关键点解释:

  • limit_conn:限制单个 IP 的并发连接数。
  • limit_req:限制请求速率,burst=20 nodelay 允许短时突发 20 个请求,超出的直接 503。

风险:阈值要结合业务调整,设太小会误伤正常用户。

回滚:

# 注释 limit_req / limit_conn 行
nginx -s reload

5.10 第十步:open_file_cache 优化

目的:高并发下 Nginx 要 open 大量静态文件,缓存 fd 减少 open 系统调用。

改动清单:

http {
    open_file_cache          max=10000 inactive=30s;
    open_file_cache_valid    60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors   on;
}

关键点解释:

  • max=10000:最多缓存 10000 个文件 fd。
  • inactive=30s:30 秒未访问清除。
  • min_uses=2:被访问 2 次以上才缓存。
  • errors on:错误也缓存,避免每次都 stat 不存在的文件。

风险:如果文件被频繁修改(比如 CDN 推送),inactive=30s 会让客户端拿到旧 fd,要根据业务调整。

回滚:

# 注释 open_file_cache 相关行
nginx -s reload

5.11 第十一步:压测验证

目的:用同样的压测,看 QPS / p99 / 错误率变化。

命令:

# 同基线一样的命令
wrk -t8 -c200 -d60s --latency http://10.0.0.10:8080/

# 输出对比:
# Before:
#   Requests/sec:  4800
#   p99:           60ms
#   Transfer/sec:  2.4MB
# After:
#   Requests/sec:  9200
#   p99:           22ms
#   Transfer/sec:  4.5MB

判断逻辑:

  • QPS 提升 50% 以上 + p99 下降 50% 以上:成功。
  • QPS 提升但 p99 没变:可能是下游没有压力,只是事件循环打满了。
  • QPS 没变但 p99 下降:可能是请求分布改善。
  • 错误率 > 0.1%:有副作用,看 nginx error log。

5.12 第十二步:固化

目的:把这次调优的“结果”变成“标准”,不要下次重新调一遍。

操作:

# 把 nginx.conf 纳入 git
cd /etc/nginx
git init
git add .
git commit -m "baseline: 2024 nginx tuned config"

# 把 sysctl 配置纳入配置管理(Ansible / Salt / Puppet)
# /etc/sysctl.d/99-nginx-tuning.conf
# /etc/security/limits.d/90-nginx.conf

# 把压测脚本纳入 CI
# wrk 脚本 + 期望值(QPS > 8000, p99 < 30ms)
# 任何 PR 改 nginx.conf 后跑一次

# 把基线指标纳入监控
# Prometheus: nginx_connections_active, nginx_http_requests_total
# 告警: QPS 跌 30% 持续 5 分钟

5.13 第十三步:监控 + 告警

目的:调优完成后,要能“看到优化是否还生效”。

指标:

  • nginx_connections_active:活跃连接数。
  • nginx_connections_reading / writing / waiting:状态分布。
  • nginx_http_requests_total:总请求数,rate() 可得 QPS。
  • nginx_upstream_response_time_seconds:上游响应时间。
  • nginx_upstream_connect_time_seconds:上游连接时间。

告警规则示例(只是模板,要按业务调整):

# Prometheus alerting rules
groups:
- name: nginx
  rules:
  - alert: NginxConnectionsHigh
    expr: nginx_connections_active > 50000
    for: 5m
  - alert: NginxQPSLow
    # 业务基线是 8000 QPS,跌到 5000 持续 5 分钟
    expr: rate(nginx_http_requests_total[5m]) < 5000
    for: 5m
  - alert: NginxUpstreamTimeHigh
    expr: nginx_upstream_response_time_seconds > 0.5
    for: 5m

验证告警:

# 故意发大请求,触发告警
wrk -t8 -c500 -d120s http://10.0.0.10:8080/
# 5 分钟后看 Prometheus 触发告警

六、常用命令

下面是一页纸速查,适合贴到工作台。

# === 看 Nginx 编译参数 ===
nginx -V
# 重点:--prefix, --with-http_v2_module, --with-http_gzip_static_module

# === 配置语法检查 ===
nginx -t

# === 优雅重载 ===
nginx -s reload

# === 关闭 ===
nginx -s quit       # 优雅退出
nginx -s stop       # 立即关闭(等同 kill TERM)

# === 看版本 ===
nginx -v
nginx -V

# === 看当前连接数 ===
ss -s
ss -tlnp | grep -E ':80|:443'
ss -tn | grep ESTAB | wc -l

# === 看 Worker 状态 ===
ps -eo pid,ppid,pcpu,pmem,rss,stat,comm | grep nginx

# === 看 access log 实时 ===
tail -f /var/log/nginx/access.log
# 排查 5xx
tail -f /var/log/nginx/access.log | awk '$9 ~ /^5/'

# === 看 error log ===
tail -f /var/log/nginx/error.log
grep -i 'error\|warn' /var/log/nginx/error.log | tail -50

# === 压测 ===
wrk -t8 -c200 -d60s --latency http://10.0.0.10:8080/
ab -n 10000 -c 100 http://10.0.0.10:8080/
siege -c 200 -t 60s http://10.0.0.10:8080/

# === 模拟客户端 ===
curl -v -H 'Host: example.com' http://10.0.0.10/
# 看 connection 是否复用
curl -v --keepalive-time 60 http://10.0.0.10/

# === 看上游状态 ===
# 启用 stub_status
# location /nginx_status {
#     stub_status on;
#     access_log off;
#     allow 127.0.0.1;
# }
# curl http://127.0.0.1/nginx_status
# 输出:
#   Active connections: 12
#   server accepts handled requests
#    12345 12345 67890
#   Reading: 0 Writing: 2 Waiting: 10

七、配置示例

下面是几个“贴上去就能用”的配置示例,涵盖主配置、HTTPS、上游、缓存。

7.1 主配置 nginx.conf

user nginx;
worker_processes auto;
worker_rlimit_nofile 1048576;
worker_cpu_affinity auto;
pid /var/run/nginx.pid;

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

http {
    include             mime.types;
    default_type        application/octet-stream;
    charset             utf-8;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    keepalive_timeout 65;
    keepalive_requests 1000;

    client_body_buffer_size 128k;
    client_max_body_size 16m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;

    open_file_cache          max=10000 inactive=30s;
    open_file_cache_valid    60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors   on;

    gzip on;
    gzip_vary on;
    gzip_proxied      any;
    gzip_comp_level 5;
    gzip_min_length 1024;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        application/x-font-ttf
        application/vnd.ms-fontobject
        image/svg+xml
        image/x-icon
        font/opentype;

    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'urt="$upstream_response_time"';

    access_log /var/log/nginx/access.log main buffer=32k flush=5s;

    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
    limit_req_zone  $binary_remote_addr zone=req_per_ip:10m rate=10r/s;

    include /etc/nginx/conf.d/*.conf;
}

7.2 HTTPS server 块

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;

    location / {
        proxy_pass         http://backend;
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

7.3 upstream 块

upstream backend {
    least_conn;                                # 负载均衡策略
    server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
    keepalive 64;
    keepalive_timeout 60s;
    keepalive_requests 1000;
}

7.4 缓存 server 块

proxy_cache_path /var/cache/nginx/proxy
    levels=1:2
    keys_zone=my_cache:10m
    max_size=10g
    inactive=60m
    use_temp_path=off;

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass         http://backend;
        proxy_cache        my_cache;
        proxy_cache_valid  200 302 10m;
        proxy_cache_valid  404 1m;
        proxy_cache_key    "$scheme$proxy_host$request_uri";
        add_header X-Cache-Status $upstream_cache_status;
    }
}

7.5 静态文件 server 块

server {
    listen 80;
    server_name cdn.example.com;
    root /var/www/static;

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|ttf)$ {
        expires 30d;
        access_log off;
        add_header Cache-Control "public, max-age=2592000";
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

7.6 限流 server 块

server {
    listen 80;
    server_name api.example.com;

    limit_conn conn_per_ip 50;
    limit_req  zone=req_per_ip burst=20 nodelay;

    location / {
        proxy_pass http://backend;
    }
}

7.7 灰度发布(server 块级别)

# 90% 流量走 stable
# 10% 流量走 canary
upstream stable {
    server 10.0.0.11:8080;
}

upstream canary {
    server 10.0.0.12:8080;
}

split_clients $request_id $upstream {
    90%     stable;
    10%     canary;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://$upstream;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

7.8 简单 WAF 防护(allow/deny 黑白名单)

server {
    listen 80;
    server_name example.com;

    # 黑名单 IP 段
    include /etc/nginx/blocklist.conf;
    deny all;  # 默认拒绝
}

7.9 WebSocket 反向代理

upstream ws_backend {
    server 10.0.0.11:8080;
    keepalive 64;
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name ws.example.com;

    location / {
        proxy_pass         http://ws_backend;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection $connection_upgrade;
        proxy_set_header   Host $host;
        proxy_read_timeout 3600s;     # WS 长连接,读超时设大
    }
}

7.10 gRPC 反向代理

upstream grpc_backend {
    server 10.0.0.11:50051;
    keepalive 64;
}

server {
    listen 80;
    server_name grpc.example.com;

    location / {
        grpc_pass         grpc://grpc_backend;
        grpc_set_header   Host $host;
        grpc_read_timeout 60s;
    }
}

7.11 Logrotate 配置

# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 nginx nginx
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
    endscript
}

生效方式:logrotate -f /etc/logrotate.d/nginx(强制)。

风险:kill -USR1 让 Nginx 重开日志文件,不重启进程;如果 Nginx 没在,postrotate 会报错但不致命。

7.12 systemd unit(以防用了 systemd)

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=1048576
LimitNPROC=65535

生效:

systemctl daemon-reload
systemctl restart nginx.service

八、日志或指标观察方法

调优完成后,要能“看得到”。

8.1 关键日志

  • access log:/var/log/nginx/access.log,记录每次请求的 URI、状态、响应时间、上游响应时间。
  • error log:/var/log/nginx/error.log,记录 Nginx 内部错误、配置错误、上游不可达等。
  • 系统日志:/var/log/messages、/var/log/syslog 记录内核层面。

8.2 关键指标

指标 来源 关注
QPS rate(nginx_http_requests_total) 与业务基线对比
p99 latency nginx_upstream_response_time_seconds < 100ms(经验值)
错误率 rate(nginx_http_requests_total{status=~"5.."}) < 0.1%
活跃连接 nginx_connections_active 与 ulimit 比例
Worker CPU ps + cpu 80% 警戒
fd 使用 cat /proc/sys/fs/file-nr 与 fs.file-max 比例
上游响应时间 nginx_upstream_response_time 慢调用治理

8.3 指标采集方式

  1. stub_status:Nginx 自带,简单文本指标。
  2. ngx_http_api_module(Nginx Plus):完整 API,商业版才有。
  3. VTS(nginx-module-vts):开源,带 Prometheus 集成。
  4. nginx-prometheus-exporter:Prometheus 官方 exporter。
  5. OpenTelemetry Nginx module:OTel 集成。

8.4 关键观察窗口

调优前后,看 1 小时、24 小时、7 天三个时间窗:

  • 1 小时:看瞬时 QPS / 错误率。
  • 24 小时:看周期(白天 / 夜间 / 早晚高峰)。
  • 7 天:看趋势(是否回归)。

8.5 实时日志分析技巧

# 看 5xx 占比
awk '$9 ~ /^5/' /var/log/nginx/access.log | wc -l
# 看 p99(粗略)
awk '{print $NF}' /var/log/nginx/access.log | sort -n | tail -n 1
# 看响应时间分布
awk '{print $NF}' /var/log/nginx/access.log | sort -n | awk 'BEGIN{n=0} {a[n++]=$1} END {print "p50:", a[int(n*0.5)], "p95:", a[int(n*0.95)], "p99:", a[int(n*0.99)]}'
# 看 TOP 慢请求
awk '$NF > 1' /var/log/nginx/access.log | sort -k $NF -n -r | head -20
# 看最热 URI
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 看 IP 维度
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

8.6 ELK / Loki 集成

如果日志量大,推荐推到 ELK 或 Loki:

  • ELK(Elasticsearch + Logstash + Kibana):成熟方案,适合大日志量。
  • Loki + Grafana:轻量,云原生友好。
  • Vector / Fluentd:日志收集 agent。

Nginx 推日志到 Logstash(filebeat 配置):

# /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
  paths:
    - /var/log/nginx/access.log
    - /var/log/nginx/error.log
  fields:
    type: nginx
output.logstash:
  hosts: ["logstash:5044"]

8.7 监控告警的“基线告警”

不要硬编码“必须 QPS > 10000”,而是要写基线告警:

# 当前 QPS 比 7 天前同时段 QPS 跌 30% 触发
(
  rate(nginx_http_requests_total[5m])
  <
  rate(nginx_http_requests_total[5m] offset 7d) * 0.7
)

这种告警能发现业务回归,而不是只看绝对值。

九、排查路径

调优后性能突然变差,按下面路径排查:

  1. 确认变更:git diff / 部署记录,最近一次改动是什么。
  2. 看 stub_status:连接数是否爆、读写是否卡。
  3. 看 access log 5xx:上游还是 Nginx 自身。
  4. 看 error log:配置错、上游不可达、磁盘满。
  5. 看系统指标:CPU / 内存 / I/O / 端口。
  6. 看上游应用:是不是上游慢了。
  7. 回滚:对照配置管理回滚,看是否能恢复。

十、风险提醒

下面这些是高风险动作,执行前必须有备份和回滚。

10.1 nginx -s reload

  • 风险:配置有语法错误,reload 失败,新连接无法接收,旧连接可能断。
  • 替代:nginx -t 先检查,nginx -T 看完整配置。

10.2 nginx -s stop

  • 风险:立即关闭,正在处理的请求被中断,客户端报 502。
  • 替代:nginx -s quit 优雅退出;先停 80/443,再 quit。

10.3 修改 sysctl

  • 风险:参数错误可能让系统无法访问网络。
  • 替代:保留 ssh 备用通道,只改单个参数,改完立刻测。

10.4 修改 Worker 数量

  • 风险:Worker 太多会导致 CPU 上下文切换;太少则不能充分利用多核。
  • 替代:从 auto 开始,逐步调整,压测对比。

10.5 开启 proxy_cache

  • 风险:缓存错会“用户看到旧内容”或“缓存命中率被错算”。
  • 替代:在测试环境开,观察一段时间,确认业务可接受。

10.6 iptables 改限流

  • 风险:可能锁死自己。
  • 替代:用 limit_req / limit_conn,不直接 iptables。

10.7 大文件 grep / awk

  • 风险:在 100G 的 access log 上跑 awk,容易引发 IO 风暴。
  • 替代:用 pv 限制,或者用专门的日志系统(ELK / Loki)。

10.8 truncate /var/log/nginx/access.log

  • 风险:进程还在写,文件变 0 字节但继续写。
  • 替代:用 logrotate,postrotate 发 nginx -s reopen。

十一、验证方式

调优后必须用“可观察的指标”验证。

11.1 压测验证

# 同基线一样的 wrk
wrk -t8 -c200 -d60s --latency http://10.0.0.10:8080/
# 对比:
#   Before: QPS=4800, p99=60ms
#   After:  QPS=9200, p99=22ms

11.2 真实业务验证

  • 业务接口 curl:curl -w "time_total=%{time_total}\n" http://10.0.0.10/api/health。
  • 业务主链路:带上 token 的实际业务接口。

11.3 监控验证

  • Prometheus 显示 Unchanged 或 Recovered。
  • 告警:不再触发。

11.4 错误率验证

# 看 5xx 比例
awk '$9 ~ /^5/' /var/log/nginx/access.log | wc -l
# 与总请求数对比
wc -l /var/log/nginx/access.log
# 错误率 = 5xx / 总数

11.5 时间窗口观察

  • 5 分钟:确认没有副作用。
  • 15 分钟:确认稳态。
  • 60 分钟:确认长期稳定。

十二、回滚方案

每个改动都要有回滚。

12.1 配置回滚

# 备份
cp /etc/nginx/nginx.conf /tmp/nginx.conf.bak-$(date +%Y%m%d-%H%M%S)

# 回滚
cp /tmp/nginx.conf.bak-* /etc/nginx/nginx.conf
nginx -t && nginx -s reload

12.2 sysctl 回滚

# 备份
sysctl -a > /tmp/sysctl.before-$(date +%Y%m%d-%H%M%S).txt

# 回滚
# 找到错误参数,改回
sysctl -w net.core.somaxconn=128

12.3 limits 回滚

# 备份
cp /etc/security/limits.d/90-nginx.conf /tmp/limits.bak

# 回滚
cp /tmp/limits.bak /etc/security/limits.d/90-nginx.conf
# 重新登录

12.4 缓存清理

# 缓存目录出错,清空(注意:会让缓存命中率掉到 0)
rm -rf /var/cache/nginx/proxy/*
# 或者更稳
mv /var/cache/nginx/proxy /var/cache/nginx/proxy.bak
mkdir /var/cache/nginx/proxy
chown -R nginx:nginx /var/cache/nginx/proxy

12.5 完整回滚剧本

  1. nginx -s reload 失败 → 用 nginx -c /tmp/nginx.conf.bak 启动。
  2. sysctl 错误 → 紧急重启,前提是机器有控制台。
  3. limits 错误 → 重新登录即可。
  4. 缓存错误 → 删除缓存目录,reload。

十三、生产环境注意事项

13.1 灰度

  • 任何改动先在 1 个 Worker / 1 个实例上试。
  • 用 try_files / error_page 切流量。
  • 持续观察 5 分钟、15 分钟、1 小时再继续。

13.2 监控先行

  • 调优前确认有 stub_status / exporter 接入监控。
  • 没有监控的调优是“裸奔”。

13.3 双人复核

  • nginx.conf 改动至少两人过。
  • 重要变更走变更流程。

13.4 备份

  • 改动前 cp 一份当前 nginx.conf 到 /tmp/。
  • 重要变更保存到 git。

13.5 文档化

  • 改动写入 CHANGELOG。
  • 配置变更在 wiki / runbook 同步更新。

13.6 不要在生产首次开启

  • proxy_cache / gzip / keepalive 在测试环境充分验证再上生产。
  • 涉及多上游的 keepalive 先在一个上游试。

13.7 关注运行时信号

  • nginx -s reload 是 HUP,worker 优雅重启。
  • nginx -s quit 是 QUIT,master 优雅退出。
  • nginx -s reopen 是 USR1,重开日志文件,logrotate 用。
  • kill -USR2:升级二进制,平滑升级。
  • kill -WINCH:优雅关闭 worker(配合 USR2 升级)。

13.8 重要指标不要硬编码

  • 阈值要结合业务调整,不能写死“必须 QPS > 10000”。

13.9 配置管理

  • nginx.conf 纳入 git。
  • 大集群用 Ansible / Salt / Puppet 统一管理。
  • 用 confd / etcd / consul-template 实现动态配置(高阶)。

13.10 容量规划

  • 业务量增长 50%,Nginx 的 CPU 是否够?fd 是否够?Worker 是否够?
  • 提前 6 个月规划扩容,不要等“双 11 前一天”才升级。
  • 容量测试:每年至少一次“业务峰值的 1.5 倍”压测。

13.11 团队知识沉淀

  • 调优经验写进 wiki,新人可以照做。
  • 失败案例写进复盘文档。
  • 团队内每月一次“性能主题分享”,互相学习。

附录 A:一页纸调优清单

□ 1. 基线压测(wrk -t8 -c200 -d60s)
□ 2. 系统层
    □ sysctl -p /etc/sysctl.d/99-nginx-tuning.conf
    □ /etc/security/limits.d/90-nginx.conf
□ 3. Nginx 主配置
    □ worker_processes auto
    □ worker_rlimit_nofile 1048576
    □ events { worker_connections 65535; multi_accept on; use epoll; accept_mutex off; }
□ 4. HTTP 层
    □ sendfile on; tcp_nopush on; tcp_nodelay on
    □ keepalive_timeout 65; keepalive_requests 1000
□ 5. 上游层
    □ upstream { keepalive 64; }
    □ proxy_http_version 1.1; proxy_set_header Connection "";
□ 6. 压缩
    □ gzip on; gzip_min_length 1024; gzip_comp_level 5
□ 7. 缓存(可选,业务可接受)
    □ proxy_cache_path + proxy_cache
□ 8. 日志
    □ access_log buffer=32k flush=5s
□ 9. 限流
    □ limit_req zone=req_per_ip burst=20 nodelay
□ 10. 验证
    □ wrk 同样压测,看 QPS / p99 / 错误率
    □ Prometheus 指标确认
    □ 业务接口 curl 验证
□ 11. 固化
    □ git 提交
    □ 配置管理更新
    □ 监控告警补全

附录 B:常见问题速查

B.1 502 Bad Gateway

  • 上游不可达
  • nginx -t 看配置
  • curl http://upstream:port 看上游是否在听

B.2 504 Gateway Timeout

  • 上游响应慢
  • 调大 proxy_read_timeout
  • 看上游应用是否在 GC / 慢 SQL

B.3 413 Request Entity Too Large

  • client_max_body_size 不够
  • 调大

B.4 499 Client Closed Request

  • 客户端提前关闭
  • 看上游是否慢

B.5 504 with upstream prematurely closed connection

  • 上游进程崩
  • 看上游应用日志

B.6 内存持续上涨

  • Worker 内存泄漏
  • 升级 Nginx 版本
  • 看模块是否有泄漏

B.7 CPU 100% 但 QPS 不高

  • 事件循环打满
  • 调 worker_processes / worker_connections
  • 看是不是 upstream keepalive 没开

B.8 502 频繁但 access log 正常

  • 上游有缓存或 sticky session 问题
  • 看 upstream 块配置

附录 C:Nginx 编译选项参考

# 装依赖
apt install -y libpcre3 libpcre3-dev libssl-dev zlib1g-dev libxml2-dev libxslt1-dev libgd-dev libgeoip-dev libgoogle-perftools-dev

# 下载源码
cd /usr/local/src
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar -xzf nginx-1.24.0.tar.gz
cd nginx-1.24.0

# 编译选项
./configure \
    --prefix=/etc/nginx \
    --sbin-path=/usr/sbin/nginx \
    --conf-path=/etc/nginx/nginx.conf \
    --error-log-path=/var/log/nginx/error.log \
    --http-log-path=/var/log/nginx/access.log \
    --pid-path=/var/run/nginx.pid \
    --with-http_ssl_module \
    --with-http_realip_module \
    --with-http_addition_module \
    --with-http_sub_module \
    --with-http_dav_module \
    --with-http_flv_module \
    --with-http_mp4_module \
    --with-http_gunzip_module \
    --with-http_gzip_static_module \
    --with-http_random_index_module \
    --with-http_secure_link_module \
    --with-http_stub_status_module \
    --with-http_auth_request_module \
    --with-http_xslt_module=dynamic \
    --with-http_image_filter_module=dynamic \
    --with-http_geoip_module=dynamic \
    --with-http_perl_module=dynamic \
    --with-threads \
    --with-stream \
    --with-stream_ssl_module \
    --with-stream_realip_module \
    --with-stream_geoip_module=dynamic \
    --with-stream_ssl_preread_module \
    --with-google_perftools_module \
    --with-debug

make
make install

注意:Ubuntu / Debian / RHEL 的发行版 Nginx 已预编译,多数情况不需要自编译;只有需要第三方模块(VTS、ModSecurity)时才需要自编译。

附录 D:给资深工程师的几条“非主流”建议

下面是我在生产环境用过的几个“非主流但有用”的技巧:

  1. 压测脚本要“去 error page”:压测时不要压有 502/504 处理的 URI,否则测出来的是 Nginx 抗 5xx 的能力,不是真实业务。
  2. keepalive_requests 是个“安全阀”:防止单个 TCP 连接被占用太久。如果客户端有 bug 不关连接,这个参数会让连接定期强制关闭。
  3. 不要在 nginx.conf 里嵌密码 / 密钥:即使 ACL 严格,也不要在配置里硬编码。用环境变量 + secret 引入。
  4. 大文件上传场景,把 client_body_buffer_size 调小,搭配 client_body_temp_path 把临时文件放到独立盘,避免上传占满主盘 IO。
  5. mirror 指令做流量镜像:把生产流量复制一份到测试环境做测试;这个能力 Web 服务器能做,但应用层做起来复杂。
  6. vts 模块(第三方):可以看每个 location 的请求数 / 状态码 / 响应时间分布,调优时帮助很大。
  7. Lua + OpenResty 做复杂业务逻辑:Lua 比直接 Nginx 配置灵活,适合“限流、AB 测试、灰度”等场景。Nginx 官方不写 Lua,要用 OpenResty。
  8. Nginx 的 try_files 比 if -e $uri 安全:if is evil,能不用就不用。
  9. map 比正则更高效:能在 server 块里用 map 做条件判断,比 location 块里用 if 高效。
  10. 整体留 20% 性能余量:不要把机器压到 100%,业务增长 20% 还能扛,不扩容都能再撑一个季度。

附录 E:调优复盘的“五个一”

每做完一次调优,写一份“调优复盘”,模板如下:

  1. 一基线:压测前 QPS / p99 / 错误率 / CPU / 内存是多少?
  2. 二改动:这次具体改了哪几个文件、哪几个参数?
  3. 三结果:压测后 QPS / p99 / 错误率是多少?错误率是否仍然 < 0.1%?
  4. 四副作用:有没有引入新问题?如果有,怎么修?
  5. 五固化:这次改动沉淀到了 git / 文档 / 监控 / runbook 里了吗?

这套“五个一”模板贴在 wiki,每次复盘照着写,30 分钟就能产出一份有信息量的复盘。

附录 F:不同业务场景的“调优配方”

不同业务对 Nginx 的关注点不同,下面给 3 个典型场景的调优配方,便于快速复用。

F.1 场景 1:API 网关(高并发、短请求)

worker_processes auto;
worker_rlimit_nofile 1048576;
events {
    worker_connections 65535;
    multi_accept on;
    use epoll;
    accept_mutex off;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000;

    # 限流(API 网关必开)
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

    # 后端 buffer(短请求,buffer 不要太大)
    proxy_buffering on;
    proxy_buffer_size 8k;
    proxy_buffers 8 16k;

    # 上游 keepalive(API 网关必开)
    upstream backend {
        server 10.0.0.11:8080;
        keepalive 64;
    }

    server {
        listen 80;
        location / {
            limit_req zone=api burst=50 nodelay;
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }
}

F.2 场景 2:静态 CDN(高吞吐、大文件)

worker_processes auto;
events {
    worker_connections 4096;     # 静态 CDN 不需要 6 万连接
    multi_accept on;
}

http {
    sendfile on;                 # 静态 CDN sendfile 必开
    tcp_nopush on;               # 包合并
    tcp_nodelay off;             # 大文件不要 nodelay
    keepalive_timeout 30;
    keepalive_requests 100;

    # 大文件优化
    output_buffers 8 32k;
    postpone_output 1460;

    open_file_cache max=10000 inactive=30s;

    server {
        listen 80;
        root /var/www/static;
        location / {
            try_files $uri $uri/ =404;
        }
        location ~* \.(jpg|png|gif)$ {
            expires 365d;
            add_header Cache-Control "public, max-age=31536000, immutable";
        }
    }
}

F.3 场景 3:WebSocket 网关(长连接)

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name ws.example.com;

    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

注意:WebSocket 场景的 worker_connections 要相应调整,因为 1 个 WebSocket 连接占用 1 个 fd,1 万个 WebSocket 就是 1 万个 fd。

附录 G:Nginx 性能调优的“4 类反模式”

最后再点 4 类“看似在优化、实际在拖后腿”的反模式:

  1. 把所有的 if 都去掉:if is evil 在 location 块内比较严重,在 server 块内相对可控。新手会把所有 if 替换成 map / try_files,但有时 if 反而更直观。判断标准:看是否引起 rewrite 死循环。
  2. 配置里加大量注释:# 这里是开启 gzip 之类的注释,会让 reload 时解析变慢。配置要简洁,文档放 wiki。
  3. 多个 server 块混合:不同业务混在同一个 nginx.conf,定位问题难。按业务拆分 conf.d/。
  4. 不开监控就调优:没监控的调优,效果“看不见”,出问题“看不见”,做完不知道好不好。先 stub_status / exporter,再调优。

最后再强调一次:本文的“翻倍”不是某一项配置的功劳,是 4 层 + 12 步的连贯执行。读者照着做,可能效果比 2 倍好,也可能不到 2 倍,关键是看业务。没有银弹,只有工程化。

十四、总结

Nginx 性能调优翻倍的关键不是某个“神奇参数”,而是 4 层 + 12 步的连贯执行:

  1. 基线压测——没基线的调优是耍流氓。
  2. 系统层——内核参数、fd、端口。
  3. 主配置——Worker 数量、events、sendfile。
  4. HTTP 层——Keepalive、gzip、缓存。
  5. 上游层——upstream keepalive、buffer、proxy 调优。
  6. 日志 / 监控 / 限流——让调优后的状态可观察、可保护。
  7. 固化——纳入 git / 配置管理 / CI。
  8. 回滚——任何改动都有回滚预案。

实际做下来,真正起作用的 6-8 个改动是:

  • worker_processes auto
  • worker_rlimit_nofile 1048576
  • events.worker_connections 65535
  • http.sendfile on; tcp_nopush on; tcp_nodelay on
  • http.keepalive_timeout 65
  • upstream.keepalive 64 + proxy_http_version 1.1 + Connection ""
  • net.core.somaxconn = 65535
  • fs.file-max = 2097152

但只改这 8 项,不一定能翻倍——翻倍需要“4 层叠加”才能触发效应。如果你照着做,发现 QPS 提升不到 50%,那大概率是上游应用拖后腿,不在 Nginx 范围。

调优完成后,要持续监控、持续灰度、持续准备回滚,不要把“翻倍”作为最终目标,而要把“业务稳定”作为目标。QPS 翻倍但错误率涨 1%,得不偿失。

祝你在自己的生产环境里,能稳定地、不出事故地做完这次调优。




上一篇:Go 编译版本不匹配:GOROOT 指向旧 SDK 的排查修复
下一篇:10 个 Linux 命令:从整机到日志,快速定位 80% 线上故障
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-18 01:59 , Processed in 0.200286 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表