数据校验放事前还是事后?——数据质量门禁(CI/CD for Data)实战
先说结论
数据质量门禁要分三层:开发阶段(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 Lint | sqlfluff/sqlcheck | SELECT *、未限制日期分区、缺字段注释 |
| 命名规范 | 自研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年大数据老兵,聊技术、聊职场、聊成长