API 优先级和公平性

    对于集群管理员来说,控制 Kubernetes API 服务器在过载情况下的行为是一项关键任务。 kube-apiserver 有一些控件(例如:命令行标志 --max-requests-inflight--max-mutating-requests-inflight), 可以限制将要接受的未处理的请求,从而防止过量请求入站,潜在导致 API 服务器崩溃。 但是这些标志不足以保证在高流量期间,最重要的请求仍能被服务器接受。

    API 优先级和公平性(APF)是一种替代方案,可提升上述最大并发限制。 APF 以更细粒度的方式对请求进行分类和隔离。 它还引入了空间有限的排队机制,因此在非常短暂的突发情况下,API 服务器不会拒绝任何请求。 通过使用公平排队技术从队列中分发请求,这样, 一个行为不佳的就不会饿死其他控制器 (即使优先级相同)。

    本功能特性在设计上期望其能与标准控制器一起工作得很好; 这类控制器使用通知组件(Informers)获得信息并对 API 请求的失效作出反应, 在处理失效时能够执行指数型回退。其他客户端也以类似方式工作。

    注意:

    属于 “长时间运行” 类型的某些请求(例如远程命令执行或日志拖尾)不受 API 优先级和公平性过滤器的约束。 如果未启用 APF 特性,即便设置 --max-requests-inflight 标志,该类请求也不受约束。 APF 适用于 watch 请求。当 APF 被禁用时,watch 请求不受 --max-requests-inflight 限制。

    API 优先级与公平性(APF)特性由特性门控控制,默认情况下启用。 有关特性门控的一般性描述以及如何启用和禁用特性门控, 请参见。 APF 的特性门控称为 APIPriorityAndFairness。 此特性也与某个 API 组相关: (a) v1alpha1 版本,默认被禁用; (b) v1beta1v1beta2 版本,默认被启用。 你可以在启动 kube-apiserver 时,添加以下命令行标志来禁用此功能门控及 API Beta 组:

    或者,你也可以通过 --runtime-config=flowcontrol.apiserver.k8s.io/v1alpha1=true 启用 API 组的 v1alpha1 版本。

    命令行标志 --enable-priority-fairness=false 将彻底禁用 APF 特性, 即使其他标志启用它也是无效。

    概念

    APF 特性包含几个不同的功能。 传入的请求通过 FlowSchema 按照其属性分类,并分配优先级。 每个优先级维护自定义的并发限制,加强了隔离度,这样不同优先级的请求,就不会相互饿死。 在同一个优先级内,公平排队算法可以防止来自不同 流(Flow) 的请求相互饿死。 该算法将请求排队,通过排队机制,防止在平均负载较低时,通信量突增而导致请求失败。

    如果未启用 APF,API 服务器中的整体并发量将受到 kube-apiserver 的参数 --max-requests-inflight--max-mutating-requests-inflight 的限制。 启用 APF 后,将对这些参数定义的并发限制进行求和,然后将总和分配到一组可配置的 优先级 中。 每个传入的请求都会分配一个优先级;每个优先级都有各自的配置,设定允许分发的并发请求数。

    例如,默认配置包括针对领导者选举请求、内置控制器请求和 Pod 请求都单独设置优先级。 这表示即使异常的 Pod 向 API 服务器发送大量请求,也无法阻止领导者选举或内置控制器的操作执行成功。

    请求占用的席位

    上述并发管理的描述是基线情况。其中,各个请求具有不同的持续时间, 但在与一个优先级的并发限制进行比较时,这些请求在任何给定时刻都以同等方式进行计数。 在这个基线场景中,每个请求占用一个并发单位。 我们用 “席位(Seat)” 一词来表示一个并发单位,其灵感来自火车或飞机上每位乘客占用一个固定座位的供应方式。

    但有些请求所占用的席位不止一个。有些请求是服务器预估将返回大量对象的 list 请求。 和所需运行时间相近的其他请求相比,我们发现这类请求会给服务器带来异常沉重的负担。 出于这个原因,服务器估算将返回的对象数量,并认为请求所占用的席位数与估算得到的数量成正比。

    watch 请求的执行时间调整

    APF 管理 watch 请求,但这需要考量基线行为之外的一些情况。 第一个关注点是如何判定 watch 请求的席位占用时长。 取决于请求参数不同,对 watch 请求的响应可能以针对所有预先存在的对象 create 通知开头,也可能不这样。 一旦最初的突发通知(如果有)结束,APF 将认为 watch 请求已经用完其席位。

    每当向服务器通知创建/更新/删除一个对象时,正常通知都会以并发突发的方式发送到所有相关的 watch 响应流。 为此,APF 认为每个写入请求都会在实际写入完成后花费一些额外的时间来占用席位。 服务器估算要发送的通知数量,并调整写入请求的席位数以及包含这些额外工作后的席位占用时间。

    排队

    即使在同一优先级内,也可能存在大量不同的流量源。 在过载情况下,防止一个请求流饿死其他流是非常有价值的 (尤其是在一个较为常见的场景中,一个有故障的客户端会疯狂地向 kube-apiserver 发送请求, 理想情况下,这个有故障的客户端不应对其他客户端产生太大的影响)。 公平排队算法在处理具有相同优先级的请求时,实现了上述场景。 每个请求都被分配到某个 流(Flow) 中,该 由对应的 FlowSchema 的名字加上一个 流区分项(Flow Distinguisher) 来标识。 这里的流区分项可以是发出请求的用户、目标资源的名字空间或什么都不是。 系统尝试为不同流中具有相同优先级的请求赋予近似相等的权重。 要启用对不同实例的不同处理方式,多实例的控制器要分别用不同的用户名来执行身份认证。

    将请求划分到流中之后,APF 功能将请求分配到队列中。 分配时使用一种称为混洗分片(Shuffle-Sharding)的技术。 该技术可以相对有效地利用队列隔离低强度流与高强度流。

    排队算法的细节可针对每个优先等级进行调整,并允许管理员在内存占用、 公平性(当总流量超标时,各个独立的流将都会取得进展)、 突发流量的容忍度以及排队引发的额外延迟之间进行权衡。

    某些特别重要的请求不受制于此特性施加的任何限制。 这些豁免可防止不当的流控配置完全禁用 API 服务器。

    流控 API 涉及两种资源。 定义可用的优先级和可处理的并发预算量,还可以微调排队行为。 FlowSchema 用于对每个入站请求进行分类,并与一个 PriorityLevelConfiguration 相匹配。 此外同一 API 组还有一个 v1alpha1 版本,其中包含语法和语义都相同的资源类别。

    PriorityLevelConfiguration

    一个 PriorityLevelConfiguration 表示单个优先级。每个 PriorityLevelConfiguration 对未完成的请求数有各自的限制,对排队中的请求数也有限制。

    PriorityLevelConfiguration 的并发限制不是指定请求绝对数量,而是在“并发份额”中指定。 API 服务器的总并发量限制通过这些份额按例分配到现有 PriorityLevelConfiguration 中。 集群管理员可以更改 --max-requests-inflight (或 --max-mutating-requests-inflight)的值, 再重新启动 kube-apiserver 来增加或减小服务器的总流量, 然后所有的 PriorityLevelConfiguration 将看到其最大并发增加(或减少)了相同的比例。

    注意:

    启用 APF 特性后,服务器的总并发量限制将设置为 --max-requests-inflight--max-mutating-requests-inflight 之和。 可变请求和不可变请求之间不再有任何区别; 如果对于某种资源,你需要区别对待不同请求,请创建不同的 FlowSchema 分别匹配可变请求和不可变请求。

    当入站请求的数量大于分配的 PriorityLevelConfiguration 中允许的并发级别时, type 字段将确定对额外请求的处理方式。 Reject 类型,表示多余的流量将立即被 HTTP 429(请求过多)错误所拒绝。 Queue 类型,表示对超过阈值的请求进行排队,将使用阈值分片和公平排队技术来平衡请求流之间的进度。

    公平排队算法支持通过排队配置对优先级微调。 可以在增强建议中阅读算法的详细信息, 但总之:

    • queues 递增能减少不同流之间的冲突概率,但代价是增加了内存使用量。 值为 1 时,会禁用公平排队逻辑,但仍允许请求排队。

    • queueLengthLimit 递增可以在不丢弃任何请求的情况下支撑更大的突发流量, 但代价是增加了等待时间和内存使用量。

    • 修改 handSize 允许你调整过载情况下不同流之间的冲突概率以及单个流可用的整体并发性。

      说明:

      较大的 handSize 使两个单独的流程发生碰撞的可能性较小(因此,一个流可以饿死另一个流), 但是更有可能的是少数流可以控制 apiserver。 较大的 handSize 还可能增加单个高并发流的延迟量。 单个流中可能排队的请求的最大数量为 handSize * queueLengthLimit

    FlowSchema

    FlowSchema 匹配一些入站请求,并将它们分配给优先级。 每个入站请求都会对所有 FlowSchema 测试是否匹配, 首先从 matchingPrecedence 数值最低的匹配开始(我们认为这是逻辑上匹配度最高), 然后依次进行,直到首个匹配出现。

    注意:

    对一个请求来说,只有首个匹配的 FlowSchema 才有意义。 如果一个入站请求与多个 FlowSchema 匹配,则将基于 matchingPrecedence 值最高的请求进行筛选。 如果一个请求匹配多个 FlowSchema 且 matchingPrecedence 的值相同,则按 name 的字典序选择最小, 但是最好不要依赖它,而是确保不存在两个 FlowSchema 具有相同的 matchingPrecedence 值。

    当给定的请求与某个 FlowSchema 的 rules 的其中一条匹配,那么就认为该请求与该 FlowSchema 匹配。 判断规则与该请求是否匹配,不仅要求该条规则的 subjects 字段至少存在一个与该请求相匹配, 而且要求该条规则的 resourceRulesnonResourceRules (取决于传入请求是针对资源URL还是非资源URL)字段至少存在一个与该请求相匹配。

    对于 subjects 中的 name 字段和资源和非资源规则的 verbsapiGroupsresourcesnamespacesnonResourceURLs 字段, 可以指定通配符 * 来匹配任意值,从而有效地忽略该字段。

    FlowSchema 的 distinguisherMethod.type 字段决定了如何把与该模式匹配的请求分散到各个流中。 可能是 ByUser,在这种情况下,一个请求用户将无法饿死其他容量的用户; 或者是 ByNamespace,在这种情况下,一个名字空间中的资源请求将无法饿死其它名字空间的资源请求; 或者它可以为空(或者可以完全省略 distinguisherMethod), 在这种情况下,与此 FlowSchema 匹配的请求将被视为单个流的一部分。 资源和你的特定环境决定了如何选择正确一个 FlowSchema。

    默认值

    每个 kube-apiserver 会维护两种类型的 APF 配置对象:强制的(Mandatory)和建议的(Suggested)。

    强制的配置对象

    有四种强制的配置对象对应内置的守护行为。这里的行为是服务器在还未创建对象之前就具备的行为, 而当这些对象存在时,其规约反映了这类行为。四种强制的对象如下:

    • 强制的 exempt 优先级用于完全不受流控限制的请求:它们总是立刻被分发。 强制的 exempt FlowSchema 把 system:masters 组的所有请求都归入该优先级。 如果合适,你可以定义新的 FlowSchema,将其他请求定向到该优先级。

    • 强制的 catch-all 优先级与强制的 catch-all FlowSchema 结合使用, 以确保每个请求都分类。一般而言,你不应该依赖于 catch-all 的配置, 而应适当地创建自己的 catch-all FlowSchema 和 PriorityLevelConfiguration (或使用默认安装的 global-default 配置)。 因为这一优先级不是正常场景下要使用的,catch-all 优先级的并发度份额很小, 并且不会对请求进行排队。

    建议的 FlowSchema 和 PriorityLevelConfiguration 包含合理的默认配置。 你可以修改这些对象或者根据需要创建新的配置对象。如果你的集群可能承受较重负载, 那么你就要考虑哪种配置最合适。

    建议的配置把请求分为六个优先级:

    • node-high 优先级用于来自节点的健康状态更新。

    • system 优先级用于 system:nodes 组(即 kubelet)的与健康状态更新无关的请求; kubelets 必须能连上 API 服务器,以便工作负载能够调度到其上。

    • leader-election 优先级用于内置控制器的领导选举的请求 (特别是来自 kube-system 名字空间中 system:kube-controller-managersystem:kube-scheduler 用户和服务账号,针对 endpointsconfigmaps 或 的请求)。 将这些请求与其他流量相隔离非常重要,因为领导者选举失败会导致控制器发生故障并重新启动, 这反过来会导致新启动的控制器在同步信息时,流量开销更大。

    • workload-high 优先级用于内置控制器的其他请求。

    • global-default 优先级可处理所有其他流量,例如:非特权用户运行的交互式 kubectl 命令。

    建议的 FlowSchema 用来将请求导向上述的优先级内,这里不再一一列举。

    强制的与建议的配置对象的维护

    每个 kube-apiserver 都独立地维护其强制的与建议的配置对象, 这一维护操作既是服务器的初始行为,也是其周期性操作的一部分。 因此,当存在不同版本的服务器时,如果各个服务器对于配置对象中的合适内容有不同意见, 就可能出现抖动。

    每个 kube-apiserver 都会对强制的与建议的配置对象执行初始的维护操作, 之后(每分钟)对这些对象执行周期性的维护。

    对于强制的配置对象,维护操作包括确保对象存在并且包含合适的规约(如果存在的话)。 服务器会拒绝创建或更新与其守护行为不一致的规约。

    对建议的配置对象的维护操作被设计为允许其规约被重载。删除操作是不允许的, 维护操作期间会重建这类配置对象。如果你不需要某个建议的配置对象, 你需要将它放在一边,并让其规约所产生的影响最小化。 对建议的配置对象而言,其维护方面的设计也支持在上线新的 kube-apiserver 时完成自动的迁移动作,即便可能因为当前的服务器集合存在不同的版本而可能造成抖动仍是如此。

    对建议的配置对象的维护操作包括基于服务器建议的规约创建对象 (如果对象不存在的话)。反之,如果对象已经存在,维护操作的行为取决于是否 kube-apiserver 或者用户在控制对象。如果 kube-apiserver 在控制对象, 则服务器确保对象的规约与服务器所给的建议匹配,如果用户在控制对象, 对象的规约保持不变。

    关于谁在控制对象这个问题,首先要看对象上的 apf.kubernetes.io/autoupdate-spec 注解。如果对象上存在这个注解,并且其取值为true,则 kube-apiserver 在控制该对象。如果存在这个注解,并且其取值为false,则用户在控制对象。 如果这两个条件都不满足,则需要进一步查看对象的 metadata.generation。 如果该值为 1,则 kube-apiserver 控制对象,否则用户控制对象。 这些规则是在 1.22 发行版中引入的,而对 metadata.generation 的考量是为了便于从之前较简单的行为迁移过来。希望控制建议的配置对象的用户应该将对象的 apf.kubernetes.io/autoupdate-spec 注解设置为 false

    对强制的或建议的配置对象的维护操作也包括确保对象上存在 apf.kubernetes.io/autoupdate-spec 这一注解,并且其取值准确地反映了是否 kube-apiserver 在控制着对象。

    维护操作还包括删除那些既非强制又非建议的配置,同时注解配置为 apf.kubernetes.io/autoupdate-spec=true 的对象。

    推荐配置没有为本地 kubelet 对 kube-apiserver 执行健康检查的请求进行任何特殊处理 ——它们倾向于使用安全端口,但不提供凭据。 在推荐配置中,这些请求将分配 global-default FlowSchema 和 global-default 优先级, 这样其他流量可以排除健康检查。

    如果添加以下 FlowSchema,健康检查请求不受速率限制。

    注意:

    进行此更改后,任何敌对方都可以发送与此 FlowSchema 匹配的任意数量的健康检查请求。 如果你有 Web 流量过滤器或类似的外部安全机制保护集群的 API 服务器免受常规网络流量的侵扰, 则可以配置规则,阻止所有来自集群外部的健康检查请求。

    1. apiVersion: flowcontrol.apiserver.k8s.io/v1beta2
    2. kind: FlowSchema
    3. metadata:
    4. name: health-for-strangers
    5. spec:
    6. matchingPrecedence: 1000
    7. priorityLevelConfiguration:
    8. name: exempt
    9. rules:
    10. - nonResourceRules:
    11. - nonResourceURLs:
    12. - "/healthz"
    13. - "/livez"
    14. - "/readyz"
    15. verbs:
    16. - "*"
    17. subjects:
    18. - kind: Group
    19. group:
    20. name: system:unauthenticated

    问题诊断

    启用了 APF 的 API 服务器,它每个 HTTP 响应都有两个额外的 HTTP 头: X-Kubernetes-PF-FlowSchema-UIDX-Kubernetes-PF-PriorityLevel-UID, 注意与请求匹配的 FlowSchema 和已分配的优先级。 如果请求用户没有查看这些对象的权限,则这些 HTTP 头中将不包含 API 对象的名称, 因此在调试时,你可以使用类似如下的命令:

    1. kubectl get flowschemas -o custom-columns="uid:{metadata.uid},name:{metadata.name}"
    2. kubectl get prioritylevelconfigurations -o custom-columns="uid:{metadata.uid},name:{metadata.name}"

    指标

    说明:

    在 Kubernetes v1.20 之前的版本中,标签 flow_schemapriority_level 的名称有时被写作 flowSchemapriorityLevel,即存在不一致的情况。 如果你在运行 Kubernetes v1.19 或者更早版本,你需要参考你所使用的集群版本对应的文档。

    当你开启了 APF 后,kube-apiserver 会暴露额外指标。 监视这些指标有助于判断你的配置是否不当地限制了重要流量, 或者发现可能会损害系统健康的,行为不良的工作负载。

    • apiserver_flowcontrol_rejected_requests_total 是一个计数器向量, 记录被拒绝的请求数量(自服务器启动以来累计值), 由标签 flow_chema(表示与请求匹配的 FlowSchema)、priority_evel (表示分配给请该求的优先级)和 reason 来区分。 reason 标签将具有以下值之一:

      • queue-full,表明已经有太多请求排队,
      • concurrency-limit,表示将 PriorityLevelConfiguration 配置为 Reject 而不是 Queue,或者
      • time-out,表示在其排队时间超期的请求仍在队列中。
    • apiserver_flowcontrol_dispatched_requests_total 是一个计数器向量, 记录开始执行的请求数量(自服务器启动以来的累积值), 由标签 flow_schema(表示与请求匹配的 FlowSchema)和 priority_level(表示分配给该请求的优先级)来区分。

    • apiserver_current_inqueue_requests 是一个表向量, 记录最近排队请求数量的高水位线, 由标签 request_kind 分组,标签的值为 mutatingreadOnly。 这些高水位线表示在最近一秒钟内看到的最大数字。 它们补充说明了老的表向量 apiserver_current_inflight_requests (该量保存了最后一个窗口中,正在处理的请求数量的高水位线)。

    • apiserver_flowcontrol_read_vs_write_request_count_watermarks 是一个直方图向量, 记录请求数量的高/低水位线, 由标签 phase(取值为 waitingexecuting)和 request_kind (取值为 mutatingreadOnly)拆分;标签 mark 取值为 highlowapiserver_flowcontrol_read_vs_write_request_count_samples 向量观察到有值新增, 则该向量累积。这些水位线显示了样本值的范围。

    • apiserver_flowcontrol_current_inqueue_requests 是一个表向量, 记录包含排队中的(未执行)请求的瞬时数量, 由标签 priorityLevelflowSchema 拆分。

    • apiserver_flowcontrol_current_executing_requests 是一个表向量, 记录包含执行中(不在队列中等待)请求的瞬时数量, 由标签 priority_levelflow_schema 进一步区分。

    • apiserver_flowcontrol_request_concurrency_in_use 是一个规范向量, 包含占用座位的瞬时数量,由标签 priority_levelflow_schema 进一步区分。

    • apiserver_flowcontrol_priority_level_request_count_samples 是一个直方图向量, 记录当前请求的观测值,由标签 phase(取值为waitingexecuting)和 priority_level 进一步区分。 每个直方图都会定期进行观察,直到相关类别的最后活动为止。观察频率高。

    • apiserver_flowcontrol_priority_level_request_count_watermarks 是一个直方图向量, 记录请求数的高/低水位线,由标签 phase(取值为 waitingexecuting)和 priority_level 拆分; 标签 mark 取值为 highlowapiserver_flowcontrol_priority_level_request_count_samples 向量观察到有值新增, 则该向量累积。这些水位线显示了样本值的范围。

    • apiserver_flowcontrol_request_queue_length_after_enqueue 是一个直方图向量, 记录请求队列的长度,由标签 priority_levelflow_schema 进一步区分。 每个排队中的请求都会为其直方图贡献一个样本,并在添加请求后立即上报队列的长度。 请注意,这样产生的统计数据与无偏调查不同。

      说明:

      直方图中的离群值在这里表示单个流(即,一个用户或一个名字空间的请求, 具体取决于配置)正在疯狂地向 API 服务器发请求,并受到限制。 相反,如果一个优先级的直方图显示该优先级的所有队列都比其他优先级的队列长, 则增加 PriorityLevelConfiguration 的并发份额是比较合适的。

    • apiserver_flowcontrol_request_concurrency_limit 是一个表向量, 记录并发限制的计算值(基于 API 服务器的总并发限制和 PriorityLevelConfiguration 的并发份额),并按标签 priority_level 进一步区分。

    • apiserver_flowcontrol_request_wait_duration_seconds 是一个直方图向量, 记录请求排队的时间, 由标签 flow_schema(表示与请求匹配的 FlowSchema), priority_level(表示分配该请求的优先级) 和 execute(表示请求是否开始执行)进一步区分。

      说明:

      由于每个 FlowSchema 总会给请求分配 PriorityLevelConfiguration, 因此你可以为一个优先级添加所有 FlowSchema 的直方图,以获取分配给该优先级的请求的有效直方图。

    • apiserver_flowcontrol_request_execution_seconds 是一个直方图向量, 记录请求实际执行需要花费的时间, 由标签 flow_schema(表示与请求匹配的 FlowSchema)和 priority_level(表示分配给该请求的优先级)进一步区分。

    调试端点

    启用 APF 特性后, kube-apiserver 会在其 HTTP/HTTPS 端口提供以下路径:

    • /debug/api_priority_and_fairness/dump_priority_levels —— 所有优先级及其当前状态的列表。你可以这样获取:

      输出类似于:

      1. PriorityLevelName, ActiveQueues, IsIdle, IsQuiescing, WaitingRequests, ExecutingRequests,
      2. workload-low, 0, true, false, 0, 0,
      3. global-default, 0, true, false, 0, 0,
      4. exempt, <none>, <none>, <none>, <none>, <none>,
      5. catch-all, 0, true, false, 0, 0,
      6. system, 0, true, false, 0, 0,
      7. leader-election, 0, true, false, 0, 0,
      8. workload-high, 0, true, false, 0, 0,
    • /debug/api_priority_and_fairness/dump_queues —— 所有队列及其当前状态的列表。 你可以这样获取:

      1. kubectl get --raw /debug/api_priority_and_fairness/dump_queues

      输出类似于:

    • /debug/api_priority_and_fairness/dump_requests —— 当前正在队列中等待的所有请求的列表。 你可以这样获取:

      1. kubectl get --raw /debug/api_priority_and_fairness/dump_requests

      输出类似于:

      1. PriorityLevelName, FlowSchemaName, QueueIndex, RequestIndexInQueue, FlowDistingsher, ArriveTime,
      2. exempt, <none>, <none>, <none>, <none>, <none>,
      3. system, system-nodes, 12, 0, system:node:127.0.0.1, 2020-07-23T15:26:57.179170694Z,

      针对每个优先级别,输出中还包含一条虚拟记录,对应豁免限制。

      你可以使用以下命令获得更详细的清单:

      输出类似于:

      1. PriorityLevelName, FlowSchemaName, QueueIndex, RequestIndexInQueue, FlowDistingsher, ArriveTime, UserName, Verb, APIPath, Namespace, Name, APIVersion, Resource, SubResource,
      2. 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,

    接下来