基于微服务的事件驱动系统的架构考量
意译自:https://developer.ibm.com/depmodels/microservices/articles/eda-and-microservices-architecture-best-practices/
使用这两种架构风格构建分布式、高扩展性和高可用、具有容错性和弹性的系统。
概念科普
首先,有必要搞清楚高可用和容错(以及灾难恢复,因为后面会聊到系统的可恢复性)的概念:
术语 | 解释 | 可视化 |
容错 | 容错解决方案有能力在一个或者多个组件发生故障的情况下保持系统不中断的运行。 | |
高可用 | 高可用设计并不保证不中断,这是容错设计要做的。高可用设计意味着中断时间很短,因为重新部署必要的组件非常快。 | |
灾难恢复 | 通过抛弃受损的部件来保护业务。好的灾难恢复方案就是从坏掉的基础设施中将业务的高价值数据和操作保留出来并有能力再次在新的基础设施中运行起来。灾难恢复并不是保护基础设施,而是保护业务。 |
背景
今天的 IT 系统正在产生、收集以及处理前所未有的不断增加的数据。并且,他们正在应对高度复杂的处理流程(自动化的)并且往往要在跨越典型组织界限的系统和设备之间集成。同时,IT 系统被期望要开发得越来越快,而且要越来越便宜,同时需要具备高可用性、可扩展性以及弹性。
要实现以上目标,开发者们正在采用诸如微服务架构和事件驱动架构的架构风格以及编程范式、以及 DevOps 等等。新的工具和框架正在被构建来帮助开发者们完成达到这些期望的交付。
开发者们正在结合事件驱动架构(EDA)以及微服务架构风格来构建特别有扩展性、可用性、容错性、并发性以及易于开发和维护的系统。
在这篇文章中,我将讨论当使用这两种架构风格来构建这样的系统时的架构特征、复杂性、关注点、主要的架构考量点以及最佳实践。虽然诸如 API、API 网关以及用户界面等等组件在架构上存在显著区别,但是本篇文章中我将仅仅聚焦在事件驱动微服务上。
事件驱动架构以及微服务架构的总览
事件驱动架构(EDA)已经存在很长时间了。云、微服务、以及 serverless 编程范式和复杂的开发框架正在增加 EDA 在解决实时性关键业务问题的可应用性。诸如 Kafka、IBM Cloud Pak for Integration、以及 Lightbend 等技术和平台,和诸如 Spring Cloud Stream、Quarkus 以及 Camel 等开发框架都提供了对 EDA 开发的一等公民级别支持。EDA 也为流数据处理做了扩展,这对开发实时人工智能或者机器学习解决方案来说是必备的。文章《事件驱动架构进阶》定义了什么是 EDA 并且解释了为什么开发者应该使用它。
微服务架构在将单体应用使用领域驱动设计拆分成自包含、独立部署的服务的转型项目中被广泛接纳和使用。Martin Fowler 和 James Lewis 的一篇文章对微服务架构及其特性做了很好的介绍。微服务可以暴露 API 并且拥有生产和消费事件的界面来和 EDA 做无缝地集成。它的很多特性都非常适合与 EDA 结合。《微服务架构风格的挑战和好处》一文中讨论了开发者们在实现微服务时面临的挑战。
以下表格显示了两种架构风格的互补:
EDA | 微服务架构 |
组件/服务间低耦合 | 捆绑上下文来提供关注点分离 |
有能力对组件进行独立伸缩 | 独立部署和扩展 |
处理组件可以各自独立开发 | 支持多种语言开发 |
高度云倾向 | 云原生 |
异步本质。并且有能力调控负载 | 弹性扩展 |
容错以及更好的弹性 | 可见性好、故障检测迅速 |
有能力构建处理流水线 | 演进的本质 |
拥有复杂事件代理的能力,降低了代码复杂性 | 一套标准可复用的技术服务,通常被称为微服务底座 |
拥有丰富的被证明了的企业集成模式 | 提供丰富的可复用的实现模式的代码仓库 |
通过结合这两种架构风格,开发者们可以构建分布式的、高度可伸缩的、高可用的、具有容错性的、以及可扩展的系统。这些系统可以消费、处理、聚合或者关联非常大量的实时性事件或者信息。开发者们可以通过使用工业级标准的开源框架以及云平台来轻松地扩展并且增强这些系统。
架构问题和复杂性
通过结合 EDA 和微服务架构风格,开发者们能够轻松地达到很高的非功能性质量要求比如性能、可伸缩性、可用性、弹性,而且还能简化开发。但是这两种架构风格也带来了一些主要的问题。
这些问题包括:
非常多数量的分布式独立部署的组件或者服务,带来了如下问题:
设计与实现的复杂度。理解并调试这样的系统很困难。事件处理工作流不直观,需要文档化。
多重单点故障。测试、调试以及异常处理的复杂度都增加了。
发布流程、部署以及系统监控更加复杂了,而且需要更高水平的自动化。
从开发视角看,渴望实现的一致性、设计得到遵守以及实现有一定标准,但是往往存在多个开发团队,这可能导致实现上的不一致从而带来质量问题。从而开发一个架构参考,列出架构模式如何使用、开发框架、开发可复用的服务和工具、以及建立一个强健并有效的管理模型变得至关重要。
异步事件处理相比同步处理更加困难,因为需要协调事件顺序、回调以及异常处理。
丢失信息或者事件显然是不被期待的。所以,极高可用性、可伸缩性、以及容错能力的系统特别重要,这使得系统的设计和部署尤其复杂。事件的生产方和消费方必须要为失败而设计,必须拥有失败事件重放的能力,以及去重能力。
维持数据的一致性。由于分布式的天然属性以及多个系统的记录,维持数据一致性非常复杂。多数情况下,由于缺少跨多个分布式系统的原子事务性,所以只能做到最终一致性。
事件消费方和生产方必须考虑那些为事件中间人、数据缓存等等的特定于产品的属性。比如,送达保证这一因素就影响着生产者和消费者的设计。
EDA-微服务系统的架构蓝图
下图是一个基于 EDA-微服务的企业架构图。一些微服务组件以及类型单独呈现以求更好的架构明晰度。
这个蓝图中涉及到的 EDA 和微服务相关组件有:
事件骨干。事件骨干主要负责传输、路由和时间序列化。它提供 API 处理时间流。事件骨干支持多种序列化格式并且对架构质量有着主要的影响,如容错、弹性伸缩、吞吐量等等。事件也可以存储起来用来创建事件存储。事件存储是恢复和弹性的关键架构模式。
服务层。服务层由微服务、集成和数据以及分析服务组成。这些服务通过不同的接口暴露其功能,包括 REST API、UI 和 EDA 事件生产者和消费者。服务层也包含针对 EDA 特定的横切关注点的服务,比如编排服务、流数据处理服务等等。
数据层。数据层通常由两个子层组成。在这个蓝图里,没有展示微服务独立的数据库。
缓存层,提供分布式的内存数据缓存或者网格来改进性能并且支持诸如 CQRS 这样的模式。这是水平可伸缩的并且可以做到一定程度的复制以及持久化以保持弹性。
大数据层,由数据仓库、ODS、数据集市、AI/ML 模型处理组成。
微服务底座。微服务底座提供必要的技术和横切面服务给到系统不同的层。它提供开发和运行时能力。通过使用微服务底座,你可以在减少设计和开发复杂度以及运维成本的同时增加对市场、交付质量以及对大量微服务的管理水平的时间投入。
部署平台:弹性、优化后的成本、安全以及对云平台的易用性都要被考虑到。开发者应该尽可能使用 PaaS 服务来减少运维和管理负担。该架构还应为混合云的建立预留资源,所以可以考虑像 Red Hat OpenShift 这样的平台。
架构的主要考量点
架构的考量点会影响系统的实际架构,它们对做架构决定有着指导作用,并且对系统的非功能性特性有主要的影响。如下的架构考量对基于事件驱动的微服务系统尤其重要:
架构模式
技术栈
事件模型
处理器的拓扑结构
部署的拓扑结构
异常处理
利用事件骨干的能力
安全
可观测性
容错和响应
架构模式
选择架构和集成模式是事件驱动的微服务系统的非常重要的架构考量。它们提供了被证明过和被测试过的解决方案以大量的符合期望的架构质量。在开发事件驱动的基于微服务的系统中,如下的架构模式尤其有用:
管道与过滤器
分阶段的事件驱动架构(SEDA)
事件溯源
命令查询职责分离(CQRS)
SAGA
流处理
微服务底座
死信队列(DLQ)
另外很多企业集成模式和微服务模式提供了事件驱动的基于微服务的系统的基础成分。
应该根据需求和系统期望的架构质量来进行模式的选择。
技术栈
诸如时间代理、数据缓存或者网格、微服务框架、安全机制、分布式数据库、监控系统和告警系统这样的组件形成了事件驱动的基于微服务的系统的技术骨干。该骨干为关键的架构质量(性能、可用性、可靠性、运维成本、容错性等)提供支持,并且简化了开发。它还影响着几个设计和开发的决策。
当你选择技术战时,要考虑这些特性:
单个组件的水平伸缩能力。伸缩能力不应该牺牲可用性。就是说添加节点不需要宕机。
单个组件的高可用。选择的产品或者框架要支持将跨不同可用区或者地理区域的成员集群、支持滚动更新、支持数据复制以及容错,即集群在节点丢失后可以重新平衡自己。
云亲和性,即部署到云上非常容易。事实上,如果可以在 PaaS 平台用作服务那么更好,这是因为减少了管理和运维负担。支持容器化是必须项。
低运维成本,即应该能够运行在廉价的硬件上,节约对于 CPU、内存和存储的使用。
可配置性以及不宕机微调行为和非功能特性的能力。
可管理性。
要避免厂商绑定。选择基于开放标准或者开源的产品。当挑选开源产品时,考虑产品被使用的广度,是否拥有繁荣的开发者社区,并且需要开放的许可证,不能太多约束(比如 Apache License V2.0)。
对事件代理和开发框架来说,应该支持:
多序列化格式(JSON、AVRO、Protobuf 等)
异常处理以及死信队列(DLQ)
流处理(包括对聚合、联合以及窗口的支持)
分区和事件顺序的保持
最好有响应式编程支持
事件骨干最好有多语言编程支持
下面的表格列出了不同组件流行的选型:
组件类型 | 选型 |
事件骨干 | Apache Kafka、诸如 IBM cloud pak for integration、Lightbend 这样的集成平台、AWS Eventbridge + Kinesis |
微服务开发框架 | 像 Spring Boot、Spring Cloud Stream 这样的 Spring frameworks,以及 Quarkus 和 Apache Camel |
数据缓存/网格 | Apache Ignite、Redis、Ehcache、Elasticsearch 和 Hazelcast |
可观测性 | Prometheus + Grafana、ELK、StatsD + Graphite、Sysdig、AppDynamics、Datadog |
事件建模
事件建模包括定义事件类型、事件层级结构、事件元数据和负载格式。以下事件建模特性需要仔细斟酌:
事件类型。在一个企业级系统中,多个业务领域相互消费和生产不同类型的事件。建模的重要方面之一是识别事件的类型和事件本身。使用诸如事件风暴以及事件溯源这样的领域驱动设计和实践来对事件进行识别和分类。事件类型可以是天然的层次化结构,从而帮助形成一个分层的事件处理形式。定义涵盖所有业务需求的事件类型和事件后需要将他们映射到不同的业务流程或者工作流中。事件类型的粒度对避免在组件中产生紧耦合至关重要。事件的类型是定义路由规则的关键点。
事件模式。事件模式包括事件元数据(比如类型、事件、源系统等等)和负载(即信息载体),由事件处理器处理。事件类型典型的用途是路由。事件元数据的典型用途是事件关联与排序,但也可以用作为审计和授权目的。负载影响队列的大小、主题和事件存储、网络性能、(反)序列化性能以及资源利用率。要避免重复内容。只要需要应当总是允许通过重放事件来重新生成状态。
版本控制。需求与实现总是随着时间不断演进,这通常会影响事件模型,而对事件模型的改变又可能会影响太多的微服务,但是同时更新所有被影响到的微服务不太现实。所以,事件模型需要支持多个版本并且保证向后兼容从而不同的微服务可以在自己方便的时刻更新。同时,通过给负载添加新的属性而不是直接更改现有属性是一个好的做法(废弃,而非更改)。版本控制和序列化格式彼此独立。
序列化格式。可以用来编码事件及其负载的序列化格式有多种,比如 JSON、protobuf 和 Apache Avro。这里模式演进支持、(反)序列化性能和序列化大小需要重点考虑。虽然 JSON 由于人类可读性友好从而便于开发和调试,但是 JSON 性能并不好,并且导致事件存储需求变大。Avro 或者 Protobuf 不仅减小了存储需求,还更快,并且支持模式演进,只是需要额外的设计和开发负担。
分区。事件的分区对于增加并发度、可伸缩性以及可用性至关重要。分区还是事件排序的关键影响因素。从架构角度上说,选取分区主键尤为重要。太粗粒度的主键影响可伸缩性和并发度。非常细粒度的主键可能对事件保序没有太多帮助。在诸如 Kafka 这样的事件代理中,分区和事件消费者的可伸缩性是绑定在一起的。
排序。一些事件可能需要基于它们的到达时间来排序(至少对给定的实体来说)。比如,对于给定的账号,该账号的交易流水需要按序得到处理。识别那些事件需要排序很重要。因为排序对性能和吞吐量有影响,所以事件排序只应该用在必要的场合。在 Apache Kafka 中,事件排序和分区直接相关。
事件耐久性。耐久性是指事件在队列或者主题中存在多长的可用时间。比如,要不要在消费事件后立即删除它?要不要删除那些超过了配置好的保持时间的事件,以及要不要删除被显式标记为死信的事件(比如 Kafka 中的 tombstones)?这些都应该基于需求做相关配置。当使用基于时间的保持性时,需要考虑到事件需要为重放保持多久?而当使用事件存储模式时,则需要额外考虑对于相同的事件或者负载需要保持多少个版本?像 Kafka 这样的事件代理为指定事件的持久性提供了不同的配置选项,可以在主题级别进行设置。
事件处理拓扑
EDA 中的处理拓扑是指生产者、消费者、企业集成模式、以及主题和提供事件处理能力的队列的组织结构。他们基本上是事件处理流水线,部分职能逻辑(处理器)使用企业集成模式和队列以及主题联合在一起。处理拓扑是 SEDA、EIP 以及管道和过滤器模式的组合。复杂的事件处理需要多个能够连接在一起的处理拓扑。
处理拓扑中的另一个关键概念是编排还是编舞。编排指一个中心编排者通过调用不同组件来编排处理工作流,需要严格控制处理过程,比如在支付处理时会选择编排模式。通常在 SAGA 模式被应用时需要编排。编排存在性能和可用性的权衡(因为编排者存在单点故障)。编舞是指完全去中心化的处理方式。也就是说,通过发布事件和感兴趣的组件去订阅主题的方式来处理。由于没有中心组件控制处理流,编舞在实现和维护上比较复杂。
创建处理拓扑时考虑以下指导方针:
处理阶段(处理器)使用持久化的队列和主题连接。
在每个队列和主题中配置分区主键和消息保持政策。
处理粒度很重要。如果处理器粒度太细,则存在处理器间紧耦合的可能。理想上,每个处理器应该彼此逻辑独立。
能够使用微服务实现处理器。这可以实现松耦合、职责分离、简化开发。
处理并发度应能在处理器级别被配置。
使用被验证过的企业集成模式(EIP)。采用内置 EIP 支持(如 Apache Camel 或者 Srping Cloud Stream)的开发框架。
构建模块化和层次化的处理拓扑从而使得复杂的事件处理可以由简单处理流水线组装而来。这让实现模块化并且易于维护。
如果处理器存在状态(随事件变化),考虑存储来备份状态从而增加容错性和可恢复性。
诸如处理事件流和事件托管状态的架构实践可被用来设计处理拓扑。定义处理拓扑时能对时间代理能力有细致的理解是很好的。例如 Kafka 流提供了对定义事件流处理拓扑的一等公民级别的支持。Kafka 也自带对事件流执行聚合和联结操作时状态存储支持。
下图摘取了一个处理拓扑的蓝图。
下图是简化的线上购物订单处理拓扑。路由器拥有动态路由事件到多个主题的能力。需要注意事件处理器还要拥有“事件过滤器”来根据上下文控制事件的消费和生产。
部署拓扑
在 EDA-微服务架构下,要部署的组件特别多。应选择能够满足架构需求的部署拓扑,这些需求与可伸缩性、可用性、弹性、安全性以及成本相关。但是,在冗余、性能和成本间需要做出权衡和取舍。部署到云让架构更加高性能、更有弹性和更有性价比。需要充分利用云部署提供的能力(比如 Kubernetes 里的高可用性)。
你的部署拓扑应该考虑这些关键原则:
每个部署组件应该可以独立伸缩和部署为集群来增加并发度和弹性。
保证每个集群分布在多个可用区。这样的安排提供了更好的韧性以防万一数据中心出现故障。这样做的额外好处是不存在被动的灾难恢复,而是用跨不同可用区/地域的多活部署代替。
复制因子决定了一个事件或者信息的副本数。没有复制,单实例故障(尽管是集群)会导致数据丢失。这对于事件代理和数据库尤其重要。但是,复制增加了计算和存储成本。复制应该基于像可用区、数据地域、节点数量等因子来建立。
如果使用 Kafka,主题分区数量是消费者并发的上限。
工作负载调节。配置线程池和消费者以及生产者的数量来调节吞吐量。这些参数要取决于下游处理器的容量和吞吐量做调整。
数据压缩。依据组织的安全标准,配置事件代理、生产者和消费者(以及数据库)间的 TLS,认证和授权。要注意开启 TLS 会增加 CPU 的使用率。
另外,对自动化部署、自动故障转移、滚动更新或者蓝绿部署、以及使得部署物的环境彼此独立的配置外部化的支持很重要。
异常处理策略
拥有完整和一致的异常处理策略对提高 EDA 的韧性很重要。异常处理策略包含下面所有或者其中一些点:
记录异常日志
以指定的重试间隔重试事件指定的次数
耗尽重试次数后移动事件到死信队列(或者停止对事件的处理)
告警
在某些情况下生成一个事件
纠正异常根因并重放事件
异常有两种:业务异常和系统异常。当校验或者业务条件失败抛出的是业务异常。系统异常是由组件失败(如数据库、事件代理、或者其他微服务等)或者资源问题(如内存耗尽错误)、网络或者传输相关问题(如负载序列化、反序列化错误)、未期待的故障代码(如空指针异常或者类型转换异常)引起的一个广泛的故障类别。
处理不同类型的异常存在显著差别。下面列出了一些异常处理机制:
预期中的业务异常通常由代码处理。处理包括记录异常日志、更新实体和状态、生成异常事件、或者消费异常并继续。
由无效负载引起的异常(包括序列化、反序列化问题)无法用重试解决。这些事件在 Kafka 中被喻为毒药丸(因为它阻碍了分区中接下来的消息流)。这种事件需要被打断。建议把它们移到死信队列(DLQ)中。DLQ 的消费者可以纠正它们并重放事件。
由于组件不可用会天然导致系统异常。所以,需要配置多次重试机制。另一个关键配置参数是退避乘数。它用来在连续的两次重试中指数式增加时间间隔。对于重试仍然有错的情况不同框架有不同策略。如 Camel 会把事件移到 DLQ。Kafka 流会停止处理。在这样的场景下推荐使用框架的默认行为。
资源问题(如内存耗尽错误)通常在组件级别并且会导致组件不可用。由于事件代理的天然容错机制,丢失事件的风险被最小化。当部署在 Kubernetes 环境时,新的 pod 会启动替换掉故障 pod。
当数据一致性非常重要并且处理过程涉及到多个微服务时可以使用 SAGA 模式。当数据一致性需求非常严格时可采用 SAGA 模式。
从最开始就要考虑恢复和重放机制而不能做事后诸葛(事后会变得异常复杂)。恢复和重放组件通常定制开发并且基于事件处理过程有所区别。最简单的重放组件可能只需要将失败事件重新发布到输入主题中。
开发框架应该拥有跨所有微服务的一致异常处理策略。它应该提供一系列预先定义好的业务异常类以及提供一个通用的可由配置定制化的异常处理器,但不应该强制异常处理相关的架构决策。多数开发框架都提供了这些支持。但是它们需要配置正确或者扩展后才能提供必要的特性。
事件骨干的能力和限制
不同事件骨干产品或者平台提供的架构质量支持是不同的。同时,它们在设计和架构上引入了限制。当定义架构时,需要考虑它们的能力和限制以最有效地满足非功能性需求。比如,以下是 Kafka 的几条重要能力和限制:
Kafka 提供基于分区主键的事件排序支持。它也保证一个分区只有一个监听消费者(线程)。这让它非常易于通过选择合适的分区主键排序事件。比如,当 OrderId 用作分区主键时,可以保证所有和特定订单的事件依照它们到达的顺序得到处理。
Kafka 支持生产者的幂等性。这意味着 Kafka 保证了一个生产者只会生成同一事件一次。开发者不需要关心这个。
Kafka 提供最少一次交付保证。这意味着消费者可以消费重复消息。
Kafka 另一个重要的方面是为消费者提供的偏移提交功能,这意味着事件是否需要自动或者手动确认。如果启用了自动提交,事件出现的错误可能会丢失(如果异常被吞掉的话)或者消费者可能看到重复消息。手动提交可能可以解决这个问题,但是需要额外的代码。诸如 Spring-cloud-stream 这样的框架可以和 Kafka 无缝对接工作,提供了错误产生时非自动提交的选项,也可以在自动/手动提交的基础上移动失败事件到 DLQ。在设计中这需要重点考量。
Kafka 流提供了处理事件流的能力,在事件流上可以容易地执行不同的高级复杂操作,如聚合和联结等。这让执行实时分析非常容易。比如,计算事件按照不同维度分组的统计量只需要很小的代码量。这是有状态的操作并且需要维护状态。Kafka 也通过状态存储提供自动容错。
安全性
在 EDA-微服务架构中开发者必须考虑这些安全方面上的因素。
传输级别安全
事件生产和消费的认证和授权访问
事件处理的审计追踪
数据安全(如认证访问和加密存储)
消除代码中易受攻击的部分
周边安全设备和模式
可观测性
可观测性包括监控、日志、追踪和告警。系统的每个组件都应该易于观察来避免故障以及快速从故障中恢复。
大多数 EDA 产品和开发框架都通过发布可以导入到工业级标准观测工具(如 Prometheus 和 Grafana、ELK、StasD 和 Graphite、Splunk 或者 AppDynamics)的指标值为可观测性提供支持。比如,Apache Kafka 提供了详细的指标可用来导入和集成到多数工具中。并且,为事件骨干提供托管服务的云平台(IBM Event Streams)给可观测性提供一等公民级别支持。诸如 Spring 或者 Camel 这样的微服务开发框架为监控的代码安装提供了良好的支持。
从 EDA 角度来看,为发布指标而设置生产者和消费者代码,发布事件代理指标以及通过指标仪表盘关联它们是非常关键的,因为 EDA 中的分布式组建数量特别多。从 EDA 的角度,关注的一些关键指标是消息的入出比,消费延迟、网络延迟和主题大小等等。
对微服务的监控,可以参考另外的专门文章以获取详细指导。
容错和响应
要提供足够的容错能力,架构需要提供冗余、异常处理和弹性伸缩(当突破阈值时向上扩展,当负载回归到正常水平向下缩容)。多数 EDA 和云很容易做到这点。事件骨干通过支持队列和主题的分区和复制来实现容错。生产者和消费者能够拥有多个部署的实例。在 Kubernetes 平台上进行容器化部署,弹性伸缩可以通过自动伸缩(使用 pod 的水平自动伸缩器)很容易地实现, 但是异常处理需要为生产者和消费者特别设计。
尽管基于 EDA 的系统通过分阶段的架构提供了韧性,为了避免延误和一致性问题,快速故障响应和恢复仍然至关重要。要实现快速恢复,需要:
自动化启动和停止实例、自动化重启故障实例。在基于 Kubernetes 的平台(比如 Red Hat OpenShift)这很容易配置。
故障发生时进行告警、触发意外事件。
良好定义的事件管理流程
可用的日志和跨多个组件关联日志和追踪的能力。追踪需要在微服务中启用,可以使用如 Spring-sleuth 等的开发框架。日志聚合工具有 ELK、或者 Splunk。这将帮助团队定位根因并且快速解决问题。
结论
开发者可以结合事件驱动架构和微服务架构风格来开发分布式、高可用、容错和高吞吐量的系统。这些系统可以处理大量信息并且特别易于扩展。但是,当构建这样的系统时,开发者必须考虑很多架构关注点以及复杂度,并且要做出关键的架构决策。本文讨论了关键的架构决策以及做这些决策时要考虑的因素。遵循这些指导,可以为实现预期的目标定义出强健的 EDA-微服务架构。