上游不回应,连接永远挂着
主线位置:请求主线第四步到第七步,建上游连接、发请求、读响应头。每一段等多久由
HttpPeer的PeerOptions决定,默认全是None。默认值表见 Peer,两个建连超时怎么分段生效见 L4 连接。
sequenceDiagram participant C as curl participant P as pingora participant U as 上游 C->>P: GET P->>U: TCP 连上 P->>U: 请求或 ClientHello Note over U: 一个字节也不回 Note over P: 没有超时 一直等 C--xP: 125 s 后放弃
实验 crate exp-pingora/pitb-lab,bin p17_timeout,脚本 tools/p17.sh,上游 tools/hold.py(debug);M2 Max、macOS 27、rustc 1.99.0,pingora 0.9.0,tokio 钉在 1.53.2(原因见怎么读这一卷)。hold.py 在 19321 上 accept、读、记日志,但从不回一个字节:对明文请求是「上游卡住了」,对 TLS 是「ServerHello 永远不来」。
现象
代理在 19320,upstream_peer 只是 HttpPeer::new("127.0.0.1:19321", tls, "silent.example".to_string()),/plain 不加 TLS,/tls 加。两个 curl 同时发,各自 --max-time 125:
/plain 000 125.001415s exit=28
/tls 000 125.003102s exit=28
上游日志(从启动算起的秒数):
[ 1.5s] conn#1 accepted
[ 1.5s] conn#1 got 1492 bytes
[ 1.5s] conn#2 accepted
[ 1.5s] conn#2 got 83 bytes
[ 126.5s] conn#2 closed by peer
[ 127.0s] conn#1 closed by peer
代理日志只有一条:
[2026-10-29T06:42:10Z ERROR pingora_proxy] Fail to proxy: Downstream ConnectionClosed context: Peer: addr: 127.0.0.1:19321, scheme: HTTP, sni: silent.example cause: context: Prematurely before response header is sent, status: 0, tries: 1, retry: false, GET /plain, Host: 127.0.0.1:19320
logging /plain after 125.0s: status=None err=Some("ConnectionClosed")
- 两边都等满了 125 秒,代理自己从没放弃过。
conn#1是 TLS 那条(收到的 1492 字节是 ClientHello),conn#2是明文请求(83 字节)。- curl 在 126.5 秒放弃时,明文那条立刻被代理关掉,日志里是一条「下游先走了」的
Downstream ConnectionClosed。 - TLS 那条没有任何日志,到 127.0 秒脚本杀掉代理进程才断。客户端早就走了,代理还在等这个永远不来的 ServerHello。
出处
PeerOptions::new 里和时间有关的五个字段全是 None:
PeerOptions {
bind_to: None,
connection_timeout: None,
total_connection_timeout: None,
read_timeout: None,
idle_timeout: None,
write_timeout: None,
// ...
connection_timeout包住 TCP 连接,TLS 连接器又用它包住握手;total_connection_timeout包住整段建连(L4 连接)。两个都是None时,握手就是一个没有期限的await。read_timeout/write_timeout在proxy_1to1开头交给上游会话:client_session.read_timeout = peer.options.read_timeout;。None时读响应头不设超时。- 为什么明文那条在客户端走后就收场:发完请求后进入双向转发,下游半边一直在读客户端,读到关闭就报
Downstream ConnectionClosed,整个请求结束(双向转发)。TLS 那条卡在更早的proxy_to_upstream里self.client_upstream.get_http_session(&*peer).await这一行,这时还没有任何代码在看下游连接。
下游那一侧也有超时的空白(第一个请求之后的 keep-alive),见 第一个请求之后,连接永不空闲超时。
本站网关的 upstream_peer 也是 HttpPeer::new 加默认选项,app 卡住时网关同样不限时等待,见在本站中。
修法
按上游的实际情况给每一段一个上限:
let mut peer = HttpPeer::new("127.0.0.1:19321", tls, "silent.example".to_string());
let o = &mut peer.options;
o.connection_timeout = Some(Duration::from_secs(1)); // TCP connect, and again the TLS handshake
o.total_connection_timeout = Some(Duration::from_secs(2)); // TCP + TLS together
o.read_timeout = Some(Duration::from_secs(5)); // each read from upstream
o.write_timeout = Some(Duration::from_secs(5)); // each write to upstream
o.idle_timeout = Some(Duration::from_secs(30)); // pooled connection
MODE=fix,curl 的上限改成 10 秒:
/plain 502 5.023250s exit=0
/tls 502 1.021815s exit=0
[2026-10-29T06:39:53Z ERROR pingora_proxy] Fail to proxy: Upstream ConnectTimedout context: connecting to server addr: 127.0.0.1:19321, scheme: HTTPS, sni: silent.example, timeout 1s cause: local address 127.0.0.1:56072, status: 502, tries: 1, retry: false, GET /tls, Host: 127.0.0.1:19320
logging /tls after 1.0s: status=Some(502) err=Some("ConnectTimedout")
[2026-10-29T06:39:57Z ERROR pingora_proxy] Fail to proxy: Upstream ReadTimedout context: Peer: addr: 127.0.0.1:19321, scheme: HTTP, sni: silent.example cause: context: while reading response headers, status: 502, tries: 1, retry: false, GET /plain, Host: 127.0.0.1:19320
logging /plain after 5.0s: status=Some(502) err=Some("ReadTimedout")
- TLS 那条 1 秒后
ConnectTimedout:报错格式connecting to server ... timeout 1s是 TLS 连接器那一层的,TCP 早就连上了,这 1 秒花在握手上。 - 明文那条 5 秒后
ReadTimedout(while reading response headers),回 502,不是 504(错误、重试与故障转移)。 - 连接失败默认不重试;超时之后要换后端,在
fail_to_connect里set_retry(true)(同一页)。
怎么确认没中招
- 搜
HttpPeer::new(:每一处后面有没有给options的connection_timeout、read_timeout赋值。用pingora-load-balancing的也一样,Backend不带超时,HttpPeer照样是自己构造的。 - 上游机器上
netstat看到大量来自代理、停在ESTABLISHED很久不动的连接,而客户端早已断开:多半是没有超时的握手或读。 - 拿上面的
hold.py当上游跑一次:curl 的--max-time先到,就是没设。
小结
PeerOptions默认没有任何超时:TCP、TLS 握手、读响应头、写请求、空闲连接都不限时。- 实测上游不回应时代理等满了 125 秒;客户端走后,明文那条因为下游半边在读而收场,卡在 TLS 握手的那条没人管,直到进程退出。
- 修法:在
upstream_peer里给connection_timeout、total_connection_timeout、read_timeout、write_timeout、idle_timeout赋值;实测分别在 1 秒、5 秒回 502。
💬 关联讨论
发起讨论加载中...