Nginx作为反向代理在生产环境中承载着大量请求转发工作,一旦后端服务响应变慢,日志里就会出现一些耐人寻味的现象:同一个客户端请求,upstream_response_time比request_time短好几秒,或者一次正常的外部请求在error.log里却对应着多次upstream连接失败的记录。这些现象背后是Nginx的超时控制与重连机制在起作用。要读懂这些日志,必须先理解Nginx内部是如何处理一次代理请求的。

一、先搞清楚日志中几个关键时间字段的含义
分析超时问题的第一步是打开日志格式,把时间相关的字段都打出来。默认的combined格式信息量太少,建议自定义一个包含详细时间信息的log_format,例如下面这个配置:
log_format timing '$remote_addr - [$time_local] "$request" '
'$status body:$body_bytes_sent '
'rt:$request_time urt:$upstream_response_time '
'uct:$upstream_connect_timeout_used uht:$upstream_header_time '
'addr:$upstream_addr status:$upstream_status '
'cache:$upstream_cache_status';
access_log /var/log/nginx/access.log timing;
这里最容易被混淆的是request_time和upstream_response_time。request_time记录的是从Nginx收到客户端请求的第一个字节开始,到把响应的最后一个字节发给客户端为止的完整耗时,它包含了排队等待、读取请求体、连接后端、转发数据等所有环节。而upstream_response_time是从Nginx向后端发起连接,到收到后端响应结束的时间,如果配置了多个后端并且发生了重试,这个字段会包含多个以逗号分隔的值。
另一个高频出现的字段是upstream_addr。当一次请求发生了重连,你会在这个字段里看到多个地址,比如10.0.1.11:8080, 10.0.1.12:8080,中间用逗号隔开。这就是判断是否发生重试的最直接证据。配合upstream_status字段,如果显示类似504, 200这样的组合,说明第一次尝试超时拿到了504,第二次重试成功返回200,客户端最终看到的是200,但整个过程耗时会叠加。
二、Nginx的超时参数究竟在控制什么
很多人把proxy_connect_timeout和proxy_read_timeout混为一谈,实际上它们控制的是完全不同的阶段。proxy_connect_timeout只约束TCP三次握手的阶段,默认60秒,但Nginx官方建议不要超过75秒。如果后端服务器负载过高导致握手都无法完成,这个超时就会被触发,此时error.log里会出现upstream timed out (110: Connection timed out) while connecting to upstream的记录。
proxy_read_timeout约束的是两个读操作之间的最大间隔,而不是整个响应的总时长。也就是说,只要后端每隔一段时间内有数据返回(哪怕只是一个字节),这个计时器就会不断重置。理解这一点很重要,因为有些慢接口整体响应需要5分钟,但如果它每30秒输出一次心跳数据,那么默认60秒的proxy_read_timeout并不会触发。相反,如果后端处理完请求后长时间不输出任何数据,即使实际处理只花了10秒,只要静默期超过阈值,连接依然会被Nginx主动断开。
还有一个容易被忽视的是proxy_send_timeout,它控制Nginx向后端转发请求体的间隔超时。当客户端上传大文件但网络不稳定、数据断断续续时,这个参数就可能触发。此外要注意,后端服务自己的超时设置(比如PHP-FPM的request_terminate_timeout、Tomcat的connectionTimeout)和Nginx的超时是相互独立的,排查时需要同时检查两端的配置是否匹配,否则会出现Nginx还在等、后端早就掐断连接的情况,表现为大量502。
三、重连与重试的触发条件:proxy_next_upstream的深层逻辑
Nginx在upstream模块中实现了自动重试机制,核心指令是proxy_next_upstream。默认值是error timeout,意思是当连接后端失败(error)或发生超时(timeout)时,Nginx会自动把请求转发给upstream池中的下一台服务器。这个机制是Nginx高可用能力的基础,但它也是很多诡异日志现象的源头。
upstream backend {
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.13:8080 backup;
}
server {
location /api/ {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
proxy_connect_timeout 3s;
proxy_read_timeout 15s;
}
}
上面配置中,proxy_next_upstream_tries限制了总的尝试次数(包括第一次),proxy_next_upstream_timeout限制了所有重试加起来的总耗时。这两个参数非常关键,因为如果不加限制,当后端有多台服务器都出问题时,一次请求可能被依次转发到每台机器上各超时一次。假设5台后端、每台超时60秒,客户端要等5分钟才能收到最终失败,这几乎等于服务不可用。所以生产环境强烈建议显式设置这两个限制值。
更微妙的是重试的副作用问题。对于GET请求,重试是无害的,幂等性保证了重复执行不会产生脏数据。但对于POST这类非幂等请求,如果第一次请求已经到达后端并执行了一半(比如已经写入数据库),Nginx因为读取响应超时又重试到第二台机器,就可能造成重复下单、重复扣款。默认配置下error timeout并不包含non_idempotent标记,所以POST默认不会被重试,这是安全的;但如果你为了提升可用性加上了non_idempotent,就必须确保后端有幂等性设计,比如通过请求ID做去重。
四、keepalive连接池失效引发的隐形超时
还有一种超时重连行为特别隐蔽:后端服务器主动关闭了空闲连接,但Nginx不知道。典型场景是Nginx配置了upstream keepalive,但后端是Tomcat或某些云负载均衡,它们的空闲连接存活时间比Nginx的复用时间短。比如后端60秒就回收空闲连接,Nginx这边连接池里的连接如果闲置了80秒后被再次取用,发送请求时才发现对端已经关闭,Nginx会在日志里记录一条error,然后重新建立连接重发请求。
这种现象在访问日志里通常表现为偶发的、随机的短延迟,upstream_addr只有一个地址,但error.log里能看到recv() failed (104: Connection reset by peer) while reading response header from upstream。解决办法有两个方向:一是缩短后端的空闲超时,让它大于Nginx的连接复用窗口;二是在Nginx侧不使用长连接或者把keepalive_requests调小,加快连接的淘汰速度。推荐前者,因为长连接能显著降低握手开销。
此外,如果开启了LVS或四层负载均衡,还要注意中间设备对空闲连接的会话超时。曾经有案例是防火墙把闲置120秒的TCP连接静默丢弃,导致Nginx复用这些连接时发送的数据石沉大海,一直等到proxy_read_timeout才触发重连,日志表现为周期性的几十秒延迟。这类问题在Nginx自身配置上完全看不出来,必须结合网络拓扑一起排查。
五、一套可落地的排查流程
总结上面的内容,遇到超时重连类问题时建议按以下顺序排查。第一步,在access.log中筛选$upstream_response_time大于阈值的记录,观察$upstream_addr是否出现多个地址,多个地址说明发生了重试,单地址说明是一次慢响应。第二步,对照error.log中同一时间点的报错关键字,while connecting to upstream对应连接阶段超时,问题多半在网络或后端进程未监听;while reading response header对应后端处理慢,问题在后端应用本身;Connection reset by peer则要怀疑连接被中间层或后端主动重置。
第三步,用下面的统计命令快速定位问题分布。比如统计各状态码占比和超时请求的后端分布:
# 统计499和504的每分钟数量趋势
awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1-2 | sort | uniq -c
# 找出重试过的请求(upstream_addr含多个地址)
grep -E 'addr:[^ ]*,' /var/log/nginx/access.log | head -20
# 统计每个后端实例出现的超时次数
awk -F'urt:|urt:' '{print $2}' /var/log/nginx/access.log | awk -F' ' '{print $1}' | sort | uniq -c | sort -rn
最后一步才是调参。调参的原则是:连接超时要短,让失败快速暴露;读超时按后端P99耗时的两到三倍来设置;重试次数控制在2到3次,并且总耗时兜底。同时务必确认客户端(或最外层的接入层)的超时大于Nginx的重试总耗时上限,否则Nginx还在重试,客户端已经断开,日志里就会留下一堆499,这是最浪费资源的组合。把这几个环节的时间链路理顺之后,超时重连行为就不再是黑盒,日志中的每一个字段都能对应到明确的系统动作,定位问题也会变得有的放矢。
Nginx日志分析upstream超时连接重试修改时间:2026-09-07 21:32:54