只看状态码就丢掉响应,连接没法复用
主线位置:响应主线的最后一步,连接什么时候回到池里。逐种情况(读完、drop 小响应、drop 大响应、拿着不读)的实测和 HTTP/2 的对照见 响应体 的「连接什么时候回池」;hyper 那一层的代码见 hyper 卷的 连接池。这一页看一个常见写法:健康检查、批量探测这类只要状态码的请求。
flowchart TB
r["拿到 Response"] --> st["只看 status"]
st --> d["drop Response"]
d --> v{"HTTP 版本"}
v -->|HTTP/1.1| dr["试着排空一次"]
dr -->|排空| pool["回池复用"]
dr -->|没排空| close["关掉连接"]
close --> next["下个请求 新连接"]
v -->|HTTP/2| rst["只重置这个流"]
rst --> pool
实验 crate:exp-reqwest/pita-lab,src/bin/p04_body.rs。服务端是本机 testsrv 的 HTTPS,?size=N 回一个 N 字节的 body,Proto::H1 / Proto::H2 让 ALPN 只给一种协议。每种组合起一个新服务器、一个新 Client,先用一个临时客户端预热 aws-lc-rs。M2 Max、macOS 27、rustc 1.99.0,release。
现象
for _ in 0..n {
let r = c.get(&url).send().await.unwrap();
assert!(r.status().is_success()); // all the caller wanted
if read {
r.bytes().await.unwrap();
}
}
20 个请求,body 100 字节或 1 MiB,读完或直接丢掉:
https HTTP/1.1 body 100 B dropped 20 requests: conns=1 tls=1 4.0ms
https HTTP/1.1 body 100 B read to end 20 requests: conns=1 tls=1 4.2ms
https HTTP/1.1 body 1048576 B dropped 20 requests: conns=20 tls=20 30.9ms
https HTTP/1.1 body 1048576 B read to end 20 requests: conns=1 tls=1 21.0ms
https HTTP/2 body 100 B dropped 20 requests: conns=1 tls=1 4.8ms
https HTTP/2 body 100 B read to end 20 requests: conns=1 tls=1 3.9ms
https HTTP/2 body 1048576 B dropped 20 requests: conns=1 tls=1 23.1ms
https HTTP/2 body 1048576 B read to end 20 requests: conns=1 tls=1 22.6ms
连跑三遍,连接数和握手数每遍都一样,耗时在 ±3 ms 内(uptime 负载 5.4)。
- HTTP/1.1 上丢掉 1 MiB 的响应:20 个请求 20 条连接、20 次握手。 读完的话只要 1 条。
- 丢掉 100 字节的响应没事,照样复用。
- HTTP/2 上怎么丢都只有 1 条连接。
- 在回环上,丢掉反而比读完慢(31 ms 对 21 ms):省下的 1 MiB 传输抵不上 19 次额外的握手。真实网络上每次新连接还要多付 TCP 和 TLS 的往返。
原因
reqwest 在这里什么也没做,Response 被 drop 时,它包着的 hyper body 也被 drop,后面全是 hyper 的事。
HTTP/1.1 一条连接同一时间只跑一个请求。 上一个响应的 body 没读完,连接上还躺着没读的字节,下一个请求不能用它。body 的接收方被丢掉后,hyper 的客户端调 Conn::poll_drain_or_close_read:再试着读一次 body,读到结尾(剩下的字节已经在读缓冲里)就算排空,连接照常 keep-alive;读不完就 close_read,连接作废。100 字节在读响应头时就一起进了缓冲,一次就排空;1 MiB 不可能。连接作废以后,池里没有可用连接,下一个请求新建。
HTTP/2 一条连接上跑很多流。 一个响应只占一个流,hyper-util 拿到响应头就把连接的引用还给池了。丢掉没读完的响应只重置那个流,连接照常给下一个请求用。
拿着 Response 既不读也不丢是第三种情况:HTTP/1.1 上连接被它占着,后面的请求同样只能新建,见 响应体。
修法
先确定要不要那个 body:
只要状态码,服务器支持
HEAD: 用client.head(url)。响应没有 body,HTTP/1.1 上连接直接回池。同一个 1 MiB 的地址换成HEAD(src/bin/p04_head.rs):https HTTP/1.1 HEAD, body 1048576 B 20 requests: conns=1 tls=1只要状态码,但只能
GET,响应不大: 读完再丢,let _ = r.bytes().await;。上面read to end那几行,1 条连接。响应很大,确实不需要内容: 读完要传完整个 body,关连接只多一次握手,哪个划算取决于 body 大小和往返时间。这时丢掉是可以接受的选择,只要知道它会关连接。
服务器支持 HTTP/2: 这个问题不存在。0.13 默认 TLS 带 ALPN,HTTPS 上服务器同意就会走 HTTP/2,见 连接池与 HTTP 版本。
不要在 HTTP/1.1 上只读一部分(比如 chunk() 读一块)就丢掉:读一半和完全不读一样,连接照样被关,见 响应体 里「1 chunk then drop」那一行。
怎么检查
- 看服务端或负载均衡的新建连接数: 健康检查、批量探测类的任务,如果新建连接数和请求数一样多,先看是不是这个坑。
- 看日志:
RUST_LOG=hyper_util=debug下,每个请求都有connecting to、没有reuse idle connection,而Client是同一个,就说明连接没回池。 - 看
r.version(): 是HTTP/2.0的话可以不管;HTTP/1.1的才要考虑读完。
小结
- HTTP/1.1 上丢掉一个没读完的大响应,hyper 排不空就关连接,下一个请求新建连接、重新握手。
- 小响应在读头时已经进了缓冲,丢掉也能复用;HTTP/2 上丢掉只重置一个流,连接不受影响。
- 只要状态码时用
HEAD,或者把 body 读完;body 很大时,「读完」和「关连接」之间按大小和往返时间取舍。