Nginx 高级配置与性能调优实战:从日均百万到千万 PV 的蜕变
接手一个日均 PV 约 120 万的电商平台时,Nginx 响应毛刺率 3.7%,高峰期 5xx 错误接近 0.8%。几轮调优下来,单机扛住了 2100 万 PV(压测数据),毛刺归零,错误率降到 0.02%。这篇文章就是那次实战的完整记录——哪些配置真有用,哪些是谣言,我全踩了一遍。
一、初始状态:先看清问题再动手
接手时的 Nginx 配置基本是"apt install 默认"水平,唯一改过的就是 worker_processes auto。先上监控数据:
# 用 ngxtop 快速看现状
ngxtop -l /var/log/nginx/access.log --top-errors
# 请求分布
requests: 1342/s
4xx: 2.1%
5xx: 0.8%
avg_response_time: 423ms
同时用 strace 抓了一把 worker 进程的系统调用:
strace -c -p $(cat /var/run/nginx.pid)
结果很明显:epoll_wait 占比正常,但 accept 互斥锁竞争严重——多核机器上 worker 进程抢连接的经典问题。
教训: 别一上来就改配置。先量化现状,不然你分不清优化到底有没有效果。
二、核心调优:这 5 个配置最值钱
1. worker 模型:从竞争到协作
默认配置下所有 worker 争抢同一个 accept_mutex。高并发时这就是瓶颈。
# 核心进程模型
worker_processes auto; # 等于 CPU 核数
worker_cpu_affinity auto; # 自动绑定 CPU
worker_rlimit_nofile 65535; # worker 可打开文件数
events {
use epoll; # Linux 必选 epoll
worker_connections 16384; # 每个 worker 的连接数
multi_accept on; # 一次 accept 多个连接
accept_mutex_delay 100ms; # 避免惊群效应
}
关键变化:multi_accept on + accept_mutex_delay。前者让一个 worker 一次取多个连接,减少上下文切换;后者让 worker 之间错开抢锁时间。
2. sendfile + TCP 优化:数据拷贝减半
Nginx 正常处理请求涉及多次内核态↔用户态数据拷贝。这组配置把拷贝次数从 4 次降到 2 次:
sendfile on;
tcp_nopush on; # 数据包攒够再发,配合 sendfile
tcp_nodelay on; # 禁用 Nagle 算法,降低延迟
# 内核层面
keepalive_timeout 65;
keepalive_requests 1000; # 单个 keepalive 连接最多处理 1000 个请求
tcp_nopush 和 tcp_nodelay 放一起看起来矛盾——一个攒包、一个立即发。实际上 tcp_nopush 在 sendfile 场景下控制的是响应包头和数据体的合并,tcp_nodelay 控制的是已发送数据是否延迟。两者在 Nginx 内部配合得很好。
踩坑: 一开始我把
keepalive_timeout设成 10s,结果 DB 连接池被打爆了。长连接减少连接建立开销,但对于后端代理场景,upstream的 keepalive 和客户端的 keepalive 要分开理解。
3. buffer 调优:匹配业务流量特征
这是最容易被忽视的部分。默认 buffer 太小,请求体大的接口直接触发磁盘 IO。
# 请求体
client_body_buffer_size 128k;
client_max_body_size 20m;
client_body_temp_path /tmp/nginx_body 1 2;
# 响应头
large_client_header_buffers 4 16k;
# 代理缓冲区(最关键)
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 128k;
proxy_busy_buffers_size 256k;
proxy_temp_file_write_size 256k;
proxy_temp_path /tmp/nginx_proxy 1 2;
调 buffer 的原则: 看你的 API 返回体大小的 P99。我跑了三天日志采样:
# 统计响应体大小分布
awk '{print $10}' access.log | sort -n | awk 'BEGIN{c=0} {a[c]=$1;c++} END{print "p50:",a[int(c*0.5)]; print "p90:",a[int(c*0.9)]; print "p99:",a[int(c*0.99)]}'
我们 P99 响应体是 96KB,所以 proxy_buffers 8 128k 刚好够——一个响应最多占用 8×128k = 1MB 的 buffer。设大会浪费内存。
4. upstream 调优:这才是吞吐量的瓶颈
业务后端有 4 个 Tomcat 实例,Nginx 的 upstream 配置决定了请求怎么分配以及连接怎么复用。
upstream backend {
# 最少连接算法,避免短请求被长请求阻塞
least_conn;
# 每个 worker 保持和后端的空闲连接
keepalive 32;
keepalive_requests 1000;
keepalive_timeout 60s;
server 10.0.1.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.2:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.3:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.4:8080 max_fails=3 fail_timeout=30s;
}
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1; # 必须!HTTP/1.1 才支持 keepalive
proxy_set_header Connection ""; # 清理 Connection 头,让 Nginx 复用连接
}
最值的改动是 keepalive。没配之前,每个请求都要和 Tomcat 新建一个 TCP 连接,延迟凭空多 2ms。加上 keepalive 后,后端连接复用率从 12% 飙到 89%。
踩坑: 别忘了上面两行的
proxy_http_version和proxy_set_header Connection。我折腾了两小时才发现 keepalive 没生效,就因为没改 HTTP 版本。
5. SSL:一次优化省 30% CPU
HTTPS 握手太吃 CPU。针对 TLS 1.3 优化的配置:
server {
listen 443 ssl;
ssl_protocols TLSv1.2 TLSv1.3; # 砍掉老协议
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
# 会话复用 —— 省 CPU 的核心
ssl_session_cache shared:SSL:50m; # 50MB ≈ 5 万个会话
ssl_session_timeout 4h; # 会话有效期延长
ssl_session_tickets on;
# OCSP Stapling —— 减少客户端验证链的额外请求
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 1.1.1.1 valid=300s;
# HSTS
add_header Strict-Transport-Security "max-age=31536000" always;
}
加了 ssl_session_cache 后,TLS 握手的 CPU 消耗降了约 60%。同一客户端第二次请求时直接复用会话,跳过非对称加密。
三、限流和容灾:别让一个流量尖峰打垮你
漏桶限流
# 定义限流区域
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=200r/s;
limit_req_zone $server_name zone=server_limit:10m rate=1000r/s;
location /api/ {
limit_req zone=api_limit burst=50 nodelay;
# burst=50 允许瞬时 50 个请求排队
# nodelay 让排队请求不等待延迟,直接拒绝超出部分
proxy_pass http://backend;
}
nodelay 的效果:超出的请求立即返回 503,而不是让客户端等。对于 API 场景,快速失败比排队更友好。
主动熔断器模式
upstream backend {
server 10.0.1.1:8080 max_fails=10 fail_timeout=60s;
server 10.0.1.2:8080 max_fails=10 fail_timeout=60s;
}
# 如果后端连续失败次数超过阈值,Nginx 主动熔断 60 秒
# 配合健康检查可以更精确
location /health {
proxy_pass http://backend;
proxy_next_upstream error timeout http_500 http_502 http_503;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
}
proxy_next_upstream 是最实用的配置:上游挂了自动切到下一个个。加 http_503 是因为我们后端在某些极端负载下会主动返回 503。
四、日志和监控:不量化就白调了
JSON 结构化日志
log_format json escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_addr":"$upstream_addr",'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_status":"$upstream_status",'
'"http_referer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.json log_json;
JSON 格式的好处是直接喂给 ELK 或 Loki,不用写 grok 解析。
实时状态页
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
用 Prometheus + nginx_exporter 定期抓 /nginx_status,搭配 Grafana 看活跃连接数、请求速率。
五、完整生产配置模板
这是我最终在生产用的配置(去掉了业务相关部分):
user nginx;
worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log warn;
events {
use epoll;
worker_connections 16384;
multi_accept on;
accept_mutex_delay 100ms;
}
http {
include mime.types;
default_type application/octet-stream;
# IO 模型
sendfile on;
tcp_nopush on;
tcp_nodelay on;
aio on;
directio 4m;
# 连接超时
keepalive_timeout 65;
keepalive_requests 1000;
reset_timedout_connection on;
client_body_timeout 10s;
send_timeout 10s;
# buffer
client_body_buffer_size 128k;
client_max_body_size 20m;
large_client_header_buffers 4 16k;
# 代理 buffer
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 128k;
proxy_busy_buffers_size 256k;
proxy_temp_file_write_size 256k;
# SSL
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 4h;
ssl_session_tickets on;
# 限流
limit_req_zone $binary_remote_addr zone=api:10m rate=200r/s;
# 日志
log_format json escape=json '{...}';
access_log /var/log/nginx/access.json log_json;
# Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript;
# upstream 放这里
include /etc/nginx/conf.d/*.conf;
}
六、最终效果
调优前后对比(基于 wrk 压测和线上 72 小时监控):
| 指标 | 调优前 | 调优后 |
|---|---|---|
| QPS(单机) | 13,000 | 48,000 |
| P99 延迟 | 860ms | 210ms |
| 5xx 错误率 | 0.8% | 0.02% |
| CPU 空闲率 | 15% | 35% |
| 内存占用 | 2.1GB | 2.8GB |
CPU 空闲率反而高了——因为之前大量 CPU 浪费在锁竞争和上下文切换上,优化后同样的吞吐量 CPU 反而更轻松。
延伸思考
这次调优让我反思了一件事:Nginx 配置大部分时间是"够用"的。真正需要动手的人不是看教程的人,而是已经遇到问题的人。如果你已经在用默认配置跑生产,不用慌——大部分业务场景默认配置确实够用。等你发现毛刺了、扛不住流量了,这篇文章里写的才值得翻出来试试。
另外,Nginx 调优有个"三不原则"值得记住:
- 不要改你不懂的配置 — 80% 的默认值背后有理由
- 不要一次改多个参数 — 你没法知道哪个起了作用
- 不要在生产验证 — 用 wrk 或 ab 在预发环境压测,用监控在灰度环境观察
\n

评论已关闭!