混部之殇-论云原生资源隔离技术之CPU隔离
The following article is from 腾讯云原生 Author 蒋彪
导语
混部,通常指在离线混部(也有离在线混部之说),意指通过将在线业务(通常为延迟敏感型高优先级任务)和离线任务(通常为 CPU 消耗型低优先级任务)同时混合部署在同一个节点上,以期提升节点的资源利用率。其中的关键难点在于底层资源隔离技术,严重依赖于 OS 内核,而现有的原生 Linux kernel 提供的资源隔离能力在面对混部需求时,再次显得有些捉襟见肘(或至少说不够完美),仍需深度 Hack,方能满足生产级别的需求。
(云原生)资源隔离技术主要包括 CPU、memory、IO 和网络4个方面。本文聚焦于 CPU 隔离技术和相关背景,后续(系列)再循序渐进,逐步展开到其他方面。
背景
无论在 IDC,还是在云场景中,资源利用率低都绝对是大部分用户/厂商面临的共同难题。一方面,硬件成本很高(大家都是靠买,而且绝大部分硬件(核心技术掌握于别人手中,没有定价权,议价能力也通常很弱),生命周期还很短(过几年还得换新);另一方面,极度尴尬的是这么贵的东西无法充分利用,拿 CPU 占用率来说,绝大部分场景的平均占用率都很低(如果我拍不超过20%(这里指日均值,或周均值),相信大部分同学都不会有意见。这就意味着,贼贵的东西实际只用了不到五分之一,如果你还想正经的居家过日子,想必都会觉得心疼。
因此,提升主机(节点)的资源利用率是一项非常值得探索,同时收益非常明显的任务,解决思路也非常直接。
常规的思维模式:多跑点业务。说起来容易,谁没试过呢。核心困难在于:通常的业务都有明显的峰谷特征。
你希望的样子可能是这样的:
但现实的样子多半是这样的:
而在为业务做容量规划,是需要按 Worst Case 做(假设所有业务的优先级都一样),具体来说,从 CPU 层面的话,就需要按 CPU 峰值(可能是周峰值、甚至月/年峰值)的来规划容量(通常还得留一定的余量,应对突发)。
而现实中大部分情况是:峰值很高,但实际的均值很低。因此导致了绝大部分场景中的 CPU 均值都很低,实际 CPU 利用率很低。
前面做了个假设:“所有业务的优先级都一样”,业务的 Worst Case 决定了整机的最终表现(资源利用率低)。如果换种思路,但业务区分优先级时,就有更多的发挥空间了,可以通过牺牲低优先级业务的服务质量(通常可以容忍)来保证高优先级业务的服务质量,如此能部署在适量高优先级业务的同时,部署更多的业务(低优先级),从而整体上提升资源利用率。
混部(混合部署)因此应运而生。这里的“混”,本质上就是“区分优先级”。狭义上,可以简单的理解为“在线+离线”(在离线)混部,广义上,可以扩展到更广的应用范围:多优先级业务混合部署。
其中涉及的核心技术包括两个层面:
底层资源隔离技术。(通常)由操作系统(内核)提供,这是本(系列)文章的核心关注点。 上层的资源调度技术。(通常)由上层的资源编排/调度框架(典型如 K8s)提供,打算另做系列文章展开,敬请期待。
混部也是业界非常热门的话题和技术方向,当前主流的头部大厂都在持续投入,价值显而易见,也有较高的技术门槛(壁垒)。相关技术起源甚早,颇有渊源,大名鼎鼎的 K8s(前身 Borg)其实源于 Google 的混部场景,而从混部的历史和效果看,Google 算是行业内的标杆,号称 CPU 占用率(均值)能做到60%,具体可参考其经典论文:
https://dl.acm.org/doi/pdf/10.1145/3342195.3387517
https://storage.googleapis.com/pub-tools-public-publication-data/pdf/43438.pdf
当然,腾讯(云)在混部方向的探索也很早,也经历了几次大的技术/方案迭代,至今已有不错的落地规模和成果,详情就不在本文多作探讨。
技术挑战
如前面所说,混部场景中,底层资源隔离技术至关重要,其中的“资源”,整体上分为4个大类:
CPU Memory IO 网络
本文聚焦于 CPU 隔离技术,主要分析在 CPU 隔离层面的技术难点、现状和方案。
CPU隔离
前面说的4类资源中,CPU 资源隔离可以说是最基础的隔离技术。一方面,CPU 是可压缩(可复用)资源,复用难度相对较低,Upstream的解决方案可用性相对较好;另一方面,CPU 资源与其他资源关联性较强,其他资源的使用(申请/释放)往往依赖于进程上下文,间接依赖于 CPU 资源。举例来说,当 CPU 被隔离(压制)后,其他如 IO、网络的请求可能(大部分情况)因为 CPU 被压制(得不到调度),从而也随之被压制。
因此,CPU 隔离的效果也会间接影响其他资源的隔离效果,CPU 隔离是最核心的隔离技术。
内核调度器
具体来说,落地到 OS 中,CPU 隔离本质上完全依赖于内核调度器实现,内核调度器是负载分配 CPU 资源的内核基本功能单元(很官方的说法),具体来说(狭义说),可以对应到我们接触最多的 Linux 内核的默认调度器:CFS 调度器(本质上是一个调度类,一套调度策略)。
内核调度器决定了何时、选取什么任务(进程)到 CPU 上执行,因此决定了混部场景中在线和离线任务的 CPU 运行时间,从而决定了 CPU 隔离效果。
Upstream kernel隔离效果
Linux 内核调度器默认提供了5个调度类,实际业务能用的基本上只有两种:
CFS 实时调度器(rt/deadline)
混部场景中,CPU 隔离的本质在于需要:
当在线任务需要运行时,尽力压制离线任务 当在线任务不运行时,离线任务利用空闲CPU运行
对于“压制”,基于 Upstream kernel (基于 CFS),有如下几种思路(方案):
优先级
可以降低离线任务的优先级,或提升在线任务的优先级实现。在不修改调度类的情况下(基于默认的 CFS),可以动态调整的优先级范围为:[-20, 20)
时间片的具体表现为单个调度周期内可分得的时间片,具体来说:
普通优先级0与最低优先级19之间的时间片分配权重比为:1024/15,约为:68:1 最高优先级-20与普通优先级0之间的时间片分配权重比为:88761/1024,约为:87:1 最高优先级-20和最低优先级19之间的时间片分配权重比为:88761/15,约为:5917:1
看起来压制比还比较高,加入通过设置离线任务的优先级为20,在线保持默认0(通常的做法),此时在线和离线的时间片分配权重为68:1。
假设单个调度周期长度为24ms(大部分系统的默认配置),看起来(粗略估算),单个调度周期中离线能分配到的时间片约为24ms/69=348us,可占用约1/69=1.4%的CPU。
实际的运行逻辑还有点差异:CFS 考虑吞吐,设置了单次运行的最小时间粒度保护(进程单次运行的最小时间):sched_min_granularity_ns,多数情况设置为10ms,意味着离线一旦发生抢占后,可以持续运行10ms的时间,也就意味着在线任务的调度延迟(RR切换延迟)可能达10ms。
Wakeup 时也有最小时间粒度保护(Wakeup 时,被抢占任务的最小运行时间保证):sched_wakeup_granularity_ns,多数情况设置为4ms。意味着离线一旦运行后,在线任务的 wakeup latency(另一种典型的调度延迟)也可能达4ms。
此外,调整优先级并不能优化抢占逻辑,具体来说,在实施抢占时(wakeup 和周期性),并不会参考优先级,并不会因为优先级不同,而实时不同的抢占策略(不会因为离线任务优先级低,而压制其抢占,减少抢占时机),因此有可能导致离线产生不必要的抢占,从而导致干扰。
Cgroup(CPU share)
Linux内核提供了 CPU Cgroup(对应于容器pod),可以通过设置 Cgroup 的 share 值来控制容器的优先级,也就是说可以通过调低离线 Cgroup 的 share 值来实现“压制"目的。对于 Cgroup v1 来说,Cgroup 的默认 share 值为1024,Cgruop v2 的默认 share(weight) 值为100(当然还可以调),如果设置离线 Cgroup 的 share/weight 值为1(最低值),那么,在CFS中,相应的时间片分配权重比分别为:1024:1和100:1,对应的CPU占用分别约为0.1%和1%。
实际运行逻辑仍然受限于 sched_min_granularity_ns 和 sched_wakeup_granularity_ns。逻辑跟优先级场景类似。
与优先级方案类似,抢占逻辑未根据 share 值优化,可能存在额外的干扰。
特殊 policy
CFS中还提供了一个特殊的调度 policy:SCHED_IDLE,专用于运行优先级极低的任务,看起来是专为”离线任务“设计的。SCHED_IDLE 类任务本质上是有一个权重为3的 CFS 任务,其与普通任务的时间片分配权重比为:1024:3,约为334:1,此时离线任务的 CPU 占用率约为0.3%。时间片分配如:
实际运行逻辑仍然受限于 sched_min_granularity_ns 和 sched_wakeup_granularity_ns。逻辑跟优先级场景类似。
CFS 中对于 SCHED_IDLE 任务做了特殊的抢占逻辑优化(压制 SCHED_IDLE 任务对其他任务的抢占,减少抢占时机),因此,从这个角度看,SCHED_IDLE 为”适配“(虽然 Upstream 本意并非如此)混部场景迈进了一小步。
此外,由于 SCHED_IDLE 是 per-task 的标记,并无 Cgroup 级别的 SCHED_IDLE 标记能力,而 CFS 调度时,需要先选 (task)group,然后再从 group 中选 task ,因此对于云原生场景(容器)混部来说,单纯使用 SCHED_IDLE 并不能发挥实际作用。
整体看,虽然 CFS 提供了优先级(share/SCHED_IDLE 原理上类似,本质都是优先级),并可根据优先级对低优先级任务进行一定程度的压制,但是,CFS 的核心设计在于”公平“,本质上无法做到对离线的”绝对压制“,即使设置”优先级“(权重)最低,离线任务仍能获得固定的时间片,而获得的时间片不是空闲的 CPU 时间片,而是从在线任务的时间片中抢到的。也就是说,CFS 的”公平设计“,决定了无法完全避免离线任务对在线的干扰,无法达到完美的隔离效果。
除此之外,通过(极限)降低离线任务的优先级(上述几种方案本质都是如此)的方式,本质上,还压缩了离线任务的优先级空间,换句话说,如果还想进一步在离线任务之间区分优先级(离线任务之间也可能有 QoS 区分,实际可能有这样的需求),那就无能为力了。
另,从底层实现的角度看,由于在线和离线均使用 CFS 调度类,实际运行时,在线和离线共用运行队列(rq),叠加计算 load,共用 load balance 机制,一方面,离线在做共用资源(比如运行队列)操作时需要做同步操作(加锁),而锁原语本身是不区分优先级的,不能排除离线干扰;另一方面,load balance 时也无法区分离线任务,对其做特殊处理(比如激进 balance 防止饥饿、提升 CPU 利用率等),对于离线任务的 balance 效果无法控制。
实时优先级
此时,你可能会想,如果需要绝对抢占(压制离线),为何不用实时调度类(RT/deadline)呢?实时调度类相比于 CFS,刚好达到”绝对压制“的效果。
确实如此。但是,这种思路下,只能将在线业务设置为实时,离线任务保持为 CFS,如此,在线能绝对抢占离线,同时如果担心离线被饿死的话,还有 rt_throttle 机制来保证离线不被饿死。
看起来”完美“,其实不然。这种做法的本质,会压缩在线任务的优先级空间和生存空间(与之前调低离线任务优先级的结果相反),结果是在线业务只能用实时调度类(尽管大部分在线业务并不满足实时类型的特征),再无法利用 CFS 的原生能力(比如公平调度、Cgroup 等,而这恰恰是在线任务的刚需)。
简单来看,问题在于:实时类型并不能满足在线任务自身运行的需要,本质上看在线业务自身并不是实时任务,如此强扭为实时后,会有比较严重的副作用,比如系统任务(OS 自带的任务,比如各种内核线程和系统服务)会出现饥饿等。
总结一下,对于实时优先级的方案:
认可实时类型对于 CFS 类型的”绝对压制“能力(这正是我们想要的) 但当前 Upstream kernel 实现中,只能将在线任务设置为比 CFS 优先级更高的实时类型,这是实际应用场景中无法接受的。
优先级反转
说到这,你心里可能还有一个巨大的问号:”绝对压制“后,会有优先级反转问题吧?怎么办?
答案是:的确存在优先级反转问题
解释下这种场景下的优先级反转的逻辑:如果在线任务和离线任务之间有共享资源(比如内核中的一些公共数据,如 /proc 文件系统之类),当离线任务因访问共享资源而拿到锁(抽象一下,不一定是锁)后,如果被”绝对压制“,一直无法运行,当在线任务也需要访问该共享资源,而等待相应的锁时,优先级反转出现,导致死锁(长时间阻塞也可能)。优先级反转是调度模型中需要考虑的一个经典问题。
粗略总结下优先级反转发生的条件:
在离线存在共享资源。 存在共享资源的并发访问,且使用了睡眠锁保护。 离线拿到锁后,被完全绝对压制,没有运行的机会。这句话可以这样理解:所有的 CPU 都被在线任务100%占用,导致离线没有任何运行机会。(理论上,只要有空闲 CPU,离线任务就可能通过 load balance 机制利用上)
在云原生混部场景中,对于优先级反转问题的处理方法(思路),取决于看待该问题的角度,我们从如下几个不同的角度来看,
优先级反转发生可能性有多大?这取决于实际的应用场景,理论上如果在线业务和离线业务之间不存在共享资源,其实就不会发生优先级反转。在云原生的场景中,大体上分两种情况:
(1)安全容器场景。此场景中,业务实际运行于”虚拟机“(抽象理解)中,而虚拟机自身保证了绝大部分资源的隔离性,这种场景中,基本可以避免发生优先级反转(如果确实存在,可以特事特办,单独处理)
(2)普通容器场景。此场景中,业务运行于容器中,存在一些共享资源,比如内核的公共资源,共享文件系统等。如前面分析,在存在共享资源的前提下,出现优先级反转的条件还是比较严苛的,其中最关键的条件是:所有 CPU 都被在线任务100%占用,这种情况在现实的场景中,是非常少见的,算是非常极端的场景,现实中可以单独处理这样的”极端场景“
因此,在(绝大部分)真实云原生场景中,我们可以认为,在调度器优化/hack 足够好的前提下,可以规避。
优先级反转如何处理?虽然优先级反转仅在极端场景出现,但如果一定要处理的话(Upstream 一定会考虑),该怎么处理?
(1)Upstream 的想法。原生 Linux kernel 的 CFS 实现中,为最低优先级(可以认为是 SCHED_IDLE )也保留了一定的权重,也就意味着,最低优先级任务也能得到一定的时间片,因此可以(基本)避免优先级反转问题。这也是社区一直的态度:通用,即使是极度极端的场景,也需要完美cover。这样的设计也恰恰是不能实现”绝对压制“的原因。从设计的角度看,这样的设计并无不妥,但对于云原生混部场景来说,这样的设计并不完美:并不感知离线的饥饿程度,也就是说,在离线并不饥饿的情况下,也可能对在线抢占,导致不必要的干扰。
(2)另一种想法。针对云原生场景的优化设计:感知离线的饥饿和出现优先级反转的可能性,但离线出现饥饿并可能导致优先级反转时(也就是迫不得已时),才进行抢占。如此一方面能避免不一样的抢占(干扰),同时还能避免优先级反转问题。达到(相对)完美的效果。当然,不得不承认,这样的设计不那么 Generic,不那么 Graceful,因此 Upstream 也基本不太可能接受。
超线程干扰
至此,还漏了另一个关键问题:超线程干扰。这也是混部场景的顽疾,业界也一直没有针对性的解决方案。
具体的问题是,由于同一个物理 CPU 上的超线程共享核心的硬件资源,比如 Cache 和计算单元。当在线任务和离线任务同时运行在一对超线程上时,相互之间会因为硬件资源争抢,而出现相互干扰的情况。而 CFS 在设计时也完全没有考虑这个问题
导致结果是,在混部场景中,在线业务的性能受损。实际测试使用 CPU 密集型 benchmark,因超线程导致的性能干扰可达40%+。
注:Intel 官方的数据:物理 core 性能差不多只能1.2倍左右的单核性能。
超线程干扰问题是混部场景中的关键问题,而 CFS 在最初设计时是(几乎)完全没有考虑过的,不能说是设计缺失,只能说是 CFS 并不是为混部场景而设计的,而是为更通用的、更宏观的场景而生。
Core scheduling
说到这,专业(搞内核调度)的同学可能又会冒出一个疑问:难道没听说过 Core scheduling 么,不能解决超线程干扰问题么?
听到这,不得不说这位同学确实很专业,Core Scheduling 是内核调度器模块 Maintainer Perter 在2019年提交的一个新 feature(基于更早之前的社区中曾提出的 coscheduling 概念),主要的目标在于解决(应该是 mitigation 或者是 workaround) L1TF 漏洞(由于超线程之间共享 cache 导致数据泄露),主要应用场景为:云主机场景中,避免不同的虚拟机进程运行于同一对超线程上,导致数据泄露。
其核心思想是:避免不同标记的进程运行于同一对超线程上。
现状是:Core scheduling patchset 在经过长达v10的版本的迭代,近2年的讨论和 improve/rework 之后,终于,就在最近(2021.4.22),Perter 发出了看似可能进入(何时能进入还不好说) master 的版本(还不太完整):
https://lkml.org/lkml/2021/4/22/501
关于这个话题,值得一个单独的深入的分享,不在这里展开,也请期待...
这里直接抛(个人)观点(轻拍):
Core scheduling 确实能用来解决超线程干扰问题。 Core scheduling 设计初衷是解决安全漏洞(L1TF),并非为混部超线程干扰而设计。由于需要保障安全,需要实现绝对隔离,需要复杂(开销大)的同步原语(比如 core 级别的 rq lock),重量级的 feature 实现,如 core 范围的 pick task,过重的 force idle。另外,还有配套的中断上下文的并发隔离等。 Core scheduling 的设计和实现太重、开销太大,开启后性能 regression 严重,并不能区分在线和离线。不太适合(云原生)混部场景。
本质还是:Core scheduling 亦非为云原生混部场景而设计。
结论
综合前面的分析,可以抽象的总结下当前现有的各种方案的优点和问题。
基于 CFS 中的优先级(share/SCHED_IDLE 类似)方案,优点:
通用。能力强,能全面hold住大部分的应用场景 能(基本)避免优先级反转问题
问题:
隔离效果不完美(没有绝对压制效果) 其他各种小毛病(不完美)
基于实时任务类型的方案,优点:
绝对压制,隔离效果完美 有机制避免优先级反转(rt_throttle)
问题:
不适用。在线任务不能(大部分情况)用实时任务类型。 有机制(rt_throttle)避免优先级反转,但开启后,隔离效果就不完美了。
基于 Core scheduling 解决超线程干扰隔离,优点:
完美超线程干扰隔离效果
问题:
设计太重,开销太大
结语
Upstream Linux kernel 为考虑通用性,设计的优雅,难以满足特定场景(云原生混部)中的极致需求,若想追求卓越和极致,还需要深度 Hack,而 TencentOS Server 一直在路上。(听着耳熟?确实以前也这么说过!)
关于 Linux kernel 的内核调度器的具体实现和代码分析(基于5.4内核(Tkernel4)),我们后续会陆续推出相应的解析系列文章,在探讨云原生场景的痛点的同时,结合相应的代码分析,以期降低 Linux 内核神秘感,探讨更广阔的 Hack 空间。敬请期待。
参考阅读
原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。