CQRS之旅——旅程8(后记:经验教训)

399 阅读20分钟

旅程8:后记:经验教训

我们的地图有多好?我们走了多远?我们学到了什么?我们迷路了吗?

“这片土地可能对那些愿意冒险的人有益。”亨利.哈德逊

这一章总结了我们旅程中的发现。它强调了我们在这个过程中所学到的最重要的经验教训,提出了如果我们用新知识开始这段旅程,我们将以不同的方式做的一些事情,并指出了Contoso会议管理系统的一些未来道路。

你应该记住,这个总结反映的是我们的具体旅程,并非所有这些发现都适用于你自己的CQRS旅行。例如,我们的目标之一是探索如何在部署到Microsoft Azure并在利用云的可伸缩性和可靠性的应用程序中实现CQRS模式。对于我们的项目,这意味着使用消息传递来支持多个角色类型和实例之间的通信。您的项目可能不需要多个角色实例,或者没有部署到云中,因此可能不需要如此广泛地(或者根本不需要)使用消息传递。

我们希望这些发现能够被证明是有用的,特别是当您刚刚开始使用CQRS和事件源时。

我们学到了什么

本节描述了我们学到的主要经验教训。它们没有以任何特定的顺序呈现。

性能问题

在我们的旅程开始时,我们对CQRS模式的一个概念是,通过分离应用程序的读和写方面,我们可以优化每个方面的性能。CQRS社区的许多人都认同这一观点,例如:

“CQRS告诉我,我可以分别优化读和写,而且我不必总是手动的反规范化到平面表中。”

  • Kelly Sommers - CQRS顾问

这在我们的实践过程中得到了证实,当我们确实需要解决性能问题时,这种分离使我们受益匪浅。

在旅程的最后阶段,测试揭示了应用程序中的一组性能问题。当我们研究它们时,发现它们与我们实现CQRS模式的方式关系不大,而与我们使用基础设施的方式关系更大。发现这些问题的根源是困难的,由于应用程序中有如此多的活动部件,获得正确的跟踪和用于分析的正确数据是一项挑战。一旦我们确定了瓶颈,修复它们就相对容易了,这主要是因为CQRS模式使您能够清楚地分离系统的不同元素,比如读和写。尽管实现CQRS模式所导致的关注点分离会使识别问题变得更加困难,但是一旦您识别出一个问题,不仅更容易修复它,而且更容易防止它的重现。解耦的体系结构使得编写重现问题的单元测试更加简单。

我们在处理系统中的性能问题时遇到的挑战更多地是由于我们的系统是一个分布式的、基于消息的系统,而不是因为它实现了CQRS模式。

第7章,“添加弹性和优化性能”提供了关于我们处理系统中性能问题的方法的更多信息,并对我们想要进行但没有时间实现的额外更改提出了一些建议。

实现消息驱动系统远非易事

我们这个项目的基础设施是在旅程中根据需要开发它。我们没有预料(也没有预先警告)需要多少时间和精力来创建应用程序所需的健壮基础设施。我们在许多开发任务上花费的时间至少是最初计划的两倍,因为我们持续发现与基础设施相关的额外需求。特别是,我们从一开始就了解到拥有健壮的事件存储是至关重要的。我们从经验中得到的另一个关键思想是,消息总线上的所有I/O都应该是异步的。

Jana(软件架构师)发言:
虽然我们的事件存储还不是生产环境完备的,但是如果您决定实现自己的事件存储,那么当前的实现很好地指示了应该处理的问题类型。

尽管我们的应用程序并不大,但它向我们清楚地说明了end-to-end跟踪的重要性,以及帮助我们理解系统中所有消息流的工具的价值。第4章“扩展和增强订单和注册限界上下文”描述了测试在帮助我们理解系统方面的价值,并讨论了由我们的顾问之一Josh Elster创建的消息传递中间语言(messaging intermediate language, MIL)。

Gary(CQRS专家)发言:
如果我们有一个用于消息传递的标准符号,就可以帮助我们与领域专家和核心团队之外的人员沟通一些问题,这也会有所帮助。

总之,我们一路上遇到的许多问题都与CQRS模式没有特定的关系,而是与我们解决方案的分布式、消息驱动特性更相关。

Jana(软件架构师)发言:
我们发现,使用不同的Topic来传输由不同聚合发布的事件,通过这样来划分服务总线有助于实现可伸缩性。有关更多信息,请参见第7章“添加弹性和优化性能”。另外,请参阅这些博客文章:“Microsoft Azure Storage Abstractions and their Scalability Targets”和“Best Practices for Performance Improvements Using Service Bus Brokered Messaging”。

使用云带来的挑战

虽然云提供了很多好处,比如可靠的、可伸缩的、现成的服务,您只需单击几下鼠标就可以使用这些服务,但是云环境也带来了一些挑战:

  • 您可能无法在任何您想要的地方使用事务,因为云的分布式特性使得ACID(原子性、一致性、隔离性、持久性)事务在许多场景中不切实际。因此,您需要了解如何使用最终的一致性。例如,请参见第5章“准备发布V1版本”,以及第7章“添加弹性和优化性能”中减少UI延迟的部分章节。
  • 您可能需要重新检查关于如何将应用程序组织到不同层的假设。例如,参见第7章“添加弹性和优化性能”中关于进程内同步命令的讨论。
  • 您不仅必须考虑浏览器或内部环境与云之间的延迟,还必须考虑在云中运行的系统的不同部分之间的延迟。
  • 您必须考虑到瞬时错误,并了解不同的云服务可能如何实现节流。如果您的应用程序使用几个可能被节流的云服务,那么您必须协调应用程序如何处理不同服务在不同时间进行节流。

Markus(软件开发人员)发言:
我们发现,代码中只有一个总线抽象,这掩盖了这样一个事实,即有些消息是在本地进程内处理的,有些消息是在不同的角色实例中处理的。要查看这是如何实现的,请查看ICommandBus接口以及CommandBusSynchronousCommandBusDecorator类。第七章“增加弹性和优化性能”包括了对SynchronousCommandBusDecorator类的讨论。

备注:我们的Visual Studio解决方案中的多个构建配置是为部分解决这个问题而设计的,也帮助人们下载和使用代码来快速入门。

CQRS是不同的

在我们的旅程开始时,有人警告我们,尽管CQRS模式看起来很简单,但实际上它要求您在考虑项目的许多方面时进行重大的转变。我们在旅途中的经历再次证明了这一点。您必须准备抛弃许多假设和预先设想的想法,在开始充分理解从模式中获得的好处之前,您可能需要先在几个限界上下文中实现CQRS模式。

这方面的一个例子是最终一致性的概念。如果您来自关系数据库背景,并且已经习惯了事务的ACID属性,那么在系统的所有级别上接受最终的一致性并理解其含义是一个很大的步骤。第5章“准备发布V1版本”和第7章“添加弹性和优化性能”都讨论了系统不同领域的最终一致性。

除了与您可能熟悉的不同之外,还没有一种正确的方法来实现CQRS模式。由于我们对模式和方法的不熟悉,我们在功能块上做了更多错误的开始,并且对所需的时间估计很差。随着我们对这种方法越来越熟悉,我们希望能够更快地确定如何在特定情况下实现模式,并提高我们估算的准确性。

Markus(软件开发人员)发言:
CQRS模式在概念上很简单,而细节才决定成败。

我们花了一些时间来理解CQRS方法及其含义的另一种情况是在限界上下文之间的集成期间。第5章“准备发布V1版本”详细讨论了团队如何处理会议管理与订单和注册上下文之间的集成问题。这部分旅程揭示了一些额外的复杂性,当您使用事件作为集成机制时,这些复杂性与限界上下文之间的耦合级别有关。我们的假设是,事件应该只包含关于聚合或限界上下文中变化的信息,但事实证明这种假设是没有帮助的,事件可以包含对一个或多个订阅者有用的附加信息,并有助于减少订阅者必须执行的工作量。

CQRS模式为如何划分系统引入了额外的思考。您不仅需要考虑如何将系统划分为层,还需要考虑如何将系统划分为限界上下文,其中一些上下文将包含CQRS模式的实现。在旅程的最后阶段,我们修改了关于层的一些假设,将一些处理从最初完成处理的工作者角色引入到web角色中。在第7章“增加弹性和优化性能”中讨论了如何在进程中发送和处理命令。应该根据领域模型将系统划分为限界上下文,每个限界上下文都有自己的领域模型和通用语言。一旦确定了限界上下文,就可以确定在哪些限界上下文中实现CQRS模式。这将影响如何以及在何处需要实现这些隔离限界上下文之间的集成。第二章“[分解领域]”介绍了我们对Contoso会议管理系统的所作的决策。

Gary(CQRS专家)发言:
单个进程(部署中的角色实例)可以承载多个限界上下文。在此场景中,您不需要为限界上下文使用服务总线来彼此通信。

实现CQRS模式比实现传统的(创建、读取、更新、删除)CRUD风格的系统更复杂。对于这个项目,第一次学习CQRS和创建分布式、异步消息传递基础设施的开销也很大。我们在此过程中的经验清楚地向我们证实了为什么CQRS模式不是顶级体系结构。您必须确保实现基于CQRS的限界上下文相关的成本是值得的,通常,您将在高竞争、高协作的领域中看到CQRS模式的好处。

Gary(CQRS专家)发言:
分析业务需求、构建有用的模型、维护模型、用代码表示它以及使用CQRS模式实现它都需要时间和金钱。如果这是您第一次实现CQRS模式,那么您还需要对基础设施元素(如消息总线和事件存储)进行开销投资。

事件源和事务日志

对于事件源和事务日志是否等同于同一件事,我们进行了一些讨论:它们都创建了所发生事情的记录,并且都允许您通过重播历史数据来重新创建系统的状态。结论是,事件的显著特征是除了记录所发生的事实之外,还能捕获意图。有关我们所说的意图的更多细节,请参阅参考指南中的第4章“深入CQRS和ES”。

涉及到领域专家的

实现CQRS模式鼓励领域专家的参与。该模式使您能够将写端上的领域和读端上的报告需求分离出来,并将它们与基础设施关注点分离开来。这种分离使领域专家更容易参与系统中他的专业知识最有价值的方面。使用领域驱动的设计概念,如限界上下文和通用语言,也有助于集中团队的注意力,并促进与领域专家的清晰沟通。

我们的验收测试证明是一种有效的方法,可以让领域专家参与进来并获取他的知识。第4章“扩展和增强订单和注册有界上下文”详细描述了这种测试方法。

Jana(软件架构师)发言:
作为一个副作用,这些验收测试还有助于我们处理伪生产版本的快速发布,因为它们使我们能够在UI级别运行一组完整的测试,以验证除单元测试和集成测试之外的系统行为。

除了帮助团队定义系统的功能需求之外,领域专家还应该参与评估一致性、可用性、持久性和成本之间的权衡。例如,领域专家应该帮助确定什么时候手动流程是可接受的,以及在系统的不同区域中需要什么级别的一致性。

Gary(CQRS专家)发言:
开发人员倾向于将所有内容都锁定到事务中,以确保完全的一致性,但有时并不值得这样做。

何时使用CQRS

现在我们已经完成了我们的旅程,我们现在可以建议您应该评估的一些标准,以确定是否应该考虑在应用程序中的一个或多个限界上下文中实现CQRS模式。您能正面回答的问题越多,就越有可能将CQRS模式应用到给定的限界上下文中,从而使您的解决方案受益:

  • 限界上下文是否实现了业务功能的一个领域,这个领域是您的市场中的一个关键区别点?
  • 限界上下文本质上是否与可能在运行时具有高争用级别的元素协作?换句话说,多个用户是否会为了访问相同的资源而竞争?
  • 限界上下文是否可能经历不断变化的业务规则?
  • 您是否已经具备了健壮的、可伸缩的消息传递和持久性基础设施?
  • 可伸缩性是这个限界上下文面临的挑战之一吗?
  • 限界上下文中的业务逻辑复杂吗?
  • 您清楚CQRS模式将给这个限界上下文带来的好处吗?

Gary(CQRS专家)发言:
这些都是经验法则,不是硬性规定。

如果我们重新开始,会有什么不同?

本节是我们反思我们的旅程的结果,以及确定了一些我们想以不同方式去做的事情和一些我们希望追求的其他机会。如果在我们掌握了现在我们所了解的CQRS和ES知识之后重来一次的话。

从消息传递和持久性的坚实基础设施开始

我们将从一个可靠的消息传递和持久性基础设施开始。我们采取的方法是从简单的先开始,并根据需要建立基础设施,这意味着我们在旅程中积累了技术债务。我们还发现,采用这种方法意味着在某些情况下,我们对基础设施的选择影响了我们实现领域的方式。

Jana(软件架构师)发言:
从旅行的角度来看,如果我们从一个坚实的基础设施开始,我们将有时间处理领域中一些更复杂的部分,比如等待列表(Wating-list)。

从一个可靠的基础设施开始也能使我们更早地开始性能测试。我们还将进一步研究其他人如何在基于CQRS的系统上进行性能测试,并在其他系统上寻找性能基准,比如Jonathan Oliver的EventStore

我们采取这种方法的原因之一是我们从顾问那里得到的建议:“不要担心基础设施。”

更多地利用基础设施的能力

从一个坚实的基础设施开始也将允许我们更多地利用基础设施的能力。例如,当我们发布一个事件时,我们使用消息发起者的ID作为会话ID在Azure服务总线传递,但从系统处理事件的部分来看,这并不总是最好的使用会话ID的方式。

作为其中的一部分,我们还将研究基础设施如何支持其他最终一致性的特殊情况,如时间一致性、单调一致性、“read my writes”和自我一致性。

我们想探讨的另一个想法是使用基础设施来支持版本之间的迁移。我们可以考虑使用基于消息的流程或实时通信流程来协调把新版本上线,而不是针对每个版本以特定的方式处理迁移。

采用更系统的方法来实现过程管理器

我们在旅程的早期就开始实现我们的过程管理器,并且仍然在强化它,并确保它的行为在旅程的最后阶段是幂等的。同样,从为流程管理人员提供一些坚实的基础设施支持开始,使他们更有弹性,这将对我们有所帮助。但是,如果我们要重新开始,我们也会等到过程的后期再实现流程管理器,而不是直接开始。

在旅程的第一阶段,我们开始实现RegistrationProcessManager类。第3章“订单和注册限界上下文”描述了初始实现。在旅程的每个后续阶段,我们都对流程管理器进行了更改。

以不同的方式划分应用程序

在项目开始时,我们会更仔细地考虑系统的分层。我们发现我们的划分的方式是把应用程序分到web角色和工作者角色中,这在第4章“扩展和增强订单和注册限界上下文“中进行了描述。但这不是最优的,在旅程的最后阶段,在第7章“增加弹性和优化性能”中,作为性能优化的一部分,我们对架构做了一些重大改变。

例如,在旅程的最后阶段,作为重新组织的一部分,我们在web应用程序中引入了同步命令处理,同时引入了已存在的异步命令处理。

以不同的方式组织开发团队

我们学习CQRS模式的方法是迭代开发、回顾、讨论,然后重构。但是,我们可以通过让几个开发人员在相同的特性上独立工作,然后比较结果,从而学到更多。这可能揭示了更广泛的解决方案和方法。

评估领域域和限界上下文是否适合使用CQRS模式

我们希望从一组更清晰的启发开始(如本章前面概述的启发),以确定特定的限界上下文是否会受益于CQRS模式。如果我们关注领域中更复杂的地方,比如等待列表(Wating-list),而不是订单、注册和支付的限界上下文,我们可能会学到更多。

性能计划

我们将在旅程的早期处理性能问题。我们尤其要:

  • 提前设定明确的性能目标。
  • 在过程中更早地运行性能测试。
  • 使用更大更实际的负载。

我们没有做任何性能测试,直到旅程的最后阶段。有关我们发现的问题以及如何解决这些问题的详细讨论,请参见第7章“添加弹性和优化性能”。

在旅程的最后阶段,我们在服务总线上引入了一些分区,以提高事件的吞吐量。此分区是基于事件的发布者完成的,因此由同一个聚合类型发布的事件将发布到同一个Topic。我们希望把当前使用一个Topic的扩展到使用多个Topic,可能会基于消息中OrderID的hash进行分区(这种方法通常称为分片)。这将为应用程序提供更大的扩展。

以不同的方式思考UI

我们认为UI与读写模型交互的方式,以及它处理最终一致性的方式都很好,并且满足了业务需求。特别是,UI检查预订是否可能成功并相应地修改其行为的方式,以及UI允许用户在等待更新读模型时继续输入数据的方式。有关当前解决方案如何工作的更多细节,请参见第7章“添加弹性和优化性能”中的“优化UI”一节。

我们想研究除非绝对需要,其他避免在UI中等待的方法,比如使用浏览器推送技术。在某些地方,当前系统中的UI仍然需要等待针对读模型的异步更新。

探索事件源的一些额外好处

我们发现在旅程的第三阶段,第5章“准备发布V1版本”中,修改订单和注册限界上下文来使用事件有助于简化这个限界上下文的实现,一部分是因为它已经使用了大量的事件。

在当前的旅程中,我们没有机会进一步探索灵活性的承诺,以及从事件源中挖掘过去事件以获得新的业务见解的能力。但是,我们确实确保系统保存了所有事件的副本(不仅仅是那些重建聚合状态所需的副本)和命令,以便在将来启用这些类型的场景。

Gary(CQRS专家)发言:
同样有趣的是,通过事件源或其他技术(如数据库事务日志或SQL Server的StreamInsight特性)来挖掘过去的事件流以获取新的业务洞察是否更容易实现?

探索关于限界上下文集成的相关问题

在我们的V3版本中,所有限界上下文都由同一个核心开发团队实现。我们希望研究在实践中,由不同开发团队实现的限界上下文与现有系统集成起来有多容易。

这是您为学习经验做出贡献的一个很好的机会:继续实现另一个限界上下文(请参阅产品backlog中的优秀用户故事),将它集成到Contoso会议管理系统中,并在旅程的另一章中描述您的经验。