3. 数据质量规则引擎架构设计:整体架构分层与核心模块

聊到规则引擎的架构,我习惯先画一张分层图。你想想看,一个规则跑起来,从数据进来,到规则匹配,再到结果输出,中间要经过好几个环节。如果全揉在一起,后期维护就是灾难。

我在项目中遇到过好几次这样的场景:业务方说“规则不生效”,结果一查,是数据源层和规则执行层的逻辑耦合太紧,改一处牵动全身。所以,分层这件事,不是理论上的花架子,是实实在在的避坑手段。

核心观点: 规则引擎的架构设计,本质上是在做“关注点分离”。每一层只干一件事,层与层之间通过标准接口通信。

3.1 整体架构分层

我把整个引擎分成四层,从上往下看:

  • 数据源层:负责对接各种数据来源。比如关系库、Hive、Kafka 消息、甚至是 Excel 文件。
  • 规则定义层:用户在这里配置规则。包括规则的条件、阈值、权重、生效范围等。
  • 规则执行层:引擎的核心。它拿到规则定义,去数据源层拉数据,然后执行校验逻辑。
  • 结果输出层:把执行结果包装成标准格式,推给下游。比如告警系统、数据血缘平台、或者直接写回表。

嗯,这里要注意:数据源层和规则执行层之间,一定要有数据抽象。我曾经见过一个项目,规则执行层直接写 JDBC 连接数据库,结果数据库一换,规则全得重写。说白了,数据源层应该把数据封装成统一的“数据记录”对象,执行层只管处理这个对象就行。

数据质量规则引擎 — 四层架构 数据源层 关系数据库 | Hive | Kafka | 文件系统 | API 规则定义层 规则配置 | 条件定义 | 阈值设置 | 权重管理 规则执行层 规则解析 | 数据拉取 | 校验执行 | 结果聚合 结果输出层 告警推送 | 质量报告 | 数据回写 | 指标统计 数据抽象接口 规则元数据 标准结果格式

3.2 核心模块设计

四层架构是骨架,核心模块才是血肉。我重点讲三个模块:规则解析器、规则调度器、结果评估器。这三个模块都在规则执行层里,但它们各司其职。

3.2.1 规则解析器

规则解析器干的事,就是把人类可读的规则配置,转成机器可执行的逻辑。比如用户配了一条规则:“订单金额不能为负数”,解析器要把它转成一个表达式对象。

我个人习惯用 AST(抽象语法树) 来做这件事。为什么?因为 AST 可以支持复杂的嵌套条件。举个例子:

// 原始规则配置(JSON 格式)
{
  "ruleId": "R001",
  "field": "order_amount",
  "operator": "GREATER_THAN_OR_EQUAL",
  "value": 0,
  "severity": "ERROR"
}

// 解析后生成的表达式对象(伪代码)
Expression expr = new ComparisonExpression(
  new FieldReference("order_amount"),
  Operator.GTE,
  new LiteralValue(0)
);

我曾经踩过一个坑:直接用字符串拼接的方式生成 SQL 来执行规则。结果遇到规则条件里有特殊字符,直接报错。后来改成 AST 解析,把规则和底层存储彻底解耦,再也没出过类似问题。

小技巧: 规则解析器最好支持“预编译”。把常用的规则提前解析好缓存起来,执行时直接拿编译后的对象,能省掉重复解析的开销。我在一个日处理亿级数据的项目里,靠这个优化把规则执行耗时压到了毫秒级。

3.2.2 规则调度器

规则调度器负责两件事:什么时候跑 和 怎么并发跑。

什么时候跑?常见的有三种触发方式:

触发方式 适用场景 我遇到的坑
定时调度 离线数仓的日/周质量巡检 高峰期调度任务扎堆,导致资源争抢
事件触发 实时数据流接入时即时校验 事件量暴增时来不及处理,需要背压机制
手动触发 数据问题排查时临时跑一次 权限管控不到位,有人乱跑全量规则

怎么并发跑?我建议用 规则组 + 依赖拓扑 的方式。比如规则 A 和规则 B 没有依赖关系,可以并行执行;规则 C 依赖规则 A 的结果,那就等 A 跑完再跑 C。调度器内部维护一张 DAG(有向无环图),按拓扑排序来调度。

// 调度器核心逻辑(伪代码)
Dag dag = buildDag(rules);  // 根据规则依赖构建 DAG
List<RuleNode> sorted = dag.topologicalSort();

for (RuleNode node : sorted) {
  if (node.getDependencies().isEmpty()) {
    // 无依赖,直接提交线程池
    executor.submit(() -> executeRule(node));
  } else {
    // 有依赖,等前置规则完成
    waitForDependencies(node);
    executor.submit(() -> executeRule(node));
  }
}
注意: 调度器一定要有超时控制和熔断机制。我曾经遇到过一条规则因为数据量异常大,跑了两个小时还没结束,把整个线程池堵死了。后来加了超时阈值,超过 30 秒直接中断并标记为“执行异常”。

3.2.3 结果评估器

结果评估器是最后一道关卡。它拿到规则执行后的原始结果(比如“字段 A 有 100 条空值”),然后做三件事:

  1. 阈值判定:空值率是否超过 5%?超过就标记为“不通过”。
  2. 严重等级赋值:根据规则配置的 severity,给结果打上 ERROR、WARN、INFO 等标签。
  3. 结果聚合:把多条规则的结果合并成一份质量报告。比如“表 T1 共有 10 条规则,通过 8 条,失败 2 条”。

我习惯在结果评估器里加一个 评分模型。不光是简单的“通过/不通过”,而是算出一个综合质量分数。比如:

// 结果评估示例(伪代码)
double score = 100.0;
for (RuleResult result : allResults) {
  if (result.getStatus() == Status.FAILED) {
    // 根据严重等级扣分
    switch (result.getSeverity()) {
      case ERROR: score -= 20; break;
      case WARN:  score -= 10; break;
      case INFO:  score -= 2;  break;
    }
  }
}
score = Math.max(0, score);  // 最低 0 分

这个分数可以直观地告诉业务方:“这张表的质量分是 85 分,主要失分点在订单金额字段的空值率过高。” 比单纯说“规则不通过”有用得多。

总结一下: 规则解析器解决“规则怎么表达”,规则调度器解决“规则怎么跑”,结果评估器解决“结果怎么用”。三个模块配合好了,引擎才算真正落地。