万字详解滴滴弹性云混部的落地历程
弹性云作为承载滴滴绝大部分业务的底层容器运行平台,已运行7余年,混部对于云来说并不是陌生的概念,业务上云的第一天就是运行在一个混部的环境中。
业务容器化上云,核心诉求是降本增效,其中降本主要通过混部来实现,增效主要通过云上高效的运维方式来实现。本文主要关注弹性云混部相关的部分,包括演进过程、核心技术能力、线上混部现状、以及未来规划等。
混部是指将不同的业务服务根据其相关特征,部署到相同的物理机/虚拟机上,以达到尽可能在保证重点业务服务质量的前提下,提升整个集群资源利用率,进而降低总成本。根据混部的类型,可以分为在线服务的混部和在离线服务混部两种。其中在线混部又可以分为公共集群在线业务之间的混部和隔离集群在线业务和存储服务的混部,在离线混部主要是在线业务与离线业务进行混部。
混部作为一种业界通用的降本的手段,充满着非常多的技术挑战,总结如下:
- 如何对业务进行合理的分级,不同级别的服务QoS如何定义
- 如何对业务进行精细化的画像,指导集群进行更合理的调度装箱,降低资源争抢的概率
- 单机如何进行内核层面的资源隔离策略,包括CPU、内存、IO、LLC cache、网络等资源,来保障高优业务的服务质量
- 单机如何进行性能干扰检测,指导单机驱逐和调度优化
弹性云混部详细介绍
总体架构
弹性云混部落地过程
阶段一:公共集群在线混部
时间追溯到2017年初,当时云计算、容器、Borg、Kubernetes、Mesos 各种新技术和产品风起云涌,一时风头无两。滴滴顺应业界潮流,加入了云计算大军,在公司内部推动业务上云的方向,助力业务降本增效。既然要推动业务上云,那么首先要回答什么是“云”,滴滴内部有没有“云”,当时业务都是运行在自己的物理机资源上,“云”对于滴滴来说就像它的名字一样虚无缥缈,所以作为公司的技术底座-基础平台部就责无旁贷的承担起了建立滴滴“云底座”的责任。
对于云来说,最底层的承载实体是一个一个的容器,当时容器技术,包括docker、container、cgroup 等技术相对成熟,各大公司都在使用,但对这些大规模容器集群调度和编排策略当时有多路线,比如 kubernetes,Mesos 等。滴滴内部也是选择两个技术路线同时演进,随着时间的推进,越来越多的公司加入了 kubernetes 阵营,kubernetes 成为容器调度和编排的事实标准,滴滴最终选择投入到 kubernetes 的怀抱!
随着顺风车、网约车、引擎、地图、中台、城运服、国际化等越来越多的业务接入弹性云,构成了弹性云混部的第一个雏形——在线业务混部。在越来越多业务接入到弹性云过程中,弹性云的部署密度越来越高,调度需求越来越多样化,这给整个弹性云带来了非常大的稳定性挑战,下面分别从容器运行时环境和集群调度两个方面进行展开介绍。
上弹性云之后,多个业务的容器同时部署在一台物理机,大家都处于一个“混部”的环境中,为了提高资源利用率,会通过超卖等技术提升容器的部署密度,意味着同一台物理机上会部署更多的容器,伴随而来的是越来越严重的资源争抢,业务延迟增加,以及更频繁的毛刺出现。所以面临的第一个重要的问题就是解决资源争抢的问题,客观来讲,在总资源一定的情况下,提升容器部署密度,带来资源的争抢是必然的,也是不可避免的,所以我们要解决的问题不是消除资源争抢,而且合理分配资源争抢,尽量保障重点服务的运行质量,下面来看一下具体怎么解决这些问题。
弹性云分级保障体系
弹性云分级保障体系
由于当前弹性云在线公共集群整体资源超卖非常严重,远超业务平均水平,建立完备的分级保障体系是进行良好混部的前提,当前分级体系的核心思路是从集群和单机两个维度提供资源的确定性,对不同优先级的服务提供不同的资源保障程度,简单总结如下:
- 根据服务的重要性和敏感性,对服务进行合理的分级,并制定相应的资源超卖规则。
- 单机层面资源(CPU、内存、磁盘IO、网络、Cache 等)分配优先保障高优服务的需求。
- 集群层面对不同级别服务提供资源保障:quota 管理和管控,k8s 分级调度能力。
k8s 调度能力支撑
k8s 调度流程图
上图是 k8s 调度的流程图,调度的核心工作是为一个新创建的 pod 选择一个最合适的 node 进行运行,整个调度流程分为两个阶段:预选策略(Predicates)和优选策略(Priorities),过程中进行各种算法策略的选择调优,且能够加入自己定制化的各种调度策略,下面介绍调度层面如何支撑上述混部场景。
1、调度预选策略增强
-
资源限制增强
-
大规格容器单机调度限制
-
IO敏感性容器调度适配
-
真实使用率调度限制
-
宿主资源争抢调度限制
-
集群拓扑打散增强
-
相同sts下tor打散策略
-
相同sts下node打散策略
-
机房内node平铺打散策略
-
定向调度策略
2、调度优选策略
- ActualBalancedResourceAllocation 策略:尽可能调度到实际资源使用均衡的宿主上
- BalancedResourceAllocation 策略:尽可能调度到资源使用均衡的宿主上
- ActualLeastResourceAllocation 策略:尽可能调度到实际资源使用最少的宿主上
- LeastResourceAllocation 策略:尽可能调度到资源使用最少的宿主上
- InterPodAffinityPriority 策略:尽可能调度到带有指定拓扑key的宿主上
- NodeAffinityPriority 策略:尽可能调度到满足指定node affinity的宿主上
- TaintTolerationPriority 策略:尽可能调度到设置了Pod可以容器的污点的宿主上
优选策略权重分配
3、重调度
由于 k8s 集群资源是动态变化的,比如集群扩缩容,机器置换;业务流量或部署模型变化也会导致其对资源使用情况随之变化,比如容器利用率创新高,并且调度时无法预知后续资源需求,所以 scheduler 在进行调度时,只能根据调度当时的集群资源及运行状态做出调度决策。除此之外,调度策略本身也可能会发生变化,如何将调度策略的变化应用于已完成的调度决策也是需要考虑的问题。因此,我们提供了重调度服务通过对集群定期巡检发现上述场景中不再合理的调度决策,触发调度器重新进行调度,从而使得集群整体的系统资源分配更合理。
重调度服务整体工作流程如下图所示,重调度服务通过定期巡检宿主/业务集群状态,根据各个重调度策略筛选出当前需重调度的宿主,再依据一定的策略筛选出待漂移容器,向调度器发起容器变更IP漂移请求。
通过弹性云分级保障体系,调度和重调度能力支撑,目前公共集群在线业务之间的混部已经比较成熟,集群高峰期CPU使用率保持在50%左右的安全水平,具体CPU使用率的情况如下图所示:
A机房CPU使用率图
B机房公共集群CPU使用率
C机房CPU使用率图
阶段二:公共集群在离线混部
在线集群峰值CPU使用率已经做到了50%,如果想进一步降低成本,是否还能从提升在线服务的部署密度的角度来提升CPU使用率呢?从技术方案、业界实践、以及收益效果等多方面看,这个思路不可行,具体表现在:
- 进一步提升在线部署密度意味着更大的资源争抢,在高峰期CPU使用率可能突破50%,到60%,甚至70%,这有非常大的稳定性隐患。大家知道,目前我们使用的物理机都是开启了超线程(HT),同一个 core 里面的两个超线程逻辑核其实是共享底层硬件资源的,所以理论上50%已是极限,如果进一步提升,资源争抢带来的各种问题就会增加,会明显影响业务的服务质量。
- 为了降本,滴滴当前在线服务的部署密度和超卖都较高,业界在线服务超卖率基本上会控制在一个相对较低的水平,这意味着在线服务本身CPU使用率不太高。
- 即使进一步提高部署密度,更多的也是提升CPU峰值利用率,仍然有长时间的低峰期CPU没有充分利用,这样得到的降本收益有限。
基于上面的分析,业界更通用的做法是将在线服务和离线服务进行混部,让离线任务充分利用在线低峰期的CPU算力,达到提升CPU平均利用率的效果,整体降本。如下图所示,如何能把其中阴影部分算力利用起来,就成了在离线混部要解决的核心问题。
在离线混部其实是在一个存在多条件约束的场景下寻找全局最优解,它要达到以下几个目标:
- 尽量提升在线集群的平均 CPU 使用率
- 尽量保障在线服务运行质量不受离线影响
- 兼顾一些离线运行质量的要求,不能无条件压制离线任务
为了实现上面这些目标,在离线混部核心要解决下面几个问题:
-
单机能力:
-
容器 QoS 保障:提供单机层面的资源隔离,保障在线服务的运行质量
-
干扰检查能力:通过干扰指标建设,实时感知离线任务对在线业务的影响,进行必要的动作,例如资源压制,驱逐等操作。
-
容器画像能力:基于宿主真实利用率,构建全混部场景下的调度画像能力,用于指导宿主机在不同时刻拥有的多种维度的混部资源。
-
k8s 混部调度能力:包括静态潮汐调度和动态调度。潮汐调度基于时间段,动态调度基于混部画像,将混部任务调度到符合条件的宿主上,在保障稳定性的情况下提升宿主利用率。
单机 QoS 和干扰检查能力
单机 QoS 保障主要是对 CPU、内存、磁盘 IO、网络、Cache 等共享资源在内核层面进行隔离,减少离线任务对在线任务的影响,但既然在离线都一起运行在一个共享的环境中,资源争抢只能减弱,不可能完全避免,所以需要建立各种资源层面的指标体系,感知干扰的发生,进而从单机和集群调度层面做一些处理。下图展示了单机层面在资源隔离方案,争抢指标建设,资源动调策略等方面所做的事情:
我们主要关注上图运行时的部分,这里分成机制和策略。机制是从内核层面提供的通用能,策略是在用户态利用这些能力根据不同的场景进行不用运用,这种设计也符合机制和策略分离的原则。资源隔离和干扰指标这块涉及到不同的资源和内核子系统,内容较多,我重点从 CPU 隔离策略的角度展开介绍。
总体来说,CPU 隔离策略有两种:cpuset(我们常说的大框绑核)和 cpushare(在离线共享 CPU 资源,通过精细化调度保障在线),下面谈一下我对这两种隔离策略的思考以及具体适合在什么场景上使用。
cpuset 的优点是该策略能实现两个实体之间在 CPU 层面的强隔离(LLC cache 还是共享的,这个需要通过其他手段进行隔离),能较好的保障在线服务的运行质量。但不足是配置不太灵活,且某些场合对在线服务不友好。所以该策略主要用于在离线混部场景,还有一些对延迟特别敏感的场景,例如 redis 混部等,目前我们大数据混部和某离线任务都是采用这个方案。
cpushare 的优点是该策略从内核 CPU 调度层面保障高优先级服务的资源,不需要用户态 agent 对资源进行调节,内核调度层面能保障毫秒级的 CPU 抢占,同时在线服务能使用所有 CPU,这样也能避免上面介绍的段时间产生大量线程的并发问题。cpushare 方案能更好的进行资源利用,进一步提升 CPU 使用率。但不足是需要内核进行开发,逻辑比较复杂,且涉及到内核核心代码,稳定性风险偏大,整个线上的落地周期比较长。
k8s混部调度能力
静态潮汐调度
弹性云混部当前基于在线业务的整体潮汐现象,通过潮汐时间段对外限制混部提供的离线算力。弹性云混部通过对离线集群设置潮汐高峰期,进而通过弹性API反馈给业务从而告知业务离线容器是否可以运行。例如,以hxy机房某离线业务混部为例,混部时间段如下:
- 低峰期(可运行2个离线容器):00:00-07:00 10:00-15:00 23:00:00-24:00:00
- 中峰期(可运行1个离线容器):15:00-17:00 20:00-23:00
- 高峰期(可运行0个离线容器):07:00-10:00 17:00-20:00
下图展示不同时间段可混部的离线容器情况:
绿线2023-07-02(周日) / 蓝线2023-07-03(周一)
潮汐调度策略简单,但会存在一些问题:
- 由于每台宿主每个时段的利用率情况并不相同,因此全局的潮汐策略使得我们一方面我们无法充分地利用宿主机的剩余资源,提供更多的算力给业务。
- 另一方面,静态调度是固定离线容器个数,而不是根据可混部的空间来调整可运行的离线容器数量,这会导致离线容器CPU使用量超过实际可混部的空间,带来一定的稳定性风险。
潮汐调度主要用在早期在离线混部的场景,现在线上都已转向动态调度方案。
动态调度
动态调度是相较于静态调度而言的,是指根据每台宿主的资源利用率及变化动态的调整每个宿主上可以调度的离线资源。相较于现有的静态调度限制,动态调度的优点包括:
- 可以充分利用每台宿主的剩余资源,最大限度挖掘在离线混部的价值。
- 可以从方案的层面上避免宿主出现热点等稳定性隐患。
动态调度的目标:
- 离线以宿主资源利用率为视角进行调度,不影响在线的quota和调度质量。
- 通过离线的动态调度将混部宿主利用率维持在稳定区间,提升资源利用率。
动态调度实现依赖下面将要介绍的容器画像,画像能预测出任何一个时间段某台物理机上可以混部的算力空间,从实现方式上看,有离线水平伸缩和离线垂直伸缩两种方式:
- 水平伸缩:根据宿主的在线利用率和画像数据,周期性通过离线pod的动态弹性伸缩来进行调度(调度宿主上离线容器的个数)。
- 垂直伸缩:每个宿主部署一个离线pod,根据宿主的在线利用率和画像数据,周期性通过调整离线pod的“规格”以将宿主的剩余资源充分利用。
从实现方式上来看,两者相比:
- 水平伸缩的方式相比垂直伸缩的主要优点是可以维持离线规格的确定性,维持现有的使用体验。但水平伸缩的主要问题为,因为当前离线pod与离线任务的生命周期不一致,频繁的扩缩可能会导致较高的杀死率,影响业务的运行效率。
- 从资源的利用上来看,垂直伸缩的效率更高,因为其可以不受容器规格的限制,避免产生较多的碎片。同时,这种方式下,无需调整workload和离线容器因此不会产生杀死率。
当前水平伸缩方案主要用在某离线任务混部场景,垂直伸缩方案主要用在大数据混部场景,当然,后面也能根据离线业务的不同需求进行调整。
容器画像能力
在动态调度方案中,离线可以使用的资源=混部目标利用率资源量-宿主在线服务已使用资源量。混部动态调度时,调度器会根据每个node上的离线可用资源来调度离线容器。由于宿主在线利用资源不断变化,离线可用资源也在不断变化,站在离线任务的角度,我们要保证离线任务执行期间离线可用资源都能满足资源需求,所以,画像需要给出未来一段时间内的离线可使用的资源。
通过预测算法来预测出未来1小时宿主在线服务利用资源最大值,这样就能得到目标混部利用率前提下可混部的资源量,如下图所示:
预测算法有7天同比算法和加权同比算法。
7天同比算法是指基于在线服务具有7天的周期性特征,使用7天前同比值作为预测值。由于误差相对较大,目前线上已经不再使用。
加权同比算法是由于7天同比算法在利用率整体水位升高或降低时的误差较大,在此基础上设计的一种改进算法,该算法综合考虑7天前历史值,1天前历史值和1小时前历史值,能明显提高预测的准确率。现在线上各机房都在使用加权同比算法,实际误差相比7天同比算法有明显降低。
线上混部现状
上面提到的单机隔离、干扰检测、容器画像和动态调度等能力在线上的混部场景已大规模应用,目前大数据混部和某离线任务混部已稳定运行了几年时间,下面是一些混部后的资源使用的情况,其中黄色线是通过离线混部后增加的CPU使用率,可以看到离线对CPU使用率的填谷效果非常有效。
某混部集群CPU使用率图
阶段三:隔离集群混部
前面介绍了弹性云公共集群的在混部和在离混部的情况,通过这两种场景的混部能极大的提高公共集群的 CPU 利用率,降低成本。但公共集群资源只占弹性云整体资源池的一部分,还有大量的隔离集群,且隔离集群的利用率普遍非常低,所以这一块就成了混部和降低的重点方向。
先介绍下隔离集群,公共集群是各种不同业务混在一起运行的公共资源池,但有些服务,比如存储的 redis、mq,还有接入层等服务对延迟非常敏感,公共集群环境无法满足它们对服务质量的要求,于是就专门隔离出一块资源池给某个服务单独使用,这样就能保障该服务的服务质量。但由于这些服务单独部署,资源使用率非常低,造成了资源和成本的浪费,下面是一些典型的隔离集群 CPU 使用率情况:
可以看出,隔离集群 CPU 使用率处于非常低的水平,存在大量的可混部空间。看到这里,可能有同学会问,既然有这么多混部空间,为啥不早点干呢?这里需要从隔离集群运行业务的特点说起,一般情况下隔离集群业务都是非常敏感,而且稳定性要求较高,比如 redis 服务,它对延迟非常敏感,对干扰几乎是零容忍,而且 redis 这种业界一般都是不进行混部,通过牺牲一部分成本来保运行质量和稳定性。Redis 资源占隔离集群资源的大头,对这块的混部我们一直在验证尝试,但始终保持比较谨慎的态度。
今年降本增效进入深水区,我们也开始将之前的各种技术积累和验证真正在隔离集群混部上落地。由于隔离集群业务的特性,我们将隔离集群混部拆解成多个阶段:
- 在线业务与存储业务混部:调度公共集群一些相对低优的在线服务到隔离集群和存储物理机集群,是峰值CPU使用率达到公共集群的水平。
- 全混部:进一步调度离线任务到已经进行混部的的隔离集群,进一步提升平均CPU使用率,最终达到无差别全混部。
当前我们正处在前一个阶段,这个阶段的核心目标是提升隔离集群的 CPU 峰值利用率,如下图,使用通过混部的在线业务将红框这部分资源利用起来。
从技术层面来说,隔离集群混部也会涉及到 k8s 调度,单机保障,还有稳定性兜底方案等方面,下面分别介绍。
k8s 调度支撑
在隔离集群混部场景下,k8s 调度的主要目标如下:
- 将混部的在线服务调度到隔离集群,整体根据利用率调度,保证利用率不超过设定的混部目标。
- 混部调度不能影响隔离集群原始业务的调度容量和质量,例如装箱率和原始打散策略等。
核心调度策略
-
真实利用率调度
-
混部侧根据混部目标利用率和画像计算每台 node 上“常驻混部”资源,并写入自定义资源 mix-mid-cpu
-
根据历史7天最大利用率在 pod 上注入此 pod 可能占用的资源
-
通过 mix-mid-cpu 等自定义资源进行调度限制
-
单机容器数量限制卡点解决方案
-
一些隔离集群,例如 redis 都会设置单机容器数量上线,由于混部容器的加入,这些限制可能会打破。调度侧可以根据不同的情况对混部服务绕过单机容器数量限制,或根据预测的混部容器数量今天单机容器数量限制的调整。
-
调度规则引擎策略注入
-
由于调度规则是通用的,正常情况下公共集群的服务无法调度到隔离集群,而且即使强行调度,也会存在非常多的通用卡点,这些卡点在隔离集群并不适用,需要进行一些适配,典型场景包括:容忍隔离集群的污点,对这些服务打通公共集群与隔离集群的通道;跳过一些公共集群默认卡点;不占用物理机真实使用率画像;设置混部相关的标签等。
重调度
由于隔离集群服务一般属于高优服务,混部在线服务后需要重调度进行基本的兜底。重调度需要对混部的隔离集群增加基本的热点处理能力,此处对重调度的需求:
- 对隔离集群服务维持原生重调度策略,做到混部对隔离集群透明。
- 对混部上来的服务需要基于根据CPU/内存/磁盘等利用率阈值(可配)进行重调度保障,进行必要的热点漂移。
单机服务质量保障
单机服务质量保障主要还是从内核资源隔离曾经进行的,总体来说,单机隔离基本还是在混部和在离混部那一套体系,由于当前隔离集群混部是混部在线服务,在 CPU 这块默认会使用 cpusare 机制,通过分级保障体系来保障服务质量,但对于 redis 这种特别敏感的服务,我们也采用了更为保守的 CPU 大框方案,并会保障 redis 实例不超卖的总体原则。
同时为了避免混部容器利用率突增导致整机 CPU 使用率突破混部目标,进而影响隔离集群原始服务的运行质量,在单机层面也引入了单机压制的能力,当检测到因混部容器导致物理机 CPU 使用率异常的时候,就会对混部容器进行压制,甚至驱逐,保障整体可控。
稳定性兜底保障
由于隔离集群敏感业务混部在滴滴是第一次尝试,很多方案都是在一步一步演进过程中,所以稳定性兜底方案就显得尤为重要。这里我重点介绍稳定性兜底方案-混部容器驱逐逻辑,整体驱逐流程如下图所示:
主要包括以下几部分:
-
驱逐触发条件
-
业务指标:如果隔离集群原生业务的业务指标出现异常,是一个重要的信号,当然并不是业务指标已出现问题就是混部导致的,这里我们也会有很多资源层面的指标来辅助判断。
-
混部水位:如果资源利用率已经超过了预设的混部水位,也需要进行容器的驱逐。
-
干扰检测:如果通过自定义的一些干扰指标发现存在明显混部容器产生的干扰,就需要将相应的容器进行驱逐。
-
人工强制触发:某些场景下需要进行强制驱逐,也需要提供对这种场景的支持。
-
驱逐核心逻辑管理
-
局部驱逐:这种情况下,不需要驱逐node上所有pod,需要准确找到最合适的驱逐对象,一般会考虑到下面这些因素,pod的优先级,pod利用率,pod的干扰指标等。
-
node驱逐:物理机上出现严重问题,需要尽快将某个node上的所有混部容器库快速驱逐。
-
服务驱逐:比如某个混部服务本身出现了问题,需要将这个服务的所有实例都驱逐到IDC或公有云。
-
驱逐目的地
-
混部集群:这种情况下混部容器被驱逐后还是会调度到混部集群的其他node上。
-
自建IDC公共集群:这种情况下混部容器被驱逐后会调度到IDC公共集群。
-
公有云:这种情况下混部容器被驱逐后会调度公有云上。
由于未来自建IDC公共集群的容量有限,并且公有云需要额外购买资源,存在成本增加,所以总体来说驱逐目的优先级是:混部集群>自建IDC公共集群>公有云,如果不是全局性问题,还是尽快在混部集群内部进行驱逐。
弹性云混部未来展望
随着未来稳态上云计划的推动,公共集群规模可能保持现状或适当的减少,未来各种隔离集群会是混部的重点算力来源,这里还是使用上面一张图来说明弹性云混部的未来展望。
在这张图中,每种混部实体都能找到自己的位置:
- total:表示物理机总资源量
- limit:表示可以提供给混部使用的资源量,limit与total之间是预留的稳定性buffer
- mid:这部分就是给混部的在线服务使用,他们主要是用来提升峰值CPU使用率
- Batch:这部分是给离线服务使用,他们主要用来提升均值CPU使用率
- Prod:这条红线是隔离集群服务本身的CPU使用率,由于服务特点,他们的使用率整体不高
这就是我们未来的全混部思路,由于更多种类的服务运行在一起,对技术能力提出了更大的挑战,未来会进一步增强集群调度、服务画像、单机隔离、干扰检测、异常感知等方面。