Rust in Everything
基于 pingora-core 0.9.0

上游不回应,连接永远挂着

主线位置:请求主线第四步到第七步,建上游连接、发请求、读响应头。每一段等多久由 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。

💬 关联讨论

发起讨论
加载中...