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的unwrappanic,这个下游连接的任务结束,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的请求头都没人读。
出处
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 没有改这两行。
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)换下一个。