作者:Karen Casella, Phillipa Avery, Robert Reta, Joseph Breuer
翻译:王鸿蒙
审校:包研
在上一篇文章中,我们介绍了Netflix下载项目以及基于事件回溯模式的使用案例。(在LiveVideoStack公众号内回复『Netflix』获取)在本文中,我们将概述一般事件回溯模式以及如何将其应用于某些关键使用案例。
“灵活”困境
当我们一开始设计下载许可服务的时候,内容许可限制其实已经被定义好了。我们必须想办法适应这种情况。 那么,当需求已经确定,如何开始设计并实现这项服务呢? 甚至如果考虑到这项服务要在当天上午6点的全球新闻发布会后即刻生效,向数百万用户开放,并持续数月呢? 如果系统可以灵活地改变,就很简单了, 对吧?
熟悉关系数据库的人都知道,如果考虑到底层表结构,类似“灵活”和“容易改变”这些词其实并不十分正确。当然,有许多方法可以改变表结构,但都需要有很深入的SQL知识储备才能理解并使用,还需要直接与数据库交互。 另外,一旦数据发生了变化,就可能丢掉引起变化的关键上下文信息,以及数据的前一个状态的信息。
众所周知,面向文档的NoSQL数据库,能够提供这种灵活的改变,我们采用这种方法来提供灵活和可扩展的解决方案。 文档模型为我们提供了数据模型所需的灵活性,但是无法让我们追溯导致数据突变的原因。鉴于数据的长效性,我们希望能够及时对状态改变进行调试。
事件回溯
事件回溯是最近重新构建的架构模式,是现代分布式微服务生态系统的重要组成部分。 Martin Fowler描述该基本模式如下:“事件回溯机制的基本思想是确保应用状态的每一个变化都被捕获到一个事件对象中,并且这些事件对象存储在其申请的序列中,与应用状态本身有相同的生命周期“。
关于事件回溯模式有许多优秀的综述。其中我们挑选了两篇较有代表性的:
- “事件回溯” (https://martinfowler.com/eaaDev/EventSourcing.html),作者:Martin Fowler;
- “模式:事件回溯” (http://microservices.io/patterns/data/event-sourcing.html),作者:Chris Richardson
简而言之,事件回溯是一种架构模式,能够为数据模型提供完整的交互历史记录。 我们不维护数据模型本身,而是维护导致数据更改的事件。 这些事件按顺序播放,从而构建完整数据域的聚合视图。 在任意时间点重播事件的能力也是一种出色的调试工具,使我们可以轻松解释为什么会员帐户处于特定状态时,我们能够迅速地测试系统变量。
模式
下图提供了我们如何应用Netflix系统事件回溯模式,并对每个组件进行了一般性说明。该模式主要负责执行下载业务规则。
事件回溯模式基于三个不同的服务层:命令,事件和聚合。
- 命令表示客户端请求更改聚合的状态。 命令处理机使用命令来确定如何创建满足该命令所需的事件列表。
- 事件是指聚合状态改变后的“不变”表示。例如,某行为改变了状态。所以, 事件总是用过去式来表示。
- 聚合是域模型当前状态的聚合表示。 聚合包含一系列事件并决定如何根据所请求的业务逻辑目的来表示聚合数据。
如图所示,有多个参与者参与实现该模式。
- REST服务是接受来自客户端的请求并将其传递给聚合服务的应用层。
- 聚合服务处理客户端请求。聚合服务首先查询现有的聚合,如果不存在,则创建一个空聚合。然后聚集服务生成与请求相关联的命令,并将命令与该聚合一起传递给命令处理机。
- 命令处理机接收聚合和命令,并基于状态转换有效性检查来评估当前状态下的命令是否可应用于聚合。如果状态转换有效,那么命令处理机会创建一个事件并将事件和聚合传递给事件处理机。
- 事件处理机将事件应用于聚合,产生新的聚合状态,并将事件列表传递给存储服务。
- 存储服务通过将新创建的事件应用于聚合来管理状态。然后将这些事件保存到事件存储中,从而使聚合的新状态在我们的系统中可用。
- 事件存储是事件读/写功能与后台数据库的抽象交互。
Netflix下载使用案例
当某个会员选择一个视频开始下载时,许可生命周期便开始了:
Netflix客户端应用程序首先请求许可证。获得许可后,Netflix客户端会下载内容,会员可以播放其新下载的内容。根据会员的行为,许可证的状态可以在整个生命周期中改变。会员可以开始、暂停、恢复或停止查看内容,也可以删除下载内容。每种操作都可能导致许可证的状态更改。许可证被创建后,可能会被更新数次,最终由会员显式地,或者基于业务规则隐式地被释放(删除)。
整个生命周期中涉及大量的业务逻辑。维护许可状态是基于事件回溯的许可记帐服务的工作,该服务追踪许可的完整交互历史、会员下载的内容和设备数据模型。这样可以按顺序回放事件,建立完整数据对象的聚合视图。
Netflix聚合
Netflix客户端应用程序会创建几种不同类型的请求,并将其转换为命令、事件和聚合。 为了支持执行许可的业务需求,我们有三个相互关联的聚合:许可,已下载的视频的标题和设备。 它们每个都有自己的服务,处理机和存储库。 以下是对上面介绍的各个概念的描述,它们都适用于“下载”特点的关键用例。
许可获取用例
以下是会员首次获得某条内容许可的一个简单使用案例。
在最初的许可请求中,客户端向许可获取端点(Acquire License Endpoint)发送一个请求,请求包含会员身份以及请求下载视频的标题,并传递给许可服务。
许可服务通过查询现有聚合数据并应用适当的业务规则,来决定是否允许请求的操作。 由于这是会员对该内容的首个请求,并假设满足设备和视频工作室业务规则,则许可服务会创建一个新的空白许可聚合(License Aggregate)和一个许可创建命令(Create License Command),并传递给许可命令处理机(License Command Handler):
许可命令处理机将许可创建命令应用于许可聚合并创建许可生成事件(License Created Event):
许可命令处理机将带有许可生成事件的空白许可聚合传递给事件处理机(Event Handler),该处理机将创建一个新的许可聚合:
许可库(License Repository)则将许可生成事件持久化到事件存储(Event Store)中:
最后,许可库将新的许可聚合返回给许可服务,该许可服务将聚合信息打包到请求响应并返给客户端。
许可更新使用案例
在许可到期之前,设备可能会请求对现有许可进行延期,即许可更新。 更新许可与获取许可的流程类似,主要区别在于当前许可聚合会连同一个许可更新命令(Renew License Command)一起传递给许可命令处理机。 在许可命令处理机生成适当的事件之后,许可事件处理机将许可更新事件(License Renewed Event)应用到许可聚合,如下图所示。 请注意,新的许可聚合的到期日期是从当前日期开始计算30天。 这30天代表目前许可续订业务规则生效。 如果想改变这个限制,我们得对事件处理机进行一个简单的配置更改。
下载限制拒绝使用案例
每次设备从许可服务请求新的许可或更新许可时,下载服务(Downloaded Service)检索该会员的当前聚合并评估业务规则验证结果。例如,其中一个验证结果要求某些内容每年只能下载两次。当设备发出许可请求时,许可服务会检查会员本年度是否已经下载了内容。可以通过检索本年度所有的许可聚合并通过过滤内容ID来获取此信息。这显然涉及大量的处理,所以我们决定对数据“去规范化”,为下载内容创建一个单独的聚合,并为会员信息和内容ID建立索引。当然,这需要新的下载事件,服务,聚合和存储库。
我们收到内容的后续事件时,可以检查以前所有关于该内容的下载次数。根据下面的顺序图,如果下载服务发现该会员已经超过了下载次数,它可以拒绝该请求。
结论和展望
对于我们的使用案例来说,事件回溯模式在实现灵活和健壮的系统时非常有用。 然而,也并不全是“阳光”和“玫瑰”,我们肯定也犯了一些错误,在很多地方需要改善(后续的文章将会讲述这些细节)。 总的来说,灵活的架构为我们提供了快速创新和对不断变化的需求作出反应的手段,能够在长时间尺度上调试包含不断变化数据状态的事件,并在相对较短的时间里,为全球数百万用户提供全新的服务。
请再多讲一点!
在本系列的下一篇文章中,我们将深入介绍实现细节,包括使用数据版本控制和快照以提供灵活性和可扩展性。 接下来,我们将分享我们在实施事件回溯方面的经验,以及我们在测试,可扩展性和优化等方面的一些教训(包括我们所犯的错误),并介绍我们计划未来改进和扩展的一些想法。
我们的团队最近在QCon New York展出了这个主题,您可以在【阅读原文】下载幻灯片并观看视频。