API 优先级和公平性
FEATURE STATE: Kubernetes v1.18 [alpha]
对于集群管理员来说,控制 Kubernetes API 服务器在过载情况下的行为是一项关键任务。 kube-apiserver 有一些控件(例如:命令行标志 --max-requests-inflight
和 --max-mutating-requests-inflight
), 可以限制将要接受的未处理的请求,从而防止过量请求入站,潜在导致 API 服务器崩溃。 但是这些标志不足以保证在高流量期间,最重要的请求仍能被服务器接受。
API 优先级和公平性( APF )是一种替代方案,可提升上述最大并发限制。 APF 以更细粒度的方式对请求进行分类和隔离。 它还引入了空间有限的排队机制,因此在非常短暂的突发情况下,API 服务器不会拒绝任何请求。 通过使用公平排队技术从队列中分发请求,这样, 一个行为不佳的 控制器 就不会饿死其他控制器(即使优先级相同)。
注意: 属于“长时间运行”类型的请求(主要是 watch )不受 API 优先级和公平性过滤器的约束。 如果未启用 APF 特性,即便设置
--max-requests-inflight
标志,该类请求也不受约束。
启用 API 优先级和公平性
APF 特性由特性门控控制,默认情况下不启用。有关如何启用和禁用特性门控的描述, 请参见特性门控。 APF 的特性门控叫做 APIPriorityAndFairness
。 此特性要求必须启用某个 API Group。 你可以在启动 kube-apiserver
时,添加以下命令行标志来完成这些操作:
kube-apiserver \
--feature-gates=APIPriorityAndFairness=true \
--runtime-config=flowcontrol.apiserver.k8s.io/v1alpha1=true \
# …其他配置与之前相同
命令行标志 --enable-priority-fairness=false
将彻底禁用 APF 特性,即使其他标志启用它也是无效。
概念
APF 特性包含几个不同的功能。 传入的请求通过 FlowSchema 按照其属性分类,并分配优先级。 每个优先级维护自定义的并发限制,加强了隔离度,这样不同优先级的请求,就不会相互饿死。 在同一个优先级内,公平排队算法可以防止来自不同 flow 的请求相互饿死。 该算法将请求排队,通过排队机制,防止在平均负载较低时,通信量突增而导致请求失败。
优先级
如果未启用 APF,API 服务器中的整体并发量将受到 kube-apiserver
的参数 --max-requests-inflight
和 --max-mutating-requests-inflight
的限制。 启用 APF 后,将对这些参数定义的并发限制进行求和,然后将总和分配到一组可配置的 优先级 中。 每个传入的请求都会分配一个优先级;每个优先级都有各自的配置,设定允许分发的并发请求数。
例如,默认配置包括针对领导者选举请求、内置控制器请求和 Pod 请求都单独设置优先级。 这表示即使异常的 Pod 向 API 服务器发送大量请求,也无法阻止领导者选举或内置控制器的操作执行成功。
排队
即使在同一优先级内,也可能存在大量不同的流量源。 在过载情况下,防止一个请求流饿死其他流是非常有价值的 (尤其是在一个较为常见的场景中,一个有故障的客户端会疯狂地向 kube-apiserver 发送请求, 理想情况下,这个有故障的客户端不应对其他客户端产生太大的影响)。 公平排队算法在处理具有相同优先级的请求时,实现了上述场景。 每个请求都被分配到某个 流 中,该 流 由对应的 FlowSchema 的名字加上一个 流区分项(Flow Distinguisher) 来标识。 这里的流区分项可以是发出请求的用户、目标资源的名称空间或什么都不是。 系统尝试为不同流中具有相同优先级的请求赋予近似相等的权重。
将请求划分到流中之后,APF 功能将请求分配到队列中。 分配时使用一种称为 混洗分片(Shuffle-Sharding) 的技术。 该技术可以相对有效地利用队列隔离低强度流与高强度流。
排队算法的细节可针对每个优先等级进行调整,并允许管理员在内存占用、 公平性(当总流量超标时,各个独立的流将都会取得进展)、 突发流量的容忍度以及排队引发的额外延迟之间进行权衡。
豁免请求
某些特别重要的请求不受制于此特性施加的任何限制。这些豁免可防止不当的流控配置完全禁用 API 服务器。
默认值
APF 特性附带推荐配置,该配置对实验场景应该足够; 如果你的集群有可能承受较大的负载,那么你应该考虑哪种配置最有效。 推荐配置将请求分为五个优先级:
system
优先级用于system:nodes
组(即 Kubelets )的请求; kubelets 必须能连上 API 服务器,以便工作负载能够调度到其上。leader-election
优先级用于内置控制器的领导选举的请求 (特别是来自kube-system
名称空间中system:kube-controller-manager
和system:kube-scheduler
用户和服务账号,针对endpoints
、configmaps
或leases
的请求)。 将这些请求与其他流量相隔离非常重要,因为领导者选举失败会导致控制器发生故障并重新启动, 这反过来会导致新启动的控制器在同步信息时,流量开销更大。workload-high
优先级用于内置控制器的请求。workload-low
优先级适用于来自任何服务帐户的请求,通常包括来自 Pods 中运行的控制器的所有请求。global-default
优先级可处理所有其他流量,例如:非特权用户运行的交互式kubectl
命令。
内置了两个 PriorityLevelConfiguration 和两个 FlowSchema,它们是内置的、不可重载的:
特殊的
exempt
优先级的请求完全不受流控限制:它们总是立刻被分发。 特殊的exempt
FlowSchema 把system:masters
组的所有请求都归入该优先级组。 如果合适,你可以定义新的 FlowSchema,将其他请求定向到该优先级。特殊的
catch-all
优先级与特殊的catch-all
FlowSchema 结合使用,以确保每个请求都分类。 一般地,你不应该依赖于catch-all
的配置,而应适当地创建自己的catch-all
FlowSchema 和 PriorityLevelConfigurations (或使用默认安装的global-default
配置)。 为了帮助捕获部分请求未分类的配置错误,强制要求catch-all
优先级仅允许一个并发份额, 并且不对请求进行排队,使得仅与catch-all
FlowSchema 匹配的流量被拒绝的可能性更高,并显示 HTTP 429 错误。
健康检查并发豁免
推荐配置没有为本地 kubelet 对 kube-apiserver 执行健康检查的请求进行任何特殊处理 ——它们倾向于使用安全端口,但不提供凭据。 在推荐配置中,这些请求将分配 global-default
FlowSchema 和 global-default
优先级, 这样其他流量可以排除健康检查。
如果添加以下 FlowSchema,健康检查请求不受速率限制。
注意:
进行此更改后,任何敌对方都可以发送与此 FlowSchema 匹配的任意数量的健康检查请求。 如果你有 Web 流量过滤器或类似的外部安全机制保护集群的 API 服务器免受常规网络流量的侵扰, 则可以配置规则,阻止所有来自集群外部的健康检查请求。
priority-and-fairness/health-for-strangers.yaml
apiVersion: flowcontrol.apiserver.k8s.io/v1alpha1
kind: FlowSchema
metadata:
name: health-for-strangers
spec:
matchingPrecedence: 1000
priorityLevelConfiguration:
name: exempt
rules:
- nonResourceRules:
- nonResourceURLs:
- "/healthz"
- "/livez"
- "/readyz"
verbs:
- "*"
subjects:
- kind: Group
group:
name: system:unauthenticated
资源
流控 API 涉及两种资源。 PriorityLevelConfigurations 定义隔离类型和可处理的并发预算量,还可以微调排队行为。 FlowSchemas 用于对每个入站请求进行分类,并与一个 PriorityLevelConfigurations 相匹配。
PriorityLevelConfiguration
一个 PriorityLevelConfiguration 表示单个隔离类型。每个 PriorityLevelConfigurations 对未完成的请求数有各自的限制,对排队中的请求数也有限制。
PriorityLevelConfigurations 的并发限制不是指定请求绝对数量,而是在“并发份额”中指定。 API 服务器的总并发量限制通过这些份额按例分配到现有 PriorityLevelConfigurations 中。 集群管理员可以更改 --max-requests-inflight
(或 --max-mutating-requests-inflight
)的值, 再重新启动 kube-apiserver
来增加或减小服务器的总流量, 然后所有的 PriorityLevelConfigurations 将看到其最大并发增加(或减少)了相同的比例。
注意: 启用 APF 功能后,服务器的总并发量限制将设置为
--max-requests-inflight
和--max-mutating-requests-inflight
之和。 可变请求和不可变请求之间不再有任何区别; 如果对于某种资源,你需要区别对待不同请求,请创建不同的 FlowSchema 分别匹配可变请求和不可变请求。
当入站请求的数量大于分配的 PriorityLevelConfigurations 中允许的并发级别时, type
字段将确定对额外请求的处理方式。 Reject
类型,表示多余的流量将立即被 HTTP 429(请求过多)错误所拒绝。 Queue
类型,表示对超过阈值的请求进行排队,将使用阈值分片和公平排队技术来平衡请求流之间的进度。
公平排队算法支持通过排队配置对优先级微调。 可以在增强建议中阅读算法的详细信息,但总之:
queues
递增能减少不同流之间的冲突概率,但代价是增加了内存使用量。 值为1时,会禁用公平排队逻辑,但仍允许请求排队。queueLengthLimit
递增可以在不丢弃任何请求的情况下支撑更大的突发流量, 但代价是增加了等待时间和内存使用量。修改
handSize
允许你调整过载情况下不同流之间的冲突概率以及单个流可用的整体并发性。说明: 较大的
handSize
使两个单独的流程发生碰撞的可能性较小(因此,一个流可以饿死另一个流), 但是更有可能的是少数流可以控制 apiserver。 较大的handSize
还可能增加单个高并发流的延迟量。 单个流中可能排队的请求的最大数量为handSize *queueLengthLimit
。
下表显示了有趣的随机分片配置集合, 每行显示给定的老鼠(低强度流)被不同数量的大象挤压(高强度流)的概率。 表来源请参阅: https://play.golang.org/p/Gi0PLgVHiUg
随机分片 | 队列数 | 1个大象 | 4个大象 | 16个大象 |
---|---|---|---|---|
12 | 32 | 4.428838398950118e-09 | 0.11431348830099144 | 0.9935089607656024 |
10 | 32 | 1.550093439632541e-08 | 0.0626479840223545 | 0.9753101519027554 |
10 | 64 | 6.601827268370426e-12 | 0.00045571320990370776 | 0.49999929150089345 |
9 | 64 | 3.6310049976037345e-11 | 0.00045501212304112273 | 0.4282314876454858 |
8 | 64 | 2.25929199850899e-10 | 0.0004886697053040446 | 0.35935114681123076 |
8 | 128 | 6.994461389026097e-13 | 3.4055790161620863e-06 | 0.02746173137155063 |
7 | 128 | 1.0579122850901972e-11 | 6.960839379258192e-06 | 0.02406157386340147 |
7 | 256 | 7.597695465552631e-14 | 6.728547142019406e-08 | 0.0006709661542533682 |
6 | 256 | 2.7134626662687968e-12 | 2.9516464018476436e-07 | 0.0008895654642000348 |
6 | 512 | 4.116062922897309e-14 | 4.982983350480894e-09 | 2.26025764343413e-05 |
6 | 1024 | 6.337324016514285e-16 | 8.09060164312957e-11 | 4.517408062903668e-07 |
FlowSchema
FlowSchema 匹配一些入站请求,并将它们分配给优先级。 每个入站请求都会对所有 FlowSchema 测试是否匹配, 首先从 matchingPrecedence
数值最低的匹配开始(我们认为这是逻辑上匹配度最高), 然后依次进行,直到首个匹配出现。
注意:
对一个请求来说,只有首个匹配的 FlowSchema 才有意义。 如果一个入站请求与多个 FlowSchema 匹配,则将基于
matchingPrecedence
值最高的请求进行筛选。 如果一个请求匹配多个 FlowSchema 且matchingPrecedence
的值相同,则按name
的字典序选择最小, 但是最好不要依赖它,而是确保不存在两个 FlowSchema 具有相同的matchingPrecedence
值。
当给定的请求与某个 FlowSchema 的 rules
的其中一条匹配,那么就认为该请求与该 FlowSchema 匹配。 判断规则与该请求是否匹配,不仅要求该条规则的 subjects
字段至少存在一个与该请求相匹配, 而且要求该条规则的 resourceRules
或 nonResourceRules
(取决于传入请求是针对资源URL还是非资源URL)字段至少存在一个与该请求相匹配。
对于 subjects
中的 name
字段和资源和非资源规则的 verbs
,apiGroups
,resources
,namespaces
和 nonResourceURLs
字段, 可以指定通配符 *
来匹配任意值,从而有效地忽略该字段。
FlowSchema 的 distinguisherMethod.type
字段决定了如何把与该模式匹配的请求分散到各个流中。 可能是 ByUser
,在这种情况下,一个请求用户将无法饿死其他容量的用户; 或者是 ByNamespace
,在这种情况下,一个名称空间中的资源请求将无法饿死其它名称空间的资源请求; 或者它可以为空(或者可以完全省略 distinguisherMethod
), 在这种情况下,与此 FlowSchema 匹配的请求将被视为单个流的一部分。 资源和你的特定环境决定了如何选择正确一个 FlowSchema。
诊断程序
启用了 APF 的 API 服务器,它每个 HTTP 响应都有两个额外的 HTTP 头: X-Kubernetes-PF-FlowSchema-UID
和 X-Kubernetes-PF-PriorityLevel-UID
, 注意与请求匹配的 FlowSchema 和已分配的优先级。 如果请求用户没有查看这些对象的权限,则这些 HTTP 头中将不包含 API 对象的名称, 因此在调试时,你可以使用类似如下的命令:
kubectl get flowschemas -o custom-columns="uid:{metadata.uid},name:{metadata.name}"
kubectl get prioritylevelconfigurations -o custom-columns="uid:{metadata.uid},name:{metadata.name}"
来获取 UID 到 FlowSchema 的名称和 UID 到 PriorityLevelConfigurations 的名称的映射。
可观察性
指标
当你开启了 APF 后,kube-apiserver 会暴露额外指标。 监视这些指标有助于判断你的配置是否不当地限制了重要流量, 或者发现可能会损害系统健康的,行为不良的工作负载。
apiserver_flowcontrol_rejected_requests_total
是一个计数器向量, 记录被拒绝的请求数量(自服务器启动以来累计值), 由标签flowSchema
(表示与请求匹配的 FlowSchema ),priorityLevel
(表示分配给请该求的优先级)和reason
拆分。reason
标签将具有以下值之一:queue-full
,表明已经有太多请求排队,concurrency-limit
,表示将 PriorityLevelConfiguration 配置为Reject
而不是Queue
,或者time-out
, 表示在其排队时间超期的请求仍在队列中。
apiserver_flowcontrol_dispatched_requests_total
是一个计数器向量, 记录开始执行的请求数量(自服务器启动以来的累积值), 由标签flowSchema
(表示与请求匹配的 FlowSchema )和priorityLevel
(表示分配给该请求的优先级)拆分。apiserver_current_inqueue_requests
是一个表向量, 记录最近排队请求数量的高水位线, 由标签request_kind
分组,标签的值为mutating
或readOnly
。 这些高水位线表示在最近一秒钟内看到的最大数字。 它们补充说明了老的表向量apiserver_current_inflight_requests
(该量保存了最后一个窗口中,正在处理的请求数量的高水位线)。apiserver_flowcontrol_read_vs_write_request_count_samples
是一个直方图向量, 记录当前请求数量的观察值, 由标签phase
(取值为waiting
和executing
)和request_kind
(取值mutating
和readOnly
)拆分。 定期以高速率观察该值。apiserver_flowcontrol_read_vs_write_request_count_watermarks
是一个直方图向量, 记录请求数量的高/低水位线, 由标签phase
(取值为waiting
和executing
)和request_kind
(取值为mutating
和readOnly
)拆分; 标签mark
取值为high
和low
。apiserver_flowcontrol_read_vs_write_request_count_samples
向量观察到有值新增,则该向量累积。 这些水位线显示了样本值的范围。apiserver_flowcontrol_current_inqueue_requests
是一个表向量, 记录包含排队中的(未执行)请求的瞬时数量, 由标签priorityLevel
和flowSchema
拆分。apiserver_flowcontrol_current_executing_requests
是一个表向量, 记录包含执行中(不在队列中等待)请求的瞬时数量, 由标签priorityLevel
和flowSchema
拆分。apiserver_flowcontrol_priority_level_request_count_samples
是一个直方图向量, 记录当前请求的观测值,由标签phase
(取值为waiting
和executing
)和priorityLevel
拆分。 每个直方图都会定期进行观察,直到相关类别的最后活动为止。观察频率高。apiserver_flowcontrol_priority_level_request_count_watermarks
是一个直方图向量, 记录请求数的高/低水位线,由标签phase
(取值为waiting
和executing
)和priorityLevel
拆分; 标签mark
取值为high
和low
。apiserver_flowcontrol_priority_level_request_count_samples
向量观察到有值新增,则该向量累积。 这些水位线显示了样本值的范围。apiserver_flowcontrol_request_queue_length_after_enqueue
是一个直方图向量, 记录请求队列的长度,由标签priorityLevel
和flowSchema
拆分。 每个排队中的请求都会为其直方图贡献一个样本,并在添加请求后立即上报队列的长度。 请注意,这样产生的统计数据与无偏调查不同。说明:
直方图中的离群值在这里表示单个流(即,一个用户或一个名称空间的请求,具体取决于配置)正在疯狂请求 API 服务器,并受到限制。
相反,如果一个优先级的直方图显示该优先级的所有队列都比其他优先级的队列长,则增加 PriorityLevelConfigurations 的并发份额是比较合适的。
apiserver_flowcontrol_request_concurrency_limit
是一个表向量, 记录并发限制的计算值(基于 API 服务器的总并发限制和 PriorityLevelConfigurations 的并发份额), 并按标签priorityLevel
拆分。apiserver_flowcontrol_request_wait_duration_seconds
是一个直方图向量, 记录请求排队的时间, 由标签flowSchema
(表示与请求匹配的 FlowSchema ),priorityLevel
(表示分配该请求的优先级) 和execute
(表示请求是否开始执行)拆分。说明:
由于每个 FlowSchema 总会给请求分配 PriorityLevelConfigurations,因此你可以为一个优先级添加所有 FlowSchema 的直方图,以获取分配给该优先级的请求的有效直方图。
apiserver_flowcontrol_request_execution_seconds
是一个直方图向量, 记录请求实际执行需要花费的时间, 由标签flowSchema
(表示与请求匹配的 FlowSchema )和priorityLevel
(表示分配给该请求的优先级)拆分。
调试端点
启用 APF 特性后, kube-apiserver 会在其 HTTP/HTTPS 端口提供以下路径:
/debug/api_priority_and_fairness/dump_priority_levels
—— 所有优先级及其当前状态的列表。你可以这样获取:kubectl get --raw /debug/api_priority_and_fairness/dump_priority_levels
输出类似于:
PriorityLevelName, ActiveQueues, IsIdle, IsQuiescing, WaitingRequests, ExecutingRequests,
workload-low, 0, true, false, 0, 0,
global-default, 0, true, false, 0, 0,
exempt, <none>, <none>, <none>, <none>, <none>,
catch-all, 0, true, false, 0, 0,
system, 0, true, false, 0, 0,
leader-election, 0, true, false, 0, 0,
workload-high, 0, true, false, 0, 0,
/debug/api_priority_and_fairness/dump_queues
——所有队列及其当前状态的列表。你可以这样获取:kubectl get --raw /debug/api_priority_and_fairness/dump_queues
输出类似于:
PriorityLevelName, Index, PendingRequests, ExecutingRequests, VirtualStart,
workload-high, 0, 0, 0, 0.0000,
workload-high, 1, 0, 0, 0.0000,
workload-high, 2, 0, 0, 0.0000,
...
leader-election, 14, 0, 0, 0.0000,
leader-election, 15, 0, 0, 0.0000,
/debug/api_priority_and_fairness/dump_requests
——当前正在队列中等待的所有请求的列表。你可以这样获取:kubectl get --raw /debug/api_priority_and_fairness/dump_requests
输出类似于:
PriorityLevelName, FlowSchemaName, QueueIndex, RequestIndexInQueue, FlowDistingsher, ArriveTime,
exempt, <none>, <none>, <none>, <none>, <none>,
system, system-nodes, 12, 0, system:node:127.0.0.1, 2020-07-23T15:26:57.179170694Z,
针对每个优先级别,输出中还包含一条虚拟记录,对应豁免限制。
你可以使用以下命令获得更详细的清单:
kubectl get --raw '/debug/api_priority_and_fairness/dump_requests?includeRequestDetails=1'
输出类似于:
PriorityLevelName, FlowSchemaName, QueueIndex, RequestIndexInQueue, FlowDistingsher, ArriveTime, UserName, Verb, APIPath, Namespace, Name, APIVersion, Resource, SubResource,
system, system-nodes, 12, 0, system:node:127.0.0.1, 2020-07-23T15:31:03.583823404Z, system:node:127.0.0.1, create, /api/v1/namespaces/scaletest/configmaps,
system, system-nodes, 12, 1, system:node:127.0.0.1, 2020-07-23T15:31:03.594555947Z, system:node:127.0.0.1, create, /api/v1/namespaces/scaletest/configmaps,
接下来
有关API优先级和公平性的设计细节的背景信息, 请参阅增强建议。 你可以通过 SIG APIMachinery 提出建议和特性请求。