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

Kubernetes 运维实务

一个 operator 拖慢了整个集群

在 TT Lab 中继续学习

目标

按顺序读取默认的 FlowSchema 和 PriorityLevelConfiguration,为失控的 ServiceAccount 创建隔离等级,并通过响应头证明分类确实发生了改变,然后确认优先级值重叠时的规则、防止滥用 exempt 的监控脚本,以及席位数指标。

为什么重要

API 服务器会对传入的所有请求进行分类和隔离。FlowSchema 是分类表,请求按 matchingPrecedence 从小到大比对,由第一个匹配的 schema 来决定。该 schema 所指向的 PriorityLevelConfiguration,决定了该等级的容量——能同时执行多少个,超出时是排队还是直接拒绝。这个结构之所以重要,原因只有一个:出现失控的客户端时,可以设计谁会挨饿。不会读分类表,在 429 不断堆积的那天,能做的就只有重启,而那意味着要再挨一次同样的失控。

步骤

  1. 把集群默认放入的 FlowSchema(为了与本实验要创建的区分开,定义为名称不以 ops- 或 zz-ops- 开头的)全部保存到 /root/ops-apf/flowschemas.tsv——每行一条,格式为 <matchingPrecedence> 制表符 <이름> 制表符 <우선순위 등급 이름>(占位符依次为 schema 名称与优先级等级名称),并按请求被评估的顺序排序(按数值排序)。
  2. 把集群默认放入的 PriorityLevelConfiguration(名称不以 ops- 开头的)全部保存到 /root/ops-apf/levels.tsv——每行一条,格式为 <이름> 制表符 <type> 制表符 <nominalConcurrencyShares> 制表符 <limitResponse.type>(占位符依次为名称、type、nominalConcurrencyShares、limitResponse.type),按名称升序。像 Exempt 等级那样没有值的列,请写 -。
  3. 创建命名空间 ops-apf,并创建三个 ServiceAccount——widget-operator、plain-reader、bulk-writer。然后创建 /root/ops-apf/classify.sh——模拟作为参数传入的 ServiceAccount 发送一个请求,并根据响应头 X-Kubernetes-Pf-Flowschema-Uid,仅把匹配到的 FlowSchema 的名称输出到标准输出。以 plain-reader 运行,并把结果保存为一行到 /root/ops-apf/baseline.tsv——plain-reader 制表符 <스키마 이름>(占位符为 schema 名称)。
  4. 在 /root/ops-apf/ops-noisy.yaml 中写入 PriorityLevelConfiguration ops-noisy——type: Limited,nominalConcurrencyShares: 5,lendablePercent: 50,borrowingLimitPercent: 20,limitResponse 为 type: Queue,queuing 为 queues: 16、handSize: 4、queueLengthLimit: 50。应用它。
  5. 在 /root/ops-apf/ops-noisy-operator.yaml 中写入 FlowSchema ops-noisy-operator——matchingPrecedence: 950,priorityLevelConfiguration.name 为 ops-noisy,distinguisherMethod.type 为 ByUser,规则中 subject 为 kind: ServiceAccount、命名空间 ops-apf 的 widget-operator,resourceRules 的 verbs、apiGroups、resources、namespaces 全部设为 *。应用它。
  6. 分别用 widget-operator 和 plain-reader 运行 classify.sh,并把结果保存为两行到 /root/ops-apf/classified.tsv——格式为 <어카운트 이름> 制表符 <스키마 이름>(占位符依次为账号名称与 schema 名称),按账号名称升序。
  7. 在 /root/ops-apf/ops-bulk-a.yaml 和 /root/ops-apf/zz-ops-bulk-b.yaml 中写入两个 FlowSchema。两者都以 ops-apf 的 ServiceAccount bulk-writer 为对象,都指向 ops-noisy,resourceRules 全部为 *。ops-bulk-a 为 matchingPrecedence: 960、distinguisherMethod.type: ByUser,zz-ops-bulk-b 为 matchingPrecedence: 940、distinguisherMethod.type: ByNamespace。两者都应用之后,把 classify.sh bulk-writer 的结果保存为两行到 /root/ops-apf/precedence.tsv——ops-bulk-a 制表符 960 制表符 <win 또는 lose>(占位符为 win 或 lose),zz-ops-bulk-b 制表符 940 制表符 <win 또는 lose>(占位符为 win 或 lose)。另外,如果两个 schema 的值同样都是 960,哪一个会获胜,请只把该名称写为一行到 /root/ops-apf/tie.txt。
  8. 在 /root/ops-apf/exempt-allow.txt 中逐行写入允许指向 exempt 优先级等级的 FlowSchema 名称(先确认当前集群中实际有哪些这样的 schema,只写这些名称)。然后创建 /root/ops-apf/exempt-guard.sh——找出所有指向 exempt 的 FlowSchema,在允许列表中则输出 OK <이름>,不在则输出 VIOLATION <이름>(占位符为 schema 名称),按名称顺序仅输出到标准输出,并且只要有一个违规,就必须以非 0 的退出码结束。在当前状态下运行时不应有违规。
  9. 从 API 服务器的指标中读取 apiserver_flowcontrol_nominal_limit_seats,并保存到 /root/ops-apf/seats.tsv——每行一条,格式为 <우선순위 등급 이름> 制表符 <좌석 수>(占位符依次为优先级等级名称与席位数),按名称升序。ops-noisy 必须在其中。

参考

按优先级顺序读取分类表

把集群默认放入的 FlowSchema(为了与本实验要创建的区分开,定义为名称不以 ops- 或 zz-ops- 开头的)全部保存到 /root/ops-apf/flowschemas.tsv——每行一条,格式为 <matchingPrecedence> 制表符 <이름> 制表符 <우선순위 등급 이름>(占位符依次为 schema 名称与优先级等级名称),并按请求被评估的顺序排序(按数值排序)。

每个传入的请求都会逐个比对 FlowSchema,并在第一个匹配处停止。决定比对顺序的是 matchingPrecedence,与名字相反,值越小越先看。用字符串排序的话 1000 会排在 2 之前,所以请使用数值排序。

读取每个等级的容量

把集群默认放入的 PriorityLevelConfiguration(名称不以 ops- 开头的)全部保存到 /root/ops-apf/levels.tsv——每行一条,格式为 <이름> 制表符 <type> 制表符 <nominalConcurrencyShares> 制表符 <limitResponse.type>(占位符依次为名称、type、nominalConcurrencyShares、limitResponse.type),按名称升序。像 Exempt 等级那样没有值的列,请写 -。

只有 Limited 等级才会分得并发执行数。Exempt 完全不接受评估,所以没有相关字段。用 jq 处理没有值的列时,// 会把 false 也当作空值,所以这里只比较 null 更稳妥。

实际测量现在会去哪个等级

创建命名空间 ops-apf,并创建三个 ServiceAccount——widget-operator、plain-reader、bulk-writer。然后创建 /root/ops-apf/classify.sh——模拟作为参数传入的 ServiceAccount 发送一个请求,并根据响应头 X-Kubernetes-Pf-Flowschema-Uid,仅把匹配到的 FlowSchema 的名称输出到标准输出。以 plain-reader 运行,并把结果保存为一行到 /root/ops-apf/baseline.tsv——plain-reader 制表符 <스키마 이름>(占位符为 schema 名称)。

APF 分类发生在授权之前——所以即使因为没有权限而返回 403,该头也会附带。要查看响应头,请使用 kubectl --v=8,并且必须接收标准错误。模拟身份的写法是 --as=system:serviceaccount:<네임스페이스>:<이름>(占位符依次为命名空间与名称)。

为失控的 Operator 创建等级

在 /root/ops-apf/ops-noisy.yaml 中写入 PriorityLevelConfiguration ops-noisy——type: Limited,nominalConcurrencyShares: 5,lendablePercent: 50,borrowingLimitPercent: 20,limitResponse 为 type: Queue,queuing 为 queues: 16、handSize: 4、queueLengthLimit: 50。应用它。

不直接写席位数而用份额来写,是有原因的——因为即使服务器的总并发执行数改变,所有等级也会以相同的比例一起增减。lendablePercent 是可以借出剩余席位的比例,borrowingLimitPercent 是可以从别人那里借入的上限。

写出送往该等级的规则

在 /root/ops-apf/ops-noisy-operator.yaml 中写入 FlowSchema ops-noisy-operator——matchingPrecedence: 950,priorityLevelConfiguration.name 为 ops-noisy,distinguisherMethod.type 为 ByUser,规则中 subject 为 kind: ServiceAccount、命名空间 ops-apf 的 widget-operator,resourceRules 的 verbs、apiGroups、resources、namespaces 全部设为 *。应用它。

FlowSchema 的 status 中有 Dangling 状况——如果所指向的优先级等级不存在,它就会变为 True,该 schema 会被忽略。应用之后养成确认这个状况是否为 False 的习惯,就能立刻发现因笔误而让 schema 整个失效的事故。

用响应头证明分类确实改变了

分别用 widget-operator 和 plain-reader 运行 classify.sh,并把结果保存为两行到 /root/ops-apf/classified.tsv——格式为 <어카운트 이름> 制表符 <스키마 이름>(占位符依次为账号名称与 schema 名称),按账号名称升序。

创建了规则,并不代表分类就改变了。不是对象的账号必须仍然匹配原来的 schema,只有对象账号才会去新的 schema。两行要一起留下,才能证明规则没有匹配得太宽。

两条规则匹配同一个请求时

在 /root/ops-apf/ops-bulk-a.yaml 和 /root/ops-apf/zz-ops-bulk-b.yaml 中写入两个 FlowSchema。两者都以 ops-apf 的 ServiceAccount bulk-writer 为对象,都指向 ops-noisy,resourceRules 全部为 *。ops-bulk-a 为 matchingPrecedence: 960、distinguisherMethod.type: ByUser,zz-ops-bulk-b 为 matchingPrecedence: 940、distinguisherMethod.type: ByNamespace。两者都应用之后,把 classify.sh bulk-writer 的结果保存为两行到 /root/ops-apf/precedence.tsv——ops-bulk-a 制表符 960 制表符 <win 또는 lose>(占位符为 win 或 lose),zz-ops-bulk-b 制表符 940 制表符 <win 또는 lose>(占位符为 win 或 lose)。另外,如果两个 schema 的值同样都是 960,哪一个会获胜,请只把该名称写为一行到 /root/ops-apf/tie.txt。

两个 schema 匹配同一个请求时,先被评估的一方获胜,比对到此结束。值完全相同时的规则写在官方文档中——比较名称的字典序,较小的一方获胜。不过文档建议不要设置相同的值。

监控指向 exempt 的规则

在 /root/ops-apf/exempt-allow.txt 中逐行写入允许指向 exempt 优先级等级的 FlowSchema 名称(先确认当前集群中实际有哪些这样的 schema,只写这些名称)。然后创建 /root/ops-apf/exempt-guard.sh——找出所有指向 exempt 的 FlowSchema,在允许列表中则输出 OK <이름>,不在则输出 VIOLATION <이름>(占位符为 schema 名称),按名称顺序仅输出到标准输出,并且只要有一个违规,就必须以非 0 的退出码结束。在当前状态下运行时不应有违规。

被送到 exempt 等级的请求,既没有并发执行数限制,也没有队列,会被立即处理。看似方便,但保护服务器的装置也随之消失,所以一旦出现新的指向该等级的 schema,必须让人知道。这样的监控很适合在 CI 中运行。

用指标确认新等级是否真的分到了席位

从 API 服务器的指标中读取 apiserver_flowcontrol_nominal_limit_seats,并保存到 /root/ops-apf/seats.tsv——每行一条,格式为 <우선순위 등급 이름> 制表符 <좌석 수>(占位符依次为优先级等级名称与席位数),按名称升序。ops-noisy 必须在其中。

指标用 kubectl get --raw /metrics 获取。再创建一个等级,并不会让总席位增加,而是把同样的总量按份额重新分配——所以创建新等级后,原有等级的席位数会一起减少。亲眼看到这些数字,份额的含义就清楚了。