订阅更新

数据校验放事前还是事后?——数据质量门禁(CI/CD for Data)实战

数据质量手册 第 4 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 传统数据开发的「事后」之痛
  3. CI层:开发阶段的质量门禁
  4. CD层:上线阶段的质量门禁
  5. Monitor层:运行阶段的实时门禁
  6. 三层门禁责任分工
  7. 面试高频追问
  8. 实战踩坑:质量门禁导致的灰度事故
  9. 总结

先说结论

数据质量门禁要分三层:开发阶段(CI)、调度阶段(CD)、生产阶段(Monitor)。三层各有职责,缺一不可:

• CI层(开发阶段): SQL代码评审、单元测试、Lint、Sample数据校验

• CD层(上线阶段): 任务依赖检查、上游就绪校验、空跑Dry-Run、灰度发布

• Monitor层(运行阶段): DQC实时监控、异常告警、自动止损

核心理念:数据质量左移——越早发现问题,修复成本越低。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

软件工程里有CI/CD,每次代码提交跑测试、过门禁、才能上线。数据领域也应该有同样的概念——数据质量门禁(Data Quality Gate)。这道题在中高级和Tech Lead面试里非常加分,因为它考的是「工程化思维」:你能不能把质量从「事后救火」变成「事前预防」。今天聊聊这套体系怎么落地。

传统数据开发的「事后」之痛

我刚到当前出海短剧公司时,团队的数据质量是这样的:开发同学写完SQL直接发上线,第二天业务方说数据不对,再查、再改、再补数。一次故障的MTTR(平均修复时间)平均要4-6小时,每周至少2-3次。

# 典型的「事后」工作流
# 1. 开发: 写SQL → 直接合到生产分支
# 2. 调度: 凌晨自动跑 → 跑成功就行
# 3. 业务: 早上看报表 → 发现数据不对 → 找数据团队
# 4. 数据团队: 紧急排查 → 补数 → 道歉
# 问题:业务感知问题比数据团队还早!

这种模式的本质问题:

• 缺少代码评审,劣质SQL直接进生产

• 缺少环境隔离,开发即生产

• 缺少回滚机制,出了问题只能补数

CI层:开发阶段的质量门禁

借鉴软件工程的CI流程,每次SQL变更走Git PR,触发自动化检查:

# .gitlab-ci.yml 数据CI流水线示例
stages:
  - lint        # SQL语法/规范检查
  - test        # 单元测试 + 样本数据验证
  - dq-rules    # 数据质量规则的语法校验
 
sql-lint:
  stage: lint
  script:
    - sqlfluff lint sql/ --dialect=ansi
    - python check_naming_convention.py sql/  # 表名/字段命名规范
 
unit-test:
  stage: test
  script:
    # 用样本数据(1%)在测试环境跑一遍,输出和断言比对
    - python run_sql_test.py --env=test --sample=0.01
 
dq-validation:
  stage: dq-rules
  script:
    - python validate_dq_yaml.py dq_rules/  # DQC规则YAML schema校验

具体每一步做什么:

检查项工具拦截内容
SQL Lintsqlfluff/sqlcheckSELECT *、未限制日期分区、缺字段注释
命名规范自研Python脚本表名不符合分层规范(dwd_/dws_/ads_)
单元测试DBT/dbtl-utils已知输入→预期输出,类似单测
数据契约JSON Schema/Avro字段类型、枚举值、必填性
DQC规则校验自研DQC规则YAML语法、阈值合理性

CD层:上线阶段的质量门禁

代码合并到主干后,到调度系统上线前还有一道门禁。我在出海硬件公司搭了一套「上线检查清单」:

# 上线前自动化检查
def pre_deploy_check(task_id):
    checks = {
        # 1. 上游依赖检查
        'upstream_ready': check_all_upstream_tasks_exist(task_id),
        # 2. 输入表存在且有近期数据
        'input_table_health': check_input_tables_have_recent_data(task_id),
        # 3. 输出表权限/分区配置
        'output_table_config': check_output_table_partitioned(task_id),
        # 4. 资源配额预估
        'resource_estimate': estimate_resource_within_quota(task_id),
        # 5. 空跑(Dry Run):在测试环境用昨日数据跑一遍
        'dry_run_pass': dry_run_with_yesterday_data(task_id),
        # 6. DQC规则已配置
        'dqc_rules_configured': check_dqc_rules_exist(task_id),
    }
    failed = {k:v for k,v in checks.items() if not v}
    if failed:
        raise Exception(f'Pre-deploy check failed: {failed}')

特别强调Dry Run——用昨天的数据在测试环境跑一遍新SQL,结果和昨天历史结果对比。这一步能拦截90%的「逻辑写错」类问题。

Monitor层:运行阶段的实时门禁

任务跑起来后,DQC作为最后一道门禁,决定数据是否能流向下游:

# 任务执行流程(DataWorks/DolphinScheduler通用模式)
def run_etl_task():
    # Step 1: 上游就绪检查
    wait_for_upstream(timeout=2*3600)
    
    # Step 2: 执行ETL
    execute_sql('insert overwrite table dwd_order ...')
    
    # Step 3: 强DQC规则(不通过则阻断下游)
    if not run_strong_dq_rules('dwd_order'):
        block_downstream()
        alert('P0', 'dwd_order strong DQ failed')
        return FAILURE
    
    # Step 4: 弱DQC规则(仅告警)
    if not run_weak_dq_rules('dwd_order'):
        alert('P1', 'dwd_order weak DQ warning')
    
    # Step 5: 标记产出就绪,触发下游
    mark_table_ready('dwd_order')
    return SUCCESS

强规则阻断是核心机制——P0规则不通过,下游不能跑。这是把「数据质量」从软约束变成硬约束的关键。

三层门禁责任分工

层级时机责任人拦截能力成本
CI(开发)代码提交时开发自己+Reviewer语法、规范、明显逻辑错低
CD(上线)任务部署时Tech Lead审批依赖、配置、Dry Run中
Monitor(运行)任务执行中/后调度系统+DQC数据本身的质量问题高

三层成本递增、修复代价递增。所以原则是「能在CI拦的不要到CD、能在CD拦的不要到Monitor」。

面试高频追问

追问一:CI/CD for Data和软件CI/CD最大的区别是什么?

三个区别:第一,数据有「时间维度」——同一份SQL今天跑和昨天跑结果不同,单元测试要用快照数据;第二,数据有「上游依赖」——任务能不能跑取决于上游表是否就绪,不是代码本身;第三,数据有「不可回滚性」——错的数据已经被下游消费了,光改代码没用,必须补数。所以数据CI/CD里Dry Run和上游就绪检查比软件领域更重要。

追问二:dbt在CI/CD for Data里是什么角色?

dbt是数据领域CI/CD最完整的工具之一。它把SQL变成「数据模型代码」(带ref、source、test),原生支持单元测试(dbt test)、文档生成(dbt docs)、血缘可视化、版本管理。在我接触的团队里,dbt主要在多云/Snowflake生态用得多;在阿里云生态用DataWorks原生方案+自研CI多一些。两者思路一致,工具不同。

追问三:质量门禁太严会拖慢发布速度,怎么平衡?

两个原则:第一,分级门禁——P0表(财务、核心指标)严控,P2表(探索分析)可放松;第二,快速反馈——所有CI检查必须5分钟内出结果,否则开发不会等。我们在出海硬件公司把全套CI压到3分钟,开发体感几乎无感知。门禁严不严不是问题,反馈慢才是开发的敌人。

实战踩坑:质量门禁导致的灰度事故

我在出海硬件公司刚上线CI/CD门禁时,犯过一个错——把所有DQC规则一刀切都设为P0强规则,结果某天上游CDC延迟了2小时,DQC检测出dwd_order表数据量比平时少30%,自动阻断了所有下游20+张表。但其实只是延迟,再等1小时数据就齐了。值班同学被钉钉@炸了,紧急把强规则降级,下游回血用了4小时。

# 教训:P0规则必须有「时间窗口」机制
def check_data_volume_with_window():
    today_count = get_today_count()
    avg_count = get_baseline_count()  # 过去30天均值
    
    # 错误:直接判定
    # if today_count < avg_count * 0.7: block_downstream()
    
    # 正确:等待窗口期,避免短期波动误伤
    wait_minutes = 0
    while today_count < avg_count * 0.7 and wait_minutes < 120:
        time.sleep(600)  # 每10分钟重试
        today_count = get_today_count()
        wait_minutes += 10
    
    if today_count < avg_count * 0.7:
        block_downstream()
        alert('P0')

教训:强规则不等于死板规则。引入容忍窗口、动态阈值、人工干预通道(值班同学可以手动放行),才能让门禁既能拦故障又不阻塞业务。

总结

数据质量门禁三层:CI(开发左移)、CD(上线左移)、Monitor(运行兜底)。核心思想是「质量左移」——越早发现修复成本越低。面试时按这三层讲清楚分工和成本递增关系,再用一个落地的小故事(dbt/sqlfluff/Dry Run)收尾,能展现完整的工程化思维。

下一篇聊一个新表上线必经的环节:数据剖析(Data Profiling)。一张新表上线前必做的体检清单,老司机都在用。

你们公司有没有数据CI/CD?最难推的是哪一层?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器