Nginx 调优的收益有上限。一套默认配置的单机实例跑静态文件,通常已经能撑住数万 QPS,真正把响应时间拖长的是后端应用、TLS 握手和磁盘 IO。所以动手改配置之前,先用 ab 或 wrk 打一条可复现的基线,一次只改一个变量,否则后面每一项「优化」都只是猜测。
先算清楚并发的数学上限
worker_processes auto 会按 CPU 核数起进程。每个 worker 是单线程事件循环,进程数超过核数没有收益,只会增加上下文切换和内存占用。真正的并发上限由两个数相乘得到:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 10240;
multi_accept on;
}
理论并发 = worker_processes × worker_connections,但这个数字要打折。作为反向代理时,每个客户端连接还要额外占用一条到 upstream 的连接,一条请求吃掉两个槽位,反代场景的实际并发大约只有静态场景的一半。同时必须同步抬高 worker_rlimit_nofile 与系统级的 fs.file-max、net.core.somaxconn,否则 Nginx 会先撞上文件描述符上限,根本到不了连接数上限。如果实例规格本身核数就紧张,先回到 云服务器选购指南 核对 CPU、内存与带宽的配比,再谈参数。
keepalive 要同时管两个方向
客户端方向的 keepalive_timeout 决定空闲连接保留多久。设太长,大量空闲连接占着 worker_connections 槽位;设太短,弱网用户每次请求都要重新握手。面向公网的门户站点取 15 到 30 秒,内网服务间调用可以更短。
更常被忽略的是 upstream 方向。默认情况下 Nginx 为每个请求新建一条到后端的 TCP 连接,高并发时握手开销与 TIME_WAIT 堆积会非常明显。要复用得同时做三件事:proxy_http_version 1.1、proxy_set_header Connection "",以及在 upstream 块里声明 keepalive 64。池子大小按「后端实例数 × 单实例可承受并发」估算,不必开得很大。
验证复用是否生效,可以用
ss -tn state established '( dport = :8080 )'观察与后端的连接数。连接数随 QPS 线性增长,说明池子没起作用;连接数稳定在一个区间,才是真复用。
| 参数 | 常见默认 | 建议起点 | 主要影响 |
|---|---|---|---|
| worker_processes | 1 | auto | 等于核数即可,多了反而切换 |
| worker_connections | 512 | 10240 | 与进程数相乘即并发上限 |
| keepalive_timeout | 75s | 15–30s | 串行请求复用,过长占用槽位 |
| upstream keepalive | 未开启 | 32–128 | 省掉每次回源的握手 |
| gzip_comp_level | 1 | 5 | 压缩率与 CPU 的折中 |
| open_file_cache | off | max=10000 | 小文件密集站点省 stat |
压缩:先分清该压什么
压缩只对文本有效。JS、CSS、JSON、SVG、纯文本的压缩比通常在 3 到 10 倍之间,值得开;JPEG、PNG、WebP、MP4、zip、gz、br 本身已经压缩过,再压一遍只会白烧 CPU,体积甚至略微变大。小于 1KB 的响应也别压,头部的额外开销可能比省下的字节还多。
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
brotli 比 gzip 通常再多压 15% 到 20%,代价是 CPU 明显更贵。更划算的做法是在构建阶段预压缩出 .br 与 .gz 文件,让 Nginx 用 gzip_static 或 brotli_static 直接下发,运行时零压缩开销。记得配合 Vary: Accept-Encoding,否则中间缓存可能把压缩版本发给不支持压缩的客户端。
静态资源:expires 与 immutable
带内容 hash 的构建产物可以给一年并标记 immutable,浏览器在这段时间里不会发任何校验请求;不带 hash 的入口文件(首页、未加指纹的 js)必须用 no-cache 或极短的 max-age,否则发版后用户拿到的还是旧引用。
location ~* .(?:js|css|woff2|png|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location = /index.html {
add_header Cache-Control "no-cache";
}
注意 add_header 一旦在子级 location 里出现,就会整体覆盖父级的 add_header 列表而不是叠加。跨域头、安全头写漏了往往就是这个原因,也是上线前必须逐项核对的点,可以对照 云服务器安全加固实战清单 一起过。
open_file_cache 值得开
静态站点的大量时间花在 stat 系统调用上。open_file_cache max=10000 inactive=30s 配合 open_file_cache_valid 60s、open_file_cache_min_uses 2,可以把文件描述符与元数据缓存住。文件更新频繁的站点要么把 valid 调小,要么干脆别开,避免读到旧内容。
proxy_buffering 与缓冲区大小
默认开启缓冲,Nginx 先把后端响应读进内存再慢慢发给客户端,慢客户端不会占住后端的 worker,这是它该有的样子。proxy_buffers 8 16k、proxy_buffer_size 16k、proxy_busy_buffers_size 32k 是普通页面的合理起点。但流式接口(SSE、大文件下载、长轮询)必须 proxy_buffering off,否则数据会卡在缓冲区里,前端看到的是长时间无响应然后一次性刷出。容器里跑 Nginx 时这套参数同样生效,容器化细节可以看 Docker 入门。
TLS 优化
- 只留 TLS 1.2 与 1.3。1.3 的完整握手是一个往返,弱网下比 1.2 少整整一轮。
- 开 session 复用:
ssl_session_cache shared:SSL:10m配ssl_session_timeout 1d,回访用户能走恢复握手,省掉证书校验与密钥交换。 - 开 OCSP stapling 并配好可用的
resolver,让浏览器不必自己去查吊销状态。 - 确认 ALPN 协商到 h2;证书链要完整,缺中间证书会让部分客户端直接握手失败而不是降级。
- 有条件时用 ECDSA 证书,握手阶段的计算量明显低于 RSA。
限流是防护,不是性能手段
limit_req 与 limit_conn 的作用是拦住异常流量,不提升吞吐。被限的请求要么排队等待,要么直接返回 503,延迟只会上升。配置时把 zone 键设成 $binary_remote_addr,rate 给到正常峰值的两倍左右,再用 burst 吸收瞬时抖动;不加 nodelay 的 burst 会让突发请求均匀排队,首页这种并发请求密集的场景反而更慢。这部分配置同时是安全基线的一部分,和成本控制一样属于长期要盯的指标,可以参考 云计算成本优化 里的监控思路。
用 ab / wrk 建立基线
压测机必须和被测机分开,否则你测的是两台机器抢 CPU 的结果,而且很容易先打满客户端。命令层面的要点:
wrk -t4 -c400 -d30s --latency URL拿 p50、p99 与每秒请求数,是最常用的组合。ab -n 10000 -c 200 -k URL里的-k表示开启 keepalive;ab 是单线程的,连接数上千之前它自己就先饱和了。- 看 p99 而不是平均值,平均值会把长尾完全掩盖。
- 一次只改一个参数,记录改动前后的同一指标,否则无法归因。
如果压测显示 Nginx 本身不是瓶颈(CPU 不到 50%、连接数远未到上限),那就该去看后端和数据库了。单机调优的天花板通常不在 Nginx 身上,把静态资源交给边缘节点才是下一步,顺序可以看 DNS 与 CDN 加速。
常见问题
worker_connections 设成 65535 是不是越大越好?
不是。它只是上限,实际并发受文件描述符、内存和上游处理能力共同约束。开得过大反而让故障时的连接堆积更难被监控发现。一般从 10240 起步,按监控里的实际峰值与文件描述符余量调整,同时把 worker_rlimit_nofile 一并抬高。
开了 gzip 之后 CPU 使用率飙升怎么办?
先把 gzip_comp_level 降到 4 到 5,再把 gzip_types 收窄到真正的文本类型,确认 JPEG、PNG、MP4 之类的二进制没有进入列表。仍然吃紧就改成构建期预压缩出 .gz 与 .br 文件,用 gzip_static、brotli_static 直接下发,运行时完全不压缩。
keepalive_timeout 设长了会不会把连接数占满?
会。空闲连接同样占用 worker_connections 槽位。公网门户在 15 到 30 秒之间比较平衡;如果监控里 Established 连接数远高于当前 QPS,说明要么值设大了,要么客户端根本没复用连接,两种情况的优化方向完全不同。
压测数字很好看,线上依然慢,问题在哪?
压测通常打的是本机静态页或固定 URL,绕过了 DNS 解析、TLS 握手、真实网络 RTT 和数据库。要按真实链路分段测量,先确认慢在解析、建连、服务端处理还是首包传输,再决定优化哪一段,否则很容易在不是瓶颈的地方反复调参。
延伸阅读
- DNS 与 CDN 加速:把首字节时间压下来的正确顺序:Nginx 调完之后的下一个瓶颈通常在网络侧,这篇讲清解析与边缘缓存的先后关系。
- 云服务器选购指南:CPU 核数直接决定 worker 上限,选型阶段就该把配比算清楚。
- 云服务器安全加固实战清单:TLS 配置与限流规则本身就是加固项,这篇给了可勾选的清单。
- Docker 入门:镜像、容器、数据卷与网络一次讲清:容器化部署时,网络模式与文件挂载会直接影响上面这些参数是否真的生效。
- 服务器监控告警怎么搭:从指标采集到告警收敛:调完参数要靠指标验证,这篇给了黄金指标与告警收敛的具体做法。