
做大数据平台这块年头久了你会发现一个特别奇怪的现象集群规模明明不大业务量看起来也没暴涨可每个月的账单却像坐了火箭一样往上蹿。老板看了一眼成本报表第一反应永远是“是不是机器不够加节点吧”。但干了几年运维和架构的都知道大部分情况加机器只是治标不治本真正的钱都浪费在看不见的地方。今天这篇不是理论科普是我把自己踩过的坑、调过的参数、被业务方追着吐槽的实战经历重新整理了一遍专门聊聊“Spot实例 队列调度”这套组合拳怎么把大数据成本打下来而且是在不改业务逻辑、不重写代码的前提下。我负责过好几个离线数仓和实时链路混部的集群也替不少朋友公司看过他们的大数据环境。但凡说成本失控的基本都有一个共同画像资源峰谷差极大白天跑着一堆定时任务深夜又有一堆补数和重跑作业任务之间互相抢资源谁嗓门大谁就能拿到队列没人管它们是核心还是临时任务。这种场景下直接上Spot实例配合一套合理的队列调度策略省下40%到50%的成本真的不夸张运气好一点还能更多。当然前提是你得先把账算明白知道钱到底花在哪了。1. 先算账大数据成本为什么越跑越高1.1 钱不是跑在业务上而是耗在“求稳”上我见过最典型的例子是一家做用户行为分析的公司。他们集群有20台物理机跑的是Hadoop Spark Hive。业务量其实没那么大但集群利用率长期不到30%。为什么因为每个业务团队都怕自己的任务在高峰期被挤掉于是所有人都把资源申请得特别大。跑一个几GB的Hive查询Spark Executor内存动辄给8G一个任务申请几十个Executor实际用到一半就不错了。更怕的是夜里的补数任务明明只需要15分钟但为了“稳妥”申请的资源足够跑两个小时。这种“稳”是有代价的。云厂商按量付费的实例你申请了就得付钱哪怕计算资源只用了十分之一。而大数据任务本身又有很明显的潮汐效应白天业务查询多凌晨ETL和训练任务多。你为了应付那两三个小时的高峰就得常备一大堆机器剩下的二十多个小时全是浪费。成本不失控才怪。1.2 三个最容易忽略的隐性成本黑洞除了资源申请过大还有三个隐性成本我每次排查都会重点看。第一个是任务重试带来的“放大效应”。一个Spark Streaming任务失败一次如果开启的是自动重启它不会只重跑失败的那一批往往会把最近一段时间的状态也一并恢复。上游Kafka积压越多重试时需要拉取的数据就越多计算时间越长消耗的资源自然成倍增长。等到你发现账单变高再去查任务早就重试了不知道多少轮。第二个是数据倾斜和计算倾斜。同一份数据几个大Key把某个Executor的负载拉满其他Executor闲着等它。看起来整个Job在跑实际上大部分资源都在空转。这种问题在离线计算里特别常见尤其是Join操作一旦分布不均匀计算时间能从10分钟拖到1个小时成本直接翻好几倍。第三个是存储成本很多人只盯着计算忘了存储。HDFS三副本虽然安全但如果是云上对象存储按量计费的读取请求量也很吓人。小文件太多的时候NameNode压力大读取请求数也暴涨。所以你会发现明明任务没怎么变账单却越来越高问题往往出在文件数量和请求频率上。把这些因素都抛到台面上你会发现所谓“加机器”根本解决不了实际问题。你加了机器任务依然申请过量资源重试依然放大倾斜依然存在。机器越多浪费的基数越大。真正应该做的是让每一台机器都尽可能被有效利用同时让那些“不挑食”的任务跑在便宜的资源上。2. Spot 实例的省钱逻辑以及为什么它不是省心的“免费午餐”2.1 Spot实例的价格和中断机制Spot实例在不同云厂商的叫法不一样AWS叫Spot Instance阿里云叫抢占式实例腾讯云叫竞价实例。不管名字怎么变核心逻辑都一样云厂商把多余的闲置计算资源以远低于按量付费的价格放出来你出价竞价获得使用权。价格通常是按量付费的两到三折我见过最低的时候打到过一折。但天下没有白吃的午餐Spot实例最大的风险是中断。云厂商随时可能回收这些资源给更高优先级的按量实例或Spot出价更高的用户给你的通知时间通常只有几十秒到两三分钟。在这期间如果你正在跑的任务没有做容错那结果就是任务失败、数据丢失、从头再来。正因为这个特性很多人一听Spot就摇头觉得不稳定不适合生产环境。这个理解不能说错但太片面了。对大数据场景来说很多任务本来就是可重试、可中断的。比如批处理的ETL任务失败了你重新拉起就行再比如数据同步任务只要记录好offset中断后接着跑就行。根本不需要每次都跑在按量付费的高价实例上白白多花钱。2.2 什么作业才真正适合用 Spot 实例判断一个作业适不适合跑在Spot上我总结了一个非常简单的三问标准。第一任务是否可以容忍延迟如果必须分钟级响应那不适合Spot比如实时风控、在线推荐。第二任务失败后能否自动恢复如果失败后需要人工介入需要花大量时间重跑那就别用Spot或者只在低峰期用。第三任务是否有持续的固定时段如果调度任务是固定的可以提前预测那就非常合适Spot。我实际用得最多的Spot场景是这几类。 夜里跑的离线ETL、数据清洗以及凌晨的批量训练任务。这类任务时效性要求不高早上8点前跑完就行中间中断两次也能赶上。 数据分析师的临时查询和探索性分析。这类任务不是核心生产链路跑挂了重写一遍SQL也花不了多少时间。 测试环境和预发环境的Spark、Flink任务。开发和联调阶段根本不需要高可用Spot便宜又大碗。 数据同步任务比如从业务库同步到数仓只要做好断点续传中断的影响非常有限。2.3 用 Spot 实例必须做的三个容错设计只把实例类型改成Spot而不做配套容错那是给自己挖坑。我踩过几次坑之后总结出三个必须提前做好的设计。第一任务必须支持断点重跑。拿Spark举例开启Spark Structured Streaming的WAL预写日志和checkpoint机制保证作业重启后可以从上次提交的offset继续消费。离线任务则要保证幂等每次重跑结果一致避免重复计算。第二要做好任务级别的监控告警而不是只看实例状态。很多平台只监控节点是否存活节点挂了就自动替换但如果作业本身没有跑起来替换节点没有意义。我通常会在任务结束时发送消息到钉钉或者企业微信如果某个时间点该跑完的任务没跑完立刻告警。第三数据不丢的保障。如果计算节点被回收临时数据可能丢失所以中间结果尽量落到可靠的存储上比如HDFS、S3、OSS而不是只存在本地磁盘。为了更直观我整理了一份对比表格方便你对照自己的场景选型。对比项按量付费实例Spot实例价格基准价格通常是按量的2~5折随市场波动稳定性高一般不回收随时可能被中断适合任务核心在线、实时要求高离线批处理、可重试任务集群管理常规管理即可需要自动替换和重试机制典型成本高低风险难以控制预算突发中断导致任务失败我遇到过一个真实的案例某个团队把离线数据仓库的日常任务全部迁移到Spot实例上为了保险起见依然保留了10%的按量实例作为兜底专门跑那些不能失败的核心任务。结果一个月下来计算成本下降了接近45%任务完成率不降反升因为之前被长尾任务占用的资源被Spot实例吃掉了按量实例上跑的核心任务反而更从容了。3. 队列调度不花钱也能“凭空”多出三成资源3.1 资源没变为什么队列调度能省钱很多人的误区是觉得调度就是管理任务排队跟省不省钱没关系。实际上在大数据平台里资源调度的好坏直接决定了你的集群是“忙得像狗”还是“闲得发慌”。同一个集群合理的调度可以多跑30%以上的任务不需要额外加一台机器。想象一下一组任务像一堆人挤一扇门。如果大家不排队、不守秩序谁力气大谁先挤进去力气小的人永远进不去整扇门的通过率其实很低。如果加个管理员按优先级排队让短任务先走、长任务绕路整扇门的通行效率立刻提高。队列调度干的就是这件事。Hadoop生态里的YARN是用的最多的调度器它有三种FIFO先来先服务、Capacity Scheduler容量调度器、Fair Scheduler公平调度器。FIFO最原始一个任务占满资源后面的全排队。Capacity则是把集群分成多个队列每个队列有独立容量上限互不干扰。Fair则是动态调整尽量让每个队列获得公平份额。现在主流用的是Capacity和Fair的组合策略因为离线场景需要隔离不同部门的资源在线场景需要保证核心任务优先。3.2 Capacity Scheduler 到底在配置什么Capacity Scheduler的核心配置其实不复杂但很多人没搞明白就乱调。我重点说几个关键参数。在YARN的capacity-scheduler.xml里面你要配置的是不同队列的资源比例。比如把集群分成root.default、root.share、root.priority三个队列。root.default给临时任务和测试任务root.share给日常离线任务root.priority给核心生产任务。然后通过capacity配置每个队列的资源百分比通过maximum-capacity配置这个队列最多能挤占多少别的队列的空闲资源。再通过user-limit-factor限制单个用户最多能占用的队列资源比例避免一个人把整个队列塞满。举个例子我通常会这么配。root.priority的capacity设置为40%maximum-capacity设置为60%root.share的capacity设置为40%maximum-capacity设置为70%root.default的capacity设置为20%maximum-capacity可以到100%。这样做的目的在于无论任务怎么提交核心生产队列永远至少有40%的资源兜底同时如果别的队列闲着生产任务最多可以占到60%日常离线队列也能在空闲时扩张临时任务则可以在别人不用的时候把所有空闲资源全吃掉因为它的maximum-capacity是100%。可能有人要问maximum-capacity设置太高会不会把主队列的资源抢走答案是如果主队列没有任务临时任务吃满资源是好事提高整体利用率。如果主队列任务来了Capacity Scheduler会自动回收临时队列占用的资源优先保障主队列。因为root.priority的maximum-capacity只有60%意味着它最多只能占总资源的60%并不会因为优先级高就无限制挤占其他队列。另外要注意一个很容易被忽略的参数yarn.scheduler.capacity.resource-calculator。默认是DefaultResourceCalculator只按内存计算资源不关心CPU。如果你跑的是CPU密集型的Spark任务不配置CPU资源很容易出现一台机器内存被瓜分完了CPU却闲置一大半。我建议改成DominantResourceCalculator按内存和CPU共同计算这样资源利用更均衡。3.3 比起 YARNKubernetes 队列调度更适合新架构如果你的大数据平台已经容器化比如Spark和Flink跑在Kubernetes上那调度逻辑就需要换一套思路。Kubernetes默认的调度器是逐个Pod调度调度完了任务就固定在那了不会动态调整。所以你需要用到队列级别的调度器比如Volcano、YuniKorn或者云厂商提供的弹性调度组件。我在生产环境里用过Volcano它支持队列Queue的概念一个队列可以绑定一组资源配额任务按队列的优先级来调度。最实用的功能是“弹性配额”即一个队列的资源使用低于配额时其他队列可以借用当配额方有任务提交时运行中的任务会被抢占或驱逐把资源还回去。这个机制跟YARN的Capacity Scheduler很相似但更灵活。对于Flink这种流式任务Kubernetes调度器的抢占逻辑需要特别谨慎。因为流式任务被驱逐的代价很高状态和checkpoint恢复都很费时间。所以我的经验是给实时任务单独建一个队列配置比离线任务更高的优先级和更稳定的资源配额不参与弹性借贷。离线任务则放在另一个队列里允许被抢占和驱逐。4. 落地实操一套不需要改代码的成本优化方案4.1 第一步给自己的作业“画个像”动手优化之前先花一天时间把集群里的所有任务梳理一遍。我一般分成四类。第一类是必须稳定、必须快的任务。典型是实时数仓的ADS层写入、线上报表查询。这类任务必须跑在按量付费实例上单独一个高优队列资源配额绝对有保障。第二类是重要但不怕慢的任务。比如每天凌晨跑的用户标签、离线统计。可以跑在Spot实例上配合中优队列。第三类是临时的、探索性的任务。比如数据分析师的即席查询跑挂了重新跑就行。这类任务也放Spot实例用低优队列只在有空闲资源时运行。第四类是测试和开发任务。直接丢到低优队列用Spot实例最大成本压缩早上来了发现任务跑挂了也没关系重新跑一下即可。你会发现分完类之后真正需要花大钱买“稳”的只有第一类可能只占总任务数量的20%。剩下80%都能用各种手段把成本压下来。4.2 第二步选择合适的基础设施组合我不打算绑定某一家云厂商因为跨云经验来看各家的Spot产品大同小异。重点是搭一套通用的架构。大致思路是一个托管Kubernetes集群作为基础调度层节点池分为普通节点池和Spot节点池。普通节点池跑核心任务Spot节点池跑可容错任务。如果用的是传统Hadoop集群那就更简单了。把YARN NodeManager部署在Spot实例上配合Capacity Scheduler的不同队列在Spot实例上跑低优队列的任务。只要弹性伸缩能把挂掉的Spot节点自动剔除并补充新的节点整体是来得及的。以Kubernetes结合Volcano为例一份简单的队列配置是这样apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: production spec: weight: 4 capability: cpu: 40 memory: 80Gi reclaimable: false --- apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: offline spec: weight: 2 capability: cpu: 60 memory: 120Gi reclaimable: true --- apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: dev spec: weight: 1 capability: cpu: 20 memory: 40Gi reclaimable: true这里weight决定队列之间的优先级权重reclaimable: false表示这个队列的资源不会被抢占。capability指队列资源上限。我把生产队列设为不可抢占离线队列和开发队列可以抢占。实际任务提交时通过指定队列名称来调度。Spark任务提交示例spark-submit --master k8s://https://api-server \ --deploy-mode cluster \ --conf spark.kubernetes.driver.queueoffline \ --conf spark.kubernetes.driver.request.cores2 \ --conf spark.kubernetes.driver.limit.memory4g \ --conf spark.kubernetes.executor.queueoffline \ --conf spark.kubernetes.executor.request.cores2 \ --conf spark.kubernetes.executor.limit.memory4g \ --conf spark.kubernetes.executor.instances20 \ --conf spark.kubernetes.container.imageyour-image \ local:///opt/spark/examples/jars/your-etl-job.jar需要说明的是不同Spark版本对Kubernetes原生调度的配置项名称有差异具体以官方文档为准。核心意思是任务进哪个队列决定了它能用多少资源、是否会被抢先挤掉。4.3 第三步把Spot实例“接进”集群并设置容错假设你用的Kubernetes托管服务支持节点池那就创建两个节点池ondemand-pool和spot-pool。给Spot节点池打上污点防止普通任务调度上去。然后通过Pod的nodeSelector或tolerations指定哪些任务可以跑到Spot节点上。为了演示可以给Spot节点池打上spottrue:NoSchedule的污点然后在Spark Pod上配置容忍tolerations: - key: spot operator: Equal value: true effect: NoSchedule这样只有明确声明了容忍Spot的任务才会被调度到Spot节点池上普通任务不会误跑上去。接下来还需要做节点中断处理。Kubernetes对Spot实例被回收这件事有专门处理的机制通常是收到中断通知后节点会被标记为不可调度同时触发Pod驱逐。我建议你配置好PodDisruptionBudgetPDB保证同一时间最多有多少个Pod可以被打断。比如一个Spark作业有20个Executor可以配置最多允许5个被打断这样剩下的15个还能继续跑配合Spark的Executor动态分配和失败重试基本能把中断影响降到最低。4.4 第四步配合弹性伸缩策略这一步是省钱的关键中的关键。如果没有弹性伸缩Spot实例即使再便宜长期闲置也是浪费。我通常配两个维度的伸缩。一个是基于时间的定时伸缩。大数据任务有明显的潮汐特性早晨8点到10点是日报高峰期资源需求大可以提前15分钟扩容Spot节点池晚上10点以后离线任务多也可以提前扩容凌晨2点到5点则是低谷可以缩容。另一个是基于指标的自动伸缩。我常监控的是YARN集群的Pending内存量或者Kubernetes集群中队列的Pending Pod数量。一旦积压任务变多自动触发扩容。这样不会盲目扩太多能够按需弹。这里要特别注意弹性伸缩有一个“冷却时间”问题扩容后不能立刻缩否则刚启动的Executor可能还没跑完任务就被缩掉了。我把冷却时间设为10到15分钟具体看任务的平均运行时长。4.5 第五步利用白名单限制成本上限最后一步是成本上限控制。云厂商一般都有预算报警我建议在大数据项目里设置两重上限第一重是月度预算总金额的报警超过80%就告警第二重是Spot实例的类型上限避免系统自动选择了某个特别贵的“伪Spot”规格。虽然Spot价格便宜但不同规格差异也很大。GPU实例的Spot可能比普通CPU按量还贵如果任务不需要GPU千万别开。另外有些云平台会把“节省计划”和“资源包”混在一起卖购买前要仔细看抵扣逻辑。我的经验是如果你的Spot已经能占到集群总资源的50%以上那就没必要囤太多按量资源包反而应该把预算花在Spot的保障时长上如果有这个服务的话可以进一步提升稳定性。5. 常见问题与排查技巧实录5.1 Spot 节点被回收任务中断怎么办先说结论不要试图让Spot节点上的任务完全不中断那是逆天而行。你要做的是让中断的影响最小化。一个经验值是给Spark作业的spark.task.maxFailures设置合理大小默认是4我一般调到8让Executor可以容忍更多的节点故障。同时开启spark.dynamicAllocation.enabledtrueExecutor被杀掉后系统能自动申请新Executor补齐。如果任务特别重要还可以在提交时用spark.blacklist.enabledtrue把故障节点拉黑不再往上面调度新任务。另外在大数据任务层面做一层“重试封装”也很有效。我一般会写一个小脚本检测任务退出码如果是因为资源中断导致的失败自动重新提交一次。这个脚本本身很轻但对整体稳定性提升巨大。5.2 队列配置了但任务还是不停抢资源这个问题的原因多半是资源配置和任务提交参数没对上。比如YARN队列里设置了内存比例但Spark任务提交时没有指定--queue所有任务都会默认进root.default队列把你给临时任务准备的队列塞得满满的。排查步骤很简单先看YARN UI或者资源管理界面确认每个队列实际运行的任务数再看提交命令里有没有写--queue。如果多个业务方各自提交任务最好让统一的提交平台封装一层把队列参数固定下来禁止任务自己乱填。5.3 用了 Spot 之后任务运行时间变长数据产出变晚这种情况通常是两方面的原因。第一Spot节点的规格可能比较老CPU主频低导致单任务性能下降。解决办法是给Spot节点池选择“较新代”的实例型号或者任务里适当增加并行度。第二调度层面因为队列优先级低任务一直排队等不到资源。这时候要看看是不是有几个大任务长期霸占着离线队列需要做任务级的内存限制。我遇到过最极端的一个情况是某个数据分析师的临时查询申请了100个Executor跑了一个小时还没结束。它不是核心任务但因为没限制资源整个离线队列被拖住了。后来我在队列上配置了maximum-applications和用户级别的资源上限这个问题才彻底解决。5.4 成本没有下降反而上升了这看起来不太可能但确实有人遇到。我的排查顺序是从这几个方面入手。第一看看Spot实例的比例是不是占比太低。如果大部分任务还是跑了按量Spot只占不到10%那成本当然降不下来。第二看看弹性伸缩是不是频繁弹出又缩回节点一启动就产生费用哪怕只跑几分钟也要按小时计费。如果都是短生命周期任务建议把缩容策略改得更保守。第三看看数据存储的读请求费用是不是暴涨。Spot任务跑得快如果从存储中读数据次数变多存储侧费用上升可能抵消计算侧省下来的钱。最后还有一个很实用的检查项看Spot实例的“中断率”。如果某段时间中断率特别高你的任务大量重跑重跑的费用加上时间成本可能比按量还贵。这种情况下建议换一个区域的Spot或者调整跑批时间避开云厂商资源紧张的高峰。5.5 队列调度相关故障速查表症状常见原因快速处理方式任务提交不到指定队列未指定--queue或队列名写错检查提交命令和队列是否存在高优任务排队严重高优队列容量不足或低优队列占用过多调大maximum-capacity或重启低优任务单个用户占满队列缺少用户级资源限制配置user-limit-factor节点闲置但任务排队节点与队列标签不匹配Pod调度不上去检查nodeSelector和污点容忍配置Spot节点频繁替换中断率高任务没有重试机制调整Spot机型或增加失败重试次数资源利用率上升但成本上升实例规格贵或读请求多分析实例账单和存储请求费用个人经验省钱的本质是让每一份资源都干它该干的活我在帮好几个团队调优之后最大的感受是成本优化不是一味地抠门而是让资源分配更符合实际需要。Spot实例解决的是“别买贵的”队列调度解决的是“别浪费”。两者之间其实是有强依赖的。没有队列调度Spot实例乱跑一通任务互相干扰中断率上升成本反而会增加。没有Spot实例队列调度再完美也只是在有限的资源里腾挪很难有质的提升。建议从小规模开始尝试先挑两三个不重要的任务迁到Spot上配置好队列和告警观察一两周再逐步扩大范围。不要一上来就把核心链路跑在Spot上也不要听说Spot便宜就全面切换。稳妥一点先把流程跑通再用数据说服自己和老板。等你真的把Spot的比例调到40%、50%以上再看账单的时候那个数字是真的会让人心情愉悦的。