Rust in Everything
基于 pingora-core 0.9.0

failed to lookup address information

主线位置:请求主线的 upstream_peer 这一步。用户在这里构造 HttpPeer,域名解析就发生在构造函数里,比连接器、连接池都早。机制见 L4 连接的「域名解析」一节。

flowchart TB
  up["upstream_peer"] --> new["HttpPeer::new 域名"]
  new --> gai["getaddrinfo 同步"]
  gai -->|出错| panic["unwrap panic"]
  gai -->|成功| first["只取第一个地址"]
  first --> conn["连接它"]
  conn -->|拒绝| e502["502 不试下一个"]
  panic --> empty["curl 52"]

实验 crate exp-pingora/pitb-lab,bin p15_dns,脚本 tools/p15.sh(debug);M2 Max、macOS 27、rustc 1.99.0,pingora 0.9.0,tokio 钉在 1.53.2(原因见怎么读这一卷)。代理在 19300,上游是 python3 -m http.server 19301 --bind 127.0.0.1。

现象

upstream_peer 直接把 "host:port" 交给 HttpPeer::new:

async fn upstream_peer(&self, s: &mut Session, _: &mut ()) -> Result<Box<HttpPeer>> {
  let path = s.req_header().uri.path().to_string();
  let peer = if path == "/slow" {
    HttpPeer::new(SlowResolve("127.0.0.1:19301"), false, String::new())
  } else {
    HttpPeer::new(target(&path), false, String::new())
  };
  Ok(Box::new(peer))
}

target 把 /name 映射到 localhost:19301,/bad 映射到 no-such-host.invalid:80,其他路径是 127.0.0.1:19301。SlowResolve 是一个自定义的 ToSocketAddrs,先 std::thread::sleep 1 秒再解析,模拟一次慢的 getaddrinfo:它和真的 getaddrinfo 一样是普通的阻塞调用。curl 的结果:

--- /fast
/fast 200 0.005276s exit=0
--- /slow, /fast 100ms later
/fast 200 0.900896s exit=0
/slow 200 1.007557s exit=0
--- /name
/name 502 0.003445s exit=0
--- /bad
/bad 000 0.011830s exit=52
--- /fast
/fast 200 0.001863s exit=0

代理的日志:

/name: HttpPeer::new took 2.466417ms, addr [::1]:19301
[2026-10-29T06:37:13Z ERROR pingora_proxy] Fail to proxy: Upstream ConnectRefused context: Fail to connect to addr: [::1]:19301, scheme: HTTP cause:  context: Fail to connect to [::1]:19301 cause: Connection refused (os error 61), status: 502, tries: 1, retry: false, GET /name, Host: 127.0.0.1:19300

thread 'Pingora HTTP Proxy Service' (5857449) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pingora-core-0.9.0/src/upstreams/peer.rs:719:56:
called `Result::unwrap()` on an `Err` value: Custom { kind: Uncategorized, error: "failed to lookup address information: nodename nor servname provided, or not known" }

三件事:

  • /bad:panic。 解析失败,peer.rs:719 的 unwrap panic,这个下游连接的任务结束,curl 拿到 Empty reply from server(退出码 52)。没有 502,也不会调用 fail_to_proxy;紧接着的 /fast 正常,服务没停。
  • /name:502。 localhost 在 macOS 上解析出 ::1 和 127.0.0.1,排在前面的是 ::1。上游只听 127.0.0.1,于是 ConnectRefused,而 127.0.0.1 从头到尾没被试过。
  • /slow:别人一起等。 /slow 发出 100 ms 后发的 /fast 用了 0.90 秒。默认 threads: 1,唯一的工作线程卡在解析里,连 /fast 的请求头都没人读。

出处

HttpPeer::new:

    pub fn new<A: ToInetSocketAddrs>(address: A, tls: bool, sni: String) -> Self {
        let mut addrs_iter = address.to_socket_addrs().unwrap(); //TODO: handle error
        let addr = addrs_iter.next().unwrap();
        Self::new_from_sockaddr(SocketAddr::Inet(addr), tls, sni)
    }

ToInetSocketAddrs 就是 std::net::ToSocketAddrs。第 719 行第 56 列是第一个 unwrap,解析出错时 panic;第 720 行的 next().unwrap() 管「解析成功但一个地址都没有」。参数是 IP 字面量时,标准库先按地址解析,成功就不碰 getaddrinfo,所以 "127.0.0.1:19301" 不会阻塞。返回值不是 Result,调用方没有机会处理错误。tag 之后到 84f21ee,main 上的 peer.rs 没有改这两行。

📝NOTE
冷知识:new_uds 有过同样的 unwrap,改掉了;new 没改

0.1.0 的 new_uds 也是一行 UnixSocketAddr::from_pathname(Path::new(path)).unwrap(); //TODO: handle error,返回 Self。提交 20cac2e(「expose new_uds err」,没有 PR 编号,首次进入 0.3.0)把它改成返回 Result<Self>,错误用 or_err(SocketError, "invalid path") 包起来,并同步改了测试里的调用。同一个文件里 new 的两个 unwrap 和那句 //TODO: handle error 从 0.1.0 留到了 0.9.0。现在 HttpPeer::new_uds 返回 Result,而 new 遇到坏域名只能 panic。

修法

在 upstream_peer 里自己解析,交给 HttpPeer::new 的永远是 SocketAddr:

async fn upstream_peer(&self, s: &mut Session, ctx: &mut Ctx) -> Result<Box<HttpPeer>> {
  let path = s.req_header().uri.path().to_string();
  if ctx.addrs.is_empty() {
    let r: std::io::Result<Vec<SocketAddr>> = if path == "/slow" {
      // the same blocking resolver, moved to tokio's blocking pool
      match tokio::task::spawn_blocking(|| SlowResolve("127.0.0.1:19301").to_socket_addrs()).await {
        Ok(r) => r.map(|i| i.collect()),
        Err(e) => Err(std::io::Error::other(e)),
      }
    } else {
      tokio::net::lookup_host(target(&path)).await.map(|i| i.collect())
    };
    // into_up(): an upstream-side error, so fail_to_proxy answers 502 instead of 500
    ctx.addrs = r.map_err(|e| Error::explain(ErrorType::ConnectNoRoute, format!("resolve {}: {e}", target(&path))).into_up())?;
  }
  let addr = ctx.addrs.get(ctx.next).copied().ok_or_else(|| Error::explain(ErrorType::ConnectNoRoute, "no address left"))?;
  ctx.next += 1;
  Ok(Box::new(HttpPeer::new(addr, false, String::new())))
}

fn fail_to_connect(&self, _s: &mut Session, _p: &HttpPeer, ctx: &mut Ctx, mut e: Box<Error>) -> Box<Error> {
  // nothing was sent yet, so trying the next address is safe for every method
  if ctx.next < ctx.addrs.len() {
    e.set_retry(true);
  }
  e
}
  • tokio::net::lookup_host 把 getaddrinfo 放进 tokio 的阻塞线程池(tokio 卷的阻塞线程池),工作线程在等待期间去处理别的连接;自己的阻塞解析器同理用 spawn_blocking。
  • 解析失败变成一个 Error 返回,走正常的 fail_to_proxy。用 Error::explain 建的错误来源是 Unset,默认会回 500;into_up() 把来源改成上游,回 502(状态码怎么选见错误、重试与故障转移)。
  • 所有地址存在 CTX 里。fail_to_connect 只在连接没建成时调用,这时什么都还没发给上游,换下一个地址重试对任何方法都安全;upstream_peer 被再调一次时取下一个地址。

同样的 curl 序列,MODE=fix:

--- /fast
/fast 200 0.003651s exit=0
--- /slow, /fast 100ms later
/fast 200 0.002536s exit=0
/slow 200 1.007358s exit=0
--- /name
/name 200 0.005888s exit=0
--- /bad
/bad 502 0.008119s exit=0
--- /fast
/fast 200 0.002083s exit=0
/name: resolved [[::1]:19301, 127.0.0.1:19301] in 3.587959ms
/name: try [::1]:19301
[2026-10-29T06:37:35Z WARN  pingora_proxy] Fail to proxy: Upstream ConnectRefused context: Fail to connect to addr: [::1]:19301, scheme: HTTP cause:  context: Fail to connect to [::1]:19301 cause: Connection refused (os error 61), tries: 1, retry: true, GET /name, Host: 127.0.0.1:19300
/name: try 127.0.0.1:19301
[2026-10-29T06:37:35Z ERROR pingora_proxy] Fail to proxy: Upstream ConnectNoRoute context: resolve no-such-host.invalid:80: failed to lookup address information: nodename nor servname provided, or not known, status: 502, tries: 1, retry: false, GET /bad, Host: 127.0.0.1:19300

/fast 不再被 /slow 拖住(2.5 ms);/name 先试 ::1 被拒,fail_to_connect 标成可重试,第二次用 127.0.0.1 成功;/bad 是一个带原因的 502。生产里还应该缓存解析结果、按 TTL 刷新,pingora 不做这些。

怎么确认没中招

  • 搜 HttpPeer::new( 和 HttpPeer::new_mtls(:第一个参数如果是 String / &str 且可能是域名,就是这个坑。参数是 SocketAddr 或 IP 字面量时不解析。
  • 日志里出现 panicked at .../upstreams/peer.rs:719:56,客户端同时看到空回复(curl 52),就是解析失败。
  • 在 macOS 上用 localhost 连只听 IPv4 的上游,日志里的地址是 [::1]:端口。

小结

  • HttpPeer::new 用 std 的 ToSocketAddrs 在工作线程上同步解析,只取第一个地址,解析失败 panic(peer.rs:719:56),客户端只看到空回复。
  • 慢解析会卡住同一工作线程上的其他请求;localhost 先给 ::1,另一个地址不会被试。
  • 修法:lookup_host / spawn_blocking 异步解析,失败返回 Error,地址全留在 CTX 里,fail_to_connect 里 set_retry(true) 换下一个。

💬 关联讨论

发起讨论
加载中...