第一个请求之后连接永不空闲超时
主线位置:请求主线的最后一步「下游 keep-alive」,也就是一个请求处理完、
reuse之后等下一个请求的那次读。respect_keepalive怎么把超时改成无限、这个值怎么经HttpPersistentSettings带给下一个会话,见 HTTP/1 服务端;这个行为从哪个版本开始,见动手。本页给出复现、0.8.0 上的对照和改法。
stateDiagram-v2 [*] --> FirstWait: 新连接 60 s FirstWait --> Closed: 60 s 没有请求 FirstWait --> Serving: 读到请求头 Serving --> Infinite: respect_keepalive Some 0 Infinite --> Serving: 下一个请求 Infinite --> Infinite: 空闲 永不超时 Serving --> Capped: early_request_filter 改成 60 Capped --> Closed: 空闲 60 s
实验 crate exp-pingora/pita-lab,bin px:明文 19250,TLS(enable_h2)19251;同一个 bin 加 PX_FIX_KA=1 PX_H2IDLE=60 起在 19256 / 19257。本站网关的拷贝 exp-pingora/pita-gw(pingora 0.8.0、tokio 1.52.3,443 / 80 改成 19232 / 19233,PITA_FIX_KA=1 打开本页的改法)。客户端 tools/idle2.py:silent 连上(TLS 的完成握手)后什么都不发;one 发一个 GET 收完响应后不再发;close 发一个带 Connection: close 的 GET;h2 发 h2 前言和一个空 SETTINGS 后不再发。所有连接同时开,最多等 130 秒。M2 Max、macOS 27、rustc 1.99.0,debug,0.9.0 的一侧 tokio 1.53.2,uptime 负载约 4.2。
现象
v09-h1-silent: alpn=None FIN after 60.0s, got 0 bytes
v09-h1-one: alpn=None still open after 130.0s, got 198 bytes
v09-tls-h1-one: alpn=http/1.1 still open after 130.0s, got 198 bytes
v09-tls-h2-silent: alpn=h2 still open after 130.0s, got 21 bytes
v09-tls-h2-preface: alpn=h2 still open after 130.0s, got 30 bytes
v08gw-443-h1-silent: alpn=http/1.1 FIN after 60.0s, got 0 bytes
v08gw-443-h1-one: alpn=http/1.1 still open after 130.0s, got 426 bytes
v08gw-80-one: alpn=None still open after 130.0s, got 148 bytes
v08gw-443-h2-preface: alpn=h2 still open after 130.0s, got 18 bytes
- 什么都不发的 HTTP/1 连接在 60.0 秒被关掉,这是新连接等第一个请求的超时。
- 发过一个请求的 HTTP/1 连接 130 秒后还开着,明文和 TLS 都一样,0.9.0 和 0.8.0 都一样。
- h2 连接不管发没发前言都不超时。
got 21 bytes是 0.9.0 服务端那个带两个参数的 SETTINGS 帧,30多一个 SETTINGS ACK;0.8.0 的 SETTINGS 是空的,所以是18。
一条这样的连接什么也不做,就一直占着一个 fd 和它的缓冲区。在本站网关的拷贝上量过,每条空闲的 TLS 连接约 68 KiB,两千条就超过网关容器 128 MiB 的内存上限,见在本站中。
出处
新连接的第一个会话由 process_new 设 set_keepalive(Some(60))。读完请求头,respect_keepalive 改掉它:
if keepalive {
let (timeout, _max_use) = self.get_keepalive_values();
// TODO: respect max_use
match timeout {
Some(d) => self.set_keepalive(Some(d)),
None => self.set_keepalive(Some(0)), // infinite
}
} else {
self.set_keepalive(None);
}
} else if self.req_header().version == Version::HTTP_11 {
self.set_keepalive(Some(0)); // on by default for http 1.1
get_keepalive_values 没有实现,总是 (None, None),所以 HTTP/1.1 的 keep-alive 请求一律得到 Some(0),也就是无限。HttpPersistentSettings 把这个值带给下一个会话,read_request 在无限时不设超时。0.8.0 是同样的写法,main 上到 84f21ee 没改。
h2 是另一条路:TLS 握完、ALPN 选了 h2 之后,handshake 要读到客户端前言才返回,外面没有超时;握手之后的接流循环只有在设了 HttpServerOptions::h2_idle_timeout(0.9.0 新加,默认 None)时才会因空闲关连接。
修法
HTTP/1:在 respect_keepalive 之后、reuse 之前把 Some(0) 换成有限值。early_request_filter 是读完请求头后第一个调用的钩子,正好在这个窗口里:
async fn early_request_filter(&self, session: &mut Session, _ctx: &mut ()) -> Result<()> {
if session.get_keepalive() == Some(0) {
session.set_keepalive(Some(60));
}
Ok(())
}
只改 Some(0):客户端要关(Connection: close、不带 keep-alive 的 HTTP/1.0)时这个值是 None,不能把它改回 keep-alive。h2:0.9.0 上给服务设 h2_idle_timeout:
let mut o = HttpServerOptions::default();
o.h2_idle_timeout = Some(Duration::from_secs(60));
app.server_options = Some(o);
HttpServerOptions 是 #[non_exhaustive],只能从 default() 改字段。同一轮里加了改法的那一组:
v09fix-h1-close: alpn=None FIN after 0.0s, got 193 bytes
v09fix-h1-one: alpn=None FIN after 60.0s, got 198 bytes
v09fix-tls-h2-preface: alpn=h2 FIN after 60.0s, got 30 bytes
v09fix-tls-h2-silent: alpn=h2 still open after 130.0s, got 21 bytes
网关的拷贝(0.8.0)打开 PITA_FIX_KA=1,early_request_filter 和 80 端口的 request_filter 里各调一次同样的判断:
v08gwfix-443-h1-close: alpn=http/1.1 FIN after 0.0s, got 439 bytes
v08gwfix-80-one: alpn=None FIN after 60.0s, got 148 bytes
v08gwfix-443-h1-one: alpn=http/1.1 FIN after 60.0s, got 426 bytes
- 发过一个请求的 HTTP/1 连接在 60.0 秒被关掉,和第一个请求之前的超时一致;要求关闭的连接照样立刻关。0.8.0 和 0.9.0 都有效。
- 发过前言的 h2 连接在 60.0 秒被
h2_idle_timeout关掉。0.8.0 没有这个选项。 - 只握手、不发 h2 前言的连接仍然开着:它卡在
handshake里,h2_idle_timeout管不到,在 pingora 外面没有办法设这个超时。这一条是上游问题,0.8.0、0.9.0 和 main 都一样。
怎么确认没中招
- 用一个脚本开一条连接、发一个请求、收完响应后什么都不做,看服务端多久关它;超过你预期的空闲超时还开着,就是中招了。上面的
idle2.py ... one 130就是这样做的。 - 看服务端的连接数(
lsof -nP -p <pid> -iTCP -sTCP:ESTABLISHED | wc -l)有没有只涨不跌。 - 前面有负载均衡或 CDN 的部署,看它们自己的空闲超时;它们会替你关掉空闲连接,直接对公网的服务就只能靠自己。
小结
- HTTP/1.1 连接只给第一个请求 60 秒;处理完一个请求后
respect_keepalive设Some(0),之后的空闲等待没有超时。0.8.0、0.9.0、main 一样。 - 在
early_request_filter里把Some(0)换成Some(60),实测 60.0 秒关闭;None不要动。 - h2:0.9.0 的
h2_idle_timeout能关握手之后空闲的连接;只握 TLS、不发 h2 前言的连接仍然永远挂着。