最开始接触“Nginx 应用实践”这件事,很多人的第一反应是:装一个 Nginx,改两行配置,把静态页面跑起来就算会了。但真正接手线上业务之后会发现,同样是转发一个请求,为什么别人的服务高并发下稳如老狗,自己的服务一压测就 502、504 满天飞。这篇内容不打算把 Nginx 的指令手册重新抄一遍,而是从实际运维和研发都绕不开的高频场景出发,把静态资源服务、反向代理、负载均衡、HTTPS 落地这几块应该怎么配、为什么这么配、踩过哪些坑,一条一条讲清楚。适合刚接触 Nginx 配置的新手,也适合已经能跑通基础服务、但遇到复杂问题就抓瞎的同学。
1. 先把 Nginx 应用实践中常见的认知误区拉直
1.1 它到底是“服务器软件”还是“网关软件”
Nginx 的本质是一个后来的 Web 服务器,但它最出名的能力早就超出了“服务器”这三个字的范畴。在很多团队里,Nginx 既承担静态资源服务器,又承担反向代理,还顺手做负载均衡、HTTPS 终结、日志采集入口。换句话说,它是整个流量链路中最先和后端打交道的那一层,是名副其实的流量入口。
这个定位决定了你学 Nginx 应用实践时,不能只记命令,而要建立一个“请求转发者”的视角。很多人配置完一个 location 就以为万事大吉,可一旦遇到类似“为什么我改了配置,reload 之后没有生效”“为什么后端接口正常,通过 Nginx 访问就超时”这种问题,往往就是因为没有理解 Nginx 的两层角色:它既要管理静态内容,也要管理动态流量。两层角色的配置逻辑完全不同,混在一起不梳理清楚,排查问题就全靠瞎试。
另外,Nginx 的高并发能力来自它的事件驱动模型,而不是像传统进程模型那样一个连接一个进程。它的 worker 进程数量通常与 CPU 核数相关,每个 worker 内部通过事件循环处理大量连接。理解了这个基础,才能明白为什么有些参数看似微不足道,却能决定你的服务在突发流量下是撑得住还是瞬间崩掉。
1.2 一个请求从进入到返回,Nginx 在中间做了什么
可以把一次完整请求比作顾客进餐厅。顾客不是直接冲到后厨找厨师,而是先到前台,由服务员看菜单、下单、传菜、上菜。Nginx 就是这个服务员。它接收顾客的连接请求,解析 HTTP 报文,按照配置决定这个请求应该被哪个 location 块处理,然后决定是直接读本地文件返回,还是把请求转给后端的某个应用服务。后端处理完,Nginx 再把结果送回给顾客。
这个比喻对应到实际流程里,大概有几步:TCP 连接建立,Nginx 接收请求头,匹配 server 块和 location 块,处理 rewrite、访问控制、代理等阶段,然后向后端或本地文件系统发起操作,最后把响应写回客户端并记录 access log。大多数配置问题,都出在“匹配 location”和“转发给谁”这两个环节上。把这两个环节搞明白,Nginx 应用实践就完成了一半以上。
举个例子,很多人分不清 server_name 和 location 的优先级。请求进来后,Nginx 先根据 Host 头和监听端口找 server,找到 server 之后,再根据 URI 在内部逐个匹配 location。很多人以为配置了 server_name 就不用管 location,或者反过来,结果就是访问时总跑到默认页。这个匹配顺序在脑海里必须形成肌肉记忆。
2. 静态资源服务:最容易上手也最容易配错的模块
2.1 root 与 alias 的区别,一张表讲透
静态资源服务是 Nginx 最基础、最常用,也最容易翻车的一块。我见过不少线上事故,根因都是 root 和 alias 用错了。表面上看两者都是指定文件路径,实际映射逻辑完全不同。
location /static/ { root /data/webroot; }上面这段配置,当用户请求/static/a.css时,Nginx 会去/data/webroot/static/a.css找文件。root 的含义是“把 location 匹配到的 URI 拼在 root 后面”。
而如果换成 alias:
location /static/ { alias /data/static_files/; }请求/static/a.css时,Nginx 会去/data/static_files/a.css找文件。alias 的含义是“把 location 匹配到的这部分前缀换成 alias 指定的目录”。
关键差异可以这样记:
- root:location 的 URI 前缀会保留在最终路径中。
- alias:location 的 URI 前缀会被替换掉,最终路径以 alias 后面写的目录为准。
使用 alias 时有个极其常见的坑:目录末尾的斜杠。如果 location 是/static/,alias 必须写成/data/static_files/,结尾带斜杠;如果你写成/data/static_files,请求拼接时就可能出问题。反过来,root 配置通常对结尾斜杠不敏感,但为了统一和避免意外,我还是建议保持一致的书写习惯。另一个建议是:尽量少用 alias,除非确实需要把短路径映射到另一个完全不同的磁盘目录。大多数场景下,让目录结构和 URI 保持一致,用 root 就够了,维护成本低很多。
2.2 缓存和压缩参数,配置完要验证
静态资源如果不做缓存,每次请求都从磁盘读文件,效率很低。Nginx 支持通过 expires、Cache-Control、ETag 等机制让浏览器缓存资源。常见配置是这样:
location /static/ { root /data/webroot; expires 7d; add_header Cache-Control "public, immutable"; }这里expires 7d会自动生成Expires和Cache-Control: max-age=604800两个响应头。后面的Cache-Control public, immutable会覆盖掉 max-age,但保留缓存允许性。对于带指纹的静态资源,比如app.8f3a2b.css,这个配置非常合适。如果资源更新不频繁,可以把缓存时间设置得更长。
压缩方面,Gzip 依然是性价比很高的优化手段。Nginx 默认就有相关模块,开启方式不复杂:
gzip on; gzip_comp_level 6; gzip_min_length 1024; gzip_types text/plain text/css application/json application/javascript image/svg+xml;需要特别提醒的是,图片中的 JPEG、PNG 本身已经是高压缩格式,再开 gzip 收获不大,反而消耗 CPU,所以一般不放进去。gzip_min_length 1024的意思是响应体小于 1KB 就不压缩,因为压缩这些小文件得不偿失。配完之后,不要只看配置语法过了就放心。用浏览器开发者工具或者命令行工具去看响应头里有没有Content-Encoding: gzip,以及Cache-Control是否符合预期,这才是真正验证配置生效的方式。
还有一个容易被忽略的参数:open_file_cache。它缓存的是文件描述符、文件大小和修改时间,而不是文件内容。高并发访问静态文件时,这个参数能从源头上减少系统调用。可以这样写:
open_file_cache max=65535 inactive=60s; open_file_cache_valid 80s; open_file_cache_min_uses 2;它解决的问题很直接:频繁打开关闭文件句柄的开销被省掉了,静态文件服务的性能能明显提升。但不要一上来就堆一个巨大的数字,要根据实际并发量评估后再调整。
3. 反向代理:把请求安全地交给后端
3.1 不要背完 proxy_pass 就以为会了
反向代理是 Nginx 应用实践里含金量最高的部分,因为这里最容易出现“配置看着没问题,跑起来全不对”的情况。先看一个最常见的问题:proxy_pass后面到底要不要加斜杠。
location /api/ { proxy_pass http://backend:8080; }这个配置下,请求/api/user/info会被完整地转发给http://backend:8080/api/user/info。也就是说,URI 原封不动传给后端。
再看另一种:
location /api/ { proxy_pass http://backend:8080/; }请求/api/user/info会被转发到http://backend:8080/user/info。也就是说,location 匹配到的/api/被替换成了proxy_pass里的/。这个行为差异非常关键。如果你后端的接口路径本身就带/api前缀,第一种写法是对的。如果后端接口前缀和前端 URL 前缀不一致,才需要第二种写法。一旦搞反,接口就会 404,而且还不容易看出来原因。
另一个高频坑是proxy_pass里带变量。如果proxy_pass后面不是静态的 IP 或域名,而是类似$http_host这样的变量,Nginx 的 URI 传递规则会发生变化,容易导致路径被重新拼接过。非必要不使用变量拼接上游地址,这条原则能避免大量诡异问题。
3.2 请求头、超时和 WebSocket 必须一起处理
反向代理不能只写一行 proxy_pass,否则后端拿到的请求信息是残缺的。最常见的三个响应头要手动透传:
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;如果不设置Host,后端收到的主机名会是 Nginx 与后端建立连接时使用的那台服务器的地址,很多按域名做路由或校验的服务就会直接报错。X-Real-IP和X-Forwarded-For则是把真实客户端 IP 传给后端,否则后端的日志和风控都拿不到用户真实地址。X-Forwarded-Proto用来告诉后端原始请求是 HTTP 还是 HTTPS,避免后端生成回调链接时全部变成 http。
超时参数也值得提前设置好。默认的proxy_connect_timeout是 60 秒,proxy_read_timeout也是 60 秒。听起来很长,但有些后端接口如果本身需要长时间计算,比如导出报表、批量任务,60 秒可能反而不够。更常见的问题是反向代理把连接断开得太快,导致前端拿到不完整数据。
proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s;connect_timeout建议短一些,因为后端连接不上大概率是网络或服务问题,没必要让用户一直等着。read_timeout和send_timeout再根据业务接口最慢耗时来定。
WebSocket 场景就更特殊了。普通 HTTP 请求结束后连接就释放了,WebSocket 要求连接长期保持。除了要设置 Upgrade 和 Connection 请求头之外,还要把超时时间拉长:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s;如果不设置read_timeout,默认 60 秒空闲后连接就会被断开,前端 WebSocket 会出现“突然断开、重连失败”的情况,这类问题在日志里还不容易直观看到。
3.3 一个可以当模板的反向代理配置
把上面的要点组合起来,一个比较完整的反向代理配置长这样:
upstream myapp_backend { server 127.0.0.1:8080 max_fails=2 fail_timeout=10s; server 127.0.0.1:8081 max_fails=2 fail_timeout=10s; keepalive 32; } server { listen 80; server_name yourdomain.com; location / { proxy_pass http://myapp_backend; 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; proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; } }proxy_buffering很多人不重视。默认开启时,Nginx 会先接收后端完整响应,攒到缓冲区再发给客户端。这对动态接口有好处,能减少后端压力。但如果你的接口是流式输出,或者说 Server-Sent Events、聊天消息这类,就必须改成proxy_buffering off,否则数据会被攒住,前端拿不到实时内容。这类需求现在不少,建议提前在模板里留好注释,提醒自己别用同一个模板套所有业务。
4. 负载均衡与高可用:从花式转发到故障转移
4.1 upstream 策略怎么选,且看这几个真实条件
Nginx 做负载均衡主要靠 upstream 块。默认的转发策略是轮询,但实际生产环境很少只用轮询,因为后端实例性能未必完全一致,请求处理时间也各不相同。
upstream backend_pool { server 127.0.0.1:8080 weight=3; server 127.0.0.1:8081 weight=2; }weight 意思是后端 8080 每处理 3 个请求,8081 处理 2 个。适合后端机器性能有差距的场景。但要注意,基于 weight 的轮询只考虑请求数量,不考虑后端当前负载。如果某个请求特别慢,还是会造成倾斜。
least_conn策略则考虑当前连接数,适合请求响应时长差异大的业务,谁空闲分给谁。ip_hash能让同一个客户端的请求始终路由到同一台后端,适合本地缓存型业务,比如需要读取用户会话数据的场景。但 ip_hash 在客户端出口 IP 固定时效果明显,如果用户从移动网络访问,IP 频繁变化,就没什么意义。
hash策略更灵活,可以按 URI 或某个变量做一致性哈希。适合需要把相同查询条件打到同一台后端做缓存命中的场景。配置如下:
upstream cache_pool { hash $request_uri consistent; server 127.0.0.1:8080; server 127.0.0.1:8081; }consistent参数让节点变化时影响尽量小的哈希环。如果你在架构里自己实现了缓存服务,这个策略往往比轮询更实用。
4.2 健康检查的被动模式怎么用才靠谱
很多教程会告诉你 Nginx 有 health_check 模块,但要注意,那是商业版本才完整支持的功能。开源 Nginx 默认自带的是被动健康检查,也就是通过转发请求后的结果来判断后端是否健康。
被动检查相关参数就写在 upstream server 后面:
upstream backend_pool { server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; server 127.0.0.1:8081 max_fails=3 fail_timeout=30s; }含义是:在 fail_timeout 的 30 秒内,如果这台后端累计失败 3 次,Nginx 就会在接下来的 fail_timeout 时间内把后端标记为不可用,不再转发新请求。30 秒后会重新放一个探测请求进去,如果成功,就恢复转发;如果继续失败,就再进入不可用状态。
这套机制能兜底,但有个明显短板:它只能感知转发失败的请求。如果一个后端虽然活着,但响应极慢、错误率很高,响应状态码是 200,被动健康检查就无法识别。我在实际项目中会把 max_fails 和 fail_timeout 当成第一道防线,真正精确的健康探测依赖两个手段:一是后端本身暴露一个/healthz接口,二是配合外部监控探活工具,定期检查后端的健康接口,发现异常自动调整 Nginx 配置或服务编排。
4.3 故障转移和“优雅下线”的落地经验
后端发布或扩容时,直接 kill 进程会导致正在处理的请求突然断掉。优雅下线是实践中必须养成的习惯。最直接的操作是:先在 upstream 里标记后端为 down,然后 reload Nginx,等存量请求处理完,再真正停掉后端进程。
upstream backend_pool { server 127.0.0.1:8080 down; server 127.0.0.1:8081; }方法听起来简单,但很多团队懒得做这一步。我在实际工作中见过太多次“直接停掉,然后线上报错”的经历。其实只要多花几秒钟 reload 一次,就能避免一拨用户操作中断。
还要留意,Nginx reload 并不会立刻销毁旧 worker 进程。旧 worker 会在处理完当前持有的连接后才退出,这个受worker_shutdown_timeout影响。这也是为什么“改了配置 reload 后竟然还有旧行为”的原因之一。等待旧 worker 退出是正常现象,不要误以为 reload 失败。
5. HTTPS 落地配置与 Nginx 性能调优
5.1 证书配置的完整基线
现在线上业务如果还在用裸 HTTP,无论如何说不过去。Nginx 配置 HTTPS 的基线其实不复杂,真正的难点在于把安全性配置到位,同时不牺牲性能。
server { listen 443 ssl; http2 on; server_name yourdomain.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; }ssl_protocols至少要有 TLSv1.2 和 TLSv1.3,老旧的 TLSv1.0、TLSv1.1 尽量关掉。ssl_ciphers这一长串都是当前主流推荐的加密套件,优先 ECDHE 握手机制,性能和安全表现都更好。ssl_session_cache特别重要,它允许同一个客户端复用 TLS 会话,大幅度减少重复握手的开销。10m 大约能缓存几千个会话,一般业务够用。
如果想把 HTTP 请求自动转到 HTTPS,再加一个 server 块:
server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri; }这个 301 会把流量整体切到 HTTPS。但要注意,如果你在 HTTPS server 里设置了 HSTS 响应头:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;浏览器会记住这个站点必须用 HTTPS,以后即使你手动访问 http,浏览器也会自动跳转。这个头在测试环境要谨慎使用,一旦设置,浏览器端会强制生效很长时间。
5.2 性能参数别照抄,先弄清这几项
网上流传着各种“Nginx 性能调优配置”,很多其实是把几个参数堆在一起凑字数。参数真实有效,但不考虑机器配置和应用场景,照抄反而坑人。
worker_processes最稳妥的设置是auto,Nginx 会按照 CPU 核数生成 worker。不要贪多,worker 数量超过 CPU 核数不会带来更高吞吐,反而增加上下文切换开销。
worker_connections表示单个 worker 最多同时处理的连接数。
worker_rlimit_nofile 65535; events { worker_connections 10240; }这俩经常被放在一起看。worker_connections设置 10240,理论上也有上限,同时要确保系统文件句柄数足够,否则 Nginx 日志里会出现“too many open files”。worker_rlimit_nofile要大于worker_connections,否则连接打满之前文件句柄先爆了。
sendfile on让 Nginx 直接用系统调用把磁盘文件发给网卡,减少内核态和用户态之间的拷贝。
tcp_nopush on和sendfile配合,能提高大文件传输效率,把多个包攒在一起发。tcp_nodelay on则是给交互类请求用的,避免小包滞留在缓冲区。两者看起来相反,但可以同时存在,因为前者偏重响应内容发送,后者偏重控制小包延迟。
keepalive_timeout控制客户端与 Nginx 的 TCP 长连接保持时间。默认 65 秒左右问题不大。设得太短会增加握手次数,设得太长又可能占用大量连接。配合高并发访问时,应根据真实会话间隔来调整。
5.3 我开始调优时会遵循的顺序
一上来就调参数,往往效果不好。我在实际项目里会按这个顺序做:先用系统和应用默认配置跑起来,压测或观察线上峰值指标,看瓶颈到底在 CPU、内存、磁盘还是网络。再用top看 Nginx worker CPU 占用,用ss看连接数状态,用 access log 统计 QPS 和平均响应时间。数据出来之后,再决定改哪个参数。
如果 CPU 使用率高,先考虑是不是 gzip 级别太高、静态文件没开缓存、反向代理把文件内容重复读取。如果连接状态里大量 TIME_WAIT,优先检查长连接和 keepalive 配置。如果磁盘 IO 高,再考虑 open_file_cache 和 sendfile 的配合。说到底,Nginx 性能调优不是“参数越极端越好”,而是让每个环节的资源消耗更合理。
我还建议记录每次调整前后的压测数据。不要靠“感觉变快了”来评判调整,用具体响应时间、错误率、吞吐量对比。这个习惯能帮你在复杂问题出现时快速定位是哪次改动引起的回归。也算是个人的一个笨办法,但非常有效。
6. 高频问题复盘:我把踩过的坑整理成速查表
6.1 状态码对应的排障路径
日常排障时最直接的就是看状态码。下面是几个出现频率极高的状态码和对应的排查思路:
| 状态码 | 常见原因 | 排查方向 |
|---|---|---|
| 502 Bad Gateway | 后端服务挂了、端口不通、后端拒绝连接 | 检查后端进程、监听端口,手动 curl 后端接口 |
| 504 Gateway Timeout | 后端处理超时,Nginx 等待时间到极限 | 调整 proxy_read_timeout,检查后端慢查询和阻塞 |
| 403 Forbidden | 目录权限不足、index 文件缺失、访问规则禁止 | 检查文件读权限、目录执行权限、配置里的 deny |
| 404 Not Found | root/alias 路径映射错误,或后端接口路径改变 | 检查 URI 被转发后的实际路径,和日志对比 |
| 499 Client Closed Request | 客户端主动断开连接,不代表后端错误 | 检查前端是否超时取消请求,再看后端真实处理时间 |
| 500 Internal Server Error | 后端程序异常 | 主要看后端应用日志,Nginx 只是转述结果 |
一个很隐蔽的问题:502 不一定每次都是后端挂了。如果 Nginx 到后端之间网络不通、后端超时、甚至后端防火墙限制,也可能出现 502。所以排查时不要只看 Nginx 日志,还要手动从 Nginx 所在主机去访问后端地址,确定到底是哪一段链路断了。
6.2 我常用的排查命令与观察点
配置改多了之后,最怕的就是改了但没有生效。每次改配置我必做两件事:
nginx -t nginx -s reload第一行检查语法,第二行才是真正加载新配置。没有语法错误不代表配置符合预期,所以还要再看:
nginx -T这条命令会把当前实际加载的全部配置输出到终端。它能帮你确认自己理解的配置和 Nginx 真正执行的配置是否一致,尤其是 include 嵌套多了以后,很容易出现“我在文件 A 里改了,文件 B 里又覆盖了”的情况。
看实时日志也是基本功:
tail -f /var/log/nginx/error.log tail -f /var/log/nginx/access.logerror.log 至少能看出连接异常、权限不足、文件不存在等直接线索。access.log 的状态码和响应时间,能帮你判断是整体慢还是某些接口慢。条件允许时,最好还看响应时间字段,比如$request_time,如果某个接口平均耗时明显高于其他,问题大概率在后端而不是 Nginx。
6.3 最容易被忽略的“环境类”故障
很多看起来像配置问题的故障,其实是环境问题。比如刚上线一个新服务,Nginx 配置完全正确,但转发请求到后端就超时。查了半天发现是服务器防火墙没放行对应端口。这种问题最气人,因为 Nginx 日志不会给你太多提示。
另一个常见问题是 DNS 解析。upstream 里如果写的是域名,Nginx 在启动时只会解析一次,不会按照系统 DNS 的 TTL 动态更新。如果上游域名切换了 IP,旧 IP 还会被继续使用,直到 reload Nginx。所以,如果你的后端服务使用域名、且 IP 可能会变化,要么写成 IP,要么在变更后主动 reload。不要相信“我改了 DNS,过一会儿就生效”这句话在 Nginx 里同样成立。
还有一个我踩过很多次的坑:文件路径权限。静态文件放在/data/www下,Nginx worker 以nginx用户运行,假设/data目录权限是 700,归属另一个用户,那你很可能收到 403。这个问题的迷惑性在于,配置文件里路径看起来完全正确。排查时不要只盯配置,要去确认 Nginx worker 用户对整条路径上的每一级目录都有读和执行的权限。
最后说一个我个人的体会:Nginx 应用实践越往后,拼的越是耐心和把链路拆开的能力。不要指望一个配置模板解决所有问题,也不要为了“优化”去堆砌网上看来的参数。先把请求路径走通,再把每一步的日志和状态码看清楚,很多疑难问题到最后都是多场景交叉的结果。你真正积累下来的,不是哪一条配置指令,而是一套“流量进来、怎么转发、怎么返回、怎么排障”的完整思路。这套思路,在任何业务形态下都不过时。