爱奇艺视频生产 Kubernetes 集群优化实践:感知业务优先级

作者:技术大发2022.04.12 15:59浏览量:243

简介:爱奇艺视频生产 Kubernetes 集群优化实践:感知业务优先级

本文介绍爱奇艺针对视频生产场景、在 Kubernetes(以下简称 K8s) 集群优化方面的实践:如何使高优先级任务获得更多的 CPU 资源,更快完成任务。

01. 背景

视频生产集群所面临的一个挑战是 K8s 原生机制无法区分业务优先级。视频生产任务拥有不同的业务优先级和资源需求,业务优先级是根据视频节目的重要程度及上线时间的紧迫程度进行区分的,重点节目相关的任务优先级高,上线时间紧迫的任务优先级高,其他节目的优先级低。

资源需求是从任务类型维度区分,如超清视频任务需要 8 CPU,普通视频任务需要 4 CPU。因此存在低优先级任务和高优先级任务申请相同数量 CPU 的情况,例如高优先级超清视频任务和低优先级超清视频任务都申请 8 CPU。

K8s 原生机制并不感知业务优先级,在上述情况下会给两者分配同样多的 CPU 资源。用户期望高优先级任务可以抢占低优先级任务的 CPU 资源,从而更快执行。

业界也提出了类似于业务优先级的概念,如文章 [1] 中提出的应用优先级,但是并未给出具体的解决方案。本文介绍了一种允许高优先级任务抢占低优先级任务 CPU 资源,加速高优先级任务执行的方案。该方案不仅可以在提交任务时指定优先级,还可以对执行过程中的任务进行优先级调整,满足视频生产效率要求。

02. 原理概述

为了给高优先级任务提供更多的 CPU 资源,我们在集群中每个物理节点上运行一个代理服务进程,该代理进程会区分节点上任务的优先级,将高优先级任务对应的 Cgroups cpu 子系统中控制 CPU 使用量的值调高,从而允许高优先级任务可以抢占低优先级任务的 CPU 资源,使其更快地执行。这个方案依赖于 K8s 集群中各个节点关闭 CPU 限流,从而使高优先级任务可以自由抢占低优先级任务的 CPU 资源,不会受到 CPU 限流的影响。

03.高优先级任务 CPU 抢占

1、前提:关闭节点 CPU 限流

在 K8s 中,任务以 Pod 形式存在,其 CPU 资源分配和使用受到 Pod 元数据中两个字段控制:CPU Resource Requests 和 CPU Resource Limits,Pod 可使用的 CPU 总量介于两者之间。CPU Resource Requests 为 Pod 提供了最少 CPU 保证,该字段的值会写入到 Pod 对应的 Cgroups cpu 子系统 cpu.shares 文件中,通过 cpu.shares 保证 Pod 使用的资源量。CPU Resource Limits 为 Pod 提供 CPU 使用上限,它的值对应到 Cgroups cpu 子系统的 cpu.cfs_quota_us 和 cpu.cfs_period_us 文件,具体对应关系请参考文章 [2]。

为了解除 CPU Resource Limits 对 Pod 的 CPU 限流,我们在视频生产 K8s 集群中通过 kubelet 的配置项 ‘—cpu-cfs-quota=false’ 关闭了 CPU CFS Quota,允许高优先级 Pod 自由抢占低优先级 Pod 的 CPU 资源而不会受到限流。

顺便一提,关闭 CPU CFS Quota 之后,Pod 在需要时,可以超额使用节点上存空闲的 CPU,提升了集群的 CPU 利用率和 Pod 的执行效率。

2、整体设计

该方案的名字为 cpu-share-syncer,基本原理章节中已经概述了其思想,整体设计如图 1 所示:

图片.jpg

⬆️图1 cpu-share-syncer 总体设计

该方案的核心组件是 cpu-share-syncer 代理服务,它运行于 K8s 各个工作节点之上,并通过与 K8s master 节点之上的 kube-apiserver 进程通信,获取其所在节点上的 Pod 信息,然后识别出节点上的高优先级 Pod,将这些 Pod 对应的 Cgroups cpu 子系统中 cpu.shares 文件值设置为用户指定的较高的值,从而赋予其较高的 CPU 权重,允许这些 Pod 抢占其他 Pod 的 CPU 资源。

3、使用方式

cpu-share-syncer 代理服务采用 K8s 中 DaemonSet 控制器的方式进行部署,使得每个 K8s 工作节点之上都运行了一个代理服务 Pod,这些 Pod 通过 hostpath 的方式挂载了宿主机 Cgroups 路径以调整 Pod 对应的 cpu.shares 文件。

代理服务如何知道哪些 Pod 是高优先级任务呢?这需要用户在创建高优先级 Pod 时,在 Pod 的注解字段中添加一个表示较高 CPU 权重的 annotation:iqiyi.com/cpu-share,该注解值的类型为整数,在含义上与 CPU Resource Requests 类似,因此用户可以根据 CPU Resource Requests 字段的规则来设置 iqiyi.com/cpu-share 的值,只是 iqiyi.com/cpu-share 仅支持以一个 CPU 的 1/1024 为单位。例如,如果期望 Pod 在运行时可以使用 10 CPU 的资源,iqiyi.com/cpu-share 的值应该设置为10 * 1024 = 10240,这和 Cgroups 中对 CPU 的计量单位保持一致。对于已经运行的 Pod,用户可以通过 patch 方式在 Pod 注解字段中添加 iqiyi.com/cpu-share 注解改变任务的优先级。

cpu-share-syncer 代理服务检测到 Pod 中存在这个 annotation,就会将其值写入到 Pod 对应 cpu.shares 文件中,达到用户指定 Pod CPU 权重的目的。

因此,对于设置了 iqiyi.com/cpu-share annotation 的 Pod,会按照 CPU Resource Requests 字段的值进行调度,按照 iqiyi.com/cpu-share的值在节点上分配 CPU 资源。

iqiyi.com/cpu-share 的值越大,Pod 对 CPU 的权重就越高,抢占其他 Pod CPU 资源就会越多。对于高优先级 Pod,用户可以根据需要将 iqiyi.com/cpu-share 的值设置为较高的值,获取更多的 CPU 资源。

4、技术细节

cpu-share-syncer 代理服务的详细工作流程如图 2 所示:

图片.jpg

⬆️图2 cpu-share-syncer 详细处理流程

cpu-share-syncer 代理是一个长生命周期的任务,它会周期性地将用户通过 Pod annotation 指定的 CPU 权重写入到 Pod 对应的 cpu.shares 文件中,周期性执行的原因是防止 Pod 由于某种原因重启之后,用户指定的 CPU 权重丢失。周期可以由用户进行配置,默认与 kubelet 进程中 CPUManager 同步周期(默认 10 秒)保持一致。需要注意的是,kubelet 中 CPUManager 会周期性的同步 Pod 中容器对应的 cpu.shares 文件,该操作仅会影响 Pod 内容器之间的相对优先级,详细信息可参考 [3]。而 cpu-share-syncer 代理同步的是 Pod 自身对应的 cpu.shares 文件,该同步工作会改变整个 Pod 相对于其他 Pod 的优先级。两个同步操作互不影响。

cpu-share-syncer 代理会从 master上的 kube-apiserver 进程获取其所在节点上的 Pod 信息,为了避免每次全量从 kube-apiserver 获取节点上所有 Pod 信息,减轻 kube-apiserver 的负担,使用 List-Watch 的方式获取。

在获取节点上所有 Pod 之后,cpu-share-syncer 代理会对这些 Pod 进行遍历,并从 Pod 注解中取出 iqiyi.com/cpu-share 对应的值。然后根据 Pod UID 找到其对应的 cpu.shares 文件,将获取的值写入到这个文件中。对于用户未指定优先级的 Pod,cpu-share-syncer 不进行处理,这些 Pod 对应的 cpu.shares 值仍为 CPU Resource Requests 对应的值,例如 1 核的 CPU 申请对应的 cpu.shares 仍为 1024,没有被改动。这样高优先级 Pod 在节点上就被赋予更高的 CPU 权重,因而可以抢占其他 Pod 的 CPU 资源,从而更快地执行。

04. cpu-share-syncer 测试结果

为了验证 cpu-share-syncer 的效果及其带来的收益,我们进行了单机功能验证和线上环境灰度测试。我们在 K8s 集群中的一个节点上,运行了两个 Pod,将它们的 CPU Resource Requests 值都设置为 10,并分别设置其 iqiyi.com/cpu-share 的值为 8192 及 16384,两个 Pod 中执行相同的压力测试命令 ‘stress —cpu 40’,表示每个 Pod 启动 40 个线程对 CPU 性能进行压力测试。使用 prometheus 收集相关指标,测试结果如下图所示:

图片.jpg

⬆️图3 两个 Pod 对比验证

图中横轴表示时钟时间,纵轴表示 Pod 运行所占用的 CPU 总时间,单位为秒,蓝色折线表示权重为 16384 的 Pod 执行所占用的 CPU 时间,黄色折线表示权重为 8192 的 Pod 执行所占用的 CPU 时间,可观察到这两个 Pod 所耗费的 CPU 时间比例大约为 2:1,说明Pod CPU 权重已经生效,图中由于两个 Pod 启动先后顺序问题,存在一定的误差。

随后在相同的节点上运行 3 个 CPU Resource Requests 为 10 CPU 的 Pod,不指定 CPU 权重,在运行一段时间后,再次启动一个 CPU Resource Requests 为 10 CPU 的 Pod,并指定其权重值为 262144 (表示极高的优先级,这是 cpu.shares 允许的最大值,参考 [4] ),在这四个 Pod 中执行的命令同样为 ‘stress —cpu 40’,测试结果如下图所示:

图片.jpg

⬆️图4 高优先级 Pod 抢占 CPU 时间

图中坐标轴含义与上图一致,黄绿蓝三条折线表示先启动的三个 Pod,红色折线表示后启动的一个高优先级的 Pod。可以明显观察到,高优先级的 Pod 启动后,它所占用的 CPU 总时间大幅度高速增长,而早期启动的三个 Pod 由于受到高优先级 Pod 的挤压,所占用的 CPU 总时间几乎保持不变,说明高优先级任务抢占了早期 Pod 的 CPU 资源。

随后我们在一个线上 K8s 集群中,进行了灰度测试,对 50% 的常规视频生产任务指定了 CPU 权重 262144,另外 50% 生产任务未指定 CPU 权重,并观察其两组任务的平均执行效率,得到如下所示的结果:

图片.jpg

⬆️图5 生产环境灰度验证

图中横轴为时钟时间,纵轴为任务执行耗时比(执行时间/介质大小),耗时比越小效率越高,绿色折线表示未指定 CPU 权重的任务,蓝色折线表示指定了高 CPU 权重的任务。发现相较于未添加 CPU 权重的任务,高 CPU 权重的 Pod 执行效率提升了约 16%,实现了对高优先级任务加速的目的。

需要注意的是,从 CPU 权重(cpu.shares)比例来看,当高优先级任务存在且持续忙碌时,低优先级任务仅能获得非常少的 CPU 资源,参考 [5],效率提升会远不止 16%。经过分析,我们发现灰度环境中的高优先级视频生产任务即使在完全放开 CPU 限流时,也不会吃掉机器上所有的 CPU 资源,它只占用自己所需的 CPU 资源。因此,灰度环境中的高优先级任务只是抢占了一部分低优先级任务的 CPU 资源满足自己的需求。另外,视频生产任务在整个生命周期中,需要经过从远端服务器下载原始数据、集群中处理、将结果上传到远端服务器三个过程。在下载、上传数据等待 IO 的过程中,高优先级任务不需要占用 CPU 资源,此时,低优先级任务可以再次获得一部分 CPU 资源。综合上面两部分原因,我们认为高优先级任务执行效率提升 16% 是合理的。

05.总结及展望

我们通过 cpu-share-syncer 在关闭了 CPU CFS Quota 的 K8s 集群中提供了高优先级任务优先使用 CPU 的能力,实现了对高优先级任务的加速。目前 cpu-share-syncer 已经广泛运行在爱奇艺视频生产集群中,为高优先级任务护航。

cpu-share-syncer 目前仅运行在工作节点上,未在调度层面对高优先级任务进行识别,可能会出现多个高优先级任务调度到同一个节点,并发生资源竞争的情况,后续我们会继续在调度层面进行优化,使高优先级任务尽量均匀分布在各个节点之上。

引用