每个开发人员都应该知道的基本概念
我们关于微服务简介的第一篇文章谈到了服务的粒度和确保松散耦合的必要性。据说服务应该是自治的,完全拥有它们的依赖关系,并尽量减少同步通信。今天,我们将探讨松散耦合的含义,并探索一个在微服务社区中似乎越来越受欢迎的交易技巧——事件驱动架构。
一个简单的定义
事件驱动架构(EDA)是一种促进事件生产和消费的软件架构范式。
事件代表具有重大意义的行动。通常,事件对应于某个实体的创建或状态更改。例如,在电子商务应用程序中提出订单就是一个事件。由于较早的订单而发送产品也是一个事件。客户为收到的产品提交评论 - 你猜对了 - 一个事件。
从未发生的事件
事件的特殊之处在于它们没有明确地传达给可能关心它们的特定方。事件“刚刚发生”。至关重要的是,无论某些方面是否对它们感兴趣,它们都会发生。这听起来像是经常被引用的哲学思想实验:“如果一棵树倒在森林里,而周围没有人听到它,它会发出声音吗?” . 但这正是事件如此强大的原因——事件转化为事件发生的自包含记录这一事实意味着事件以及它们的发射器从根本上与它们的处理程序分离。事实上,事件记录的生产者通常不知道消费者是谁**可能是,消费者是否存在。
记录通常包含描述事件所需的信息。在我们之前的订单示例中,相应的事件可能由一个简单的 JSON 文档描述,该文档可能如下所示:
“orderId”:“760b5301-295f-4fec-95f8-6b303a3b824a”,
“customerId”:28623823,
“productId”:31334,
“数量”:1,
“时间戳”:“2021-02-09T11:12:17+ 0000"
注意:尽管存在细微差别,但记录和事件通常可以互换使用;即,术语“事件”用于表示该事件的“记录”。为了让事情变得更容易,我们将允许自己从现在开始享有同样的自由。
诚然,上面的示例可能是对订单的过度简化,但它就足够了。提出订单的应用程序(例如,购物车服务)不知道谁将处理订单、何时、如何甚至为什么。生产者确保捕获潜在消费者处理事件所需的一切。也就是说,订单记录并不严格需要包括其履行所需的每一个属性。例如,产品的尺寸,其库存位置和客户的送货地址不是直接指定的,而是可以通过在订单记录中捕获的ID来解决。您可能从关系数据库中熟悉的外键概念也适用于事件。
引导事件
如果事件的生产者和消费者彼此不知道,他们如何沟通?
线索就在“记录”一词中。事件通常保存在一个众所周知的位置,称为日志。(有时,可以使用术语账本。)日志是低级的、仅附加的数据结构,允许生产者将事件保存在其他方(称为消费者)以后可以访问它的位置。对日志的所有操作都由代理(位于生产者和消费者之间的持久中间件)提供便利。发布事件后,任何人和每个人都可以使用该事件。
在处理事件驱动系统时,我们经常使用术语流来描述一个或多个日志的接口。虽然日志是一个物理概念(使用文件实现),但流是一种逻辑结构,它将事件表示为无限的记录序列,受某些排序约束。不同的事件流平台可能使用专有名称来指代流。Apache Kafka——迄今为止最流行的事件流平台——根据主题和分区来描述流。
以下参考模型描述了生产者、消费者和流之间的关系。
帮助巩固我们理解的快速检查点:
- 事件是在离散时间点发生的感兴趣的动作,可以从外部观察和描述。
- 事件作为记录保存。事件和记录尽管是相关的,但在技术上是不同的东西。事件是某事的发生(例如,状态变化),它本身是无形的。记录是对该事件的准确描述。我们经常使用术语事件来指代它的记录。
- 生产者是通过将相应记录发布到流中来检测事件的受体。
- 流是记录的持久序列。它们通常由一个或多个基于磁盘的日志支持。同样,流可能由数据库表、分布式共识协议,甚至是区块链式的去中心化账本支持。
- 代理管理对流的访问,促进读写操作,处理消费者状态并在流上执行各种内务管理任务。例如,当记录溢出时,代理可能会截断流的内容。
- 消费者从流中读取数据并对记录的接收做出反应。对事件的反应可能会带来一些副作用;例如,消费者可能会将一个条目持久保存到其本地数据库中——从其发布的“更新”事件中重建远程实体的状态。
- 消费者和生产者可能重叠;例如,对事件的反应可能是产生一个或多个衍生事件。
通过异步和通用性解耦
回到我们开始的地方,为什么 EDA 会导致耦合程度显着降低?
耦合的一个实用定义是组件受其他组件影响的程度。耦合既存在于空间中——组件在结构上是相关的,也存在于时间中——时间的概念会影响它们之间关系的程度。后者的一个很好的例子是一个服务同步调用另一个服务的 REST API。如果被调用的服务宕机,被调用者通常无法继续——它在响应中被阻塞。如果两个服务必须同时运行,那么它们之间存在一定程度的时间耦合。如果组件之间有很强的相互依赖关系,我们说组件是紧耦合的,否则就是松耦合的。
EDA 采用双管齐下的方法来抑制耦合。
- 回想一下,事件没有被传达,它们只是发生了。引发事件的组件(通过发布记录)不知道可能存在或不存在的其他组件。因此,如果消费者不可用,生产者不会停止工作——前提是代理可以持久地缓冲事件而不会对生产者施加背压。
- 代理上事件记录的持久性在很大程度上消除了时间的概念。生产者可能在T1时间发布事件,而消费者可能在T2读取它,T1和T2可能以毫秒(如果一切顺利)或几小时(如果某些消费者情绪低落或挣扎)分开。
EDA 不是灵丹妙药。它并没有完全消除耦合的概念——否则,系统中的组件将不再共同发挥作用。现在我们的注意力转向了代理:为了让生产者和消费者有意义地解耦,他们必须转而依赖(因此将自己耦合到)代理。这增加了系统架构的复杂性并引入了另一个故障点。这就是为什么经纪人必须具有高性能和容错性,否则我们只是将一组问题换成了另一组问题。
事件处理方式
事件处理通常分为三种名义风格。这些风格不是相互排斥的,经常一起出现在大型的事件驱动系统中。
离散事件处理
离散事件的处理;例如,在社交媒体平台上发布帖子。离散事件处理的特点是存在通常彼此无关并且可以独立处理的事件。
事件流处理
处理无限制的相关事件流,其中事件记录以某种顺序出现,并在处理过去事件的一些知识的情况下进行处理。一个很好的例子可能是对业务实体的更改联合。消费者可以按照生产者规定的顺序应用这些更改,以将实体的副本保存在其本地数据库中。由于顺序很重要,离散地处理这些更改记录可能不会减少它。消费者还需要避免竞争条件,即多个消费者实例可能会尝试同时对数据库中的同一条记录应用更改,从而由于无序更新而导致数据不一致。
像 Kafka 这样的流行事件流平台依靠记录键控和分区来保持更新的顺序。Kafka 还保证对实体的所有更改都由一个消费者实例处理,从而避免多个消费者天真地并行处理事件时可能导致的并发竞争。
复杂事件处理
复杂事件处理 (CEP) 从一系列简单事件中派生或识别复杂事件模式。CEP 的一个示例可能是监控建筑物中的一组温度和烟雾传感器,以推断火灾已经发生并跟踪其进展。个别温度变化可能不足以引发警报;然而,温度峰值的聚集和变化率可能会提供更有意义的见解,最终可以挽救生命。
这种处理通常涉及更多,需要事件处理器跟踪先前的事件并提供查询和聚合它们的有效方式。
何时使用 EDA
有几个用例可以发挥事件驱动架构的优势:
- 不透明的消费者生态系统。生产者通常不了解消费者的情况。后者甚至可能是短暂的过程,可以在短时间内来去匆匆!
- 高扇出。一个事件可能由多个不同的消费者处理的场景。
- 复杂的模式匹配。事件可能被串在一起以推断更复杂的事件。
- 命令查询职责分离。CQRS 是一种将数据存储的读取和更新操作分开的模式。实施 CQRS 可以提高应用程序的可扩展性和弹性,但需要权衡一些一致性。这种模式通常与 EDA 相关联。
EDA 的好处
- 缓冲和容错。事件可能以与其生产不同的速率被消费,生产者不能放慢速度让消费者赶上。
- 生产者和消费者解耦,避免笨拙的点对点集成。向系统添加新的生产者和消费者很容易。只要遵守约束事件记录的合同/模式,更改生产者和消费者的实现也很容易。
- 大规模的可扩展性。通常可以将事件流划分为不相关的子流并并行处理这些子流。如果事件积压增加,我们还可以扩展消费者数量以满足负载需求。像 Kafka 这样的平台能够以严格的顺序处理事件,同时允许跨流的大规模并行性。
EDA的缺点
- 仅限于异步处理。虽然 EDA 是一种用于解耦系统的强大模式,但它的应用仅限于事件的异步处理。EDA 不能很好地替代请求-响应交互,其中发起者必须等待响应才能继续。
- 引入了额外的复杂性。传统的客户端-服务器和请求-响应式计算仅涉及两方,而采用 EDA 则需要第三方——代理来调解生产者和消费者之间的交互。
- 故障屏蔽。这是一个特殊的问题,因为它似乎与解耦系统的本质背道而驰。当系统紧密耦合时,一个系统中的错误往往会迅速传播,并经常以痛苦的方式引起我们的注意。在大多数情况下,这是我们希望避免的:一个组件的故障应该对其他组件的影响尽可能小。故障掩蔽的另一面是它无意中隐藏了本应引起我们注意的问题。这是通过向每个事件驱动组件添加实时监控和日志记录来解决的,但这会增加复杂性。
需要注意的事项
EDA 不是灵丹妙药,并且像任何强大的工具一样,它很容易被误用。下面的列表不应该被解读为 EDA 的直接缺点,而更多的是作为谨慎的开发人员和架构师在设计和实现事件驱动系统时应该注意的一组陷阱。
- 错综复杂的编舞。对于松散耦合的组件,人们可能会陷入架构可能类似于 Rube Goldburg 机器的情况,其中整个业务逻辑被实现为一系列伪装成事件的副作用:一个组件可能引发一个触发事件的事件另一个组件中的响应引发另一个事件,触发另一个组件,等等。这种组件之间的交互方式很快就会变得难以理解和推理。
- 将命令伪装成事件。事件是对已经发生的事情的纯粹描述;它没有规定应如何处理该事件。另一方面,命令是针对特定组件的直接指令。因为命令和事件都是各种消息,所以很容易被带走并将命令误认为是事件。
- 对消费者保持不可知论。事件应该以不限制如何处理这些事件的方式捕获相关属性。这说起来容易做起来难。有时我们可能会知道更多信息,理论上这些信息可以添加到事件记录中,但尚不清楚将这些信息添加到记录中是否有用,或者它是否只会导致无用的膨胀。
结论
微服务架构范式是构建更具可维护性、可扩展性和健壮性的软件系统的更广泛难题的一部分。从问题分解的角度来看,微服务非常棒,但它们留下了很多棘手的问题;一个这样的问题是耦合。与你开始的地方相比,一个随意分解成几个微服务的单体实际上可能会让你处于更糟糕的状态。我们甚至有一个术语:“分布式单体”。
为了帮助完成这个难题并解决耦合问题,我们研究了事件驱动架构。
EDA 是一种通过使用生产者、消费者、事件和流的概念对交互进行建模来减少系统组件之间耦合的有效工具。一个事件代表一个感兴趣的动作,并且可能被甚至不知道彼此存在的组件异步发布和消费。EDA 允许组件独立运行和发展。杀死所有恶魔并不是灵丹妙药,但如果 EDA 是一个合适的选择,它带来的好处远远超过采用它的成本。有人可能会说,EDA 是任何成功的微服务部署的基本要素。