Some(H2Options::new()):有界的 h2 默认值没了
主线位置:请求主线里下游走 h2 的那条岔路,
process_new选中 h2 后调handshake,服务端的 SETTINGS 在这里发出。h2 选项从哪里来、0.9.0 为什么加了上限,见 HTTP/2。本页讲自定义选项时最容易写出的那一行。
flowchart TB
opt{"h2_options"} -->|None| def["default_h2_options"]
opt -->|"Some new"| raw["h2 Builder 默认"]
opt -->|"Some 从默认改"| mod["上限加改动"]
def --> b["100 流 64 KiB"]
mod --> b
raw --> u["不通告 不限"]
b --> s431["80 KB 头 431"]
u --> s200["80 KB 头 200"]
实验 crate exp-pingora/pita-lab:bin px 开 h2c,PX_H2OPTS=none(h2_options 保持 None)、new(Some(H2Options::new()))、default(Some(default_h2_options()) 再改窗口)三个进程分别在 19265、19266、19267;tools/h2settings.py 发 h2 前言和空 SETTINGS,打印服务端的 SETTINGS;bin h2probe 用 h2 crate 做客户端,先发一个带 20 个 4000 字节头(共 80 KB)的请求,再在一条连接上同时发 120 个 /sleep?s=1。上游是 tools/echo_upstream.py。M2 Max、macOS 27、rustc 1.99.0,debug,tokio 1.53.2。
现象
想把 h2 的初始窗口调大一点,顺手写成:
let mut o = H2Options::new();
o.initial_window_size(1 << 20);
proxy.app_logic_mut().unwrap().h2_options = Some(o);
H2Options::new() 这一步就把默认上限丢了。只看最小的形式 Some(H2Options::new()),和不设、从默认值改三者对比:
== PX_H2OPTS=none
SETTINGS MAX_CONCURRENT_STREAMS=100, MAX_HEADER_LIST_SIZE=65536
SETTINGS ACK
80 KB header list: status 431 Request Header Fields Too Large
120 concurrent /sleep?s=1 on one connection: 120 x 200 in 2.03s
== PX_H2OPTS=new
SETTINGS (empty)
SETTINGS ACK
80 KB header list: status 200 OK
120 concurrent /sleep?s=1 on one connection: 120 x 200 in 1.02s
== PX_H2OPTS=default
SETTINGS MAX_CONCURRENT_STREAMS=100, INITIAL_WINDOW_SIZE=1048576, MAX_HEADER_LIST_SIZE=65536
SETTINGS ACK
80 KB header list: status 431 Request Header Fields Too Large
120 concurrent /sleep?s=1 on one connection: 120 x 200 in 2.03s
- 不设时,服务端通告 100 个并发流、64 KiB 头列表;80 KB 的头被 431 拒绝,120 个请求按 100 个一批分两批跑完,用了 2 秒。
Some(H2Options::new())的 SETTINGS 是空的:并发流按协议就是不限,头列表的本地上限回到h2自己的 16 MiB。80 KB 的头被放行转给上游,120 个请求在一条连接上同时进行。- 改窗口这类正常的需求,结果看起来一切正常:请求都成功了,甚至更快了。丢掉的是对恶意客户端的保护,平时测不出来。
出处
H2Options 就是 h2::server::Builder 的别名,H2Options::new() 是 h2 的 Builder::new(),什么都不设。上限只在 default_h2_options 里设,而 handshake 只在调用方没给选项时才用它:
pub fn default_h2_options() -> H2Options {
let mut options = H2Options::default();
options.max_header_list_size(DEFAULT_MAX_HEADER_LIST_SIZE);
options.max_concurrent_streams(DEFAULT_MAX_CONCURRENT_STREAMS);
options
}
pub async fn handshake(io: Stream, options: Option<H2Options>) -> Result<H2Connection<Stream>> {
let options = options.unwrap_or_else(default_h2_options);
// ...
代理服务的选项是 HttpProxy 上的 pub h2_options,一旦是 Some,unwrap_or_else 就不会调 default_h2_options。default_h2_options 的文档注释写的是「Use this as the starting point when customizing options」,而 H2Options 这个类型自己的构造函数 new() / default() 来自 h2,不知道 pingora 的上限。上限是 90890bb 在 0.9.0 加的,同样的改动以 d193c8d 先发在 0.8.1 的 Security 一节里;本站网关锁在 0.8.0,连这两个上限都没有,见在本站中。main 上到 84f21ee 这几行没变。
修法
从 default_h2_options() 开始改:
let mut o = default_h2_options();
o.initial_window_size(1 << 20);
app.h2_options = Some(o);
上面 PX_H2OPTS=default 那一组就是它:SETTINGS 里在 100 和 65536 之外多了 INITIAL_WINDOW_SIZE=1048576,80 KB 的头照样 431,120 个请求照样两批。
要比默认值更宽或更紧,就在 default_h2_options() 的基础上显式写 max_concurrent_streams、max_header_list_size,让数字出现在自己的代码里,而不是靠「没写」。
怎么确认没中招
- 搜
H2Options::new()、H2Options::default()、h2::server::Builder::new():每一处都应当换成default_h2_options(),或者在后面显式设两个上限。 - 发一次 h2 前言,看服务端的第一个 SETTINGS 帧(
h2settings.py PORT):里面应当有MAX_CONCURRENT_STREAMS和MAX_HEADER_LIST_SIZE;SETTINGS (empty)说明上限没了。TLS 端口要先握手、ALPN 选 h2 再发。 - 升级 pingora 时注意:0.8.0 的
handshake是options.unwrap_or_default(),从 0.8.0 带过来的代码就算写的是None也没有上限,要升到 0.8.1 或 0.9.0。
小结
- 0.9.0 的 h2 服务端在
h2_options为None时用default_h2_options():100 个并发流、64 KiB 头列表。 - 传
Some(H2Options::new())会绕过它,SETTINGS 是空的:80 KB 的头被放行、120 个流同时进行,平时看起来还更快。 - 自定义 h2 选项从
default_h2_options()改起,用 SETTINGS 帧确认两个上限还在。