2. 批流管道设计原则

批流一体管道最怕的不是“跑不起来”,而是“跑起来了但结果不可信”。同一份业务数据,批任务重跑一次结果翻倍,流任务重启后重复写入,上游加了一个字段下游解析崩溃——这些问题都不是性能问题,而是设计原则缺失。本节围绕五个相互咬合的原则展开:数据分层决定数据放在哪里、以什么粒度存在;幂等性决定重复执行是否安全;Exactly-Once 语义决定故障恢复后结果是否精确;Schema 演进决定结构变化时管道能否平滑过渡;数据质量约束决定脏数据在哪一层被拦截。它们不是并列的五个知识点,而是一条从“存储组织”到“结果可信”的约束链。

数据分层:把“原始事实”和“加工结论”分开

在 Databricks 的 Lakehouse 中,推荐采用 Bronze / Silver / Gold 三层组织。Bronze 层保存从 Kafka、文件或数据库 CDC 摄入的原始数据,只做最小化处理(如追加摄入时间戳、来源标识),保留完整历史,允许存在重复和脏数据;Silver 层做清洗、去重、标准化、关联,形成可信的明细事实;Gold 层面向业务聚合,输出指标、宽表或特征。分层的核心价值在于:任何一层都可以独立重算,而不影响其他层的语义。Bronze 是“不可变事实账本”,Silver 和 Gold 是可重建的派生视图。

一个常见的错误是把去重逻辑放在 Bronze。一旦 Bronze 被去重,原始重复事件就永久丢失,后续若发现去重键选错,无法回溯。正确做法是 Bronze 保留原始,Silver 用 MERGE 或 dropDuplicates 生成去重结果。分层还带来成本收益:Bronze 可用较低成本的存储和较粗的分区,Gold 用 Delta 的 Z-ORDER 优化查询。

幂等性:让“重跑”成为安全操作

幂等性指同一操作执行一次和执行多次,对目标状态的最终影响相同。批流管道中,任务失败重试、补数、回滚重跑都是常态,没有幂等性就没有可运维性。实现幂等的关键不是“避免重复”,而是“让重复不产生副作用”。

在 Delta Lake 上,最常用的手段是 MERGE INTO 按业务主键做 upsert,而不是盲目 append。例如订单表以 order_id 为主键:

MERGE INTO silver.orders AS t
USING staging.orders_dedup AS s
ON t.order_id = s.order_id
WHEN MATCHED AND s.updated_at > t.updated_at THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *

这样即使同一批数据被处理两次,第二次只会命中 MATCHED 分支,且因 updated_at 不更新而不产生变化。对于纯追加场景,可用 INSERT ... WHERE NOT EXISTS 或基于分区覆盖(INSERT OVERWRITE 指定分区)实现幂等。流任务中,Structured Streaming 的 foreachBatch 配合 MERGE 是标准幂等写法。

Exactly-Once 语义:端到端精确一次

Exactly-Once 不是单点能力,而是“源—处理—汇”三段协同的结果。Kafka Source 通过 offset 管理实现可重放;Spark Structured Streaming 通过 checkpoint 记录已处理 offset 和状态;Delta Sink 通过事务日志保证写入原子性。三者结合,才能在故障恢复后既不丢也不重。

需要区分三个层次:At-Most-Once(可能丢,不重)、At-Least-Once(不丢,可能重)、Exactly-Once(不丢不重)。很多团队实际做到的是 At-Least-Once + 幂等写入,效果等价于 Exactly-Once,这也是工程上更常见的组合。配置上要确保 checkpoint 路径稳定、trigger 合理、Sink 支持事务。若 Sink 是 Kafka,需开启幂等生产者;若 Sink 是 Delta,天然支持。

批流管道五原则协同关系 分层定位置 · 幂等保重跑 · Exactly-Once 保精确 · Schema 保兼容 · 质量保可信 数据分层 Bronze 原始层 原始摄入 · 保留重复 追加时间戳 · 不可变 可完整重放 Silver 明细层 清洗 · 去重 · 标准化 MERGE upsert 可信明细事实 Gold 聚合层 指标 · 宽表 · 特征 Z-ORDER 优化 面向业务消费 Exactly-Once 三段协同 Kafka offset 可重放 Spark SS checkpoint Delta 事务日志 源可重放 + 状态可恢复 + 写入原子 = 不丢不重 工程常见:At-Least-Once + 幂等写入 ≈ Exactly-Once Schema 演进 加字段 → 默认值兼容 改类型 → 需显式转换 删字段 → 下游需容错 数据质量约束:分层拦截 Bronze 入口校验 非空 · 格式 · 枚举 失败进隔离表 不阻断主流程 Silver 一致性校验 主键唯一 · 外键存在 时间单调 · 金额非负 异常告警并阻断 Gold 业务校验 指标波动阈值 环比同比异常 触发人工复核

Schema 演进:结构变化不能靠“停机改表”

上游系统加字段、改类型、删字段是常态。Delta Lake 支持 Schema 演进,但“支持”不等于“随便改”。加字段时,若下游用 SELECT *,新字段会自动出现,需确保下游能容忍;改类型(如 int 变 string)必须显式使用 mergeSchema 或 ALTER TABLE,否则写入报错;删字段时,下游若按位置读取会错位,应始终按列名读取。流任务中,Schema 变化会导致 checkpoint 失效,需评估是否重启并指定新 Schema。

实践建议:在 Bronze 层使用宽松 Schema(如字符串接收),在 Silver 层做显式类型转换和字段映射,把 Schema 变化的影响收敛在 Silver 的转换逻辑中。同时维护一份 Schema 契约文档,记录每个字段的来源、类型、是否可空、变更历史。

数据质量约束:在哪一层拦截,决定故障半径

质量约束不是“最后检查一下”,而是分层嵌入。Bronze 入口做基础校验(非空、格式、枚举),失败记录写入隔离表,不阻断主流程;Silver 做一致性校验(主键唯一、外键存在、时间单调、金额非负),异常触发告警并阻断下游;Gold 做业务校验(指标波动阈值、环比同比异常),触发人工复核。这样做的目的是:让脏数据在影响范围最小的一层被拦截。如果所有校验都堆在 Gold,问题发现时已经污染了全部明细。

在 Databricks 中可用 Delta Live Tables 的 EXPECT 约束实现声明式质量规则,例如 CONSTRAINT valid_amount EXPECT (amount >= 0) ON VIOLATION DROP ROW,把质量规则和管道定义放在一起,便于版本管理和审计。

五原则的协同:一个订单管道的例子

假设 Kafka 中有订单事件流,目标是在 Gold 层输出“每日各品类销售额”。Bronze 层用 Auto Loader 或 Kafka Source 追加写入 bronze.orders_raw,保留原始 JSON 和摄入时间;Silver 层用 foreachBatch + MERGE 按 order_id 去重并解析字段,写入 silver.orders;Gold 层按 dt, category 聚合,写入 gold.daily_sales。质量规则:Bronze 校验 order_id 非空,Silver 校验 amount >= 0 且 order_id 唯一,Gold 校验日销售额波动不超过 ±30%。Schema 演进:上游新增 coupon_code 字段时,Bronze 自动接收,Silver 显式映射,Gold 按需使用。幂等性:Silver 的 MERGE 保证重跑不翻倍,Gold 用 INSERT OVERWRITE 按分区重写保证幂等。Exactly-Once:checkpoint + Delta 事务日志保证故障恢复后不丢不重。

这五个原则的共同目标是:让管道在重复执行、结构变化、数据异常和故障恢复下,仍然输出可信结果。设计时先问“这一层的数据能否独立重算”,再问“重复执行是否安全”,然后问“故障后结果是否精确”,最后问“结构变了会不会崩、脏数据会不会扩散”。把这四个问题回答清楚,批流管道的设计骨架就立住了。