2020 Google开发者大会重磅开幕 了解详情

虚拟小组:利用事件溯源获得成功

2018 年 3 月 14 日

关键点

  • 事件溯源(Event Sourcing)的做法有点像分类账。每个跟一样“事物”(又名“聚集”)有关的事件被记录在一个持久化的日志中(又称事件存储)。无论何时需要知道某个事物目前的状态,都可以通过“重放”所有的事件到事物重新创建出来。
  • 要重视我们有界上下文的边界和识别,并且我们在代码中非常仔细地管理域之间的边界也很重要。这意味着在特定的事件上要避免跨域的依赖关系。
  • 开发人员是否应该定制 CQRS 和事件溯源框架的核心部分?不应该。
  • 各团队必须做出两个互斥的选择:是否选择事件驱动架构?要不要应用事件溯源?

事件溯源的想法早就有了。但是最近,我们看到了这个数据存储和检索模式的更多。什么时候该用这种方法?架构的影响有哪些?哪些地方依赖平台还是应用框架?为了回答这些问题,InfoQ 请了两位专家给予帮助。其中Ben Wilcock 是 Pivotal Labs 的高级方案架构师,Allard Buijze 是 AxonIQ 的创始人兼 CTO,AxonIQ 就是广受欢迎的 Axon 框架的幕后公司。

InfoQ:请问什么是事件溯源?能否给我们举个实例,说明这个模式是对更为传统架构的改进?

Buijze:事件溯源是一种存储实体的风格,不是直接存储状态,而是一系列事件(比如,事件流),描述了过去在实体上发生的所有变化。实体当前的状态是通过在一个“空”实例上重放所有发生过的事件重新计算后得出。一般来说,事件流描述了在一个聚集上发生过的变化:一组实体被看做是状态变化的单位。例如,不再存储订单的状态,而能够查看特定物品订购和取消订购的情况,有了事件溯源,可以存储订单创建、加入了一些物品、移除了一些物品、确认订单、然后发货,然后取消的事实。事件溯源包含了很多可能有价值的信息,但是如果只存储状态,这些信息就会丢失。

事件溯源有很多优点(当然也有缺点!)。例如,它提供了一个可靠的审计追踪,可以查看某个组件过去发生过什么事。那是因为它不会把这个“追踪”作为副产品来存储,而是把它作为当前状态的唯一依赖。所做出的任何决策能用这些过去的事件来解释。

在我参与的一个项目中,我们使用事件溯源使审计就像应用的“原生”功能一样。我们知道这些信息对分析来说也会有用,但是还根本不知道如何使用。该应用程序是一个在线纸牌游戏(叫做桥牌),能让用户打锦标赛并赢取真正的现金奖金。和扑克锦标赛有点类似。因为涉及大笔现金奖励,审计是非常重要的。

我们还没对项目深入研究,就发现我们不喜欢在“游戏引擎”中使用的领域模型,即实现游戏规则的组件。因为我们使用了事件溯源,我们所有的测试都是“给出过去的事件,当执行这个命令时,期望发布这些新事件”这样的风格。无论是命令还是事件,都是通过功能需求驱动的,而不是由技术驱动。这意味着我们能让这些测试保持不变,可任意重新实现“游戏引擎”。

一段时间之后,随着系统的积极使用,审计追踪已经证明它是有效的:产生了很多用户有“可疑举动”的投诉。首席开发员决定创建一些分析工具来调查一下。通过重放过去的事件,在此基础上创建不同的视图模型,他设法揭开了共谋玩家的网络,这个网络增加了他们获胜的机会。他们设法在支付之前,从其账户中获取大笔金钱。

如果只存储当前状态,揭开这个欺诈所需的信息有很大可能已经丢失了。

Wilcock:当我和客户在一起时,我尝试用他们能产生共鸣的方式来解释事件溯源,并映射到他们所认知的领域。所以如果我和零售商在一起工作,我也许用“产品目录管理”做例子。传统上,如果开发人员被要求为这样的领域开发一个解决方案,他们或许首先会从基于持久层的 CRUD 的设计而不是事件溯源开始着手。然而,很快,因为领域真实属性被发现,人们很快就会明白,给每个产品限定为一行记录是不够的。事实上,产品加入目录是通过一个复杂的生命周期完成的,其中充满了关键决策点(事件),出于各种原因,这些点需要进行认真地追踪,这些原因包括:监管及竞争合规性、安全因素、盈利因素、供应链完整性因素、运营因素和许多其他影响产品进入目录的过程,还有产品在目录中时的产品生命周期。

当我们尝试利用纯粹的基于 CRUD 的模式来处理复杂的需求时,很快就需要开始增加“审计表”、“提示信息”、“关系”、“回滚”和“报告”,并且所有这些大大增加了我们 CRUD 模型的复杂性。一开始看起来是“简单可行的事”,很快变得非常复杂,而不是一个特别整洁或优雅的解决方案。以 CRUD 开始着手,我们也许很快会陷入困境。

出于这个原因,我尝试在设计过程中尽早给客户介绍事件溯源。事件溯源有着根本性的不同。它是像我刚才描述的一个复杂领域的真实属性驱动和塑造的。事件溯源对待领域事件像是方案设计的一等公民(而不是像用 CRUD 那样经常是事后考虑因素)。对于那些刚接触这个概念的人来说,事件溯源的运作有点像分类账(是我们所知的最古老和最成功的记录保存系统)。每个事件和一个“事物”相关(又名“聚集”)被记录在一个持久的日志中(又名事件存储)。无论何时需要了解某个事物的当前状态,可以通过重放发生在该事物上的所有事件来重建。因此,例如,如果我们在追踪一个产品的 RRP,我们用 PriceChanged 来做,那些事件也许会显示最初是 Bob 设置价格为 $199,然后 Jill 把价格降到 $149,现在的价格 $109 是 Jane 改的。“事件存储”会为该产品保存所有这些事件,同时可以通过“重放”PriceChanged 随时获得该产品的当前价格。

这个方法的美妙之处在于无需任何单独的审计或价格历史表。我们知道 Bob、Jill 和 Jane 在过去的某个时间点改动过产品价格,因为我们保存了所有这些事件并通过它们来“溯源“当前的产品。我们还可以很容易回到过去显示历史价格。最后,我们能回顾性地添加有意义的完整历史报告,比如“谁是最早的价格变动者”、“哪个季度的价格波动最大”以及其他业务洞察。因为我们有所有事件的记录,可以做很多非常齐整的事情,能建立一个系统的新版本,查看在投入生产前用真实事件的记录会有怎样的表现!

当然,事件溯源最初会有一条相应的学习曲线,但是一旦考虑到企业规模领域的真正复杂性,通常它是更优雅的解决方案架构。

InfoQ:在一个事件溯源的世界里,我们该怎样思考跨领域交互?在 Ben 举的例子中,我们是否需要维持 PriceEvents 和另一个“事物”跨聚集边界的一致性期望?在长期运行的 Sagas 中,这个如何做出来?

Wilcock:我认为我会把它作为更普遍的领域驱动设计(Domain Driven Design)问题来解释,而不是一个纯粹的事件溯源问题。因此,我建议读者找找 Vaughn Vernon( @VaughnVernon )的《DDD Distilled》和《 Implementing DDD》来看看,以获取一些该主题相关的伟大见解。

关于如何思考跨域交互的具体主题,我认为重要的是尊重我们有界上下文的边界,认识到我们在代码中非常小心地管理领域之间的边界的重要性。我们也许希望避免的是引入在特定事件上跨领域的依赖关系(像 PriceChanged 事件)。如果我们这么做(比如,在跨有界上下文中共享事件的类定义或格式),就会减弱我们独立编写代码的能力,并在无意识的代码中引入物理绑定。

当然,这会影响我们对一致性的看法,但是我认为每个相信 DDD 和微服务想法的人也许已经接受这么一个事实:要保持跨领域的一致性是困难的,从而可能接受最终一致性和松耦合。正是出于这个原因,像防腐层(Anti Corruption Layer)之类的模式是 DDD 架构的一个共同特征。

Buijze:从本质上说,事件回溯的概念本身从没跨界。尽管确切的定义不同,我认为事件溯源是将对象的状态作为一系列事件(而不是状态)持久化的选择。一个对象是如何被持久化的,不应该泄漏到组件之外,更不用说上下文了。

然而,在实践中,描述对象更改的事件通常对其他组件非常有用,它们常被用于同步不同的模型。例如,应用 CQRS 时在同一上下文或不同上下文中更新视图模型。

正如 Ben 所指出的,在后一种情况下,需要额外的预防措施以确保在上下文之间不产生不想要的耦合。要避免的一件事是简单地发布所有事件给任何组件使用。举个例子:一个 Shipping 模块想知道下订单的时间。但是,Order 模块不会精确地发出该事件(Order 比这个更棘手)。相反,它发出 OrderCreated、ItemAdded、ItemRemoved、PaymentInformationRegistered 和 OrderConfirmed。Shipping 模块需要监听所有这些事件才能获得其工作所需的信息。比监听所有事件更糟的是,它也重复了很多与处理这些事件相关的“逻辑”,比如如何匹配 ItemRemoved 和 ItemAdded。

用到跨上下文时,这就是应该仔细处理事件的地方。通常,我们建议客户只在处于同一上下文的组件之间共享事件。为了在上下文之间同步组件,需要以更粗粒度的事件来描述发生的事。一个解决方案应该发出一个不同的事件,比如确认订单时,OrderPlaced 包含相关的订单详情。另一种方案是把 OrderConfirmed 事件看作所谓的“里程碑”事件,让它包含更多相关信息,这样它在 Order 上下文之外也有价值。

两种方案各有优缺点,在 DDD 中被看作是策略设计(Strategic Design)的一部分。

在这个意义上,Sagas“只是一个部件”。比如,它们处于同一上下文中,处理低层次的跟订单相关的细节,或者处于不同的上下文中,协调一些更高层次的流程,例如确保在下了订单后生成发票并计划发货。它们有助于减少上下文的直接耦合,因此发货模块不需要确切知道“何时”创建发货。Saga 可以协调。

InfoQ:在事件溯源中,应用程序框架与平台组件的作用是什么?也就是说,在一个 ES/CQRS 体系结构中,我们应该期望从事件流处理器或数据库,还有像 Axon 之类的 Java 框架中得到什么?此外,开发人员是否应该定制构建这个架构的核心部分?

Wilcock:我会请 Allard 来比较一下框架、流处理和事件溯源。但是对于这个问题,开发人员是否应该定制构建这个架构的核心部分,我的回答是很坚决的,不应该,就算他们有这个选择也不行。

最近,这个同样的问题出现了几次,我的回答一如既往,不要做。设计、测试和交付一个安全的生产级 CQRS 和事件溯源架构是相当困难的(我确信 Allard 会在这个问题上支持我),而且也容易出错。那不是说做不到,可以做的,但是改变不了“非竞争要素带来的开销”这个事实。

在 2018 年建立自有的 CQRS/ES 架构肯定是没有价值的,它不会给常规的银行、零售商、生产商、服务供应商等等增加任何价值。要开发人员在一个 DIY 方案上工作也许在技术上有点吸引力,但是这是一项复杂的工作,需要时间、智力和金钱,而那些真正重要的是吸引新客户或者增加利润。

对我来说,更明智的做法是去找有声誉的开源替代品,从它开始入手。有很多可靠的选择:对于 Java 开发人员来说, Axon 框架是个非常好的选择。它成熟、稳定、可扩展、和 Spring Boot 配合得很好,在各行业中有一些很好的参考用户。.NET 上的开发人员和用 Node( Wolkenkit )及一些其他编程语言的使用者一样有很多不同的选择(比如 Brighter )。我会先调查这些替代品,然后再做。因为它是开源的,所以如果真有必要,稍后你总是可以复制出自己的分支的。

Buijze:显然,作为 Axon 框架的创作者,我是完全站在它这边的,但是我也完全赞同 Ben 所说的。我想补充一点 Axon 框架存在的理由,我在开始用 CQRS 和事实溯源实现一个应用程序的时候,发现需要大量的“管道”。我就开始分享代码中的通用部分,大家去复用它们。那是 2010 年年初。因此,你再自己做一套就意味着你忽视了我这 8 年的经验。

如今,我把 Axon 框架定位于一个帮助分离业务逻辑和基础设施逻辑的框架,允许开发人员用 DDD 原则实现业务逻辑,提供开箱可用的(通用)基础架构元素。显然,组件需要交互。我们已经确定了这种交互的 3 个原因,每个有自己的消息类型:命令——系统必须执行某些操作(比如,更改状态),事件——通知发生了什么事,以及查询——请求信息。每个消息有非常不同的路由模式。

使用适当的抽象时,组件不需要知道确切的用于传送消息的基础架构组件。甚至组件是否作为一个独立部署单元的一部分,还是在不同的单元中部署,这些都没关系。必须选择 / 设置基础架构元素以匹配选定的部署风格和非功能性的需求。这正是选择事件流处理器的地方。这是一个应用程序框架的角色,以确保能用适当的抽象访问这些处理器,因此,业务逻辑就不会受到实施特定选择的约束。

在 AxonIQ,我们已经注意到大多数数据库(特别是关系型的)适合作为事件存储提供服务。但是,当在数据库的数据量增加时,性能会受到影响。尽管我们有种自然的倾向,认为 NoSQL 是解决方案,事实上它们比起好的古老的 DBMS 也没有多少优化。重要的是明白数据库(特别是 NoSQL 实现的)做了什么选择。这些选择通常和事件存储实现的期望有冲突。这就是我们为什么在最近发布 AxonDB(一个为事件溯源而存储大量事件的优化数据库)的原因。

关于开发人员是否应该创建自己的核心部分的问题,我的回答是否定的。可惜,我们有时不得不下结论说,还没有什么太合适的选择可以匹配我们的非功能部分。

InfoQ:对那些有很多团队的大型组织中,尝试事件溯源的团队,您有什么建议?陷阱在哪里?什么应该分享,什么不应该分享?

Buijze:我的建议会是两个明显不同的选择。一个是是否选择事件驱动架构,另一个是是否应用事件溯源。虽然这两种技术能相互促进,但是两者之间没有严格的依赖关系。

在组件之间的通信中是否把事件作为主要元素是一个架构决策。所有的(至少是大多数)组件都应该支持它,以保持真正的价值。我相信之前的问题的答案已经表明,在建立复杂系统时,使用事件是非常强大的方法。

是否使用事件溯源是每个组件要做出的决定。这可以是架构层面的指示或指导,跟何时做或不做有关,但是个本地决策。就算不是在内部用那些事件作为唯一的状态来源,也仍然可以发布事件。你仍然可以查看发生了什么(通过存储发布的事件)和异步通知其他组件,通过这些能力来获益。它作为审计线索的可靠性不高,因为不能保证状态和历史的匹配。这就好像把一个小分类账放在 Kenny 的涂鸦墙边上,所有覆盖之前作品的艺术家都必须签字。这个主意不错,但是有时有很大的可能会出现条目丢失的情况。没有审计追踪也许比有个证明你做错了的东西更好。

我也敦促那些在大型(分布式)系统上工作的人,特别在用事件溯源架构时,要读读域驱动设计(Domain Driven Design)。特别在关于有界下上文部分有一些很好的指导方针,用于选择在哪里放置某些逻辑和如何在组件间设计交互。仔细选择依赖关系的方向。事件是反转依赖关系的好方法,但是我们不要忘了还有命令(Commands)和查询(Queries)。

最后,我很高兴看到在现代架构中,事件占据了更主导的地位。我们必须要小心谨慎,不要过度反应,觉得处处有事件(小心不要因为手里拿了锤子,就到处是钉子)。我们也不要忘了命令和查询,要给予它们同样的关注。

Wilcock:我可能会先说孤立的事件溯源绝对是有用的,但是用于补充 CQRS 架构和域驱动设计时,它的功能和潜力被放大了。使用孤立的事件溯源的一个缺陷是,它会被简单地看作是一个持久性机制的替代品,但是这破坏了其更广泛的潜力,它有能力(连同 CQRS 和 DDD)把分布式事件驱动架构放到一个优雅和可扩展方法架构的中心。类似的,如果事件只是简单地“出现”,不带有追溯性或者“因果关系”,无法回溯出是哪些命令产生了它们,那么似乎也错失了让其充分发挥的良机。命令产生事件,这对很多系统问题来说,的确是个优雅的解决方案(在我的书里,远比传统的 CRUD 方法优雅)。

在大型组织中,另一个潜在的缺陷是团队之间的共性或标准化。如果有很多团队,各自都有实现事件溯源的方案,会导致时间、金钱和智力的极大浪费,未来几年将出现显著的维护难题。在我看来,更好的方法是从以一个共同的框架作为基准开始,并且只有在充分理由下才能偏离此框架。CQRS 和事件溯源是一个已被解决的问题。不要重新造轮子,更不要同时有 10 个团队造轮子。

最后,对在团队间分享什么事件数据要万分小心。有些事件可以共享,但很多是不能分享的,不要引入团队间的依存关系和耦合,在未来会很难解耦。实践证明,像“防腐层(anti-corruption layer)”之类的模式可以有效防止这些无意识耦合,但是也会给你的工作增加一些复杂性和开销。无论你做了什么决定,小心设计事件、思考在事件中应该有什么数据、什么可以不用管、考虑事件分类以用于识别和区分“内部”和“外部”事件时是有帮助的。设计正确的事件驱动解决方案会是个挑战,但是回报是巨大的!

小组成员简介

Ben Wilcock 是 Pivotal Labs 的高级解决方案架构师。他帮助 Pivotal 的财富 500 强客户利用 Pivotal Application Service(PAS)和 Pivotal Container Service(PKS)让云本地化。Ben 对 CQRS、事件溯源、微服务、云和移动应用充满热情。他也是一位成熟的技术博客作者,他的文章在 DZone、Java Code Geeks、InfoQ、The Spring Blog 等等上面都有介绍。你可以关注他的推特账号 @benbravo73 ,阅读他的博文

Allard Buijze 是 AxonIQ 的创始人和 CTO。从 6 岁开始,他就对编程产生了极大的热情,已经指导了大大小小的组织构建高性能和可扩展的应用程序。现在,他正在致力于利用域驱动设计、命令 - 查询责任隔离和事件驱动架构的概念让大型系统的实现更容易。最初,作为一个实验,他创造了 Axon 框架,但是当大型机构和组织都开始使用 Axon 作为他们复杂问题的解决方案时,AxonIQ 就诞生了。他的信念是只有通过和他人不断及密切的交流才能得到好产品,藉由此理念,Allard 现在成了经常在会议和聚会上发表演讲的人,他乐于为开发人员和架构师们提供培训。Allard 也经常出现在董事会上,为高级管理人员解释 DDD、CQRS 和 EDA 的概念和价值。

阅读英文原文: https://www.infoq.com/articles/panel-event-sourcing

感谢冬雨对本文的审校。

2018 年 3 月 14 日 18:20 1027
用户头像

发布了 199 篇内容, 共 64.2 次阅读, 收获喜欢 270 次。

关注

评论

发布
暂无评论
发现更多内容

Python时间序列分析简介(1)

计算机与AI

Python pandas 数据处理 时间序列

架构师训练营第三周:系统架构

m

干货 | 全面解析“数字经济”

CECBC区块链专委会

数字经济 经济 经济建设

第三周作业

Geek_ac4080

云原生虚机应用托管-设计篇

8小时

区块链技术最重要价值所在

CECBC区块链专委会

区块链 数字经济 经济

~~寒露节记~~

wo是一棵草

JavaScript 语言通识 — 重学 JavaScript

三钻

JavaScript 前端进阶

【第三周】代码重构

云龙

「剑指offer」27道Mybatis面试题含解析

Java架构师迁哥

如何使用 dotTrace 来诊断 netcore 应用的性能问题

newbe36524

微服务 .net core netcore ASP.NET Core

架构师训练营第一期 - 第四周课后 - 作业一

架构师训练营第一期

如果朋友圈没有点赞功能,你还会发朋友圈吗

彭宏豪95

微信 产品 互联网 写作

【第三周】课后作业

云龙

第三节课后作业

happy

架构师训练营 第三周作业

haha

架构师训练营第一期

看动画学算法之:linkedList

程序那些事

数据结构和算法 看动画学算法 看动画学数据结构 算法和数据结构

各角色如何从DevOps中受益?

DevOps 产品经理 测试 开发 运维工程师

MySQL-技术专题-主从复制原理

李博@Alex

2N方定点算法

刀斧手何在

php 数据库 分布式 算法 后端

手把手教你锤面试官 03——Spring怎么那么简单

慵懒的土拨鼠

第四周

Geek_fabd84

Nginx 整合 FastDFS 实现文件服务器

哈喽沃德先生

nginx 文件系统 分布式文件存储 fastdfs 文件服务器

线上服务平均响应时间太长,怎么排查?

小Q

Java 程序员 测试 Jmeter 性能调优

基于区块链技术实现“资产通证化”

CECBC区块链专委会

资产证券化 流动性

开源的意义与价值

Braisdom

Java 开源 ORM

关于代码审查的一点体会

KJ Meng

敏捷开发 研发管理 代码审查 Code Review

私有云PAAS平台的思考

8小时

这可能是GitHub上最适合计算机专业学生看的编程教程

小Q

Java 学习 编程 面试 基础

usdt承兑商支付系统开发源码,区块链支付搭建

WX13823153201

第三周总结

Geek_ac4080

虚拟小组:利用事件溯源获得成功-InfoQ