调度与资源层

本页讲解湖仓一体架构中的调度与资源管理层:Apache DolphinScheduler 负责"作业何时跑、按什么顺序跑",Apache YARN 负责"作业分到多少 CPU 与内存、在哪个节点跑"。学完本页,你将能够独立设计湖仓批流作业的 DAG 编排与 YARN 队列隔离方案,并掌握 Compaction 调度化、数据就绪驱动等湖仓特有的调度实践。
调度编排层:DolphinScheduler(何时跑 / 按什么依赖顺序跑)
资源管理层:Apache YARN(分多少资源 / 在哪些节点跑)
计算层:Spark / Flink / Trino
存储层:对象存储 / HDFS + 开放表格式(Iceberg / Hudi / Delta)

调度与资源是两条互补的问题域:DolphinScheduler 解决"时间维度"——按 cron 定时或依赖事件触发工作流,管理任务依赖、补数、重试与告警;YARN 解决"空间维度"——把集群 CPU 与内存抽象为队列与 Container,在多租户之间做配额、隔离与弹性共享。二者协同的典型链路是:DolphinScheduler 在约定时刻把 Spark/Flink 作业提交到 YARN 的指定队列,YARN 按队列容量与优先级分配 Container,作业失败由 DolphinScheduler 感知并按策略重试、告警。

分工记忆法:作业"迟到"(没按时跑起来)先查调度层——cron 表达式、依赖阻塞、Worker 容量;作业"饿死"(提交后长时间 PENDING)先查资源层——队列容量、配额、抢占策略。
Web UI / REST 客户端 工作流编排 · 租户与项目管理 ApiServer 统一入口 · 鉴权 · 元数据存储 AlertServer 邮件 · 钉钉 · 企微告警分发 MySQL / PostgreSQL 工作流定义与运行实例元数据 MasterServer(active/standby) DAG 切分 · 状态机驱动 · 分发命令 ZooKeeper 选主 · 容错 · 任务事件监听 WorkerServer(分组 A) 批处理任务执行 WorkerServer(分组 B) 流处理任务执行 WorkerServer(分组 C) 维护类任务执行 作业提交 → YARN 队列(realtime / offline / default)→ Container 分配执行

Apache DolphinScheduler

DolphinScheduler(下称 DS)是 Apache 顶级开源的分布式可视化 DAG 工作流调度系统,以"去中心化 Master 集群 + Worker 分组"支撑高可用与水平扩展,是国产湖仓/数仓场景中使用最广泛的调度器之一。相对于 Crontab、Azkaban、Airflow,DS 的优势集中在:DAG 可视化编排 补数 多租户 Worker 分组 四个生产刚需上。

1. 服务架构

组件 角色定位 关键职责
MasterServer 调度核心(可多实例 HA) 把工作流定义切分为任务实例、驱动状态机流转、向 Worker 分发执行命令;通过 ZooKeeper 选主与容错
WorkerServer 任务执行器(按分组横向扩展) 在执行线程池中运行 Shell / SQL / Spark / Flink / MR 等任务,回写状态与日志
AlertServer 告警服务 消费告警事件,分发到邮件、钉钉、企微、飞书、Webhook 等告警渠道
ApiServer REST API 与 Web UI 后端 工作流定义、项目与租户管理、任务实例查询,元数据落库(MySQL / PostgreSQL)
部署要点:Master 建议至少 2 个实例 + ZooKeeper 集群实现热备;Worker 按"分组"部署(如 batch 组、stream 组、ops 组),任务定义时指定分组即可实现执行侧隔离;元数据库务必配置定时备份——工作流定义全部在此,是 DS 的"命根子"。

2. DAG 编排核心概念

概念 含义 湖仓场景用法
任务依赖 同一工作流内上下游任务形成 DAG;上游成功才触发下游 清洗 → 聚合 → Compaction → 质量校验的依赖链;跨工作流用 DEPENDENT 任务节点引用上游实例
补数(补历史数据) 指定起止日期区间回跑工作流,时间参数按区间逐日替换,支持串行/并行与并行度 上游数据回刷后按区间重跑 30 天分区;并行度 = 4 时同时回补 4 天
多租户 租户绑定 Linux 用户(或 K8s 命名空间),任务以该身份在 Worker 执行 不同团队作业以各自系统账号运行,实现 HDFS / Hive 权限与审计隔离
Worker 分组 Worker 注册时声明分组,任务只能路由到指定分组 重型 Spark 批任务固定到 batch 组(大内存机器),就绪探测等轻任务走 ops 组
定时与依赖组合 cron 定时触发 + 工作流内依赖 + 跨工作流 DEPENDENT 凌晨批次"cron 兜底 + 上游实例依赖"双保险,避免上游延迟导致脏跑

3. 湖仓任务定义要点

DS 提供多种任务类型,湖仓链路中最常用的是 SHELL、SQL、SPARK、FLINK 四种。无论哪种类型,都应遵守两条规范:时间一律用内置参数(如 $[yyyy-MM-dd])而非硬编码,保证补数可回放;任务必须幂等(Iceberg/Hudi 表按主键或分区覆盖写),保证重试安全。

SHELL 任务——封装 spark-submit(常用于带复杂参数的批作业):

#!/bin/bash
# DS SHELL 任务:Spark 清洗作业(ODS -> DWD),日期由 DS 参数替换
set -euo pipefail

dt="$[yyyy-MM-dd]"        # 内置时间参数:补数时按区间逐日替换

spark-submit \
  --master yarn \
  --deploy-mode cluster \
  --queue lakehouse_offline \
  --name "dwd_orders_clean_${dt}" \
  --conf spark.sql.adaptive.enabled=true \
  --conf spark.sql.shuffle.partitions=800 \
  --num-executors 20 \
  --executor-cores 4 \
  --executor-memory 8g \
  --class com.lakehouse.job.DwdOrdersClean \
  hdfs:///lakehouse/jobs/dwd-orders-clean-1.0.0.jar \
  --date "${dt}"

SQL 任务——SparkSQL 数据源(适合声明式的分区覆盖写):

-- DS SQL 任务(SparkSQL 类型):DWD -> DWS 日聚合
SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.coalescePartitions.enabled = true;

INSERT OVERWRITE lakehouse.dws_orders_day
PARTITION (dt = '$[yyyy-MM-dd]')
SELECT user_id,
       count(*)          AS order_cnt,
       sum(order_amount) AS order_amt
FROM   lakehouse.dwd_orders
WHERE  dt = '$[yyyy-MM-dd]'
GROUP BY user_id;

FLINK 任务——流作业常驻提交:

# DS FLINK 任务类型核心参数(等价于如下 flink run 命令)
flink run \
  -m yarn-cluster \
  -yqu lakehouse_realtime \
  -yjm 2048 -ytm 4096 -ys 2 \
  -yD metrics.reporter.prom.factory.class=org.apache.flink.metrics.prometheus.PrometheusReporterFactory \
  -c com.lakehouse.stream.BinlogToIceberg \
  hdfs:///lakehouse/jobs/binlog-to-iceberg-1.0.0.jar \
  --kafka-topic ods_binlog --iceberg-table lakehouse.ods_orders

SQL 任务——JDBC 数据源(质量校验最常用):

-- DS SQL 任务(MySQL / JDBC 类型):把质量校验规则做成"违规明细查询"
-- 规则 1:当日分区行数不得低于昨日分区的 50%(同比波动)
-- 规则 2:主键重复率不得超过 0.1%
SELECT 'partition_cnt_drop' AS broken_rule, '$[yyyy-MM-dd]' AS dt
FROM   qc.dws_orders_cnt
WHERE  dt = '$[yyyy-MM-dd]' AND cnt < threshold * 0.5
UNION ALL
SELECT 'pk_duplicate', '$[yyyy-MM-dd]'
FROM   qc.dws_orders_dup
WHERE  dt = '$[yyyy-MM-dd]' AND dup_rate > 0.001;
-- 结果写入 qc 违规明细表后,由下游 SHELL 任务统计行数:
-- 违规行数 > 0 则 exit 1,把工作流标记失败并阻断指标产出

DEPENDENT 任务——上游工作流实例依赖(跨链路就绪保障):

# 就绪探测 SHELL:上游分区到位才放行下游清洗(DS 内置于依赖节点后)
#!/bin/bash
set -euo pipefail
dt="$[yyyy-MM-dd]"

for i in $(seq 1 60); do
  # 检查上游 HDFS 分区的 _SUCCESS 标记(binlog 入湖作业完成时写入)
  if hdfs dfs -test -e "/warehouse/ods_orders/dt=${dt}/_SUCCESS"; then
    files=$(hdfs dfs -count "/warehouse/ods_orders/dt=${dt}" | awk '{print $2}')
    # 分区文件数大于 0 视为就绪,退出码 0 放行下游任务
    [ "${files}" -gt 0 ] && exit 0
  fi
  sleep 60
done
echo "上游分区 dt=${dt} 超过 60 分钟未就绪" >&2
exit 1    # 非零退出码触发 DS 失败重试 / 告警

4. 定时 cron 与依赖的组合策略

DS 使用 Quartz 6 域 cron 表达式(秒 分 时 日 月 周),示例:

表达式 含义 典型用途
0 0 2 * * ? 每日 02:00:00 凌晨批次启动
0 30 2 * * ? 每日 02:30:00 错峰启动第二个批次
0 0/30 * * * ? 每 30 分钟一次 准实时任务探测
0 0 0/2 * * ? 每 2 小时整点 小时级增量入湖
0 0 3 ? * MON-FRI 工作日 03:00 工作日报表前序

三种触发组合的取舍:纯时间驱动最简单,但上游延迟时下游空跑或脏跑;时间 + 工作流内依赖保证链内顺序,是最常用的日批模式;时间 + 跨工作流 DEPENDENT + 就绪探测最稳,上游未就绪时任务阻塞等待而非错误运行。

5. 失败重试与告警策略

配置层级 配置项 推荐值 说明
任务级 失败重试次数 2 ~ 3 覆盖网络抖动、JM 重启等瞬时故障
任务级 失败重试间隔 10 分钟 避免立刻重试撞上同一个未恢复的故障
任务级 超时告警 / 失败 时长 = 历史 P95 × 1.5 卡死任务及早暴露,超时可选择仅告警或直接杀掉
工作流级 失败策略 关键链路"结束";旁路任务"继续" 指标刷新等旁路任务失败不应阻塞主链路
告警 发送策略 仅失败时告警 成功也告警会造成告警疲劳
告警 渠道分级 钉钉/企微(即时)+ 邮件(留档) 核心链路失败升级电话值班
最佳实践:把"失败重试"视为应对瞬时故障的机制,而不是应对数据问题的机制。重试前先看日志:语法错误、数据倾斜类失败重试 100 次也不会成功,徒增集群负载。

6. 湖仓场景典型 DAG

以电商湖仓的日批次为例,完整链路为:数据就绪探测 → 清洗 → 聚合 → Compaction → 质量校验 → 指标产出。DAG 中的每一环都安排了错峰时刻,避免峰值资源叠加:

① 数据就绪探测 02:00 · _SUCCESS / 分区探测 ② ODS → DWD 清洗 02:10 · Spark · offline 队列 ③ DWD → DWS 聚合 02:50 · SparkSQL · offline 队列 ④ Iceberg Compaction 04:30 · 小文件合并 + 快照过期 ⑤ 数据质量校验 05:10 · 行数/主键/空值规则 ⑥ 指标产出 / 通知 05:30 · 报表刷新 · 告知下游
注意:质量校验节点必须放在 Compaction 之后、指标产出之前,且"校验失败即阻断"——否则带病数据会被下游报表放大。校验规则至少覆盖:分区行数同比波动、主键重复数、关键列空值率。

Apache YARN

YARN(Yet Another Resource Negotiator)是 Hadoop 生态的资源管理中枢。湖仓集群中 Spark、Flink、Hive/Tez 等计算引擎都以 Client/AM 模式向 YARN 申请资源,因此队列设计与容量规划直接决定了多引擎、多租户能否和平共处。

1. 核心架构

角色 定位 关键职责
ResourceManager(RM) 全局唯一(可 HA) 接收作业提交,按调度器(Capacity / Fair)把资源分配给队列与应用;内含 Scheduler 与 ApplicationsManager
NodeManager(NM) 每节点一个 周期性汇报节点可用资源,负责 Container 的启停、监控与日志聚合
ApplicationMaster(AM) 每个应用一个 代表应用向 RM 申请 Container、命令 NM 启动任务、监控作业进度与容错
Container 资源抽象单位 封装一段固定额度(vcores + memory)与优先级的资源,是调度与隔离的最小单元
ResourceManager(全局调度器) CapacityScheduler 队列分配 · ApplicationsManager 管理 AM 生命周期 ApplicationMaster(每个应用一个) 申请 Container · 监控作业 · 失败重试 队列树 root / { realtime, offline, default } capacity 配额 · maximum-capacity 弹性 · ACL 提交权限 NodeManager · node-01 Container Container NodeManager · node-02 Container Container NodeManager · node-03 Container Container

2. 队列模型与资源隔离:capacity-scheduler.xml

湖仓集群的典型队列划分是"实时流队列 + 离线批队列 + 兜底队列"。以下配置将集群切分为三个一级队列:realtime(Flink 常驻流作业,高保障)、offline(Spark/Hive 离线批,弹性最大)、default(临时查询与开发调试兜底):

<configuration>

  <!-- 一级队列:实时流 / 离线批 / 兜底,capacity 之和必须为 100 -->
  <property>
    <name>yarn.scheduler.capacity.root.queues</name>
    <value>realtime,offline,default</value>
  </property>

  <!-- ========== realtime:Flink 常驻流作业,高保障队列 ========== -->
  <property>
    <name>yarn.scheduler.capacity.root.realtime.capacity</name>
    <value>30</value>
    <!-- 正常时段最低保障 30% 集群资源 -->
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.realtime.maximum-capacity</name>
    <value>50</value>
    <!-- 空闲时最多借用至 50%,防止单队列吃满集群 -->
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.realtime.acl_submit_applications</name>
    <value>flink,rt_user</value>
    <!-- 仅允许流平台账号提交,防止批任务挤占 -->
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.realtime.acl_administer_queue</name>
    <value>yarn,flink</value>
  </property>
  <property>
    <!-- 实时队列中的作业不允许被抢占(关键作业保障) -->
    <name>yarn.scheduler.capacity.root.realtime.disable_preemption</name>
    <value>true</value>
  </property>

  <!-- ========== offline:Spark / Hive 离线批,弹性最大 ========== -->
  <property>
    <name>yarn.scheduler.capacity.root.offline.capacity</name>
    <value>60</value>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.offline.maximum-capacity</name>
    <value>80</value>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.offline.user-limit-factor</name>
    <value>2</value>
    <!-- 单用户最多占用队列容量的 2 倍(借用弹性时) -->
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.offline.acl_submit_applications</name>
    <value>spark,batch_user</value>
  </property>

  <!-- ========== default:临时查询与开发调试兜底 ========== -->
  <property>
    <name>yarn.scheduler.capacity.root.default.capacity</name>
    <value>10</value>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.default.maximum-capacity</name>
    <value>30</value>
    <!-- 限制临时队列的毛刺,避免挤占关键链路 -->
  </property>

  <!-- ========== 全局调度行为 ========== -->
  <property>
    <name>yarn.scheduler.capacity.maximum-applications</name>
    <value>10000</value>
    <!-- 集群并发应用上限,防止小任务风暴拖垮 RM -->
  </property>
  <property>
    <name>yarn.scheduler.capacity.maximum-am-resource-percent</name>
    <value>0.1</value>
    <!-- AM 最多占用 10% 资源,给 Task Container 留余量 -->
  </property>
  <property>
    <name>yarn.scheduler.capacity.node-locality-delay</name>
    <value>40</value>
    <!-- 放弃节点本地性调度前等待的心跳轮数 -->
  </property>
</configuration>
参数语义速记:capacity 是"保底"(队列随时可用的份额,同级之和恒为 100),maximum-capacity 是"上限"(借空闲资源时不得超过的弹性天花板),user-limit-factor 限制单用户的借用倍数,ACL 则从入口上禁止越队提交。

3. 标签调度:节点级物理隔离

队列隔离是"逻辑配额",仍共享同一批节点;当实时流作业对延迟极度敏感(不希望与重批任务争抢磁盘/网络)时,用 Node Label 把集群划分为物理分组,让 Flink 作业独占高配机器:

# 1) yarn-site.xml:开启节点标签(RM 侧)
<property>
  <name>yarn.node-labels.enabled</name>
  <value>true</value>
</property>
<property>
  <name>yarn.node-labels.fs-store.root-dir</name>
  <value>hdfs:///yarn/node-labels</value>
</property>

# 2) 创建标签并绑定节点(无需重启集群)
yarn rmadmin -addToClusterNodeLabels "realtime,offline"
yarn rmadmin -replaceLabelsOnNode \
  "node-01.foo.com=realtime,node-02.foo.com=realtime,node-11.foo.com=offline,node-12.foo.com=offline"

# 3) capacity-scheduler.xml:把标签授权给队列
<property>
  <name>yarn.scheduler.capacity.root.realtime.accessible-node-labels</name>
  <value>realtime</value>
</property>
<property>
  <name>yarn.scheduler.capacity.root.realtime.accessible-node-labels.realtime.capacity</name>
  <value>100</value>
  <!-- realtime 队列独占 realtime 标签节点 100% 容量 -->
</property>
<property>
  <name>yarn.scheduler.capacity.root.offline.accessible-node-labels</name>
  <value>offline</value>
</property>
<property>
  <name>yarn.scheduler.capacity.root.offline.accessible-node-labels.offline.capacity</name>
  <value>100</value>
</property>
注意:启用标签后,"默认分区"(无标签节点)的各队列容量配置仍然生效且总和必须为 100;每个标签分区内的容量按该分区的百分比独立计算。作业只需提交到对应队列,无需修改业务代码。

4. 核心容量参数规划公式

# ---------- 节点与集群容量 ----------
单节点可分配内存 = 物理内存 × 0.75 − OS / 监控 Agent 预留(通常 4 ~ 8GB)
单节点可分配 vcores = 物理核数(超线程开启且 CPU 不敏感时可放宽至 1.5 倍)
集群总容量 = Σ 各节点可分配资源

# ---------- 队列配额 ----------
队列 capacity(保底)   = 队列高峰稳态需求 ÷ 集群总容量 × 100
队列 maximum-capacity = 历史峰值毛刺需求 ÷ 集群总容量 × 100 + 20% 弹性缓冲

# ---------- Container 规格 ----------
单 Container 内存 = 执行器 JVM 堆 × (1 + 10% ~ 15% 堆外 overhead)
memory : vcore 经验比 = 4GB ~ 8GB : 1(重 shuffle 批作业取高值)

# ---------- 校验 ----------
稳态并发 Container 总内存 ≤ offline 队列 capacity × 集群总内存(保底可满足)
高峰并发 Container 总内存 ≤ offline 队列 maximum-capacity × 集群总内存(弹性可消化)

资源隔离落地

队列隔离、标签隔离、K8s 混合部署是湖仓生产环境三种主流的资源隔离方案,隔离强度与成本依次递增:

队列隔离

逻辑配额 同一集群内以 capacity 队列切分配额,靠 ACL 与 maximum-capacity 控制弹性,零改造成本,是中小规模湖仓的首选。

标签隔离

物理节点 Node Label 把节点划分成组,实时作业独占标签节点,彻底避免与批任务争抢磁盘与网络,适合延迟敏感的流批混跑集群。

K8s 混合部署

容器化 批流作业统一上 K8s,用 Namespace + ResourceQuota + Yunikorn/Volcano 做调度,弹性伸缩最佳,但要求作业容器化改造能力。

维度 队列隔离 标签隔离 K8s 混合部署
隔离层级 逻辑配额(共享全部节点) 物理节点分组(独占机器) 容器 / Pod(独占内核命名空间)
资源保障程度 中:保底容量 + 可被高优队列抢占 高:标签节点上的资源完全独占 高:Quota 限制 + 优先级抢占
弹性共享 好:队列间可借用空闲资源 中:仅标签内借用 好:节点池统一调度 + HPA 弹性
改造成本 低:只改提交参数与调度器配置 低~中:配置标签并规划节点分组 高:作业镜像化、日志与监控重建
适用场景 中小规模、批流可共存 流作业延迟敏感、硬件异构 云原生湖仓、多引擎统一平台
逻辑隔离家族(队列):所有作业共享全部节点,靠 capacity 配额 + ACL 划分份额。优点是资源利用率最高——空闲队列的容量可被繁忙队列借用;代价是隔离仅停留在"配额"层面,IO 与网络争抢依然存在。
物理隔离家族(标签 / K8s):实时与离线作业在物理节点或容器层面分而治之,隔离强度高、故障域清晰;代价是牺牲弹性共享,需要为每个分组预留容量余量,机器利用率低于逻辑隔离方案。

关键作业保障:抢占配置

当高保障队列(如 realtime)被低保障队列占用了保底容量时,YARN 抢占机制会先向被抢占队列发出通知(SIGTERM),超过宽限期后强制回收(SIGKILL)。核心配置:

<!-- 全局开启抢占(capacity-scheduler.xml) -->
<property>
  <name>yarn.scheduler.capacity.preemption.enabled</name>
  <value>true</value>
</property>
<property>
  <name>yarn.scheduler.capacity.preemption.monitor_interval</name>
  <value>15s</value>
  <!-- 抢占监测周期 -->
</property>
<property>
  <name>yarn.scheduler.capacity.preemption.max_wait_before_kill</name>
  <value>15000</value>
  <!-- 被抢占 Container 收到通知后的自行退出宽限期(ms) -->
</property>
<property>
  <name>yarn.scheduler.capacity.preemption.total_preemption_per_round</name>
  <value>0.2</value>
  <!-- 每轮最多回收全集群 20% 资源,避免"抢占风暴" -->
</property>

<!-- 队列级开关:<queue-path> 替换为实际队列路径 -->
<property>
  <name>yarn.scheduler.capacity.<queue-path>.disable_preemption</name>
  <value>false</value>
  <!-- false = 本队列允许被高保障队列抢占;关键队列(如 realtime)设 true 免被抢 -->
</property>
抢占实践:只让"关键队列免抢 + 离线队列可被抢"单向生效;抢占宽限期给足 15 秒以上,让 Spark/Flink 收到停止信号后完成日志落盘;每次扩容或改配后用一次压测验证抢占路径真实可用。
风险提示:流作业被抢占会导致 Checkpoint 中断甚至状态回退,凡是带状态的常驻作业所在队列都应设置 disable_preemption=true;同时避免两个关键队列互相抢占形成"乒乓",抢占只作为最后防线,日常靠容量规划避免触达。

调度与湖仓的衔接

湖仓一体引入开放表格式后,表的维护作业(Compaction、快照过期、Manifest 合并)必须像业务作业一样被调度化、被监控——它们不跑,表的读写性能就会慢性劣化。这正是调度层与存储层的衔接点。

1. Compaction 作业调度化

以 Iceberg 为例,三个维护性存储过程建议固定在日批次末尾执行(凌晨低峰):

-- 1) 合并小数据文件:binpack 策略,目标 512MB / 文件
CALL lakehouse.system.rewrite_data_files(
  table    => 'lakehouse.dws_orders_day',
  strategy => 'binpack',
  options  => map('target-file-size-bytes', '536870912')
);

-- 2) 合并 manifest 元数据文件,降低查询计划阶段的扫描量
CALL lakehouse.system.rewrite_manifests(
  table => 'lakehouse.dws_orders_day'
);

-- 3) 过期快照:保留最近 7 天与最后 100 个快照
--    $[yyyy-MM-dd-7] 为 DS 内置时间参数,自动替换为 7 天前日期
CALL lakehouse.system.expire_snapshots(
  table       => 'lakehouse.dws_orders_day',
  older_than  => TIMESTAMP '$[yyyy-MM-dd-7] 00:00:00',
  retain_last => 100
);

在 DolphinScheduler 中落地为 SHELL 任务(便于参数化与失败告警),接入日批次 DAG:

#!/bin/bash
# DS SHELL 任务:Iceberg 表维护(快照过期 + 小文件合并)
set -euo pipefail

TABLE="lakehouse.dws_orders_day"
EXPIRE_DT="$[yyyy-MM-dd-7]"       # 保留 7 天快照

spark-sql \
  --master yarn \
  --queue lakehouse_offline \
  --name "iceberg_maintain_${TABLE}" \
  --num-executors 10 \
  --executor-cores 4 \
  --executor-memory 8g \
  --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
  --conf spark.sql.catalog.lakehouse=org.apache.iceberg.spark.SparkCatalog \
  --conf spark.sql.catalog.lakehouse.type=hive \
  -e "CALL lakehouse.system.expire_snapshots(table => '${TABLE}', older_than => TIMESTAMP '${EXPIRE_DT} 00:00:00', retain_last => 100); \
      CALL lakehouse.system.rewrite_data_files(table => '${TABLE}', strategy => 'binpack');"
Hudi 的对应做法:Hudi 表常以 schedule_compact + run_compaction 两个步骤调度(0.12+ 也支持内联自动合并),Delta 则用 OPTIMIZE table_name 与 VACUUM 两条 SQL——思路一致:把存储过程当成"受调度管理的一等公民作业"。

2. 数据就绪:事件驱动 vs 纯时间驱动

维度 时间驱动(cron) 事件驱动(就绪触发)
触发方式 固定时刻无条件触发 上游完成事件 / _SUCCESS 标记 / 分区探测
上游晚到 空跑或脏跑,需事后补数 自动顺延,数据到即处理
链路延迟 调度间隔即延迟下限 接近理论最小延迟
可预测性 高,便于运维排班 低,负载毛刺时刻不可控
实现复杂度 低,一条 cron 即可 中:需事件总线或探测任务兜底
适用场景 稳定日批、报表链路 上游到达时间波动大的多源依赖链
推荐混合模式:cron 兜底 + 就绪探测前置任务。凌晨 02:00 启动探测任务,最长等待 60 分钟;上游提前到达则立即放行(拿到更低延迟),超时则失败告警人工介入(保住可预测性)。
纯事件驱动的坑:事件丢失(Kafka 消费异常)或乱序(多分区到达顺序不一)会造成下游静默等待。设计上必须保证探测/事件消费幂等,并为"事件超时未到"配置显式告警,而不是无限等待。

调优要点

1. 作业错峰

凌晨批次的失败大头不是代码,而是"峰值叠加"——多个大任务同一分钟提交,队列瞬间打满。错峰排布示例:

时刻 作业 队列 峰值申请
02:00 上游同步 + 就绪探测 realtime / ops 小(探测类)
02:10 ODS → DWD 清洗 offline 200 vcore / 1.2TB
02:50 DWD → DWS 聚合 offline 400 vcore / 2.4TB
04:30 Compaction / 快照过期 offline 100 vcore / 800GB
05:30 质量校验 + 指标产出 offline 小(校验类)

经验法则:同一时刻并行任务的峰值申请之和,不应超过 offline 队列 maximum-capacity 对应容量的 80%,剩余 20% 留给重试与临时查询。

2. 大任务拆分

3. Worker 容量

# DS 3.x application.yaml 关键字段(不同版本命名略有差异,以部署文档为准)
worker:
  exec-threads: 100          # Worker 并发处理命令的线程数
  exec-task-num: 20          # 单 Worker 同时执行任务数上限
  heartbeat-interval: 10s    # 心跳周期,Master 依此判定存活
# Worker 容量经验公式
安全并发任务数 = floor((Worker 物理内存 − 4GB 预留) ÷ 单任务客户端峰值内存)
示例:64GB Worker,spark-submit 客户端峰值约 2GB → 并发 ≤ 30,exec-task-num 取 24 留 20% 余量
过载信号:DS 任务"提交成功但长时间无日志输出"、Worker 端任务队列等待时间 > 2 × 任务历史 P50 时长

4. YARN 内存与 vcore 比例

负载类型 memory : vcore 说明
Spark 重 shuffle 批处理 6 ~ 8GB : 1 排序、聚合、内存压缩开销大,vcore 给多也跑不动
Spark 常规批 / Hive 4 ~ 6GB : 1 湖仓日批的主力负载
Flink 流作业 2 ~ 4GB : 1 RocksDB 状态走堆外,CPU 密集度低于批任务

两个高频坑:其一,忘记 spark.executor.memoryOverhead(默认仅 executor 堆的 10%),堆外溢出导致 Container 被 YARN 杀掉(exit code 143 / 137),湖仓作业跑 Parquet 压缩时尤甚,建议显式调到 15%;其二,Container 粒度过小(如 1GB)会让集群碎片化、调度效率下降,粒度过大则排队变长,用上面的比例表对齐后不要频繁改动。

适用边界

1. 小规模团队需要 DolphinScheduler 吗?

先回答三个问题:每日作业是否少于 20 个?链路是否单链无分叉?是否没有补数与多租户诉求?如果三个都是"是",那么 crontab + 依赖探测脚本(或单机 Airflow)完全够用,引入 DS 反而背上"ZooKeeper + 元数据库 + 三类服务进程"的运维成本。

出现以下任意两条,就值得上 DS:

2. 什么时候不适合标签调度?

反模式提醒:不要为了隔离而隔离。见过不少团队在 5 台机器的测试集群上配置 3 层队列 + 2 组标签 + 抢占,结果每天在"配额调参"上花的时间比开发还多。隔离方案应与集群规模、SLA 要求同阶演进。
演进路径建议:起步用"队列隔离 + ACL"即可;当流作业延迟被批任务干扰时引入"标签隔离"把流作业独立到专属节点;当集群规模与团队规模到了需要秒级弹性时,再评估"K8s 混合部署"。每一步都由明确的痛点驱动,而非技术追新。