Rust in Everything
基于 pingora-proxy 0.9.0

(52) Empty reply from server

主线位置:请求主线第五步之后的重试循环。process_request 在一个 while 里反复调 proxy_to_upstream,次数上限是配置里的 max_retries。循环本身见 process_request,重试标记和次数见错误、重试与故障转移。

flowchart TB
  start["retries = 0"] --> cond{"retries 小于 max_retries"}
  cond -->|是| try["proxy_to_upstream"]
  try --> ok{"成功或不可重试"}
  ok -->|否| cond
  ok -->|是| fin["finish"]
  cond -->|"否 max_retries 0"| none["一次都不试"]
  none --> fin
  fin --> empty["没有响应 关连接"]

实验 crate exp-pingora/pitb-lab,bin p20_retries,脚本 tools/p20.sh,配置 conf/p20-zero.yaml 和 conf/p20-one.yaml(debug);M2 Max、macOS 27、rustc 1.99.0,pingora 0.9.0,tokio 钉在 1.53.2(原因见怎么读这一卷)。代理在 19350,上游 python3 -m http.server 19351,一直正常。

现象

想让代理「失败就算了,不要重试」,在配置文件里写:

---
version: 1
threads: 1
max_retries: 0

程序用 Server::new(Some(Opt::parse_args())) 读 -c,upstream_peer、fail_to_proxy、logging 各打一行日志,RUST_LOG=info:

[curl exit 52]
[2026-10-29T06:43:42Z INFO  pingora_core::server::bootstrap_services] Bootstrap starting
[2026-10-29T06:43:42Z INFO  pingora_core::server::bootstrap_services] Bootstrap done
max_retries=0
[2026-10-29T06:43:42Z INFO  pingora_core::server] Server starting
[2026-10-29T06:43:42Z INFO  pingora_core::server] Starting services in dependency order: ["Pingora HTTP Proxy Service"]
[2026-10-29T06:43:42Z INFO  pingora_core::server] Starting service: Pingora HTTP Proxy Service
logging /fast status=None err=None

上游是好的,curl 却得到 Empty reply from server。upstream_peer 没有被调用,fail_to_proxy 也没有,logging 收到的是 err=None、没有响应。没有 warn,也没有 error。

出处

process_request 的循环:

        let mut retries: usize = 0;

        let mut server_reuse = false;
        let mut proxy_error: Option<Box<Error>> = None;

        while retries < self.max_retries {
            retries += 1;

            let (reuse, e) = self.proxy_to_upstream(&mut session, &mut ctx).await;
            // ...

先比较、再加一,所以 max_retries 是 proxy_to_upstream 的调用次数,也就是总尝试次数,不是「第一次之外再重试几次」。等于 0 时循环一次都不进,proxy_error 停在 None;之后只在有错误时才调 fail_to_proxy,于是没人写响应,finish 拿到「不复用、没有错误」,关掉连接。字段的文档注释写的是「The maximum number of retries that will be attempted when an error is retry-able」,读起来正好是用户以为的那个意思(ServerConf)。

ServerConf::validate 只检查 max_blocking_threads 和两个运行时指标字段不为 0,不看 max_retries,0 被原样接受。tag 之后到 84f21ee,main 上没有改这段循环。这是一个上游问题候选,修法要么把循环改成至少一次,要么在配置校验里拒绝 0。

📝NOTE
冷知识:0.5.0 之前,这个数写死在代码里

pingora-proxy/src/lib.rs 原来有一行 const MAX_RETRIES: usize = 16;,循环是 while retries < MAX_RETRIES。提交 6c5d602(「Add ability to configure max retries for upstream proxy failures」,没有 PR 编号,首次进入 0.5.0,CHANGELOG 0.5.0 有这一条)删掉了常量,在 ServerConf 里加了 max_retries(默认 DEFAULT_MAX_RETRIES = 16),HttpProxy 从配置读进来,循环条件换成 self.max_retries;同一个提交加了 examples/backoff_retry.rs,示例配置里写的是 max_retries: 5。常量是 16 的时候,「0」根本写不出来;变成配置项以后,循环的「先比较再加一」没变,0 就成了「一次都不试」。

修法

最少写 1。1 的意思才是「只试一次,不重试」:

---
version: 1
threads: 1
max_retries: 1
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.14.10
Date: Thu, 29 Oct 2026 06:43:34 GMT
Content-type: application/octet-stream
Content-Length: 11
Last-Modified: Thu, 29 Oct 2026 06:37:09 GMT
Connection: keep-alive

hello fast
[curl exit 0]
...
max_retries=1
upstream_peer /fast
logging /fast status=Some(200) err=None

配置来自文件时,在启动时校验一次,比每个请求都静悄悄失败好查:

let mut server = Server::new(Some(Opt::parse_args())).unwrap();
server.bootstrap();
// fail at startup instead of answering every request with nothing
assert!(server.configuration.max_retries >= 1, "max_retries must be at least 1: it counts attempts, not retries");
max_retries=0

thread 'main' (5881982) panicked at src/bin/p20_retries.rs:45:5:
max_retries must be at least 1: it counts attempts, not retries

只想在某些请求上关掉重试,不必动全局配置:重试只发生在错误被标成可重试时,在 fail_to_connect / error_while_proxy 里 e.set_retry(false) 就是这个请求只试一次(错误、重试与故障转移)。

怎么确认没中招

  • 配置文件和代码里搜 max_retries:值为 0 就是这个坑;值为 N 时,最多尝试 N 次,不是 N+1 次。
  • 客户端大量 Empty reply from server(curl 52)、浏览器报连接被重置,而代理日志里没有任何 error,上游也没收到请求。
  • 在 logging 里打 session.response_written():None 且 err 也是 None,说明请求根本没走到上游。

小结

  • max_retries 是总尝试次数,循环先比较后加一;默认 16。
  • 设成 0:一次都不连上游,fail_to_proxy 不调用,客户端空回复,日志里没有错误。0.5.0 起它才可配置(6c5d602),main 上仍然如此。
  • 修法:最少 1;启动时校验;只想对个别请求不重试,就在钩子里 set_retry(false)。

💬 关联讨论

发起讨论
加载中...