
简介本资源是面向数据工程师与开发人员的Apache SeaTunnel配置案例源码包聚焦MySQL到HDFS、Hive到MySQL两类典型数据迁移场景适合正在学习数据集成、需要快速跑通迁移流程的中级开发者参考。压缩包共3个文件以inscode工程配置、html说明页面及gitignore忽略规则为主整体约8KB体量轻便便于直接导入环境验证。内容围绕前置目录创建、连接器jar包放置、Hadoop与Hive服务启动以及源端与目标端参数配置展开并给出两种MySQL到HDFS的配置示例帮助理解不同参数对迁移结果的影响。目前已有27人学习读者可据此掌握SeaTunnel连接器的实际用法、配置文件的组织方式与常见排错思路为后续扩展更多源端与目标端的数据同步任务提供可复用的模板与参考路径。1. 数据集成任务为什么总在“最后一公里”翻车做过数据同步的工程师大概都有类似经历源端连接正常、目标端写入也没报错但任务跑着跑着就卡死或者数据对不上账。问题往往不在数据库本身而在中间那层配置——并行度设多少、批大小怎么调、脏数据往哪丢。SeaTunnel 这类数据集成平台的价值就在于把这些“最后一公里”的细节收敛成一份可版本化、可复现的配置文件。它支持批流一体能从 MySQL、PostgreSQL、Kafka、Hive 等源端抽取数据经过转换后写入目标端整个过程靠一份 conf 文件驱动。这篇文章面向的是需要落地数据同步任务的开发者尤其是那些已经跑通过单表同步、但在多表、增量、容错场景下反复踩坑的人。下面我会用一份可运行的配置案例把参数含义、调优边界和排查路径讲清楚。2. SeaTunnel 配置文件的骨架与执行引擎选型2.1 一份最小可运行配置由哪四段组成SeaTunnel 的配置文件采用 HOCON 格式本质上是 JSON 的超集支持注释和变量引用。一份完整的同步任务配置由四段构成env段声明执行环境source段定义数据从哪来transform段做字段映射和过滤sink段指定数据往哪去。这四段缺一不可但transform在简单同步场景下可以省略。先看一份从 MySQL 同步到 MySQL 的最小配置env { # 并行度决定同时启动多少个任务线程 parallelism 2 # 任务名称便于在日志中定位 job.name mysql_to_mysql_demo # 检查点间隔单位毫秒影响容错恢复粒度 checkpoint.interval 10000 } source { Jdbc { url jdbc:mysql://127.0.0.1:3306/source_db driver com.mysql.cj.jdbc.Driver user sync_user password sync_pass # 查询语句支持 where 条件做增量 query select id, name, amount, create_time from orders where id 1000 # 结果集获取大小影响内存占用 fetch_size 1000 } } sink { Jdbc { url jdbc:mysql://127.0.0.1:3306/target_db driver com.mysql.cj.jdbc.Driver user sync_user password sync_pass # 目标表名支持 ${table_name} 变量 table orders_copy # 批量写入条数过小则频繁提交过大则内存压力高 batch_size 500 # 主键用于生成 upsert 语句 primary_keys [id] } }这段配置的逻辑很直白env里的parallelism控制并发度checkpoint.interval决定故障恢复时从哪个检查点重放。source的fetch_size是 JDBC 驱动每次从结果集拉取的记录数设太小会导致网络往返频繁设太大则单次内存占用高。sink的batch_size是攒批写入的阈值和primary_keys配合生成INSERT ... ON DUPLICATE KEY UPDATE语句。参数调整时有个经验法则parallelism不要超过源端数据库连接池上限batch_size和fetch_size保持在同一数量级checkpoint.interval在 10 秒到 60 秒之间根据数据量调整。如果任务频繁重启先看检查点是否过大导致超时。2.2 Zeta 与 Flink 引擎怎么选SeaTunnel 支持两种执行引擎自研的 Zeta 和 Apache Flink。选型不是拍脑袋要看现有技术栈和运维成本。Zeta 是 SeaTunnel 内置的引擎开箱即用不需要额外部署 Flink 集群。它的优势在于轻量适合中小规模同步任务单节点就能跑资源占用低。缺点是生态不如 Flink 成熟复杂事件处理和状态后端调优的文档相对少。Flink 引擎适合已经有 Flink 集群的团队能复用现有的资源调度和监控体系。Flink 的状态后端、检查点机制经过大规模验证在超大规模数据同步场景下更稳。代价是需要额外维护 Flink 集群配置项也更多。我一般会这样判断如果团队没有 Flink 运维经验任务规模在每天千万条以内直接用 Zeta如果已经有 Flink 集群且任务需要和现有 Flink 作业共享资源选 Flink。切换引擎只需要改启动命令配置文件本身不用动。# 使用 Zeta 引擎启动 ./bin/seatunnel.sh --config ./config/mysql_to_mysql.conf -e zeta # 使用 Flink 引擎启动 ./bin/seatunnel.sh --config ./config/mysql_to_mysql.conf -e flink-e参数指定引擎类型不传时默认走 Zeta。启动前确认SEATUNNEL_HOME环境变量已设置否则脚本找不到依赖包。2.3 多表同步用变量替换省掉重复配置实际项目里很少只同步一张表。如果每张表写一份配置维护成本会随表数量线性增长。SeaTunnel 支持在配置里用变量占位启动时通过-i参数传入实际值。source { Jdbc { url jdbc:mysql://127.0.0.1:3306/source_db driver com.mysql.cj.jdbc.Driver user sync_user password sync_pass query select * from ${table_name} where update_time ${last_sync_time} fetch_size 1000 } } sink { Jdbc { url jdbc:mysql://127.0.0.1:3306/target_db driver com.mysql.cj.jdbc.Driver user sync_user password sync_pass table ${table_name} batch_size 500 primary_keys [id] } }启动时传入变量./bin/seatunnel.sh --config ./config/multi_table.conf \ -i table_nameorders \ -i last_sync_time2024-01-01 00:00:00-i可以传多次每次覆盖一个变量。这样一份配置文件能覆盖所有结构相似的表只需要在调度脚本里循环传入表名和时间戳。注意变量值里如果有空格要用引号包住否则解析会出错。3. 增量同步与脏数据处理的配置细节3.1 基于时间戳的增量抽取怎么配才不漏数全量同步只适合首次初始化后续必须走增量。最常见的增量方案是基于时间戳字段每次只拉取update_time大于上次同步时间的记录。source { Jdbc { url jdbc:mysql://127.0.0.1:3306/source_db driver com.mysql.cj.jdbc.Driver user sync_user password sync_pass query select id, name, amount, update_time from orders where update_time ${last_sync_time} and update_time ${current_time} fetch_size 2000 # 分区字段用于并行切分 partition_column id # 分区数量和 parallelism 配合 partition_num 4 # 分区下限和上限不设则自动探测 partition_lower_bound 1 partition_upper_bound 1000000 } }这里的关键是partition_column和partition_num。SeaTunnel 会根据分区字段把查询拆成多个子查询并行执行partition_num决定拆几份。如果源表数据量大这个值设大能提升吞吐但要注意源库的 CPU 和 IO 承受能力。时间戳增量有个经典坑如果源端有事务在同步窗口内提交但update_time落在窗口边界上可能漏掉。解决办法是把窗口右边界设为current_time减去一个安全间隔比如 5 秒或者改用数据库的 binlog 方式。我一般会在调度脚本里把last_sync_time存到一张元数据表每次任务成功后更新失败则不更新保证至少一次语义。3.2 脏数据不让任务挂掉容错配置与日志定位同步过程中遇到类型转换失败、字段超长、主键冲突是常事。默认情况下 SeaTunnel 会直接抛异常终止任务这在生产环境不可接受。需要开启容错并指定脏数据落盘路径。env { parallelism 2 job.name mysql_to_mysql_with_dirty checkpoint.interval 10000 # 开启容错 job.retry true # 重试次数 job.retry.times 3 # 重试间隔单位毫秒 job.retry.interval 5000 } sink { Jdbc { url jdbc:mysql://127.0.0.1:3306/target_db driver com.mysql.cj.jdbc.Driver user sync_user password sync_pass table orders_copy batch_size 500 primary_keys [id] # 脏数据策略跳过并记录 data_save_mode APPEND_DATA # 脏数据落盘路径 custom_sql # 支持的最大脏数据比例超过则任务失败 # 这里设为 0.1 表示 10% # 具体参数名以实际版本为准 } }脏数据会以文件形式写到seatunnel-engine的日志目录下文件名包含任务 ID 和时间戳。排查时先看日志里的DirtyData关键字定位到具体记录再回源查为什么类型不匹配。常见原因有源端字段是varchar但目标端是int源端有null但目标端非空约束源端时间格式和目标端不一致。注意脏数据比例阈值不要设太高否则大量数据被跳过而任务显示成功对账时才发现问题。我一般设在 1% 以内超过就人工介入。3.3 批量写入的 batch_size 与 checkpoint 怎么配合batch_size和checkpoint.interval是一对需要联调的参数。batch_size决定攒多少条写一次checkpoint.interval决定多久做一次状态快照。如果batch_size很大而检查点间隔很短可能出现检查点触发时还有大批数据在内存里没写出去导致检查点超时。env { parallelism 4 checkpoint.interval 30000 # 检查点超时时间单位毫秒 checkpoint.timeout 60000 # 两次检查点之间至少间隔多少毫秒 checkpoint.min-pause 5000 } sink { Jdbc { batch_size 1000 # 批量提交超时单位毫秒超时则强制提交 batch_interval_ms 10000 } }调参思路是先估算每秒写入条数乘以checkpoint.interval得到单个检查点周期内需要处理的数据量再除以parallelism得到每个并行子任务的处理量。batch_size应该小于这个值否则检查点触发时数据还没写完。batch_interval_ms是兜底防止数据量小的时候长时间不提交。如果任务报Checkpoint expired错误优先调大checkpoint.timeout或调小batch_size。如果报Too many open files检查parallelism是否超过了数据库连接数上限。4. 避坑与常见问题排查4.1 任务启动即报 ClassNotFoundException现象执行启动命令后立刻退出日志里出现ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因SeaTunnel 的lib目录下没有放对应数据库的 JDBC 驱动包。SeaTunnel 本身不内置各数据库驱动需要手动放入。解决从数据库厂商官网下载对应版本的驱动 jar放到SEATUNNEL_HOME/lib目录下。注意驱动版本要和数据库服务端版本匹配MySQL 8.x 用mysql-connector-j-8.x.jarMySQL 5.7 用mysql-connector-java-5.1.x.jar。放好后重启任务不需要重启整个 SeaTunnel 服务。4.2 同步到一半卡住不动日志无报错现象任务运行一段时间后不再输出进度CPU 和内存占用正常但目标端没有新数据写入。原因通常是源端查询被锁或者目标端写入被阻塞。源端如果有长事务持有锁JDBC 查询会一直等待。目标端如果批量写入时遇到行锁冲突也会卡住。解决先在源库执行show processlist看是否有长时间运行的查询在目标库看是否有锁等待。如果是源端锁考虑把查询拆小或者避开业务高峰。如果是目标端锁检查primary_keys是否设置正确避免全表扫描导致的锁升级。另外检查fetch_size是否过大导致单次网络传输超时。4.3 增量同步重复写入相同数据现象目标端出现重复记录或者update_time没有推进每次同步都拉取相同范围。原因last_sync_time没有正确更新或者源端update_time字段在数据更新时没有刷新。还有一种可能是任务失败后重试但检查点没有回滚导致部分数据重复写入。解决确认调度脚本在任务成功后才更新last_sync_time。检查源端表是否有ON UPDATE CURRENT_TIMESTAMP约束如果没有需要在业务代码里显式更新。对于重复写入在sink端设置primary_keys并启用 upsert 模式让数据库做主键去重。如果目标端不支持 upsert可以在写入前用transform做去重但会增加内存开销。4.4 并行度调高后目标端连接被打满现象把parallelism从 2 调到 8 后目标数据库报Too many connections其他业务受影响。原因每个并行子任务会独立创建数据库连接parallelism等于 8 时至少创建 8 个连接加上源端连接和检查点连接实际连接数可能是并行度的两到三倍。解决先查目标库的max_connections配置确保 SeaTunnel 用的连接数不超过总上限的 30%。如果不够要么调低parallelism要么在 JDBC URL 里加连接池参数复用连接。我一般会在配置里显式限制每个子任务的连接数而不是依赖默认值。source { Jdbc { url jdbc:mysql://127.0.0.1:3306/source_db?useSSLfalseserverTimezoneUTCuseUnicodetruecharacterEncodingutf8 # 连接超时单位毫秒 connection_check_timeout_sec 30 # 最大重试次数 max_retries 3 } }4.5 时间字段时区不一致导致数据错乱现象源端create_time是2024-01-01 08:00:00同步到目标端变成2024-01-01 00:00:00差了 8 小时。原因源端数据库时区是Asia/Shanghai目标端是UTCJDBC 驱动在读取和写入时做了时区转换。解决在 JDBC URL 里统一指定serverTimezone源端和目标端保持一致。如果无法改数据库配置可以在transform里对时间字段做偏移。更稳妥的做法是在同步前确认两端时区并在配置里显式声明。source { Jdbc { url jdbc:mysql://127.0.0.1:3306/source_db?serverTimezoneAsia/Shanghai } } sink { Jdbc { url jdbc:mysql://127.0.0.1:3306/target_db?serverTimezoneAsia/Shanghai } }5. 用 transform 做字段映射与轻量清洗5.1 字段改名、类型转换和常量补充transform段是 SeaTunnel 里最容易被低估的部分。很多人只把它当字段映射用实际上它还能做类型转换、常量补充、条件过滤。下面这份配置把源端的amount从字符串转成数字给目标端补一个sync_time常量并把name字段改名为customer_name。transform { # 字段映射和类型转换 Sql { # 源表别名对应 source 段的查询结果 source_table_name orders # 目标表别名供 sink 段引用 result_table_name orders_transformed # SQL 语句支持大部分标准函数 query select id, name as customer_name, cast(amount as decimal(10,2)) as amount, create_time, now() as sync_time from orders where amount is not null } }source_table_name要和source段里定义的名称一致result_table_name是转换后的虚拟表名sink段通过这个名称引用转换结果。SQL 里可以用cast、now()、ifnull等函数但不要用太复杂的子查询否则会拖慢整体速度。5.2 用 SQL transform 做字段脱敏和过滤脱敏是数据同步里的常见需求。比如手机号只保留后四位邮箱只保留域名。这些都可以在transform里用 SQL 函数完成。transform { Sql { source_table_name users result_table_name users_masked query select id, concat(****, substring(phone, -4)) as phone_masked, substring(email, instr(email, )) as email_domain, case when age 18 then minor when age between 18 and 60 then adult else senior end as age_group from users where status active } }substring(phone, -4)取手机号后四位instr(email, )找邮箱里的位置case when做条件分组。这些函数在 SeaTunnel 的 SQL transform 里都支持底层用的是 Calcite 解析器。注意不同数据库的函数名可能有差异写之前先确认 SeaTunnel 支持的函数列表。5.3 transform 链式串联的注意事项多个transform可以串联前一个的result_table_name作为后一个的source_table_name。但链式串联会增加数据在内存里的驻留时间如果中间结果集很大容易 OOM。transform { Sql { source_table_name orders result_table_name orders_filtered query select * from orders where amount 100 } Sql { source_table_name orders_filtered result_table_name orders_final query select id, amount * 1.1 as amount_with_tax from orders_filtered } }我一般会把过滤条件尽量下推到source段的query里让数据库先过滤再传输减少网络和内存开销。transform只做数据库不擅长的轻量清洗比如常量补充和简单脱敏。如果transform里用了聚合函数注意并行度会影响聚合结果的正确性需要配合partition或group by使用。提示transform里的 SQL 语法和标准 SQL 有细微差异写完后先用小批量数据跑一遍确认字段类型和预期一致再上生产。5.4 用 transform 做多表合并的字段对齐当源端有多张结构相似的表需要合并写入同一张目标表时transform可以做字段对齐。比如orders_2023和orders_2024都有id、amount、create_time但字段顺序不同可以用union合并。source { Jdbc { url jdbc:mysql://127.0.0.1:3306/source_db query select id, amount, create_time, 2023 as year from orders_2023 union all select id, amount, create_time, 2024 as year from orders_2024 result_table_name orders_union } } transform { Sql { source_table_name orders_union result_table_name orders_aligned query select id, cast(amount as decimal(12,2)) as amount, create_time, year from orders_union } }union all在源端数据库执行比在 SeaTunnel 里合并效率高。transform只做类型统一确保写入目标端时字段类型匹配。如果源端不支持union可以拆成两个source用result_table_name区分后在transform里合并但这样会占用更多内存。6. 生产环境下的调度与监控配置6.1 用调度工具传参实现每日增量生产环境不会手动执行同步命令通常用调度工具如 DolphinScheduler、Airflow定时触发。核心是把last_sync_time作为参数传入任务成功后更新元数据表。#!/bin/bash # 从元数据表读取上次同步时间 LAST_SYNC$(mysql -h127.0.0.1 -usync_user -psync_pass -N -e select last_sync_time from meta.sync_log where table_nameorders) # 计算当前时间窗口 CURRENT_TIME$(date %Y-%m-%d %H:%M:%S) # 执行同步任务 ./bin/seatunnel.sh --config ./config/orders_incremental.conf \ -i last_sync_time$LAST_SYNC \ -i current_time$CURRENT_TIME # 判断退出码成功则更新元数据 if [ $? -eq 0 ]; then mysql -h127.0.0.1 -usync_user -psync_pass -e update meta.sync_log set last_sync_time$CURRENT_TIME where table_nameorders else echo sync failed, skip update fi这段脚本的关键是退出码判断。SeaTunnel 任务成功返回 0失败返回非 0。只有成功才更新last_sync_time保证失败重跑时不会漏数据。current_time作为窗口右边界避免同步过程中新写入的数据被漏掉。6.2 监控任务状态和脏数据量的三个指标生产环境需要监控三个指标任务是否存活、同步延迟、脏数据量。SeaTunnel 的 Zeta 引擎提供了 REST API 查询任务状态。# 查询运行中的任务列表 curl -s http://127.0.0.1:5801/hazelcast/rest/maps/running-job # 查询指定任务的详情 curl -s http://127.0.0.1:5801/hazelcast/rest/maps/job-info/{jobId}返回的 JSON 里包含jobStatus、readCount、writeCount等字段。readCount和writeCount的差值如果持续增大说明写入跟不上读取需要调大batch_size或parallelism。脏数据量可以从日志目录下的dirty-data文件夹统计文件行数。我一般会写一个定时脚本每 5 分钟拉一次任务状态如果jobStatus不是RUNNING就告警。同时统计脏数据文件大小超过阈值就通知人工检查。这些指标比单纯的“任务是否退出”更能反映同步质量。6.3 任务失败后的恢复策略与幂等保证任务失败后重跑最怕的是重复写入。保证幂等的核心是sink端配置primary_keys并启用 upsert。如果目标端不支持 upsert可以在写入前先按主键删除但这样会有短暂的数据空洞。sink { Jdbc { url jdbc:mysql://127.0.0.1:3306/target_db table orders_copy primary_keys [id] # 支持 upsert 的数据库会自动生成 ON DUPLICATE KEY UPDATE # 不支持的需要在 custom_sql 里手写 support_upsert_by_query_primary_key_exist true # 写入前是否清空表全量同步时用 # 增量同步保持 false # is_exactly_once true } }support_upsert_by_query_primary_key_exist设为true时SeaTunnel 会先查询主键是否存在再决定插入还是更新。这会增加一次查询开销但能保证幂等。如果目标端是 MySQL直接用ON DUPLICATE KEY UPDATE更高效。如果目标端是 ClickHouse 这类不支持 upsert 的数据库需要在transform里做去重或者接受最终一致性的短暂重复。注意is_exactly_once开启后会启用两阶段提交对目标端有额外要求不是所有数据库都支持。开启前先确认目标端的事务隔离级别和连接池配置。6.4 一个可复用的配置模板结构把上面的经验固化成一个模板后续新表同步只需要改几个变量。模板结构如下配置段必填项可变量调优项envparallelism, job.namejob.namecheckpoint.interval, checkpoint.timeoutsourceurl, driver, user, password, queryquery 里的表名和时间条件fetch_size, partition_numtransformsource_table_name, result_table_name, queryquery 里的字段映射无sinkurl, driver, user, password, tabletable 名batch_size, primary_keys模板放在版本控制里每次新任务从模板复制一份只改变量部分。这样能避免每次重新写配置时漏掉容错参数。我习惯在模板里把checkpoint.interval设为 30000batch_size设为 500fetch_size设为 1000这三个值在大多数场景下不需要改。如果任务量特别大再针对性调整。这套配置方案我用了两年多最大的教训是不要等到任务失败才去看日志。每次上线新任务前先用小批量数据跑一遍确认脏数据量为零、时间字段无偏移、主键无冲突再切全量。另外last_sync_time的更新一定要放在任务成功之后这个顺序错了后面对账会非常痛苦。希望帮到你。本文还有配套的精品资源点击获取