Nginx 高级配置与性能调优实战:从日均百万到千万 PV 的蜕变

2026-05-30 11:14 Nginx 高级配置与性能调优实战:从日均百万到千万 PV 的蜕变已关闭评论

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_nopushtcp_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_versionproxy_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 调优有个"三不原则"值得记住:

  1. 不要改你不懂的配置 — 80% 的默认值背后有理由
  2. 不要一次改多个参数 — 你没法知道哪个起了作用
  3. 不要在生产验证 — 用 wrk 或 ab 在预发环境压测,用监控在灰度环境观察

\n

你可能感兴趣的文章

来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
转载请注明出处: https://teachcourse.cn/4182.html ,谢谢支持!

资源分享

分类:Android 标签:
自定义弹窗 VS AlertDialog分享弹窗 自定义弹窗 VS AlertDialog分
99-学习案例汇总 99-学习案例汇总
Android事件处理机制 Android事件处理机制
Python常用100个关键字详细示例(4) Python常用100个关键字详细示例

评论已关闭!