因为还差最后一段管子:从厂里的清水池,把水送到城里的管网。
这一关说的是工厂区里那两根管子——一根进水、一根出水。长得像,接口不一样,装错一次,后面所有页面都喝不到水。
一、先说人话
整套同步从一开始就分成两件事,不是"一个工具干完":DataX 管进厂,SeaTunnel 管出厂。原文的说法是:DataX 负责离线入湖,SeaTunnel 负责湖内入仓。
人话就是:河在城外(业务库不归你管),厂在城里(数据湖),城市管网在更里面(Doris 仓)。第一段是"从别人的河里往自己厂里抽",第二段是"从自己厂里往自己管网里送"。两段水的性质完全不同,所以工具也分开。
| 管子 | 干什么 | 从哪 → 到哪 | 海豚上的节点 |
|---|---|---|---|
| DataX 抽水泵 | 入湖 | 各子公司业务库 / 文件服务器 / 外部数据接口 → 湖(HDFS / OSS 上的 Hudi) | DATAX,Worker 分组 datax |
| SeaTunnel 主干管 | 湖内入仓 | 湖(Hudi)→ Doris;也支持其他源端和目标端 | SEATUNNEL,Worker 分组 seatunnel |
为什么不拿一个工具干两件事?因为两头要的东西正好相反。抽水那一段,关键指标是"别把源库压死"——源库是别人的生产库,DataX 靠主从架构,对源端压力小;送水那一段,关键指标是"快",SeaTunnel 吃的是湖里那套分布式计算资源,表再大也是几台机器一起搬。
原文把这句话写成了产品优势的第一条:双引擎分工明确,各取所长,链路清晰。分开的另一个好处是不互相拖累:入湖那边在抽一条几千万行的源表,不会把出湖的管子堵住;出湖这边要重建 Doris 表结构,也不用等入湖跑完。
看图只用记三件事:左边那池水本来就有(湖里确实有数据),中间那台泵是 SeaTunnel,右边才是页面真正连的地方(Doris)。泵上方两块牌子是"接头"的名字——两块牌子不是同一根管子,装混了,泵就在往别的地方抽水。
牌子上写的是接头的大名。目标端那根按环境分名字:SIT 实跑的叫 SIT_DORIS_GK_SINK,生产是另一根,名字自己核,别照图上的字面抄。
二、拆开看:每个零件是什么
2.1 它是个"产品",不是一堆脚本
像什么:装水管最土的办法是自己在机房里写脚本、一个个参数拼;产品化的办法是给你一个面板,源、路径、目标表、周期四格填完,剩下的它自己接。
其实是什么:用户通过统一可视化界面配置源端、湖存储路径、数仓目标表及调度周期,系统自动生成串联任务;同时提供断点续传、数据校验、异常重试与全链路监控告警。原文对这套东西的总结是四个词:不丢、不重、高效、可管。
- 产品名
- 《离线数据同步解决方案》(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 是作业模式。这份配的是 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 是平台上注册好的连接配置名——地址、账号、库都封在里面。源端这边湖的接头叫 HUDI_GK_LINK;目标端这边,SIT 用的是 SIT_DORIS_GK_SINK。
- 源端
HUDI_GK_LINK(湖,读)- 目标端(SIT)
SIT_DORIS_GK_SINK(Doris,写)- 目标端(生产)
- 不是 SIT 那一根;生产侧 LINK 的具体名字这一步的实际值要现场核
像什么:接头编号对了,还得说清插到哪个池子、哪个货位。
其实是什么:库.表 写全。这条链是 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=RECREATE_SCHEMA 是目标表结构照源端重建;write_mode=overwrite 是整段覆盖,不是追加。
RECREATE_SCHEMA- 结构以源端为准:上游加列、改类型,Doris 这边跟着重建,不用人去手工改表
overwrite- 整表覆盖:同一批跑两遍,结果一样(幂等)。这是"全量刷新",不是"往里加"
overwrite 不等于"增量追加"。你要是以为它在追加,就会按"追加"的思路去设计下游对账,一对就懵:怎么每天都是全量数。真需要追加语义,得换写法,不是改个注释就行。RECREATE_SCHEMA 之下,Doris 侧的表结构跟着源端走。所以别指望在 Doris 里手工改个字段能留住——下一批同步就照源端重建了。2.3 节点参数:泵装在海豚上,还得填四格
配置只是泵里那套管子;泵装在海豚上,外面还有一圈参数。真实值如下(同样来自 add_seatunnel.py):
| 参数 | 真实值 | 人话 |
|---|---|---|
startupScript | seatunnel.sh | 泵的启动钥匙——调的就是这个脚本 |
deployMode | cluster | 集群模式:活交给集群干,不绑在一台机器上 |
timeoutFlag / timeout | OPEN / 1800 | 半小时不动就判死,别让它挂一整夜 |
failRetryTimes | 0 | 这个节点自己不再重试,重试交给下游统一做 |
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:海豚不报错,任务照绿,生产跑的是测试数据。等业务发现数字不对,最难的恰恰是"它看着完全正常"——所以要靠发布前那次回读,不是靠人眼记。table_path 和 LINK 里有没有 _sit。这三样对上了才 release,然后 start。回读这一步不是形式,它是唯一能证明"我发的是我以为的那份"的证据。2.5 失败了怎么办:三层兜底
同步这件事,失败是常态不是异常。原文给的产品能力里就写着"异常重试",落到现场是三层,从下往上兜:
- 节点自己那一层:
failRetryTimes。海豚节点参数里就有这个开关,本脚本明确设成0——意思是"别在节点上反复重试",因为同一批重试三次和重试一次,失败原因不会变,只是多排三次队。 - 下游那一层:
retry_to_doris。专门有个任务,把当天没同步成功的重跑一遍(现场是 08:15 触发)。近 8 天 45,571 次成功 / 296 次失败 / 529 次被杀,失败率 0.65%,retry_to_doris自动重跑之后全部成功。 - 人那一层: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 步:回读不是可选项,它是这道工序里唯一一道"自己检查自己"的关口。
- 先确认接头在。源端
HUDI_GK_LINK、目标端 SIT 的SIT_DORIS_GK_SINK,两个都得是平台上已经注册好、能连通的。接头不存在,写再漂亮的配置也是空转。 - 写配置。三块:
env(parallelism+job.mode)、source(LINK+table_path)、sink(LINK+table_path+schema_save_mode+write_mode)。 - 取模板骨架。不要从零手搓节点,取一个本项目已验证可用的 SEATUNNEL 任务当模板,只留该留的字段:
taskType、workerGroup、environmentCode、failRetryTimes、timeout这些,其余换成新的。 - 申请 task code。海豚的节点编号得先申请(
gen-task-codes),一次申请几个就加几个节点,编号不能重。 - 拼节点。
taskParams里塞进rawScript(上面那份 JSON),再补startupScript=seatunnel.sh、deployMode=cluster、failRetryTimes=0、timeout=1800。 - 挂依赖 + 提交 + 回读。上游加工跑完才准开泵(本例:
06_指标宽表_全量 → 07_同步宽表至Doris、05_口径校验_全量 → 08_同步校验至Doris)。整份定义 PUT 上去之后,立刻 GET 回来对三样:版本号、任务数、依赖数;再逐个节点看有没有_sit残留。 - 发布、启动。
release之后start。写操作之前要先拿临时管理员权限(1 小时),申请理由写清具体场景。 - 跑完验数。到 Doris 里对行数和关键指标,跟湖里那张表比一遍。任务绿只是"搬完了",行数对上才叫"搬对了"。
回头看这八步:真正容易出事的是第 1 步和第 6 步,都不是"技术难",是"省了那一眼"。接头没确认,配出来的是个连不通的任务;回读没做,发出去的是份自己没看过的定义。
四、搞错了会怎样
前面几关的错,多半错在"理解";这一关的错,多半错在"省了一眼"——接头没核、回读没做、参数写反了方向。它有个共同点:任务都是绿的。
下面这几条,前三条是红线级的,后三条是"绿着也要盯"的。
table_path 里还带 _sit。任务绿、日志干净、数据进了测试库。红线原文写得没有余地:正式定义不得引用 SIT 运行对象。发布前 GET 回读,就是为了抓这一条。write_mode=overwrite 是整段覆盖。要是按"每天在昨天的数上追加"的思路去设计对账,永远对不上;反过来,如果哪天真需要追加语义却还在用 overwrite,下游看到的就永远是最后一批,历史全被冲掉。5,平台统一限流之后实际跑的是 3(最终并发 = Math.min(parallelism, parallelism_max))。想快,先看是不是被这道限流按住了,别一上来就怀疑机器不够。retry_to_doris 补回来,但"补回来了"这件事得有人确认:近 8 天就有 296 次失败,全部靠重跑收口。所以每天该看的不是"有没有红",是"红过之后重跑成功了没有"。五、一句话记住
落到手上的动作就三件:先看 SeaTunnel 的 overview 里那个 job 是什么状态,再看海豚上这个节点的 table_path 和 LINK 有没有指错厂区,最后才回去看加工那段的 SQL。顺序反了,能省的时间全赔进去。
再补一句给以后的人:这根管子身上贴着四张纸条,别撕——deployMode=cluster(活给集群干)、timeout=1800(半小时不动就判死)、failRetryTimes=0(重试交给下游)、write_mode=overwrite(整段覆盖)。撕掉哪一张,后面都会有人重新踩一遍。