[降本提效]如何可靠的大规模使用AWS竞价实例
作者简介:陈宗舒。先后从事过通讯软件、分布式系统、云计算等开发,在2016年之后一直从事容器相关工作。目前在货拉拉负责国内容器化相关架构和海外容器架构优化
1、前言
什么? 虚拟机只要1折起?我不允许我的小伙伴还不知道“竞价实例”这种类型的虚拟机!
现在互联网行业已经慢慢从“跑马圈地”粗放的爆发式增长,开始过渡到精细化运营的阶段。所以成本,更加成为了一个被关心的问题
降低云资源的成本,技术上无外乎2种方式: 用更少的资源干相同的事情或用相同的资源干更多的事情。于是竞价实例这种类型的虚拟机资源开始有更多公司开始尝试使用
在AWS,竞价实例就称为Spot
2、Spot竞价实例简介
• 一个AWS的老数据:2016年平均每周的EC2 Spot Instance的资源使用量大于2012一整年全区域所有EC2的使用量
• 携程目前在AWS的海外业务90%在Spot上
• 汇量科技70%使用Spot,每年节约140w$
• AutoDesk使用Spot节约50%
2.1、竞价实例
所谓的竞价实例,就是一种低价的抢占式ECS实例,我们出一个价格,云厂商把没卖出去的闲置资源便宜卖给我们,一但按需实例的需求量上来了,或者别人出价更高,可以回收我们的资源
对于云厂商来说,区域机房的总容量是有上限的,资源保障的优先级为 预留实例 > 按需实例 > 竞价实例
•预留实例:客户先付钱,预定需要多少资源,可以走批发价格,即使没使用那么多量也要付钱
•按需实例:字面意思,你有需要的时候就买
•竞价实例:客户竞价的抢占式实例,可能随时被回收
申请资源的时候,云厂商不保证一定能申请到某种类型的ECS实例,即使你申请到了,也不保证你能使用多久,可能随时被回收,这对任务的调度和整体服务的可用性带来很大的挑战。
竞价实例有四个特点:
1.便宜是真便宜。成本可以是按需的10%-30%
2.不是人人都能用好。
因为竞价实例随时会被回收,所以使用姿势要正确,具体见2.3.2一小节
1.不是你想要啥就有,不是你想用的时候就能用。
竞价实例的某种类型,在需要购买的时候,云厂商不保证你能买到
1.或迟或早,最终一定会被抢走。按照我们的实际体验,即使是不常用的机型,过几周也会发生回收事件
2.2、Spot发展史
Spot实例是AWS的竞价实例的叫法。竞价实例,各家云厂商原来有不同的叫法
Plain Text
AWS: EC2 Spot Instances
Google Cloud:Preemptible VMs
阿里云: 竞价实例
Azure: LowPriority VMs
腾讯云: 竞价实例
华为云: 竞价计费型实例
但是慢慢也都接受了Spot的叫法(按需实例AWS叫On-Demand实例)
AWS的Spot实例历史已经超过10年了,中间计费模式也调整过几次,以便让Spot回收更加平滑,现在的计费方式是每隔1小时重新定价一次。
2.3、怎么用好Spot?
2.3.1、Spot和On-Demand对比
上面有说OD(On-Demand)是按需实例,Spot和On-Demand对比如下:
指标 | Spot实例 | On-Demand实例 |
优点 | 便宜 | 稳定 |
启动 | 需要指定类型来购买,容量够可以立刻启动 | 默认的类型 |
可用容量 | 对特定机型,容量/水位不定,可能会申请不到 | 正常情况能申请到 |
每小时价格 | 因供需而异。 | 保持不变。 |
中断告警 | 当实例处于高中断风险时发送警告信号。 | 无,主动终止或休眠实例。 |
中断场景 | 当容量不再可用、价格超过预算的最大费率或对 Spot实例的需求增加时,可由 AWS EC2 中断。 | 在主动终止或休眠实例之前保持不间断。 |
2.3.2、使用姿势
首先说明的就是,Spot和k8s/容器是最佳搭档。因为Spot实例会经常回收,如果你手动操作资源调度,任务调度,那就几乎是不可能完成的任务了,这么短基本没法完成任务关闭/转移/部署到新资源上这一系列动作。如果没有正确地处理业务的关闭和退出,则有可能造成数据的丢失。
当然现在也有很多第三方供应商,提供了EC2上任务的调度功能,但是基本上都要接管你的AWS账号,而且传统应用可能也要做改造。
这个时候,k8s的自动调度功能,和Spot就显得非常契合了。
Spot使用的通用最佳姿势
1.混合使用Spot、按需实例和预留实例。
在按需实例中保存关键数据,或者运行数据库等关键服务
2.避免在Spot上运行不能中断的任务,而运行对错误容忍度高和使用灵活的应用
比如大数据,容器化的工作任务,高性能计算HPC,无状态的web服务器,渲染、CI/CD和其他测试和开发工作负载。
3.把需要比较长时间的大型工作任务拆分成大量小的、异步的短时间工作任务
减少被中断的可能性
4. 充分利用Spot的价格浮动特性
在适当的时间购买可被抢占实例,降低计算成本,并在整体成本下降的前提下,提升业务在该时间周期内的吞吐量。比如在晚上或周末这种非高峰时段运行大型Spot集群。
5.支持断点续算的智能调度模式
6.合理使用云厂商提供的工具
AWS的Spot Instance Advisor 可以帮用户确定中断可能性最低的池,提供与按需费率相比可节省的成本信息。在选择实例时,用户可以权衡应用程序对中断的容错能力和自身的成本节省目标。中断率越低,Spot 实例的运行时间可能就越长。
3、Spot机制讲解
Spot的特殊之处,就在于可能随时被回收,所以搞清楚Spot的回收机制和流程,对于用好Spot的是非常必要的,这一节就来介绍下Spot的回收机制、信号和流程
Spot回收流程,后面的具体判断机制是黑盒,咨询过AWS的SA也没有得到具体的答案。只有大致条件
Spot实例中断的可能原因:
•价格 – 目前Spot价格高于我们以前购买的价格
•容量 – 如果没有足够的未使用的EC2实例来满足Spot实例的需求,Amazon EC2会中断Spot实例。 Amazon EC2确定实例中断的顺序。
•约束 – 如果您的请求包含约束,例如启动组或可用区组,那么,当不再满足约束条件时,这些Spot实例将作为一个组终止。
Spot中断时候,会通过信号通知节点,然后托管节点组会自己处理,下面讲讲关键信号和流程
3.1、关键信号
这里主要托管节点组的Capacity Rebalancer讲一下Spot在回收时期的两个主要信号和生命周期
•RBR:Rebalance Recommendation
当Spot 实例处于中断风险较高时发送的信号,一般会提前10分钟发出,如果RBR信号处理打开,则这时候会开始替换风险高的节点,但是RBR信号不保证一定发出
•ITN:Instance Termination Notification
Spot 实例中断信号,一般会提前2分钟发出,收到此信号,会立马驱逐所有Pod,然后2分钟之后关闭Spot实例
3.2、节点回收流程
简单总结整个流程就是:
AWS回收节点 -> 节点组收到RBR信号 -> 新建节点 -> 驱逐将要关闭节点的Pod -> 关闭节点
加上ITN信号的整体流程如下图
•AWS根据自己的模型预测,准备回收某种风险高类型的Spot节点,然后提前10min发出RBR信号
•托管节点组收到RBR信号之后,「1.1」步会先启动1个新的节点
•如果「1.1」成功,然后会执行「1.2」驱逐回收节点上的Pod
•「1.2」驱逐成功之后,会「1.3」回收节点
•如果RBR信号丢失或者上面RBR处理流程中某些步骤失败,确实要回收节点,AWS会提前2min发出ITN信号
•如果托管节点组收到ITN信号,会执行「2.1」驱逐Pod操作
•如果「2.1」在2分钟之内完成,开始「2.2」执行回收节点
•如果「2.1」驱逐动作发生2分钟之后没驱逐Pod完成,就会强制回收节点
4、货拉拉海外 Spot大规模落地实践
先分享一个数据
货拉拉海外的测试环境90%使用SPOT,每月节省ECS成本57.38%;
预发布环境90%使用SPOT,每月节省ECS成本62.6%;
4.1 、挑战
SPOT优势是价格,带来的挑战主要来自两方面
•来自AWS Spot特性:
○资源层面:SPOT本身回收的频率和在新节点扩容的时候,是否有足够的资源分配到新节点
○事件处理层面:AWS在回收资源时候,信号的不确定性质。RBR不一定能发出,但是ITN信号只会提前2分钟,这个时间服务能否完全优雅退出,流量摘干净,这些都是需要注意的问题
•应用韧性的挑战:
○应用是否能优雅关闭
○应用在副本减少的情况下,能否正常对外提供服务
○应用启动时间不能太长
4.3、系统架构
整体架构如下
•针对应用可用性方面:
○在部署层面,创建专门的Spot节点组,然后把符合准入规则的应用通过策略调度到Spot节点组,Spot节点组不允许非Spot应用调度到该节点组里面,防止一些系统应用或者核心应用被频繁回收导致系统异常
○为保证节点基础依赖可用性,自研“node-status Controller”,保证Spot节点上依赖的基础daemonset正常了,才允许该节点被调度
○为了解决在Spot节点重启过程中Pod的堆叠,提高应用高可用性,使用“Rescheduler Controller”在低峰期二次打散应用的Pod
•针对Spot资源特性的挑战:
○在可观测性层面,开发了一个“notify handler”去收集并展示Spot生命周期的各种数据,比如覆盖率,各种机型的RBR信号,服务的Pod打散情况,Pending时间等等,为下一步决策提供数据。比如如果某个AZ的某种机型回收率过高,可能把这种机型从Spot节点组里面去掉
○在集群可用性方面,使用OD 节点组来兜底,如果遇到Spot节点扩容不出来的时候,最后扩容OD节点,来保证应用不因为资源问题长时间处于Pending状态。
下面将对具体规则和落地策略做一一详细阐述
4.3、准入规则
根据Spot的特点和容器应用的一些特性,我们定制了货拉拉 Spot准入的一些准入门槛,来保证服务在迁移到Spot的时候,不会有太大问题
•系统服务比如CoreDNS等、
•有状态服务
•副本数大于1
•能够在2分钟内优雅关闭(指标确定)
•启动时间在2分钟内
•能够http检测自身健康状态
•支持优雅缩容(缩容时不会报5XX错误)
4.4、具体策略
落地方案围绕这两方面:
1. 减少Spot中断概率,以及减少Spot回收对应用的影响;
2. 应用可用韧性设计
根据实际Spot落地需要考虑到问题,来设置对应的策略(左侧是问题,右侧绿色是对应策略)
4.4.1 节点组设计
节点组多个AZ
因为云厂商在每个AZ的热度和使用率不同,如果节点组跨AZ设计,ASG会自动选择资源最丰富的 AZ,这样就降低了Spot回收的几率
配置多个节点组、多个机型
可以选择多种机型,这样就如果当某种类型资源不足,申请不到新节点的时候,会选择其他节点组,不会最后扩不出新节点
而且ASG会自动给我们选择当前水位最低的机型来创建实例,可以降低回收率
自研组件收集Spot回收信息
我们自己开发了一个组件从AWS的信息中心eventBridge去接收RBR和ITN信号,并上传到Prometheus里面,这样我们可以从监控面板感知到Spot的某些AZ和类型回收率,可以调整节点组配置
4.4.2 CA及节点扩容设计
CA配置,使用OD节点组来兜底
创建一个configmap,名字要固定,叫 cluster-autoscaler-priority-expander ,调整了CA扩容的节点组权重,优先扩容Spot节点组,但是当Spot节点组都不能扩容的时候,会扩容OD节点组
YAML
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority-expander
namespace: kube-system
data:
priorities: |-
20:
- spot-.*
10:
- .*
修改ca的deployment的启动配置,在启动参数增加最后2项,减小了CA扩容节点组轮询的等待时间
SQL
kubectl -n kube-system edit deploy cluster-autoscaler
spec:
containers:
- command:
- ./cluster-autoscaler
- --v=4
- --stderrthreshold=info
- --cloud-provider=aws
- --skip-nodes-with-local-storage=false
- --expander=least-waste
- --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/Cluster-PRD01
- --balance-similar-node-groups
- --skip-nodes-with-system-pods=false
- --expander=priority
- --max-node-provision-time=5m0s
低优先级的占位应用提前去扩容新节点
正常如果收到RBR信号,会先扩容,但是有可能RBR信号会丢失或者可以手动关闭节点组对RBR信号的处理,如果真正要回收的时候,AWS会直接发ITN信号。收到ITN信号,ASG直接驱逐节点上面的Pod,这个时候可能资源不足于容纳该节点的所有Pod,会使得Pod长时间处于Pending,等待CA扩容
这个时候可以用一个低优先级占位Pause容器来提前扩容,真正的业务容器被驱逐的时候,可以踢到该占位容器
YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
namespace: default
spec:
replicas: 1
selector:
matchLabels:
run: overprovisioning
template:
metadata:
labels:
run: overprovisioning
spec:
priorityClassName: overprovisioning
containers:
- name: reserve-resources
image: k8s.gcr.io/pause
resources:
requests:
cpu: "200m"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning-autoscaler
namespace: default
labels:
app: overprovisioning-autoscaler
spec:
selector:
matchLabels:
app: overprovisioning-autoscaler
replicas: 1
template:
metadata:
labels:
app: overprovisioning-autoscaler
spec:
containers:
- image: k8s.gcr.io/cluster-proportional-autoscaler-amd64:1.1.2
name: autoscaler
command:
- ./cluster-proportional-autoscaler
- --namespace=default
- --configmap=overprovisioning-autoscaler
- --default-params={"linear":{"coresPerReplica":1}}
- --target=deployment/overprovisioning
- --logtostderr=true
- --v=2
serviceAccountName: cluster-proportional-autoscaler-service-account
特殊时期缩小SPOT节点组最大值
在特殊时节比如黑色星期五这种资源使用高峰期,或者观测到SPOT回收率太高的时候,可以缩小SPOT节点组最大值,让应用调度到OD节点组上去(目前亲和性都是软亲和,即满足最好,不满足也不会直接Pending)。
4.4.3 加强应用韧性设计
调度策略,准备部署到Spot的应用才调度到Spot节点
Spot节点组要设置污点,这样才不会让一些公共的核心应用比如CoreDNS、Istiod调度到Spot节点组上
使用软亲和性,使得如果Spot节点组扩容不出来,或者整个回退时候,应用能够调度到OD节点组上
YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${app}
namespace: ${namespace}
spec:
template:
spec:
tolerations:
- key: "node-type"
operator: "Exists"
effect: "NoSchedule"
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: node-type
operator: In
values:
- spot
亲和性拓扑配置,打散同一应用的不同Pod到不同节点
节点不稳定,带来的可能就是应用的不稳定,可以根据应用等级使用亲和性或者拓扑配置把Pod打散不同AZ、不同节点、不同类型,这样当部分节点回收时候,不会导致全部Pod在重启中
YAML
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 30
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- ${appid}
topologyKey: failure-domain.beta.kubernetes.io/zone
- weight: 20
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- ${appid}
topologyKey: beta.kubernetes.io/instance-type
- weight: 10
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- ${appid}
topologyKey: kubernetes.io/hostname
PDB配置,保证驱逐时应用不可用Pod个数只有1个
PDB的设置可以让服务配置最多只允许一个应用里面多少Pod被驱逐或重启,这样可以保证一个应用的最大可用副本数。
YAML
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: appid
spec:
minAvailable: 4
selector:
matchLabels:
app: appid
Pod启动之前的依赖检查
现有的SOA架构Pod是去连接本节点的Consul Agent去注册发现服务,但是由于新节点启动的时候,Consul Agent和应用本身并没有先后顺序,应用健壮性又比较差,连不上Consul没什么进程不会退出而且不会重试,导致应用实际有问题。
临时方案是使用一个initContainer去检查Consul Agent状态,如果没成功则先不启动应用,最好应用调整自身逻辑;长期方案是自己写一个节点控制器,去检查新节点上的前置项状态,没通过之前把节点设置为not ready。
YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${appid}
namespace: ${namespace}
spec:
template:
spec:
initContainers:
- name: check-consul
image: xxxx/busybox:1
env:
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
command:
- /bin/sh
- -c
- code=1; while [ $(code) -ne 0 ]; do sleep 1; curl http://$(host_IP):8500/v1/status/leader 2>/dev/null | grep -E '".+"'; code=$?; echo "return code is $(code)"; done;
PreStop探针,保证应用优雅关闭
无论使用SLB还是注册中心,在实际中,当Pod关闭去注销时,SLB或者注册中心的上游会有一点延时,这个时候可能还有流量没摘干净,导致上游抛异常,我们可以用preStop探针sleep一段时间
PowerShell
lifecycle:
preStop:
exec:
command:
- sh
- -c
- sleep 15
应用预热
一些java服务,启动时间较长,需要做很多初始化动作(并且使用了懒加载),如果这时候有流量进来,会导致报错,我们增加了startup探针,来保证应用能够预热
Bash
startupProbe:
failureThreshold: 30
exec:
command:
- sh
- -c
- code=`curl -m 10 -o /dev/null -s -w %{http_code} 127.0.0.1:${httpStartupProbePort}/${httpStartupProbeUrl}`;
if [ $code -ge 200 -a $code -lt 400 ]; then sleep 6; else exit 1; fi
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
4.4.4 其他
集群内所有应用使用云商或者私有镜像仓库
Dockerhub现在限制了免费用户每天下载镜像的次数只50次,如果Spot节点频繁回收节点,而我们服务中有Dockerhub镜像,会导致我们频繁去Dockerhub拉镜像。所以我们的Pod的Sidecar中不能使用Dockerhub镜像仓库,不然可能导致因为限制下载不了镜像而Pod启动失败。
可以使用云商的镜像仓库或者自己搭建Harbor仓库
4、在新扩容节点的时候,上面依赖服务和应用之间启动顺序
应对策略:
•临时方案是使用一个initContainer去检查Consul Agent状态,如果没成功则先不启动应用,最好应用调整自身逻辑
•长期方案是自己写一个节点控制器,去检查新节点上的前置项状态,没通过之前把节点设置为not ready。
•检查所有公共服务和sidecar,使用云商的镜像仓库或者自己搭建镜像仓库
4.5、回滚策略
当应用迁移到Spot节点或者当AWS某个AZ的Spot大面积出现问题的时候,需要有机制或者功能能够回退到OD节点组
4.5.1、单应用常规回滚
由CICD平台提供一键回滚功能,然后把部署到Spot实例上面的应用迁移到On-Demand实例上
4.5.2、某类节点回收率过高
因为AWS不能动态调整节点组里面的实例类型,所以我们只能一个新建不包含该类节点的节点组,然后把原来的节点组设置为不可调度,然后驱逐之后等待缩容,最后删掉原节点组
4.5.3、SPOT整个回滚
如果遇到整个SPOT资源申请不到,或者有在不停回收新分配等问题。可以对全部SPOT进行回滚,
下面是一个操作流程和15个节点的模拟:(按照15个节点预计耗时33分钟左右)
1.OD节点组扩大,控制台设置节点组最值和SPOT节点组相当 ( 15个节点 2分钟 )
2.原节点组设置不可调度 ( 10秒 )
3.把SPOT节点组最大值调整为当前值 ( 1分钟 )
4.驱逐老的节点组 ( 1个节点 25秒 + 90秒sleep, 总共约28分钟,其中22分钟是sleep时间)
5.SPOT节点组最大值缩小1,并设置为不可调度。
6.新节点组最小值改为1
5、总结
SPOT技术本身是提高了稳定性风险,来换取成本的降低。我们能做的就是在保证稳定性的前提下来降低成本。
容器层面的解决方案是围绕着下面两方面进行:
•降低资源中断概率,提高资源响应速度
•减少中断时间,保证应用高可用
在应用层面也需要配合做一定的调整:
•比如优雅关闭、自身状态检测
•存储和计算分离等