重构完成后沉淀的关键架构决策(ADR),给自己看的备忘。
90-重构路线.md / 91-重构备忘.md / 92-重构进度.md 三份过程文档会在重构收尾时删除,关键决策、权衡、反悔条件压到这里留档。新增一条决策时,复制以下模板到 §2 列表内。编号连续,不复用已弃用编号。
### ADR-NN 决策标题
- **状态**:已采纳 / 被 ADR-MM 取代 / 已弃用
- **背景**:当时面对的问题、约束、触发决策的场景。
- **决策**:最终选了什么方案,一句话。
- **后果**:带来的好处、新增的代价、影响到的模块。
- **候选方案**:考察过但否决的方案,以及否决理由。
- **反悔条件**:什么条件下会重新评估或反悔。
-backfill 开日循环用于存量回填状态:草案
老 spark-sql-starter 的 get_date_range 支持 20260401-20260410 范围格式自动展开;DataX 入口从未用过。本项目调度用 DolphinScheduler,DS 原生支持业务日期补数(时间区间选定后,按调度周期逐日实例化 task)。用户老 DS 配置即 -start-date=${dt} -stop-date=${cdt} 单日传参。2026-04-24 出现场景:接入新源表需一次性回填 N 年历史存量数据——这类一次性手工批量 DS 的日历补数组件实例化太慢,走外部工具更合适。
决策:DataX 入口默认单日语义(start_date / stop_date 对应一个 dt 分区),T+1 增量补数仍归 DS;hive-import 入口 2026-04-24 补 -backfill flag,显式开启后 -start-date / -stop-date 作外层回填范围,按日循环调用单日逻辑,用于存量回填。
后果:
-backfill 内置支持,不再需要外部 bash 循环 / 临时 dispatcher-start-date / -stop-date 在 -backfill 模式下含义不同);DS 任务模板规范上不得加 -backfill(团队约定)候选方案:
bin/datax-backfill.py 工具文件——否决(和 hive-import 共享 100% 参数,独立文件冗余;单 flag 切换更简洁)反悔条件:出现必须在 DataX 层做日期展开的默认场景(如 DS 彻底换掉);或 -backfill 被误用频繁到需物理隔离
状态:草案
DataX 老入口 single-job-starter.sh 内置 -random + workers.ini 权重加权随机选 worker + ssh 分发。DS 自身亦有 worker group 机制(group 绑定机器列表、task 落到 group 内一台 worker)。两层叠加:DS 选 node01 → node01 上 DataX 再 random 到 node03。【查证 kb/91 §4.4】:用户老 DS 配置不传 -random,说明 DS 层已完成分发,DataX 只在本机跑——两层分发在实际运营中就没被"同时启用"过
决策:DataX 入口不做 worker 分发;分布式执行在 task 粒度靠 DS worker group 承担。DataX 入口的 select_worker 等同"返回 current_host",ssh 分支可删,workers.ini 可移除
后果:
worker.py / ssh 远端执行 / workers.ini 可裁);维护成本下降-parallel)场景无法在 DataX 层散到多 worker,要分布式必须在 DS 层拆成多 task候选方案:保留 DataX 两层分发——否决,"两层独立随机"破坏 DS worker group 语义
反悔条件:DS 换成无 worker group 支持的调度器;或单 task 内批量规模大到 DS 拆分成本过高
update_time 过滤单日窗口 [day-start, day-stop) 同步时,源库在同步执行期间持续写入——跨零点的记录 update_time 会从 N 号"漂到" N+1 号,单日窗口无法捕获漂移记录。漂移概率和漂移数据量的相关变量:
业界一般做法:小数据量、用户低活跃度的场景下,通常凌晨 0:30 前后跑 T+1,漂移窗口 30 分钟、活跃度低,单日固定窗口 [day-start, day-stop) 即可,忽略极少量漂移数据是可接受的工程权衡。
本项目特殊性:业务高峰在凌晨 6 点前,同步定时必须避开高峰定在 6 点后,漂移窗口 6 小时;且若干用户行为集中在 0-6 点,漂移窗口和活跃度两个放大因子都踩中,漂移不能忽略——需要单独设计。
极端场景佐证:某用户习惯 0-6 点更新自己的数据,若走业界做法的单日固定窗口,数仓永远只能看到该用户最早的 create_time 版本,最新状态永远抓不到。
决策:
where update_time >= '{day-start} 00:00' AND update_time < '{day+1-stop} 00:00',在关闭侧加 1 天 buffer 覆盖漂移dt = start_date(业务日)统一落当日分区,分区内允许含次日漂移数据PARTITION (dt),按每行 update_time 真实日期归位INSERT OVERWRITE 覆盖式 + 双源 union raw dt=T-1 + raw dt=T-2,按 WHERE DATE(update_time) = T-1 过滤,dedupe by (pk, max(update_time))。覆盖式写入支持重跑;双源 union 完整捕获 update_time ∈ [T-1, T) 范围的所有版本(含跨 raw 任务时段被业务覆盖更新跨日的早期版本)设定:业务日 04-27,跑批日 04-28,raw 任务凌晨 03:00 跑批,业务库覆盖式更新会推 update_time 字段。
订单 X 的 update 时间线(业务库视角):
update_time = 04-27 02:00(状态 A)update_time = 04-28 02:00(状态 B,跨日)raw 抓取过程:
update_time = 04-27 02:00 → 落 raw dt=04-26 / update_dt=04-27update_time = 04-28 02:00 → 落 raw dt=04-27 / update_dt=04-28ods 04-28 跑批写 dt=04-27 分区(WHERE DATE(update_time)='04-27'):
update_time = 04-28 02:00,DATE=04-28,不入 → ods dt=04-27 漏 X 的 04-27 状态 ❌update_time = 04-27 02:00(DATE=04-27,入),raw dt=04-27 里 X update_time = 04-28 02:00(不入)→ ods dt=04-27 完整包含 X 的 04-27 状态 A ✓X 的 04-28 状态 B 第二天 ods 04-29 跑批时落 ods dt=04-28 分区,拉链表轨迹完整无断节。
业务高峰跨零点(如本项目订单 0-6 点高峰)下,跨日漂移是常态,双源 union 是必要机制不是 defensive。
本设计基于 T+1 batch ETL,容忍范围明确:
如果没有 raw 宽窗 + ods 双源(即单日固定窗口 + 单源),会出现"幽灵更新":
设想某用户固定在每天凌晨 2 点 update 同一条记录 X:
[N, N+1) 不覆盖 update_time=N+1 02:00 → raw dt=N 漏 X[N+1, N+2) 又不覆盖 → 又漏宽窗(48h)+ 双源 union 配套解决幽灵更新:raw dt=N 任务窗口 [N, N+2) 抓到 X update_time=N+1 02:00 漂到 N+1 范围;ods 写 dt=N+1 时双源 union(raw dt=N + raw dt=N+1)把这条早版本归位到 ods dt=N+1 分区。
后果:
候选方案:
[day-start, day-stop) 固定区间(无 buffer):业界小公司通用做法,本项目因漂移窗口 + 活跃度双放大否决now 右界 [day-start, now):可复现性、复跑、补数场景难处理,否决[day-start, day+6h-stop):和具体同步时刻耦合,调定时就得改窗口,否决(pk, max(update_time)) 去重:破坏拉链表基础,否决INSERT INTO 追加 + 分区内去重(早先并列方案 B):意图保留"每日 ods 跑时刻"的中间快照轨迹。但与"支持重跑"语义冲突(追加模式重跑会重复入数据),且方案 A 双源 union 已能完整捕获跨日漂移版本,方案 B 的"中间快照"价值不抵复杂度代价,否决REPEATABLE READ snapshot isolation:业务库长事务风险,否决反悔条件:迁 CDC
状态:草案
老代码 job_config_generator.py:60-67 把 speed 三参(channel / byte / record)按工作时段硬编码:07:50-19:00 走 channel=10 / byte=10MB / record=40k(保护业务 DB),其余时段走 channel=6 / byte=256MB / record=100k。时段边界和各档值完全写死在 Python 代码里,调优需要改代码 + 发布。压测 / 回填场景下 speed 是高频调参项,需要运行时覆盖能力。
决策:speed 三参走三级合并,优先级 L1 < L2 < L3。
conf/datax-tuning.conf:项目级默认,承载宽松时段 relaxed_period.{start,stop} 边界 + strict/relaxed 两档各 3 个 speed 参值,共 8 个 key;时段配置用 HH:MM 字符串,代码解析为 HHMM 整数比较[speed] 段:单 ini 级覆盖 channel / byte / record;一旦在 L2 显式写了,忽略时段判断直接使用-channel / -byte / -record:本次运行级覆盖,bin/datax-{hive-import,hdfs-export}-starter.py 暴露,逐层透传到 dw_base.datax.cli gen-json 子进程,最终到 JobConfigGeneratorJobConfigGenerator.assemble() 内存里,结果写进生成的 json(job.setting.speed + core.transport.channel.speed);ini / conf 文件本身不动[start, stop);每 key 的来源打印到 stdout 便于审计strict_period 不工作——本项目业务高峰跨午夜(12:00 至次日 06:00),HHMM 整数比较 1200 ≤ x < 0600 为空集;宽松时段在低峰期(如 06:00-12:00)天然连续不跨日后果:
[tuning] log 逐层打印缓解;conf/datax-tuning.conf 是新文件需同步到所有 worker(sync-all.sh 已覆盖)候选方案:
ini 覆盖硬编码 够用,外配反而复杂--datax-conf key=value,...——否决,speed 高频用独立 flag 更直观,Spark 入口风格参考反悔条件:三级注入使用场景长期 <5%(即 L2 L3 基本不用),退回单层 conf;或者 DataX 被替换
老 kb/20 §5.5 走"合 vs 拆"二分法(粒度相同则合粒度不同则拆),举例订单履约 / 拼团生命周期均"合到 acc 累积快照"——一行一实体,每个里程碑一列时间戳,状态单向推进。该写法属 Kimball 维度建模理论里的 Accumulating Snapshot Fact Table,在传统数仓(Teradata / Oracle / Greenplum)里是经典模式。
2026-04-26 启动埋点 + 业务库接入业务建模时重新审视,发现两个核心问题:
期间在 inbox 收了一篇分层分区语义草稿,主张"事实表只承载事件、状态归 DIM 拉链"。讨论后认为该结论方向正确,但论证过度(直接否定 Kimball 三类事实表的全部);最终采纳折中表述:默认拆派 + 留 acc 例外口子。
决策:默认采用"事件 vs 状态"拆派组合:
dwd_{域}_{业务过程}_apd_d,append-only,业务时间分区dim_{域}_{实体}_zip_d,SCD Type 2 每次状态变更生成新行后果:
is_current 切片或按 dt 切片,直观end_date 置昨天 + 新行 insert)候选方案:
is_current 直接——否决反悔条件:
raw / ods 表 DDL 长期手写(参考 sync ini column 列表 + 类型映射)易错且重复——80+ 字段表抄一遍 PG schema 已经痛苦,未来 50 张业务表 × 2 层(raw + ods)无法维护。需要工具自动出 DDL,开发者只做表注释 / LOCATION / 字段注释微调。
决策:
bin/hive-ddl-gen.py 一脚本两层产出(独立于 datax-sync-template-gen.py,单脚本一职责)python3 bin/hive-ddl-gen.py -l {raw|ods} -ini <sync.ini path> [-o [DIR]]-l 指定层级——避免 raw / ods 误生成;不做"同时生成两层"datax-sync-template-gen.py 的 resolve_datasource + query_columns_full 拿字段中文注释 + PG 类型conf/pg-to-hive-type.ini,PG type → Hive type 规则(参考 kb/20 §8.4.1)etl_time / src_sys / src_tbl)+ ORCnumeric(p,s) 是否保留原精度还是统一 DECIMAL(20,4):实施时拍板month_trunc 时间 → STRING),实施时考虑-l ods 在 ods 实施前报"未实现"后果:
pg_description 自动取,避免手抄漏候选方案:
datax-sync-template-gen.py 加 --ddl raw/ods flag:单脚本多产物耦合度高,sync 模板 vs DDL 两件事语义不同——否决,独立脚本反悔条件:
raw / ods 入仓后需要监控两类质量问题:(a) PG 业务库 schema 变更(新增 / 删除字段)没及时通知数仓 → 字段悄悄漏入或老字段入仓后失效;(b) PG vs Hive 行数不一致 → 同步任务可能漏抓数据。当前依赖人工巡检不可持续。merchant_open 是 schema drift 实例:PG 业务库有此字段,分析师库没同步过来,本次 generator 跑出来才发现——证明流程缺失,需 dq 任务补齐。
一、复用 mask conf 作 schema drift 探查的 ground truth
jobs/raw/{域}/{table}.mask.ini 是单一真值——sync 生成器和 dq 模块双消费:
-mask-conf)二、注册表机制
conf/dq.ini 列出需 dq 任务的表:
[card_group_order_info]
domain = trd
sync_ini = jobs/raw/trd/raw_trd_card_group_order_info_inc_d.ini
mask_conf = jobs/raw/trd/raw_trd_card_group_order_info_inc_d.mask.ini
count_threshold = 0.01
dq 任务 daily 跑:遍历注册表 → 每张表跑两类检查 → 异常推 alerter。
三、检查类型
Schema drift 探查:
数据量比对:
count(*) WHERE update_time ∈ [day-start, day+1-stop)count(*) WHERE dt='{业务日}'count_threshold → alerter四、实现归属
dw_base/dq/(kb/92 已建空骨架)schema_drift.py + count_check.py + runner.py(读 conf/dq.ini 调度)bin/dq-runner.py 命令行 / DS 调度复用 datax-sync-template-gen.py 的 resolve_datasource + query_columns_full + pg8000
后果:
候选方案:
反悔条件:
实施节奏:表数 ≥ 5 张时启动;< 5 张靠人工巡检 + 偶尔重跑 sync 生成器 diff。