我们这次主要讨论公有云的服务器成本优化和服务器利用率优化。

郑韩

货拉拉  技术中心  核心基础设施部

架构师

  • 在货拉拉主导了Kubernetes从0到1落地全过程,致力于探索符合货拉拉特点的云原生之道。

今天的分享主要包含以下五个方面的内容:

一、货拉拉的基本情况介绍

二、基于K8S的成本优化手段

三、符合货拉拉特点的成本优化路线

四、竞价实例成本优化实践

五、定时扩缩容成本优化实践

一、货拉拉的基本情况介绍

在探讨成 本优化 之前 ,我们 先来 看看 货拉拉 的基 本情 况,让 大家 对货拉拉有一个初步的认识。

  • 货拉拉的生产环境是100%跑在公有云上的,所以我们的成本优化也是针对公有云。

  • 货拉拉是一间全球化的互联网公司,我们的业务遍布世界各地,在新加坡、印度、拉美和国内都有集群,所以我们必定会有多个集群跑在不同的云厂商上,这就决定了我们的所有方案都必须是通用的,不绑定任何云厂商的。

  • 货拉拉的流量比较规律,高峰低谷也比较明显,货运行业一般没有什么突发事件导致流量突增,不像微博这种服务,某个明星突然丢出一个瓜就引来一大波吃瓜群众,导致流量突增。同时,同城货运是个日出而作、日落而息的行业,所以白天高峰期比较稳定,晚上低峰期流量也会降到很低,这样的流量特点可以使我们通过简单的预测算法就达到较好的预测效果,同时低峰期的成本优化也将会是我们的优化重点。

  • 货拉拉也会有大量的大数据离线任务,大数据的离线任务占了我们公司大概一半的计算资源,并且高峰期正好是业务的低峰期,所以如何通过离在线的混合部署提高整体资源利用率也是我们的优化方向之一。

二、基于K8S的成本优化手段

讲完货拉拉的基本情况,我们再来看一下云原生时代下,一般有哪些成本优化手段,我把主流的成本优化手段分成四类:

  • 私有云的服务器成本优化

  • 公有云的服务器成本优化

  • 服务器利用率优化

  • 服务性能优化

由于货拉拉是100%跑在公有云上,所以我们本次不讨论私有云方面的优化。而服务性能优化是非常个性化的优化,所以也不在我们这次讨论范围内,我们这次主要讨论公有云的服务器成本优化和服务器利用率优化。

1、公有云的服务器成本优化

所谓公有云的服务器成本优化,其实就是研究如何在保证满足公司实际需求的前提下,以尽可能低的价格买到服务器实例。

公有云的服务器,一般有三种优惠模式:

  • 包年包月: 按固定周期购买一台服务器,这种模式稳定性高,价格实惠,但是弹性不足,特别是晚上想缩容根本缩不动。

  • 节省计划: 这种模式其实就是通过承诺每小时最低消费换取折扣,稳定性有保障,服务器也可以随意伸缩变换,但是每小时的总消费不能低于承诺消费,一般来讲公司都会按最低需求购买。

  • 竞价实例: 竞价实例就是云厂商将闲置的实例通过固定折扣或竞价的方式出售,折扣一般能到一折或两折,竞价实例有最便宜的价格以及不输按需实例的灵活性,但是不稳定,随时有被回收的风险,使用这种实例对公司的技术能力和服务质量都有较高要求。

2、服务器利用率优化

下面我们再来看看提升服务器利用率的一些常用优化手段。

1)合理request/limit

如何科学合理地配置request/limit是每个公司使用k8s都会遇到的问题,request调整太大节点利用率低,调整太小服务容易OOM或者被驱逐。一般来讲优化方式分两步,第一步是根据应用画像和过往负载设置一个初始值,第二步是建立巡检机制定期巡检,根据实际负载和策略动态调整request/limit配置。

2)HPA 

水平POD自动扩缩容,提前配置好指标和阈值,当指定指标超过阈值时自动扩容,低于阈值时自动缩容,比如CPU高于35%就扩容。HPA在应对突发流量时很不错,但是HPA的扩容有一定的滞后性,如果负载增长过快,可能会出现来不及扩容的情况,导致短时间内故障或雪崩。

3)CronHPA 

定时水平扩缩容,这种很容易理解,设置时间点和副本数,到扩容时间点就扩容,到缩容时间点就缩容,适用于可预期或计划内的场景。

4)智能调度 

智能调度是指根据公司实际需求增强调度器。k8s默认的调度器比较简单,但一般不能完全符合公司需求,并且有较大的优化空间,例如实际负载感知、权重计算增加更多维度、磁盘GPU、优化堆叠策略等,通过优化更符合公司实际情况的调度算法,可以有效地提升节点资源利用率。

5)在离线混合部署 

离在线混合部署是利用离线任务可以中断以及高峰期与在线服务相反的特点,充分利用服务器算力的一种方式。由于离线任务一般是在晚上运行,如果将离线任务和在线服务整合进一个k8s集群,那刚好可以弥补在线服务低峰期时的资源浪费,同时,如果离线任务可以支持随时中断,那通过自动避让以及资源隔离等手段,还可以实现在高峰期利用空闲资源运行离线任务,榨干服务器最后一滴性能,但是这项能力在大幅度提升服务器利用率的情况下,也带来了巨大的技术挑战。

三、符合货拉拉特点的成本优化路线

通过结合上面说的业界最佳实践和货拉拉的实际情况,我们探索出了以下这条货拉拉成本优化演进路线。

首先我们通过节省计划以相对优惠的价格保证了货拉拉的基础算力和稳定性,然后使用价格低廉且伸缩灵活的竞价实例来提供弹性伸缩的算力,当然我们也做好了准备面对竞价实例带来的稳定性挑战,到此基本上解决了服务器价格的优化问题。

接下来我们就需要解决服务器利用率的问题。

首先我们需要解决低峰期资源浪费的问题。刚刚讲过货拉拉的流量是很有规律的,所以我们通过自研CronHPA实现可预测和计划内的弹性伸缩,同时通过HPA实现流量突增时的紧急扩容。这是我们已经做完的内容。

我们目前正在做的是智能request,通过应用画像和过去的指标计算出合理的request和limit,期望通过这个功能提高高峰期的节点资源利用率。

当我们完成前面这几项后,我相信我们已经极大的提高了节点的资源利用率,剩下的就是走完最后一公里,通过智能调度和在离线混合部署榨干服务器的最后一滴性能,我们计划在2023年完成这些功能,之后再整体不断优化。

接下来我将重点介绍下我们已经做完的两点优化, 竞价实例  定时扩缩容。

四、竞价实例

1、什么是竞价实例

我们都知道云厂商提供的服务器实例都是他们的物理机房虚拟化出来的,那既然是物理机房,服务器就是相对固定的,能够提供的算力也是固定的,但是我们购买的实例数却是浮动的,所以云厂商总有些算力是闲置的,而闲置的服务器产生的经济价值就是0,所以云厂商就将这些闲置的算力拿出来打折或竞价销售,这些打折或竞价销售的实例就是竞价实例。

竞价实例的由来所有云厂都是一样的,但是各个云厂商对竞价实例的价格计算方式却存在差异,主要有两种方式:一种是 固定折扣 ,基本上都是打两折,这个很好理解;另外一种是 竞价 ,竞价比较难理解。下面我介绍一下竞价的基本过程。

2、竞价方式介绍

我们购买竞价实例时,会填写一个可接受的最高价格,然后云厂商会根据所有人的报价和库存情况计算出一个价格,出价高于或等于这个价格的人就能以这个价格买到实例,低于价格则购买失败。上图有个细节不知道大家有没有发现,整幅图只有定价这里我是用了黑色,那是因为这个价格到底是怎么计算出来的完全是个黑盒,其中的算法只有云厂商自己知道,我们只能知道他最后得出来一个价格,这个价格最低可以低到1折,最高可以等于没打折。

3、竞价实例原理

下面我讲讲使用竞价实例的基本原理。

首先我们带着机器型号和一个能接受的最高价向云厂商提出购买申请,云厂商如果判断指定型号有库存并且价格在我们的报价之下,则给我们分配实例,购买完成,这时我们就可以正常使用了。

在我们正常使用该竞价实例的过程中,云厂商会一直监控库存和价格变化,当发现价格实时价格高于我们的报价或库存不足时,将向我们发送中断信号,收到信号后我们需要做一些措施保证该实例被回收不影响业务,在我们收到中断信号的几分钟后,实例就会被回收。

不同云厂商在回收机制上有些不同。aws会有预测算法,在预测到库存不足时提前通知你,让你有更多反应时间;阿里云有一小时保护机制,竞价实例运行的第一个小时能保证不被回收。但是都不会等我们做完我们想做的事再回收,无一例外。比如我们想再买台机器将服务迁移过去,但是可能还没迁移就被回收了,所以我们应对突然中断的手段一定要足够快,才能避免对业务产生影响。

4、竞价实例特点

介绍完竞价实例的基本原理,我们再总结一下竞价实例的特点:

  • 性价比高。 和按需实例对比,竞价实例通常仅是其价格的10-20%。和预留实例对比,竞价实例通常仅是其价格的30-60%。

  • 竞价实例是将闲置资源打折出售。 当没有闲置资源时,也就无法购买了,不是你什么时候想买云厂商都有货。

  • 随时中断。 云厂商会动态检测当前的市场价格和库存,一旦库存不足,或者你的出价小于市场价格,云供应商可以在任何时候回收这些实例。

性价比高是我们使用竞价实例的目标,库存没保障和随时中断是我们需要解决的问题。

5、竞价实例结合K8S 落地

下面我将介绍竞价实例如何结合K8S落地,以及如何解决竞价实例带来的稳定性问题。

首先从图中我们可以看到,我将k8s的节点分成了多个节点组,然后通过cluster autoscaler进行弹性伸缩。节点组中有按需实例的节点组,也有竞价实例的节点组,这是因为并不是所有服务都适合部署在竞价实例上,我们需要一些更稳定性的节点部署跑不适合跑在竞价实例上的服务,同时我们也需要在竞价实例库存不足时有按需实例的节点组提供足够的算力支持业务正常运行。

然后我们再来看中断回收部分。当竞价实例需要中断时,云厂商会发送一个中断信号,如果是托管集群,云厂商会自己处理这个中断信号,帮我们起一个新的节点并回收旧节点,但如果是自建集群,我们则需要自己实现一个中断信号处理服务,在该服务中处理中断问题。

在货拉拉虽然我们是托管集群,但是我们还是自己开发了一个notify handler,该服务主要是为了收集中断信号,用于监控中断频率以便后续用于告警和触发应急预案。

整体架构还是比较简单的,但是我们在实际使用过程中还是发现有不少地方需要优化,下面我就介绍下几个主要的优化点。

6、竞价实例优化点

1)增加竞价实例的型号

上面我们讲过竞价实例的库存没有保障,所以我们能做的就是扩大这个池子,池子越多,库存不足的概率就越低,所以我们创建的节点组需要覆盖更多的可用区和更多的实例型号。但是需要注意的是,受到cluster autoscaler的限制,同一个节点组里的节点cpu和内存必须一致。

2)设置节点组优先级

目前我们的集群中有按需实例节点组和竞价实例节点组,我们希望资源不足时优先弹出竞价实例,只有当竞价实例无法弹出时才弹出按需实例,这样可以确保最大化的利用竞价实例,同时又能确保竞价实例库存不足时及时弹出按需实例保证业务稳定运行。

这里用到的是cluster autoscaler的优先级配置,我们在cluster autoscaler的一个configmap中配置节点组的优先级,将竞价实例的优先级设置成20,其他节点组是10,这样ca就会优先弹出竞价实例,弹不出竞价实例时弹出按需实例。

3)设置pod亲和性

由于竞价实例随时可能被中断,且一旦中断很有可能是同一个型号的节点同时被中断,所以我们要避免把鸡蛋放在一个篮子里,尽量把同一个服务的pod分散在不同实例、不同型号甚至是不同的可用区中,避免某一个服务所有的pod被同时驱逐,造成服务不可用。

这里用到的就是pod的亲和性配置。从配置上我们可以看到,我们给了可用区最大的权重,其次是实例型号,最后是实例名,这样k8s会尽量将同一个app的pod分散到不同的可用区,如果没有合适的节点则分散到不同的实例类型,依然没有合适的再分散到不同的实例,这样可以最大化地打散同一个服务的pod,避免由于竞价实例中断导致服务不可用。

4)配置PDB

尽管我们之前已经做了很多措施,避免同一个服务的pod在同一时间由于实例中断被驱逐,但是没有哪一项是能100%避免这种情况的,所以我们需要再上一层保险,这层保险就是PDB,通过PDB我们可以设置同一个服务的pod必须保证同一时间有多少副本是可用的。像下面这张图的配置里面就保证了服务70%的副本是正常的,这样就算遇到需要同时驱逐的情况,k8s也会在强制保证至少有70%的副本可用的情况下滚动驱逐,确保整个过程服务都是可用的状态。

但是需要注意的是,这个配置会导致本来可以并发的驱逐变成串行,这会影响到排空节点的时间,所以这个需要服务的启动速度足够快,在节点被真正回收前执行完整个迁移过程。

5)利用低优先级的pause pod给集群预留空间

上面我们讲过,竞价实例从收到中断信号到真的被回收就那么两三分钟时间,时间一到不管我们是什么情况实例都会被回收,所以我们必须保证在实例被回收前完成整个迁移动作,那这两三分钟的每一秒是很珍贵的,我们必须想方设法提高迁移的效率,但是创建一个新的节点少则几十秒,多则一两分钟,等新节点准备好其实已经浪费了很多时间,所以我们需要想办法在收到中断信号的时候就直接将旧节点排空,但是要排空节点就需要保证集群随时有足够的空间运行被驱逐的pod。而保证有足够空间的做法就是我们现在讲的这个利用低优先级的pause pod给集群预留空间。

k8s里面的pod有一个优先级的概念,高优先级的pod可以在资源不足时抢占低优先级的pod。从下面这张图我们可以看到,我们在Node1和Node2都放置了一些低优先级的Pod,当Node1被中断回收后,高优先级的pod会直接抢占低优先级的pod从而实现快速启动,而不需要等新的节点准备就绪,而低优先级的pod则会全部变成pending状态触发扩容或等待新的节点ready后启动,从而重新创建了一块预留空间。

下面是具体的yaml,这里面优先级不必非得是-1,只要保证比正常pod的优先级小即可,低优先级pod主要是要把PriorityClassName填对,然后根据实际情况设置副本数和request即可。

7、不适宜部署在竞价实例的服务

  • 单副本服务。 这个不用解释大家都明白。

  • 启动时间过长的服务。 因为需要保证服务能在实例被回收之前启动完成,所以服务的启动时间不能太长。

  • 无法容忍任何非优雅停止的服务。 我们需要认识到不管我们做了多少措施,都不能完全杜绝实例在没有排空之前就被回收,所以对于不能优雅停止无法容忍的服务也不适合部署在竞价实例上。

  • 有状态的服务。 有状态的服务迁移起来远没有无状态的服务灵活,为避免出现各种各样的问题,也不建议有状态的服务部署在竞价实例上。

五、定时扩缩容

1、背景

弹性伸缩的本质是为了提高服务器的利用率,那我们来看看货拉拉一个没有任何优化的集群的CPU利用率是怎么样的。

从这个图我们可以看到,这个集群在白天最高峰时的CPU利用率是35%,半夜低峰期的CPU利用率却只有2.5%,2点的时候还有一个小高峰,是因为我们在这个时候有大量定时任务在执行,可以先不用考虑。目前这个利用率如果是放在虚拟机时代,还算是一个比较能接受的利用率,但是放在云原生时代,我们还是有很多手段可以提升这个利用率。

我们的优化目标是希望把高峰期节点CPU平均利用率达到50%,低峰期节点CPU平均利用率达到30%。

2、默认HPA的不足

按照正常做法,我们把HPA套上去应该就可以解决这些问题,随着流量上升自动扩容,流量下降后自动缩容,事实上我们也这样做过,但是很快就发现了问题。

第一个问题是 扩容有一定滞后。 HPA需要等负载上来触发阈值后才开始扩容,而负载升得太快可能会导致扩容不及时,我们每天早上9点和下午2点都有一波很迅速的流量上升,这时HPA会大量扩容,但是大量扩容就需要先大量扩节点,导致这两个时间点扩容总是太慢,每天这两个时间点都会有些报错,然后触发告警。

第二个问题是 扩容阈值受到限制 。为了白天流量上升时扩容尽量稳定,我们不能把最小副本数设置得太低,这就导致我们晚上的CPU利用率上不去。而为了扩容及时,我们也不能把CPU的阈值设置得太高,因为越高就意味扩容时反应约迟钝,而设置得太低又导致高峰期CPU利用率上不去,例如我们在HPA把CPU的阈值设置为35%,那我们的pod的CPU使用率基本上都不能超过35%,因为超过就会扩容,然后把CPU利用率拉下来。

第三个问题是 扩缩容时机无法控制。 HPA是完全根据配置的指标和阈值来扩容,无法根据企业特点定制策略,例如货拉拉的流量白天还是比较稳定的,我们在白天高峰期时就算浪费点资源也不希望出现过多扩缩容影响稳定性,但是HPA并没有提供这样的配置。

3、CronHPA+HPA

结合我们对HPA的实践经验和货拉拉的业务特点,我们总结探索出了符合货拉拉特点的水平弹性伸缩方式,那就是自研的CronHPA+HPA组合。

CronHPA是根据设定好的时间调整对应HPA 的最小副本数,实现可预期或计划内的弹性伸缩。

考虑到货拉拉流量规律的特点,我们在自研的CronHPA中通过定时+过往指标分析+简单预测算法可以做到比较高质量的预扩容以及定时缩容。例如早上9点流量开始快速上升,我们可以在7点到8点就提前把集群扩容到高峰期的水平,晚上10点后流量已经下降了很多,我们可以放心地缩容,并且有了预扩容打底,我们晚上可以把副本数缩到一个极低的水平而不用担心白天来不及扩容。

CronHPA没有直接操作deployment的副本数,而是操作HPA的最低副本数,是因为尽管CronHPA可以覆盖我们99%的扩缩容需求,但是仍然需要HPA的自动扩缩容来解决1%偶发流量突增的问题,尽管我们很少遇到流量突增的情况,但是有了HPA可以让我们在缩容或者设置高峰期副本数时不必为偶发的流量突增添加额外的buff,从而提升整体的资源利用率。

4、架构

讲完定时扩缩容的原理,我们再来讲讲定时扩缩容的架构。

我们在K8S部署了一个自研的hll-cronhpa-controller,并且添加了一个CronHPA的CRD,用户主要是设置CronHPA的CRD来设置弹性伸缩,并不直接设置HPA和deployment副本数,hll-cronhpa-controller会监听CronHPA CRD对象的变化,当发现有新增或者更新时,就会同步修改HPA对象,其中除了最小副本数其他都是直接透传,CronHPA的所有业务逻辑最终都会体现在HPA最小副本数的变化上。

而CronHPA的业务逻辑主要是hll-cronhpa-controller根据cronHPA对象的配置,从Prometheus拉取过往指标分析,同时结合从应用管理平台拉取的应用画像以及从配置中心拉取的扩缩容策略综合分析后在合适的时间点为对应的HPA对象设置一个合适的最小副本数,这么说可能有点抽象。

举个例子,假设我目前有个服务设置了晚上需要缩容,我们通过分析该服务过往夜间资源利用率得出晚上可以缩到剩下2个副本,同时我们从应用画像中查到该服务是核心服务,而配置中心的扩缩容策略是核心服务最低不能少于5个副本,那综合分析我们夜间需要给他设置的副本数就是5。

5、实现路径

下面讲一下我们的实现路径。我们最开始自研的时候就确定了cronHPA+HPA的架构。

第一阶段是纯手工配置的,我们通过分析流量设置了一个全局缩容时间点和全局扩容时间点,然后各个服务根据经验人工估算出一个缩容比例。这个阶段由于全局是在同一个时间点同时扩缩容,在扩容瞬间对基础设施的压力比较大,同时人工估算出来的缩容比例还是过于保守。

第二阶段是根据过往指标分析自动设置扩缩容的时机,这样可以有效分散扩缩容压力,同时让扩缩容时机更为合理。例如有些服务可能晚上6点就没流量了,阶段一还需要等到晚上10点才缩容。

第三阶段是根据appid自动计算缩容比例。人工估算缩容比例还是过于保守,例如高峰期100个pod,晚上其实可能只需要10个,但是一般人工评估只会缩到50个,但是通过算法自动计算就会相对科学很多,同时自动计算还可以通过分析过往指标不断修正,例如某个pod经过计算晚上缩容到2个pod,但是通过分析过往指标发现过去7天晚上都会通过HPA扩容到3个pod,那第八天就把缩容副本数自动调整到3个。

第四阶段是自动识别低峰期。原来手工设置低峰期时间段,程序在从这个时间段里选择扩缩容时机,到了这一阶段,我们可以自动识别每个服务自己的业务低峰期,做到个性化缩容。

第五阶段是自动分阶段扩缩容,前几个阶段都是确定一个扩缩容时间点,直接把副本数扩缩到目标副本数,但流量其实是逐渐变化的,通过这个阶段我们可以实现服务在高峰期来临前随着流量上升逐步把副本数提上去,而在低峰期,随时流量降低逐步降低副本数,这个阶段与HPA最大的不同在于,HPA是基于指标和阈值,而cronHPA是基于预测。

6、未来规划

最后讲一下我们对于弹性伸缩的未来规划,我们希望未来弹性伸缩可以形成这样一个机制,服务初始化时可以通过应用画像自动设置reqeust/limit和副本数,然后自动在高低峰通过过往指标分析和算法实现预扩容和及时缩容。

如果遇到突发流量可以通过原地升配实现快速纵向扩容,原地升配对比VPA最大的区别在于不用重启pod,所以可以做到秒级扩容。如果所在节点资源不足则通过HPA横向扩容,同时根据指标分析不断修正服务的reqeust/limit和副本数,实现全自动的弹性伸缩闭环。