07主干管:SeaTunnel 把水送进城目录
第 07 关同步主干管 · SeaTunnel
厂里有两根管子:一根往里抽(DataX 入湖),一根往外送(SeaTunnel 入仓)。接反了,水就倒流——湖里有、仓里没有。
厂子建好了,水也净好了,可城市那头的水龙头还是拧不出水。
因为还差最后一段管子:从厂里的清水池,把水送到城里的管网。
这一关说的是工厂区里那两根管子——一根进水、一根出水。长得像,接口不一样,装错一次,后面所有页面都喝不到水。

一、先说人话

整套同步从一开始就分成两件事,不是"一个工具干完":DataX 管进厂,SeaTunnel 管出厂。原文的说法是:DataX 负责离线入湖,SeaTunnel 负责湖内入仓。

人话就是:河在城外(业务库不归你管),厂在城里(数据湖),城市管网在更里面(Doris 仓)。第一段是"从别人的河里往自己厂里抽",第二段是"从自己厂里往自己管网里送"。两段水的性质完全不同,所以工具也分开。

管子干什么从哪 → 到哪海豚上的节点
DataX
抽水泵
入湖各子公司业务库 / 文件服务器 / 外部数据接口 → 湖(HDFS / OSS 上的 Hudi)DATAX,Worker 分组 datax
SeaTunnel
主干管
湖内入仓湖(Hudi)→ Doris;也支持其他源端和目标端SEATUNNEL,Worker 分组 seatunnel

为什么不拿一个工具干两件事?因为两头要的东西正好相反。抽水那一段,关键指标是"别把源库压死"——源库是别人的生产库,DataX 靠主从架构,对源端压力小;送水那一段,关键指标是"快",SeaTunnel 吃的是湖里那套分布式计算资源,表再大也是几台机器一起搬。

原文把这句话写成了产品优势的第一条:双引擎分工明确,各取所长,链路清晰。分开的另一个好处是不互相拖累:入湖那边在抽一条几千万行的源表,不会把出湖的管子堵住;出湖这边要重建 Doris 表结构,也不用等入湖跑完。

源接口 HUDI_GK_LINK 目标接口 DORIS_GK_SINK 湖 · Hudi 水池:原样存着 dm_gk_hudi_sit ads_log_inv_kpi_wide 湖里已经有数据了 SeaTunnel 泵:湖 → 仓 parallelism 5 job.mode=BATCH Doris 仓 货架:按表摆好 dm_gk_dws ads_log_inv_kpi_wide 页面 / 报表直连 失败 → retry_to_doris 重跑当天 厂外那段(源库 → 湖)归 DataX,见第 03 关

看图只用记三件事:左边那池水本来就有(湖里确实有数据),中间那台泵是 SeaTunnel,右边才是页面真正连的地方(Doris)。泵上方两块牌子是"接头"的名字——两块牌子不是同一根管子,装混了,泵就在往别的地方抽水。

牌子上写的是接头的大名。目标端那根按环境分名字:SIT 实跑的叫 SIT_DORIS_GK_SINK,生产是另一根,名字自己核,别照图上的字面抄。

二、拆开看:每个零件是什么

2.1 它是个"产品",不是一堆脚本

🧰 配管 App(可视化配一气呵成)

像什么:装水管最土的办法是自己在机房里写脚本、一个个参数拼;产品化的办法是给你一个面板,源、路径、目标表、周期四格填完,剩下的它自己接。

其实是什么:用户通过统一可视化界面配置源端、湖存储路径、数仓目标表及调度周期,系统自动生成串联任务;同时提供断点续传、数据校验、异常重试与全链路监控告警。原文对这套东西的总结是四个词:不丢、不重、高效、可管。

产品名
《离线数据同步解决方案》(DataX + SeaTunnel 双引擎)
监控入口
http://seatunnel.sinopharmholding.com:10001/#/overview
入口不止页面
同一个地址下还有 /submit-job,可以用 HTTP 客户端直接提交任务(大数据量取数就是这么干的)

原文那四句优势,翻成人话是这样:

原文说法人话
双引擎分工明确谁管进厂、谁管出厂写死在产品定位里,不靠人自觉。DataX 专注入湖,SeaTunnel 专注湖内 ETL 入仓
资源占用低抽水那段不压源库;送水那段用湖里已有的分布式机器,不用另买一批
模式灵活全量、增量、条件过滤都能配,业务一天一更、一月一更都接得住
高可靠性断点续传 + 自动重试 + 数据对账,原文的说法是"零丢失、零重复"
注意原文还写了两条:运维简单(拖拽式配置、不写代码,监控告警完善)和复用中台能力(用现有的调度与存储资源,不重复建设)。这两条容易被当成场面话,其实很实在——它说明这套同步没另起一套调度,跑的还是海豚那张时刻表。

2.2 一个真实节点长什么样

下面这份是从真实构建脚本(add_seatunnel.py,给工作流 187800689635488 加"07_同步宽表至Doris")里原样拿出来的配置,只调了换行和缩进,值一个没动:

{
  "env": {
    "parallelism": 5,
    "job.mode": "BATCH"
  },
  "source": [{
    "LINK": "HUDI_GK_LINK",
    "table_path":
      "dm_gk_hudi_sit.ads_log_inv_kpi_wide"
  }],
  "sink": [{
    "LINK": "SIT_DORIS_GK_SINK",
    "table_path":
      "dm_gk_dws.ads_log_inv_kpi_wide",
    "schema_save_mode": "RECREATE_SCHEMA",
    "write_mode": "overwrite"
  }]
}

看着像代码,其实是四个零件的清单:环境(几台泵、跑哪种活)、源(从哪接头取哪张表)、目标(往哪根接头灌哪张表)、灌法(表结构怎么办、数据怎么进去)。逐个说:

⚙️ parallelism 与 job.mode:开几个泵、跑哪一种活

像什么:泵房里有几台泵同时开,以及这次是"一批抽完就收工"还是"常年不停机"。

其实是什么:parallelism 是并发度,job.mode 是作业模式。这份配的是 5 和 BATCH——同时开 5 路搬,一批跑完任务就结束,不是常驻进程。

真名
parallelism = 5;job.mode = BATCH
同一个脚本里
"08_同步校验至Doris" 那张校验表的 parallelism 配的是 1——小表开 5 路只是多开 5 个连接
平台的限流
同步软件统一限制过并发数(早期并发设置不合理),最终并发是 Math.min(parallelism, parallelism_max);要真放大得另配 parallelism_max
注意写 5 不等于真的跑 5 路。平台的统一限流是 3,所以这份配置实际按 3 走;要压到更大,得配 parallelism_max,别只改前面那个数就以为加了机器。
🔌 LINK:管子接头叫什么名

像什么:厂里所有管子都不是直接插的,先装一个标准接头,接头有编号。泵只认编号,不认"大概那个位置"。

其实是什么:LINK 是平台上注册好的连接配置名——地址、账号、库都封在里面。源端这边湖的接头叫 HUDI_GK_LINK;目标端这边,SIT 用的是 SIT_DORIS_GK_SINK。

源端
HUDI_GK_LINK(湖,读)
目标端(SIT)
SIT_DORIS_GK_SINK(Doris,写)
目标端(生产)
不是 SIT 那一根;生产侧 LINK 的具体名字这一步的实际值要现场核
会出事目标端随手写成 SIT 那根 LINK,任务在海豚上照样绿,数据却全灌进了测试库。第二天业务说"今天数据没更新",你在生产库里查一小时都查不出毛病——因为根本没往这儿灌。
🗺 table_path:接头插到哪张表

像什么:接头编号对了,还得说清插到哪个池子、哪个货位。

其实是什么:库.表 写全。这条链是 dm_gk_hudi_sit.ads_log_inv_kpi_wide → dm_gk_dws.ads_log_inv_kpi_wide:湖里的宽表,搬到仓里同名同结构的表。

源
dm_gk_hudi_sit.ads_log_inv_kpi_wide
目标
dm_gk_dws.ads_log_inv_kpi_wide
另一条
ads_log_inv_kpi_check(口径校验表),同一个工作流里一起同步

目标端还是同一个 dm_gk_dws 库、同一个 ads_ 前缀——仓里的表不是另起一套名字,而是湖里那张表的同名落位,页面才知道该连哪张。

🪣 schema_save_mode 与 write_mode:表结构怎么办、水怎么灌

像什么:灌之前先决定两件事:货架要不要照源头的规格重搭一遍;还是每次把整格的水倒掉换新的,或者只往里添。

其实是什么:schema_save_mode=RECREATE_SCHEMA 是目标表结构照源端重建;write_mode=overwrite 是整段覆盖,不是追加。

RECREATE_SCHEMA
结构以源端为准:上游加列、改类型,Doris 这边跟着重建,不用人去手工改表
overwrite
整表覆盖:同一批跑两遍,结果一样(幂等)。这是"全量刷新",不是"往里加"
注意overwrite 不等于"增量追加"。你要是以为它在追加,就会按"追加"的思路去设计下游对账,一对就懵:怎么每天都是全量数。真需要追加语义,得换写法,不是改个注释就行。
注意RECREATE_SCHEMA 之下,Doris 侧的表结构跟着源端走。所以别指望在 Doris 里手工改个字段能留住——下一批同步就照源端重建了。

2.3 节点参数:泵装在海豚上,还得填四格

配置只是泵里那套管子;泵装在海豚上,外面还有一圈参数。真实值如下(同样来自 add_seatunnel.py):

参数真实值人话
startupScriptseatunnel.sh泵的启动钥匙——调的就是这个脚本
deployModecluster集群模式:活交给集群干,不绑在一台机器上
timeoutFlag / timeoutOPEN / 1800半小时不动就判死,别让它挂一整夜
failRetryTimes0这个节点自己不再重试,重试交给下游统一做
workerGroup / environmentCode从已验证可用的模板节点继承(本脚本取 187355975537826 的骨架)排到哪台机器、哪个环境跑;规范要求 SEATUNNEL 节点是 seatunnel 分组

为什么用集群模式?因为这段活天生是"多台一起搬"。cluster 模式下任务提交给集群,几台机器分片搬,调度机只管发号。反过来用单机模式,调度机既当发包人又当干活的人,一张大表就能把它按住——现场已经有现成的例子:单项目 756 个任务分在 10 台 worker 上,其中 10.160.10.26 一台就扛了 218 个、排队 3.8 分钟,其余九台基本闲着。负载已经不均,再让谁单机硬扛就是雪上加霜。

为什么超时是 1800 秒?这条宽表正常几分钟就跑完。1800 秒不是"给足时间",是判死线:真跑过半小时,多半不是慢,是引擎心跳停了(见 2.5)。与其让它挂着耗到天亮,不如判失败、让下游重跑一遍——重跑的成本远低于"早上八点发现昨晚什么都没进仓"。

2.4 SIT 和生产:两根不同的管子

🚧 测试厂区和生产厂区,管子不能串

像什么:两个厂区各有一套管子,阀门口径一样、颜色一样,串起来也能通水。所以只能靠制度:谁也别去拧另一个厂区的阀。

其实是什么:这套链路在 SIT 和生产各有一份定义。SIT 那份源端读的是湖里的测试库(库名带 _sit),目标端用的是 SIT 那根 LINK;生产那份读正式库、写生产 LINK。

SIT 的源
dm_gk_hudi_sit.ads_log_inv_kpi_wide——库名带 _sit
SIT 的目标
LINK 是 SIT_DORIS_GK_SINK;规范里同步保存限制写的是 UAT 只能同步到 _sit / _uat 的对象
生产的限制
生产侧禁止读测试 schema 的表
红线原文
正式定义不得引用 SIT 运行对象
会出事发布前不核,正式定义里留着 _sit 的库表或 SIT 的 LINK:海豚不报错,任务照绿,生产跑的是测试数据。等业务发现数字不对,最难的恰恰是"它看着完全正常"——所以要靠发布前那次回读,不是靠人眼记。
小窍门发布前把整份定义 GET 回读一遍:先对版本号、任务数、依赖数,再逐个节点看 table_path 和 LINK 里有没有 _sit。这三样对上了才 release,然后 start。回读这一步不是形式,它是唯一能证明"我发的是我以为的那份"的证据。

2.5 失败了怎么办:三层兜底

同步这件事,失败是常态不是异常。原文给的产品能力里就写着"异常重试",落到现场是三层,从下往上兜:

  1. 节点自己那一层:failRetryTimes。海豚节点参数里就有这个开关,本脚本明确设成 0——意思是"别在节点上反复重试",因为同一批重试三次和重试一次,失败原因不会变,只是多排三次队。
  2. 下游那一层:retry_to_doris。专门有个任务,把当天没同步成功的重跑一遍(现场是 08:15 触发)。近 8 天 45,571 次成功 / 296 次失败 / 529 次被杀,失败率 0.65%,retry_to_doris 自动重跑之后全部成功。
  3. 人那一层:SeaTunnel 的 overview 页面。http://seatunnel.sinopharmholding.com:10001/#/overview 看 job 状态、并行度、有没有在重试。这一步是给"重跑也没好"的情况准备的,不是第一反应。
💔 真实根因:大多是心跳停了,不是数据错了

像什么:泵没坏、水也没脏,是泵和值班室之间的对讲机断了一下,值班室以为泵停了,就把这次任务记成失败。

其实是什么:历史失败的主要根因是引擎心跳超时——报错关键词是 OperationTimeoutException,日志里对应 Hazelcast 的 operation-heartbeat-timeout。数据本身没问题,重跑一遍就好。

报错关键词
OperationTimeoutException / operation-heartbeat-timeout
处置
交 retry_to_doris 重跑;跑完在 Doris 里对一下行数和指标
小窍门看到任务红,先看日志关键词再决定动不动手:出现心跳超时 → 重跑;出现字段不存在、类型不匹配、连接失败 → 重跑一百遍也没用,那是元数据或配置的问题,得回去改。分不清这两种,就会把"配置错"当成"偶发失败"一直重试到下午。

2.6 它还能干别的:不止"湖 → Doris"

这套泵不是只会往 Doris 一个方向灌。原文写的是"同步至 Doris 数据仓库(或其他指定数据存储形式)",实际用过的还有这几种:

用法怎么配人话
同步到文件服务器sink 的 LINK 换成 FTP_GK_TEST,用 file_name_expression 起文件名,文件从 http://ftp.sinopharmholding.com:10001/ 取不进城,直接装车拉走:给别人一份 CSV
分区sink 加 pt_cols:配 * 表示自动识别 Hudi 分区并自动建 Doris 分区表;配具体字段表示按这些字段分区货架按天分格,新的一天自动加格,老格子覆盖
字段映射 / 加工source 加 transfer_fields,例 *,m_float_v(ar_acc) as ar_acc:全部字段照搬,只把那一个字段加工一下不用把几十个字段全写一遍,只点出要动的那一个
脱敏同一层里调用内置函数:m_cn、m_mobile、m_id_card、m_bank_card、m_email、m_addr、m_car、m_reg_replace 等出厂前把该打码的打掉:m_mobile('15000000000') → 150****0000

文件服务器那条有两个坑是原文专门标出来的:一个数据集只要生成 1 个文件,并发数必须设成 1;部分外部服务器没给删除权限(原文点名紫竹 FTP),传错了删不掉,属于数据泄露风险,所以正式同步前可以在数据集里先加 1=0 条件空跑一遍,验证文件名和路径对了再放开。

2.7 同一件事,历史上还有第二条路径

出湖入仓这件事,不是只有 SeaTunnel 一根管子。历史上湖 → DWS 这一段也用过 CDM 同步,证据就在监控看板里:它判断一张表的状态,是按四段任务的结果算的——入湖一致性检查、入湖 CDM 任务、湖仓开发任务、同步 CDM 任务。最后那一段,就是当年跑"湖 → 仓"的那条路径。

现在新链路以 SeaTunnel 为主:规范里出湖节点的类型就写死了 SEATUNNEL,Worker 分组 seatunnel;而 CDM 主要留在入湖那一段(初始化批量、增量批量)。两条路径都通,意味着排查时先要认清"这条链现在到底是谁在跑"——拿老 CDM 任务的日志去解释新 SeaTunnel 节点的失败,能查一下午,查的还是别人家的账。

三、串起来:一次完整的上线流程

下面这八步就是真实干过一遍的顺序。注意第 6 步:回读不是可选项,它是这道工序里唯一一道"自己检查自己"的关口。

  1. 先确认接头在。源端 HUDI_GK_LINK、目标端 SIT 的 SIT_DORIS_GK_SINK,两个都得是平台上已经注册好、能连通的。接头不存在,写再漂亮的配置也是空转。
  2. 写配置。三块:env(parallelism + job.mode)、source(LINK + table_path)、sink(LINK + table_path + schema_save_mode + write_mode)。
  3. 取模板骨架。不要从零手搓节点,取一个本项目已验证可用的 SEATUNNEL 任务当模板,只留该留的字段:taskType、workerGroup、environmentCode、failRetryTimes、timeout 这些,其余换成新的。
  4. 申请 task code。海豚的节点编号得先申请(gen-task-codes),一次申请几个就加几个节点,编号不能重。
  5. 拼节点。taskParams 里塞进 rawScript(上面那份 JSON),再补 startupScript=seatunnel.sh、deployMode=cluster、failRetryTimes=0、timeout=1800。
  6. 挂依赖 + 提交 + 回读。上游加工跑完才准开泵(本例:06_指标宽表_全量 → 07_同步宽表至Doris、05_口径校验_全量 → 08_同步校验至Doris)。整份定义 PUT 上去之后,立刻 GET 回来对三样:版本号、任务数、依赖数;再逐个节点看有没有 _sit 残留。
  7. 发布、启动。release 之后 start。写操作之前要先拿临时管理员权限(1 小时),申请理由写清具体场景。
  8. 跑完验数。到 Doris 里对行数和关键指标,跟湖里那张表比一遍。任务绿只是"搬完了",行数对上才叫"搬对了"。

回头看这八步:真正容易出事的是第 1 步和第 6 步,都不是"技术难",是"省了那一眼"。接头没确认,配出来的是个连不通的任务;回读没做,发出去的是份自己没看过的定义。

四、搞错了会怎样

前面几关的错,多半错在"理解";这一关的错,多半错在"省了一眼"——接头没核、回读没做、参数写反了方向。它有个共同点:任务都是绿的。

下面这几条,前三条是红线级的,后三条是"绿着也要盯"的。

会出事 · 管子接反拿 SeaTunnel 去做入湖、拿 DataX 去出湖,等于把出厂管当进水管装:接头对不上是一层,更要命的是它会"通"——数据能流,只是流的方向和你想的相反。表现就是本关开头那句:湖里有、仓里没有。这时候去改 SQL 是白改,先查同步。
会出事 · 正式定义里留着 SIT目标 LINK 还是 SIT 那根,或者 table_path 里还带 _sit。任务绿、日志干净、数据进了测试库。红线原文写得没有余地:正式定义不得引用 SIT 运行对象。发布前 GET 回读,就是为了抓这一条。
会出事 · 把覆盖当追加write_mode=overwrite 是整段覆盖。要是按"每天在昨天的数上追加"的思路去设计对账,永远对不上;反过来,如果哪天真需要追加语义却还在用 overwrite,下游看到的就永远是最后一批,历史全被冲掉。
注意 · parallelism 写了不等于开了配置里写 5,平台统一限流之后实际跑的是 3(最终并发 = Math.min(parallelism, parallelism_max))。想快,先看是不是被这道限流按住了,别一上来就怀疑机器不够。
注意 · 任务绿了不等于数据到了心跳超时那类失败会被下游 retry_to_doris 补回来,但"补回来了"这件事得有人确认:近 8 天就有 296 次失败,全部靠重跑收口。所以每天该看的不是"有没有红",是"红过之后重跑成功了没有"。
注意 · 拿老路径的日志解释新任务湖 → 仓历史上 CDM 也跑过,监控看板上那段就叫"同步 CDM 任务"。现在主力是 SeaTunnel。两条路径同时在账上,排查前先确认这条链上跑的到底是哪一个,否则容易对着一份过时的日志下结论。

五、一句话记住

做对了管子接错方向,水会倒流——湖里有、仓里没有,先查同步,别急着改 SQL。

落到手上的动作就三件:先看 SeaTunnel 的 overview 里那个 job 是什么状态,再看海豚上这个节点的 table_path 和 LINK 有没有指错厂区,最后才回去看加工那段的 SQL。顺序反了,能省的时间全赔进去。

再补一句给以后的人:这根管子身上贴着四张纸条,别撕——deployMode=cluster(活给集群干)、timeout=1800(半小时不动就判死)、failRetryTimes=0(重试交给下游)、write_mode=overwrite(整段覆盖)。撕掉哪一张,后面都会有人重新踩一遍。

事实来源:离线数据同步解决方案 · add_seatunnel.py · 数据同步增加并发数方法 · SeaTunnel 分区/字段映射/脱敏这一关只管"出厂"下一关:Doris 货架怎么摆