WIX是如何从CRUD转换到Event Sourcing?


Wix.com是一个基于云计算的Web开发平台,它允许用户通过使用他们的在线拖放工具来创建HTML5网站和移动网站。
WIX的产品愿景是朝着反应式Reactive函数发展,这意味着在正确的上下文中实时对多个领域事件做出反应。问题在于,我们的单体应用被设计为经典的 CRUD 系统,在发生状态变化时同步运行业务逻辑。
本文是关于我们如何将事件溯源和事件驱动架构引入我们的客户支持平台Wix Answers的系列文章中的第一篇,这种方式允许逐步迁移,现在可以提供新的业务价值,而不会将现有功能置于风险之中。传统的系统设计CRUD 方法侧重于状态以及多个用户在分布式环境中如何创建、更新和删除状态,而事件溯源方法侧重于领域事件、它们何时发生以及它们如何表达业务意图。在事件溯源方法中,状态是事件的具体化,这只是领域事件的许多可能用法之一。
Wix Answers 是一种客户支持解决方案,它将票务、帮助中心和呼叫中心等支持工具整合到一个具有高级内置自动化和分析功能的直观平台中。
 
我们是如何将领域事件引入我们的整体 CRUD 系统的?
我们需要问的第一件事是真相的来源是什么。我们的单体系统通过 REST API 接受状态改变命令,更新 MySQL 中的实体,然后将更新的实体返回给调用者。
这使得 MySQL 成为事实的来源。如果不对我们的单体应用以及它与客户端通信的方式进行重大更改,我们就无法改变它,而后者必须变得异步。这将导致客户端发生重大变化。


将数据库二进制日志流式传输到 Kafka 是一种众所周知的做法,旨在复制数据库。对表行的每次更改都保存在 binlog 中,作为具有先前和当前行状态的记录,有效地将每个表转换为可以以一致方式具体化为实体状态的流。我们使用Debezium源连接器将 binlog 流式传输到 Kafka。
使用Kafka Streams stateless转换,我们把一个CDC记录到一个命令发布到总命令主题。我们这样做有几个原因:
  • 在很多情况下,我们有多个表使用实体 id 作为二级索引。我们希望我们的聚合处理与相同 id 相关的所有命令。例如:您可能有一个带有主键 orderId 的“Order”表和一个带有 orderId 列的“OrderLine”表。通过将 Order CDC 记录转换为 UpdateOrderCdc 命令,将 OrderLine CDC 记录转换为 UpdateOrderLineCdc 命令,我们确保相同的聚合将处理这些命令并可以访问最新的实体状态。
  • 我们想为所有聚合命令定义一个模式。该模式可能以 CDC 更新命令开始,但可能演变为更细粒度的命令,这些命令也可以由相同的聚合处理,从而实现向真正的事件溯源架构逐渐演变。

随着聚合处理命令,它逐渐更新 Kafka 中的实体状态。我们可以重新创建源连接器并再次流式传输相同的表 - 但是,我们的聚合会根据 CDC 数据与从 Kafka 检索到的当前实体状态之间的差异生成事件。在某种程度上,就我们与 Monolith 并存的流媒体平台而言,Kafka 成为了真相的来源。
 
CDC 记录代表已提交的更改 - 为什么它们不是事件?
CDC feed的目的是以最终一致的方式复制数据库,而不是生成域事件。获取包含 before 和 after 元素的 CDC 记录并通过在 before 和 after 之间执行 diff 操作将其转换为域事件可能很诱人。但是,仅依赖 CDC 记录存在一些主要缺点。
当我们执行无状态转换时,我们无法正确响应来自不同表的 CDC 记录,因为不同表之间没有顺序保证。我们可能会在获取订单记录之前处理订单行记录。一个好的领域事件将提供一些 Order 上下文作为 OrderLine 事件的一部分。有状态转换允许我们使用聚合状态作为 OrderLine 的存储,并且只在 Order 数据到达时发布 OrderLine 事件。这是作为实体事件源的聚合责任的一部分。请记住,我们无法一次性实现纯架构,而只能实现并做边改模式。
 
引入快照
binlog 永远不会包含所有表的完整更改历史记录;因此,我们为新表配置的每个新 CDC 连接器都将从快照阶段开始。连接器将在 binlog 中标记当前位置,然后执行全表扫描并将所有行的当前状态流式传输为带有快照标志的特殊 CDC 记录。这本质上意味着在每个快照中我们都会丢失领域事件信息。如果订单状态随时间变化多次,快照将只提供最新状态。这是因为 binlog 的目标是复制状态,而不是成为事件溯源的支柱。这是聚合状态存储和聚合命令主题变得至关重要的地方。我们希望以每个表只执行一次快照的方式设计我们的解决方案。
事件溯源的强大功能之一是能够通过回放历史事件或命令来重建状态或重新创建领域事件。在这里执行另一个快照不是正确的解决方案,因为快照会丢失事件信息。
如果我们想重新创建我们的领域事件,我们需要重置我们的命令主题的消费者。命令主题将 CDC 记录打包成命令,并且已经以正确的顺序(或我们的聚合知道如何处理的顺序)存储来自不同表的命令。
 
总结
我们只涉及了使 Monolith 具有反应性的旅程中的基本步骤。我们讨论了如何使用 CDC 来构建命令主题以及为什么 CDC 记录不是命令。一旦我们有了一个命令主题,我们就可以使用有状态转换来创建事件,我们就可以开始享受事件溯源的好处:重放命令以重新创建事件,重新处理事件以实现状态。
在接下来的文章中,我们将讨论更高级的主题:
  • 如何使用 Kafka Streams 来表达聚合的事件溯源概念。
  • 我们如何支持一对多关系。
  • 如何使用事件通过重新分区事件来驱动反应式应用程序。
  • 我们如何重新处理命令历史记录以重新创建事件,而无需停机来响应事件的响应式服务。
  • 最后,我们如何在多 DC Kafka 中运行有状态转换(提示:镜像主题真的不够)。