跳到主要内容
版本:Next

SeaTunnel 设计理念

1. 概述

本文档阐述了塑造 SeaTunnel 架构的核心设计原则、理念和权衡。理解这些原则有助于贡献者做出一致的设计决策,并帮助用户了解系统的优势和局限性。

2. 核心设计原则

2.1 引擎独立性

原则:将连接器逻辑与执行引擎解耦。

动机

  • 数据同步专用引擎 Zeta 出现之前,用户可能已有 Flink 或 Spark 集群
  • 不同引擎适用于不同场景(批处理 vs 流处理、资源约束)
  • 连接器开发人员不应需要理解多个引擎 API

实现

  • 统一的 SeaTunnel API 层抽象引擎特定细节
  • 转换层将 SeaTunnel API 适配到引擎特定 API
  • 连接器逻辑尽量与执行引擎解耦;在转换层支持的前提下,同一套连接器实现可复用到不同引擎(具体可用性以连接器能力与引擎支持为准)

权衡

  • 优点:最大化可重用性 - 复用连接器逻辑,减少引擎适配重复开发
  • 优点:更简单的连接器开发 - 只需学习单一 API
  • 缺点:无法利用引擎特定的优化
  • 缺点:额外的转换开销
  • 缓解措施:转换层轻薄且优化;大部分开销在 I/O 而非转换

示例:连接器仅实现 SeaTunnel API 的抽象(Source/Sink/Transform),不同执行引擎通过转换层完成适配;因此连接器逻辑与引擎 API 变更解耦。

2.2 协调与执行分离

原则:将控制逻辑(协调)与数据处理(执行)分离。

动机

  • 协调逻辑是单线程且轻量级的
  • 执行逻辑是并行且资源密集的
  • 容错需要为每个部分独立管理状态

实现原理

协调层(Master 侧)

  • 运行位置:主节点,维护全局视图
  • 核心职责:资源发现、工作分配、故障检测、状态协调
  • 运行特点:单线程、轻量级、不处理实际数据
  • 维护状态:分配计划、待处理工作单元、全局进度

执行层(Worker 侧)

  • 运行位置:工作节点,独立并行执行
  • 核心职责:本地数据处理、进度汇报、参与检查点
  • 运行特点:多线程、资源密集、处理大量数据
  • 维护状态:本地处理进度、缓冲数据、执行上下文

通信机制

  • 协调层 → 执行层:通过事件分发工作(如:分配新的数据分片)
  • 执行层 → 协调层:通过消息汇报进度(如:完成分片、请求新工作)
  • 检查点时:各自快照自己的状态,互不干扰

权衡

  • 优点:清晰的关注点分离
  • 优点:枚举器可以在失败时重新分配分片
  • 优点:提交器实现全局事务协调
  • 缺点:额外的通信开销
  • 缺点:连接器开发人员的 API 更复杂
  • 缓解措施:合理的默认值;简单连接器可以使用简单的枚举器/提交器

示例

  • 主节点侧:负责“发现/生成工作单元(split)+ 分配 + 回收 + 快照状态”。
  • 工作节点侧:负责“执行读取/写入 + 汇报进度 + 参与 checkpoint”。

这样设计的关键原因是:容错需要区分“控制状态”(分配/待处理 split)和“执行进度”(每个 split 的 offset/position),才能在失败后做到精准恢复与快速重分配。

2.3 基于分片的并行度

原则:将数据源划分为可独立处理的分片。

动机

  • 实现无需紧密协调的并行处理
  • 支持动态负载均衡和故障恢复
  • 提供检查点粒度(每个分片的进度)

实现

  • 数据源划分为分片(文件块、DB 分区、Kafka 分区等)
  • 枚举器延迟或急切地生成分片
  • 读取器独立处理分片
  • 未处理的分片可以在失败时重新分配

权衡

  • 优点:出色的可扩展性 - 添加工作节点以处理更多分片
  • 优点:细粒度故障恢复 - 仅需要重新处理失败的分片
  • 优点:动态负载均衡 - 将更多分片分配给空闲的工作节点
  • 缺点:某些数据源的分片生成开销
  • 缺点:需要跟踪每个分片的状态
  • 缓解措施:延迟分片生成;分片状态轻量级

示例

  • 数据库场景:split 通常表达“分片键范围/分页区间/分区”一类可独立读取的范围。
  • 文件场景:split 通常表达“文件 + 起始偏移 + 长度”或“单文件”。

这里不展示具体结构体代码,重点在于 split 的边界:必须能被独立处理、可序列化传输、可在失败后重新分配。

2.4 通过两阶段提交实现精确一次语义

原则:保证端到端精确一次数据传递。

动机

  • 数据集成不能丢失或重复数据
  • 失败可能在任何时候发生(网络、进程崩溃)
  • 外部系统需要事务保证

实现原理

两阶段提交协议将数据写入过程分为两个独立阶段:

  1. 准备阶段(Prepare Phase)

    • 时机:在检查点屏障到达时触发
    • 动作:写入端生成"可提交但未提交"的凭证(如事务 ID、临时文件路径)
    • 约束:不对外部系统产生可见副作用(数据对外不可见)
    • 状态:凭证信息随检查点一起持久化
  2. 提交阶段(Commit Phase)

    • 时机:检查点完整成功后
    • 动作:协调端使用凭证信息原子性地提交变更(如提交事务、移动文件)
    • 效果:数据对外部系统可见
    • 保证:幂等性,重复提交不产生副作用
  3. 中止处理(Abort Handling)

    • 时机:检查点失败或超时
    • 动作:清理准备阶段产生的临时资源(如回滚事务、删除临时文件)
    • 效果:保证不会产生部分写入或不一致状态

权衡

  • 优点:强一致性保证
  • 优点:自动从失败中恢复
  • 缺点:需要数据 Sink 中的事务支持(或幂等操作)
  • 缺点:增加延迟(数据仅在提交后可见)
  • 缺点:提交信息的额外状态
  • 缓解措施:可选特性;非事务性数据 Sink 可使用至少一次模式

示例:典型的 Exactly-Once 落地方式是“写入端先生成可提交凭证(commit info),checkpoint 成功后再由协调端执行最终提交”。这样做的原因是:把副作用(对外部系统的可见变更)延后到 checkpoint 成功之后,避免失败重启时产生重复可见写入。

2.5 模式作为一等公民

原则:将模式视为通过管道传播的显式、类型化的元数据。

动机

  • 数据集成需要模式转换和验证
  • 模式演化(DDL 变更)必须显式处理
  • 类型不匹配应该尽早捕获

实现

  • CatalogTable 封装完整的表元数据
  • TableSchema 定义结构(列、主键、约束)
  • 模式通过数据源 → 转换 → 数据 Sink 传播
  • SchemaChangeEvent 表示 DDL 变更(ADD/DROP/MODIFY 列)

权衡

  • 优点:类型安全 - 在作业提交时验证模式
  • 优点:模式演化 - 在运行时处理 DDL 变更
  • 优点:更好的错误消息 - 尽早检测模式不匹配
  • 缺点:无模式数据源的额外复杂性
  • 缺点:某些数据源的模式发现开销
  • 缓解措施:模式推断助手;可选的模式覆盖

示例:数据源产出“显式模式”(列、主键、约束、分区、选项等),转换对模式进行验证与映射,数据 Sink 在接收端再次校验。这样做的原因是:把“类型不匹配/缺列/主键冲突”等问题尽早暴露在提交阶段,而不是让它们在运行时以隐式的脏数据形式出现。

2.6 具有类加载器隔离的插件架构

原则:连接器是动态加载的插件,具有隔离的依赖。

动机

  • 避免依赖冲突(例如,多个 JDBC 驱动程序版本)
  • 实现热插拔连接器,无需重新构建核心
  • 减少核心分发大小

实现

  • 用于连接器发现的 Java SPI
  • 每个连接器具有隔离的类加载器
  • 遮蔽插件依赖以避免冲突
  • 用于实例化的工厂模式

权衡

  • 优点:依赖隔离 - 无版本冲突
  • 优点:更小的核心分发
  • 优点:易于添加第三方连接器
  • 缺点:类加载器复杂性
  • 缺点:某些共享库(如 Guava)可能存在问题
  • 缓解措施:谨慎遮蔽;核心中的共享通用库

示例

seatunnel-engine/lib/              # 核心库
connector-jdbc/lib/ # JDBC 驱动程序(隔离)
connector-kafka/lib/ # Kafka 客户端(隔离)

# 每个连接器由单独的 ClassLoader 加载
ConnectorClassLoader(connector-jdbc) -> 加载 mysql-connector-java-8.0.26.jar
ConnectorClassLoader(connector-kafka) -> 加载 kafka-clients-3.0.0.jar

2.7 具有检查点存储抽象的状态管理

原则:将状态管理与存储实现解耦。

动机

  • 不同部署需要不同的存储(HDFS、S3、本地、OSS)
  • 状态大小差异很大(KB 到 TB)
  • 存储耐久性和性能要求不同

实现

  • 可插拔 checkpoint storage(例如 localfile/hdfs 等,取决于插件与配置)
  • 状态的可插拔序列化
  • 增量检查点支持
  • 自动状态清理

权衡

  • 优点:灵活性 - 根据部署选择存储
  • 优点:增量检查点减少开销
  • 缺点:存储性能影响检查点延迟
  • 缺点:生产环境需要分布式文件系统
  • 缓解措施:异步检查点上传;可配置间隔

2.8 多表同步

原则:支持在单个作业中同步多个表。

动机

  • 数据库迁移通常涉及数百个表
  • 为每个表创建一个作业浪费资源
  • 模式演化必须应用于所有表

实现

  • MultiTableSource / MultiTableSink 包装单个表数据源/Sink
  • TablePath 将记录路由到正确的表
  • 按表传播模式变更
  • 支持副本以提高吞吐量

权衡

  • 优点:资源效率 - 一个作业而不是数百个
  • 优点:跨表一致快照
  • 优点:集中监控
  • 缺点:一个表失败可能影响其他表
  • 缺点:更复杂的错误处理
  • 缓解措施:可配置的错误容忍度;按表的指标

3. 架构权衡

3.1 简单性 vs 性能

选择:优先考虑简单性和正确性而非极端性能优化。

理由

  • 数据集成是 I/O 密集型的,而非 CPU 密集型
  • 正确的语义(精确一次)比原始速度更关键
  • 简单的代码易于维护和调试

证据

  • 网络和磁盘 I/O 主导处理时间(> 90%)
  • 转换层开销可以忽略不计(< 1%)
  • 代码可读性优先(例如,清晰的状态机,无微观优化)

3.2 灵活性 vs 易用性

选择:提供合理的默认值,同时允许高级定制。

理由

  • 大多数用户想要简单的配置
  • 高级用户需要细粒度控制
  • 两种需求可以通过分层 API 满足

实现

  • 常见情况的高级配置(例如,jdbc://host:port/db
  • 专家的低级选项(例如,连接池调优)
  • 合理的默认值(并行度、检查点间隔、缓冲区大小)

3.3 通用性 vs 专业化

选择:通用 API 与专业化实现。

理由

  • 统一的 API 简化了学习和使用
  • 不同的数据源具有独特的特征(有界 vs 无界、可分片性)
  • 专业化发生在连接器实现中,而非 API 中

示例

  • SourceSplitEnumerator 足够通用,可用于文件、数据库和消息队列
  • 文件连接器使用基于文件的分片
  • Kafka 连接器使用基于分区的分片
  • JDBC 连接器使用基于查询的分片

3.4 强一致性 vs 延迟

选择:提供精确一次(高延迟)和至少一次(低延迟)模式。

理由

  • 某些应用需要强一致性(金融、计费)
  • 其他应用可以容忍重复以获得更低延迟(日志、指标)
  • 让用户根据需求选择

配置

env {
checkpoint.mode = "EXACTLY_ONCE" # 或 "AT_LEAST_ONCE"
checkpoint.interval = 60000 # 毫秒
}

4. 从 V1 到 V2 的演进

4.1 V1 的局限性

SeaTunnel V1(2.3.0 之前)存在重大架构局限性:

  1. 引擎特定连接器:Spark 和 Flink 的单独实现
  2. 无统一 API:无抽象层,与引擎紧密耦合
  3. 有限的容错:完全依赖引擎检查点
  4. 无模式管理:模式隐式,无演化支持
  5. 仅单表:不支持多表同步

4.2 V2 改进

SeaTunnel V2(2.3.0+)重新设计了架构:

方面V1V2
API引擎特定统一的 SeaTunnel API
连接器重复代码单一实现
容错依赖引擎显式检查点协议
模式隐式显式 CatalogTable
多表不支持原生支持
引擎支持Spark、FlinkSpark、Flink、Zeta
精确一次部分端到端 2PC

4.3 迁移路径

V1 和 V2 连接器共存但使用不同的 API:

  • V1 连接器:seatunnel-connectors/(已弃用)
  • V2 连接器:seatunnel-connectors-v2/(推荐)

V2 是未来;V1 处于维护模式。

5. 关键设计决策

5.1 为什么分离枚举器和读取器?

替代方案:单个组件同时处理分片生成和读取。

决策:分离组件。

理由

  • 分片生成是协调逻辑(应在主节点上运行)
  • 数据读取是执行逻辑(应在工作节点上运行)
  • 一方的失败不应影响另一方
  • 允许在不重启读取器的情况下重新分配分片

5.2 为什么三级数据 Sink 提交(写入器 → 提交器 → 聚合提交器)?

替代方案:两级(写入器 → 提交器)或直接写入器提交。

决策:可选的三级提交。

理由

  • 写入器:并行、有状态、每个任务
  • 提交器:并行、无状态、聚合每个写入器的提交
  • 聚合提交器:单线程、有状态、全局协调器

许多数据 Sink 只需要写入器 + 提交器;聚合提交器用于复杂情况(例如,需要单一全局操作的 Hive 表提交)。

5.3 为什么 LogicalDag → PhysicalPlan 分离?

替代方案:直接从配置生成物理执行计划。

决策:两阶段规划。

理由

  • LogicalDag 表示用户意图(可移植、引擎独立)
  • PhysicalPlan 表示执行策略(引擎特定、优化)
  • 分离实现:
    • 跨引擎可移植性(相同的 LogicalDag,不同的 PhysicalPlan)
    • 优化传递(融合、分片重新分配)
    • 测试(单独验证逻辑计划)

5.4 为什么基于管道的执行?

替代方案:单一全局任务图。

决策:作业划分为管道。

理由

  • 每个管道独立的检查点协调
  • 更清晰的失败边界
  • 更容易推理数据流
  • 支持复杂的 DAG(多个数据源/Sink )

5.5 为什么不使用引擎原生检查点?

替代方案:完全依赖 Flink/Spark 检查点机制。

决策:显式 SeaTunnel 检查点协议。

理由

  • 引擎独立性 - 需要跨引擎的一致语义
  • Zeta 引擎否则将没有检查点
  • 更多对精确一次语义的控制
  • 统一的监控和可观测性

但是,对于 Flink 转换,SeaTunnel 检查点与 Flink 检查点对齐以避免重复。

6. 未来方向

6.1 计划增强

  • 动态扩缩容:在作业执行期间添加/移除工作节点
  • 自适应批量大小:根据吞吐量自动调整批量大小
  • 查询下推:将过滤器/投影下推到数据源
  • 向量化执行:处理批量行(列式)
  • 推测执行:缓解掉队者

6.2 研究方向

  • 机器学习集成:基于 ML 的优化(分片大小、并行度)
  • 统一批处理和流处理:真正的统一处理模型
  • 全局查询优化:跨管道优化

7. 经验教训

7.1 成功之处

  1. 引擎独立性:通过成功添加 Zeta 引擎而无需 API 更改得到验证
  2. 基于分片的并行度:扩展到 1000+ 并行任务
  3. 显式模式:尽早捕获许多错误,实现模式演化
  4. 两阶段提交:可靠的精确一次语义

7.2 可以改进之处

  1. API 复杂性:枚举器/提交器增加了简单连接器的学习曲线
  2. 类加载器问题:遮蔽依赖偶尔冲突
  3. 检查点延迟:大状态导致检查点延迟
  4. 文档差距:架构文档落后于代码

7.3 如果重新开始

  1. 简化 API:为简单的数据源/Sink 提供更高级的抽象
  2. 异步 I/O 支持:非阻塞连接器的一等异步 API
  3. 内置指标:API 中的标准化指标收集
  4. 模式注册表集成:与外部模式注册表更紧密的集成

8. 结论

SeaTunnel 的架构反映了竞争关注点之间的仔细权衡:

  • 引擎独立性 vs 引擎特定优化
  • 简单性 vs 灵活性
  • 一致性 vs 延迟
  • 通用性 vs 专业化

V2 重新设计解决了 V1 的主要局限性,同时建立了长期演进的原则。理解这些设计理念有助于贡献者做出一致的决策,并帮助用户了解 SeaTunnel 的优势和适用场景。

9. 参考资料

学术论文