Переглянути джерело

docs(kb/93): ADR-09 N=2 改滚动 N=30 - 写清 N=2 三问题 + 退款窗实测依据

N=2 问题: 只 cover ≤1天漂移不覆盖退款窗(15-20天)、补数缺数(5月缺31%)。
退款窗实测 P95=15/P99=20/MAX=20(5月9万退款单),定 N=30(>max20+buffer)。
每天滚动重算最近30天(复用 backfill 宽扫窄落+status外层),临时方案,
目标态走 ADR-12 支付/退款独立 dwd。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tianyu.chu 1 тиждень тому
батько
коміт
cc54f121a0
1 змінених файлів з 48 додано та 26 видалено
  1. 48 26
      kb/93-架构决策.md

+ 48 - 26
kb/93-架构决策.md

@@ -354,47 +354,69 @@
   - dim 表数据量增长导致 sche shuffle 性能下降,评估改"分区裁剪 + 局部重算"
   - 业务库消除"主动置 NULL" 场景后,可放宽到字段级 COALESCE 简化写法
 
-### ADR-09 DWD 事件表跑批回算窗口 N=2
+### ADR-09 DWD 事件表跑批回算窗口(N=2 → 滚动 N=30)
 
-- **状态**:已采纳
+- **状态**:已采纳(滚动 N=30,**临时方案**;目标态走 ADR-12 支付/退款独立 dwd)
+
+- **修订历史**:
+  - 2026-05-10:初版 N=2(commit `a9b6eaa`),按"跨零点 OLTP 漂移 ≤ 1 天"通用假设
+  - 2026-06-03:退款窗实测后改 **滚动 N=30**,写清 N=2 暴露的问题与 N=30 依据
+
+- **背景(N=2 的三个问题)**:
+
+  ods 层按 ADR-03 严格按 `update_time` 归位 dt。dwd 事件表按业务时间(`payment_success_time`)分区。N=2 初衷只兜底跨零点漂移(`payment_success_time=T-1 23:59`、`update_time` 延迟到 T 落 `ods.dt=T`),但实际暴露三个问题:
+
+  1. **只 cover ≤1 天漂移,cover 不到订单状态机生命周期**:订单支付后历经发货/收货/退款,`update_time` 持续刷新,退款窗长达 15~20 天(见下"实测")。N=2 窗外的退款/状态变化不被回算 → dwd 状态过期(窗外退款单 dwd 仍记有效)
+  2. **调度中断 + 补数场景缺数**:调度断几天后补数,某业务日的单 `update_time` 已漂到数天后的 `ods.dt`,N=2 窄窗(只扫 `ods.dt IN (dt,pdt)`)扫不到漂走的版本 → 缺数(2026-05 实测 `dwd.dt=20260522` 缺 31%)
+  3. (叠加 status-before-rownum bug,已单独修复:status 移到 ROW_NUMBER 外层对 id 最新版本判,见 backfill / 日调度 SQL)
+
+- **退款窗实测(2026-06-03,定 N=30 依据)**:
 
-  本项目 ods 层按 ADR-03 严格按 `update_time` 归位 dt(双源 union 完整捕获 `update_time ∈ [T-1, T)` 范围所有版本),dwd 事件表理论上单分区扫 `ods.dt=T-1` 即可拿到所有"业务时间 T-1 + update_time T-1"的事件。
+  5 月退款单 90,769(`refund_time` 100% 非空),`DATE(refund_time) - DATE(payment_success_time)` 分布:
 
-  但业务库 OLTP 写入业务事件时间(如 `payment_success_time`)与 `update_time` 不一定严格同步刷新——典型场景:跨零点支付,`payment_success_time = T-1 23:59:59.500`,业务库 ORM 延迟 600ms 刷 `update_time = T 00:00:00.100`,该行落 `ods.dt=T`;事件本应归 `dwd.dt=T-1`,但 dwd 单分区扫 `ods.dt=T-1` 拿不到。这种漂移在业务高峰跨零点场景下不可忽略。
+  | 延迟 | 占比 |
+  |---|---|
+  | 0-7 天 | 39% |
+  | 8-15 天 | 58% |
+  | 16-30 天 | 2.8% |
+  | 30+ 天 | 0 |
 
-  本项目第一张 dwd 事件表 `dwd_trd_order_pay_apd_d` 落地反复讨论后定下范式(5/9 一度采纳"信 OLTP 契约不回算" → 5/10 自破契约编漂移场景 → 复盘 sessions 5/9 T8-T10 发现违反共识 → 用户重新拍"后端不可信"→ 业界范围 N=2/3 拍 N=2 → commit `a9b6eaa`)。
+  **P50=9, P90=P95=15(硬边界), P99=20, MAX=20**。退款占非有效态 98%,故退款窗 ≈ 订单状态稳定窗。业务方"15 天退款窗"= P95,准确
 
-- **决策**:DWD 事件表跑批回算近 N=2 日:
+- **决策**:DWD 事件表日调度改 **每天滚动重算最近 N=30 天**
 
-  - sche sched=T 时,扫 `ods.dt IN (${dt}, ${pdt})`(即 T-1 + T-2)
-  - 过滤业务时间 `DATE(business_event_time) IN (${dt}, ${pdt})`(如 dwd_pay 用 `payment_success_time`)
-  - 写入 dwd `dt IN (${dt}, ${pdt})`:`INSERT OVERWRITE PARTITION (dt)` 动态分区,kb/26 §8 项目默认 DYNAMIC mode 只覆盖 SELECT 出现的 dt,不动其他历史分区
-  - `dwd.dt=T-1` 在 sched=T 跑批时首次写入;sched=T+1 跑批时通过扫 `ods.dt=T` 兜回业务时间 T-1 漂移到 `ods.dt=T` 的事件,重写 `dwd.dt=T-1`
-  - N=2 业界主流(阿里 OneData / 字节 / 美团默认)
+  - 每天 `start_date = today-30`、`stop_date = cdt`(>退款窗 MAX 20 + buffer,覆盖所有退款/状态变化)
+  - 复用 ADR-11 backfill 机制(**宽扫窄落 + status 在 ROW_NUMBER 外层判最新版本**):ods.dt 宽扫 `[start_date-1, 不限]`、payment 窄落 `[start_date, stop_date)`
+  - 滚动窗内每天全覆盖重算 → dwd 恒 ≈ PG 当前快照;>30 天的老数据退款窗已过、稳定,不重算
+  - 成本:~30 天 ≈ 200万单/天,几分钟(比每天全量 2 小时轻、比 N=2 准、比"当月重算"均匀不受月边界影响)
+
+- **为什么不是 N=2 也不是全量**:
+
+  - N=2 < 退款窗 20,窗外漂移漏(上述问题 1/2)
+  - 每天全量(实测 2 小时)太重,不可每天跑
+  - 滚动 N=30 是"覆盖退款窗 + 成本可控"的平衡点
+
+- **临时性**:N=30 滚动仍是"快照口径"(每天重算反映当前状态),非纯事件表。**目标态见 ADR-12**:支付事件(不筛 status,收全部 `payment_success_time IS NOT NULL`)+ 退款事件独立表,净值 = 支付 LEFT JOIN 退款 —— 届时支付事件不可变、无需回算窗,退款独立 append。
 
 - **后果**:
 
-  - 正面:
-    - 兜底跨零点 ods 漂移(业务库 `update_time` 不严格同步刷新场景)
-    - 不依赖 OLTP 应用层契约的强假设
-    - 与 kb/20 §7.3 通用兜底一致
-  - 负面:
-    - 每个 dt 分区被回算 2 次(首次 + 次日兜底),写入 / 计算成本上升 2×
-    - 动态分区写入需注意 dynamic mode 默认行为(kb/26 §8 已实测)
+  - 正面:覆盖退款窗全程,dwd ≈ PG;调度中断 ≤30 天可自愈(滚动窗自然 cover);统一 backfill 机制(日调度 = 每天滚动 backfill)
+  - 负面:每天重算 30 天(30× 单日成本,但绝对值几分钟可接受);数据量大涨后需评估降 N 或迁 ADR-12
 
 - **候选方案**:
 
-  - **N=1 单分区不回算**(5/9 一度采纳):基于"业务库 update_time 与业务事件时间同步刷新 OLTP 契约"假设;OLTP 后端实际不可信,跨零点漂移会永久丢失事件——否决
-  - **N=3 回算近 3 日**:偏保守,金融 / 强稳定性场景;本项目业务侧无此强稳定性需求,2× 写入对齐业界默认即可——否决但留作"业务库延迟 > 1 天频繁时上调"反悔触发
-  - **N=7 回算近 7 日**:极端保守,少见——否决
-  - **MERGE INTO**(Spark 3+ / Iceberg / Hudi / Delta Lake):不需回算逻辑,引擎原生 upsert 处理漂移——本项目 Spark 2.4 不支持,本阶段不取
-  - **延后 1 天跑批**(sched=T+1 写 dwd.dt=T-1,扫 ods.dt IN (T-1, T)):dwd 数据延迟 1 天可用,下游标签延迟,业务侧不可接受——否决
+  - **N=2**:窗外退款漏、补数缺数——否决(本 ADR 起因)
+  - **每天全量**:实测 2 小时,太重——否决
+  - **每天当月重算(month-to-date)**:月末跨月退款漏(5-28 单退款到 6 月),覆盖不均——否决,滚动 30 天更均匀
+  - **MERGE INTO**(Spark 3+ / Iceberg / Hudi):引擎原生 upsert 无需回算——Spark 2.4 不支持,本阶段不取
+  - **支付/退款独立 dwd**(ADR-12):目标态,本阶段先滚动 N=30 过渡
 
 - **反悔条件**:
 
-  - 业务库延迟 > 1 天的漂移场景频繁出现(如系统升级 / 故障恢复多日数据回流),N 上调到 3 或更大
-  - 迁 Spark 3+ / Iceberg / Hudi / Delta Lake 后改 MERGE INTO(更优)
-  - 业务侧能严格保证 OLTP 应用层契约(update_time 与事件时间同步刷新),可降到 N=1(实际很难保证)
+  - 数据量大涨,滚动 30 天成本不可接受 → 降 N 或迁 ADR-12
+  - 退款窗实测变化(业务流程调整)→ 重测调 N
+  - 迁 Spark 3+ / Iceberg / Hudi → MERGE INTO
+  - ADR-12 支付/退款独立落地 → 本 ADR 回算窗下线
 
 ### ADR-10 TDM 跨层下钻 DWD(1 期专用,dws 单下游场景)