自建数据质量检核引擎 vs 开源方案,选型踩坑记录
我们做数据治理平台的时候,数据质量检核是绕不开的模块。需求很明确:支持空值检查、唯一性校验、值域合规、跨表一致性、自定义 SQL 规则,支持定时调度和告警,能接入现有的数据治理平台。

面对这个需求,两条路:自建,还是用开源方案。
我们花了三周时间,对 Apache Griffin、Amazon Deequ、Great Expectations 三个开源方案做了深度 POC,同时搭了一个自建原型做对比。最终的选择出乎意料——核心检核引擎自建,辅助能力用开源补位。
这篇文章记录整个选型过程和踩过的坑,希望能帮到面临同样抉择的团队。
一、候选方案速览
1.1 Apache Griffin
Apache 基金会旗下的数据质量方案,2016 年进入孵化器,2018 年毕业。架构上分三块:Measure(检核规则定义)、Job(调度执行)、Service(结果展示)。
技术栈:Spark Livy Elasticsearch MySQL。检核逻辑在 Spark 上执行,通过 Livy 提交 Spark Job,结果写入 ES 和 MySQL。
1.2 Amazon Deequ
AWS 开源的数据质量库,基于 Spark 实现。核心是 Analyzer(指标计算)和 VerificationSuite(规则校验),纯库形式,不提供调度、UI、告警等平台能力。
1.3 Great Expectations
Python 生态最流行的数据质量框架,核心理念是"Expectation"——声明式定义数据应该长什么样。支持 Pandas、Spark、SQL 三种后端,文档和社区活跃度极高。
1.4 自建方案
基于 Spark SQL 规则引擎的思路,规则配置存 MySQL,Spark 任务定时执行,结果写回 MySQL,前端展示和告警用现有平台能力。
二、POC 对比
我们用同一份测试数据(5000 万行订单表,32 个字段,Hive 格式,Snappy 压缩),配置了 15 条典型质量规则,在三套方案上跑了一轮完整的 POC。
2.1 部署复杂度
方案部署组件首次可运行耗时GriffinSpark Livy ES MySQL ZK2 天DeequSpark(纯库依赖)30 分钟Great ExpectationsPython Spark/Pandas1 小时自建Spark MySQL4 小时
Griffin 的部署是最痛苦的。它依赖 Livy 来提交 Spark 任务,但 Livy 本身是一个半死不活的项目(Apache 孵化器,社区活跃度极低),版本兼容性令人抓狂。我们试了 Griffin 0.7 Livy 0.7,报错;降到 Griffin 0.6 Livy 0.6,还是报错;最后在 GitHub Issue 里翻到某个老哥的 Hack 方案,手动改了 Griffin 源码里的 Livy 客户端版本才跑通。
结论:Griffin 的部署成本远超预期,不建议在生产环境直接使用。
2.2 检核性能
5000 万行数据,15 条规则:
从执行结果看,几种方案在时间和资源占用上的差异还是比较直观的:Griffin 用时 4 分 12 秒,资源配置为 8 executors × 4g;Deequ 用时 2 分 48 秒,同样是 8 executors × 4g;Great Expectations(Spark)耗时 3 分 35 秒,配置依旧是 8 executors × 4g;自建方案最快,为 2 分 15 秒,资源占用也是 8 executors × 4g。
Deequ 和自建方案最快,两者的检核逻辑都是直接在 Spark DataFrame 上做聚合操作,没有额外的序列化和中间存储开销。Griffin 最慢,因为它需要经过 Livy 的 REST 接口提交任务,中间多了一层序列化/反序列化,而且它的 Measure 抽象层引入了额外的计算开销。
2.3 规则表达能力
能力GriffinDeequGreat Expectations自建空值检查✓✓✓✓唯一性✓✓✓✓值域/枚举✓✓✓✓正则匹配✓✗✓✓跨表一致性✓✗✗✓自定义 SQL✓✗✓✓时间窗口✓✓✗✓增量检核✗✓✗✓
Deequ 最大的短板是不支持跨表一致性检查。比如"订单表的客户 ID 必须在客户主数据表中存在"这种引用完整性校验,Deequ 做不了。这在生产环境是刚需。
Great Expectations 的跨表一致性也不支持,它的设计哲学是"一次只检查一个数据集"。
Griffin 的规则定义能力最全,但配置方式非常繁琐——需要通过 JSON 文件定义 Measure,格式复杂,可读性差。
2.4 平台集成能力
能力GriffinDeequGreat Expectations自建原生 UI✓✗✗可定制调度能力✓✗✗可定制告警通知有限✗✗可定制REST API✓✗✗可定制数据治理平台集成困难灵活灵活无缝
Griffin 有 UI 但不好用。 它的 UI 是 Angular 1.x 写的,2019 年后就没更新过,界面风格和交互体验停留在 5 年前。而且它的前端和后端耦合很紧,想把检核结果嵌入到我们自己的数据治理平台,需要改 Griffin 源码。
Deequ 和 Great Expectations 没有 UI。 它们都是库,不是平台。检核结果需要你自己消费、自己展示、自己告警。如果你已经有数据治理平台,这反而是优势——更灵活。
三、各自的坑
3.1 Griffin 的坑
Livy 依赖是最大痛点。 前面说了部署问题,但更麻烦的是运行时稳定性。Livy 的 Session 管理有严重的内存泄漏问题,长时间运行后 Session 会 OOM,导致 Spark Job 提交失败。我们 POC 期间跑了 3 天,Livy 挂了 2 次。
JSON 规则定义可维护性差。 一条"检查 customer_id 不为空"的规则,Griffin 的 JSON 配置超过 30 行:
代码语言:ja vascript复制{"name": "accu_check","type": "accuracy","process.type": "batch","data.sources": [{"name": "source","connector": {"name": "hive","version": "1.2","config": {"database": "ods","table.name": "orders"}}}],"evaluate.rule": {"rules": [{"dsl.type": "griffin-dsl","dq.type": "accuracy","rule": "customer_id IS NOT NULL","details": {"source": "source","target": "target"}}]}}
30 行 JSON 表达一条"不为空"规则,业务方根本没法自助配置。每次新增规则都要数仓开发来写 JSON,效率极低。
社区活跃度低。 Griffin 的 GitHub 最后一次 Release 是 2021 年的 0.7.0。2022 年至今只有 3 个 commit,基本上处于维护停滞状态。对于一个需要长期维护的基础组件,这个风险不可接受。
3.2 Deequ 的坑
Analyzer 和 VerificationSuite 是两个模型,关联逻辑复杂。 Deequ 的 Analyzer 负责计算指标(如完整性、唯一性、均值),VerificationSuite 负责检查规则。但如果你想把 Analyzer 的结果作为 VerificationSuite 的输入(比如"检查完整性是否低于上个月"),需要手动拼接两个 API,代码体验很差。
不支持自定义 SQL 规则。 Deequ 的规则定义基于它的 DSL,无法直接嵌入 SQL。如果你的质量规则是"订单金额 > 0 且订单金额 < 100000",没问题。但如果规则是"执行这段 SQL,结果集为空则通过",做不到。
Scala 优先,Ja va/Python API 不完整。 Deequ 的核心 API 是 Scala 的,虽然提供了 Python 封装(PyDeequ),但很多高级功能(如 Anomaly Detection、增量检核)在 Python 版中不可用。
3.3 Great Expectations 的坑
Expectation Suite 的 JSON 文件膨胀。 一个包含 50 条规则的 Suite,JSON 文件超过 5000 行。虽然 GE 提供了 CLI 和 Jupyter Notebook 交互式创建 Expectation 的方式,但在生产环境中,规则的管理和维护仍然是个问题——版本控制、规则复用、批量修改,都不方便。
Python 生态,与 Ja va/Spark 主栈的集成有摩擦。 我们的数据平台是 Ja va 技术栈,GE 是纯 Python 项目。虽然可以通过 Airflow 调度 GE 的 Python 脚本,但在日志、监控、告警等环节的集成成本比纯 Ja va 方案高。
Data Docs 好看但不够实用。 GE 的 Data Docs 是它的特色功能——自动生成漂亮的数据质量报告。但静态 HTML 报告在企业场景下不够用,你需要的是一个 API,让前端按需查询检核结果,而不是打开一个 HTML 文件。
四、最终方案:自建核心 开源补位
综合 POC 结果和团队情况,我们最终选择了自建核心检核引擎,部分能力用开源方案补位。
4.1 核心检核引擎:自建
架构很简单:
代码语言:ja vascript复制定时调度(Azkaban/DolphinScheduler)→ 拉取规则配置(MySQL)→ 生成 Spark SQL→ 提交 Spark Job→ 结果写回 MySQL→ 前端展示 告警
规则配置表设计:
代码语言:ja vascript复制CREATE TABLE dq_rule (idBIGINT PRIMARY KEY AUTO_INCREMENT,rule_name VARCHAR(200)NOT NULL COMMENT '规则名称',rule_type VARCHAR(50) NOT NULL COMMENT '规则类型:NOT_NULL/UNIQUE/RANGE/REGEX/CROSS_TABLE/CUSTOM_SQL',target_db VARCHAR(100)COMMENT '目标库',target_table VARCHAR(100) NOT NULL COMMENT '目标表',target_colVARCHAR(100)COMMENT '目标字段',rule_config TEXTNOT NULL COMMENT '规则配置JSON',expect_result VARCHAR(50) COMMENT '期望结果:PASS/FAIL',schedule_cron VARCHAR(50)COMMENT '调度表达式',enabled TINYINT DEFAULT 1,create_time DATETIMEDEFAULT CURRENT_TIMESTAMP);
规则配置 JSON 示例:
代码语言:ja vascript复制{"type": "NOT_NULL","threshold": 0.99,"comment": "customer_id 完整率不低于 99%"}代码语言:ja vascript复制
{"type": "CROSS_TABLE","source_table": "ods.orders","source_col": "customer_id","target_table": "dim.customer","target_col": "id","join_type": "LEFT_ANTI","comment": "订单表的客户ID必须在客户主数据表存在"}代码语言:ja vascript复制
{"type": "CUSTOM_SQL","sql": "SELECT COUNT(*) FROM ods.orders WHERE order_amount <= 0 OR order_amount > 100000","expect": "ZERO","comment": "订单金额不得为负数或超过10万"}
Spark 执行逻辑(核心代码):
代码语言:ja vascript复制def executeRule(rule: DqRule, spark: SparkSession): DqResult = {rule.ruleType match {case "NOT_NULL" =>val config = parseConfig(rule.ruleConfig)val totalCount = spark.sql(s"SELECT COUNT(*) FROM ${rule.targetTable}").collect()(0).getLong(0)val nullCount = spark.sql(s"SELECT COUNT(*) FROM ${rule.targetTable} WHERE ${rule.targetCol} IS NULL").collect()(0).getLong(0)val ratio = 1.0 - nullCount.toDouble / totalCountDqResult(rule.id, rule.ruleName, ratio >= config.threshold, ratio, s"完整率: ${ratio * 100}%")case "CROSS_TABLE" =>val config = parseConfig(rule.ruleConfig)val violateCount = spark.sql(s"""SELECT COUNT(*) FROM ${config.sourceTable} s |LEFT ANTI JOIN ${config.targetTable} t |ON s.${config.sourceCol} = t.${config.targetCol}""".stripMargin).collect()(0).getLong(0)DqResult(rule.id, rule.ruleName, violateCount == 0, violateCount,s"引用完整性违规: ${violateCount} 条")case "CUSTOM_SQL" =>val config = parseConfig(rule.ruleConfig)val result = spark.sql(config.sql).collect()(0).getLong(0)val pass = config.expect == "ZERO" && result == 0DqResult(rule.id, rule.ruleName, pass, result, s"自定义SQL结果: ${result}")}}
4.2 用开源补位:Deequ 做指标分析
自建规则引擎覆盖了 80% 的日常检核场景,但有一个能力我们没做——数据画像和异常检测。
Deequ 的 Analyzer 和 Anomaly Detection 在这块做得很好。我们把它作为辅助模块集成进来:每周跑一次完整的数据画像(完整性、唯一性、均值、标准差、直方图),用 Deequ 的 AnomalyDetectionStrategy 检测指标异常。
代码语言:ja vascript复制import com.amazon.deequ.analyzers._import com.amazon.deequ.anomalydetection.{AnomalyDetectionStrategy, RelativeRateOfChangeStrategy}val analyzers = Seq(Completeness("customer_id"),Uniqueness("order_id"),Mean("order_amount"),StandardDeviation("order_amount"))val analysisResult = AnalysisRunner.onData(df).addAnalyzers(analyzers).useRepository(metricsRepository)// 把历史指标一并纳入,用来做异常检测.run()
4.3 成本对比
如果把项目自建和开源方案放在一起看,差异其实很直接:首次开发分别是3 人月和1 人月;持续维护分别是0.5 人月/年和1 人月/年,后者这部分主要花在修兼容问题、跟进社区变化上。再往下看,定制化成本是低和高的区别;平台集成方面,一个是原生支持,另一个通常还得做二次开发;至于长期风险,一边更依赖团队本身,另一边则更看社区活跃度。
自建的前期投入更高,但长期维护成本更低,定制化效率远超开源方案。
五、选型决策框架
如果你的团队面临同样的抉择,我建议按以下决策树判断:
代码语言:ja vascript复制是否需要 UI 和调度能力?├─ 需要 → 团队有 Ja va 技术栈? → 自建├─ 需要 → 团队是 Python 栈? → Great Expectations Airflow└─ 不需要 → 继续是否需要跨表一致性检查?├─ 需要 → 自建 或 Griffin(如果接受部署复杂度)└─ 不需要 → 继续核心检核逻辑是否能用 SQL 表达?├─ 能 → 自建(成本最低,效果最好)└─ 不能 → Deequ(复杂统计分析场景)是否需要长期维护和社区支持?├─ 需要 → 自建 或 Great Expectations└─ 不需要 → Griffin(如果团队能接受停更风险)
六、最后说一句
数据质量检核引擎在技术上没有银弹——开源方案看起来"拿来就用",但真正接入生产环境后,你会发现大量的时间花在修兼容问题、补缺失功能、改 UI 集成上。这些时间加起来,往往不比自建少。
自建不意味着从零开始。 用 Deequ 做指标分析,用 Great Expectations 做数据文档,用 Spark SQL 做规则检核——把开源方案当作"库"而不是"平台",各取所长,组装成适合自己的方案。
技术选型的最高境界,不是选最好的方案,是选最适合你团队能力和业务场景的方案。
本文基于作者团队在数据治理平台建设中的真实选型经验。性能数据来自 5000 万行测试数据集,实际生产环境数据量级不同,结论可能不同,请以自身 POC 结果为准。
