Go 1.27 默认 500 个请求头 value 触发 431:kube-apiserver 大批 group 用户被拒的排查与修复
Go 1.27 给 net/http 服务端加了一个独立的请求头计数上限 Server.MaxHeaderValueCount,默认 500 个 header value,超限直接返回 431 Request Header Fields Too Large。在这之前没有单独的计数限制,请求头只受 MaxHeaderBytes(默认 1MB)约束。
真实踩到的是 kube-apiserver:被 impersonate 或经代理的用户,如果带上 500 个以上 group,请求会被 431 拒掉。kube-apiserver 在 1.35.10 里把这个上限提到了 16384(#142772, @cwedgwood,SIG API Machinery and Auth)。
现象
- 客户端收到的是 431,不是 413,也不是 400。
- 请求头总量并不大,离 1MB 很远,只是条目多。
- 同一个二进制换成 Go 1.26 构建,同样的请求能正常通过。
判断是不是这个问题
按顺序看三件事:
- 状态码:431。413 指向
MaxHeaderBytes超限,431 才指向计数上限。两者是独立维度,别混。 - 数一遍 header value 的数量:看服务端访问日志、审计日志里这条请求的请求头,或者把发送侧实际发出的 header 全量导出后数行数。数出来 ≥ 500 基本就锁定了。
- curl 复现:构造一个 header 数量超过 500 的请求打过去,看是否稳定复现 431。
一个请求的头数量怎么算
规则很简单:
- 每个 header 行算一个 value;
- 重复的 header 名,每出现一次各算一个。
所以下面这段是 500 个 value,不算 1 个:
-H "Impersonate-Group: g1" \
-H "Impersonate-Group: g2" \
...
-H "Impersonate-Group: g500"
注意不是「header 名字去重后的个数」。Accept 或任意 X-Foo 重复 500 次,计数同样是 500。impersonate 场景之所以容易撞上,正是因为 group 是通过重复 header 逐条传的。
用 curl 复现
拼 N 条重复 header,直接看状态码:
args=()
for i in $(seq 1 520); do
args+=(-H "X-Test-Header: value-$i")
done
curl -s -o /dev/null -w '%{http_code}\n' "${args[@]}" https://your-server:6443/healthz
把 520 换成 499、500、501 各打一次,能清楚看到阈值卡在哪里。复现 impersonate 场景时,把 header 名换成 Impersonate-Group:
args=(-H "Impersonate-User: alice")
for i in $(seq 1 520); do
args+=(-H "Impersonate-Group: group-$i")
done
curl -s -o /dev/null -w '%{http_code}\n' \
"${args[@]}" \
--cacert ca.crt --cert client.crt --key client.key \
https://kube-apiserver:6443/api/v1/namespaces
Go 侧怎么设 MaxHeaderValueCount
自己写服务端时,显式抬高这个值:
srv := &http.Server{
Addr: ":6443",
Handler: handler,
MaxHeaderValueCount: 16384,
}
if err := srv.ListenAndServeTLS("server.crt", "server.key"); err != nil {
log.Fatal(err)
}
MaxHeaderValueCount 只管条目数,请求头的总字节数仍由 MaxHeaderBytes 把关(默认 1MB)。如果同时放宽两者,记得分别评估内存占用。
kube-apiserver 不需要自己加这个字段:1.35.10 及之后的版本已经内置把上限调到 16384,升级到该版本即可。
对照:用 Go 1.26 构建的版本没有这个上限
Go 1.26 构建的同一份代码没有独立的 header 计数限制,只有 MaxHeaderBytes,所以上千个 group 的请求也不会因为「条目太多」被拒。
这条对照在排障时很有用:如果换回 Go 1.26 构建的二进制就能过,说明卡住的是 MaxHeaderValueCount;如果照样失败,就去看请求头总字节是否撞了 MaxHeaderBytes,或者问题根本不在请求头大小上。