本文描述了我们最近遇到的一个真实案例:在一个集群中,Kubernetes APIServer 由于大量请求而瘫痪。今天,我们将讨论我们是如何解决这个问题并提供一些预防措施的。
一天早上,我们接到了技术支持工程师的反馈,说一个客户的 Kubernetes 集群(包括生成环境)几乎无法正常工作了,要求必须帮助解决这个问题。当我们连接到故障集群后发下 APIServer 已经占用了所有内存,它们会崩溃、重启、再次崩溃、再次重启,一直这样循环下去,这就导致 Kubernetes APIServer 无法访问,完全无法正常工作了。
由于这是一个生产集群,为了能够快速解决问题,我们优先尝试通过向控制平面节点增加 CPU 和内存资源来临时修复问题,最开始我们添加资源后发现仍然不够,不过幸运的是,在继续添加一批资源之后,APIServer 稳定了下来。
虽然问题临时解决了,但我们仍然需要找到问题的根源,以便能够避免类似的问题再次发生。最初,控制平面节点有 8 个 CPU 和 16GB 的内存。在我们的干预之后,增加到了 16 个 CPU 和 64GB 的内存。
以下是问题发生时的内存消耗图表:
从上图可以看到内存消耗已经高达 50GB 了,后面经过分析我们发现是由于某些原因,Cilium pods 向 APIServer 发送了大量的 LIST 请求,由于集群规模较大且节点数量众多(超过 200 个),同时请求大大增加了内存的使用量。
然后我们与客户商定,在一个测试窗口内操作,重新启动 Cilium 代理,然后我们得到了下面的一些情况:
经过分析我们认为限制同时向 APIServer 发送 cilium-agent 请求的数量应该可以解决这个问题。在这种情况下,稍慢一点的 LIST
请求执行不会影响 Cilium 的性能。
根据我们的分析,我们决定使用 Kubernetes 的流控管理功能来解决这个问题。我们使用了两个 Kubernetes 的流控管理功能:PriorityLevelConfiguration
和 FlowSchema
。
我们创建了以下 FlowSchema
和 PriorityLevelConfiguration
资源清单:
---
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
直接将上面的两个资源对象应用到集群中!创建后发现问题解决了,APIServer 的内存消耗也恢复到了正常水平,重新启动 cilium-agent 不会再次导致显著的内存消耗变化。因此,我们能够将节点资源削减为其原始资源。
在 Kubernetes 中,请求队列管理由 API 优先级和公平性 (APF) 处理。从 Kubernetes 1.20 版本开始,默认就启用了 APF,此外 APIServer 还提供两个参数:--max-requests-inflight
(默认为 400)和 --max-mutating-requests-inflight
(默认为 200),用于限制请求数量。如果启用了 APF
,则这两个参数会相加,这就是 APIServer 的总并发限制的定义方式。
此外还有一些细节我们需要考虑:
Long-running
运行的 API 请求(例如,在 pod 中查看日志或执行命令)不受 APF 限制,WATCH
请求也不受限制。exempt
,该级别的请求会立即得到处理。APF 确保 Cilium agent 请求不会限制用户 API 请求。APF 还允许您设置限制,以确保重要请求始终得到处理,而不受 K8s APIServer 负载的影响。
我们可以使用以下两个资源对象来配置 APF:
PriorityLevelConfiguration
:定义可用的优先级级别之一。FlowSchema
:将每个传入请求映射到单个 PriorityLevelConfiguration
。每个 PriorityLevelConfiguration
都有自己的并发限制(共享)。总并发限制按照它们的共享比例在现有的 PriorityLevelConfigurations
之间分配。
让我们通过以下示例计算该限制:
~# 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
AssuredConcurrencyShares
的值(260)。workload-low
优先级级别计算为:(400+200)/260*100 = 230
个请求每秒。假如我们改变其中一个值,看看会发生什么。例如,将 deckhouse-pod
的 AssuredConcurrencyShares
从 10 增加到 100。请求限制将降至(400+200)/350*100 = 171
个请求每秒。
通过增加 AssuredConcurrencyShares
的数量,我们增加了特定级别的查询限制,但降低了所有其他级别的限制。
如果优先级级别中的请求数量超过允许的限制,请求将排队等待。你可以选择自定义队列参数,还可以配置 APF 以立即丢弃超过特定优先级级别限制的请求。
让我们看一下下面的示例:
---
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
在这里,优先级级别被配置为具有 AssuredConcurrencyShares = 5
。如果没有其他自定义优先级级别,这将产生每秒 12 个请求。请求队列设置为 200 个请求(handSize * queueLengthLimit
),并创建 16 个内部队列以更均匀地分发来自不同代理的请求。
关于 Kubernetes 流量控制中的优先级级别配置,还有一些重要的细节需要注意:
queueLengthLimit
可以处理高流量突发而不会忽略任何请求。然而,查询的处理速度较慢,并且需要更多的内存。handSize
,你可以调整流之间碰撞的可能性以及在高负载情况下单个流的总并发性。这些参数是通过实验选择的:
接下来我们来看下 FlowSchema
资源,它的作用是将请求映射到适当的 PriorityLevel
。其主要参数包括:
matchingPrecedence
:定义 FlowSchema
的应用顺序,数字越低,优先级越高。rules
:定义请求过滤规则,格式与 Kubernetes RBAC 中的格式相同。distinguisherMethod
:指定一个参数(用户或命名空间),用于在将请求转发到优先级时将请求分离到流中,如果省略该参数,所有请求将分配给同一流(flow)。示例:
---
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
在上面的示例中,我们选择了所有针对 apiGroup: cilium.io
的 LIST
请求,包括集群范围的请求以及从所有命名空间发送到所有资源的请求。主体包括来自 d8-cni-cilium
服务账户的所有请求。
那么如何找出请求所在的 FlowSchema
和 PriorityLevelConfiguration
呢?
在响应时,APIServer 会提供特殊的 Header X-Kubernetes-PF-FlowSchema-UID
和X-Kubernetes-PF-PriorityLevel-UID
,你可以使用它们来查看请求的去向。
例如,让我们从 Cilium agent 的 ServiceAccount 向 API 发出一个请求:
TOKEN=$(kubectl -n d8-cni-cilium get secrets agent-token-45s7n -o json | jq -r .data.token | base64 -d)
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
输出显示该请求属于 d8-serviceaccounts
的 FlowSchema
和 d8-serviceaccounts
的 PriorityLevelConfiguration
。
Kubernetes APIServer 提供了几个需要关注的有用指标:
Apiserver_flowcontrol_rejected_requests_total
:被拒绝的请求总数。Apiserver_current_inqueue_requests
:队列中当前请求的数量。Apiserver_flowcontrol_request_execution_seconds
:请求执行时间。一些 debug
端点也可能有助于获取有用的信息:
kubectl get --raw /debug/api_priority_and_fairness/dump_priority_levels
PriorityLevelName, ActiveQueues, IsIdle, IsQuiescing, WaitingRequests, ExecutingRequests
system, 0, true, false, 0, 0
workload-high, 0, true, false, 0, 0
catch-all, 0, true, false, 0, 0
exempt, <none>, <none>, <none>, <none>, <none>
d8-serviceaccounts, 0, true, false, 0, 0
deckhouse-pod, 0, true, false, 0, 0
node-high, 0, true, false, 0, 0
global-default, 0, true, false, 0, 0
leader-election, 0, true, false, 0, 0
workload-low, 0, true, false, 0, 0
kubectl get --raw /debug/api_priority_and_fairness/dump_queues
PriorityLevelName, Index, PendingRequests, ExecutingRequests, SeatsInUse, NextDispatchR, InitialSeatsSum, MaxSeatsSum, TotalWorkSum
exempt, <none>, <none>, <none>, <none>, <none>, <none>, <none>, <none>
d8-serviceaccounts, 0, 0, 0, 0, 71194.55330547ss, 0, 0, 0.00000000ss
d8-serviceaccounts, 1, 0, 0, 0, 71195.15951496ss, 0, 0, 0.00000000ss
...
global-default, 125, 0, 0, 0, 0.00000000ss, 0, 0, 0.00000000ss
global-default, 126, 0, 0, 0, 0.00000000ss, 0, 0, 0.00000000ss
global-default, 127, 0, 0, 0, 0.00000000ss, 0, 0, 0.00000000ss
我们最终通过设置请求队列管理解决了我们的问题,值得注意的是,这并不是我们在实践中遇到的唯一此类案例。在多次需要限制 API 请求后,我们将 APF 配置作为 Kubernetes 平台的重要组成部分。利用它可以帮助我们和我们的客户减少大型高负载 Kubernetes 集群中 API 拥塞问题的数量。
如果你在实践中也遇到了类似的问题,并且找到了其他解决方法,请在评论中分享你的经验。
原文链接:https://blog.palark.com/kubernetes-api-flow-control-management/