首页/运维/数据漂移治理

数据漂移治理

KB 描述的是「我们以为数据长什么样」,数据库会变。漂移治理要回答的不是 「变了没有」,而是「变了之后哪些答案不再可信、该怎么处置」。

四级分类:能自动检测的只有前两级

漂移被分成四级。这个分类的价值在于明确说出哪些不检测—— 比含糊地声称「全面监控」诚实,也更有用。

级别含义能否自动检测
L1 结构漂移 表/列还在不在——对象存在性 ✅ 比对 KB schema_notes.yml 与活库
L2 引用漂移 语义模型里引用的表/字段是否还可达 ✅ 由语义层 provider 计算
L3 语义漂移 取值是否还能锚定、关系是否可加 ❌ 尚未实现(判据上需要活库探测)
L4 口径漂移 业务的算法改了(如坏账定义变了) ❌ 不检测,只承接外部声明

L3 那一格写的是判据上的可能性,不是现状:要锚定取值得先探测活库, 而这条探测还没接——今天真正实现的检测器只有 L1、L2(SUPPORTED_LEVELS)。 并且请求一个未实现的级别会被明确报错,而不是返回一份空报告: 「L3 干净」与「L3 没跑」在报告上必须是两件事,这正是这一层其余设计的前提。

models.py:36 L1 service.py:49 SUPPORTED_LEVELS service.py:176 UnknownLevel

flowchart LR
    LIVE["活库 schema"]:::ext --> D1["L1 结构漂移
表 / 列还在不在"]:::exec LIVE --> D2["L2 引用漂移
语义模型引用的表 / 字段还可达吗"]:::exec EXT["外部声明
治理流程 / 人的通知"]:::ext --> D4["L4 口径漂移
业务算法改了,库里没有痕迹"]:::ext D1 --> ENT["同一条漂移条目
影响面快照 + 同一套处置流程"]:::entry D2 --> ENT D4 --> ENT classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px; classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px; classDef ext fill:transparent,stroke:#71717a80,stroke-width:1.5px,stroke-dasharray:4 4;
图 1 · 四级里只有 L1、L2 是检测出来的,L4 是「承接」进来的声明——所以它们最终落在同一条条目上,走同一套影响面与处置流程。L3 尚未实现,请求它会被明确报错,而不是返回一份空报告。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。
为什么把 L4 明确写成「不检测」

「本季度的不良率口径改了」这件事,数据库里没有任何痕迹—— 表和列都没变,变的是一句业务定义。任何声称能自动发现它的系统都是在猜。

所以 L4 的设计是承接:接受外部声明(来自数据治理流程或人的通知), 把它记成一条漂移条目,然后走同一套影响面与处置流程。承认边界,然后在这个边界内做到最好。

检测与落库

检测的唯一入口是一个服务类。检测结果会落库——这一点是这次改造的核心: 漂移从「报完即忘」变成了有状态的、可处置的条目。

service.py:91 DriftService service.py:130 detect schema_drift.py:16 detect_drift

存储层有两条值得单独说的设计。第一,幂等:以 (datasource, level, kind, subject) 做唯一键,重检只刷新「最后见到时间」与 「见过几次」,不会产生重复条目。

store.py:63 _SEMANTIC_DRIFT_SQL store.py:105 _migrations

第二,已解决的条目再次被检出 = 回归,会重新打开为 open。 这一条很关键:如果没有它,「已解决」会变成一个谎言—— 修好的问题又出现了,而系统还显示一切正常。 被显式豁免(waived)的条目则保持豁免,不再打扰。

后台巡检:serve 自己只会跑 L1

trove serve 带一条后台巡检(retention.sweep_interval_hours, 默认 24 小时)。它挂在记忆子系统的生命周期清理上,顺带跑一次 detect_schema_drift()(开关 memory.schema_drift_check,默认开) ——只跑 L1:拿 KB 的 schema_notes.yml 比活库列集合。 想跑 L2(语义引用)得有人在 API 或 check_drift.py 上显式触发。

两个细节决定这条巡检有没有用。第一,它和 /v1/admin/drift 读写同一个 drift 库(组装时注入同一个 DriftStore)—— 两处各建各的库,表现就是「巡检跑了,管理端一条也看不到」。 第二,落库的是全部数据源,返回的只是有漂移的那些: 干净的一次也要留痕,否则历史里只剩有漂移的 run,读起来像这个库一直在漂。

app.py:245 run_lifecycle memory/service.py:465 detect_schema_drift main.py:301 drift_store

检测没跑完 ≠ 没有漂移

这是最容易出事的地方。拿不到活库 schema 时,系统不会报告「干净」, 而是标记为 skipped——本次检测只写一行运行记录,不碰任何条目。

models.py:185 levels_verified models.py:187 verified

报告里带一个「哪些级别真的被验证过」的集合,调用方可以据此区分 「检查过且没问题」与「没检查成」。把「不知道」和「没问题」分开, 是这个模块里最值得学的一个设计。

条目状态与处置

状态含义
open检出,待处理
acknowledged已看到,处理中
resolved已解决(再次出现会重开)
waived已知且接受,不再提醒

models.py:43 STATUS_OPEN service.py:240 resolve service.py:261 declare_external

处理必须给理由,这是硬约束

resolve 强制要求一个 reason,代码里的原话是「无理由的 resolve 等于删记录」。

这条约束防的是一种很常见的退化:把「关掉告警」当成「解决问题」。 半年后有人问「这个漂移当时怎么处理的」,理由字段是唯一的答案。

影响面:发现时冻结的快照

一条漂移的危害程度取决于有多少东西依赖它: 哪些指标引用了消失的列、哪些示例用了它、哪些规则提到它。

维度内容
metrics受影响的指标
examples引用了它的 KB 示例
rules提到它的规则
lessons相关的教训

models.py:122 metrics service.py:225 impact

影响面读的是发现时冻结的快照,不实时重算。因为「当时影响面有多大」 是处置决策的依据,事后重算会随着 KB 的修改而漂移,让记录失去意义。

现状:形状与读取口已落地,漂移检测侧写入口未接

上面这张表对漂移条目而言目前仍是契约,不是现有数据。

四个维度、ImpactSet 形状、读取 API(impact())都已实现, 并有测试正面钉住;填充它的 ImpactResolver(drift/impact.py) 也已存在,并被语义变更评审消费——变更详情里的影响面就是它现算的。 但漂移检测的落库路径仍未接线:落库时 store.record(report) 不传 impact=,store.py 于是回退到空 ImpactSet,affected 列因此恒为空 (四个维度都是空列表)。

所以漂移条目上的「哪些指标引用了消失的列」今天仍问不出来,任何按影响面排序或过滤 漂移的界面读到的都是空集——要等检测侧落库时传入 impact= 才有答案。 这里冻结的是空快照,与变更评审现算的影响面不是同一条数据,不要混看。

API 与门禁脚本

端点作用
POST /v1/admin/drift/check跑一次检测
GET /v1/admin/drift列出条目
GET /v1/admin/drift/{id}单条详情 + 影响面快照
GET /v1/admin/drift/runs运行历史(含 skipped 的轮次)
POST /v1/admin/drift/{id}/resolve解决(需 reason)
POST /v1/admin/drift/{id}/waive豁免
POST /v1/admin/drift/external登记外部声明的漂移(L4 入口)

端点都挂在 /v1 前缀下(与其他路由一致)。除 /external 从请求体取 datasource 外,其余都要带 ?datasource=<名字>——条目按数据源隔离,没有「默认源」可用。

drift.py:162 check drift.py:180 503 drift.py:225 drift_detail

检测端点有未完成就返回 503 的约定,而不是「200 + 空列表」。 理由和上面 skipped 一样:一个空列表会被读成「没有问题」。

命令行门禁脚本 scripts/check_drift.py 的退出码分三档: 0 干净且每一项检查都真的跑了、1 发现漂移、 2 出错或任一检查未能完成。最后那句限定词是关键—— 它让「没检查成」不会被 CI 当成通过。

check_drift.py:51 _check_datasource

继续读

想了解去哪
KB 的 schema_notes 是什么知识库
语义模型引用不达会怎样语义层
管理端怎么处置运维管理
其他离线门禁脚本评测与回归门
本页源码锚点的基准提交:anchor-base: 86ca61e GitHub 仓库 文档首页