T
traeai
登录
返回首页
InfoQ

Presentation: Write-Ahead Intent Log: A Foundation for Efficient CDC at Scale

8.5Score
Presentation: Write-Ahead Intent Log: A Foundation for Efficient CDC at Scale

TL;DR · AI 摘要

Write-Ahead Intent Log (WAIL) 是 DoorDash 为解决传统 CDC 在高负载下性能瓶颈而设计的架构,通过分离意图与状态负载实现高效 CDC。

核心要点

  • 传统 CDC 在高负载下性能受限,DoorDash 采用 WAIL 架构进行优化。
  • WAIL 使用 dumb producer proxy 和 smart consumer 模式分离意图与状态负载。
  • WAIL 适用于大规模 CDC 场景,提升系统在高峰订单流量下的稳定性。

结构提纲

按章节快速跳转。

  1. DoorDash 在高峰订单流量下面临数据库性能瓶颈,传统 CDC 无法满足需求。

  2. 传统 CDC 在高负载下性能受限,无法处理异构数据库的复杂场景。

  3. WAIL 通过 dumb producer proxy 和 smart consumer 模式分离意图与状态负载,提升 CDC 效率。

  4. WAIL 的架构设计确保在高负载下仍能保持稳定性和性能。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Write-Ahead Intent Log (WAIL)
    • 背景与挑战
      • 传统 CDC 的局限性
      • DoorDash 的高峰订单流量需求
    • 架构设计
      • dumb producer proxy
      • smart consumer 模式
      • 意图与状态负载分离

金句 / Highlights

值得收藏与分享的关键句。

  • DoorDash 在高峰订单流量下面临数据库性能瓶颈,传统 CDC 无法满足需求。

    引言

    ⬇︎ 下载 PNG𝕏 分享到 X
  • WAIL 通过 dumb producer proxy 和 smart consumer 模式分离意图与状态负载,提升 CDC 效率。

    Write-Ahead Intent Log (WAIL) 架构

    ⬇︎ 下载 PNG𝕏 分享到 X
  • WAIL 的架构设计确保在高负载下仍能保持稳定性和性能。

    WAIL 的核心机制

    ⬇︎ 下载 PNG𝕏 分享到 X
#CDC#数据库#架构设计#DoorDash
打开原文

预写意图日志:实现可扩展高效 CDC 的基础 - InfoQ

InfoQ 首页 演讲 预写意图日志:实现可扩展高效 CDC 的基础

AI、机器学习与数据工程

QCon 旧金山(11 月 16-20 日):深入的技术会议。改变你思维方式的同行对话。

预写意图日志:实现可扩展高效 CDC 的基础

喜欢

新下拉阅读列表

  • 阅读列表

查看演讲

  • 垂直
  • 水平
  • 全屏

速度:

  • 1x
  • 1.25x
  • 1.5x
  • 2x

下载

  • 演示文稿

51:26

概要

Vinay Chella 和 Akshat Goel 讨论了在高峰订单流量期间,跨异构数据库运行传统 CDC 所面临的挑战。他们解释了 Debezium 在高负载下遇到的限制,并分享了他们如何构建预写意图日志(WAIL)——一种自定义架构,利用“愚蠢”的生产者代理和“聪明”的消费者模式,将意图与状态负载清晰地分离。

人物简介

Vinay Chella 是 DoorDash 的工程负责人,他领导存储和流媒体基础设施组织,该组织为市场上的关键任务系统提供支持。Akshat Goel 是 DoorDash 的高级软件工程师,他构建了存储访问平台,这是一个统一的抽象层,为所有在线数据存储提供支持。

关于会议

软件正在改变世界。QCon 旧金山通过促进知识和创新在开发者社区中的传播,赋能软件开发。作为以实践者驱动的会议,QCon 是为技术团队负责人、架构师、工程总监和项目经理设计的,这些人员在团队中影响创新。

INFOQ 活动

  • 2026 年 6 月 25 日,东部时间下午 1 点 构建自主可靠性:将 AI 集成到你的可观测性堆栈中 演讲者:Justin Griffin - NeuBird AI 产品负责人
  • 2026 年 7 月 9 日,东部时间中午 12 点 人工智能分析时代重新思考日志 演讲者:Nicolas Jung - Datadog 日志产品经理
  • 2026 年 7 月 16 日,东部时间下午 1 点 为代理时代做工程准备:如何规范、构建、测试和运营 AI 驱动的系统 演讲者:Juveria Kanodia - Harness 工程高级总监

演讲稿

Vinay Chella:我们在 DoorDash 的每一天都在确保当有人下单时,我们准备食物,用汽车取走并送达。不,我们并不真的做这些。我们确保当有人下单时,跨各种系统的事件序列能够顺利进行。这其中包括我们略微恐慌的部分。当数据库出现故障恰好发生在高峰时段,也就是交通高峰期和大量订单涌入时,我们会略微恐慌。今天,我们要谈论一个听起来非常技术化的话题,但事实上,这正是我们在 DoorDash 所做事情的核心。从确保你能在屏幕上看到你的配送预计时间,到商家平板上的提示,准备你的食物。

这个话题是变更数据捕获(CDC),以及我们最终对传统 CDC 的依赖感到厌倦,并开始构建我们自己的方法,我们称之为预写意图日志(WAIL)。在深入细节之前,我想先讲一个小故事,说明为什么这个问题实际上很重要。

我是 Vinay Chella,我在 DoorDash 领导存储和流媒体基础设施。我帮助构建分布式数据库和分布式团队。

Akshat Goel:我是 Akshat,我是 DoorDash 的高级软件工程师。我帮助 Vinay 构建分布式数据应用。

Vinay Chella:我们一起花费大量时间构建分布式数据库抽象,以便业务团队能够专注于解决业务问题,而不是处理各种数据库和流处理的复杂问题。

DoorDash 的订单流程

想象一下,正值晚餐高峰时段,订单如潮水般涌入,餐厅被订单淹没,司机在街头穿梭,而顾客则又饿又急切地等待订单送达。在屏幕上,你看到的是一个理想的世界,一个完美的世界,只需轻轻一点屏幕,订单就能流畅地通过系统。订单进入订单数据库,餐厅收到通知,司机被派送,应用程序立即更新。在现实世界中,这些步骤紧密相连且高度协调。即使一个信号失败,就有可能导致步骤顺序出现不同步,团队经常尝试用双写等方法来解决这些问题。你写入数据库,同时向流处理系统发送事件。听起来很简单,但实际上非常脆弱。

一个操作可能成功,另一个却可能失败,突然之间你的系统就有了不同的真相。这里真正的难题不是用户界面,也不是餐厅里的厨师。而是确保你想要送达的杂货或食物的意图,能够以正确的方式通过我们复杂的系统,并通知到所有的物流系统。我真的想在这里暂停一下,提供一个类比。我相信你们中很多人都在家里尝试过折叠贴身床单。这非常困难。你努力尝试,花费很多时间,最后只能将它塞进衣柜,然后说这就是预期的结果。尝试协调这些系统,尝试对这些系统进行双写,就正好像是折叠贴身床单一样。

让我们看看在这个复杂系统中,当一个意图失败时会发生什么。订单已经进入订单数据库,但餐厅从未收到通知,配送也从未被派送。顾客被困在订单屏幕上,盯着那个处理中的旋转图标,而每过一分钟都更加饥饿。一个缺失的更新最终可能导致退款、升级投诉和不满意的顾客。很多时候,你只能依赖一些解决方案,比如轮询数据库,但这也不是真正的解决方案。它会给数据库增加负担,会导致延迟,而且你仍然会在游戏进行到很晚的时候才做出反应。这正是为什么像 CDC 这样的系统存在,为什么 CDC 很重要。CDC 给我们提供了一个实时的更改流,因此每个下游系统都可以保持同步,并完成它们需要做的工作。CDC(变更数据捕获)并不是魔法,也不是万能的解决方案。

它有很多棘手的边缘情况。你可能会遇到连接器中断的情况,也可能会遇到数据接收端宕机的情况。不同的数据库使用着不同的方言。这个世界是真实的,我们需要确保数据也是真实的。对 DoorDash 来说,这意味着每一个订单更新都必须是实时的,并且能够可靠地在整个微服务链中传递。今天演讲的其余部分将重点介绍我们如何从这个脆弱的架构转变为更加稳固、更加可见、可恢复、并且痛苦更少的系统,我们将这种系统称为意图日志。再次强调,如果你还记得这两张幻灯片,你可以带走的一个重要观点是:这不是关于状态,而是关于意图,即你试图完成的是什么。确保意图在你的系统流程中顺畅传递是关键,其余的一切都只是管道。

内容

从高层次来看,我们将介绍 CDC 对我们、我们的业务以及 CDC 典型挑战的重要性,还会讨论我们所构建的基础,以及在系统中全面推广时所获得的一些运营经验。

布局(DoorDash 上的 CDC)

CDC 在 DoorDash 的大多数功能中都发挥着关键作用。CDC 直接出现在我们的业务问题中。我们客户和商户的体验的很大一部分都依赖于实时数据和实时状态。当一个订单被创建、更新、取消或送达时,数十个系统需要立即被通知并作出反应。对于商业分析,团队依赖于最新数据来了解市场状况、了解配送时间、了解服务质量。如果数据过时,我们就无法以正确的方式推动业务发展,也无法产生我们期望的影响。对于用户体验,想象一下,一家餐厅某种商品售罄了,他们更新了商品的可用性。一家杂货店某种商品售罄了,他们更新了库存,但用户端如果没有看到这个更新,用户可能会下单,从而导致商品不可用。配送因此被延迟,客户看到的却是商店里没有的商品,这基本上就是业务上的混乱。对于 DoorDash 来说,CDC 不是可有可无的,CDC 基本上是我们所做工作的基础。当数据以更快速、更可靠的方式流经这些系统时,我们的市场平台才能可靠地运行。CDC 后面的工程问题也相当具有挑战性,并且相当复杂。你可能会遇到像搜索和缓存这样的系统,这些可能是内部定制的解决方案,与你的源数据库有所不同。

CDC 中的数据基本上可以保持你的源数据库与衍生数据存储(如搜索和缓存系统)同步,这使我们能够在应用程序中提供业务功能。我还提到了一种叫做“出站模式”的方法,开发人员经常需要构建这种模式。在这种模式下,你有一个事务表和一个出站表,它会通知下游消费者所有发生的变化。如果有 CDC,这会变得容易得多。此外,多数据源或物化视图,或者保持异构数据存储同步,也是另一个技术需求。当你拥有异构存储时,需要像为搜索和缓存系统提供快速数据的索引之类的功能。你的事务数据或源数据库可以是完全不同的系统。保持它们同步的最佳方式是通过像 CDC 这样的系统。

这就是传统 CDC 的工作方式。你有一个应用程序写入数据库,数据库通过一些连接器,最终进入像 Kafka 这样的事件流系统,然后你的应用程序从这些系统中消费数据。这是一种已被验证的模式,但一旦出现问题,比如模式变更导致连接器失效,或者下游同步变慢,整个管道就会变得复杂。传统 CDC 最终会让我们紧密耦合到数据库的内部结构和这些系统的运行方式。当你试图扩展系统时,这变得非常难以管理。

CDC 的挑战

现在我们已经了解了传统 CDC 在理论上是如何工作的,让我们来谈谈当你尝试在大规模上运行这种系统时会出现哪些现实问题。首先出现在我脑海中的问题是抽象挑战。每种数据库都有不同的 CDC 方言,我相信你所处的环境里肯定运行着各种不同的数据库。PostgreSQL 有逻辑复制,类似 Cassandra 的系统有提交日志,而 Scylla 使用的是不同的格式。有些系统甚至没有 CDC。你得到的不是一条清晰的管道,而是一堆连接器、保证和特性,你需要逐一处理。如果你是一个平台团队,正在尝试提供统一的存储解决方案和存储抽象,就像我们正在尝试做的那样,这会非常痛苦。

你基本上在不同数据库之间充当数据库翻译者,而你试图与这些顽固拒绝在 CDC 的通用方言、通用术语和通用保证上达成一致的数据库进行协作。特别是当你尝试在大规模上实现这一点时,情况会变得更糟。数据库中任何小的更新差异,语义、审计保证、检查点格式或事务行为上的差异,都会向上层传播到你的客户。现在你的每个 CDC 消费者都需要理解底层数据库在做什么。这与我们通过构建这一层间接层和平台抽象所期望的正好相反。这就是为什么构建抽象成为 CDC 中的第一个主要挑战,因为你无法在彼此之间无法沟通的砖块上可靠地构建这一基础。

接下来是可扩展性和简洁性。这是 CDC 架构中的下一个重大决策,也是一种权衡。你是希望优化快速和简便的设置,还是希望拥有可持续扩展的架构?通常,团队会从非常简单的路线开始,使用一个单一的 CDC 连接器,配置非常少,只有一个服务来管理。在产品初期,这种方式运行得非常好。但随着流量增长,随着越来越多的表被引入系统,这个连接器就会成为瓶颈。最终,它甚至会成为整个系统的单点故障。另一种选择是生产级别的可扩展架构,其中包含分布式组件。你内置了缓冲、路由和可观测性等功能。这种架构更加健壮,但同时也需要大量的前期投入来构建。这正是我们在使用 Debezium 时所经历的情况。

Debezium 是一个非常优秀的开源选项。当我们尝试将其扩展并进行扩展测试时,它表现得非常糟糕。我们看到了 CPU 的峰值,延迟几乎翻倍,在多区域设置中出现了消息重复,而且在路由和可靠性方面存在诸多限制。它根本无法跟上我们所进行的扩展测试。我们的教训非常明确:选择简洁性可以带来早期不错的回报,但随着时间的推移,它所隐藏的复杂性会让你在扩展时面临巨大的困难。

传统 CDC 架构的下一个挑战是其生态系统本身。如果你浏览一下这些数据库及其 CDC 功能,你会发现有一些不错的开源选项,但它们的使用非常简单。当你需要一些复杂的功能,比如生产环境中期望的“恰好一次”语义、模式演变处理等,这些功能通常都隐藏在付费墙之后。你可以免费获得基础功能,但一旦你真正需要满足生产需求,你最终将付出高昂的代价。如果你考虑这些连接器的成熟度,它们之间的差异也非常明显。如果你看一下,PostgreSQL 的 CDC 连接器经过充分测试,被广泛接受。而如果你探索像 Cassandra 或 Scylla 的连接器,它们的可靠性以及所需的基础设施设置差异很大,比如依赖 Kafka Connect 等等。

在操作这个集中式数据库平台时,你最终会遇到很多不一致的问题。这使得平台很难为你提供任何类型的保证。在这些情况中,如果你使用了像 DynamoDB 这样的云托管服务,那么对于这些特定服务,CDC 看起来可能更方便,但它们会随着时间的推移造成强烈的供应商锁定。一旦你采用这些服务,要跟上你的演进并提供系统设计的灵活性就变得非常困难。如果你仔细想想,即使在你开始业务和业务逻辑之前,生态系统本身就已经在你的 CDC 架构周围引入了大量摩擦和长期限制。

这将我们带到了下一个挑战,即CDC管道本身的脆弱性。即使拥有正确的工具,CDC管道往往比看起来更加脆弱。让我通过几个例子来说明这一点。在CDC生态系统中,有许多移动部件,比如连接器、下游消费者等。每个部分都有自己的限制和失败模式。例如,上游数据库的模式变更可能会破坏连接器,或者Cassandra的提交日志可能会延迟或顺序错乱。如果你的目标是Kafka,而下游消费者滞后,那么这种反向压力会积累并破坏整个管道。当这种复杂架构中出现问题时,恢复过程很少是无缝的。你可能需要重放日志,可能需要对数据库进行全量加载,并调试某些事件为何丢失。

尤其是在你处理TB或PB级别的数据时,这变得非常困难。这种脆弱性就是为什么在开始扩展时,仅仅依赖传统的CDC变得相当危险。想想看,系统的运行效果只取决于其最薄弱的组件。当你设计这个精美的系统,并且当你拥有那个作为你所有协调工作的核心的CDC时,情况就会变得混乱。这些挑战促使我们重新审视并彻底重新思考基础,这引导我们制定了构建该解决方案的基础原则,这些原则也帮助我们塑造了即将讨论的解决方案。

我们解决方案的基础

现在我们已经讨论了为什么CDC对我们业务如此重要,以及为什么传统的CDC并不足够,现在让我们深入探讨我们是如何实际解决这个问题的。在这一部分,因为我只是一个演讲者,不实际做事情,Akshat才是构建这些内容的人,并在设计中发挥基础性作用,以及我们正在学习的内容。

Akshat Goel:我想通过这次演讲传达的一个观点是,每个人解决方案看起来都不同,每个人遇到的问题也不同。即使我们对DoorDash最终实现的解决方案没有达成一致,我也希望你们能够记住,当你设计自己的系统时,需要考虑哪些关键基础,或者需要记住哪些关键原则,以实现可扩展性以及与数据库的解耦。这将我们带入了我们基础的第一部分,即设计上的解耦。Vinay提到,不同的数据库技术以不同的方式谈论CDC,它们有不同的语义,使用不同的技术。

我们在设计初期希望解决的一个问题是,我们的CDC连接器和CDC变更流应完全与我们使用的数据库技术解耦,这是我们第一个原则。下一个原则是,无论你从第一天起如何构建系统,它在未来会变得更加复杂。我们希望在开始开发解决方案时降低这种复杂性,让业务或技术用例来推动系统复杂性的演变,而不是我们一开始就构建复杂性。在许可和生态系统挑战方面,我们希望摆脱许可或生态系统的要求。如果你还没有看到,Redis从开源许可证转向了Valkey,这催生了Valkey。

我们希望系统具备足够的灵活性,以便根据其授权需求和生态系统的变化,替换不同的组件。同时,我们应具备足够的灵活性,以使用我们想要的任何开源技术。最后,Vinay谈到模式变更会破坏连接器,破坏变更数据流,因此我们要确保随着业务和技术挑战的发展,我们的契约也能随之演进。我们的系统架构不应让我们陷入必须重新思考整个设计的困境,因为契约发生了变化。从一开始,我们的设计就应该能够满足数据契约演进的需求。

解决方案

现在我们已经构建了这些基础,让我们看看基于这些基础的解决方案会是什么样子。这就带我们来到了写前意图日志(Write-Ahead Intent Log)。现在,可能会出现一个问题:我听说过写前日志(Write-Ahead Log),但什么是写前意图日志(Write-Ahead Intent Log)?如果你将其与写前日志进行比较,写前日志会发布脏键、操作、一些元数据,还会发布正在被修改的有效负载。在意图的情况下,我们不会这样做。我们不会将有效负载写入变更数据流,这是有意为之,因为我们不想再次走上数据契约嵌入到CDC系统中的老路。我们希望数据契约能够独立于我们如何执行CDC而演进。这就带我们来到了设计中的第一个组件,即“愚蠢的生产者”。

我们的“愚蠢的生产者”将我们从确定CDC连接器、选择数据库技术的复杂性中解耦,只专注于将变更写入日志。这使我可以跳过许多需要在适当时间执行的验证,并且不依赖于任何消费者或变更数据流的最终目的地是否准备就绪,就可以继续前进。这就是“愚蠢的生产者”的样子。你有你的应用程序。传统上,它连接到数据库,但在我们的情况下,它连接到这个“愚蠢的生产者”,我们称之为代理(proxy)。代理负责处理变更,并将其写入你的Kafka,也就是我们的意图日志,最后将其持久化到你需要的数据库中。

如果你注意到这张图,代理在整个流程中从未理解它所处理的数据模型。它只是负责接收变更并将其写入适当的位置。这就是我们所说的“愚蠢的生产者”。

接下来可能会出现一个问题:如果生产者不处理复杂性,那么系统中的复杂性在哪里?任何CDC系统,任何大规模分布式系统都内置了复杂性。无论你是在系统的一部分、另一部分还是整个系统中看到它,这种复杂性必须存在于某个地方。这就是我们系统中的复杂性所在,即“智能消费者”。智能消费者负责跟踪不同的主题、不同的表以及系统的不同状态。它理解你的数据模型,并根据你组织可能面临的业务或技术需求采取相应的行动。从那个“愚蠢的生产者”开始,“愚蠢的生产者”将你的意图写入Kafka。这就是我们的智能消费者,即意图消费者。它轮询意图,并再次通过代理进行处理。

它验证状态。这再次基于我们之前提到的业务需求。它通过与数据库进行比对,判断意图是否真正被实现。最后,一旦验证了状态,它就会将该事件发布到我们的事件总线中。再次提到一个可能的问题是,数据模型和与该事件相关的元数据在哪里?这些信息存储在我们的模式仓库中。如果你注意到了,这个模式仓库并不直接位于我们的数据平面路径中。这使得我们的模式和数据模型可以独立于消费者如何处理变更流而演进。如果数据模型发生变化,你只需要更改模式仓库,并在意图消费者中处理这种失败情况,变更流就可以继续正常运行。

你不需要去更新你的设计或更改任何连接器来适应新的数据模型。在之前的图表中,我们展示了这个事件总线,用于发布最终状态。事件总线本质上是 DoorDash 内部的一个基于推送的事件流处理解决方案。如果你想从高层次理解事件总线的作用,它基本上抽象了 Kafka 的分区、偏移量跟踪等复杂性,为产品团队提供了一个基于推送的模型来处理任何事件。

现在我们已经了解了现代 CDC 或 Write-Ahead Intent Log 的基本结构,接下来我们讨论的挑战是,如何让这个易于构建的系统实现良好的扩展性?为此,我们构建了一个并发骨干系统。每次你看到一个箭头或一个组件向另一个组件传递数据时,这都是一个机会,让你的系统要么因为没有为可扩展性做设计而变得缓慢,要么变得高度可扩展。我们在 DoorDash 做的事情,基本上是通过配置驱动的方式来增加或优化这些组件之间交接的机制。例如,如果我预计或观察到某个表的负载非常大,我可以增加 Kafka 上的分区数量。

这反过来可以帮助我增加连接到 Kafka 的消费者数量,从而提升同时处理的消息或意图数量。如果意图消费者有很多消息,但无法与代理快速通信,那么在这种情况下,你也可以增加代理的连接池,从而独立于从 Kafka 接收到的消息数量进行扩展。最后,当你与数据库交互时,你可以根据所选的数据库驱动进行扩展。当你将最终消息发布到事件总线时,你也可以再次独立进行扩展,因为这类似于一个 gRPC 连接。

回顾一下,我们最初的情况是这样的。你有一个应用程序,它直接与数据库通信。你依赖于数据库的特性来确定你的 CDC 策略。你将数据发布到一个或多个 Kafka 实例,然后你有其他应用程序监听这些 Kafka 实例,如果其中任何一个应用程序或 Kafka 消息出现波动,可能会导致数据漂移。最终我们得到了这样的结果:应用程序不再直接与数据库通信,而是通过一个代理进行通信。这个代理是我们所谓的“傻瓜生产者”,它不理解数据模型,也不理解你的业务需求,它只是将数据写入 Kafka,同时也会写入数据库。然后你的意图消费者从 Kafka 中拉取数据,它处理所有与业务和技术需求相关的复杂性,并从模式中拉取数据,以确定你需要哪些验证或数据模型。

学习经验

这一切听起来不错,但系统并不会像这样运作。我们从中也学到了一些东西。每当构建一个系统时,总会有人提出他们还需要其他功能。我们的情况正是如此。开发人员和业务人员并不总是希望得到相同的东西。他们希望有不同的失败模式。我们中的大多数人,或者至少是很多人,都了解双重写入的问题。我们正在向两个不同的系统写入数据,而这两个系统并没有被包含在同一个事务中。如何处理这两个系统各自独立失败的情况?我们所做的,是将这个责任交给我们的终端客户,让他们告诉我们他们希望采用事务性失败还是独立失败的模式。在这种情况下,我们所说的事务性失败是指,如果写入 Kafka 主题失败,我们不会写入数据库。

在这种情况下,我们整个事务都会失败,我们拒绝你的变更,不会有消息被发送,什么都不会发生。如果 Kafka 写入失败,但我在做某些统计工作,例如跟踪我获得的星标数或 YouTube 的观看数,那么在这种情况下,我并不一定需要让数据库事务失败。我可以在会计期末进行对账,或者在数据库上设置一个尾部跟踪。在这种情况下,我可以独立地处理失败,我不需要仅仅因为无法写入意图就让数据库事务失败。再次强调,这取决于你的业务需求和技术使用场景,你如何解决业务中的问题。

快速问一个问题,如果 Kafka 写入成功,但数据库写入失败了会怎样?

我们是可以处理这种情况的,但在这个情况下,消费者或客户已经验证了状态。当它验证状态时,它知道意图实际上并没有实现,因此它会拒绝该消息并继续处理。你不需要处理这种情况。再次强调,这非常依赖于具体的业务需求。如果你需要,你可以根据你的业务需求来处理这个事件。

我们拥有所有这些 Kafka 和数据库,但一个经常出现的问题是,如果我的表与其他位于同一 broker 上的表不兼容怎么办?如果我的消费者没有连接到相同的 Kafka topic 又会怎样?我们构建了一个动态流量重定向机制,使我们能够根据需要扩展这些表或将它们移动到不同的 Kafka topic。这就是我们最初开始的方式。我们有一个应用程序,它将数据写入代理,然后写入数据库。你已经多次看到这个图表了。这个 Kafka 实际上是一个负担。我们有一个表每秒处理数百万次查询,而这个 Kafka 上还有其他表,导致了“噪音邻居”问题。我们所做的就是启动一个新的 Kafka broker,附加一个新的意图消费者。它遵循相同的业务规则,因为所有内容都是从模式注册表中派生出来的。

这个意图消费者中并没有任何本质上其他意图消费者无法知道的内容。一旦我们完成所有这些设置,我们允许代理连接到这个 Kafka。我们实现这一点的方式是向代理发布路由信息。我们告诉代理,对于这张表,现在你不再需要连接到原来的 Kafka,而是需要连接到这个新的 Kafka,它理解新的表语义。而所有在旧 Kafka broker 中未被消费的消息仍然可以由相同的意图消费者处理,但任何新的消息都不会发布到现有的 Kafka。另一个我们面临的问题是,如何在这些事件总线主题之上扩展不同的接收器或不同的消费者。

对我们来说,解决方案很简单,因为我们已经将数据库与 CDC 流程解耦,我们可以在事件总线上附加任意数量的 CDC 消费者。由于事件总线负责基于推送的机制,它使我们无需担心分区处理、何时压缩 broker 或 topic,同时允许事件总线持续前进。

关键要点和注意事项

关键要点是什么?在传统的 CDC 中,我们讨论了抽象的问题,不同的数据库使用不同的语言,这使得在单一方式上标准化变得更加困难。可扩展性是一个问题,因为你的变更流与数据库本身紧密耦合,变更流运行在实际的 pod 上,因此你无法很好地进行扩展。你存在供应商锁定,你依赖于特定数据库提供的变更流机制。如果某个数据库没有变更流机制,你就无法做任何事情。最后,一旦 CDC 变更被打破,你该如何继续?你的应用程序如何恢复?这些都是我们在传统 CDC 中看到的挑战。而现代 CDC 有助于回答其中的一些问题。其本身的设计是可演进的。

你可以选择适合你需求的部分,而不是专注于某个特定设计的某个方面。它为你提供了很高的杠杆作用。由于我们有这些可重用的组件,如意图消费者和模式日志,你可以灵活地移动事物。你可以增加你从整体架构中获得的杠杆作用。你可以共享资源,可以优化事物。最后,它与你的整个环境非常松耦合。无论你使用的是哪种数据库,使用的是哪种流处理解决方案,你都可以遵循相同的架构原则来构建变更数据捕获。

没有任何系统是完美的。同样,这个系统也不是完美的。我们有一些权衡取舍。我们想讨论的几个权衡之一是“读取你自己的写入”语义。在消费者端,我们看到你必须回到代理以验证状态。这导致了一种“读取你自己的写入”语义,即在将消息发布给任何其他客户端之前,对于每个意图或每个变更,你至少要从数据库中读取一次状态。你可能已经注意到,当多个写入操作在发送给消费者时相互追赶或竞争时会发生什么。在这种情况下,这种现代的 CDC 允许你捕获状态,而不是任何单个变更数据字段。在我们的情况下,这是一个可接受的解决方案。

并不是每次都需要捕获实际发生的变更。大多数时候,产品团队或应用程序更关心系统当前的最新状态,而不是导致系统达到这个状态的变更。最后,在验证状态时,如何确保写入确实失败了?我应该直接拒绝这个意图,还是它只是延迟得足够多,以至于我无法验证其状态?这再次是你可以与技术或业务团队协商确定的,以确定你的应用程序需要等待的合适时间。

将所有内容联系起来,这个系统的性能如何?由于“愚蠢”的生产者是愚蠢的,它不理解数据模型,也不需要进行任何验证。它允许你的数据库保持高性能。你可以从你选择的任何数据库技术中获得最大性能,因为它不需要担心与 CDC 管理相关的额外内容。每个组件都有单一目的。代理的唯一目的是将所有内容推送到 Kafka 或你的数据库。你的消费者有单一目的,即从 Kafka 中轮询,然后确定需要进行哪些模式验证,然后将其推送到相应的位置。事件总线负责确保每个同步操作都能从系统中获取最新的状态。这是成本有效的,因为现在你有不同 Kafka 代理、不同消费者和代理,你可以进行按需扩展。

如果你需要增加处理的写入量,只需扩展代理和 Kafka 以处理这些写入。如果你还担心处理这些意图时的延迟,可以在需要时按需增加消费者数量以适应这些扩展。一旦需求结束,就可以再次缩减规模。

问答环节

参与者 1:在什么规模下,你们开始看到 Debezium 连接器在数据量或事务数量方面出现性能问题?

Vinay Chella:我们看到 Debezium 连接器面临哪些挑战?

参与者 1:是的,在什么规模下,你们开始看到它在数据量和每秒事务数方面开始出现性能问题?

Vinay Chella:尤其是在我们使用 Debezium 进行负载测试时,我们特别用它来测试 Cassandra。随着我们扩展 Cassandra,Debezium 会从主进程中消耗大量 CPU。特别是在多区域模式下运行时,Debezium 连接器会导致写入顺序混乱。我们的延迟几乎翻了一倍,而且对 CPU 的需求非常高,导致 Cassandra 进程因 CPU 饥饿而影响可用性 SLO。

参与者 2:我的问题与你之前提到的场景有关,即意图写入成功,但数据库写入失败。我想进一步探讨这个场景。我想问,在这种情况下,如果应用程序想要执行某种优雅的服务降级,例如切换到另一个服务(该服务提供的用户体验较差),或者指导用户手动绕过某些操作,从而减轻影响,那么在这种情况下,代理是否会将失败信息回传给应用程序,以便应用程序做出相应的决策?

Vinay Chella:是的,完全正确。当这些失败发生时,代理在响应中会告知意图日志是否已持久化,响应码中将包含这些详细信息,服务可以根据需要采取适当的行动。此外,这还可在表配置级别进行操作。在配置代理中的表时,你可以设计它以指定是使用事务模型还是独立模型,并采取相应的措施。

参与者 2:根据你的值模式,你可以相应地做出反应。

参与者 3:我有一个关于代理的问题。代理在单写入者方面是单例的吗?可以有多个应用程序写入代理,但写入顺序始终是按顺序进行的,一次一个写入到 Kafka 队列中吗?

Vinay Chella:代理内部被称为 Taulu。它是在数据库之上的键值抽象,是一个分布式系统。它使用在所有代理实例之间同步的写入意图时间日志,该时间戳会被传递到 Kafka,并且也会持久化到你的数据库中。

参与者 3:这是代理本身的时间戳。

Vinay Chella:正确。

参与者 3:那么一个相关的问题是,如果数据血缘很重要,如何修改系统以同时捕获顺序以及所做出的确切更改,而不仅仅是最终状态?

Vinay Chella:不幸的是,顺序无法得到保证。顺序由代理中维护的时间戳来保证。我们有意选择不在写入路径上执行“先读后写”操作,以真正了解发生了哪些更改。在传统的 CDC 中,当你从传统数据库获取日志时,你会看到预像和后像,从而可以重建数据。但这并不是本系统的一部分。

参与者 4:我的问题是关于代理服务与数据库之间的通信。可能会出现一种情况,即存在多个数据源,数据需要持久化。代理如何根据应用程序的请求来维护这些信息,以便了解该请求需要持久化到哪里?

Vinay Chella:这属于表的元数据部分。表的元数据会说明表的类型,以及它是否应该持久化到如 Redis、Cassandra 或 Postgres 这样的系统中,这些元数据使我们能够知道数据应该持久化到哪里。

Akshat Goel:当我们将路由信息发布到代理时,这些信息不仅包含有关新 Kafka 的信息,还包含有关数据库的信息。

参与者 5:我记得在谈话开始时提到代理写入意图,但实际上并不包含负载。负载是在这个路径的哪个位置被提取的?我假设负载最终会被发送到事件总线?

Vinay Chella:是的,没错。意图消费者是从数据库中提取负载并将其放入事件总线的组件,所有复杂性都被抽象掉了。负载很大,我们有灵活性将负载放入 S3 并在事件总线中放置指针。这种解耦对我们也进行了抽象。

参与者 5:在这种情况下,每次写入都会将意图持久化到 Kafka。意图消费者需要从数据库中读取以提取负载。这会不会导致一些问题,比如增加数据库的额外负担,因为每次写入现在都需要在数据库上进行一次读取以执行 CDC?

Vinay Chella:这是我们考虑的一个因素。我们有意选择了这种设计,以实现写入后读取的场景。当我们遇到这些规模限制时,我们有一些未来的思路,比如如果你使用的是传统数据库,你可以扩展到只读副本,意图消费者与代理通信时,我们会智能地路由请求,以确保主流量不会中断。如果你使用的是像 Cassandra 这样的无主分布式数据库,你总是可以依赖一个数据中心,其唯一任务是响应这个代理。我们目前还没有遇到这种情况,因为我们仍在部署中,但这些是我们后备的思路。

参与者 6:当意图消费者从 Kafka 消费到某条信息,但该信息尚未写入数据库时,你们如何处理?这可能会发生,因为它们是独立的。我们只是重试,还是有其他方法?

Akshat Goel:我们会重试,并让客户选择重试的时长。默认情况下,我们允许该消息最多可见 10 秒或写入可见 10 秒。你可以选择在验证状态时,特定消息应重试多长时间。在此之后,我们认为写入失败。

参与者 7:使用意图消费者时,你们会从 Kafka 接收到推送,然后需要检查数据库。这可能尚未发生,或者你需要检查各种其他内容。你们如何确保这仍然保持在推送的思维模式中,而不是仅仅拉取数据库?

Vinay Chella:作为意图消费者的一部分,我们考虑了多个因素。其中一个因素是元数据中携带的时间戳。我们会使用该时间戳检查数据库,以确定它是旧状态还是最新状态。获取该信息后,将其放入事件总线。事件总线负责推送。事件总线的设计是基于推送的,所有订阅事件总线的 CDC 消费者都会收到这些更改的通知。

参与者 8:什么是意图?

Akshat Goel:在我们的情况下,意图只是脏键、操作和表的元数据。它就是值,但不包含负载。这就是我们的意图。

查看更多带有文字稿的演讲

录制于:

2026 年 6 月 18 日

  • Vinay Chella
  • Akshat Goel

#### 相关赞助商

  • 理解 AI 代理安全

#### 相关赞助商

使用 Promptfoo,自信地测试、评估和对您的 LLM 应用进行红队演练 —— 捕捉回归问题、基准测试模型,并更快地推出高质量的 AI 功能;立即开始测试您的提示。了解更多。

#### 此内容属于 AI、机器学习与数据工程主题

##### 相关主题:

  • 架构与设计
  • AI、机器学习与数据工程
  • 平台工程
  • QCon 旧金山 2025
  • 演讲稿
  • 数据访问
  • 敏捷开发
  • QCon 软件开发会议
  • DevOps
  • 数据库
  • InfoQ
  • 相关编辑内容
  • InfoQ 上的热门内容 ArrowJS 达到 1.0,成为首个面向代理时代的 UI 框架 Anthropic 发布并暂时暂停 Claude Fable 5 Slack 在 EMR 管道中淘汰 SSH,将 700 多个任务迁移至基于 REST 的架构 Anthropic 解释 Claude 如何构建自己的执行框架 Spring Boot 4.1 添加 gRPC 自动配置、SSRF 防御和 Kotlin 2.3 支持 提升用户的数据自主权:从 BlueSky 的 AT 协议到本地优先软件运动

AI 可能会生成不准确的信息,请核实重要内容