数据管道总是悄无声息地出故障。仪表板显示的是昨天的数字;上游某一列改了类型,某个作业没有报错就丢掉了一些行;或者一个早已无人记得的复制槽,让生产数据库上的预写日志不断堆积,直到磁盘被占满。这些问题中的每一个,都是一个从未被画出来的环节或契约。
数据管道架构图展示数据从哪里来、如何流动、每个阶段存放在哪里、由谁在何时转换,以及大家如何知道数据是新鲜且正确的。本文以一种常见架构为例绘制这样一张图:通过 Kafka 把 PostgreSQL 的变更数据捕获(CDC)送入数据湖和数据仓库,并说明每个环节应该标注什么。
批量抽取、CDC 还是事件
从业务数据库中取出数据,常见的方法有三种:
| 方法 | 读取方式 | 典型新鲜度 | 需要注意 |
|---|---|---|---|
| 批量抽取 | 定时查询,例如 updated_at 晚于上次运行时间的行 |
数小时 | 对源库的负载;硬删除永远不会出现 |
| 基于日志的 CDC | 读取数据库的事务日志 | 数秒到数分钟 | 为读取方保留的日志,以及与表结构的耦合 |
| 应用事件 | 服务发布领域事件,通常借助 outbox 表 | 数秒 | 需要修改代码;事件必须面向消费者来设计 |
基于日志的变更数据捕获(CDC)之所以在分析场景中流行,是因为它不需要修改应用、能看到删除,而且几乎不增加查询负载。它的成本在运维层面,而这些成本应该画在图上。
PostgreSQL 中基于日志的 CDC 如何工作
PostgreSQL 通过逻辑解码暴露变更,逻辑解码把预写日志(WAL)转换为行变更流。它需要 wal_level = logical、一个输出插件和一个复制槽。Debezium PostgreSQL 连接器使用 PostgreSQL 10 及以上版本自带的标准插件 pgoutput。首次启动时,它会为各表做一次一致性快照,然后流式传输每一次已提交的插入、更新和删除,默认每张表对应一个 Kafka 主题,命名为 topicPrefix.schemaName.tableName。
真正会咬人的是复制槽。PostgreSQL 文档解释说,即使没有任何连接,复制槽也会阻止删除其消费者仍需要的 WAL。如果连接器停了一个周末,主库上的 WAL 就会一直增长。max_slot_wal_keep_size 设置用于限制复制槽可以保留的 WAL 量;其默认值 -1 表示不设上限。设置上限可以保护数据库,代价是复制槽会失效,也就意味着要重新做快照。无论哪种方式,都要监控复制槽的延迟,并把复制槽画在图上。
数据湖分层与数据仓库
变更进入 Kafka 后,由 Sink 写入存储,再经过转换变得可用。Databricks 推广的奖章架构常被用来给各阶段命名:
- 铜层保存原样到达的原始数据,并附带加载元数据,这样无需重新读取源库就能重新处理。
- 银层保存经过清洗、去重和统一的记录:每个订单一行当前数据,类型和键保持一致。
- 金层保存仪表板和模型读取的业务级表和聚合结果。
每一层放在哪里是一个设计决策:可以是采用 Apache Iceberg 这类开放表格式的对象存储,也可以是数据仓库本身。画出它实际所在的位置,因为成本、访问控制以及谁能查询它都由此决定。
编排与契约
从铜层到金层的转换按计划运行,或者在新数据到达时运行,由 Airflow 或 Dagster 这样的编排器管理,通常运行 dbt 中的 SQL 模型。决定一条管道是资产还是负担的,有两件事:
- 契约。 变更事件的模式连同兼容性规则一起注册在模式注册表中,这样源表上的破坏性变更会在生产者那里大声失败,而不是在报表里悄悄出错。
- 检查。 每一步之后都运行新鲜度、行数、唯一性和空值检查,一旦失败就在仪表板出错之前通知负责人。
示例:一家网店
网店的 PostgreSQL 数据库保存订单、支付和客户数据。Debezium 运行在 Kafka Connect 中,通过复制槽读取 WAL,并发布 shop.public.orders、shop.public.payments 和 shop.public.customers。Sink 连接器把变更事件追加到铜层表中。编排器每 15 分钟运行一次 dbt 模型来构建银层和金层,然后运行数据检查。仪表板读取金层。
图表内容
- shop DB
- Debezium
- Kafka
- Sink connector
- Schema registry
- Lake and warehouse
- Gold: marts
- Silver: cleaned
- Bronze: raw changes
- BI dashboards
- Orchestrator: dbt
- Data checks
- WAL via slot
- change events
- register schemas
- topic per table
- append raw changes
- merge, dedupe
- aggregate
- SQL
- runs models
- every 15 min
- then checks
这张图的读法如下:
- 上排,采集。 变更通过复制槽离开 PostgreSQL,在 Debezium 中变成变更事件,Debezium 会注册它们的模式;随后变更事件经由每表一个的 Kafka 主题到达 Sink 连接器。
- 下排,存储,从右到左。 Sink 把原始变更追加到铜层。转换把它们合并进银层,再把银层聚合为金层,供仪表板查询。
- 下方,控制。 编排器运行转换和检查;检查失败时会触发告警。
- Markdown 块列出了复制槽和模式变更的运维规则。
需要标注什么
- 在第一条连线上标注复制槽及其上限,以及它落后时通知谁。
- 主题名称或命名规则,让消费者知道该订阅什么。
- 在转换连线上标注计划和新鲜度目标,例如“每 15 分钟一次,延迟不超过 30 分钟”。
- 每层的保留期。 铜层通常保留最久的历史,它就是你的重放缓冲区。
- 每一层和每项检查的负责人。
- 个人数据。 标出它们存放在哪里,以及在哪里被脱敏或丢弃;参见 GDPR 数据映射图。
常见错误
- 从数据库到数据仓库只画一个箭头。 它隐藏了复制槽、模式,以及每一个可能丢失或延迟数据的地方。
- 在主库上运行分析查询。 针对生产主库的批量抽取会与客户流量争抢资源;请使用 CDC 或副本,并画出用的是哪一个。
- 没有重放路径。 如果不保留铜层,转换中的一个 bug 就意味着要重新读取源库。
- 把原始表当作公共接口。 读取内部表变更事件的消费者会在模式变化时出错。对于面向其他团队的事件,请改为发布专门设计的事件;事件驱动架构图介绍了这一部分。
- 只记日志的检查。 失败了却不通知任何人的测试,只是一条注释。
关于同一系统的数据库部分,请参阅数据库复制图和 ER 图示例。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加数据节点包中的一个 Postgres 节点表示源数据库,以及一个 Kafka 节点表示各主题。
- 添加 C4 模型节点包中的 容器 节点,分别表示 Debezium、模式注册表、Sink 连接器和仪表板。
- 添加一个基础节点包中的 框架,命名为 Lake and warehouse,在其中放入数据节点包中的两个 对象存储库 节点表示铜层和银层,以及一个 数据仓库 表示金层。
- 添加 DevOps 节点包中的一个 流水线 表示编排器,以及一个 告警 用于数据检查。控制连线画成虚线。
- 添加一个基础节点包中的 Markdown 块,写明复制槽和模式的规则。
这个示例共用 13 个图表节点,在 Free 套餐的限制之内。如果你用 SQL 维护源库的表结构,PostgreSQL 转 ER 图工具可以画出主题背后的表。
常见问题
CDC 比批量抽取更好吗? 就新鲜度、删除和源库负载而言,通常是的。批量方式运维更简单,如果可以接受数小时的延迟,且行从不被硬删除,批量就足够了。
做 CDC 需要 Kafka 吗? Debezium 最常运行在 Kafka Connect 上,Kafka 能提供重放能力并支持众多消费者。也有其他架构;画出你实际运行的传输方式及其保留期。
如何处理模式变更? 带兼容性规则注册模式,用新增列代替修改列,并让下游模型能够容忍新列。把注册表画出来,因为契约就在那里。
数据质量检查应该在哪里运行? 在每一层之后运行,作为编排流程的一部分,并向负责人发送告警。金层上的新鲜度检查能发现大多数静默故障。
如何让管道图保持最新? 在修改连接器和编排器配置的同一个拉取请求中修改它;参见不会过时的架构图。