从一次504错误说起
线上流量突增,某PHP应用的后端服务器开始出现间歇性超时。运维同学在Nginx错误日志里看到大量upstream timed out记录。检查配置后发现proxy_pass指向的单台服务器已成为瓶颈,而未使用upstream做后端组。这让我意识到:多数人会用proxy_pass http://backend,但真正理解upstream块与proxy_pass配合规则的人并不多。本文不讲理论翻译,只讲你调试时一定会遇到的那些坑。
upstream块:必须放在http层
在Nginx配置中,upstream块只能出现在http上下文内,不能放在server或location里。一个典型示例:
http {
upstream myapp {
server 192.168.1.10:8080 weight=3;
server 192.168.1.11:8080;
server 192.168.1.12:8080 backup;
}
server {
listen 80;
location / {
proxy_pass http://myapp;
}
}
}
这里proxy_pass http://myapp中的myapp必须与upstream定义的名称完全一致。若upstream名称包含下划线或特殊字符,建议用引号包裹测试,但官方推荐使用字母数字和下划线。
server指令参数详解
- weight:权重,默认1,越大分配请求越多
- max_fails:允许失败次数,默认1,超过后Nginx将标记节点不可用
- fail_timeout:节点被标记为不可用后的冷却时间,默认10秒
- backup:标记为备用节点,仅在主节点全部不可用时启用
- down:永久标记为不可用,用于维护时临时移除而不修改配置
注意:max_fails和fail_timeout搭配使用才能生效。很多新手只配了max_fails=3却忽略fail_timeout,导致节点频发切换。
proxy_pass的URI陷阱
proxy_pass是否带URI(即/后面的路径)直接影响转发行为。看三个常见写法:
不带URI:原样转发
location /api/ {
proxy_pass http://backend;
}
# 请求 /api/user -> 转发到 /api/user
此时upstream里的路径完全由客户端请求决定。如果后端服务需要剥离前缀,就必须用以下写法。
带URI:替换location匹配部分
location /api/ {
proxy_pass http://backend/newapi/;
}
# 请求 /api/user -> 转发到 /newapi/user
注意:proxy_pass的URI部分会直接替换掉location匹配到的路径。如果location使用正则表达式,那么带URI的proxy_pass会报错(除非使用rewrite配合)。官方文档明确:如果proxy_pass包含URI,且location使用正则,则不会自动替换,需要手动rewrite。
根location的特殊性
location / {
proxy_pass http://backend/;
}
# 请求 / -> 转发到 / (因为location匹配的是/,被替换为空再拼接/)
# 实际测试:location / 时带URI的proxy_pass会将/请求转发到 / 根路径,但看似没区别
一个常见错误是:所有请求都加上/导致后端路径多一层斜杠。例如proxy_pass http://myapp/如果后端本身期望/api,却收到//api(双斜杠),某些框架会报400。所以建议根据后端实际情况测试。
负载均衡算法:不止轮询
upstream默认使用加权轮询(weighted round-robin)。多数场景够用,但以下算法可定向配置:
| 指令 | 行为 | 典型场景 |
|---|---|---|
least_conn | 转发给当前活跃连接数最少的节点 | 长连接服务如WebSocket |
ip_hash | 对客户端IP进行哈希,固定转发到同一节点 | 需要session持久化的应用(无共享会话时) |
hash $cookie_xxx | 自定义哈希键,如cookie、uri等 | 基于用户标识的粘性会话 |
random two least_conn | 随机选取两个节点,再选连接数少的 | 高并发下避免热点,但又保持相对均匀 |
配置方式:在upstream块内使用上述指令。注意ip_hash与backup节点配合时,backup节点只有在主节点全部不可用时才会接收请求,而哈希规则仅对主节点生效。
ip_hash的局限性
如果客户端经过CDN或代理,所有请求的IP可能相同,导致流量全部打到一个后端节点。此时应改用hash $http_x_forwarded_for,或者使用sticky模块(Nginx Plus或第三方模块)。开源版Nginx没有内置sticky,可通过cookie模块代替:
upstream backend {
hash $cookie_route consistent;
server 10.0.0.1;
server 10.0.0.2;
}
由应用在下发cookie时设定route值,配合consistent(一致性哈希)在节点变更时影响最小。
proxy_pass与HTTPS后端
后端服务可能是HTTPS,此时upstream中需指定端口443:
upstream secure {
server 192.168.1.10:443;
}
server {
location / {
proxy_pass https://secure;
proxy_ssl_verify off; # 如果后端证书是自签的,需要关闭验证
}
}
注意:proxy_pass的协议必须与后端一致,如果后端是HTTP则写http://,是HTTPS则写https://。否则Nginx直接报502错误。另外,反向代理到HTTPS后端时需配置proxy_ssl_name与proxy_ssl_server_name以避免SNI问题。
常见故障场景与验证方法
配置完upstream后,务必执行以下验证:
- 语法检查:
nginx -t输出成功方可重载 - 请求分发验证:在每台后端返回不同header,如
add_header X-Backend $hostname;,再用curl -I查看 - 超时回滚:模拟一台后端宕机,观察请求是否自动切换到健康节点
- 日志监控:访问日志中增加
$upstream_addr、$upstream_status、$upstream_response_time字段
举个例子,在日志格式中加入:
log_format upstreamlog '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'upstream: $upstream_addr status=$upstream_status '
'response_time=$upstream_response_time';
access_log /var/log/nginx/access.log upstreamlog;
从日志可以清晰看到每个请求的是哪个后端IP、响应状态码以及处理耗时。
调优参数:防止雪崩
高并发下,upstream层有三个参数容易成为瓶颈:
- proxy_connect_timeout:与后端建立连接的超时时间,默认60秒,内网建议5秒
- proxy_read_timeout:从后端读取响应的超时,默认60秒。如果后端处理慢,可以适当增大到120秒,但需配合应用日志排查
- proxy_send_timeout:向后端发送请求的超时,默认60秒
- keepalive:upstream块内单独配置,用于复用后端连接。如
keepalive 32;,需配合proxy_http_version 1.1;和proxy_set_header Connection ""
一个高效的反向代理配置示例:
http {
upstream app {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
keepalive 64;
}
server {
listen 80;
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://app;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
}
}
注意:keepalive参数只会维持Nginx到后端的连接池,并不影响客户端到Nginx的连接。很多误解认为它优化用户长连接,实际上它降低的是Nginx与后端之间的TCP握手开销。
回滚与灰度发布实战
在生产环境中修改upstream配置可能引发连锁反应。一个安全做法:
- 先新增一台服务器到upstream,权重设为0(
server IP weight=0;),但Nginx不支持weight=0,需要临时标记为down - 用
server IP down;加入配置,重载后新增节点不接收流量 - 移除
down再重载,节点开始接收流量。如果出现问题,立刻加回down并重载 - 对于核弹级回滚,建议保留旧版本的upstream块,通过
include指令动态切换:include /etc/nginx/upstream_current.conf;,修改只需替换此文件再重载
回滚时要注意nginx -s reload是优雅重载,不会中断现有连接。但如果新旧配置中upstream名称相同但后端列表变化,Nginx会立刻使用新列表处理新连接,旧连接仍由旧worker处理。这在实际中几乎没有副作用,但若节点摘除太快,正在处理的长请求可能被中断。
总结
proxy_pass + upstream是Nginx反向代理的核心组合,配置本身并不复杂,但URI替换规则、负载均衡算法选择、超时与健康检查参数往往成为隐患。建议每次修改后执行单元测试,并用日志验证分发逻辑。记住:配置文件中的每一行都有原因,不带理解地抄配置才是线上故障的根源。
延伸阅读
