:ETL 同步的替代之道?)
最近我留意到一个项目标题把话说得很直接Tabsdata – Pub/Sub for Tables to Replace ETL Pipelines。中文理解大致是“面向表的发布订阅用来替代 ETL 管线”。第一次听到的人通常会冒出一个疑问表是用来存的不是用来“订阅”的事件从哪里来但如果你在数据团队里做过一段时间会立刻意识到另一种感受——你可能很少在生产环境里写真正复杂的特征计算你更多的时间是在修 A 系统和 B 系统之间的同步脚本。这类项目的真正价值不在“推送比定时调度快”而在于把“请求一次数据”改成了“建立一次数据关系”。过去你在做的是排队、拉取、清洗、再写入现在更接近发布、订阅、校验、消费。这个变化看起来只是模式不同实际会影响你维护数据管道的方式。我后面聊的内容不会替 Tabsdata 或同类项目打保票而是围绕“表作为数据通道”这种设计拆开它到底想替代什么又替代不了什么。1. 当 ETL 变成日常“搬数”真正的成本就被隐藏了1.1 你维护的不是管道而是两套系统之间的“潜规则”进入数据团队之后你会慢慢发现很多叫“ETL 管线”的东西本质不是复杂的抽取转换加载而是两套系统之间维持一份“潜规则”源表字段改名了同步脚本不知道下游就拿到空列。源系统深夜扩容连接数爆了凌晨调度失败第二天早上业务才反馈。源端删除了一条记录目标端的 ODS 层还留着旧数据因为全量同步任务只做覆盖更新不做删除。业务表从一个库迁到另一个库表名不变但连接串变了负责同步的人花了两小时才查出来。这些问题表面上是调度失败、脚本 bug、字段类型不匹配本质上是一个更底层的问题你在用“定时抓取”的方式去维护一份需要持续保持一致的数据副本。这里尤其要提一下 ODS 层。很多数仓里的 ODS 层本意是“贴源存储”源系统长什么样你尽量原样保留。但为了完成这个“贴源”你会不由自主地写很多清洗、去重、更新策略。这其实不是在加工数据而是在复述上游数据。Tabsdata 这类项目瞄准的恰恰就是这个区间如果表本身可以变成一种发布订阅通道你就不需要每天写一个“复述型”的同步任务。1.2 表不是静止的仓库它也可以成为事件流传统意义上我们总把数据库表理解成一个“状态”当前有哪些行这些行长什么样。ETL 做的就是把这个状态周期性地复制到另一个地方。但表不是每时每刻都静止的。每次 insert、update、delete其实都是一次事件字段值从旧版本变成新版本也是一次事件。“表的 Pub/Sub”做的就是把这些事件组织成一种可以被订阅的结构。发布方不需要关心下游有多少消费者订阅方也不需要轮询源库。你订阅的是一张表得到的不是一串无上下文的消息而是“这张表现在长什么样 它后面发生了什么变化”。换句话说它把数据从“被动等待你来查”变成了“主动通知你已经变了”。对于同步型场景这比定时 ETL 更接近数据变化的真实节奏。2. “表的 Pub/Sub”到底在讲什么2.1 发布端把表当成一个可以订阅的通道如果用一句话解释表的发布订阅我会这么说你不再调用对方接口去拉一份数据而是先声明“我这张表可以被订阅”然后由平台负责把这份表的变化分发给已经订阅的消费者。发布端要解决的事情通常有三件如何让一张表对外可见并控制谁能看到。如何捕捉表里的行级变化而不只是整表快照。如何让新加入的订阅者拿到“完整历史状态 后续增量”。中间那一步是很多方案的关键。它可能是基于数据库日志的解析也可能依赖表里的更新时间戳、版本号或主键变化。但落到使用者视角你不需要关心底层是 binlog 还是触发器你只需要关心“我发布的这张表别人能不能及时、完整地看到”。订阅端则更简单也更难。简单在于它不再需要自己写定时任务难在于它必须处理好重复数据、乱序更新、删除事件和字段变化。订阅者拿到的不是“一次性导出”而是一段持续的数据流因此消费程序本身的稳定性变得和管道一样重要。2.2 订阅端拿到的不是一条日志而是一张可用的表普通消息队列里你消费到的是一个事件、一条 JSON、一个字节数组。你还要自己解析字段、处理嵌套结构、维护表结构。表发布订阅想提供的抽象更高一层你直接拿到一张结构化的表字段名、类型、主键已经定义好你只需要对接目标表的写入逻辑。从工程体验看这个抽象对数据工程师更友好。因为数据同步任务里最常见的不是“不会写逻辑”而是“源端字段语义不稳定”。如果每个订阅本身就带 schema可以提前校验很多问题会在入口被拦住而不是等到目标表写入失败以后才暴露。更关键的是订阅状态。普通 HTTP 拉取常常是“这次拉到什么就是什么”失败了下次再拉全量而表订阅通常会维护每个订阅者的消费位置。断点续传、重放、追平滞后这些能力才决定了它能不能成为生产环境里稳定使用的同步底座。2.3 它和传统消息队列、CDC 的差异很多人会想到 Kafka、CDC、Debezium 这一系列方案。它们确实有血缘关系都是“变化数据捕获”或者“事件驱动”。但差异在于抽象层级维度传统消息队列表的发布订阅消息内容字节、JSON、业务事件结构化表、行变更、schema订阅粒度topic / partitiontable / dataset消费状态consumer offsetsubscriber cursor / 消费位点历史访问日志保留窗口内回放快照 增量组合使用目标解耦服务调用替代表同步、数据分发、部分 ETL使用者心智面向开发者写事件处理面向数据工程师管理表关系这样说不是为了证明谁取代谁。真实架构里它们会共存。Tabsdata 这类产品的意义是把数据复制这件事从“需要自己搭一套 CDC 消息队列 消费程序”变回“我订阅一张表就好”。这个变化会让更多团队愿意把同步任务交给平台而不是每个人自己维护一套轮子。3. 能替代哪些 ETL替代不了哪些 ETL3.1 适合替代面向共享数据的“同步型 ETL”第一类适合被替代的 ETL是把数据从一个地方复制到另一个地方几乎不改变业务含义。比如将业务库里的客户表同步到数仓 ODS 层。将主数据系统里的产品表分发给多个下游应用。将订单表的核心字段复制到数据分析库供报表查询。将用户表从旧系统迁移到新系统并保持后续持续同步。这些任务里真正消耗时间的是“同步”本身而不是“转换”。你不需要跨表计算不需要复杂清洗只需要保证目标端的数据足够接近源端。表发布订阅天然适合这类场景。它的优势在于增量触达。过去订单一到凌晨才批量拉一次现在源库有变化就能订阅到过去每次全量重跑现在可以“先快照再增量”。对下游来说新鲜度提高了对源库来说压力也变小了。3.2 不适合替代面向深度计算的“加工型 ETL”如果任务需要把订单表和用户表关联成宽表再算出用户生命周期价值最后写入报表层那表发布订阅不是直接答案。你可以依靠它同步基础表但真正的 join、聚合、去重、维度建模、业务规则校验仍然需要一套可管理的计算过程。这类“加工型 ETL”背后往往不是简单的表关系而是多层依赖ODS 到 DWDDWD 到 DWSDWS 到 ADS。每一层都有明确的口径和加工逻辑。表的发布订阅可以把底层 ODS 同步得更干净但不能替你完成业务口径的定义。还有一个边界要注意跨业务库的强一致事务。如果目标是“订单一旦支付成功订单表和支付流水表必须同时出现在某处且金额对得上”你不能只依赖表订阅。因为表订阅解决的是“送达”问题不解决“分布式事务”问题。最终对账和一致性校验仍然要由上层控制。3.3 三句话判断该不该用我用三句话来做选型判断如果下游只是想要一份尽可能接近源端的表而且刷新越及时越好适合用表订阅。如果下游还需要多张表做复杂关联、聚合、指标口径加工表订阅只解决前一半后一半仍要围绕计算层设计。如果业务对“多系统间的一致性和全链路审计”有硬要求表订阅可以作为数据入口但不能当作完整答案。所以更准确地说它不是“替代所有 ETL”而是“替代 ETL 里搬运和同步的那一部分”。4. 把旧管线迁到表订阅一套渐进替换流程4.1 第一步先用小表验证“快照 增量”不要第一天就把核心交易表切到新方案上。我的建议是先找一张风险低、结构稳定、变更频率可控的小表例如“产品分类表”或“区域配置表”。把它作为第一个订阅发布给测试消费端验证三件事历史快照能否完整到达。新增、修改、删除三种操作能否被正确识别。消费端重复启动后数据是否能从上次位点继续消费而不是重复或丢失。这个阶段要保留原有的同步脚本不要直接停。两条链路并行跑一两天比较行数、最大更新时间、关键主键差异降为零再考虑切换。4.2 第二步建立契约检查而不是默认相信字段名表发布订阅最怕的一个问题是源表字段悄悄变了下游却没有任何感知。为了避免这个坑你在发布方和订阅方之间最好显式约定一个“表契约”至少要包含表名、schema 版本号。主键字段。期望的字段列表和类型。变更操作类型insert、update、delete还是全部允许。数据保留周期和回放策略。一份示意结构大致是这样{ publisher: crm.customer, schema_version: 2026-01-20, primary_key: [customer_id], allowed_operations: [INSERT, UPDATE], data_type_policy: strict }这不是某个产品的真实配置只是一个常见写法。关键不是格式而是“schema 版本”这个概念。一旦版本变化订阅方可以选择自动适配也可以选择暂停并人工确认。如果默认自动适配你就一定要有字段变更监控否则下游很容易写进一堆错位数据。4.3 第三步别把消费位点放在内存里消费程序需要记录“我已经处理到哪个版本的数据”。实现时很多人会用一个局部变量保存看起来够用一旦程序重启就会出问题。更好的做法是把消费位点持久化到数据库或平台提供的存储里。每次成功写入目标端后再推进消费位点。也就是“先落数据再记进度”。如果目标写入成功但位点没有推进恢复后最多重复消费一段数据如果位点先推进了但数据写入失败那恢复后就会漏数据。对比两种结果前者是可控的后者是危险的。4.4 第四步提前定义失败语义和服务等级接下来要回答一个问题消费失败时你的团队希望它怎么处理通常有两种选择自动重试适合瞬时网络抖动但要注意重复数据。进入失败队列或告警适合需要人工介入的情况但要避免告警风暴。大多数生产环境会采用“自动重试 达到上限后转入待人工处理”的组合。目标端写入逻辑也必须幂等。最稳妥的更新方式是用业务主键做 upsert而不是无脑插入。否则一个事件被重试两次目标表里就会出现主键冲突或重复记录。5. 落地时最容易翻车的四个边界问题5.1 Schema 变了谁最先知道表发布订阅比传统定时同步更“近实时”但 schema 变更不会因为消息变快而消失。源端加了一个字段、删了一个字段、改了类型都会直接影响下游。最怕的不是变更本身而是变更没有被感知。建议在发布端把 schema 版本和表数据一起发布消费端拿到数据后先校验版本。严格模式下版本不匹配就停止消费宽松模式下允许新字段追加但删除或改类型必须人工审批。实际落地时这套流程需要和上游研发团队约定。如果上游说“我加个字段不就行了”简化版也能用但你必须明确通知订阅方。这里最容易翻车的是“上游只改了测试环境却忘了同步生产环境”。5.2 回放窗口和生命周期表订阅并不是无限保存所有历史。平台一般会有保存周期或回放窗口超过窗口的数据可能无法重新消费。你需要提前知道新订阅者可以回溯多长时间。如果消费端停机超过保留周期是否会丢失数据。数据量增长后回放成本会不会超出预期。当一个消费者长时间宕机恢复后往往会触发大量历史数据回溯这可能会压垮消费端和下游数据库。建议在恢复前估算回放数据量如果积压太大先考虑“跳过历史、从当前位置继续”或者先快照再增量而不是强行追平几个月的数据。5.3 Delete 事件最容易被忽略很多同步任务在验证时只测试了 insert 和 update没有测试 delete。结果源端删除一条记录后目标端始终保留旧数据。表面上“数据多了”实际对业务来说这是脏数据。在表发布订阅里delete 也是一种需要明确处理的变更。如果目标端业务上不允许物理删除你也应该在底层记录一个“删除标记”而不是完全忽略。否则下游报表会一直把已取消的订单、已离职的用户算进去。5.4 权限和行级过滤比想象中重要发布一张表不等于把整张表直接开放给所有订阅者。不同消费者可能只应该看到不同行和不同列。例如客户表共享给销售分析团队时可能需要过滤掉某些敏感字段订单表共享给不同业务线时可能需要按地区或渠道做行级隔离。如果平台不支持这些能力你在“数据可用性”和“数据安全”之间会非常被动。所以选择这类方案之前至少要确认一件事你发布的是整表快照还是可以按字段、行、规则进行裁剪。所有外部可订阅的数据接口最终都会变成一种对外暴露面权限模型不能省。5.5 问题排查从发布端往消费端逐层查真遇到数据对不上的时候不要先怀疑平台。我的固定排查顺序是查发布端源表里到底有没有这条数据变更是否已经提交查订阅事件数据是否真的被平台分发出来了事件内容是否完整查消费位点消费程序处理到哪一条了有没有积压查目标写入更新键是否匹配字段是否错位删除是否生效查最后成功记录最后一次成功推进位点的时间是什么时候大多数“表同步不一致”都不是平台丢了数据而是某个环节的提交时序理解错了。从源端到目标端按这条链路一层层看定位会快很多。6. 真正的价值不是省掉 ETL而是让数据在系统之间“讲契约”6.1 从“搬数据”到“订阅数据”是一层认知变化过去我们思考数据集成默认答案往往是“搭一条管道”它什么时候跑、跑多久、失败了重跑多少次。这些细节很重要但也会占据太多注意力我们常常忘了所有管道最终是在维护同一个东西——表与表之间的关系。表发布订阅把关系变得显式化一张表就是发布物一个下游就是订阅者。它不会消灭所有转换计算也不会消灭复杂的离线数仓模型。它真正消灭的是那些只为了“复制数据”而存在的脚本、调度、监控和加班。这层认知变化会影响工具选型也会影响数据团队的职责。数据工程师不再只是“管道维护者”而更像是数据契约的管理者。你关心的是哪张表对谁可用、schema 如何演进、消费端如何保证一致性而不是今天哪个定时任务又失败了。6.2 下一步最该先做的一件事如果你对这个方向感兴趣最值得做的不是立刻把核心 ETL 全部替换而是先挑出一张“同步价值高、逻辑简单、变更频繁”的表验证一遍快照、增量和删除能力。过程中你会很快确认几件关键事平台是否支持 schema 演进消费位点是否可以持久化失败重试是否可控目标端写入是否幂等这些才是决定它能不能长期使用的核心因素而不是它宣称的“替代 ETL”这个口号。数据架构没有银弹。Tabsdata 这类想法真正打动我的地方是它提醒我们很多所谓 ETL本不该由每个团队反复发明。把数据变成可订阅的资源把管道变成关系是这个方向真正值得长期关注的原因。