如果您是一名企业架构师,您可能听说过微服务架构,并使用过它。虽然您过去可能使用REST作为服务通信层,但是越来越多的项目正在转向事件驱动的体系结构。让我们深入了解这种流行架构的优缺点、它所包含的一些关键设计选择以及常见的反模式。
什么是事件驱动的微服务体系结构?
在事件驱动的体系结构中,当服务执行其他服务可能感兴趣的某些工作时,该服务将生成一个事件—执行操作的记录。其他服务使用这些事件,以便它们能够执行由于该事件而需要的任何自己的任务。与REST不同,创建请求的服务不需要知道使用请求的服务的详细信息。
这里有一个简单的例子:当一个订单被放置在一个电子商务网站,一个单一的“订单放置”事件产生,然后被几个微服务消费:
1.order服务,它可以向数据库写入一个order记录。
2.客户服务,它可以创建客户记录。
3.支付服务,它可以处理支付。
事件可以以多种方式发布。例如,可以将它们发布到保证将事件交付给适当使用者的队列中,也可以将它们发布到发布事件并允许访问所有相关方的“发布/订阅”模型流中。在这两种情况下,生产者发布事件,消费者接收该事件,并做出相应的反应。注意,在某些情况下,这两个角色还可以称为发布者(生产者)和订阅者(消费者)。
为什么使用事件驱动的体系结构
与REST相比,事件驱动架构提供了以下几个优点:
异步——基于事件的架构是异步的,没有阻塞。这使得资源可以在他们的工作单元完成后自由地转移到下一个任务,而不用担心之前发生了什么或者接下来会发生什么。它们还允许对事件进行排队或缓冲,从而防止使用者向生产者施加压力或阻塞它们。
•松耦合——服务不需要(也不应该)知道或依赖于其他服务。在使用事件时,服务独立运行,不了解其他服务,包括其实现细节和传输协议。事件模型下的服务可以独立地、更容易地更新、测试和部署。
•易于扩展——由于服务在事件驱动的体系结构下解耦,而且服务通常只执行一项任务,因此跟踪特定服务的瓶颈,并对该服务(且仅对该服务)进行扩展变得很容易。
•恢复支持——带有队列的事件驱动架构可以通过“重播”过去的事件来恢复丢失的工作。当用户需要恢复时,这对于防止数据丢失非常有用。
当然,事件驱动的架构也有缺点。通过分离紧密耦合时可能更简单的关注点,它们很容易过度设计;它们可能需要大量的前期投资;而且常常导致基础设施、服务契约或模式、多语言构建系统和依赖关系图的额外复杂性。
也许最大的缺点和挑战是数据和事务管理。由于事件驱动模型的异步性,它们必须小心处理服务之间不一致的数据、不兼容的版本、监视重复的事件,并且通常不支持ACID事务,而不支持最终的一致性,因为后者更难以跟踪或调试。
即使有这些缺点,事件驱动的体系结构通常也是企业级微服务系统的更好选择。主要的优点是可伸缩的、松散耦合的、开发人员操作友好的。
何时使用REST
然而,有时REST/web接口可能仍然更可取:
•您需要一个异步请求/应答接口。
•您需要对强事务的支持。
•您的API对公众可用。
•您的项目很小(REST的设置和部署要简单得多)。
您最重要的设计选择—消息传递框架
一旦决定了事件驱动的体系结构,就该选择事件框架了。事件生成和使用的方式是系统中的一个关键因素。目前已有数十种经过验证的框架和选择,选择正确的框架需要时间和研究。
分俩个大类: 消息处理或流处理。
消息处理
在传统的消息处理中,组件创建消息,然后将其发送到特定的(通常是单个的)目的地。一直处于空闲状态并等待的接收组件接收消息并相应地执行操作。通常,当消息到达时,接收组件执行单个流程。然后,删除消息。
消息处理体系结构的一个典型例子是消息队列。尽管大多数较新的项目使用流处理(如下所述),但是使用消息(或事件)队列的体系结构仍然很流行。消息队列通常使用代理的“存储和转发”系统,事件在此系统中从一个代理传递到另一个代理,直到它们到达适当的使用者。ActiveMQ和RabbitMQ是消息队列框架的两个流行示例。这些项目都有多年的实践经验和成熟的技术社区。
流处理
另一方面,在流内处理中,组件在达到某个状态时发出事件。其他感兴趣的组件在事件流中侦听这些事件并作出相应的反应。事件不针对特定的收件人,而是对所有感兴趣的组件可用。
在流内处理中,组件可以同时对多个事件作出反应,并对多个流和事件应用复杂的操作。有些流包括持久性,即事件在流上停留的时间可以根据需要延长。
通过流处理,系统可以重现事件的历史,在事件发生后联机并仍然对其作出反应,甚至执行滑动窗口计算。例如,它可以从每秒的事件流计算每分钟的平均CPU使用量。
最流行的流处理框架之一是Apache Kafka。Kafka是许多项目使用的成熟和稳定的解决方案。它可以被认为是一种工业强度的流处理解决方案。Kafka有一个庞大的用户群、一个有用的社区和一个改进的工具集。
其他的选择
还有其他框架提供流和消息处理的组合,或者提供它们自己独特的解决方案。例如,Apache的最新产品Pulsar是一个开源的发布/订阅消息系统,它支持流和事件队列,所有这些都具有极高的性能。Pulsar的特点是丰富的-它提供多租户和地理复制-因此复杂。据说Kafka的目标是高吞吐量,而脉冲星的目标是低延迟。
NATS是另一种具有“合成”队列的发布/订阅消息系统。NATS是为发送小而频繁的信息而设计的。它提供了高性能和低延迟;然而,NATS认为某种程度的数据丢失是可以接受的,优先考虑性能而不是交付保证。
其他的设计考虑
一旦你选择了你的事件框架,这里有几个其他的挑战需要考虑:
•Event Sourcing
很难实现松耦合服务、不同的数据存储和原子事务的组合。一个可能有所帮助的模式是事件源。在事件源中,从来不直接对数据执行更新和删除;相反,实体的状态更改被保存为一系列事件。
•CQRS
上面的事件来源引入了另一个问题:由于需要从一系列事件构建状态,查询可能会很慢,而且很复杂。命令查询责任隔离(CQRS)是一种设计解决方案,它为插入操作和读取操作调用单独的模型。
•事件发现
事件驱动体系结构中最大的挑战之一是对服务和事件进行编目。在哪里可以找到事件描述和详细信息?事件发生的原因是什么?是哪个团队创造了这个活动?他们在积极地工作吗?
•应对变化
事件模式会改变吗?如何在不破坏其他服务的情况下更改事件模式?随着服务和事件数量的增长,如何回答这些问题变得至关重要。
成为一个好的事件消费者意味着要为变化的模式编码。成为一个好的事件生产者意味着要认识到模式更改如何影响其他服务,并创建经过良好设计的事件,这些事件被清楚地记录下来。
•内部部署vs.托管部署
无论您的事件框架是什么,您还需要在自行部署框架(消息代理的操作并不简单,特别是在高可用性的情况下),还是使用托管服务(如Heroku上的Apache Kafka)之间做出选择。
反模式
与大多数体系结构一样,事件驱动的体系结构具有自己的一组反模式。以下是一些需要注意的地方:
设计过多的事件
注意不要对创建事件过于兴奋。创建太多的事件将在服务之间创建不必要的复杂性,增加开发人员的认知负担,增加部署和测试的难度,并导致事件使用者的拥塞。不是每个方法都需要是一个事件。
通用的事件
不要使用通用事件,无论是在名称中还是在目的上。您希望其他团队了解您的事件为何存在、应该用于什么以及应该在什么时候使用。事件应该有特定的目的,并相应地命名。事件与通用名称或通用事件与混乱的旗帜,导致问题。
复杂的依赖关系图
注意那些相互依赖的服务,并创建复杂的依赖关系图或反馈循环。每个网络跳都会给原始请求增加额外的延迟,特别是离开数据中心的南北网络流量。
这取决于保证的订单、交付或副作用
事件是异步的;因此,包含顺序或重复的假设不仅会增加复杂性,而且会抵消基于事件的体系结构的许多关键优点。如果使用者有副作用,例如在数据库中添加值,则可能无法通过重播事件进行恢复。
过早优化
大多数产品一开始很小,然后随着时间的推移而增长。虽然您可能梦想将来需要扩展到大型复杂组织,但是如果您的团队很小,那么事件驱动架构的额外复杂性实际上可能会降低您的速度。相反,考虑使用简单的体系结构来设计系统,但是要包含必要的关注点分离,以便您可以随着需求的增长将其替换掉。
期望事件驱动来修复所有问题
在较低的技术级别上,不要期望事件驱动的体系结构能够修复所有的问题。虽然这种体系结构肯定可以改进许多技术功能障碍的领域,但它不能解决核心问题,比如缺乏自动化测试、缺乏团队沟通或过时的开发-ops实践。
理解事件驱动架构的优缺点,以及它们最常见的一些设计决策和挑战,是创建尽可能好的设计的重要部分。