429 Too Many Requests 不是应用返回的,是 API Server 的 APF 在保护自己。这篇把 APF 的默认配置、请求怎么被归类、并发席位怎么算,以及超限时 Reject 和 Queue 两种处理方式的真实差别都跑了一遍。
优先级名称 类型 并发份额 超限时 队列数 并发席位(seats) catch-all Limited 5 Reject - 13 exempt Exempt - - - 0 global-default Limited 20 Queue 128 49 leader-election Limited 10 Queue 16 25 node-high Limited 40 Queue 64 98 system Limited 30 Queue 64 74 workload-high Limited 40 Queue 128 98 workload-low Limited 100 Queue 128 245 FlowSchema 共 11 个;其中 matchingPrecedence 最小(最先匹配)的前 4 个:exempt(1)→exempt, probes(2)→exempt, system-leader-election(100)→leader-election, system-node-high(400)→node-high
集群里有 8 个 PriorityLevelConfiguration 和 11 个 FlowSchema。每个优先级按“份额”分到并发席位:总并发(默认 400 + 200 = 600)乘以份额再除以所有 Limited 级别的总份额(245),向上取整,所以 workload-low(100 份)得到 245 席,node-high、workload-high(40 份)各 98 席,catch-all(5 份)13 席。超限时 catch-all 直接 Reject,其他级别排队;exempt 不受限。FlowSchema 按 matchingPrecedence 从小到大匹配,先匹配上的生效。
kubectl(客户端证书,身份 kubernetes-admin|["kubeadm:cluster-admins","system:authenticated"]):FlowSchema=global-default PriorityLevel=global-default 匿名访问 /version:HTTP 200;FlowSchema=global-default PriorityLevel=global-default ServiceAccount k17/hammer(绑定了 view)请求 pods(HTTP 200):FlowSchema=service-accounts PriorityLevel=workload-low 无效令牌:HTTP 401;响应头里 没有 FlowSchema(认证失败的请求在进入 APF 之前就被拒了)
响应头 X-Kubernetes-PF-FlowSchema-UID 和 …PriorityLevel-UID 能反查出归类结果。意外的是:kubeadm 的管理员证书(kubernetes-admin,组 kubeadm:cluster-admins)并不是 exempt,它和匿名访问一样归 global-default;ServiceAccount 归 service-accounts → workload-low,即使没有 RBAC 权限、请求最终是 403 也是一样——APF 在鉴权之前。认证失败(无效令牌)的 401 响应头里则没有归类,在进入 APF 之前就被拒了。
创建 PriorityLevelConfiguration k17-tiny(份额 1,limitResponse=Reject)和 FlowSchema k17-fs(matchingPrecedence=100,匹配 SA hammer);k17-tiny 得到的并发席位 nominal_limit_seats=3
hammer 再请求一次:FlowSchema=k17-fs PriorityLevel=k17-tiny(原来是 service-accounts / workload-low,因为 100 比默认的更靠前)
80 个线程连续请求 5 秒(GET /api/v1/pods):共 6041 个请求(1208/秒),状态码 {200: 5729, 429: 312};成功请求的延迟 p50=65 ms p95=117 ms 最大=392 ms
第一个 429 的响应头 Retry-After=3;x-kubernetes-pf-flowschema-uid → k17-fs;响应体:Too many requests, please try again later.
apiserver_flowcontrol_rejected_requests_total{priority_level="k17-tiny"} 增加了 312(reason=concurrency-limit)自建 k17-tiny(份额 1,得到 3 个席位,limitResponse: Reject)和一个 matchingPrecedence: 100 的 FlowSchema 把 SA hammer 引过去。80 个线程连续请求 5 秒:6041 个请求里 312 个被 429,响应头带 Retry-After: 3,响应体是 Too many requests, please try again later.;apiserver_flowcontrol_rejected_requests_total{reason="concurrency-limit"} 增加了 312。client-go 会按 Retry-After 退避重试,自己写的客户端要记得处理 429。
limitResponse=Queue(queues=4,queueLengthLimit=100,handSize=2),同样 80 线程 5 秒:共 6071 个请求(1214/秒),状态码 {200: 6071};成功请求的延迟 p50=63 ms p95=112 ms 最大=164 ms
这一轮 rejected 增加了 0;排过队后执行的请求(request_wait_duration_seconds_count{execute="true"})增加了 6071同样的压力下把 limitResponse 改成 Queue(4 个队列、每队最长 100):6071 个请求全部是 200,没有 429,rejected 增加 0,所有请求都是先排队、再执行。成功请求的 p95 约 112 ms、最大 164 ms,Reject 那轮分别是 117 ms、392 ms。这轮压力下两者吞吐几乎一样(约 1210 个/秒),差别在于排队的请求最终都成功了,而 Reject 把超出的直接打回。队列满了才会 429。