写点什么

从单体到事件驱动架构:找到新架构中的接缝

2020 年 10 月 18 日

从单体到事件驱动架构:找到新架构中的接缝

本文要点


  • 单体应用程序并不总是单层的;分布式单体看起来跟微服务架构很像,但它们的行为却是单体行为;

  • 所谓的事件驱动架构,就是将事件作为变化的单元;

  • 在 CQRS 中,命令(Command)和查询(Query)之间的区别比分离(Separation)更为重要;

  • 事件溯源将事件与状态分离,为系统的转换状态提供必要的抽象;

  • 当遗留元素被移除后将出现目标状态架构,这就是过渡性架构中的接缝(Seam)。


为什么要进行迁移?


除了单层的单体架构之外,三层架构也是一种常见的架构模式,一般由表示层、业务层和数据层组成。但是,这些层之间的复杂性几乎是不成比例的。在某些情况下,我们会在表示层或数据层的存储过程中发现业务逻辑。而在其他情况下,应用程序层按照系统功能被分成很多个服务。有些人甚至称之为微服务,但如果分离不得当,它只是一个分布式的单体。



图 1:一些架构模式的比较


当然,这些并非都是糟糕的架构风格,但每一种都存在风险。当做出变更的风险足够大或者无法控制时,说明该向新架构迁移了。搞清楚进行架构迁移的原因与在迁移之前进行可行性验证同样重要。


事件是 EDA 的核心


事件驱动架构(EDA)并不是新鲜事物,但我们可能会发现,我们在实现时所采用的实践经常与核心原则相背离,导致我们忽略了 EDA 实际上是关于将事件作为系统变更单元的实事。我们在 EDA 中所采用的实践(如消息传递和异步消息)可能会引入这种噪音。你也可以在一个单体系统中构建一个事件驱动的系统,就像在分布式系统中所做的那样,只要看看大多数操作系统内核(如 Windows 或 Linux)就知道了。


但事件驱动并不像看上去的那么简单。我们最终得到的不只是一个事件(比如 PantsStainedEvent),还需要执行动作(如 SpillCoffee)。一个事件必须是由某个东西引起的,因为它是一个自然的历史元素,并且是不可变的。在系统中,事件的创建应该由用户或时间等参与者驱动,通常是通过调用 API 或队列消息等通信方式。


EDA+CQRS


CQRS 模式强调命令和查询的分离,但同时也意识到请求系统状态和请求系统修改其状态之间的区别比分离本身更重要。实际上,你会发现 CQRS 有多种变体,从逻辑层面到物理层面都有。


将 EDA 与 CQRS 模式相结合是系统设计的一个自然增量,因为命令是事件的生成器。结合使用 CQRS 和命令,为在架构过渡时迁移数据提供了一个接缝,在迁移结束后,可以移除接缝。这个接缝的概念将在“数据迁移接缝”小节详细介绍。


EDA+事件溯源


事件溯源是指将事件作为事实的来源。原则上,一个事件要在系统中具有持久性,才能被进一步处理。就像一个作家的故事在被写成书之前根本不算是故事一样,事件在具备足够的持久性(比如被持久化到数据存储中)之前不应该被投射、重放、发布或以其他方式处理。其他将事件视为二等公民的设计并不是事件溯源,而只是一个事件记录系统。


将 EDA 与事件溯源模式相结合是系统设计的另一个自然增量,因为同时遵循了 EDA 原则(事件是变化的单元)和事件溯源原则(事件应该先被存储再进行处理)。


事件类型


回想一下我们之前对 CRUD 的了解,我们可以将它们联系起来,因为我们构建的所有应用程序都围绕着创建、读取、更新或删除这些核心操作。我们甚至发现 RESTful 接口也有类似之处,因为 HTTP 方法被映射到这些操作。CRUD 由来已久,但我们也可以在现代架构风格和工程模式(如 EDA、CQRS 和事件溯源)中看到它的身影。


如果我们仔细分析 CRUD 和 CQRS 之间的关系,就会发现,当命令与查询分离时,CRUD 也是分布式的。C、U 和 D 被分到“命令”组,而 R 被分到“查询”组。这应该很直观。


如果我们分析一下 CRUD 与事件以及 CQRS 之间的关系时,看到了类似的关联。如果命令生成事件,那么 CUD 和事件之间也存在关联。我们假设所有的事件也必须是创建类型、更新类型或删除类型。如果以这种方式对事件进行分类,那么事件就会有一个属性,例如动作或类型,值可以是“create”、“update”和“delete”。



我们将把这三种事件类型与起始及迭代的概念结合起来。


起始与迭代


任何系统中的事件都是不可变的历史事件。你可以用一个表示时间的直线图对它们进行可视化,任何一个事件都对应直线上的一个位置。直线图中的事件可以分为两组:第一组称为起始(Inception),只包含一个事件,第二组称为迭代(Iteration),包含第一个事件之后的所有其他事件。它们之所以被称为起始和迭代,是因为它们表示实体正在发生的事情。


你以为就这样了吗?其实还有一种分组需要考虑。因为所有的事件都是 CUD,所以当我们将起始和迭代与 CUD 联系起来时,我们发现 C 与起始相关联,而 UD 与迭代相关联。


有时候,为了找到架构中的接缝,需要深入到架构中。在本例中,我们将简单的 CRUD 概念映射到更多的概念,如起始和迭代。



新系统和过渡系统


在新系统中,事件时间轴非常简单。添加或创建新实体的惟一方法是使用创建类型的事件(event.type = “create”)。如果系统处于过渡状态,那么引入一个新的实体到系统中就不仅仅是“event.type = create”。你还可以有迁移事件类型,比如“event.type = migrate”,作为在新系统中创建新实体的另一种方式。


为什么这个很重要?系统在重放事件时,需要有严格或确定性的规则来规定如何进行重放。例如,你可能有一条规则是“第一个事件必须 event.type = create”,这个完全没有问题。这个规则不需要考虑快照,问题的关键在于,快照也遵循相同的规则。如果你的系统处于过渡状态,那么可以在这个规则中包含“event.type = migrate”。


之所以加入“event.type = migrate”,是因为它也是一种接缝。当你转到新的架构时,有两种方法可以用来创建实体。在转换完成后,你可以将其中一种删除,只留下一个。实际上,你可以删除所有与“event.type = migrate”相关的代码。


本文的最后两个小节将对其进行更深入的解释。对于数据迁移,我们关心的是在新系统首次上线时如何获得初始状态,而在进行数据同步时,遗留系统和新系统都需要上线,并且双方都发生了状态变更事件。


数据迁移接缝


迁移数据就是将遗留系统的状态加载到新系统。有时候,这是一次性事件,不过你也可以采用时间段(比如每天),因为迁移是幂等的,并且没有重复状态。那么,这到底是什么样子的?让我们从逻辑层面来看一下。



从上图可以看到,一种解决方案是引入一个名为 MigrateDataCommand 的命令。在调用这个命令时,它将进入遗留系统,获取状态,并创建“migrate”事件。需要注意的是,从业务规则的角度来,“migrate”事件与“create”事件实际上是不一样的。我们很有可能将一组业务规则与“create”事件关联起来,比如验证(例如一个有效的国家代码)和生成(例如创建一个新的 ID),但很少需要将业务规则附加到“migrate”事件中。迁移所产生的事件是既存状态的移动。状态的形成是因为它确实已经通过了业务规则,尽管是在遗留系统中。“migrate”事件的目的是将状态传递到新系统,类似于快照。


这是一个接缝,在完成过渡状态后,你就可以删除 MigrateDataCommand,不再需要调用它了,并停止生成“migrate”事件。你不会有事件处理程序,如果有,也会将其删除。


数据同步接缝


如果幸运的话,你可以启动新系统,并关闭旧系统。但是,我们大多数人都没有这么幸运。通常,在将遗留系统完全替换成新系统之前,遗留系统中仍然保留着业务价值。这意味着,当两个系统都在运行时,两边都发生了事件,更重要的是,有些事件可能针对的是同一组实体。


那么该如何同步这两个系统呢?当然是使用事件。假设你对遗留系统有一定的控制权,我的意思是,你可以改变它的代码,重新部署,访问数据,或者做一些其他的事情。


让我们从逻辑层面看一下。



可以看到,我们必须对遗留系统进行修改,把它变成事件感知的,而非事件驱动的。我们对它做了更改,这样在它将状态持久化到数据存储之后,也会生成一个事件。这个事件带有“legacy”的属性,这从它的命名就可以看出来。在新系统中,我们有针对这些事件的处理程序。处理程序本身并不知道也不关心事件是哪个系统生成的,而且从某种程度上说,它就不应该太关心它是来自遗留系统。这些处理程序将使用与其他事件相同的规则重放这些事件。


当然,同步是双向的。让我们来看看它是什么样的。



在进行反向同步时,当命令生成新事件,旧事件处理程序将读取新事件。它的唯一职责是将新状态写入遗留系统。


这是一个接缝,因为在过渡状态结束后,一旦遗留系统下线,不再产生遗留事件,就可以删除遗留处理程序。


串在一起


那么,在实际当中,一个转向事件驱动的单体架构是什么样的呢?哪些是隔离遗留元素和新元素的接缝代码?我们有一个名为 Notez 的笔记系统,顾名思义,你可以用它创建和管理笔记。我们真的无法想象这些接缝的位置,除非我们更深入地了解新系统是什么样子的。


Notez 系统



我们假设遗留系统有一些缺点,新系统将命令和查询分成不同的服务,以便最大限度地实现 CQRS 模式。具体的实现可能会有所不同。命令服务的任务是接收命令,如果有效,就创建事件。这些事件最终会在两个地方存在。首先,事件被保存在事件存储库中,在存储成功后,又被发布到事件流中。查询服务侦听事件,并将这些事件委托给事件处理程序。事件处理程序的主要任务是基于该事件创建投射,查询服务通过利用投影来提供查询。这是一个最终一致的系统。


注意:要构建这个系统,成本很高,但它为我们提供了一个很好的系统演化视角。在进行系统演化时,你需要决定是要预先还是事后支付成本。这个系统是预先支付变更成本,从而使未来的变更成本最小化。


命令服务剖析


命令服务使用分层设计来分离职责。职责分离是必要的,这样它们就可以以不同的速度进行演化。例如,你可能会在未来添加另一个 API。由于集成和冗余方面的成本,分层的代价很高,但它们的好处是让我们更容易发现架构中的接缝。



尽管每个层都很重要,但在过渡阶段,应用层无疑是最重要的一层。我们主要会在应用层中找到迁移和同步命令。我们假设遗留系统包含了一些需要迁移的笔记,新系统能够识别它们,并且这两个系统都在线。


在过渡架构中,我们有两个方面需要注意:迁移和同步,迁移是将已有的数据传给新系统,同步是让遗留系统和新系统都接收到新数据。


迁移



在进行迁移时,我们将创建一个新的命令 MigrateNotesCommand。这个命令具有遗留系统的信息,是两个系统之间的接缝,为信息的流通提供了桥梁。最关键的地方可能是生成 NoteMigratedEvent 事件。对于新系统来说,它只是另一个需要处理的初始事件(例如,要创建一个投射)。


图中没有显示的是已发布事件发生了什么。迁移事件的处理方式与其他事件一样。这实际上是位于命令-业务-应用层的接缝的一个隐藏点。


同步


从新系统同步



当新系统的状态发生变化,比如接收了 CreateNoteCommand 这样的命令,就会发布一个 NoteCreatedEvent 事件。遗留系统也需要接收这个新状态。发布事件后,命令服务可以使用事件处理程序侦听该事件。当事件处理程序接收到 NoteCreatedEvent 事件时,它将调用同步 SyncNoteCommand。最后,SyncNoteCommand 将更新遗留系统。在上图中,更新操作直接连接数据存储,不过也可以调用 Web 服务或其他接口,具体视情况而定。


从遗留系统同步



当遗留系统的状态发生改变时,新系统会收到一个通知事件。事件由遗留系统新的部分创建,可能是一个命令或其他类似的模式,在成功持久化实体之后立即被调用。它发送给消息代理的消息是一个事件,本例中为 LegacyNoteCreatedEvent。对于命令服务来说,这个事件就是一个信号,让它存储一个类似的事件。这个时候要么按原样存储遗留事件,要么删除“遗留”名称对象。通常最好是删除名称对象,因为这样可以避免处理程序出现混乱。换句话说,就是最好不要有监听遗留事件的处理程序,应该依赖命令服务来生成类似的事件。你也希望避免对可能还没有被持久化到存储中的事件做出响应。


接缝在哪里?


有趣的是,接缝就在我们面前。在迁移和同步场景中,用蓝色元素描述的就是接缝。你可以在架构中添加这些元素来启用这些场景,但在不再需要它们时快速移除它们。


作者简介:


Jayson Go(JGo)是一家专注于投资技术的金融科技公司的软件开发和工程负责人。他在软件系统方面有超过 20 年的经验,目前担任平台架构师的角色。他的兴趣领域包括开发代码、描述解决方案、指导他人及需求设计。如果你遇到他,可以跟他讨论有关领域驱动设计、事件溯源、防御设计、面向对象编程中的函数式编程、分布式系统和软件架构治理等方面的问题。


原文链接


From Monolith to Event-Driven: Finding Seams in Your Future Architecture


2020 年 10 月 18 日 09:001059

评论

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

week002 作业

徐培

架构师训练营第三周总结

人世间

极客大学架构师训练营

第三周课程总结

考尔菲德

手写单例和组合模式运用实例

林昱榕

单例模式 极客大学架构师训练营 组合模式

架构师训练营 第三周-作业

无心水

单例模式 极客大学架构师训练营 组合模式 23种设计模式

架构师训练营:第三周作业

zcj

极客大学架构师训练营

第三周作业

重新来过

设计模式—week3总结

小叶

极客大学架构师训练营

<<架构师训练营>>第三周作业二

0x12FD16B

第三周作业

魔曦

架构是训练营

架构师训练营第三周作业

olderwei

极客大学架构师训练营

第三周-学习总结

molly

极客大学架构师训练营

Week002 总结

徐培

Week3

架构师训练营第三周作业

Melo

【第三周】架构师训练营作业

星星

架构师 0 期 | 组合模式使用

刁架构

设计模式 极客大学架构师训练营 组合模式

第三周作业

数字

架构师训练营第三周学习总结

fenix

单例模式小结

L001

<<架构师训练营>> 第三周作业一

0x12FD16B

架构师训练营第三周作业及总结

强哥

极客大学架构师训练营

第三周感想

数字

架构师训练营 -week3- 学习总结

暖丶冬

架构师训练营第三周作业

战峰

架构师训练营 - 第三周学习总结

hellohuan

极客大学架构师训练营

Week 03 学习总结

总结

chenzt

架构师第三课总结

Dennis

第三周学习总结----几种设计模式的练习

Geek_165f3d

第三周总结

uangguan

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

从单体到事件驱动架构:找到新架构中的接缝-InfoQ