# 架构决策 > 重构完成后沉淀的关键架构决策(ADR),给自己看的备忘。 ## 说明 - 本文档是**重构完成态**的沉淀:`90-重构路线.md` / `91-重构备忘.md` / `92-重构进度.md` 三份过程文档会在重构收尾时删除,关键决策、权衡、反悔条件压到这里留档。 - 当前仍在重构中,本文档先留骨架,内容待重构收尾时填入。 - 单条 ADR 格式按业界主流(Michael Nygard 版精简):**背景 / 决策 / 后果 / 候选方案 / 反悔条件** 五段;不套公司标准模板的多余字段。 ## 模板 > 新增一条决策时,复制以下模板到 §2 列表内。编号连续,不复用已弃用编号。 ```markdown ### ADR-NN 决策标题 - **状态**:已采纳 / 被 ADR-MM 取代 / 已弃用 - **背景**:当时面对的问题、约束、触发决策的场景。 - **决策**:最终选了什么方案,一句话。 - **后果**:带来的好处、新增的代价、影响到的模块。 - **候选方案**:考察过但否决的方案,以及否决理由。 - **反悔条件**:什么条件下会重新评估或反悔。 ``` ## 决策清单 ### ADR-01 DataX 入口不做日期展开,按天补数归 DolphinScheduler - **状态**:草案 老 `spark-sql-starter` 的 `get_date_range` 支持 `20260401-20260410` 范围格式自动展开;DataX 入口从未用过。本项目调度用 DolphinScheduler,DS 原生支持**业务日期补数**(时间区间选定后,按调度周期逐日实例化 task)。用户老 DS 配置即 `-start-date=${dt} -stop-date=${cdt}` 单日传参。 - **决策**:DataX 入口只接受单日语义(`start_date` / `stop_date` 对应一个 dt 分区);按天展开 / 批量补数 / 回溯全部交由 DS 工作流承担。 - **后果**: - 正面:DataX 层职责单一;补数、回溯、失败重跑在 DS 层统一可视化 / 可审计;DataX 不维护日期展开状态(哪天已做、哪天失败、重试) - 负面:不走 DS 的一次性手工补 N 天需要外部 bash 循环或 `workspace/` 下临时 dispatcher - **候选方案**:DataX 入口层实现"日期范围自动展开 + 多 json 分发多 worker"——否决,理由是**重复 DS 职责** + 引入状态管理复杂度 - **反悔条件**:项目从 DS 迁走到无补数功能的调度系统;或出现"必须在 DataX 层展开"的硬场景 ### ADR-02 分布式分发归 DolphinScheduler worker group,DataX 不重复随机 - **状态**:草案 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` 可移除 - **后果**: - 正面:消除两层独立随机叠加打乱 DS 负载均衡;DataX 链路大幅简化(`worker.py` / ssh 远端执行 / `workers.ini` 可裁);维护成本下降 - 负面:单 DS 任务节点内部的批量(多 ini + `-parallel`)场景无法在 DataX 层散到多 worker,要分布式必须在 DS 层拆成多 task - **候选方案**:保留 DataX 两层分发——否决,"两层独立随机"破坏 DS worker group 语义 - **反悔条件**:DS 换成无 worker group 支持的调度器;或单 task 内批量规模大到 DS 拆分成本过高 ### ADR-03 零点漂移决策 - **状态**:草案 T+1 按 `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` 版本,最新状态永远抓不到。 - **决策**: - raw 层 where 右界扩展为次日同期:`where update_time >= '{day-start} 00:00' AND update_time < '{day+1-stop} 00:00'`,在关闭侧加 1 天 buffer 覆盖漂移 - buffer 取 1 天而非 N 小时:**定时可变更**(同步执行时刻从 6 点调到 3 点或 8 点都不影响窗口语义)+ **可维护**(固定区间可复跑、可回刷) - raw 层不纠正分区漂移:所有抓到的记录(含"漂到次日"的)按 ini `dt = stop-1` 统一落当日分区 - ods 层 Spark SQL 用**动态分区** `PARTITION (dt)`,按每行 `update_time` 真实日期归位 - ods 写入模式两方案并列(默认方案**本 ADR 不预设**,实施时按具体表需求选 / 可表级混用): - **方案 A:`INSERT OVERWRITE`(覆盖式)** —— 每次 ods 跑覆盖对应 dt 分区。不丢数据:**数据只会向后漂移一天**(某条 update_time=N 号 的记录若 N 号 raw 没抓到漂到 N+1 号,N+1 号 raw 必然抓到——漂移不会再漂到 N+2 号),N+1 号 ods 覆盖 dt=N 号 时能补齐 - **方案 B:`INSERT INTO` 追加 + 单分区内 `(pk, max(update_time))` 去重** —— 同 pk 在同 dt 分区内只保留最新 `update_time` 一条。保留"每日 ods 跑时刻的 dt=X 版本"轨迹(审计、回溯),防止覆盖后丢失"update 恰为当天"的中间快照 - ods 层**跨 dt 不去重**:同一业务 id 允许在多个 dt 分区并存(每条代表一个"时间段状态快照")——上层拉链表(SCD Type 2)的必要基础 - **后果**: - 正面:不漏漂移记录;raw 简单(只管多抓、不管分对);ods 动态分区自动归位;同 id 多 dt 并存作为拉链表底层 - 负面:raw 每日抓量约翻倍;ods SQL 必须统一动态分区写法(方案 A 或 B),开发规范需明文约束 - **候选方案**: - 单日窗口 `[day-start, day-stop)` 固定区间(无 buffer):业界小公司通用做法,本项目因漂移窗口 + 活跃度双放大**否决** - 动态 `now` 右界 `[day-start, now)`:可复现性、复跑、补数场景难处理,**否决** - 精确匹配漂移的半天 buffer `[day-start, day+6h-stop)`:和具体同步时刻耦合,调定时就得改窗口,**否决** - ods 跨 dt 按 `(pk, max(update_time))` 去重:破坏拉链表基础,**否决** - 源库 `REPEATABLE READ` snapshot isolation:业务库长事务风险,**否决** - CDC(PG 逻辑复制 / MySQL binlog 流读):架构正路,需独立立项,**本阶段不取**(见 kb/12 CDC 演进节) - **反悔条件**:迁 CDC