4. 计算资源配额:Spark 作业资源分配、Delta Lake 写入/读取并发控制、查询队列管理、动态资源调整策略

聊完了存储和内存,咱们得直面一个更现实的问题——计算资源怎么分。说白了,集群就那么多核,那么多 slot,你给哪个作业?给多少?给多了别的作业饿死,给少了这个作业跑不动。我在项目里见过最惨的一次,一个批处理作业把整个集群的 executor 全占了,实时流任务直接断流,业务报警响了半小时。嗯,从那以后,我对资源配额这件事就特别敏感。

4.1 Spark 作业资源分配:别让作业抢破头

Spark 作业的资源分配,核心就三个参数:spark.executor.instances、spark.executor.cores、spark.executor.memory。但光设参数不够,你得有策略。

我的分配原则:

  • 静态分区 + 动态预留:给核心业务(比如实时入湖)固定 30% 的 executor,剩下的 70% 给批处理作业共享。批处理作业之间用 Fair Scheduler 做内部排队。
  • 避免超大 Executor:单个 executor 超过 8 core 或 32 GB,GC 压力会陡增。我习惯控制在 4-6 core、16-24 GB。
  • 按作业类型分池:ETL 作业用 4 core/16 GB,Ad-hoc 查询用 2 core/8 GB,流作业用 2 core/8 GB 但实例数固定。

举个例子,一个典型的 Delta Lake 写入作业,我会这样配:

--conf spark.executor.instances=20
--conf spark.executor.cores=4
--conf spark.executor.memory=16g
--conf spark.dynamicAllocation.enabled=false
--conf spark.sql.shuffle.partitions=200

为什么关掉动态分配?因为写入 Delta 表时,如果 executor 频繁上下线,事务日志的并发控制会变得很复杂。我个人习惯在写入作业里固定资源,读作业才开动态分配。

小技巧: 用 spark.yarn.executor.memoryOverhead 给 off-heap 留 10-15% 余量。我在生产里见过因为 off-heap 不够导致 executor 被 YARN 杀掉的案例,排查了半天才发现是 memoryOverhead 设小了。

4.2 Delta Lake 写入/读取并发控制:别让事务冲突拖垮你

Delta Lake 的并发控制,核心是乐观并发控制(OCC)。但乐观不代表你可以乱来。我见过一个团队,20 个流同时往一张表写,结果每分钟都有事务冲突重试,写入延迟从 5 秒飙到 2 分钟。

为什么会这样?因为 Delta Lake 的 OCC 在提交阶段会检查事务日志的版本号。如果两个写入事务同时尝试提交,后提交的那个会检测到冲突,然后重试。重试次数多了,性能就崩了。

4.2.1 写入并发控制

我的建议是:

  • 控制写入并发数:单张 Delta 表,写入并发不要超过 8-10 个。超过这个数,冲突概率会指数级上升。
  • 使用分区裁剪:如果写入的是不同分区,冲突概率会大大降低。比如按日期分区,每个流写不同日期,基本不会冲突。
  • 调整重试策略:Delta Lake 默认重试 10 次,间隔指数退避。我习惯改成 5 次,间隔从 100ms 开始,每次翻倍。重试太多反而拖慢整体。
# 设置 Delta 写入重试参数
spark.conf.set("spark.databricks.delta.retry.maxRetries", "5")
spark.conf.set("spark.databricks.delta.retry.initialDelayMs", "100")
spark.conf.set("spark.databricks.delta.retry.maxDelayMs", "5000")

4.2.2 读取并发控制

读取并发相对宽松,但也要注意两点:

  • 快照隔离:Delta Lake 的读操作基于快照隔离,读不会阻塞写,写也不会阻塞读。但如果你在读的时候,有大量写入在提交,读到的可能是旧版本的数据。
  • 避免长时间读取:如果一个读取作业跑了几个小时,Delta 表的版本文件可能已经被清理了(通过 VACUUM),读作业会报文件不存在的错误。我建议把读取作业控制在 1 小时内,或者调大 delta.logRetentionDuration。

注意: 我曾经遇到过一个坑——读取作业用了 timeTravel 查询 7 天前的数据,但 VACUUM 已经把旧版本文件删了。结果作业直接报 FileNotFoundException。从那以后,我要求所有 timeTravel 查询必须配合 delta.deletedFileRetentionDuration 一起调整。

4.3 查询队列管理:让查询排好队

查询队列管理,说白了就是给不同优先级的查询分配不同的资源池。Spark 的 Fair Scheduler 和 Capacity Scheduler 都能做,但我更推荐 Fair Scheduler,因为它更灵活。

我的做法是:

  1. 定义队列:创建三个队列——critical(核心业务)、batch(批处理)、ad-hoc(临时查询)。
  2. 分配权重:critical 权重 5,batch 权重 3,ad-hoc 权重 1。
  3. 设置最小资源:critical 最少保证 20% 的集群资源,batch 最少 10%,ad-hoc 最少 5%。

Fair Scheduler 的配置文件大概长这样:

<allocations>
  <pool name="critical">
    <minShare>20</minShare>
    <weight>5</weight>
    <schedulingMode>FAIR</schedulingMode>
  </pool>
  <pool name="batch">
    <minShare>10</minShare>
    <weight>3</weight>
    <schedulingMode>FAIR</schedulingMode>
  </pool>
  <pool name="ad-hoc">
    <minShare>5</minShare>
    <weight>1</weight>
    <schedulingMode>FIFO</schedulingMode>
  </pool>
</allocations>

注意,ad-hoc 队列我用的是 FIFO 模式。为什么?因为临时查询通常比较短,FIFO 能保证先来先服务,避免长查询把短查询堵死。

经验之谈: 队列管理一定要配合监控。我习惯在 Grafana 上放一个队列积压量的面板,一旦某个队列的等待任务超过 50,就自动触发告警。这样你可以在用户抱怨之前发现问题。

4.4 动态资源调整策略:让资源跟着负载走

动态资源调整,是解决「资源给多了浪费,给少了不够」的终极方案。Spark 的 spark.dynamicAllocation 可以自动增减 executor,但用不好反而会出问题。

我的配置经验:

参数 推荐值 说明
spark.dynamicAllocation.enabled true(读作业)/ false(写作业) 写入作业建议关闭,避免事务冲突
spark.dynamicAllocation.minExecutors 5 保证最低资源,避免冷启动
spark.dynamicAllocation.maxExecutors 50 防止单个作业把集群占满
spark.dynamicAllocation.initialExecutors 10 初始资源,避免启动时太慢
spark.dynamicAllocation.executorIdleTimeout 60s 空闲 executor 多久后释放

你可能会问:为什么写入作业要关掉动态分配?我解释一下——Delta Lake 的写入事务在提交时会检查所有参与写入的 executor。如果 executor 在写入过程中被动态回收了,事务就会失败。我踩过这个坑,一个 200 个分区的写入作业,因为动态分配把 executor 从 50 缩到了 20,结果一半的分区丢失,事务回滚,重试了 3 次才成功。

我的动态调整策略总结:

  • 读作业:开动态分配,min=5, max=50, idleTimeout=60s。
  • 写作业:关动态分配,固定 executor 数量。
  • 流作业:关动态分配,固定 executor,但可以配合 Auto Scaling 做节点级别的扩缩容。

4.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,从作业提交到资源分配,再到并发控制和队列管理,最后通过动态调整形成闭环。

计算资源配额管理核心逻辑 作业提交 Spark 作业资源分配 写入/读取并发控制 查询队列管理 动态资源调整 Delta Lake 事务执行 监控 & 反馈调整 动态调整反馈 核心原则:写入固定资源,读取动态伸缩,队列按权重分配,监控驱动调整

这张图从作业提交开始,经过资源分配,然后分流到并发控制、队列管理和动态调整三个方向,最终汇聚到 Delta Lake 事务执行,再通过监控反馈形成闭环。你可以在实际项目中把它当作一个检查清单——每次上线新作业,都过一遍这个流程,基本不会出大问题。

最后提醒一句: 资源配额管理不是一劳永逸的事。业务在变,数据量在涨,你的配额策略也得跟着调。我每季度会做一次资源使用复盘,看看哪些队列的资源利用率低于 30%,哪些作业的等待时间超过了 SLA。该砍的砍,该加的加,别心疼。