第 8 篇|Apache DolphinScheduler 与 Flink Spark 数据引擎的边界、协同与最佳实践
在数据平台不断演进的过程中,一个非常常见但又隐蔽的误区是:团队会不自觉地让调度系统承担越来越多“本不属于它”的职责,比如在调度层写复杂业务逻辑、控制计算参数,甚至试图统一管理不同计算引擎的执行细节。短期来看似乎提升了效率,但从长期来看,这种设计往往会让系统变得高度耦合、难以维护,甚至在规模上来之后失去稳定性。
因此,在讨论具体实践之前,我们必须先把一件事情讲清楚:调度系统与数据引擎之间的边界。
调度系统与数据引擎的职责边界
要理解整个体系的运行方式,可以先记住一个非常核心的原则:调度系统只负责“什么时候运行”和“依赖关系”,而具体“如何计算”,必须交给 Spark、Flink 或 SeaTunnel 这样的执行引擎。换句话说,DolphinScheduler 是流程的编排者,而不是计算的执行者。
从工程视角来看,这种分工可以用一张表清晰表达:
| 组件 | 核心职责 |
|---|---|
| DolphinScheduler | DAG 编排、任务调度、依赖管理、失败重试 |
| Spark | 离线批处理计算 |
| Flink | 实时流处理 |
| SeaTunnel | 数据集成(批处理 / 流处理 / CDC) |
在实际开发中,这条边界最容易被打破的地方,往往是 Shell 任务。很多人习惯在一个节点里写复杂的分支逻辑,例如根据日期决定执行不同的 Spark 作业:
if [ "$day" == "2026-04-01" ]; then
spark-submit job_a.py
else
spark-submit job_b.py
fi
这种写法虽然“能跑”,但会带来三个问题:首先,逻辑被隐藏在脚本中,DAG 无法感知;其次,依赖关系不再清晰,影响调度系统的可视化能力;最后,后期运维和排障成本会显著增加。
而更合理的方式,是将分支逻辑显式建模在工作流中,通过条件节点来控制执行路径,让整个流程在界面上是可见、可控的。
批处理、流处理与 CDC 的调度差异
当职责边界清晰之后,再来看不同类型任务的调度方式,就会发现它们在本质上是三种完全不同的模式,而不是同一种调度逻辑的简单变体。
首先是批处理任务,这是最符合传统调度模型的一类场景,例如数仓中的 T+1 任务或按小时运行的聚合计算。这类任务有明确的时间窗口,并且上下游依赖关系清晰,非常适合通过 DAG 来表达。
在实践中,通常会按照 ODS、DWD、DWS 进行分层拆分,每一层对应一个或多个独立任务,并通过参数(如 ${biz_date})进行驱动。例如一个典型的 Spark 提交方式如下:
spark-submit \
--class com.example.ETLJob \
--master yarn \
--deploy-mode cluster \
etl-job.jar \
--date ${biz_date}
在这个过程中,调度系统的职责是串联任务关系、控制执行顺序以及处理失败重试,而不应该深入到具体的计算逻辑中。
与批处理形成鲜明对比的是流处理任务。流处理的本质是“持续运行”,而不是“被周期性触发”。如果用调度系统每隔几分钟去启动一次 Flink 作业,本质上是在用错误的方式解决问题。
一个设计合理的流任务,应该依赖 Flink 自身的状态管理和 checkpoint 机制长期运行,而 DolphinScheduler 在这里扮演的角色,更像是一个“守护者”,负责初次启动、状态检测以及异常恢复,而不是频繁干预。
进一步来看 CDC 场景,它本质上也是流处理的一种,但更偏向于数据集成,这正是 SeaTunnel 的典型应用场景。通过 SeaTunnel,可以非常方便地实现数据库到消息队列的实时同步,例如从 MySQL 到 Kafka:
env {
execution.parallelism = 2
}
source {
MySQL-CDC {
hostname = "localhost"
port = 3306
username = "root"
password = "123456"
database-names = ["test_db"]
table-names = ["test_db.user"]
}
}
sink {
Kafka {
topic = "user_cdc"
bootstrap.servers = "localhost:9092"
}
}
对应的启动命令如下:
./bin/seatunnel.sh \
--config config/mysql_cdc.conf \
-e local
在调度层面,CDC 与流处理的原则是一致的:只启动一次,长期运行,并通过状态检测机制保证稳定性,而不是通过周期调度反复触发。从这个角度来看,批处理、流处理和 CDC 的核心区别,其实在于“是否需要被重复调度”。
为什么调度系统不应该侵入计算引擎
当系统规模逐渐扩大之后,一个更深层的问题就会浮现出来:为什么我们反复强调调度系统要保持“克制”? 原因在于,一旦调度系统开始侵入计算引擎的职责范围,整个架构的可控性就会迅速下降。
例如,在调度脚本中直接写入 Spark 的资源参数:
spark-submit \
--executor-memory 8G \
--conf spark.sql.shuffle.partitions=500 \
job.sql
这种做法的问题在于,它把执行层的配置硬编码进了调度层,使得参数管理分散且难以统一。一旦需要调整资源配置,就必须修改调度任务,甚至重新发布工作流。而更合理的方式,是将这些参数放在 Spark 配置中心或作业内部管理,让 DolphinScheduler 只负责触发执行:
spark-submit job.sql
这种解耦方式可以显著提升系统的可维护性,使每一层都专注于自己的职责。
从整体架构来看,一个成熟的数据平台通常可以抽象为三层结构:最上层是以 DolphinScheduler 为代表的调度层,负责流程编排;中间是以 Spark、Flink、SeaTunnel 为核心的执行层,负责具体计算与数据处理;最底层是 YARN 或 Kubernetes 这样的资源层,负责资源分配与隔离。只有当这三层边界清晰时,系统才能在复杂度提升的同时保持稳定性。

一个融合 SeaTunnel 的实战架构示例
在真实生产环境中,这种分层思想通常会体现在完整的数据链路中。例如,可以通过 SeaTunnel 实现 MySQL 到 Kafka 的 CDC,同步实时数据;随后由 Flink 进行实时计算,产出在线指标;同时将数据落地到存储系统,再由 Spark 完成离线数仓加工。在这个过程中,DolphinScheduler 负责统一编排这些任务,包括启动 CDC、监控流任务以及调度离线计算。

从流程上看,可以抽象为一条清晰的数据链路:数据从源端进入,通过 SeaTunnel 进入实时通道,由 Flink 处理后服务在线系统,同时落地到存储,再由 Spark 完成分层加工,而 DolphinScheduler 始终作为“中枢”,协调各个环节的执行顺序与依赖关系。
总结:让系统回归“各司其职”
当我们回到最初的问题,其实可以用一句话总结整个体系的设计原则:DolphinScheduler 是“大脑”,而 Spark、Flink、SeaTunnel 是“肌肉”。调度系统负责决策与编排,而执行引擎负责具体计算与处理。
在实际落地中,可以进一步归纳为三条简单但非常关键的原则:第一,所有流程逻辑必须体现在 DAG 中,而不是隐藏在脚本里;第二,所有计算逻辑必须下沉到执行引擎内部,避免调度层膨胀;第三,流处理和 CDC 任务必须以“常驻运行”为前提设计,而不是按批处理思路调度。
当这三点被严格执行之后,数据平台就能够从“能跑”逐步演进到“稳定、可扩展、可治理”,这也是从工程化走向体系化的关键一步。
浙公网安备 33010602011771号