谁该收到 429 由我们决定
一句话总结
API 服务器会用 FlowSchema 对所有请求分类,放进名为 PriorityLevelConfiguration 的等级中,并为每个等级 分配并发执行数。出现失控的客户端时,谁会挨饿,可以用这两种对象事先设计好。
为什么需要这个机制
早先的 kube-apiserver 只有 --max-requests-inflight 和 --max-mutating-requests-inflight 两个
旋钮。它们是对同时处理的请求数做整体限制。这种方式的问题很明显——
它不区分是谁占了那个位置。如果一个写错的控制器每秒列出几千次,
就会把位置全部占满,排在它后面的 kubelet 的节点状态更新、
运维人员的 kubectl get pods 同样会被挤在后面。没有办法把重要的请求和吵闹的请求区分开。
API Priority and Fairness 把这种情况分成两步。先对请求进行分类,再为分类出来的每个等级
单独分配容量。而且对于短暂的突发,不会拒绝,而是让它们暂时排队。它在 1.29 中成为稳定特性,
API 组是 flowcontrol.apiserver.k8s.io/v1。
工作原理
FlowSchema 是分类表。传入的请求会按 matchingPrecedence 数值从小到大依次比对,
在第一个匹配的地方停止。与名字相反,值越小越先匹配,这是一个总是让人混淆的地方。
如果有两个值完全相同的 schema,就比较名称的字典序,较小的一方获胜,但官方文档建议
不要依赖这种情况,而是不要让值重叠。
规则由 subjects(谁)和 resourceRules/nonResourceRules(做什么)配对写成。在每个字段中使用 *,
表示完全不考虑该项。此外,distinguisherMethod 会在一个等级内部把请求进一步拆分成
流(flow)。ByUser 时,一个用户无法让另一个用户的份额挨饿,
ByNamespace 时,则以命名空间为单位提供同样的保护。如果留空,匹配该 schema 的所有请求
会被视为一个流。
PriorityLevelConfiguration 是等级。type 为 Exempt 时,完全不接受评估,立即处理。
为 Limited 时,会分得并发执行数,但不是用绝对席位数,而是用名为 nominalConcurrencyShares 的
份额来写。这是为了在改变服务器的总并发执行数时,让所有等级以相同比例一起增减
而设计的。再加上 lendablePercent(可以借出剩余席位的比例)和 borrowingLimitPercent(可以借入的
上限),当一个等级闲置时,它的席位就会流向其他等级。
超出时的行为由 limitResponse 决定。Reject 则立即返回 429,Queue 则排队。队列的
形状由 queues、handSize、queueLengthLimit 三个值决定,这三个值通过名为 shuffle sharding 的技术,
调节“大象踩到老鼠的概率”。一个流最多能积压的请求数是
handSize 乘以 queueLengthLimit。
默认内置的那些内容也值得了解。在四个必需(mandatory)对象中,exempt schema
会让 system:masters 组的请求免于评估,catch-all 则保证没有匹配任何规则的请求
有去处。此外,作为建议(suggested)配置,还会加入 system-leader-election、system-nodes、
kube-controller-manager、service-accounts 之类的 schema。
观测由以 apiserver_flowcontrol_ 开头的指标负责。拒绝数是
apiserver_flowcontrol_rejected_requests_total,reason 标签以 queue-full、
concurrency-limit、time-out、cancelled 之一告知原因。每个等级实际分到了多少席位,
直接显示在 apiserver_flowcontrol_nominal_limit_seats 中。
在现场相遇的样子
最常见的是单个 Operator 失控。如果部署了一个不使用缓存、每次都列出完整列表的控制器, 使用同一等级的其他 ServiceAccount 也会跟着变慢。这时该做的,是添加一个 FlowSchema,只把那个 账号送到范围较窄的等级。这样一来,受害范围就被限制在这个等级之内。
第二种是滥用 exempt。看到 429,一着急就把那个工作负载转到 exempt。症状 消失了,但现在那个客户端会整个绕过保护服务器的装置。下一次失控时,API 服务器 会一起倒下。所以指向 exempt 的 schema 列表,是需要人来监控的一份很短的清单。
第三种是悄悄失效的 schema。如果把所指向的优先级等级名称写错一个字母,不会报错,
只会在 status 中留下 Dangling 状况为 True。该 schema 会像不存在一样运作,想要隔离的工作负载
仍然留在默认等级里,让别人挨饿。
本实验环境的局限
这个集群里没有真正可以让其失控的工作负载——kwok 的 Pod 不是容器,无法在其中运行
客户端。所以不做真正制造出 429 的实验。取而代之的是实际测量分类的结果。kubectl --v=8 输出的
X-Kubernetes-Pf-Flowschema-Uid 头中,包含该请求实际匹配的 schema 的 UID,并且由于分类发生在授权之前,
即使模拟无权限账号的请求以 403 告终,该头仍然会附带。席位数也可以用真实的指标来确认。
下一项实验要做什么
从按优先级顺序读取并保存默认分类表和等级列表开始。然后模拟 ServiceAccount 发送请求,通过响应头实际测量“现在会去哪个等级”。为失控的 Operator 创建范围较窄的等级和送往该等级的规则,并用同样的方法证明分类已经改变。接着实际测量 两条规则匹配同一个请求时的胜负,编写监控指向 exempt 的 schema 的脚本,然后 在指标中确认新等级实际分到了多少席位。
参考文档:
- https://kubernetes.io/docs/concepts/cluster-administration/flow-control/
- https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/