K8s API-Server 流量治理指南(2023):FlowControler 入门介绍

译者序

最近,我们为了增加一个功能,在运维 K8s 集群资源更新时,触发了 cilium 大量并发更新 endpoint 的 identity 资源,导致 API-Server 内存暴涨,请求无法响应。

事后,我们在思考如何落地对 API-Server 限流,偶然中检索到了一篇外文博客,里面介绍了和我非常类似的故障情景,因此翻译整理出来留作参考。

本文来自 2023 年 palark 的博客 Kubernetes API and flow control: Managing request quantity and queuing procedure。 翻译了其中感兴趣的部分。

译者水平有限,不免存在遗漏或错误之处。如有疑问,敬请查阅原文。

以下是译文:

我们遇到的故障

在一个非常普通的早晨,我们开始了一项漫长的研究—— Kubernetes API以及对其请求的优先级。

我们接到了一位技术支持工程师的电话,他报告说客户的一个集群无法正常运行。我们连接到出现故障的集群,发现 Kube-API-Server 占用了所有内存。并且不断在崩溃、重启,然后再次崩溃、再次重启,如此反复。

这样的直接后果是 Kubernetes API无法访问,完全失去了功能。

原本的控制平面节点有 8个CPU和16GB的RAM。我们将规格增加到了16 CPU 、64GB的RAM,最终才稳定住了集群。

扩容后,我们通过监控发现点节点 api-server 内存消耗高达50GB。
进一步分析,我们发现集群中 Cilium Agent pod 不明原因的大量向API发送 LIST 请求。由于集群较大且节点数量较多,同时发出的请求大大增加了使用的内存量。

我们准备对 cilium-agent 的API 请求并发数做个限制。这就用到了

flowcontrol.apiserver.k8s.io/v1beta1。

以下是我们配置的 flowcontrol 资源对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: FlowSchema
metadata:
name: cilium-pods
spec:
distinguisherMethod:
type: ByUser
matchingPrecedence: 1000
priorityLevelConfiguration:
name: cilium-pods
rules:
- resourceRules:
- apiGroups:
- 'cilium.io'
clusterScope: true
namespaces:
- '*'
resources:
- '*'
verbs:
- 'list'
subjects:
- group:
name: system:serviceaccounts:d8-cni-cilium
kind: Group
---
apiVersion: flowcontrol.apiserver.k8s.io/v1beta1
kind: PriorityLevelConfiguration
metadata:
name: cilium-pods
spec:
type: Limited
limited:
assuredConcurrencyShares: 5
limitResponse:
queuing:
handSize: 4
queueLengthLimit: 50
queues: 16
type: Queue

使用 flowcontrol 管控 K8s API 流量

K8s 中有默认开启管控 API 流量的机制 —— APF, 全称为 API Priority and Fairness

在 k8s 1.20 之前,API Server 提供了两个参数:

  • –max-requests-inflight: default is 400
  • –max-mutating-requests-inflight:default is 200

这两个参数共同限制 API 的请求总数。

不过也有些情况,不受 APF 限制:

  1. 长连接, logs, exec 或者 watch 请求。不受限流的影响。
  2. 属于预定义 exempt Level 的请求,直接执行不受限流影响。

flowcontrol API 有两个核心对象

PriorityLevelConfiguration

用来声明优先级和对应的限流值。

我们看个例子,来说明下具体这些字段的含义

1
2
3
4
5
6
7
8
9
10
11
12
~# kubectl get prioritylevelconfigurations.flowcontrol.apiserver.k8s.io
NAME TYPE ASSUREDCONCURRENCYSHARES QUEUES HANDSIZE QUEUELENGTHLIMIT AGE
catch-all Limited 5 <none> <none> <none> 193d
d8-serviceaccounts Limited 5 32 8 50 53d
deckhouse-pod Limited 10 128 6 50 90d
exempt Exempt <none> <none> <none> <none> 193d
global-default Limited 20 128 6 50 193d
leader-election Limited 10 16 4 50 193d
node-high Limited 40 64 6 50 183d
system Limited 30 64 6 50 193d
workload-high Limited 40 128 6 50 193d
workload-low Limited 100 128 6 50 193d
  1. 总的份额是 Sum(AssuredConcurrencyShares) = 260
  2. 具体Level 的限额,比如 workload-low 这个优先级,它允许的最大 QPS = (400+200)/260 * 100 = 230. 400, 200 是前面 --max-requests-inflight--max-mutating-requests-inflight统一指定的集群最大并发数。260 是全部 level 的份额总和。

如果当某个有限级的并发数大于允许值时,请求就会被入队。此时另外三个控制队列的参数就会发生作用。当然也可以配置直接丢弃请求,不进行入队。

队列的控制参数有:

  • handSize:调整不同请求流的碰撞概率。
  • queueLengthLimit:每个队列最大长度
  • queues:队列个数, 提高 queues 可以减少不同请求流的碰撞概率。

需要说明的是:

  • queues 越大,流之间的碰撞次数越小,但会增加内存开销
  • queueLengthLimit越大,在高流量爆发时,能够缓存的请求量越多,减少请求被丢弃的风险。但会增加延迟和增加内存开销。
  • 通过改变handSize,可以调整流之间碰撞的可能性以及在高负载情况下单个流可用的整体并发量。
  • 单流的最大请求缓存量为 handsize * queueLengthLimit

这里 handsize 和 queues 是两个比较难直观理解的参数。但他们的作用都是为了实现流间公平性的,在大象流和老鼠流之间做到公平限流。具体算法可以参考这里
KEP-1040

有趣的是K8s 文档中给了个算法效果的表格, 具体请运行下这个示例程序Go Playgroud

  1. 当 handsize=12, queues=32 时,当分别有1,4,16 个大象流并发时,单个老鼠流被抑制的概率为:0.00000000442,0.113,0.993

  2. 当 handsize=10, queues=32 时,老鼠流被抑制的概率为:0.0000000155,0.06,0.97

  3. 当 handsize=10, queues=64 是,被抑制的概率变为:0.0000000000066,0.000455,0.49

笔者简单总结下:

  1. queues 增加可以显著降低多个大象流对老鼠流的影响
  2. handsize 减少可以减少多流碰撞的概率,但对于单个流会更有可能占据全部限流带宽。

在生产上,既要保护 API-Server 不因突然的流量激增而过载,又要确保各优先级请求延迟不会太大。 因此需要多次调参,才能达到好的效果:

FlowSchema

用来声明哪些请求归属于该优先级。

主要参数有三个:

  • matchingPrecedence:定义 FlowSchema 应用的顺序。数字越低,优先级越高。这样你可以按照从更具体的情况到更一般的情况,编写重叠的 FlowSchemas。

  • rules:定义请求过滤规则;格式与 Kubernetes RBAC中的相同。

  • distinguisherMethod:在将请求转发到优先级时,指定一个参数(用户或命名空间)以将请求分离成流。如果省略该参数,所有请求将被分配到同一个流。

写到这里,不知道你是否有这样的疑问:如何知道请求匹配到了哪个优先级?

答案是:API-Server 会在请求的返回头中,加入X-Kubernetes-PF-FlowSchema-UID
X-Kubernetes-PF-PriorityLevel-UID两个 header来表明匹配到的流控优先级 uId 。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl https://127.0.0.1:6445/apis/cilium.io/v2/ciliumclusterwidenetworkpolicies?limit=500  -X GET --header "Authorization: Bearer $TOKEN" -k -I
HTTP/2 200
audit-id: 4f647505-8581-4a99-8e4c-f3f4322f79fe
cache-control: no-cache, private
content-type: application/json
x-kubernetes-pf-flowschema-uid: 7f0afa35-07c3-4601-b92c-dfe7e74780f8
x-kubernetes-pf-prioritylevel-uid: df8f409a-ebe7-4d54-9f21-1f2a6bee2e81
content-length: 173
date: Sun, 26 Mar 2023 17:45:02 GMT

kubectl get flowschemas -o custom-columns="uid:{metadata.uid},name:{metadata.name}" | grep 7f0afa35-07c3-4601-b92c-dfe7e74780f8
7f0afa35-07c3-4601-b92c-dfe7e74780f8 d8-serviceaccounts

kubectl get prioritylevelconfiguration -o custom-columns="uid:{metadata.uid},name:{metadata.name}" | grep df8f409a-ebe7-4d54-9f21-1f2a6bee2e81
df8f409a-ebe7-4d54-9f21-1f2a6bee2e81 d8-serviceaccounts

监控指标

上了 FlowControl 的功能,自然会想要从指标测能够监测到流控的效果。为此 K8s FlowControl 提供了三个有用监控指标:

  • Apiserver_flowcontrol_rejected_requests_total:被拒绝的请求总数
  • Apiserver_current_inqueue_requests:当前缓冲在队列中未被处理的请求数
  • Apiserver_flowcontrol_request_execution_seconds:请求执行时长

总结

本文介绍了社区原生提供的流控管理能力。随着集群规模越来越大,对 API-Server 的访问压力也会陡增。而且在开放的多租户环境中,不同来源的 CRD controllers, webhooks 等组件与API-Server 通信的模式无法预估,更应该加强对 API-Server 的流控管理。

此外,也有另一种思路是通过引入 proxy 的方式,将 API-Server 隐藏在 Proxy 后面,对其的请求先打到 Proxy 上,流控功能在 Proxy 上实现。字节跳动开源的 kubeGateway 项目就是这种思路落地的,据说也取得了不错的效果。