云计算账单是许多团队绕不开的支出大头,而EC2 Spot实例正是AWS提供的最激进的省钱手段之一。它利用数据中心中未被使用的闲置算力,以远低于按需实例的价格提供给用户,折扣幅度通常在60%到90%之间。听起来很美好,但代价也很明确:当AWS需要回收这些算力时,你的实例会在两分钟内被中断。经过一年的真实使用,我们对Spot实例的适用边界、成本收益和架构要求有了比较完整的认识,这篇文章把这些经验整理出来。

Spot实例的工作原理与中断机制
Spot实例本质上是对AWS闲置产能的竞购。每个可用区对每种实例类型都有一个实时浮动的Spot市场价格,当市场价格低于你愿意支付的最高出价时,实例持续运行;当市场价超过出价,或者AWS因容量需求要回收资源时,实例就会被中断。现在的Spot定价模型已经简化为完全由供需驱动,用户不再需要手动设置复杂的出价策略,默认按需实例价格封顶,实际扣费按当前市场价计算。
中断前两分钟,实例会通过EC2元数据服务收到中断通知,这个通知可以通过轮询元数据端点获取,也可以配合EventBridge事件触发Lambda函数处理。元数据端点的访问方式如下:
#!/bin/bash # 在实例内部轮询中断通知 TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \ -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") curl -s -H "X-aws-ec2-ec2-metadata-token: $TOKEN" \ http://169.254.169.254/latest/meta-data/spot/instance-action
正常情况下该接口返回404,一旦收到中断,接口会返回一个包含具体回收时间的JSON,格式类似{"action": "stop", "time": "2024-01-15T08:22:30Z"}。两分钟的窗口看似很短,但对于设计良好的无状态服务来说,足够完成连接排空、任务落盘和状态上报。实践中我们总结出一条原则:任何跑在Spot上的进程,都必须假设自己随时会死,所有关键状态都不能只存在内存里。
成本对比:一年下来的真实账单
空谈折扣没有意义,直接看数据。我们以一台c5.2xlarge(8核16G)为例,对比三种购买方式在美东us-east-1区域的年成本。按需实例每小时约0.34美元,一年不间断运行约2978美元;同样的负载跑在Spot上,实际结算均价约每小时0.09到0.12美元,年成本大约在800到1050美元之间,节省幅度接近70%。如果负载本身只在业务高峰运行,比如每天12小时,节省的绝对金额会缩小,但比例不变。
| 购买方式 | 小时单价(美元) | 年成本(724小时运行) | 中断风险 |
|---|---|---|---|
| 按需实例 | 0.34 | 约2978 | 无 |
| Spot实例 | 0.09-0.12(浮动) | 约800-1050 | 存在,两分钟警告 |
| 预留实例(1年) | 约0.21 | 约1839 | 无 |
从表中可以看出,Spot的成本优势非常明显,但需要注意两点。第一,Spot价格是波动的,不同可用区、不同时段价差可能达到数倍,善用Spot放置评分(Spot Placement Score)选择合适的区和实例族能进一步压低成本。第二,实际支出还取决于中断频率,如果实例频繁被回收,任务重跑和启动开销也会摊薄节省的金额。一年下来我们的批量计算任务平均每天遇到0到2次中断,属于可接受范围。
另一个省钱技巧是实例类型多样化。不要锁定单一机型,而是在启动模板中配置一组等价的实例类型,比如c5、c5a、m5、m5a的相近规格,让ASG自动扩容时从当前最便宜、最不容易中断的类型中取用。AWS的Spot容量优化分配策略会自动完成这件事,我们切换到该策略后,中断率明显下降。
哪些业务适合跑Spot,哪些必须避开
适合Spot的场景有一个共同特征:任务可重跑、可分片、状态可外置。典型的包括CI/CD构建流水线,代码编译任务中断后重新触发即可,损失只是几分钟的计算时间;批量数据处理和日志分析,通过EMR或自研调度框架分片执行,单片中断不影响整体;容器化微服务,配合EKS的节点组和Pod漂移能力,实例被回收时工作负载自动转移到新节点;还有渲染、科学计算、爬虫等吞吐型任务。
绝对要避开的是有状态且难以快速恢复的服务。数据库主节点、需要长时间维持TCP长连接的网关、执行到一半无法断点续传的巨型单线程任务,这些场景一旦中断,造成的损失可能远超省下的费用。我们的教训来自一个早期的批处理任务:当时没做检查点,一个跑了六小时的任务在第五小时被中断,全部重来。后来改造为每处理1000条记录写一次检查点到S3,中断后从断点恢复,单次损失被压缩到分钟级。检查点逻辑的核心伪代码如下:
import boto3, json, time
s3 = boto3.client('s3')
CHECKPOINT_KEY = 'checkpoints/job-a.json'
def load_checkpoint():
try:
obj = s3.get_object(Bucket='my-bucket', Key=CHECKPOINT_KEY)
return json.loads(obj['Body'].read())
except s3.exceptions.NoSuchKey:
return {'last_offset': 0}
def save_checkpoint(offset):
s3.put_object(Bucket='my-bucket', Key=CHECKPOINT_KEY,
Body=json.dumps({'last_offset': offset}))
# 主循环:处理完一个分片就保存进度
offset = load_checkpoint()['last_offset']
for batch in read_batches(start=offset):
process(batch)
save_checkpoint(batch.end_offset) # 中断后可从断点继续还有一类容易被忽视的适用场景是测试环境。开发测试集群对可用性要求低,夜间和周末基本闲置,全部换成Spot实例后,测试环境的月账单直接降了六成多,而团队几乎没有感知,因为测试任务中断后重跑一次就好。
实战架构建议与避坑经验
把Spot用好,架构上的核心动作有三个。第一,无状态化改造,所有会话、缓存、任务队列外置到ElastiCache、SQS或数据库,实例本身随时可丢弃。第二,自动化容错,用Auto Scaling组管理Spot实例,设置健康的实例数下限,配合生命周期钩子在实例回收前执行清理脚本;对EKS用户来说,使用Karpenter并给节点打上中断容忍标签,节点被回收时Pod会被优雅驱逐重建。第三,混合编排,关键链路保留少量按需实例兜底,非关键弹性容量交给Spot,这也是AWS官方推荐的best practice。
几个具体的坑值得记录。其一,不要忽略两分钟警告的完整性:实例收到中断通知后,EBS卷的数据仍然保留,如果用的是Stop中断行为(hibernate或stop),实例之后还可能重新启动,但要确保根卷没有设置随实例删除。其二,监控要做在前面,开启CloudWatch的Spot中断指标,把中断事件推送到Slack或钉钉,否则你会对实例消失一无所知。其三,中断频率与实例类型强相关,较新的大规格机型和GPU实例在热门时段中断率明显更高,跑GPU训练任务时建议混合使用Spot和按需实例,用Spot跑可以重跑的试验、用按需跑关键实验。
总结这一年的使用体验:Spot实例不是免费的午餐,而是一笔用工程复杂度换真金白银的交易。如果你的负载天生无状态、可重跑,改造成本几乎为零,直接上Spot就能省下六到八成计算费用;如果负载有状态但可以改造,加上检查点和服务发现之后,大部分容量也能安全迁移;只有那些完全无法容忍中断的核心服务,才值得老老实实付按需或预留的价格。先用小规模流量验证中断应对流程,再逐步扩大Spot占比,是风险最低的落地路径。