一个 operator 拖慢了整个集群
目标
按顺序读取默认的 FlowSchema 和 PriorityLevelConfiguration,为失控的 ServiceAccount 创建隔离等级,并通过响应头证明分类确实发生了改变,然后确认优先级值重叠时的规则、防止滥用 exempt 的监控脚本,以及席位数指标。
为什么重要
API 服务器会对传入的所有请求进行分类和隔离。FlowSchema 是分类表,请求按 matchingPrecedence 从小到大比对,由第一个匹配的 schema 来决定。该 schema 所指向的 PriorityLevelConfiguration,决定了该等级的容量——能同时执行多少个,超出时是排队还是直接拒绝。这个结构之所以重要,原因只有一个:出现失控的客户端时,可以设计谁会挨饿。不会读分类表,在 429 不断堆积的那天,能做的就只有重启,而那意味着要再挨一次同样的失控。
步骤
- 把集群默认放入的 FlowSchema(为了与本实验要创建的区分开,定义为名称不以
ops-或zz-ops-开头的)全部保存到/root/ops-apf/flowschemas.tsv——每行一条,格式为<matchingPrecedence>制表符<이름>制表符<우선순위 등급 이름>(占位符依次为 schema 名称与优先级等级名称),并按请求被评估的顺序排序(按数值排序)。 - 把集群默认放入的 PriorityLevelConfiguration(名称不以
ops-开头的)全部保存到/root/ops-apf/levels.tsv——每行一条,格式为<이름>制表符<type>制表符<nominalConcurrencyShares>制表符<limitResponse.type>(占位符依次为名称、type、nominalConcurrencyShares、limitResponse.type),按名称升序。像Exempt等级那样没有值的列,请写-。 - 创建命名空间
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 名称)。 - 在
/root/ops-apf/ops-noisy.yaml中写入 PriorityLevelConfigurationops-noisy——type: Limited,nominalConcurrencyShares: 5,lendablePercent: 50,borrowingLimitPercent: 20,limitResponse为type: Queue,queuing为queues: 16、handSize: 4、queueLengthLimit: 50。应用它。 - 在
/root/ops-apf/ops-noisy-operator.yaml中写入 FlowSchemaops-noisy-operator——matchingPrecedence: 950,priorityLevelConfiguration.name为ops-noisy,distinguisherMethod.type为ByUser,规则中 subject 为kind: ServiceAccount、命名空间ops-apf的widget-operator,resourceRules 的verbs、apiGroups、resources、namespaces全部设为*。应用它。 - 分别用
widget-operator和plain-reader运行classify.sh,并把结果保存为两行到/root/ops-apf/classified.tsv——格式为<어카운트 이름>制表符<스키마 이름>(占位符依次为账号名称与 schema 名称),按账号名称升序。 - 在
/root/ops-apf/ops-bulk-a.yaml和/root/ops-apf/zz-ops-bulk-b.yaml中写入两个 FlowSchema。两者都以ops-apf的 ServiceAccountbulk-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。 - 在
/root/ops-apf/exempt-allow.txt中逐行写入允许指向exempt优先级等级的 FlowSchema 名称(先确认当前集群中实际有哪些这样的 schema,只写这些名称)。然后创建/root/ops-apf/exempt-guard.sh——找出所有指向exempt的 FlowSchema,在允许列表中则输出OK <이름>,不在则输出VIOLATION <이름>(占位符为 schema 名称),按名称顺序仅输出到标准输出,并且只要有一个违规,就必须以非 0 的退出码结束。在当前状态下运行时不应有违规。 - 从 API 服务器的指标中读取
apiserver_flowcontrol_nominal_limit_seats,并保存到/root/ops-apf/seats.tsv——每行一条,格式为<우선순위 등급 이름>制表符<좌석 수>(占位符依次为优先级等级名称与席位数),按名称升序。ops-noisy必须在其中。
参考
matchingPrecedence的值越小,越先被评估。kubectl --v=8会输出响应头——分类结果以 UID 的形式包含在其中。- APF 分类先于授权,所以即使返回 403,该头也会附带。
- 如果 FlowSchema 的 status 中 Dangling 为 True,说明该 schema 正在被忽略。
- 常见错误:用字符串排序来排列优先级,导致 1000 排在 2 之前。
- 常见错误:因为着急而让它指向 exempt,把保护服务器的装置整个去掉。
- 参考:https://kubernetes.io/docs/concepts/cluster-administration/flow-control/
按优先级顺序读取分类表
把集群默认放入的 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 获取。再创建一个等级,并不会让总席位增加,而是把同样的总量按份额重新分配——所以创建新等级后,原有等级的席位数会一起减少。亲眼看到这些数字,份额的含义就清楚了。