TT Lab
开始
学习 学习路径 课程

Kubernetes 运维实务

谁该收到 429 由我们决定

在 TT Lab 中继续学习

一句话总结

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 的脚本,然后 在指标中确认新等级实际分到了多少席位。

参考文档: