Published

DataOps 第三集:数据可观测性——从指标到血缘

Author

Robin

前言:管道又死了,但没人知道

还记得开篇那个故事吗:凌晨三点,老板贴出 GMV 报表——上游改了个字段名,ETL 没报错,只是默默地把一整列写成 NULL。报表照常产出,数字照常是错的。

这类事故有个共同的名字:静默失败(silent failure)。管道看起来是绿的,任务都跑完了,日志里没有 ERROR,但产出的数据是错的、是旧的、是不全的。传统的"任务成功率监控"对它毫无办法——监控只告诉你管道有没有跑完,回答不了"跑出来的东西对不对"。

这正是数据可观测性要解决的问题。借用站内可观测性系列的一句话:

Monitoring tells you whether something is wrong. Observability tells you why.

把这句话翻译到数据世界就是:任务状态告诉你管道有没有跑完,数据可观测性告诉你产出的数据还能不能信。

本篇是 DataOps 系列第三集。我们从五大支柱讲起,搭指标体系,再落地血缘追踪,最后讲告警怎么才不扰民。

一、数据可观测性的五大支柱

业界对数据可观测性没有统一标准定义,但实践里大家基本收敛到五个维度。我习惯把它们叫"五大支柱",少了任何一个,你的管道都有盲区:

1. Freshness(新鲜度)

数据是不是按时到达的?昨天的报表有没有包含昨天的数据?

这是最基础、也最容易被忽视的维度。上游延迟两小时,你的下游报表如果没有新鲜度检查,就会用"缺了两小时"的数据算出一个"看起来正常"的数字。dbt 的 source freshness 就是为这个设计的——后面会给出配置示例。

2. Volume(数据量)

这一批的数据量跟平时比正常吗?

行数突增 10 倍,可能是上游重放了数据;行数骤降到 0,可能是上游分区写错了。数据量异常是很多静默失败的第一个信号,而且极其廉价——count(*) 就能算。

3. Distribution(分布)

数据的"形状"变了没有?

订单金额的均值突然从 300 掉到 30?某个枚举字段新增了一个从没见过的值?性别字段的 NULL 率从 1% 飙到 40%?分布漂移往往意味着业务逻辑或上游 schema 变了,而你的管道还在用旧假设跑。

4. Schema(结构)

表结构变了没有?字段类型、字段名、必填性有没有悄悄改?

开篇惨案的直接凶手就是 schema 变更:上游改了字段名,ETL 按旧名字取数,取到 NULL 也不报错。schema 监控是防止这类事故的最后一道门——字段删除、类型变更必须在进生产前被拦截。

5. Lineage(血缘)

这份数据从哪儿来,又流向了哪儿?

前四个支柱回答"数据现在是什么状态",血缘回答"数据经历了什么"。上游改了字段,你能不能在 10 分钟内列出所有受影响的下游报表和 ML 模型?如果不能,你的事故响应就是人肉翻 SQL。血缘是数据可观测性里最难建、但事故时价值最高的一块。

支柱回答的问题典型信号
Freshness数据按时到了吗延迟分钟数、SLA 达成率
Volume量级正常吗行数、字节数同比
Distribution数据形状变了吗均值/分位数、枚举值集合、NULL 率
Schema结构变了吗字段增删改、类型变更
Lineage从哪来、到哪去上下游依赖图、影响面分析

二、指标体系怎么建:从 Prometheus 开始

理论有了,怎么落地?我的建议:别一上来就买商业平台,先用你已经会的东西把指标跑起来。你已经有了 Prometheus + Grafana,这套组合完全能撑起前四个支柱的指标层。

思路很简单:每个管道、每个关键数据集,定期计算五个维度的指标,用 Prometheus 的 exposition 格式暴露出去,Grafana 做看板和告警。

下面是一个极简的 Python exporter 骨架(语言:Python,用 prometheus_client):

from prometheus_client import Gauge, start_http_server
import time

# 新鲜度:距离上次成功写入过去了多少秒
freshness_lag_seconds = Gauge(
    "pipeline_freshness_lag_seconds",
    "Seconds since the last successful write",
    ["pipeline", "dataset"],
)

# 数据量:本次批次行数
batch_row_count = Gauge(
    "pipeline_batch_row_count",
    "Row count of the latest batch",
    ["pipeline", "dataset"],
)

# 分布:关键指标的 NULL 率
null_ratio = Gauge(
    "pipeline_column_null_ratio",
    "NULL ratio of a monitored column",
    ["pipeline", "dataset", "column"],
)

def collect(pipeline: str, dataset: str):
    # 这里换成你真实的查询逻辑:查元数据表、查数仓
    lag = query_last_write_lag(pipeline, dataset)
    rows = query_batch_row_count(pipeline, dataset)
    nulls = query_null_ratio(pipeline, dataset, "amount")

    freshness_lag_seconds.labels(pipeline, dataset).set(lag)
    batch_row_count.labels(pipeline, dataset).set(rows)
    null_ratio.labels(pipeline, dataset, "amount").set(nulls)

if __name__ == "__main__":
    start_http_server(8000)
    while True:
        for pipeline, dataset in PIPELINES:
            collect(pipeline, dataset)
        time.sleep(300)  # 每 5 分钟采集一次

Grafana 里给每个指标配两条线就够用:当前值和历史基线(比如过去 7 天同一时段的均值 ± 3 倍标准差)。行数突然腰斩,一眼就能看出来。

如果你用 dbt,新鲜度甚至不用自己写 exporter。dbt 原生支持 source freshness,直接在 YAML 里声明:

# models/sources.yml
version: 2

sources:
  - name: shop
    tables:
      - name: orders_raw
        loaded_at_field: created_at
        freshness:
          warn_after: {count: 6, period: hour}
          error_after: {count: 12, period: hour}

dbt source freshness 跑完,哪个源表迟到了、迟到了多久,清清楚楚。把这条命令塞进 CI 或者调度里,新鲜度监控就有了。

schema 变更的拦截,放在 CI 里做最划算:每次 PR 自动对比 manifest.json 里模型的字段列表,新增/删除/改类型都必须显式确认。DataOps 开篇里讲的"CI for data",在这里就是具体的一条流水线规则。

三、血缘追踪实战:OpenLineage + Marquez

指标体系解决"数据现在健不健康",血缘解决"出事了我该找谁、影响了谁"。自建血缘系统的事实标准组合是 OpenLineage + Marquez:

  • OpenLineage:开放的血缘元数据标准(openlineage.io),LF AI & Data 基金会项目。它定义了 Job、Run、Dataset 三个核心概念,以及统一的事件格式——谁产生、谁消费,一次讲清。
  • Marquez:OpenLineage 的参考实现(marquezproject.ai),一个元数据服务:收 OpenLineage 事件、存进 PostgreSQL、提供可视化血缘图和 Lineage API。

这个组合的好处是解耦:Airflow、Spark、Flink、dbt、Dagster 都有 OpenLineage 集成,事件发到 Marquez(或者其他兼容后端),换工具不换标准。

步骤 1:把 Marquez 跑起来

官方提供 Docker Compose 一键启动(API + Web UI + PostgreSQL),一条 docker compose up 之后,API 监听 5000 端口,Web UI 在 3000 端口。先确认 UI 能打开,再往里面发事件。

步骤 2:让 Airflow 上报血缘

Airflow 2.7 之后有官方的 apache-airflow-providers-openlineage provider。配置方式是环境变量或 airflow.cfg 的 [openlineage] 段,核心就两项:发给谁(transport URL)、命名空间是什么。

# airflow.cfg
[openlineage]
namespace = data-team
transport = {"type": "http", "url": "http://marquez:5000", "endpoint": "api/v1/lineage"}

配好之后,DAG 每次运行的 START / COMPLETE 事件会自动发往 Marquez,输入输出数据集的边也自动画出来。不需要改 DAG 代码——这是 OpenLineage 集成最香的地方。

注意:上面是 2.7+ 的新版配置写法;老版本(2.5–2.7 之前)用的是 OPENLINEAGE_URL 等环境变量。按你实际的 Airflow 版本查 provider 文档确认,别硬套。

步骤 3:让 dbt 也接入

dbt 侧有 openlineage-dbt 集成(据公开资料,社区提供了 dbt-ol 命令封装 dbt run,跑完自动上报血缘事件)。这样血缘图就不会断在"Airflow 调度 → dbt 转换"的边界上——同一套标准,图是连通的。

步骤 4:补上"最后一公里"——dbt exposures

OpenLineage 告诉你数据在管道内部怎么流转,但报表和看板在管道外面。dbt 的 exposures 就是用来声明"这个模型被哪些下游消费"的:

# models/marts/_exposures.yml
version: 2

exposures:
  - name: exec_gmv_dashboard
    type: dashboard
    maturity: high
    url: https://bi.company.com/dashboards/gmv
    description: "管理层每日 GMV 看板"
    depends_on:
      - ref('fct_orders')
      - ref('dim_customers')
    owner:
      name: 数据团队
      email: data@company.com

exposures 不跑 SQL,它唯一的 job 就是把血缘图延伸到 BI 层。有了它,fct_orders 要改字段时,你能提前知道"管理层 GMV 看板会受影响",而不是等老板凌晨三点贴图。

步骤 5:验证血缘图

跑一次完整的 DAG,去 Marquez UI 里点开任意一个 dataset:上游是哪些 job 产出的,下游被哪些 job 和 exposure 消费,一张图全有。这张图就是你的事故响应地图——下次上游出问题,影响面分析从"人肉翻 SQL"变成"点开 UI 看一眼"。

四、异常告警策略:阈值 vs 异常检测,告警分级与降噪

指标和血缘都建好了,最后一步是告警。这一步做不好,前面全白费——要么告警太多没人看,要么关键告警淹没在噪音里。

阈值告警 vs 异常检测

阈值告警:简单、确定、可解释。"新鲜度延迟超过 2 小时就告警"——人人都懂。缺点是阈值是拍脑袋定的,业务有周期性(比如大促期间数据量翻 5 倍是正常的)时,阈值告警会疯狂误报。

异常检测:基于历史基线动态判断。"行数偏离过去 14 天同时段均值超过 3 个标准差就告警"。优点是自适应业务周期,缺点是需要历史数据积累,且"为什么告警"不如阈值直观。

实战建议:两者混用。硬红线(比如"核心报表延迟超过 SLA")用阈值,一拍即合;软信号(分布漂移、量级波动)用异常检测,减少误报。先让阈值跑起来,再逐步把高噪音的规则迁到异常检测上。

告警分级

不是所有异常都值得凌晨三点叫醒人。建议三级:

  • P0(立即处理):核心数据产品不可用或数据错误已流向业务方。比如 GMV 看板数字错了、核心事实表延迟超过 SLA。电话 + 短信,直接叫醒。
  • P1(工作时间处理):非核心管道异常,或核心管道的预警信号。比如某个非关键维表延迟 1 小时。工作群 @ 相关人,当天处理。
  • P2(记录跟踪):轻微偏离基线,可能是噪音。比如某列 NULL 率轻微上浮。只记 ticket,每周 review 一次,连续出现再升级。

分级的关键是事先定义好什么算 P0,而不是出事了再争论"这算不算严重"。

降噪:让告警值得被看

三个实战技巧:

  1. 告警合并:同一个根因的告警只发一条。上游源表延迟了,下游 20 个模型的新鲜度告警应该合并成一条"上游延迟,下游 N 个模型受影响"——血缘图在这里又派上用场了:按血缘做告警分组,天然就是根因分组。
  2. 抖动抑制:指标在阈值上下反复横跳时(flapping),不要每跳一次发一条。设置"持续 N 分钟才触发"和"恢复后冷却 M 分钟",Prometheus 的 for: 子句就是干这个的。
  3. 告警路由到人:每条告警必须有明确的 owner。dbt exposures 里声明的 owner、Marquez 里 job 的 owner,就是路由表。没有 owner 的告警等于没有告警——出了事没人认领,比没告警更糟。

最后一条铁律:每条 P0 告警都要有 runbook。告警信息里直接贴 runbook 链接,值班的人点开就知道第一步查什么、第二步找谁。没有 runbook 的 P0 告警,本质上是在用值班人员的睡眠做赌注。

结语:从"管道绿了"到"数据可信"

回顾一下这三集的路径:

  • 第一集,我们说 DataOps 是把 DevOps 的工程化纪律应用到数据管道:版本控制、CI、CD。
  • 第二集,我们讲了数据质量门禁:怎么在烂数据进生产前把它拦下来。
  • 这一集,我们讲了数据可观测性:五大支柱搭起"数据健康"的完整视图,OpenLineage + Marquez 画出"数据从哪来、到哪去"的地图,告警策略保证"出问题时有人知道、知道找谁"。

开篇那个凌晨三点的故事,如果当时有这套东西,会是什么样?schema 变更在 CI 就被拦截;即使漏过去了,新鲜度/分布指标会在第一时间告警;即使告警被忽略了,血缘图也能在 10 分钟内定位"GMV 看板的上游到底是谁改了字段"。

DataOps 的终点不是"管道永远是绿的"——那不现实。终点是:当数据出问题时,你能在业务方发现之前发现,在影响扩大之前定位,在复盘时有完整的证据链。 这就是数据可观测性要交付的东西。

下一集,我们讲数据管道的 CI/CD 与回滚策略:变更怎么灰度、怎么回滚。周三见。