云上的计算成本一直是Kubernetes使用方最关心的问题之一。竞价实例(Spot Instance)通常能比按量付费便宜60%到90%,代价是云厂商随时可能在库存紧张时回收资源;而云厂商提供的节省模式(如节省计划、预留实例券)价格优惠幅度有限,但供给稳定。把两种资源在同一个集群里混用,让可容忍中断的任务跑在竞价节点上,让核心服务跑在节省模式保障的节点上,是目前比较主流的降本思路。本文从架构设计、调度配置、中断处理三个层面展开,给出一套可操作的混用方案。

一、节点池分层:混用方案的整体架构
混用不是简单地往集群里塞几台竞价虚拟机,而是要先把节点分层。推荐的分层方式是三个节点池:第一个是稳定池,使用节省计划或包年包月覆盖,专门承载核心组件和有状态服务;第二个是弹性池,全部使用竞价实例,承载批处理任务、CI构建、无状态扩容副本;第三个是兜底池,使用按量付费,容量很小,只在前两个池都无法满足需求时临时顶上。
之所以要保留一个小的兜底池,是因为竞价实例存在区域性库存波动。某些热门规格在高峰时段可能完全抢不到,如果集群只依赖竞价节点,Pod会因为无法调度而大量Pending。通过节点标签和节点池的分离,配合Cluster Autoscaler的扩缩容策略,可以让 autoscaler 按顺序尝试节点池:先扩竞价池,失败后再扩按量池,这样既拿到了价格优势,又避免了完全无资源可用。
在成本分配上,建议给每个节点池打上成本中心的标签,例如 pool=stable、pool=spot,方便后续用 Kubecost 或云厂商账单工具统计各业务实际消耗的资源成本,为下一步调整竞价比例提供数据依据。
二、调度配置:污点、容忍度与优先级
竞价节点必须打上污点,防止不相关的工作负载误调度上去。以云厂商托管集群为例,竞价节点通常自带污点,如果是自建节点,需要手动添加:
# 给竞价节点打污点,effect 使用 NoExecute 以便节点被回收时自动驱逐 kubectl taint nodes spot-node-1 spot=true:NoExecute # 同时打上标签便于 nodeSelector 或 nodeAffinity 匹配 kubectl label nodes spot-node-1 pool=spot ```
能跑在竞价节点上的Pod,需要在声明中同时写容忍度和节点亲和性。只写容忍度是不够的,那样普通任务也会被调度到竞价节点上,失去了分层的意义。正确的写法是容忍度加节点亲和性成对出现:
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-worker
spec:
replicas: 10
selector:
matchLabels:
app: batch-worker
template:
metadata:
labels:
app: batch-worker
spec:
# 高优先级保证抢占时优先保留
priorityClassName: high-priority
tolerations:
- key: "spot"
operator: "Equal"
value: "true"
effect: "NoExecute"
tolerationSeconds: 60 # 容忍60秒,给优雅退出留时间
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: pool
operator: In
values: ["spot"]
- weight: 20
preference:
matchExpressions:
- key: pool
operator: In
values: ["on-demand"]
这里有一个关键技巧:preferredDuringScheduling 而不是 requiredDuringScheduling。使用软亲和性,调度器会优先把Pod放到竞价节点,竞价池满了就自动溢出到按量节点,业务不会因为硬性约束而无法调度。再加上 tolerationSeconds 设置一个合理的驱逐宽限期,让应用有时间处理完手头的请求再退出。
对于优先级,建议定义两个PriorityClass:核心服务用高优先级,批处理任务用低优先级甚至可牺牲级别。当竞价节点被回收、Pod需要重新调度到资源紧张的稳定池时,低优先级任务会被抢占,核心服务始终有资源保障,这是混用架构里稳定性兜底的最后一道防线。
三、中断处理:让任务优雅面对实例回收
竞价实例被回收前,云厂商会提前发出中断通知。AWS是提前两分钟通过实例元数据服务暴露中断事件,其他云厂商也有类似的机制,比如通过元数据接口或事件总线推送。稳妥的做法是在节点上部署一个DaemonSet,轮询中断信号,一旦捕获到就立刻删除节点对象或打上不可调度污点,触发Pod提前迁移,把两分钟的应急窗口变成五分钟以上的从容迁移。
应用层面要做好三件事。第一是优雅退出,处理SIGTERM信号,把处理到一半的任务状态写入外部存储,而不是依赖本地临时文件。第二是任务可拆分,批处理任务尽量设计成幂等的小分片,某个副本被驱逐后,Kubernetes重新拉起副本继续处理即可,配合工作队列(如Kafka、Redis队列)实现断点续跑。第三是PDB(PodDisruptionBudget)要谨慎设置,有状态服务如果不希望竞价池上的副本被同时驱逐,可以通过反亲和性把副本打散到不同节点,并设置 minAvailable 保证最小可用副本数。
对于完全不能容忍中断的组件,比如数据库、消息队列的主节点,直接在容忍度上做排除即可:不写spot污点的容忍,这类Pod就永远不会落到竞价节点上。另外建议对核心Deployment设置 podAntiAffinity,避免竞价池承担的副本身份过于集中导致单节点回收影响过大的流量。
四、成本监控与比例调优
混用方案上线后,需要持续观察几个指标来调整竞价比例:竞价节点的实际中断频率、Pending Pod的数量变化、按量兜底池的扩容次数,以及各节点池的单位成本。如果某类任务的中断重建成本高于竞价省下的钱,就应该把它迁回稳定池;如果兜底池长期零扩容,说明竞价比例还可以继续提高。
一个经验性的起点是:无状态在线服务可以放50%到70%的副本到竞价池,批处理和CI任务可以100%上竞价,有状态服务全部留在稳定池。再配合Pod级别的水平自动伸缩(HPA),让流量高峰期优先在竞价池扩容,低谷期优先缩竞价副本,整体成本通常能压缩40%到60%。通过Kubecost或云厂商的成本分析工具按命名空间出账,可以清楚看到每个业务团队的省钱效果,也方便向管理层证明这套架构的价值。
Kubernetes竞价实例Spot节点修改时间:2026-09-02 04:36:31