《微服务与事件驱动架构》读书分享

《微服务与事件驱动架构》读书分享

|------------------------------------------------------------------------|
| Building Event-Driver Microservices 英文原版由 O'Reilly Media, Inc. 出版,2020 |

|-------------------------------------------------------------------------------------------------------------|-------------------------------|
| |---------------------------------------------------| | 作者: [ ] 亚当 贝勒马尔 | | |------------| | 译者:温正东 | |

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 作者简介: 这本书由亚当·贝勒马尔(Adam Bellemare)撰写,他是一位经验丰富的微服务架构师,曾在Shopify担任数据平台高级工程师,并在Flipp和Black Berry担任过软件开发工程师。贝勒马尔的专长包括Dev Ops、技术领导力、软件开发和数据工程等领域,特别是在使用Kafka、Spark、Mesos、Kubernetes、Solr、Elasticsearch、HBase和ZooKeeper集群构建和扩展事件驱动型微服务方面有着丰富的经验。 |

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 内容简介: 《微服务与事件驱动架构》是一本关于微服务和事件驱动架构的指南,旨在教授读者如何从头开始构建完整的事件驱动型微服务架构,并根据实际业务需求调整和扩展微服务。 事件驱动型微服务架构是由一系列技术发展,才变得简单可行的技术实现的。 这些数据解决方案基于大数据与近乎实时的事件处理需求的融合。微服务得益于容器化和计算资源获取的便利性,这使得对成千上万的微服务进行托管、伸缩和管理变得简单。才支撑事件驱动型微服务的技术对于如何思考和解决问题有着深远的影响,也深深地影响着企业和组织的结构。事件驱动型微服务改变了企业的工作方式、问题的解决方式,以及团队、人员和业务单元的沟通方式。 |

  1. 1 为什么用事件驱动型微服务

1.1 什么是事件驱动型微服务

面向服务的架构 (service-oriented architecture,SOA)通常由多个相互直接同步通信的微服务构成.
在现代的事件驱动型微服务架构中,系统通过发布和消费事件来通信。这些事件并不会像消息传递系统中那样在某次消费的时候就被销毁,而是可被其他多个消费者按需消费

1.2 领域驱动设计和界限上下文

|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 领域驱动设计由 Eric Evans 在他的同名图书《领域驱动设计:软件核心复杂性应对之道》 1 中提出,书 中介绍了一些构建事件驱动型微服务所必需的概念。鉴于已有丰富的文章("What is the Domain Model in Domain Driven Design?")、图书(《实现领域驱动设计》) |

以下概念支撑了领域驱动设计:

领域

某个企业所涉足并提供解决方案的问题空间。它涵盖了这个企业必须关注的所有内容,包括规则、流程、想法、特定于业务的术语,以及所有跟这个问题空间相关的东西,无论该企业是否与之相关。领域的存在跟企业是否存在无关。

子领域

子领域是主领域的一个部分。每个子领域都聚焦于一个特定的职责子集,并且通常反映了企业的部分组织结构(如仓储、销售和工程部门)。一个子领域本身可视为一个领域。跟领域一样,子领域也属于问题空间。

领域(子领域)模型

这是一种用于商业目的的对实际领域的抽象。在领域中对企业最重要的部分和属性被用来生成模型。领域模型是解决方案空间的一部分,因为它是企业用来解决问题的框架。

界限上下文

界限上下文是与子领域相关的逻辑边界,包括输入、输出、事件、需求、流程和数据模型。虽然在理 想情况下,界限上下文会和子领域保持完全一致,但是历史遗留的系统、技术债和第三方集成等因素通常 会带来一些例外。界限上下文也是解决方案空间的一个属性,对微服务如何相互交互有着重要的影响。

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 限上下文应该是高内聚的。上下文内部的运作应该是紧密相关的,绝大部分的通信发生在内部而不会跨越边界。拥有高内聚的职责可以缩小设计范围和简化实现。 界限上下文之间的关联应该是低耦合的,因为一个界限上下文内部的更改应该最小化或消除对相邻上下文的影响。低耦合可以确保在某个上下文内的需求变更不会向相邻的上下文传播大量依赖变更。 |

1.2.1 运用领域模型和界限上下文

每个组织都会在其自身和外部世界之间形成一个单独的领域。

1.2.2 保持界限上下文与业务需求一致

保持界限上下文和业务需求一致,可以让开发团队以高内聚、低耦合的方式对微服务的实现做出变更。这为团队提供了设计和实现特定业务需求解决方案的自主权,可以大大减少团队之间的依赖,并且让每个团队都高度聚焦于自身需求。

围绕着业务需求对事件驱动型微服务架构建模是更好的选择,

产品开发者可能会尝试与其他产品共享数据资源或耦合边界的方式来减少复制行为。在这些例子中,从长远来看,后续的紧耦合可能比重复代码和存储冗余数据带来更高的成本。

1.3 沟通结构

要达成目标,一个组织的团队、系统和人员必须相互沟通。这些沟通形成了一个相互连接的依赖拓扑结构,被称为沟通结构。沟通结构主要有3种,每一种都影响着企业的运作方式。

1.3.1 业务沟通结构

业务关联结构

1.3.2 实现沟通结构

实现沟通结构是由组织确定的子领域模型相关的数据和逻辑。它确立了业务流程、数据结构和系统设计,以便快速高效地执行业务操作。

1.3.3 数据沟通结构

数据沟通结构是在业务之间,特别是实现之间进行数据通信的过程。

1.3.4 康威定律和沟通结构

|---------------------------------------|
| 设计系统的组织......受到约束,产生的设计是这些组织的沟通结构的副本。 |

这句话被称为康威定律,它表明一个团队将根据其组织的沟通结构来构建产品。业务沟通结构将人员组织成团队,这些团队通常生产由他们的团队边界限定的产品.

因为领域概念跨越了不同业务,所以同一个组织里的不同界限上下文之间通常需要对方的领域数据。实现沟通结构一般不擅长提供这种通信机制,虽然它们在满足自身界限上下文的需求时表现得很出色。它们会以两种方式来影响产品设计.

首先,由于在整个组织内传递所需的领域数据很低效,它们不鼓励创建逻辑独立的新产品

其次,它们会提供能轻松访问现有领域数据的方式,但存在要不断扩展领域以满足新业务需求的风险

数据库方案也许仅仅提供了一个只读副本,但是这可能会不必要地暴露其内部数据模型。批处理程序能够将数据转存到文件中以被其他程序读取,但这种方法会造成数据一致性和多信息源的问题.最后,所有这些解决方案都会导致实现之间的紧耦合,并进一步将系统架构强化为点对点的关系

|--------------------------------------------------------------------------------------------------------------------------|
| 注意: 如果你发现在组织中访问数据很困难,或者由于所有数据都存在于单独的实现中,你的产品不得不不断扩大其涵盖的范围,那你可能正受到糟糕的数据沟通结构的影响。这个问题会随着组织的发展、新产品的开发以及对访问通用领域数据需求的增加而进一步扩大。 |

1.4 传统计算中的沟通结构

一个组织的沟通结构极大地影响了工程实现的创建方式。在团队的层面也是如此:团队的沟通结构影响了其为所负责的特定业务需求所构建的解决方案。下面来看看具体实践。

1.4.1 选项 1 :创建一个新服务

如果业务需求与原服务差异足够大,那么可以将其放入新的服务。但是数据呢?新的业务功能需要一些旧数据,但那些数据目前限定在现有的服务内。

团队必须找到一种从原有数据库中复制合适的数据到新数据库中的方法。他们需要确保不会暴露内部工作逻辑,并且对数据结构的变更不会影响任何其他复制了他们数据的团队。此外,被复制的数据通常是过时的,因为他们只能每30分钟实时同步一次生产数据,这样才不会给数据库带来大量查询,进而造成过载。复制数据的连接需要被监控和维护,以保证它的正确运行。

让新服务启动并运转起来也有风险。他们需要管理两套数据库、两个服务,并且要为它们建立日志记录、监控、测试、部署和回滚流程。他们还必须注意同步任何数据结构的变更,以免影响相关的系统。

1.4.2 选项 2 :将它加入现有服务中

另一个选项是在现有服务里创建新的数据结构和业务逻辑。所需的数据已经在当前的数据库中,日志记录、监控、测试、部署和回滚流程也早已经被定义和使用。团队很熟悉这个系统并且能正确地实现逻辑,他们的单体模式支持这种服务设计方法。

这种方法也有一些风险,尽管它们更不易被察觉。随着变更的不断发生,实现的边界可能会变得模糊,特别是因为不同的模块通常被捆绑在同一个代码库里。通过跨边界和直接跨模块耦合来快速添加特性实在太容易了。能够快速迭代是一个很大的优势,但这是以高耦合、低内聚和缺少模块化为代价的。

1.4.3 两种选项的利弊

大部分团队会选择第二个选项,即添加功能到现有的服务中。这个选择并没有错,单体架构是很有用且强大的结构,能够为企业带来巨大的价值。

第一个选项两个问题

  • 难以可靠地访问另一个系统的数据,特别是在大规模访问和实时访问的情况下;
  • 创建和管理新的服务会带来很大的开销和风险,特别是在组织没有处理此类事情的既定方法的情况下。

第二种的问题:

跨越实现和业务沟通边界。随着数据、连接数和性能需求的增长,这会变得 越来越难以维护和扩展。

无法在整个公司内正确地传播数据并非由于概念上的根本性缺陷。恰恰相反,这是由于数据沟通结构的弱化或缺失。在前面的场景中,团队的实现沟通结构作为相当有限的数据沟通结构在执行着双重任务。

1.4.4 团队场景(续)

假如采用第二种,第二个选项,在同一服务内加入新特性。这种做法简单便捷,并且从那之后他们已经 实现了很多新特性。随着公司的成长,团队也在壮大,经过一年时间,是时候考虑将原团队拆分为两个更 小、更聚焦的团队了。

  • 哪个团队应该拥有哪些数据?
  • 数据应该存储在哪里?
  • 两个团队都需要修改值的数据怎么办?

1.4.5 冲突的压力

随着团队规模的扩大,需要拆分业务沟通结构------紧跟而来的是要给新的团队重新分配业务需求
这些问题都来自同一个根源:一种在实现沟通结构之间弱化的、不明确的数据通信方式。

1.5 事件驱动的沟通结构

事件驱动方法提供了一种替代传统的实现行为和数据沟通结构的选项。

|-------------------------------------------|
| 基于事件的通信不是"请求--响应"通信的简单替代品,而是服务之间完全不同的通信方式 |

一个事件流的数据沟通结构从数据访问场景中解耦了数据的生产者和所有者。服务之间不再通过一个"请求--响应"API发生联系,而是通过在事件流里定义的事件数据发生联系

1.5.1 事件是通信的基础

所有可共享的数据都被发布到一系列事件流中,构成了一个连续、规范的叙述,详细描述了组织中发生的所有事情。

大部分事物能作为事件进行通信,从简单的某个事情的发生到复杂的、有状态的记录。

事件就是数据。它们不仅仅是表明数据已经准备就绪的信号,也不仅仅是从一个实现向另一个实现直接进行数据传输的方法。相反,它们既是数据存储又是服务间异步通信的一种方式。

1.5.2 事件流提供了单一事实来源

事件流里的每个事件都是事实的一个状态,所有这些状态合起来构成了单一事实来源------这是组织内所有系统相互通信的基础。

1.5.3 消费者执行自己的建模和查询

事件不能提供数据查询和查找功能。所有的业务和应用逻辑必须被封装在事件的生产者和消费者内部。

数据访问和建模需求被完全转移到了消费者一方,每个消费者需要从源事件流中获取他们自己的事件副本。任何查询复杂度也会从数据所有者的实现沟通结构转移到消费者的实现沟通结构。消费者全权负责来自多个事件流的数据混合、特定的查询功能,或者其他特定业务的实现逻辑。

生产者和消费者都不必为了数据通信方式而提供查询机制、数据传输机制、API(应用编程接口)和跨团队的服务

1.5.4 整个组织的数据沟通得到改善

数据沟通结构的使用是一种逆转,所有可共享的数据都暴露在了实现沟通结构外部

不是所有的数据都必须共享,因此也不是所有的数据都需要发布到一系列事件流中

这提供了系统架构中长期缺失的正式的数据沟通结构,并且更好地遵守了"高内聚,低耦合"的界限上下文原则。

新的服务可以简单地从典型事件流中获得所需的数据,创建它们自己的模型和状态,并执行任何必要的业务功能,而无须依赖与其他服务的直接点到点连接或API。这就释放了组织在任何产品中更高效地使用海量数据的潜力,甚至可以以独特而强大的方式整合来自不同产品的数据。

1.5.5 高可访问的数据利于业务变更

|-------------------------------------------------------------------------------------------------------------------------------------|
| 事件流包含对业务操作至关重要的核心领域事件。虽然各团队可能重组,项目也会一个接一个地开展,但是重要的核心领域数据可以随时提供给任何有需要的新产品, 这个能力独立于任何具体的实现沟通结构 。这为业务提供了空前的灵活性,因为访问核心领域事件不再依赖于任何具体的实现。 |

1.6 异步的事件驱动型微服务

事件驱动型微服务支持业务逻辑转换和操作以满足界限上下文的需求。这些应用的任务是满足这些需求并 向其他下游消费者发布任何必要的事件

优点:

  • 颗粒化 服务可以恰当地映射界限上下文,并且在业务需求发生变化时可被轻易重写。
  • 可伸缩性 单独的服务可以根据需要进行扩缩容。
  • 技术灵活性 服务可以使用最合适的语言和技术,也可以使用最前沿的技术进行简单的原型开发。
  • 业务需求灵活性 粒化微服务的所有权很容易进行重组。与大型服务相比,它们有更少的跨团队依赖,并且组织可以快速响应业务需求的变化,否则这些变化会受到数据访问障碍的阻碍。
  • 松耦合 事件驱动型微服务耦合于领域数据而不是特定的实现API。数据schema能极大地改进 管理数据变更的方式,这部分内容会在第3章中详细介绍。
  • 持续交付支持 发布一个小型的、模块化的微服务很容易,并且可在需要时将其回滚。
  • 高可测试性 微服务的依赖比大型单体服务少,因此可以更容易地模拟所需的测试端点并保证适当的代码覆盖率。

使用事件驱动型微服务的示例团队

回到上面的团队使用事件驱动型微服务:

之前所说的技术问题,比如确定从哪里获取数据和如何接收数据、批处理的同步,以及需要实现同步 API 等,在采用事件驱动型微服务后都基本被消除了。

业务风险也得到了缓解,因为小型的、细粒度的服务允许由单一团队来负责

1.7 同步式微服务

微服务可以通过事件(本书所建议的方法)实现成异步的形式,或者实现成同步的形式(同步服务通常出现在面向服务的架构中)。同步的微服务通常采用"请求--响应"的方法来实现,服务间通信直接通过API来满足业务需求。

1.7.1 同步式微服务的缺点

以下是同步的"请求--响应"型微服务的几大缺点。

  • 点到点的耦合
  • 有依赖的可伸缩性
  • 服务故障的处理

如果一个依赖服务关闭,则必须做出如何处理这个异常的决策。生态系统中的微服务越多,决定如何处理停机、何时重试、何时失败以及何时恢复以确保数据一致性就越困难。

  • API版本和依赖管理

多个API定义和服务版本通常需要同时存在。强制让客户端升级到最新的API并不总是可行或可取的。这会增加跨多个服务协调API更改请求的复杂性,特别是当它们伴随着对底层数据结构的更改时。

  • 数据访问耦合于实现
  • 分布式的单体应用

服务可以被组合成一个分布式的单体应用,在它们之间进行许多相互交织的调用。这种情况通常出现在团队分解一个单体应用,并决定使用同步的点到点调用来模拟这个单体应用内部的边界时。点对点的服务可以很容易地模糊界限上下文的边界,因为对远程系统的函数调用可以一行一行地插入现有单体式代码。

  • 测试

集成测试可能很困难,因为每个服务都需要完全可操作的依赖项,而这些依赖项又需要自己的依赖项。

1.7.2 同步式微服务的优点

同步式微服务有许多不可否认的优点。一些数据访问模式很适合直接的"请求--响应"耦合,比如验证用户身份和AB测试中的上报。与外部第三方解决方案的集成大多数时候使用同步机制,并且通常使用HTTP以提供一种灵活的、与语言无关的通信机制。

经验因素也是非常重要的,尤其是当今市场上的许多开发者往往对同步、单体类型的编码更有经验。总的来说,这使得获取熟悉同步系统的人才比获取熟悉异步事件驱动开发的人才更容易

|----------------------------------------------------------------|
| 一个公司的架构很少完全基于事件驱动型微服务。混合式架构肯定会成为常态,同步和异步的解决方案会根据问题空间的需要进行并行部署。 |

  1. 2 事件驱动型微服务基础

事件驱动型微服务是被构建用于满足特定界限上下文的小型应用程序。

事件驱动型微服务是一系列输入事件流的消费者,同时也是另外一些输出事件流的生产者。

这些服务可能是无状态的、有状态的,可能包含同步的"请求--响应" API。

共享从事件代理消费事件或往事件代理生产事件的通用功能。

事件驱动型微服务之间的通信是完全异步的。

事件流由事件代理提供服务。

2.1 构建拓扑

|----------------------------------------------------------------------------------------|
| 拓扑这个术语经常出现在事件驱动型微服务的讨论中,通常用于表示单个微服务的处理逻辑。它也可能用于表示在独立的微服务、事件流和"请求--响应"API 之间的类似图的关系 |

2.1.1 微服务拓扑

微服务拓扑是单个微服务内部的事件驱动拓扑。它定义了对传入事件执行的数据驱动操作,包括转换、存储和发布 。

2.1.2 业务拓扑

业务拓扑是实现复杂业务功能的一系列微服务、事件流和 API。它是服务的任意分组,可以表示由单个团队或部门所拥有的服务,或是那些实现复杂业务功能超集的服务。

|------------------------------------------|
| 微服务拓扑详细描述了单个微服务的内部工作方式,业务拓扑详细描述了服务之间的关系。 |

2.2 事件内容

事件可以是发生在业务沟通结构范围内的任何事情。收到一张发票、预订一间会议室、要一杯咖啡(是的,你可以把咖啡机连到一个事件流上)、雇用一位新员工以及完成任意的代码等。

事件是关于"发生了什么"的记录,非常类似于用应用程序的信息和错误日志。但是,与这些日志不同,事件还是事实的单一来源。因此,它们必须包含准确描述所发生事件所需的所有信息。

2.3 事件的结构

事件通常用键 / 值(key/value)格式表示。值存储着事件的完整细节,键则用于标识目的、路由以及基于相同的键对事件执行的聚合操作。键并不是所有事件类型的必选字段

|------|------------|
| 键 | 值 |
| 唯一ID | 与唯一相关的详细信息 |

2.3.1 无键事件

无键事件用于描述一个单一的事实陈述。举例来说,一个客户与产品发生了交互,比如用户在电子书平台上打开了一本书,这就是一个事件

2.3.2 实体事件

实体是一个唯一的东西,并且使用唯一 ID 作为键。实体事件描述了一个实体,通常是业务上下文中的一个对象,在某个时间节点上的属性和状态。

实体事件在事件驱动架构中相当重要。它们提供了一个实体状态的连续历史,并可用于物化状态。只需要最新的实体事件来确定实体的当前状态。

2.3.3 键控事件

键控事件包含了一个键,但并不表示一个实体。键控事件通常用于划分事件流,以保证在单个事件流分区中的数据局部性。

事件可以按键聚合,这样就可以为每个 ISBN 组成一个用户列表,从而产生以 ISBN 为键的单个实体事件

2.4 物化来自实体事件的状态

通过从实体事件流中按顺序处理实体事件,可以将信息物化成一个有状态的表。

对于一个给定的键,表中表示的就是最新读到的事件。相反,通过将每次更新发布到事件流,可以将表转化成一个实体事件流。这就是所谓的表流二元性(table-stream duality),它是事件驱动型微服务中创建状态的基础。

一个关系型数据库表是通过一系列插入、更新和删除命令来创建和填充的。这些命令可以作为事件生成到不可变日志中,比如一个本地追加文件(如 MySQL 中的二进制日志)或者一个外部事件流。通过回放日志中的所有内容,可以精确地重建表及其所有数据内容。

|-------------------------------------------------------------------------------------------------------------------|
| 这种表流二元性用于在事件驱动型微服务之间传递状态。任何消费者客户端都可以读取键控事件的事件流并物化成自身的本地状态存储。这种简单而强大的模式让微服务可以单纯通过事件共享状态,而无须在生产者服务和消费者服务之间有任何直接的耦合。 |

键控事件的删除是通过生成一个"墓碑"(tombstone)来处理的。墓碑是一个值被设为 null 的键控事件。

除非进行压缩,否则追加的不可变日志可能会无限增长。压缩由事件代理来执行,通过只保留指定键的最新事件以减小其内部日志的大小。

在事件驱动架构中,为业务逻辑维护状态是一种非常常见的模式。几乎可以肯定地说,一个完整的业务模型不可能适应完全无状态的流式领域,因为过去的业务决策将影响你今天做出的决策。

举个具体的例子,如果你从事零售业务,那么你需要知道你的库存水平以确定何时重新订购,进而避免向客户销售不存在的商品。你也希望跟踪应付账款和应收账款。也许你想每周向所有提供了电子邮箱地址的客户推送一次促销活动。所有这些系统都要求你能够将事件流物化为当前状态表示。

2.5 事件数据的定义和 schema

事件数据作为长期的且与实现无关的数据存储方式,也是服务间的通信方式。因此,事件的生产者和消费者对数据的含义有共同的理解是很重要的。这就需要一种用于生产者和消费者之间通信的通用语言,类似于同步的"请求--响应"型服务之间的 API 定义。

2.6 微服务单一写原则

每个事件流有且只有一个生产微服务。这个微服务是生成到流中的每个事件的所有者。这使得可以通过系统追踪数据的流转,而令任何给定的事件都具有权威的事实来源。

2.7 用事件代理赋能微服务

事件代理是每个生产就绪的事件驱动型微服务平台的核心。这是一个接收事件、在队列或分区的事件流中存储事件,并将它们提供给其他进程进行消费的系统。事件通常会基于它们的底层逻辑含义被发布到不同的流中。

多个分布式事件代理在一个集群中协同工作,为事件流的生产和消费提供平台。

  • 可伸缩性

可以添加更多的事件代理实例以提升集群的生产、消费和数据存储能力。

  • 持久性

在节点之间复制事件数据。这使得代理集群在一个代理失败时还可以保持并继续提供数据务。

  • 高可用性

事件代理集群可以在某个代理失败时让该节点的客户端连接到集群的其他节点。这使得客户端可以保持完整的可用时间。

  • 高性能

多个代理节点共享生产和消费负载。此外,每个代理节点必须具有高性能,以便每秒处理数十万次的读写操作。

2.7.1 事件存储和服务

以下是代理对底层数据存储的一些最低要求。

分区

事件流可以被划分为单独的子流,子流的数量根据生产者和消费者的需要而变化。这种划分机制允许一个消费者的多个实例并行处理每个子流,从而实现更大的吞吐量。请注意,队列不需要分区,但出于性能考虑,对它们进行分区可能很有用。

严格有序

事件流分区中的数据是严格有序的,并且其提供给客户端的顺序跟它原来发布到事件流中的顺序完全一样。

不变性

一旦发布,所有事件数据就是完全不可变的。事件发布之后没有任何机制可以对其数据进行修改,只能通过发布一个带有更新数据的新事件来修改之前的数据。

索引

事件在写入事件流时被分配了一个索引。消费者可以用它来管理数据消费,因为它们可以指定从哪个偏移量开始读取数据。消费者的当前索引和尾部索引之差就是消费滞后程度。

无限保留

事件流必须能够在无限长的时间内保留事件。这个属性是维持事件流状态的基础。

可重放性

事件流必须是可重放的,这样任何消费者都可以读取它需要的任何数据。这为单一的事实来源提供了基础,也是微服务之间进行状态通信的基础

2.7.2 需要考虑的其他因素

  1. 支撑工具

支撑工具对于高效开发事件驱动型微服务来说必不可少。其中许多工具绑定在事件代理本身的实现中,包括:

浏览事件和 schema 数据;

限额、访问控制和主题管理;

监控、吞吐量和滞后测量。

有关你可能需要的工具的更多信息,请参阅第 14 章。

  1. 托管服务

托管服务允许你将事件代理的创建和管理外包。

托管解决方案是否存在?

你将购买托管解决方案还是在内部托管?

托管代理商是否提供监控、扩缩容、灾难恢复、备份以及多地区部署能力?

它是否将你与一个特定的服务提供商关联在一起?

是否提供专业的支持服务?

  1. 客户端库和处理框架

可供选择的事件代理很多,每个都有不同级别的客户端支持。因此,你常用的语言和工具能够与事件

代理提供的客户端库很好地协同工作是很重要的。

是否存在所需语言的客户端库和框架?

如果不存在,你是否能够构建相关的库?

你在使用常用的框架还是在尝试自己的框架?

  1. 社区支持

社区支持是选择事件代理的一个非常重要的方面。一个开源的免费项目,比如 Apache Kafka,就是具有大型社区支持的事件代理的一个特别好的例子。

是否有在线社区支持?

这项技术是否成熟并生产就绪?

这项技术是否是许多组织中通用的技术?

这项技术对未来的雇员有吸引力吗?

员工们会为使用这些技术而兴奋吗?

  1. 长期和分层存储

根据事件流的大小和保留时间的长短,最好将较旧的数据段存储在较慢但更便宜的存储中。分层存储提供了多层访问性能,事件代理或其数据服务节点的本地专用磁盘提供了最高的性能层。在这之后的层可以包括专用的大规模存储层服务(例如,Amazon 的 S3、谷歌云存储和 Azure 存储)等选项。

分层存储是否是自动支持的?

是否可以根据使用情况将数据转入较低或更高的层?

无论数据存储在哪一层,是否都能被无缝检索?

2.8 事件代理与消息代理

事件代理是围绕提供有序的事实日志而设计的。事件代理满足消息代理无法满足的两个非常具体的需求。

首先,消息代理只提供消息的队列,消息的消费是基于每个队列进行处理的。共同消费一个队列的应用程序只能接收到记录的一个子集。这就无法通过事件来正确地传递状态,因为每个消费者都无法获得所有事件的完整副本**。与消息代理不同,事件代理维护着一个单独的记录总账,并通过索引管理每个单独的访问,因此每个单独的消费者能够访问所有必需的事件**。

此外,消息代理会在应答之后删除事件,而事件代理会根据组织的需要保留它们。消费后删除事件使得消息代理不足以向所有应用程序提供无限期存储、全局可访问、可重放和单一的事实来源

2.8.1 从不可变日志中消费

通常可用的事件代理使用一种只追加的不可变日志。事件被追加到日志尾部并分配一个自增的索引 ID。数据消费者使用索引 ID 的引用来访问数据。然后,可以依据业务需要和事件代理能够提供的功能,以事件流或队列的形式消费事件 。

  1. 以事件流形式消费
  1. 以队列形式消费

在基于队列的消费中,每个事件仅被一个微服务实例所消费。一旦被消费,该事件就被事件代理标记为"已消费"并不再提供给任何其他消费者。当以队列形式消费时,

消费者数量与分区数量就变得没有耦合性,因为任意数量的消费者实例都能用于消费

不是所有的事件代理都支持队列。例如 Apache Pulsar 目前支持队列,Apache Kafka 则不支持 。

2.8.2 提供单一事实来源

持久和不可变的日志为单一事实来源提供了存储机制,事件代理成了服务消费和生产数据的唯一位置。这样,每个消费者都能获得一份完全相同的数据副本。

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 采用事件代理作为单一事实来源需要组织进行一次文化转变。之前团队可能只需编写直接的 SQL 查询语句来访问单体数据库中的数据,而现在团队还必须把单体数据发布到事件代理上。管理单体数据库的开发者必须确保生成的数据是完全正确的,因为事件流和单体数据库之间的任何不一致都会被认为是生产团队的事故。数据消费者不再直接耦合于单体数据库,而是从事件流中进行消费。 |

采用事件驱动型微服务可以创建只使用事件代理进行存储和访问数据的微服务。虽然微服务的业务逻辑肯定会使用事件的本地副本,但事件代理仍然是所有数据的唯一事实来源 。

2.9 大规模管理微服务

大规模管理微服务会变得越来越困难。每个微服务都需要特定的计算资源、数据存储、配置、环境变量和其他一系列特定于微服务的特性。

2.9.1 将微服务放到容器内

容器(比如最近很流行的 Docker)将应用程序相互隔离。容器通过共享内核模型来使用已有的宿主操作系统。这提供了容器之间的基本分离,而容器本身隔离了环境变量、库和其他依赖项。

2.9.2 将微服务放到虚拟机内

传统的虚拟机为每个实例提供了自包含操作系统和特定虚拟化硬件的完全隔离能力。

  • 安全性比容器更高
  • 每个虚拟机的开销更高,启动时间更长,系统占用空间更大。

2.9.3 管理容器和虚拟机

容器和虚拟机是通过各种专门构建的软件,即所谓的容器管理系统(container management system,CMS)来管理的。

|------------------------------------------------------------|
| Kubernetes、Docker Engine、Mesos Marathon、Amazon ECS 和 Nomad |

2.10 缴纳微服务税

微服务税是与实现微服务架构的工具和组件相关的成本总和,包括财务、人力和机会成本。它包含了管理、部署以及操作事件代理、CMS、部署管道、监控解决方案和日志服务的开销。

  1. 3 通信和数据契约

|-------------------------------------------------------|
| 通信的根本问题是在一个点上精确或近似地再现在另一个点选择的消息。 ------Claude Shannon |

在事件驱动生态系统中,事件就是消息,且是通信的基本单元。一个事件应该尽可能准确地描述发生了什么以及为什么会发生。这是事实的一个状态,当把系统中的所有其他事件结合在一起时,就能提供事实发生的完整历史。

3.1 事件驱动数据契约

要传递的数据的格式和创建数据的逻辑构成了数据契约。

定义良好的数据契约有两个组成部分。第一部分是数据定义,或者说是会产生什么(例如,字段、类型和各种数据结构)。第二部分是触发逻辑,或者说是为什么会产生(例如,触发事件创建的特定业务逻辑)。随着业务需求的发展,可以对数据定义和触发逻辑进行更改。

|------------------------------------------------------------------------------------------------|
| 变更数据定义时必须特别注意,以避免删除或改变正在被下游消费者使用的字段。类似地,改变触发逻辑时也必须很小心。更改数据定义比更改触发逻辑要常见得多,因为改变后者往往会破坏原有事件定义的含义。 |

3.1.1 使用显式 schema 作为契约

实施数据契约并提供一致性的最佳方法是为每个事件都定义一个 schema。生产者定义了一个显式的schema 来详细描述数据定义和触发逻辑,所有相同类型的事件都遵循此格式。

3.1.2 schema 定义的注释

schema 定义中对注释和元数据的支持对于传递事件的含义至关重要。围绕事件的生产和消费的知识应该尽可能接近事件定义。

指定事件的触发逻辑。这通常写在 schema 文件的首部,并且应该清楚地说明生成事件的原因。

给出结构化 schema 中特定字段的上下文和清晰解释。例如,日期时间字段的注释可以说明格式是UTC、ISO 还是 Unix 时间

3.1.3 全能的 schema 演化

schema 格式必须支持一整套 schema 演化规则。schema 演化使生产者能够更新其服务的输出格式,同时允许消费者继续不间断地消费事件。业务变更可能需要添加新字段、弃用旧字段或扩展字段范围。

  • 向前兼容性 允许像读取旧 schema 生成的数据一样读取新 schema 生成的数据。
  • 向后兼容性 允许像读取新 schema 生成的数据一样读取旧 schema 生成的数据。
  • 完全兼容性 将向前兼容性和向后兼容性结合起来,这是最强的保证,并且应该尽可能地使用这种方式。

3.1.4 有代码生成器支持

代码生成器用于将一个事件 schema 转化成指定编程语言的类定义或等价的结构。

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 有代码生成器支持的最大好处是能够根据所选语言的类定义编写应用程序。如果使用的是编译型语言,那 么代码生成器将提供编译器检查,以确保没有错误地处理事件类型或忘记填充任何指定的非空数据字段。 除非遵循 schema 格式,否则不能编译代码,因此如果不遵循 schema 数据定义,你将无法发布应用程 序。编译型语言和非编译型语言都受益于用一个类实现来编写代码。当你试图将错误的类型传递给构造函 数或 setter 时,现代 IDE 会提醒你,而如果你使用一个通用的格式,比如键/值映射对象,则不会收到 任何提醒。减少错误处理数据的风险可以在整个生态系统中提供更加一致的数据质量。 |

3.1.5 破坏性的 schema 变更

有时候,必须以某种方式变更 schema 定义,但这可能导致破坏性的演化。发生这种情况的原因有很多,包括不断变化的业务需求改变了原始域的模型、对原始域的不适当的范围界定,以及定义 schema 时的人为错误。

但数据契约的重新协商和域模型的更改需要所有人

支持。除了重新协商 schema,还需要执行一些额外的步骤来适配新 schema 和从中创建的新事件流。破坏性的 schema 变更对无限期存在的实体往往有相当大的影响,但对在给定时间段后过期的事件影响较小。

  1. 让实体适配破坏性的 schema 变更

  2. 让事件适配破坏性的 schema 变更

3.2 选择事件格式

许多选项可用于格式化和序列化事件数据 ,但数据契约最好使用定义明确的格式(如 Avro、

Thrift 或 Protobuf)来实现。一些最流行的事件代理框架支持序列化和反序列化用这些格式编码的事件。例如,Apache Kafka 和 Apache Pulsar 支持 JSON、Protobuf 和 Avro 的schema 格式 。

|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| 非结构化的纯文本事件通常会成为生产者和消费者的负担,尤其是用例和数据会随着时间的推移而发生变化。如前所述,建议选择支持受控的 schema 演化的强定义、显式的 schema 格式,比如Apache Avro 或 Protobuf。不推荐 JSON,因为它不提供完全兼容的 schema 演化。 |

3.3 设计事件

在创建事件定义时,有许多最佳实践可以遵循,也有一些需要避免的反模式。请记住,随着由事件驱动型微服务支持的架构数量不断增加,事件定义的数量也在不断增加。设计良好的事件将最大限度地减少消费者和生产者在其他方面的重复痛点。

3.3.1 只讲述事实

优秀的事件定义不仅仅是一条表明某件事情发生的消息,而是对在该事件期间发生的所有事情的完整描述。用业务术语来说,这就是在接收输入数据并应用业务逻辑时生成的结果数据。这个输出事件必须被视为唯一的事实来源,并且必须被记录为一个不可变的事实,以供下游消费者消费。

3.3.2 每个流都使用单一事件定义

事件流应该包含表示单一逻辑事件的事件。不建议在一个事件流中混合不同类型的事件,因为这样做会混淆事件的定义和流代表的含义。在这种场景中,由于可能会动态地添加新 schema,因此很难对正在生成的事件进行 schema 验证。

3.3.3 使用最窄的数据类型

为事件数据选用最窄的类型。这可以让你依赖代码生成器、语言类型检查(如果支持的话)和序列化单元测试来检查数据边界。

  • 使用字符串类型来存储数值

这需要消费者解析字符串并将其转换成数值,并且通常会出现像 GPS 坐标那样的奇怪数值。这很容易出错,特别是在发送空值或空字符串时。

  • 将整型作为布尔型使用

虽然 0 和 1 可以分别用来表示假和真,但是 2 表示什么呢?-1 又表示什么呢?

  • 将字符串类型作为枚举型使用

这对于生产者来说是个问题,因为生产者必须确保发布的值与接收的伪枚举列表匹配。这不可避免地会出现输入错误和不正确的值。对这个字段感兴趣的消费者需要知道可能的值的范围,这将需要与生产团队沟通,除非在 schema 注释中指定了这个范围。这两种做法都是隐式定义,因为生产者无法监控字符串中值的范围的任何更改

3.3.4 保持事件的单一用途

一种常见的反模式是向事件定义中添加 type 字段,其中不同的 type 值表示事件的特定子功能。这通常是针对"相似但不同"的数据执行的操作,常常是由于实现者错误地将事件标识为单一用途而造成的结果。

想象一个简单的网站,用户可以在上面读书或看电影。当用户第一次使用此网站时,比如打开书或播放电影,后端服务会将此作为事件(名为 ProductEngagement)发布到事件流中。这个事件的数据结构可能如下所示。

|-----------------------------------------------------------------------------------------------------------------------------------------|
| JavaScript TypeEnum: Book, Movie ActionEnum: Click ProductEngagement { productId: Long, productType: TypeEnum, actionType: ActionEnum } |

现在想象一下,一个新的业务需求来了:你需要跟踪谁在观看电影之前看了电影预告片。图书是没有预览的,虽然布尔值适用于观看电影的场景,但它必须是可以为空的,以适合读书的场景。

|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| JavaScript ProductEngagement { productId: Long, productType: TypeEnum, actionType: ActionEnum, //只应用于productType=Movie watchedPreview: {null, Boolean} } |

在这一点上,watchedPreview 与 Book 没有任何关系,但是无论如何它都被添加到了事件定义中,因为我们已经用这种方式捕获了产品的使用情况。如果你觉得对下游消费者特别有帮助,那么可以在 schema中添加注释,告诉它们该字段只与 type=Movie 相关。

另一个新的业务需求出现了:你需要跟踪在图书中放置了书签的用户,并记录它在哪一页。同样,由于只定义了一个表示产品使用的事件结构,因此你的行为被限制为添加一个新的动作实体(Bookmark)和添加

一个可为空的字段 pageId。

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| JavaScript TypeEnum: Book, Movie ActionEnum: Click, Bookmark ProductEngagement { productId: Long, productType: TypeEnum, actionType: ActionEnum, //只应用于productType=Movie watchedPreview: {null, Boolean}, //只应用于productType=Book, actionType=Bookmark pageId: {null, Int} } |

正如你现在所看到的,业务需求中的一些更改可以极大地使试图服务于多种业务用途的 schema 复杂化。

这增加了数据生产者和消费者的复杂性,因为他们都必须检查数据逻辑的有效性。要收集和表示的数据总是复杂的,但是通过遵循单一责任原则,可以将 schema 分解得更易于管理。下面来看看根据单一职责拆分每个 schema 会是什么样。

|----------------------------------------------------------------------------------------------------------------------------------------|
| JavaScript MovieClick { movieId: Long, watchedPreview: Boolean } BookClick { bookId: Long } BookBookmark { bookId: Long, pageId: Int } |

productType 和 actionType 枚举现在已经不存在了,schema 也相应地扁平了。现在有 3 个事件定义而不是 1 个,虽然 schema 定义数量增加了,但是每个 schema 的内部复杂性大大降低了。按照每个流一个事件定义的建议,你将看到为每个事件类型创建一个新的流。事件定义不会随时间推移而变化,触发逻辑不会改变,消费者可以在单一用途事件定义的稳定性中获得安全。

3.3.5 最小化事件

当事件很小、定义明确且易于处理时,它们会工作得很好。不过,有时候确实会出现"大"事件。

3.3.6 让潜在的消费者参与事件设计

在设计新事件时,让这些数据的所有潜在消费者都参与进来是很重要的。与生产者相比,消费者将更好地理解自己的需求和预期的业务功能,并可能有助于澄清需求。消费者也将更好地了解即将到来的数据。一次联席会议或讨论可以解决两个系统之间有关数据契约的任何问题。

3.3.7 避免将事件作为信号量或信号

避免将事件作为信号量或信号来使用。这些事件只是表明某些事情已经发生,而不是单一事实来源的结果。

考虑一个非常简单的例子。系统输出一个事件,表明某一作业的工作已经完成。虽然事件本身表明工作已完成,但实际的工作结果不包括在事件中。这意味着要正确消费此事件,必须找到已完成工作的实际位置。一旦一段数据有两个事实来源,就会出现一致性问题

  1. 4 将事件驱动架构与现有系统集成

|----------------------------------------------------------------------------------------------|
| 将组织转变到事件驱动架构需要将现有系统集成到新的生态系统中。你的组织可能有一个或多个单体关系型数据库应用程序。如果是从头建立一个事件驱动型微 服务架构,没有历史遗留系统,则本章可以忽略 |

任何业务领域中都存在通常需要跨多个子领域的实体和事件。例如,一个电子商务零售商将需要提供产品 信息、价格、库存和图像到各种界限上下文中。也许付款数据由一个系统收集,但需要在另一个系统中进 行验证,然后在第三个系统中分析购买模式。将这些数据放于一个中心化的位置作为新的单一事实来源, 当数据可用时,每个系统都可以消费它们。迁移到事件驱动型微服务需要在事件代理中提供必要的业务领 域数据,这些数据可以作为事件流使用。这是一个被称为数据解放 (data liberation)的过程,涉及从 现有系统和包含数据的状态存储中 获取 数据。

4.1 什么是数据解放

数据解放是对跨领域数据集的识别并发布到相应事件流中的过程,也是事件驱动架构 迁移策略的一部分。

数据解放强化了事件驱动架构的两个主要特性:单一事实来源和消除系统间的直接耦合。

过充当单一事实来源,这些事件流标准化了跨组织系统访问数据的方式。系统不再需要直接耦合底层的 数据存储和应用程序,而是可以只在事件流的数据契约上进行耦合。

4.1.1 数据解放的折中方案

数据集及其解放的事件流必须保持完全同步,尽管由于事件传播的延迟,此要求仅限于最终一致性。解放的事件流必须能物化成原表的一个精确副本,这个特性被广泛应用于事件驱动型微服务。

在理想的情况下,所有的状态都要从作为单一事实来源的事件流中创建、管理、维护和恢复。所有共享状态都应该 首先 被发布到事件代理中,然后物化到所有需要物化这些状态的服务中

虽然很希望进行重构,但现实中有许多问题会阻止其进行。

  • 缺乏开发人员支持
  • 重构成本
  • 遗留支持风险

有一种折中的方案。可以使用数据解放的模式从数据存储中提取数据并创建必要的事件流。这是一种单向的事件驱动架构形式。

4.1.2 将被解放的数据转化成事件

被解放的数据也要遵循第 3 章中介绍的结构化建议。定义良好的事件流的一个特点是,它所包含的事件有一个显式定义的、可演化的兼容 schema。

4.2 数据解放模式

数据解放模式可用于从底层数据存储中提取数据。因为解放数据意味着要形成新的单一事实来源,所以它必须包含数据存储中的全部数据集合。此外,新的插入、更新和删除操作必须保持数据一致。

  • 基于查询
  • 基于日志
  • 基于表

4.3 数据解放框架

解放数据的一种方法是使用专用、集中式的框架将数据提取到事件流中。捕获事件流的集中式框架包括 Kafka Connect(专门用于 Kafka 平台)、Apache Gobblin 和 Apache NiFi。每个框架都让你可以对底 层数据集执行查询,将结果通过管道传输到输出事件流。这里介绍的每个框架都是可伸缩的,这样就可以 添加更多的实例来增加执行"变更数据捕获"(change data capture,CDC)作业的能力。

4.4 通过查询实施数据解放

基于查询的数据解放涉及查询数据存储并将所选择的结果发布到相关的事件流中。一个使用合适的 API、 SQL 或类 SQL 语言的客户端会被用于向数据存储请求特定的数据集。必须能够批量查询数据集以提供事 件的历史记录,然后定期更新,以确保数据的更改被发布到输出事件流中。

4.4.1 批量加载

执行批量查询并加载数据集中的所有数据。当需要在每个轮询间隔加载整张表时,以及在进行增量更新之 前,都需要执行批量加载。批量加载成本很高,因为它需要从数据存储中获取整个数据集。

4.4.2 增量时间戳加载

使用增量时间戳加载,可以查询并加载自上一个查询结果的最大时间戳以来的所有数据。这种方法使用数 据集中的一个 updated_at 列或字段来跟踪记录最后一次修改的时间。在每次增量更新时,只查询 updated_at 时间戳晚于最后一次处理时间的记录。

4.4.3 自增 ID 加载

增 ID 加载是查询并加载比上一次处理的 ID 值大的所有数据。这种方法通常用于查询存储不可 变记录的表

4.4.4 自定义查询

当客户端只需要较大数据集中的某个数据子集时,或者联结多个表中的数据并对其进行非范式化以避免内部数据模型过度暴露时,通常使用这种方法。例如,用户可以根据 特定的字段过滤业务伙伴的数据,然后将每个合作伙伴的数据发送到自己的事件流。

4.4.5 增量更新

任何增量更新的第一步都是确保数据集中的记录有必需的时间戳或自增 ID。

第二步是确定轮询频率和更新时延。

一旦选定增量更新字段并确定了更新频率,最后一步就是在增量更新启动之前执行一次批量加载。这次批 量加载必须在进一步增量更新之前查询并生成数据集中的所有存量数据。

4.4.6 基于查询更新的优点

  • 可定制性

可以查询任何数据存储,并且所有客户端类型都能用于查询数据。

  • 独立的轮询周期

可以以较高的频率执行某些特定查询以满足更严格的 SLA,而对于其他开销较大的查询可以降低执行 频率以节省资源

  • 内部数据模型的隔离

内部数据模型的隔离 关系型数据库可以通过使用底层数据的视图或物化视图来达到与内部数据模型的隔离。该技术可用来 隐藏不应该暴露在数据存储之外的领域模型信息。

4.4.7 基于查询更新的缺点

  • 需要 updated_at 时间戳
  • 无法跟踪的硬删除
  • 数据集 schema 和输出事件 schema 之间脆弱的依赖关系
  • 间歇捕获 (只能间接捕获)
  • 生产资源消耗
  • 数据变更导致的查询性能变化

查询和返回的数据量取决于对底层数据所做的变更。在最坏的情况下,每次都会更改整个数据集。如果某次查询在下一次查询开始时仍未结束,则会出现竞争状态。

4.5 使用变更数据捕获日志解放数据

解放数据的另一种模式是使用数据存储底层的 变更数据 捕获日志(MySQL 中叫二进制日志,PostgreSQL 中叫 预写式日志)作为信息源。这是一种只追加数据的日志结构,它会详细记录被跟踪的数据集随时间推 移所发生的所有变更。

4.5.1 使用数据存储日志的优点

  • 删除跟踪
  • 对数据存储的性能影响最小
  • 低时延更新

4.5.2 使用数据库日志的缺点

  • 暴露内部数据模型
  • 数据存储外的非范式化

|---------------------------------------------------------------------------------------------------------|
| 变更日志只包含事件数据。一些变更数据捕获机制可以从物化视图中提取数据,但是对大多数情况来说,在数据存储外部必须进行数据的非范式化,否则可能会产生高度范式化的事件流,这就需要下游微服 务处理外键连接和非范式化 |

  • 数据集 schema 和输出事件 schema 之间脆弱的依赖关系

与基于查询的数据解放过程很相似,基于二进制日志的解放过程存在于数据存储应用程序之外。合法 的数据存储变更,比如更改数据集或重新定义字段类型,可能会与事件 schema 的特定演化规则完全不兼容。

4.6 使用发件箱表解放数据

发件箱表包含对数据存储的内部数据所做的重要修改,每个重要的更新都被存储为单独的一行记录。一旦 对标记为需要做变更数据捕获的数据存储表做出插入、更新或删除操作,就会往发件箱表写入一条对应的 记录。处于变更数据捕获下的每个表都有自己的发件箱表,或者有一个单独的发件箱表记录所有表的变更。

内部表的更新和发件箱表的更新必须绑定到 一个事务中让事件流作为单一事实来源

有些数据库,比如 SQL Server,不提供变更数据捕获日志,而是提供变更数据表。这些表通常用于 审计数据库操作,并作为内置选项提供。外部服务,比如前面提到的 Kafka Connect 和 Debezium, 可以连接到使用变更数据捕获表而不是变更数据捕获日志的数据库,然后使用基于查询的模式提取事 件并将其导入事件流。

插入时分配的自增 ID 最适用于确认发布事件的顺序。同时也应该维护一个 created_at 时间戳列,因为 它反映了在数据存储中创建记录的事件时间

4.6.1 性能考虑

使用发件箱表会给数据存储及其请求处理应用程序带来额外的负载。

4.6.2 隔离内部数据模型

发件箱表不需要与内部表成 1∶1 的映射关系。事实上,发件箱表的一个主要优点就是数据存储客户端可 以将内部数据模型与下游消费者隔离。

4.6.3 确保 schema 兼容性

schema 序列化以及校验也可以构建到捕获工作流中。它可以在事件被写入发件箱表之前或之后执行。

在发布之前执行序列化的一个缺点是,由于序列化开销,性能可能会受到影响

|-----------------------------------------------------|
| 事实发生前的序列化提供了比事实发生后的序列化更强大的兼容性保证,并防止了违反其数据 契约的事件的传播。 |

优点

  • 多语言支持   任何拥有事务能力的客户端或框架都支持此方法。
  • 事实发生之前的 schema 操作   在将数据插入发件箱表之前可以进行 schema 校验和序列化。
  • 隔离内部数据模型   数据存储应用程序的开发人员可以选择要将哪些字段写入发件箱表,保持内部字段隔离。
  • 非范式化   数据可以根据需要在写入发件箱表之前进行非范式化。

缺点:

  • 需要变更应用程序代码
  • 影响业务处理性能
  • 影响数据存储性能

4.6.4 使用触发器捕获变更数据

使用触发器的优点如下。

  • 大部分数据库支持   触发器存在于大部分关系型数据库中。
  • 对小数据集来说开销低   对于小数据集,维护和配置都相当容易。
  • 自定义逻辑

触发器代码可以自定义为只暴露部分特定字段。它可以为暴露给下游消费者的数据提供一定程度 的隔离。

缺点:

  • 性能开销
  • 变更管理的复杂性
  • 糟糕的可伸缩性
  • 事后结构化

4.7 对处于捕获的数据集做数据定义变更

在数据解放框架中集成数据定义变更是很困难的。数据迁移对许多关系型数据库应用程序来说是常见操作,并且数据捕获过程需要支持它们。关系型数据库的数据定义变更包括对列的添加、删除和重命名,更改列的类型,以及添加或删除默认值。虽然所有这些数据集变更操作都是合法的,但它们可能会对解放事 件流数据造成问题。

如果要求完全的 schema 演化兼容性,就不能删除处于捕获中的数据集中没有默认值的非空列,因为正在 使用之前所定义的 schema 的消费者期望有这个字段的值。

|--------------------------------------------------------------------------------------------------------|
|   数据定义是对数据集的正式描述。例如,关系型数据库中的一张表是用 数据定义语言 (data definition language,DDL)来定义的。结果表、列、名称、类型和索引都是其数据定义的一部分。 |

4.7.1 为查询和 CDC 日志模式处理事后数据定义变更

对于查询模式,可以在查询时获取 schema,并推断事件 schema。新事件 schema 可以与输出事件流 schema 进行比较,使用 schema 兼容性规则来允许或禁止事件数据的发布。

对于 CDC 日志模式,数据定义更新通常被记录到 CDC 日志中的专有部分。需要从日志中提取这些变更, 并推断其对应数据集的 schema。一旦 schema 生成,就可以对下游事件 schema 进行校验。但是,对该功能的支持是有限的。目前,Debezium 连接器只支持 MySQL 的数据定义变更。

4.7.2 为变更数据表捕获模式处理数据定义变更

变更数据表是输出事件流 schema 和内部状态 schema 之间的桥梁。应用程序校验代码或数据库触发器函数中的任何不兼容都将阻止数据写入变更数据表,并将错误发送回栈。对变更数据捕获表所做的变更需要 有 schema 演化,根据 schema 兼容性规则与输出事件流进行兼容。这种先写入变更数据表再写入事件流 的两阶段处理,将大大减少无意的变更进入生产的机会

4.8 将事件数据落地到数据存储

从事件流中落地数据包括消费事件数据并写入数据存储。这可以通过集中式框架或独立的微服务来实现。 实体事件、键控事件或无键事件等任何类型的事件都可以落地到数据存储。

事件落地的典型应用是替代遗留系统之间直接的点到点耦合。一旦将源系统的数据解放到事件流中,用很 少的改动就能把它落地到目标系统。落地进程是在外部无形地对目标系统进行操作。 需要执行基于批处理的大数据分析团队也经常将数据落地。他们通常将数据放入 Hadoop 分布式文件系统 来实现这一点,Hadoop 分布式文件系统提供了大数据分析工具。 在像 Kafka Connect 这样的通用平台上,可以使用简单的配置指定落地器,并在共享基础设施上运行它 们。独立的微服务落地器提供了另一种解决方案。开发人员可以在微服务平台上创建、运行和独立管理它 们。

4.9 数据落地和获取对业务的影响

集中式框架对解放数据的处理有较低的开销。这种框架可能会在一个团队内大规模使用,然后反过来支持 组织内其他团队的数据解放需求。

集中式框架有两个主要的陷阱。首先,数据获取/落地的责任现在由多个团队分担。操作集中式框架的团队负责框架和每个连接器实例的稳定性、可伸缩性和健康度。。同时,操作处于被捕获状态的系统的团队是 独立的,可以做出改变连接器性能和稳定性的决策,比如添加和删除字段,或者更改会影响通过连接器传 输的数据量的逻辑。这在两个团队之间引入了直接的依赖。这些更改可能会破坏连接器,但只有连接器管 理团队才能检测到,从而导致线性扩展的、跨团队的依赖性。随着变更数量的增加,这可能会成为一个难 以管理的负担。

第二个问题更加普遍,尤其是在一个只部分采用事件驱动原则的组织中。系统可能过于依赖框架和连接器 来完成事件驱动工作。

同时,操作处于被捕获状态的系统的团队是 独立的,可以做出改变连接器性能和稳定性的决策,比如添加和删除字段,或者更改会影响通过连接器传 输的数据量的逻辑。这在两个团队之间引入了直接的依赖。这些更改可能会破坏连接器,但只有连接器管 理团队才能检测到,从而导致线性扩展的、跨团队的依赖性。随着变更数量的增加,这可能会成为一个难 以管理的负担。

|------------------------|
| 减少对 CDC 框架的依赖,传播"事件优先" |

  1. 5 事件驱动处理基础

5.1 构建无状态拓扑

大部分事件驱动型微服务至少遵循以下 3 个步骤。

  1. 从输入事件流中消费事件。

  2. 处理事件。

  3. 生产任何所需的输出事件。

还有一些事件驱动型微服务从同步的"请求--响应"交互中得到它们的输入事件(在后续章节介绍)

processEvent为微服务处理拓扑的入口点。从这里开始,数据驱动 的模式会对数据进行转换并处理以满足界限上下文的业务需求

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| JavaScript Consumer consumerClient = new consumerClient(consumerGroupName, ...); Producer producerClient = new producerClient(...); while(true) { InputEvent event = consumerClient.pollOneEvent(inputEventStream); OutputEvent output = processEvent(event); producerClient.produceEventToStream(outputEventStream, output); //"至少一次"处理 consumerClient.commitOffsets(); } |

5.1.1 转换

转换会对单个事件进行处理并发出 0 个或更多输出事件。

5.1.2 分流与合流

消费者应用程序需要对事件流进行 分流 ,即对事件应用逻辑运算,然后根据结果将其输出到新流。

|--------------------------------------------------------------------------------------------|
| (例如,国家/地区、时区、来源、产品或任何数量 的特性)决定将它们路由到何处。第二种常见场景是将结果发送到不同的输出事件流,比如在处理错误时将事件输出到死信流,而不是完全丢弃它们。 |

应用程序还需要合并流 ,它消费来自多个输入流的事件,可能以某种有意义的方式进行处理,然后输出到 单个输出流。

5.2 对事件流再分区

事件流根据事件的键和事件划分逻辑进行分区。对于每个事件,应用事件分区器,并为要写入的事件选择 一个分区。再分区是生成具有以下一个或多个属性的新事件流的操作

例子:

假设有一个用户数据流,其数据从面向 Web 的终端传入。用户的操作被转换成事件,事件的内容同时包 含用户 ID 和其他任意事件数据,标记为 x。 此状态的消费者希望确保属于特定用户的所有数据都在 同一个分区 中,而不管源事件流是如何分区的。

将给定键的所有事件放入同一个分区中,这样做为确保 数据局部性 提供了基础。消费者只需要消费单个分 区中的事件,就可以构建与该键相关的事件的完整视图。

5.3 对事件流协同分区

协同分区 是将一个事件流重新划分为新的事件流,该事件流具有与另一个流相同的分区数和分区分配逻 辑。当一个事件流中的键控事件需要与另一个流的事件并置时(为了数据局部性),这是必需的。这是有 状态流处理的一个重要概念,因为许多有状态操作(如流联结)要求对给定键的所有事件,无论来自哪个 流,都必须通过同一个节点进行处理。(第七章介绍有状态流)

5.4 给消费者实例分配分区

每个微服务都维护着自己唯一的消费者组,这个消费者组表示其输入事件流的共有偏移量。第一个上线的 消费者实例将使用消费者组的名称在事件代理上进行注册。一旦注册,就需要给消费者实例分配分区。

一些事件代理,比如 Apache Kafka,将分区分配委托给每个消费者组的第一个在线客户端。作为消费者 组的领导者,这个实例负责履行分区指派者的职责,确保每当新实例加入该消费者组时,输入事件流分区 都被正确分配。 其他事件代理,比如 Apache Pulsar,在代理中维护着集中式的分区分配权。在这种情况下,分区分配和 再平衡由代理完成,但通过消费者组进行标识的机制保持不变。分配分区之后,工作可以从最后一个已知 的消费事件偏移量开始。

5.4.1 使用分区分配器分配分区

处理大数据通常需要多个消费者微服务实例,无论是专用流处理框架还是基础的生产者/消费者实现。 分 区分配器 可以确保将分区以均衡和公平的方式分配给处理实例

|-----------------------------------------|
| 根据所选择的事件代理, 此组件可能被构建到消费者客户端中,或者由事件代理维护。 |

5.4.2 分配协同分区

分区分配器还负责确保满足所有协同分区的需求。所有标记为协同分区的分区必须分配给同一个消费者实例。

|----------------------------------------------------|
| 这方面的最佳实践是分 区分配器实现校验机制,可以查看事件流是否具有相等的分区数,并在不相等时抛出异常 |

5.4.3 分区分配策略

  1. 循环分配
  1. 静态分配

当特定的分区必须分配给特定的消费者时,可以使用静态分配协议。当在任意给定实例上物化大量有 状态数据时,此方法非常有用,它通常用于内部状态存储。当消费者实例离开消费者组时,静态分配 器不会重新分配分区,而是会等到丢失的消费者重新上线。

  1. 自定义分配

通过利用外部信号和工具,可以根据客户的需要定制分配策略。例如,可以基于输入事件流中的当前 延迟进行分配,以确保在所有消费者实例中平均分配工作

5.5 从无状态处理实例故障中恢复

从无状态故障中恢复实际上等同于向消费者组添加新实例。无状态处理程序不需要任何状态恢复。这意味着一旦分配了分区并确定了流时间,它们就可以立即恢复处理事件

  1. 6 具有确定性的流处理

主要讨论和解决 3 个问题

  • 当从多个分区消费事件时,微服务如何选择事件处理的顺序
  • 微服务如何处理乱序及迟到事件
  • 对于近实时的方式处理流和从流开始的位置进行处理这两种情况,如果确保微服务产生确定性结果

本章还探讨了乱序和迟到事件是如何发生的、处理它们的策略以及如何减轻它们对工作流的影响。

6.1 事件驱动工作流的确定性

事件驱动的微服务两种主要处理状态:

  1. 近实时(长期运行的微服务)
  1. 历史事件追赶(扩充或新的微服务)

确定性处理的首要目标就是让微服务无论是以实时方式处理事件,还是正在追赶当前事件,都产生相同的输出。

|-------------------------------------------------------------------------------------------------------------------------------|
| 完全确定性的处理是理想的情况,在这种情况下所有事件都无延迟地及时到达,没有生产者和消费者发生 故障,也没有间歇性的网络问题。但现实是微服务只能尽最大努力获得确定性 。 具备条件:一致的时间戳、精心挑选的事件键、分区分配、事件调度以及处理迟到事件的策略 |

6.2 时间戳

事件可以在任何时间、任何地点发生,并且需要与来自其他生产者的事件进行协作。如果要跨分布式系统比较事件,经过同步的一致的时间戳是一个硬性要求。

事件流中的事件既有偏移量又有时间戳。

时间戳类型有:

事件时间

代理摄取时间

消费者摄取时间(可以将其设为事件代理记录中指定的事件时间的值,也可以是挂钟时间 )

处理时间

6.2.1 同步分布式时间戳

一致的时钟时间主要通过与"网络时间协议"(network time protocol,NTP)服务器保持同步来获得。

用时间戳赋值的创建时间和摄取时间可以高度一致,尽管少部分乱序问题仍然会发生。

6.2.2 处理带时间戳的事件

时间戳提供了一种以一致的时间顺序处理分布于多个事件流和分区中的事件的方式。许多情况要求基于时

间来保持事件之间的顺序,并且无论何时处理事件流,都需要一致的、可重复产生的结果。使用偏移量作

为比较方法仅适用于单个事件流分区内的事件,而事件通常需要从多个不同的事件流中进行处理。

6.3 事件调度和确定性处理

确定性处理要求一致地处理事件,这样在将来的某个时间便可以复现结果。事件调度就是当从多个输入分

区消费事件时,选择要处理的下一个事件的过程。对于基于日志且不可变的事件流,事件的消费是以基于

偏移量的顺序进行的。但是,如图 6-2 所示,事件必须基于记录所提供的事件时间进行交错处理以确保

正确的结果,无论这个事件来自哪个输入分区 。

|--------------------------------------------------------------------------------------------------------------------------------------------|
| 最常见的事件调度实现从所有分配的输入分区中选择具有最早时间戳的事件并将其分派到下游 处理拓扑中。 事件调度是许多流处理框架的一个特性,但在基础的消费者实现中通常不存在。你需要确定自己的微服务 是否需要实现它 ,如果消费和处理事件的顺序会影响业务逻辑,那么微服务将需要事件调度。 |

6.3.1 自定义事件调度器

一些流处理框架允许实现自定义的事件调度器。例如,Apache Samza 让你可以实现 MessageChooser

类。在这个类中,可以基于很多因素来选择处理哪个事件,包括事件流之间的优先级、挂钟时间、事件时

间、事件元数据以及事件本身的内容

6.3.2 基于事件时间、处理时间和摄取时间进行处理

选择哪个事件作为时间戳更合适?

为了更准确地描述现实世界中的事件,最好使用本地赋值的事件时间(生产者产生时间时间),前提是它的准确性值得依赖。如果生产者有不可靠的时间戳(并且无法修复),那么次优选择是基于事件被摄取到事件代理中的时间来设置时间戳。只有在事件代理和生产者无法通信的少数情况下,真实事件时间和代理赋值的时间之间才可能

有很大的延迟。

6.3.3 消费者提取时间戳

消费者如果想确定如何对处理的事件排序,就必须知道记录的时间戳。在消费者摄取阶段,要用一个时间

戳提取器从被消费的事件中提取时间戳。这个提取器可以从事件有效载荷的任何部分获取信息,包括键、

值和元数据。提取器会给每个被消费的记录设置一个时间戳,用于表示其"事件时间"。一旦设置了这个时间戳,消费者框架就会在处理期间使用它。

6.3.4 对外部系统的 " 请求 -- 响应 " 调用

在事件驱动拓扑中,对外部系统发起的所有非事件驱动请求都可能导致非确定性结果。

外部系统是在微服务外部进行管理的,这意味着在任意时间点上其内部状态及对请求做出的响应可能是不同的。

6.4 水位

水位(watermark)用于在处理拓扑中跟踪事件时间的进度,并且表明给定事件时间(或早于该时间)的

所有数据都已得到处理。这是在许多主流流处理框架(比如 Apache Spark、Apache Flink、Apache Samza 和 Apache Beam)中常用的技术。

水位是对处于相同处理拓扑中的下游节点的一种声明,它声明处于或早于时间 t 的所有事件都已得到处

理。

并行处理中的水位

比如 aggregate,从多个上游输入中消费事件和水位。节点的事件时间是其所有输入源事件

时间的最小值,节点会在内部保持对这些时间的跟踪。

在这个例子中,一旦来自 groupByKey-1 节点的水位到达,两个 aggregate 节点都会将它们的事件时间从

13 更新到 15 。

6.5 流时间

在流处理程序中维护时间的第二个选项,就是所谓的流时间,它是 Apache Kafka 流所推崇的方法。从一

个或多个事件流中读取数据的消费者应用程序维护着其拓扑的流时间,它是所有已被处理事件的最大时间

戳。

消费者节点基于其接收到的最大事件时间值维护着一个单一的流时

间。这个流时间目前设为 20,因为这是最近一次处理事件的事件时间。下一个要处理的事件是两个输入

缓存中的最小值------在这个例子中是事件时间为 30 的事件。事件向下分发给处理拓扑,然后流时间会被

更新为 30。

拓扑在处理每个事件的过程中会维护对应的流时间,直到开始处理下一个事件。在拓扑包含再分区流的情况下,每个拓扑会分裂为两个子拓扑,而每个子拓扑会维护自己的各不相同的流时间。

一个子拓扑里边只有一个事件得以处理。 使用水位方法的话,每个处理节点的输入会缓存事件,每个节点的事件时间是独立更新的。

并行处理中的流时间

显著的区别是,Kafka Streams 的方法使用所谓的内部事件流将再分区的事件发送回事件代理。然后实例重新消费这个流,在新的流里,所有再分区的数据根据键被重新分配到对应的分区

这在本质上与重量级集群的洗牌机制是一样的,但不需要有专门的集群。

6.6 乱序事件和迟到事件

如果一个事件的时间戳不等于或不大于事件流中位于它前面的事件的时间戳,就被称为是乱序的。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 有边界的数据集,比如进行批处理的历史数据,通常对乱序数据有相当强的弹性。可以把整个批处理想象 成一个大的窗口,如果批处理还没有开始,那么以数分钟甚至数小时偏差乱序到达的事件就不会有真正的 影响。在这种方式下,批处理的有界数据集可以产生高确定性的结果,但这是以高延迟为代价的,特别是 对传统的大数据批处理作业来说,这些作业的结果在 24 小时后才能使用 |

|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 对于无边界的数据集,比如那些持续更新的事件流,开发人员在设计微服务时必须考虑延迟和确定性的要 求。这就从技术需求扩展到了业务需求的范畴,所以任何事件驱动型微服务开发人员都必须问一句:"我 的微服务是否要根据业务需求处理乱序和迟到的事件?"乱序事件要求业务就如何处理它们做出具体决 定,并且确定延迟和确定性的优先级。 |

6.6.1 使用水位和流时间的迟到事件

假设有两个事件,一个事件的时间是 t(将该事件称为事件 t),另一个事件的时间是 t'(将该事件称

为事件 t')。事件 t' 有比事件 t 更早的时间戳。

  • 水位

事件 t' 在水位 W(t) 之后到达会被认为是迟到的。如何处理此事件取决于具体的节点。

流时间

如果事件 t' 在流时间之后到达,而流时间已经递增超过了 t',则 t' 会被认为是迟到的。如何处理此事件取决于拓扑中的每个操作。

|--------------------------------|
| 一个事件只有在晚于某个消费者指定的截止时间到达时才是迟到的。 |

6.6.2 乱序事件的原因和影响

原因

  1. 来源于乱序数据:最明显的是当事件来源于乱序数据时。这种情况会发生在从已经乱序的流中消费数据时或事

件正来自存在乱序时间戳的外部系统时。

  1. 来多个生产者写到多个分区

由于一旦检测到有较大时间戳的事件,流时间就会增加,因此最终可能会出现大量事件因重新排序被

认为是迟到的情况。

6.6.3 时间敏感的函数和窗口化

迟到事件是基于时间的业务逻辑的主要关注点 。窗口函数是基于时间的业务逻辑的一个很好的例子。

窗口化意味着根据时间将事件分组到一起。这对于有相同键的事件特别有用,通过相同键的事件可以看到

在那一时间段内发生了什么。有 3 种主要的事件窗口类型 :

  1. 滚动窗口

滚动窗口是一种固定大小的窗口。前面和后面的窗口不会发生重叠。

  1. 滑动窗口

滑动窗口具有固定的窗口大小和递增的步长(称为窗口滑动量)。它只反映当前窗口内事件的聚合。

图 6-11 展示了滑动窗口的例子,包括窗口大小以及它向前滑动的量。

  1. 会话窗口

会话窗口是一种大小动态变化的窗口。窗口的终止是由超时决定的,而超时的发生是由于会话不活动

了,发生在超时之后的任何激活动作都将启动一个新会话。

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 这里的每个窗口函数都必须处理乱序事件。你必须决定要等待任意一个乱序事件多长时间,才能认为 其太迟而不再考虑。流处理的一个基本问题就是你永远无法确定是否已接收到了所有事件。等待乱序 事件是可以的,但最终你的服务还是需要有放弃动作,因为它不能无限期地等待。其他要考虑的因素 包括存储多少状态、迟到事件的可能性以及丢弃迟到事件对业务的影响。 |

6.7 处理迟到事件

  • 丢弃事件

简单地丢弃事件。窗口一旦关闭,所有基于时间的聚合就已完成。

  • 等待

延迟窗口结果的输出,直到经过了固定的时间之后才进行输出。这是以较高的延迟带来较高的确定

性。旧窗口需要保持可更新状态,直到预先确定的时间过去。

  • 宽限期

一旦窗口被认为完成就输出窗口结果。然后,窗口在预定的宽限期内可用。一旦一个迟到事件到达该

窗口,就会更新聚合并输出更新的结果。这跟等待策略类似,不同的是一旦迟到事件到达就会进行更新。

6.8 再处理与近实时处理

不可变事件流提供了重置消费者组偏移量和从任意时间点重新处理事件的能力。这被称为再处理,每个事

件驱动型微服务都需要在设计时考虑再处理问题。

  1. 确定起始点。作为最佳实践,所有有状态的消费者应该从它们订阅的每个事件流的起点开始再处理事

件。

  1. 确定重置哪些消费者偏移量。所有包含用于有状态处理的流的偏移量都应该被重置到流的起点,因

为如果你从一个错误的位置开始再处理,那么很难确保最终能得到正确的状态。

  1. 考虑数据量。有些微服务可能要处理大量事件。考虑再处理事件需要花费多长时间,以及任何可能存

在的瓶颈。此外,如果预计会产生大量再处理输出数据,则需要通知所有的下游消费者。如果不具备自动伸缩能力,则需要据此扩容服务。

  1. 考虑再处理的时间。再处理可能需要花费很多时间,所以要计算需要多少中断时间。当你的服务进

行再处理时,确保下游消费者也能够应对可能出现的过时数据。增加消费者实例的数量以最大化并行

度可以有效减少中断时间,一旦完成再处理,可以再减少实例数。

  1. 考虑影响。当进行再处理时,有些微服务可能会执行你不想发生的动作。例如,一个服务会在用户的

包裹已发货时向用户发送电子邮件,在再处理事件时不应再向用户发送电子邮件,因为这将是一种糟

糕的用户体验。

6.9 间歇性故障和迟到事件

在近实时处理期间,事件可能会迟到(水位或流时间增加),但在事件流再处理期间,事件流中的该事件

会如期可用。这个问题很难检测,但它确实表明了事件驱动型微服务之间的关联性,以及上游的问题是如

何影响下游消费者的。

6.10 生产者 / 事件代理的连接性问题

6.11 小结与延展阅读

本章首先介绍了确定性以及在无边界流中达到确定性的最佳方法。然后研究了如何在多个分区之间选择要

处理的下一个事件,以确保在近实时处理和再处理时达到最大努力的确定性。无边界事件流加上间歇性故

障的本质意味着永远无法实现完全确定性。合理的、尽力而为的、大部分时间能正常工作的解决方案提供

了延迟和正确性之间的最佳折中。

乱序和迟到事件是设计中必须考虑的因素。本章探讨了如何使用水位和流时间来识别并处理这些事件。

  1. 7 有状态的流

有状态的流是事件驱动型微服务最为重要的组成部分,这是因为大部分应用程序需要为它们的处理需求或

多或少地维护状态。 本章将更深入地介绍事件驱动型微服务要如何构建、管理和使用状态。

7.1 状态存储与从事件流中物化状态

  • 物化状态(materialized state)

来自源事件流的事件投影(不可变)。

  • 状态存储(state store)

存储服务业务状态的地方(可变)。

7.2 记录状态到变更日志事件流

变更日志记录了对状态存储中的数据做的所有变更。它是"表--流"二元性中的"流",状态表被转换成

包含独立事件的流。作为在微服务之外维护的持久性状态副本,变更日志可用于重建状态,并且在事件处

理进程中可以起到检查点的作用。

变更日志流与其他流一样存储在事件代理中,如前所述,它们提供了重建状态存储的方法。变更事件流可

以进行压缩,因为它们只需要用最新的键/值对来重建状态。

7.3 将状态物化至内部状态存储

内部状态存储与微服务业务逻辑共同存在于相同的容器或虚拟机环境中。特别是,内部状态存储的存在依

附于微服务实例的存在,它们都运行在相同的底层硬件上

每个微服务实例从分配给它的分区中物化事件,保持各个分区的数据在存储中的逻辑分离。这些逻辑分离

的物化分区可以让微服务实例在消费者组再平衡之后简单地删除废除分区中的状态。通过保证物化状态只

存在于拥有对应分区的实例上面,避免了资源泄露和多个事实来源的情况。

7.3.1 物化全局状态

全局状态存储是内部状态存储的一种特殊形式。与只物化分配给定的分区不同,全局状态存储能够物化给

定事件流的所有分区的数据,以向每个微服务实例提供事件数据的完全副本。

|--------------------------------------------------------------------------------------------------------------------------------------------|
| 当每个实例都需要一个完整的数据集时,全局状态存储非常有用,并且倾向于包含小的、常用的、很少变 化的数据集。全局物化不能有效地作为事件驱动逻辑的驱动器,因为每个微服务实例都拥有数据的完整副 本,因此会产生重复的输出和不确定的结果。 最好只是将全局物化用于通用数据集查找和维度表。 |

7.3.2 使用内部状态的优点

  1. 开发人员不用考虑可伸缩性要求

使用本地磁盘上的内部状态存储的主要优点是,所有的可伸缩性要求完全转移到了事件代理和计算资

源集群上面。

  1. 基于磁盘的高性能方案

对大多数现代微服务来说,物理连接的本地磁盘性能相当好。本地磁盘实现倾向于使用高随机访问模

式,通常由 SSD 支持。

  1. 使用网络连接磁盘的灵活性

网络连接磁盘的一个主要优点是可以在数据卷中维护状态 ,可以直接回复而不需要重建

7.3.3 使用内部状态的缺点

  1. 仅限于使用运行时定义的磁盘

内部状态存储仅限于使用在服务运行时定义并连接到节点的磁盘。变更所连接数据卷的大小和数量通

常要暂停服务、调整卷,然后重启服务。此外,许多计算资源管理方案只允许增加数据卷的大小,因

为减少卷的大小意味着需要删除数据

  1. 浪费磁盘空间

具有周期性的数据模式,比如下午 3 点到凌晨 3 点对购物网站产生的流量,需要周期性的存储量。

也就是说,这些模式可能需要为峰值流量准备最大容量的磁盘空间,而在其他情况下只需要少量磁盘

空间

7.3.4 内部状态的伸缩和恢复

从状态恢复的角度看,将处理扩展到多个实例和恢复故障实例是相同的过程。新实例或恢复的实例需要物

化其拓扑定义的所有状态,然后才能开始处理新事件。

  1. 使用热副本

当首领副本所在节点终止工作时,消费者组必须再平衡分区的分配。分区分配器会确定热副本的位置

(它之前分配了所有分区,并且知道所有分区到实例的映射)并据此重新分配分区。

  1. 从变更日志中恢复和扩展

当一个新创建的微服务实例加入消费者组时,所有分配给它的有状态分区都可以简单通过消费其变更

日志进行重新加载。在此期间实例不能处理新事件,因为如果这样做会产生不确定和错误的结果

  1. 从输入事件流中恢复和扩展

微服务实例可以从输入流中重建其状态存储。它必须从分配给它的事件流分区的开始位置重新消费所有输入事件。必须以严格递增的顺序消费并处理每个事件,更新其状态,并且产生后续的输出事件。

7.4 将状态物化至外部状态存储

外部状态存储存在于微服务的容器或虚拟机外部,但通常位于同一个本地网络内部

包括关系型数据库,文档型数据库,基于 Lucene 的地理空间搜索系统,以及分布式、高可用的键/值存

储。

|------------------------------------------------------------------------------------------------------------------------------------------------------|
| 虽然一个微服务的外部状态存储可以使用公共的数据存储平台,但是数据集本身应该与所有其他 微服务的实现保持逻辑上的隔离。对想要使用公共的物化数据集来服务多个业务需求的外部数据存储实现 者来说,在微服务之间共享物化状态是一种常见的反模式。这会导致完全不相关的产品和特性之间的强耦 合,应该避免。 |

7.4.1 外部状态的优点

  1. 完全数据局部性

可以查找其他分区的数据

  1. 技术

技术灵活,可以自己选

7.4.2 外部状态的缺点

  1. 管理多种技术
  1. 由于网络延迟造成的性能损耗
  1. 外部状态存储服务的财务成本
  1. 完全数据局部性

外部状态存储中的可用数据来源于多个处理程序和多个分区,每个处理程序和分区都以自己的速率进行处理。这就很难推断和调试任何特定处理实例对共享状态的贡献。

7.4.3 外部状态存储的伸缩和恢复

  1. 从源流重建
  1. 使用变更日志
  1. 创建快照

对于外部状态存储,更常见的是由它们提供自身的备份和恢复处理机制,并且许多托管的状态存储服

务会提供简单的"一键式"解决方案

7.5 重建与迁移状态存储

对现有状态存储数据结构的更改通常需要伴随新的业务需求。微服务可能需要向现有事件添加新信息,与

另一个物化表执行一些额外的联结步骤,或者存储新派生的业务数据。在这种情况下,需要通过重建或迁

移来更新现有的状态存储以反映数据。

7.5.1 重建

重建微服务的状态存储通常是更新应用程序内部状态的最常见方法。微服务首先要停机,然后将消费者输

入流偏移量重置为开始位置。重建必须删除所有中间状态,比如存储在变更日志中或位于外部状态存储中

的状态。最后,启动新版本的微服务,从输入事件流中读取事件并重建状态。该方法确保了完全按新业务

逻辑指定的方式构建状态。同时也会创建所有新的输出事件并向下游传播给订阅消费者。这些事件不被视

为重复事件,因为业务逻辑和输出格式已经更改,而且这些更改必须传播到下游。

7.5.2 迁移

与变更所带来的影响相比,大型状态存储需要很长时间来重建,或者会导致高昂的数据传输成本。假设发

生了一个业务需求变更,其中一个附加(但可选)的字段将被添加到微服务的输出事件流中。这个变更可

能需要往微服务的状态存储中添加一个列或字段。但是,这个业务可能不需要重新处理旧数据,只需将逻

辑应用于未来的新输入事件。对于由关系型数据库支持的状态存储,你只需要更新与相关表定义有关系的

业务逻辑。可以执行一次简单的新列插入,使其允许接受为空的默认值,并且在一系列快速测试之后,重

新部署应用程序。

7.6 事务与有效一次处理

有效一次处理确保了对单一事实来源的任何更新都始终得到应用,无论生产者、消费者或事件代理发生任

何故障。虽然不是很准确,但有效一次处理有时也被描述为精确一次处理。

幂等写入是事件代理实现(比如 Apache Kafka 和 Apache Pulsar)中普遍支持的功能之一。它们允许一

个事件只被写入事件流一次。如果生产者或事件代理在写入时发生故障,幂等写入功能会确保在重试时不

创建重复的事件。

事件代理也可以支持事务特性。目前,只有 Apache Kafka 提供了完整的事务支持

7.6.1 示例:库存计算服务

7.6.2 使用 " 客户端 -- 代理 " 事务的有效一次处理

任何支持事务的事件代理都能帮助达成有效一次处理。使用这种方法,任何输出事件、由变更日志支持

的内部状态的更新以及消费者偏移量的递增都被包裹在单个原子事务中。只有当这 3 个更新都存储在代

理中各自的特定事件流中时,才有可能实现这一点。

如果生产者在事务执行期间遭遇致命性异常,通过从变更日志恢复,可以简单地重建其

替代实例。事件流的消费者组偏移量也可以被重置到偏移量事件流中最后已知的处理位置。

7.6.3 没有 " 客户端 -- 代理 " 事务的有效一次处理

  1. 产生重复事件

如果生产者已成功地将事件写入事件流,但是没能接收到写入确认且进行重试 ,或者在更新其消费者

偏移量之前崩溃,则会产生重复事件

  1. 识别重复事件

事件流中存在重复事件(具有唯一的偏移量和唯一的时间戳),那就需要你来消除它们的影响。首先,确定重复事件是否真的会导致任何问题。在许多情况下,重复事件的影响很小,甚至可以忽略不计。对于那些确实会因重复事件而导致问题的场景,需要指出如何识别它们。一种方式就是让生产者为每个事件生成一个唯一 ID ,这样所有重复事件都会生成相同的唯一哈希函数

  1. 防范重复事件

完美的去重要求每个消费者无限期地维护对已处理的各个去重 ID 的查找,但如果想防范的范围过

大,那么时间和空间成本可能会变得非常高昂。实际上,去重通常只会对特定的滚动时间窗口或偏移

窗口执行,以尽最大努力达成目的。

  1. 保持一致状态

微服务可以利用其状态存储的事务能力而不是事件代理能力来执行有效一次处理。这就要求将消费者

组的偏移量管理从事件代理转移到数据服务中,让一个有状态的事务原子地更新状态和输入偏移量。

对状态所做的任何更改都与对消费者偏移量所做的更改完全一致,这保持了服务内部的一致性。

此方法让处理程序可以有效一次处理,而不是有效一次生产事件。该服务生成的所有事件都只

是达到了至少一次生产的要求。

7.7 小结

本章介绍了内部状态存储和外部状态存储,它们如何工作,它们的优点和缺点,以及何时使用它们。数据

局部性在系统的延迟和吞吐量方面起着很大的作用

变更日志在备份和恢复微服务的状态存储方面扮演着重要角色,尽管该角色还可以由支持事务的关系型数

据库以及定期保存的快照来扮演。支持事务的事件代理可以实现非常强大的有效一次处理,从而减轻消费

者防止重复的职责,而去重工作可以使没有此类支持的系统实现有效一次处理

  1. 8 用微服务构建工作流

微服务只在整个组织的业务工作流的一小部分上运行。工作流是组成业务流程的一组特定操

作,包括所有逻辑分支和补偿操作。工作流通常需要多个微服务,每个微服务有自己的界限上下文,执行

自己的任务并向下游消费者发出新的事件。目前为止我们看到的大部分内容是在介绍单个微服务如何在底

层运作。下面来看看多个微服务如何协同工作以实现更大的业务工作流,以及一些由事件驱动型微服务方

法引起的陷阱和问题。

两种主要的工作流模式,即编排(choreography)和编制(orchestration)

8.1 编排模式

术语编排架构(也称为响应式架构)通常指高度解耦的微服务架构。微服务以完全独立于上游生产者和

下游消费者的形式对到达的输入事件做出响应

8.1.1 一个简单的事件驱动编排示例

简单的事件驱动编排工作流的业务变更

8.1.2 创建和修改编排的工作流

虽然编排模式可以很方便地在工作流尾部添加新步骤,但是如果在工作流中间插入步骤或者调整工作流顺

序会存在问题。

8.1.3 监控编排的工作流

孤立地看,分布式的编排工作流会导致难以识别特定事件的处理进度。对于事件驱动系统,监控业务核心工作流可能需要监听每个输出事件流并将其物化到状态存储,以掌握事件无法得到处理或在处理过程中遇到困难的情况。

8.2 编制模式

编制模式中,一个中央微服务,即编制器,向下级工作者微服务发出命令并等待其响应。

|-------------------------------------------------------------------|
| 编制器等待来自受指挥的微服务的响应,并根据工作流逻辑处理响应结果。这与编排的工作流 形成鲜明的对比,编排的工作流中没有集中的协调。 |

|--------------------------------------------------------------------|
| 确保编制器的界限上下文仅限于工作流逻辑且包含最小的业务实现逻辑。编制器只包含工作流 逻辑,受指挥的微服务则包含大量业务逻辑。 |

8.2.1 一个简单的事件驱动编制模式例子

8.2.2 一个简单的直接调用的编制模式例子

编制也可以使用"请求--响应"模式:

|----------------------|
| 该模式特别适用于实现 FaaS 解决方案 |

8.2.3 对比事件驱动编制模式和直接调用的编制模式

  • 事件驱动工作流:

可以使用与其他事件驱动型微服务相同的 I/O 监视工具和延迟扩展功能;

仍然允许事件流被其他服务消费,包括那些在编制之外的服务;

通常更健壮,因为编制器和从属服务都通过事件代理与彼此的间歇性故障隔离;

对于故障有内置的重试机制,因为事件可以保留在事件流中用于重试。

  • 直接调用的工作流:

通常更快,因为没有生产事件到事件流和从事件流消费的开销;

存在可能需要由编制器管理的间歇性连接问题。

8.2.4 创建和修改编制工作流

通过物化每个进来和出去的事件流以及"响应--请求"结果,编制器可以跟踪工作流中的事件。因为工作

流本身只在编制器服务中定义,所以修改工作流只需在单一点上进行变更。在许多情况下,可以不中断部

分事件而将更改应用于工作流。

编制模式导致了服务之间的紧耦合。必须明确定义编制器服务和依赖的工作者服务之间的关系。

8.2.5 监控编制工作流

可以通过查询物化状态来观察工作流,所以可以很容易地查看任何特定事件的进度以及工作流中可能出现

的任何问题。可以在编制器级别实现监视和日志记录,以检测导致工作流失败的任何事件。

8.3 分布式事务

分布式事务是跨越一个或多个微服务的事务。每个微服务负责处理其事务部分,以及在事务被中止和回滚

的情况下回退该处理。实现和撤销逻辑都必须驻留在同一个微服务中,这既是出于可维护性的目的,也是

为了确保在启动新事务时一定要具备回滚事务的能力。

|--------------------------------------------------------------------------------|
| 最好尽量避免实现分布式事务,因为它们会给工作流增加显著的风险和复杂度。你必须考虑一 系列问题,比如系统间的同步工作、促使回滚、管理实例的瞬时故障、网络连接等 |

在事件驱动世界中的分布式事务通常被称为saga,可以通过编排模式或编制模式来实现。saga 模式要求

参与到工作流中的微服务能够执行还原操作,以恢复其执行的事务

8.3.1 编排型事务: saga 模式

编排的 saga 模式适用于简单的分布式事务,特别是那些具有很强的工作流顺序需求的事务,并且这些顺

序不太可能发生变动。

8.3.2 编制型事务

编制型事务构建在编制器模型上,在其上面增加了可以从工作流中的任意一点回退事务的逻辑。可以通过

反转工作流逻辑并确保每个工作者微服务都能提供互补的反转操作来回滚这些事务

编制型事务还可以支持各种信号,比如超时和人工输入。可以使用超时来定期检查本地物化状态,以查看

事务处理了多长时间。通过 REST API(参见第 13 章)的人工输入可以作为取消指令与其他事件一起被

处理。编制器的中心化特性有利于近距离地监控任何给定事务的进度和状态

事务回滚后,由编制器决定下一步要做什么来完成对该事件的处理。它可能会多次重试事件、放弃事件、

终止应用程序或输出失败事件。作为单一的生产者,编制器发送事务失败的事件到输出流,让下游消费者

处理它。这与编排模式不同,在遵循单一写原则的情况下,编排模式无法做到通过单一的流消费所有输出

事件。

比起编排型事务,编制型事务提供了对工作流依赖更好的可见性、更灵活的变更和更清晰的监控。编制器

实例会增加工作流的开销并且其自身也需要管理,但它可以为复杂的工作流提供编排型事务无法提供的清

晰结构。

8.4 补偿工作流

并非所有的工作流都需要完全可逆并受事务约束。工作流中可能会出现许多不可预见的问题,在很多情况

下,你可能需要尽最大努力来保障它的完成。如果出现失败,那么可以事后采取行动来补救。

8.5 小结

编排模式带来了业务单元和独立工作流之间的松散耦合。它适用于简单的分布式事务和简单的非事务型工

作流,其中微服务数量比较少且业务操作的顺序不太可能改变。

相比于编排模式,编制模式对工作流有更好的可视化和监控能力。与编排模式相比,编制模式可以处理更

复杂的分布式事务,并且通常只需要在一个地方进行修改。易变更且包含许多独立微服务的工作流非常适

合编制模式。

最后,不是所有的系统都需要分布式事务来保证成功的操作。一些工作流可以在失败时提供补偿动作,依

靠业务的非技术部分来解决面向客户的问题。

  1. 9 使用 " 函数即服务 " 的微服务

" 函 数 即 服 务 " ( function-as-a-service , FaaS ) 是 近 些 年 越 来 越 流 行 的 一 种 " 无 服 务 端 "

(serverless)解决方案。FaaS 解决方案使个人能够构建、管理、部署和扩展应用程序功能,而无须管

理基础设施开销。

函数是当某些触发条件发生时执行的一串代码。函数启动、运行直到完成,然后在工作完成后终止。FaaS

解决方案可以轻松地根据负载上下调整函数执行的数量,密切跟踪高度可变的负载。

9.1 设计基于函数的微服务解决方案

FaaS 解决方案包含了许多不同的函数,它们的操作总和构成了业务界限上下文的解决方案。 有很多种,本节主要介绍通用设计原则

9.1.1 确保界限上下文的严格的成员关系

构成解决方案的函数及内部事件流必须严格从属于某个界限上下文,这样函数和数据的所有者就相当明确

了。虽然许多微服务解决方案会 1∶1 地映射到界限上下文,但 n∶1 的映射并不少见,因为许多函数可能用于同一个界限上下文中。确定哪个函数属于哪个界限上下文是很重要的,因为函数的高度颗粒化可能会模糊这些界限

确保数据存储是私有的,与外部上下文隔离;

当要耦合其他上下文时,使用标准的"请求--响应"或事件驱动接口;

围绕"哪些函数属于哪些界限上下文"(函数对产品 1∶1 地映射)维护严格的元数据;

在映射到界限上下文的代码仓库中维护函数代码

9.1.2 只在完成处理之后提交偏移量

偏移量会在函数启动时或函数完成处理时发生提交。对于给定的一个事件或一批事件,只在处理完成之后

提交偏移量是 FaaS 的最佳实践 。

9.1.3 少即是多

FaaS 框架的一个经常被推崇的特性是,它们可以很容易地编写一个函数并在多个服务中复用它。

FaaS 解决方案可以组合多个函数来解决界限上下文的业务需求,虽然这不是一个罕见或不好的做法,但

较好的经验法则是,少量函数比许多细粒度函数更好。只对一个函数进行测试、调试和管理比对多个函数

做同样的事情容易得多。

9.2 选择 FaaS 供应商

与事件代理和 CMS 一样,FaaS 框架可以选择免费的开源解决方案或者付费的第三方云供应商。

使用自己的私有 CMS 进行微服务操作的组织,也可以从私有的 FaaS 解决方案中受益。有许多免费开源的选项,

比如 OpenWhisk、OpenFaaS 和 Kubeless,它们可以使用已有的容器管理服务。Apache Pulsar 有自己内

置的 FaaS 解决方案,可以与其事件代理配合运行。通过利用公共资源调配框架,你的 FaaS 解决方案可

以与微服务解决方案保持一致。

Amazon Web Services(AWS)、Google Cloud Platform(GCP)和 Microsoft Azure 等第三方服务供应

商也有自己的专有 FaaS 框架,每个框架都提供了吸引人的特性和功能,但仍然与其提供的专有事件代理

紧密集成。这是一个严重的问题,因为当前这 3 家供应商都将其事件代理内的保留期限制为 7 天。云供

应商和开源事件代理之间的集成方案虽然存在(比如 Kafka Connect),但可能需要额外的工作来搭建和

管理。也就是说,如果你的组织已经使用了 AWS、GCP 和 Azure 的服务,那么开启这个体验的开销就很

小 。

9.3 在函数之外构建微服务

基于函数的解决方案需要4个元素:

  • 函数
  • 输入事件流
  • 触发逻辑
  • 带元数据的错误处理和伸缩策略

FaaS 实现的第一个组件是函数本身:

|-------------------------------------------------------------------------------------------------------|
| Java public int myfunction(Event[] events, Context context) { println ("hello world!"); return 0; } |

events 参数包含了要处理的事件数组,每个事件包含 key、value、timestamp、offset 和 partition_id

等字段。context 参数包含了关于函数及其上下文的信息,比如名字、事件流 ID 和函数的剩余寿命。

然后需要为函数写一些触发逻辑:

|---------------------------------------------------|
| 策略和元数据可能包含: 消费者组 消费者属性,比如批量大小和批量窗口 重试和错误处理策略 伸缩策略 |

一旦建立了触发器、元数据和策略,函数就会准备处理到来的事件。当新的事件到达输入事件流时,FaaS

框架会启动函数,向其传递一批事件,然后函数开始进行处理。一旦处理完成,函数会终止操作并等待更

多事件到来。这是一个典型的事件流监听器模式的实现

9.4 冷启动和热启动

冷启动是函数在第一次启动或在足够长的不活动时间后启动时的默认状态。此时容器必须启动并加载代

码、创建事件代理连接,并且建立所有跟外部资源的客户端连接。

一旦所有元素都准备就绪,函数就处于一个"热"的状态并准备处理事件。"热"函数开始处理事件,在到期或完成处理时暂停并进入挂起状态。

9.5 用触发器启动函数

触发器用于通知函数启动并开始处理。不同的 FaaS 框架支持的触发器会有所不同,但通常都有相同的分类。现在来看看有哪些信号可以用来启动函数,以及分别在什么时候可能需要使用它们。

9.5.1 基于新事件触发:事件流监听器

当事件被发布到事件流中时可以触发函数。事件流监听触发器用预先定义好的消费者屏蔽了事件消费逻辑,这减少了开发者必须编写的代码量。

来自谷歌、微软和亚马逊的 FaaS 解决方案为其专用事件代理提供了此类触发器

相反,开源解决方案(如 OpenFaaS、Kubeless、Nuclio 等)为各种事件代理(如 Kafka、Pulsar 和

NATS)提供了各种触发插件。例如,Apache Kafka Connect 允许你触发第三方 FaaS 框架的函数。由于

Kafka Connect 运行在 FaaS 框架之外,因此最终只会作为一个事件流监听器的角色而存在,如图 9-2

所示。

同步触发器在函数处理完一批事件之后才会发出下一批事件。这对于保证处理顺序是相当重要的,并且受

限于被处理的事件流的并行度。相反,异步触发可以发送多个事件到多个函数,每个函数在完成处理之后

返回。但是,这将无法保证处理的顺序,并且只能用于处理顺序对业务逻辑不重要的场景。

在流监听触发器中,批量大小和批量窗口是需要考虑的两个重要属性。批量大小表明了发送给函数处理

的事件的最大数量,批量窗口表明了等待额外事件的最长时间,而不是立即触发函数

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Java public int myEventfunction(Event[] events, Context context) { for(Event event: events) try { println (event.key + ", " + event.value); } catch (Exception e) { println ("error printing " + event.toString); } //向FaaS框架表明批处理已经完成 context.success(); return 0; } |

9.5.2 基于消费者组的滞后度触发

消费者组的滞后度量是另一种触发函数的方法。

滞后度监控通常涉及计算并向所选择的监控框架上报滞后度量值。

然后监控框架可以调用 FaaS 框架以通知其启动注册在该事件流上的函数。高滞后度值表明要启动多个函数实例来更快速地处理负载,而低滞后度值只需要一个函数实例来处理积压

与之前提到的事件流监听触发器最大的不同是,用滞后度触发的函数直到启动之后才开始消费事件。滞后

度触发器启动的函数有更广的职责范围,包括建立与事件代理的客户端连接、消费事件和提交偏移量更

新。

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Java public int myLagConsumerfunction(Context context) { String consumerGroup = context.consumerGroup; String streamName = context.streamName; EventBrokerClient client = new EventBrokerClient(consumerGroup, ...); Event[] events = client.consumeBatch(streamName, ...); for(Event event: events) { //执行事件处理操作 doWork(event); } //将偏移量提交回事件代理 client.commitOffsets(); //向FaaS框架表明函数已成功执行 context.success(); //返回,让滞后度触发系统知道函数已成功执行 return 0; } |

9.5.3 按调度表触发

函数可以被安排成定期启动或者在特定日期时间启动。调度好的函数按指定的时间间隔启动,轮询源事件流中的新事件,并根据需要进行处理或关闭

9.5.4 使用网络钩子触发

函数也可以通过直接调用触发,允许与监控框架、调度程序和其他第三方应用程序进行自定义集成

9.5.5 触发资源事件

更改资源也可能是一种触发源。例如,创建、更新或删除文件系统里的一个文件可以触发函数,对数据存

储中的一行记录进行更新也可以触发函数。

9.6 用函数执行业务工作

FaaS 方法尤其适用于需要灵活地按需进行资源调配的解决方案:

FaaS 的水平伸缩能力和按需特性可以快速调配和释放计算资源。

9.7 维持状态

由于函数的寿命很短,因此大多数基于 FaaS 的有状态的解决方案需要使用外部有状态的服务。部分原因

是许多 FaaS 提供商的目标是提供快速、高度可扩展的处理能力单元,而不依赖于数据的位置。如果函数

要获得之前操作得到的本地状态,那么当前操作就只能限定在与本地状态相同的节点上执行。这大大降低

了 FaaS 供应商的灵活性,因此它们通常强制执行"无本地状态"策略

|------------------------------------------------------------------------------------------------------------|
| 一些 FaaS 框架已经添加了持久的有状态的函数支持,比如 Microsoft Azure 的 Durable Functions, 它抽象了显式的状态管理并允许你使用本地内存,该内存会自动持久化到外部状态存储上 |

9.8 调用其他函数的函数

函数通常用于执行其他函数,也可以用于编排和编制工作流。函数之间的通信可以通过事件、"请求--响

应"调用或二者相结合异步地进行

9.8.1 事件驱动通信模式

可以将函数的输出作为事件生产到事件流中供其他函数消费。一个界限上下文可能由许多函数和内部事件

流组成,每个函数定义了不同的触发和伸缩逻辑

函数 A 独立于函数 B 和函数 C 的触发器而触发。事件流 2 和事件流 3 被认为是内部事件流,界限上下文之外的所有函数都无法访问它们的内容。每个函数都使用相同的消费者组从其源流消费事件,因为这些函数全都处于相同的界限上下文

9.8.2 直接调用模式

在直接调用模式中,一个函数可以通过其代码直接调用其他函数。对其他函数的直接调用可以是异步的

  1. 编排模式与异步的函数调用

异步直接调用的一个主要缺点是难以确保仅在事件得到成功处理时更新消费者偏移量。在这个例子

中,因为函数 B 不会返回结果给函数 A,所以函数 B 发生错误时不会阻止工作流提交错误的消费者

组偏移量 。

  1. 编制模式与同步的函数调用

同步的函数调用允许调用其他函数并等待结果,然后再处理剩下的业务逻辑。

9.9 终止和关闭

一旦函数完成其工作,或者存活期限到期(通常配置为 5~10 分钟),就会终止执行。函数实例被挂起并

进入休眠状态,在该状态下实例是可以立即恢复的。由于资源或时间限制,挂起的函数最终也可能被彻底

清除。

如果一个函数一直在线且正在处理事件,那么关闭连接和再平衡消费者组是没有必要的。该函数可能只会

在其存活期限结束时暂时挂起,短暂进入休眠状态,并立即恢复到运行状态

对于仅间歇运行的消费者函数,最好关闭所有连接并放弃事件流分区的分配。下一个函数实例必须重新创建连接,无论其是冷启动还是热启动。

9.10 调整函数

每个函数都有基于其工作量的具体需求。优化函数在执行期间使用的资源可以确保在保持低成本的同时保

持高性能。在建立资源和调整函数的参数时,有一些事情需要考虑

9.10.1 分配足够的资源

每个函数可以被分配一定量的 CPU 和内存资源。重要的是要根据函数需求来调整这些参数:过度分配会

使成本变高,过少分配则可能导致函数崩溃或耗时太久。

最大执行时间是另一个要考虑的因素,因为它限制了函数可能运行的时长

还必须考虑 FaaS 解决方案的界限上下文中对状态存储的所有外部 I/O。函数的工作负载随输入事

件流的变化而变化,有些工作负载需要对外部状态进行持续的 I/O 操作,而其他工作负载只需要零星的

I/O。未能提供足够的 I/O 资源会导致吞吐量和性能降低。

9.10.2 批量事件处理的参数

如果函数无法在其执行时间期限内处理分配给它的批量事件,那么函数的执行就被认为是失败的并且必须

重新进行批处理。

增加函数的最大执行时间;

减小函数要处理的事件的最大批处理大小

9.11 FaaS 的伸缩方案

FaaS 解决方案为工作的并行化提供了卓越的能力,特别是对于数据处理顺序不重要的队列和事件流。对

于分区的事件流,如果事件的顺序确实很重要,那么并行化的最大程度会受限于事件流的分区数,就如所

有微服务实现那样

伸缩策略通常由 FaaS 框架提供,请检查你的框架文档确认其提供了哪些选项。常见的选项基于消费者滞

后度、一天中的特定时间段、处理吞吐量和性能指标等维度进行伸缩

对于自己负责实例化和管理事件代理连接的函数,请注意当消费者进入或离开消费者组时分区分配再平衡

的影响。如果消费者频繁地加入或离开消费者组,那么消费者组最终可能会处于一种近乎持续的再平衡状

态,从而无法取得处理进展。在极端情况下,其可能会陷入再平衡的虚拟死锁中,函数在其生命周期中反

复分配和删除分区。

静态分区分配消除了动态分配的消费者组的再平衡开销,也可用于协同分区事件流。函数将预先知道它们

会从哪些分区进行消费。不需要再平衡,只要触发该函数,就可以简单地消费事件

9.12 小结

FaaS 属于云计算的其中一个领域并且正处在高速发展阶段。许多 FaaS 框架提供了多种函数开发、管

理、部署、触发、测试和伸缩工具,让你可以使用函数构建自己的微服务。可以通过事件流中的新事件、

消费者组滞后状态、挂钟时间和自定义逻辑触发函数。

  1. 10 基础的生产者和消费者微服务

基础的生产者和消费者(basic producer and consumer,BPC)微服务从一个或多个事件流中摄取事件,

应用必要的转换逻辑或业务逻辑,然后向输出事件流发出必要的事件。

BPC 微服务的特征是使用了基础的消费者客户端和生产者客户端。基础的消费者客户端不涉及任何事件调

度、水位、物化机制、变更日志或水平伸缩带本地状态存储的处理实例。

10.1 BPC 的适用场合

尽管缺少全能型框架组件的大部分能力,但 BPC 微服务可以满足广泛的业务需求。BPC 既能轻松实现像

无状态转换这样的简单模式,也能实现不需要确定性事件调度的有状态模式。

在 BPC 实现中,外部状态存储比内部状态存储更常被使用,因为在没有采用全能型流框架的情况下,在

多实例之间伸缩本地状态和从实例故障中恢复是很困难的。外部状态存储可以为多个微服务实例提供统一

的访问以及数据备份和恢复机制。

BPC 用例:

10.1.1 集成现有遗留系统

通过将生产者/消费者客户端集成到代码库中,遗留系统可以参与到事件驱动架构中来。这种集成通常在

采用事件驱动型微服务的早期阶段就开始 。

在某些场景中,想安全地修改遗留系统的代码库以让其具有生产和消费事件的能力是很困难的。边车

(sidecar)模式特别适用于解决此类问题,因为它无须影响源代码库就能获得一些事件驱动能力。

示例:

电子商务商店有一个前端界面,用于显示其所包含的所有库存和产品数据。以前,前端服务通过使用定时

批处理作业从只读的从属数据存储中同步所有数据

现在有了两个事件流,一个包含产品信息,另一个包含产品库存水平。你可以使用"边车"来将数据落地

到数据存储中,BPC 在其中消费事件并将它们插入相关的数据集中。前端服务无须更改任何系统代码就可

获得近乎实时的产品更新数据。

边车驻留于自己的容器内,但必须是前端服务的单独可部署部分。你必须执行额外的测试以确保集成的边

车按预期运行。边车模式使得你无须对遗留代码库做出重大变更就能向系统添加新功能。

10.1.2 不依赖于事件顺序的有状态的业务逻辑

许多业务流程对事件到达的顺序没有任何要求,但是所有必要的事件最终都要到达。这被称为浇注模式

(gating pattern),而 BPC 实现很适用于该模式。

示例:图书出版

出版的前提条件:

内容:必须已经编写完了图书内容。

封面:必须设计好了图书的封面。

定价:必须根据地区和格式设置好了价格

10.1.3 当数据层完成大部分工作时

事件处理的复杂性完全转移到了底层数据层,生产者和消费者组件仅仅简单实现了集成机制。

以一家电子商务公司为例,其接收从网站上爬取下来的新产品并使用 BPC 微服务进行分类,而它的后端数据层是经过批量训练的机器学习分类程序。

10.1.4 处理层和数据层独立伸缩

微服务的处理需求和数据存储需求并不总是线性相关的。例如,微服务必须处理的事件量可能随时间而变

示例:对事件数据进行聚合以了解用户参与情况

一旦 24 小时聚合会话完成,就会从数据存储中刷出结果数据并发送到输出事件流,从而释放数据存储空间

服务的处理需求会随着使用产品的用户的睡眠/清醒周期而变化。在晚上,当大多数用户处于睡眠状态时,执行聚合操作所需要的处理能力与白天相比要小得多。为了节省处理能力方面的费用,要在夜间对服务进行缩容 。

白天,可以上线更多的处理实例以处理增加的事件负载。在此场景中,数据存储的查询率也会增加,但是

缓存、分区和批处理等有助于将负载保持在低于跟随处理需求线性增长所要求的值

10.2 具有外部流处理的混合 BPC 应用程序

BPC 微服务还可以利用外部流处理系统来完成本地可能难以完成的工作。这是一种混合的应用程序模式,

业务逻辑分布在 BPC 和外部流处理框架之间。

示例:使用外部流处理框架来联结事件流

假设 BPC 服务需要使用流处理框架的联结能力(流处理框架擅长于联结物化事件流的大型集合)。外部

流处理器可以简单地将事件流物化到表,并将具有相同键的行联结到一起

混合的 BPC 需要使用兼容的客户端来启用在外部事件流处理框架上的工作。这个客户端会把代码转换成

框架的指令,框架本身会处理消费、联结并生成事件到联结的输出事件流中。

这种模式的主要优点是,它解锁了你的服务原本不具备的流处理特性。这些特性是否可用受限于相应流处

理客户端的语言,并非任何语言都支持所有功能。这种模式经常与轻量级和重量级框架一起使用

此种模式的主要缺点与复杂性的增加有关。测试应用程序会变得异常复杂,因为你还必须找到将外部流处

理框架集成到测试环境中的方法。

最后,处理微服务的界限上下文可能会变得更加复杂,因为你要确保

可以轻易地管理混合应用程序的部署、回滚和操作

10.3 小结

BPC 模式简单而强大。它是许多无状态和有状态的事件驱动型微服务模式的基础。你可以使用 BPC 模式

轻易地实现无状态的流和简单的有状态的应用程序。

  1. 11 使用重量级框架的微服务

重量级流框架

定义:

  • 独立集群资源:需要独立的处理资源集群来执行操作,通常由多个工作者节点和主节点组成。
  • 内部机制丰富:具备自己的失败处理、恢复、资源分配、任务分配、数据存储、通信及协调机制。

特点:

  • 依赖集群服务:如Apache ZooKeeper用于高可用性和协调集群领导者选举(虽然不是绝对必要)。
  • 全面的功能集:这些框架不仅处理数据流,还承担了原本可能由CMS(集群管理服务)和事件代理完成的任务,例如资源管理、失败恢复和系统伸缩等。
  • 复杂性较高:管理和维护额外的集群框架增加了复杂度,尤其是相对于轻量级框架而言。
  • 演进趋势:一些重量级框架正在向更类似于轻量级框架的执行模型发展,以更好地集成到现有的微服务架构中。

例子:

  • Apache Spark
  • Apache Flink
  • Apache Storm
  • Apache Heron
  • Apache Beam

与轻量级框架(第12章介绍)对比:

  • 简化集成:轻量级框架更易于与现有的CMS和事件代理集成,减少了独立管理和维护的负担。
  • 侧重于特定功能:不像重量级框架那样提供广泛的内置功能,而是更多依赖外部服务来进行资源管理、失败恢复等功能。

11.1 重量级框架的简单历史

重量级流处理框架的起源与发展

  1. 批处理时代的奠基者:Apache Hadoop
  • 发布与影响:Hadoop于2006年发布,成为开源大数据技术的先驱,提供了海量并行处理、失败恢复、数据持久性和内部节点通信等功能。
  • MapReduce:作为最早的批量数据处理方法之一,虽然功能强大但执行速度较慢,适用于处理超大批量数据(大数据)。

数据规模的增长与需求变化

  • 数据增长:随着数据集从GB级别扩展到TB甚至PB级别,对更快处理速度、更强能力以及近实时流处理的需求日益增加。
  • 性能瓶颈:传统的基于批处理的MapReduce作业无法满足现代数据处理的速度和灵活性要

这就是 Spark、Flink、Storm、Heron 和 Beam 发挥作用的地方。开发这些解决方案是为了处理数据流, 并比基于批处理的 MapReduce 作业更快地提供可操作的结果。

11.2 重量级框架的内部运作

重量级流处理集群是一组专用的处理和存储资源:分为两个主要角色:

主节点,它对工作者节点上的执行者和任务进行优先级排序、分配和管理。

执行者,它使用工作者节点可用的处 理能力、内存、本地和远程磁盘来完成这些任务。在事件驱动处理中,这些任务会连接到事件代理并从事 件流中消费事件。

|----------------------------------------------------------------------------------------------|
| ZooKeeper 在历史上一直是提供分布式重量级框架协调能力的主要组件。新的框架可能使用也可能不使用 ZooKeeper。无论哪种情况,分布式协调对于可靠运行分布式负载都是至关重要的 |

作业 是用框架的软件开发包(SDK)构建的流处理拓扑,其作用是解决具体界限上下文的问题。它在集群中持续运行,在事件到达时处理事件。

作业被提交到集群之后,其所定义的流处理拓扑被分解为任务并分配给工作者节点。任务管理器会监控任务并确保它们得到完成。当发生故障时,任务管理器会挑选一个可用的执行者重新开启工作。

11.3 优点和局限性

重量级框架的优点

  1. 强大的分析能力:
  • 近乎实时的事件分析:这些框架能够快速处理大量事件,支持快速决策。
  • 丰富的使用模式:
  • ETL操作:抽取、转换并加载数据到新的存储中。
  • 基于会话和窗口的分析:提供灵活的时间窗口机制进行数据分析。
  • 异常行为检测:识别不寻常的行为模式。
  • 状态维持与聚合:支持流数据的状态管理和复杂聚合操作。
  • 无状态流操作:执行各种类型的简单流处理任务。
  1. 成熟度与社区支持:
  • 广泛的采用:许多组织在使用这些框架,并积极贡献源代码。
  • 丰富的资源:有大量的书籍、博客文章、文档和支持材料,以及示例应用程序可供参考。

重量级框架的局限性

  1. 部署复杂性:
  • 专用资源集群需求:需要独立的资源集群,而不仅仅是事件代理和CMS,增加了管理复杂性和成本。
  • 微服务适配问题:最初设计并未考虑微服务架构,使得它们不适合直接用于微服务部署。
  1. 语言限制:
  • JVM依赖:大部分框架基于JVM,限制了开发语言的选择。解决方法是将重量级框架作为独立应用运行,通过状态存储与其他语言的应用程序交互。
  1. 物化流的支持不足:
  • 有限的永久表支持:并非所有框架都支持将流物化为永久表,这影响了创建复杂的联结操作(如表联结或流-表联结)的能力。
  • 文档不足:即使支持流物化和联结,相关特性的文档化程度较低,导致这些功能不如其他特性那样被广泛理解和利用。
  1. 重点偏向时间序列分析:
  • 宣传和资源聚焦:大部分文档和社区讨论集中在基于时间的聚合上,而对全局窗口和其他高级特性(如自定义联结)的关注较少。

11.4 集群搭建方案和执行模式

11.4.1 使用托管服务

例如,亚马逊提供了托管 Flink 和 Spark 服务;谷歌、Databricks 和微软提供了它们自己的 Spark 捆绑服务;谷歌提供了 Dataflow(它 自己实现的 Apache Beam 的运行器)。

11.4.2 构建自己的完整集群

一个重量级的框架可能有自己独立于 CMS 的专用可伸缩资源集群。这种部署是重量级集群的历史常态, 因为它密切模拟了原始的 Hadoop。当重量级框架被需要大量(成百上千个)工作者节点的服务使用时, 这是很常见的

11.4.3 使用 CMS 集成来创建集群

  1. 使用 CMS 部署和运行集群

使用 CMS 部署重量级集群有许多优点。主节点、工作者节点和 ZooKeeper(如果适用的话)在它们 自己的容器或虚拟机中启动。

  1. 使用 CMS 为单个作业指定资源

Spark 和 Flink 使你能够直接利用 Kubernetes 进行可伸缩的应用程序部署,而不仅仅是其原来的专用集群配置,其中每个应用程序都有自己的专用工作者节点集。例如,Apache Flink 允许应用程 序使用 Kubernetes 在它们自己的独立会话集群中独立运行。Apache Spark 提供了类似的选项,允 许 Kubernetes 扮演主节点的角色,并为每个应用程序维护隔离的工作资源。

这种部署模式有以下优点:

  • 利用了 CMS 的资源获取模型,包括伸缩需求;
  • 作业之间是完全隔离的;
  • 可以使用不同的框架和版本;
  • 重量级流应用程序可以像微服务一样处理,并且使用相同的部署流程。

当然,这种模式也有一些缺点:

不是所有的主流重量级流框架都支持此模式;

不是所有主流的 CMS 都支持这种集成;

可能还无法支持如自动伸缩这种在完全的集群模式中可用的特性

11.5 应用程序提交模式

将应用程序提交到重量级集群进行处理有两种主要方式:驱动器模式和集群模式。

11.5.1 驱动器模式

  • 定义:在驱动器模式下,虽然驱动器(Driver)在集群资源内运行,但它本质上是一个独立的本地程序,负责协调和执行用户应用程序。
  • 功能:
  • 协调与进度跟踪:驱动器协调集群资源以确保应用程序按预期进度执行。
  • 错误报告与日志记录:可以用于报告错误、执行日志记录和其他管理任务。
  • 生命周期管理:驱动器的终止会导致整个应用程序的终止,提供了一种简单的方式部署和终止流应用程序。
  • 微服务集成:可以使用集群管理服务(CMS)将驱动器作为微服务部署,并从重量级集群中获取工作者资源。这意味着驱动器可以像其他微服务一样启动和停止。

11.5.2 集群模式

  • 定义:在集群模式下,整个应用程序被提交到集群进行管理和执行,集群返回一个唯一的ID来标识该应用程序。
  • 功能:
  • 唯一ID:这个ID用于识别应用程序并通过集群API向其发出命令(如启动、停止等)。
  • 直接通信:命令必须直接与集群通信以部署和停止应用程序,这可能不适合传统的微服务部署管道,因为需要额外的集群管理接口。
  • 默认模式:这是Storm和Heron作业的默认部署模式。

11.6 处理状态和使用检查点

可以使用内部状态存储或外部状态存储(参见第 7 章)来持久化有状态的操作,尽管为了高性能和高可 伸缩性,大部分重量级框架倾向于内部状态存储。有状态的记录保存在内存中以供快速访问,但是当状态 增长到超出可用内存时,出于数据持久化目的,状态也会溢出到磁盘上。

检查点: 是应用程序当前内部状态的快照,可在伸缩或节点故障发生之后用于重建状态。为了防止数据丢 失,可以将检查点持久化到应用程序工作者节点外部的持久性存储器上。保存检查点可以使用所有与框架 兼容的存储方案,比如 Hadoop 分布式文件系统(HDFS,一种通用选项)或者高可用的外部数据存储。然后,每个分区状态存储可以从检查点进行恢复,这在应用程序发生故障的情况下提供了完全恢复功能,在 伸缩和工作者节点发生故障的情况下提供了部分恢复功能。

|------------------------------------------|
|  恢复检查点状态在功能上等同于使用快照恢复外部状态存储,如 7.4.3 节所述。 |

11.7 伸缩应用程序和处理事件流分区

因为重量级的处理框架特别适合计算大量用户生成的数据,所以在白天看到具有大量计 算需求夜晚却很少的循环模式是很常见的。

|------------------------------------------------------------|
| 伸缩有状态的应用程序有两种主要策略,虽然具体的策略因技术而异,但它们有一个共同的目标,即最小 化应用程序的中断时间。 |

11.7.1 伸缩运行中的应用程序

第一个策略允许你移除、添加或重新分配应用程序实例,而不会停止应用程序或影响处理的准确性。

Spark 的动态资源分配实现了这种伸缩策略。但是,它需要使用粗粒度模式进行集群部署,并使用 外部洗 牌服务 (external shuffle service,ESS)作为隔离层。

停止后:

11.7.2 通过重启伸缩应用程序

第二个策略是通过重启对应用程序进行伸缩,所有的重量级流框架都支持该策略。流的消费会暂停,应用 程序会创建检查点,然后停止程序。接下来,使用新的资源和并行度重新初始化应用程序,并根据需要从 检查点重新加载有状态数据。例如,Flink 提供了一个简单的 REST 机制来实现这个功能,而 Storm 提 供了自己的再平衡命令。

11.7.3 自动伸缩应用程序

自动伸缩是根据特定指标自动伸缩应用程序的过程。这些指标可能包括处理延迟、消费者滞后度、内存使 用率和 CPU 使用率等。一些框架有自建的自动伸缩选项,比如谷歌的 Dataflow 引擎、Heron 的健康管 理器和 Spark Streaming 的动态分配功能。

11.8 从故障中恢复

如果工作者节点发生故障,那么在该节点上正在运行的任务就会被转移到其他可用的节点上。任何所需的 内部状态都会从最近的检查点与分区分配一起重新加载。主节点故障对于已经执行的应用程序应该是透明的,但是根据集群的配置,在主节点中断期间,你可能无法部署新作业。

11.9 考虑多租户问题

随着集群中应用程序数量的增长,除了集群管理的开销外,还必须考虑多租户问题,特别是资源获取的优先级、备用资源与已提交资源的比率,以及应用程序声明资源的速率。例如,新的流应用程序可能会大量占用空闲集群资源,影响现有应用的服务级别目标(SLO),导致业务问题。

缓解方法

  1. 运行多个小型集群:
  • 隔离性:每个团队或业务单元拥有独立集群,完全隔离。
  • 自动化:通过编程方式请求资源以降低操作开销。
  • 缺点:较高的财务成本,由于协调节点和监控/管理的额外开销。
  1. 命名空间:
  • 资源划分:在单个集群内划分多个命名空间,每个团队有自己的专用资源。
  • 防止资源饥饿:限制应用程序只能获取自己命名空间内的资源,避免其他应用资源不足。
  • 缺点:可能导致更大的空闲资源碎片,即使不需要也必须分配备用资源。

这两种方法各有优劣,选择时需权衡操作复杂性和成本效益。

11.10 语言和语法

  • 重量级流处理框架植根于其前身的 JVM 语言中,Java 是最常用的,其次是 Scala。Python 也是一种常 见的语言
  • 类 SQL 的语言也越来越普遍。这使得可以用 SQL 转换来表示拓扑,并且减少了学习新框架的特定 API 的认知成本。Spark、Flink、Storm 和 Beam 都提供了类 SQL 语言

11.11 选择一个框架

选择重量级流处理框架与选择 CMS 和事件代理非常类似。你必须确定你的组织愿意批准多少运营开销, 以及其是否足以支持大规模运行完整的生产集群。这种开销包括常规的操作任务,比如监控、伸缩、故障 排除、调试和分配成本,所有这些都是实现和部署实际应用程序所必需的。

11.12 小结

本章介绍了重量级的流处理框架,包括它们的发展简史以及它们在解决问题过程中存在的一些问题。这些 系统具有高度可伸缩性,使你可以根据各种分析模式来处理流。

  1. 12 使用轻量级框架的微服务

轻量级框架提供了与重量级框架相似的功能,但是是通过大量使用事件代理和 CMS 的方式来实现的

12.1 优点和局限性

将流物化到表,加上简单的开箱即用的联结功能,使得处理流以及其中的相关数据变得相当简单。

12.2 轻量级处理

单个实例根据拓扑处理事件,事件代理提供了实例间的通信层,以实现超越单个实例的可伸缩性。

轻量级框架利用事件代理提供通信路径,这说明了轻量级应用程序与事件代理的深度集成。重量级框架则 明显不同,它要求在节点之间直接进行大量协调。当与 CMS 提供的应用程序管理方案相结合时,轻量级 框架比重量级框架更符合现代微服务所需的应用程序部署和管理的特征。

12.3 处理状态和使用变更日志

轻量级框架的默认操作模式是使用事件代理中存储的变更日志所支持的内部状态。使用内部状态使得每个微服务可以通过部署配置来控制它所获取的资源。

12.4 伸缩和故障恢复

轻量级框架模型的一个主要优点是应用程序可以在执行过程中动态地伸缩。尽管由于消费者组的再平衡和对变更日志中状态的重新物化,处理过程中可能会有延迟,但更改并行度无须再重启应用程序

看伸缩轻量级应用程序时主要考虑的因素:

12.4.1 事件洗牌

是将事件再分区到内部事件流中以供下游消费

12.4.2 状态分配

分配了新状态的实例必须先从变更日志中加载数据,之后才能处理新的事件。这类似于重 量级解决方案中从持久性存储器里加载检查点的过程。所有事件流分区(包括输入流和内部流)的算子状 态(映射为 存储在每个应用程序的消费者组中。键控状态( 对)存储在应用程序的每个状态存储的变更日志中。

12.4.3 状态复制和热副本

热副本是从变更日志中物化的状态存储的副本。当提供数据服务的主实例出现故障 时,它提供了切换到备用实例的能力,同时也可以对有状态的应用程序进行柔性缩容。

12.5 选择一个轻量级框架

目前,有两个主要的适用于轻量级框架模型的方案,它们都需要使用 Apache Kafka 事件代理:

12.5.1 Apache Kafka Streams

Kafka Streams 是一个嵌入到某个应用程序中的功能强大的流处理库,其中输入事件和输出事件存储在 Kafka 集群中。

12.5.2 Apache Samza :嵌入模式

Samza 的嵌入模式使你可以在单个应用程序中嵌入它的功能。这个部署模式不需要依赖于专门的重量级集群,而是依赖于前面讨论过的轻量级框架模型。默认情况下,Samza 依赖 于使用 Apache ZooKeeper 来进行跨单独实例的协调,但是可以将其修改为使用其他的协调机制(如 Kubernetes)。

|------------------------------------------|
| Apache Samza 的嵌入模式可能不会提供它在集群模式下所拥有的所有功能。 |

12.6 语言和语法

  • Kafka Streams 和 Samza 都是基于 Java 的,这使得它们只能用于基于 JVM 的语言。其高级 API 是一 种 MapReduce 语法形式
  • Apache Samza 支持开箱即用的类 SQL 语言,尽管这个功能目前仅限于简单的无状态查询。虽然 Kafka Streams 的企业赞助商 Confluent 在自己的社区许可证下提供了 KSQL,但是 Kafka Streams 并没有支 持自己的 SQL。

12.7 小结

本章介绍了轻量级流处理框架,包括它们的主要优点和折中点。它们是高度可伸缩的处理框架,重度依赖 于与事件代理的集成来执行大规模数据处理。与 CMS 的高度集成为每个单独的微服务提供了可伸缩性。

  1. 13 集成事件驱动型和 " 请求 - 响应 " 型微服务

尽管事件驱动型微服务模式功能强大,但它们不能满足组织的所有业务需求。

"请求--响应"型服务是指相互之间直接通信(通常是通过同步的 API)的服 务

  • 收集来自外部源的度量值,比如用户手机上或物联网(Internet of Things,IoT)设备上的应用程 序;
  • 集成已有的"请求--响应"应用程序,特别是组织外部的第三方应用程序;
  • 为 Web 和移动设备用户提供实时内容;
  • 基于实时信息(比如位置、时间和天气等)发起动态请求。 事件驱动的模式仍然在领域中扮演着重要的角色,而在其中集成"

13.1 处理外部事件

由于历史、优先级、熟悉程度、便利性以及大量其他原因,外部事件主要通过"请求--响应"API 从外部 发送。

两种主要的外部生成事件类型:

13.1.1 自动生成的事件

第一种类型的事件是通过你的产品由客户端发送给服务端自动产生的事件。这些请求通常被定义为产品的 度量值或测量值,比如关于用户正在做什么的信息、活动的定期测量或某种类型的传感器读数。这些事件统称为分析事件,它们描述了有关产品操作的测量和事实陈述。

任何来自外部产品且基于源自该产品的操作的请求,都算作外部生成的事件。

13.1.2 由响应生成的事件

第二种类型的外部生成事件是响应式事件,它是响应来自某个服务的请求而生成的。你的服务构造了一个 请求,将其发送到对端并等待响应。在某些情况下,唯一重要的事是确保请求被接收,而请求客户端无须 从响应中获得任何其他信息。如果你需要发出请求以发送广告电子邮件,那么从处理请求的第三方服务收 集响应并变成事件可能并没有什么用处。一旦请求成功发出(HTTP 202 响应),你就可以假设第三方电 子邮件应用程序会实现该请求。

13.2 处理自动生成的分析事件

|---------------------------------------------------------------------------------------------------------------------|
| 当在客户端生成事件时,要对事件进行 schema 编码。这确保了一个高保真的来源,减少了下 游消费者的误解,同时为生产者提供了创建和填充事件的详细要求。 结构化的事件对于大规模消费分析事件是很关键的。schema 明确了收集的内容 |

13.3 集成第三方 " 请求 响应 "API

向外部服务发起请求给工作流带来了不确定性因素。 重新处理事件,即使只是重新处理一次失败的批处理,也可能会得到与原始处理过程不一样的结果 。在设计应用程序时一定要考虑这个问题。如果"请求--响应"端点是由组织外部的第三方控制的,那么对 API 或响应格式的更改可能导致微服务失败。

最后,请考虑向对端发送请求的频率。假设你发现微服务中存在一个缺陷,现在需要重置输入流以进行再处理。事件驱动型微服务通常会尽可能快地执行代码以消费和处理事件,这样做可能会导致发送到外部 API 的请求激增。

13.4 处理并提供有状态的数据

创建提供"请求--响应"端点的事件驱动型微服务,这种微服务可以提供对状态的随机访问。

13.4.1 实时请求内部状态存储

事实上,当存在大量实例时,大部分请求第一次无法命中实例,需要进行一次重定向,从而增加响应的延 迟和应用程序的负载。

13.4.2 实时请求外部状态存储

与请求内部状态存储方法相比,请求外部状态存储有两个优点。

第一,所有状态对每个实例来说都是可用 的,这意味着请求不需要按照内部状态存储模型转发到承载数据的微服务实例。

第二,消费者组再平衡也 不需要微服务在新实例上再物化内部状态,因为所有状态都维护在实例外部。

  1. 通过物化事件驱动型微服务来提供请求能力
  1. 通过单独的微服务来提供请求能力

13.5 在事件驱动的工作流中处理请求

处理用户界面事件

在将用户输入作为事件流处理时,需要解决许多问题。将请求作为事件处理的应用程序设计必须包含异步 UI。还必须确保应用程序的行为能管理好用户的预期。

13.6 " 请求 响应 " 应用程序中的微前端

13.7 微前端的优点

微前端模式与事件驱动型微服务后端非常匹配,并继承了它们的许多优点,比如:

  • 模块化
  • 业务关注点分离
  • 自治团队以及部署
  • 语言和代码库的独立性

13.7.1 基于组合的微服务

微前端是一种可组合的模式,意味着你可以根据需要将前端服务添加到现有的 UI。值得注意的是,微前 端与事件驱动的后端可以很好地配合,后者本质上也是基于组合的

13.7.2 容易与业务需求对齐

通过将微前端严格地与业务界限上下文对齐(就像处理在后端运行的其他微服务一样),可以直接跟踪特 定的业务需求到它们的实现

13.8 微前端的缺点

比如一致的 UI 元素和对每个元素布局的完全控制。微前端还继承了所有微服务所共有的一些问 题,比如可能出现重复代码以及管理和部署微服务的操作问题

13.8.1 可能不一致的 UI 元素和样式

13.8.2 不同的微前端性能

13.9 小结

本章涵盖了事件驱动型微服务与"请求--响应"API 进行集成的内容。外部系统主要通过"请求--响应" API 进行通信,这些请求可能是人或设备发起的,微服务需要将 API 的请求和响应转换为事件。设备发 起的请求会提前结构化,通过"请求--响应"API 发往服务器端进行事件收集。调用第三方 API 通常需 要将其响应包装成自己的事件,这种事件会由于第三方变更的发生而变得脆弱

  1. 14 支持性工具

支持性工具使你能够高效地管理大规模的事件驱动型微服务。虽然这些工具中的大多数可以由管理员执行的命令行界面提供,但最好拥有一系列自助服务工具。

14.1 ** 微服务** - 团队分配系统

当一个公司拥有少量系统时,很容易使用部落知识或非正式方法来跟踪谁拥有哪些系统。在微服务领域, 明确跟踪微服务实现和事件流的所有权是很重要的。

可以内部开发一个简单的微服务来跟踪并管理人员、团队和微服务之间的所有依赖关系。

14.2 事件流的创建和修改

团队需要有创建事件流和修改它们的能力。微服务应该有权自动创建自己的内部事件流,并完全控制像分区数、保留策略和复制因子这样的重要属性。

14.3 事件流元数据标记

一种分配所有权的有用技术是用元数据标记流。只有拥有流生产权限的团队才能添加、修改或删除元数据 标记。有用的元数据示例包括但不限于以下这些:

  • 流所有者(服务)
  • 个人可识别信息(personally identifiable information,PII)(这类信息需要更严格的安全保障措施,因为通过它可以直接或间接地识别用户)
  • 财务信息: 任何与金钱、账单或其他重要的创收活动有关的信息
  • 命名空间 (与业务的嵌套界限上下文结构对齐的描述符)
  • 弃用
  • 自定义标记 (其他任何适合你的业务的元数据都可以而且应该使用元数据标记进行跟踪)

14.4 限额

限额通常由事件代理在全局层面进行设置。例如,可以将事件代理设置为只允许将 20% 的 CPU 处理时间 用于为单个生产者或消费者组提供服务。限额可以防止突发的拒绝服务(由于突然活跃的生产者或高并发 的消费者组从大数据量的事件流开始位置消费)。

14.5 schema 注册表

显式 schema 为事件建模提供了强大的框架。数据的精确定义,包括名称、类型、默认值和文档,为事件 的生产者和消费者提供了清晰的信息。schema 注册表是一种服务,它允许生产者注册他们用来编写事件 的 schema。这提供了几个明显的好处:

  • 事件 schema 不需要与事件一起传输。可以使用简单的占位符 ID,显著减少带宽的使用;
  • schema 注册表为获取事件的 schema 提供了唯一参考;
  • schema 支持数据发现,特别是全文本搜索。

|----------------------------------------------------------------------------------------------|
| Confluent 为 Apache Kafka 提供了一个出色的 schema 注册表实现。它支持 Apache Avro、Protobuf 和 JSON 格式,并可免费用于生产环境 |

14.6 schema 创建和修改通知

事件流 schema 在标准化通信方面是很重要的。有一个问题会出现,特别是在有大量事件流的时候,那就 是如果通知其他团队他们所依赖的 schema 发生了变化.

通知系统的目的只是在消费者的输入 schema 发生变化时提醒消费者。访问控制列表(ACL,将在本章后 面讨论)是确定哪个微服务会消费哪个事件流以及依赖于哪些 schema 的一种很好的方法。

14.7 偏移量管理

事件驱动型微服务要求在处理数据之前管理偏移量。在正常操作中,微服务将在处理消息时前移其消费者 偏移量。但是,在某些情况下,必须手动调整偏移量。

应用程序重置:重置偏移量(变更微服务)

应用程序重置:前移偏移量(务不需要旧数据,只消费最新的数据)

应用程序恢复:指定偏移量

对于生产级 DevOps,团队必须是微服务的拥有者才能修改其偏移量,这是微服务--团队分配系统提供的 功能。

14.8 事件流的权限和访问控制列表

对数据的访问控制不仅从业务安全的角度来看是重要的,而且也是实施单一写原则的一种手段。权限和访问控制列表(ACL)确保了界限上下文可以强制限定它们的边界。对给定事件流的访问权限只能由拥有生 产微服务的团队授予,你可以使用微服务--团队分配系统来实施这一限制。权限通常分为以下几类(当 然,这取决于事件代理的实现):读、写、创建、删除、修改和描述。

14.9 状态管理和应用程序重置

工具需要:

  • 删除微服务的内部流和变更日志流;
  • 删除任何外部状态存储的物化(如果适用的话);
  • 将消费者组偏移量重置到每个输入流的开始位置。

14.10 ** 消费者偏移量滞后度监控**

消费者滞后度是事件驱动型微服务需要扩容的最佳指标之一。你可以使用工具来监控这一指标,该工具定期计算相关的消费者组的滞后度。

14.11 流水线型的微服务创建流程

  1. 创建一个仓库。
  1. 使用持续集成管道创建任何必要的集成(参见 16.2.1 节)。
  1. 配置任何 Webhook 或其他依赖项。
  1. 使用微服务--团队分配系统给团队分配所有权。
  1. 注册输入流的访问权限。
  1. 创建所有输出流并应用所有者权限。
  1. 提供应用模板或代码生成器的选项以创建微服务的框架。

团队将重复多次执行这个流程,所以将其流水化会极大地节省时间和工作量

14.12 ** 容器管理控制**

容器通过 CMS 来管理。我建议暴露 CMS 的某些能力,以便团队能够充分利用自己的 DevOps 能力,比如:

  • 为他们的微服务设置环境变量;
  • 指定在哪个集群(例如,测试、集成、生产)上运行微服务;
  • 根据微服务的需要管理 CPU、内存和磁盘资源;
  • 手动或根据 SLA 及处理滞后度增加或减少服务数量;
  • 根据 CPU、内存、磁盘或滞后度量自动伸缩。

14.13 集群创建和管理

多集群管理,包括动态跨区域通信和灾难恢复,是一个非常复杂的主题。

14.13.1 事件代理的程序化创建

负责管理事件代理集群的团队通常还提供了创建和管理新集群的工具。也就是说,商业云提供商也正在进 入这个领域。例如,Apache Kafka 集群现在可以在 AWS 中按需创建(截至 2018 年 11 月),其他一些 云服务提供商也在加入。不同的事件代理技术可能需要不同的工作量来支持,领域专家应该对此进行仔细 研究。

14.13.2 计算资源的程序化创建

要创建一组独立于所有其他资源的计算资源。并不总是需要创建全新的容器管理服务,因为现有的通常可以服务于多个命名空间。与事件代理一样,云计算提供商通常会提供托管服务(比如谷歌和亚马 逊的托管 Kubernetes 解决方案),可以按需为你提供这些功能

14.13.3 跨集群事件数据复制

在集群之间复制事件数据对于将事件驱动型微服务扩展到单个集群之外非常重要,示例场景包括灾难恢复、常规跨集群通信和以编程方式生成的测试环境。

需要考虑一下几点:

  • 是否能自动复制新添加的事件流?
  • 如何处理删除或修改的事件流的复制?
  • 数据是精确复制(有相同的偏移量、分区和时间戳),还是近似复制?
  • 复制延迟是多少?业务需求是否可接受?
  • 性能指标是什么?它能根据业务需要伸缩吗?

14.13.4 工具的程序化创建

的所有工具集也应该以程序化方式提供给新集群。这提供了一组通用工具,你可以 将其部署到任何集群,而无须依赖事件代理本身以外的任何数据存储。

14.14 依赖跟踪和拓扑可视化

跟踪微服务之间的数据依赖关系对于组织运行事件驱动型微服务非常有用。唯一的要求是组织必须知道哪 些微服务正在读写哪些事件流。为了实现这一点,可以采用一个自上报系统,让消费者和生产者上报自己 的消费模式和生产模式。

  • 确定数据沿袭。
  • 发现数据源。
  • 度量互连性和复杂性。
  • 映射业务需求到微服务。

14.15 小结

使用多个自主服务需要你仔细考虑如何管理这些系统。本章介绍的工具旨在帮助你的组织管理其服务。

  1. 15 测试事件驱动型微服务

测试事件驱动型微服务的一大优点是它们非常模块化。服务的输入由事件流或来自"请求--响应"API 的 请求所提供。状态被物化到服务自己的独立的状态存储,输出事件则被写入服务的输出流中。微服务的 "小"和"目标明确"的特性使得它们比大而复杂的服务更易于测试。

15.1 通用测试原则

功能性测试

  • 比如单元测试
  • 集成测试
  • 系统测试和回归测试

非功能性测试

  • 性能测试、
  • 负载测试、
  • 压力测试和恢复测试

15.2 单元测试拓扑函数

事件驱动拓扑通常会将转换、聚合、映射和归约函数应用于事件,这些函数是单元测试的理想候选函数。

15.2.1 无状态的函数

myMapFunction 和 myFilterFunction 是独立的函数,它们都不保存状态。每个函数都应该进行单元测 试,以确保其正确地处理预期范围内的输入数据,特别是边界情况。

15.2.2 有状态的函数

有状态的单元测试还要求持久化状态(无论是模拟外部数据存储 还是临时内部数据存储)在测试期间可用。

该函数用于对每个键的 eventValue 求和。模拟端点是在测试期间提供数据存储的可靠实现的一种方法。

另一种方法是创建一个本地可用的数据存储版本,不过这更类似于集成测试

15.3 测试拓扑

功能完整的轻量级框架和重量级框架通常提供了本地测试整个拓扑的方法。

每个流框架对测试拓扑有不同程度的支持,你必须将可用的选项暴露出来。

这些测试框架不需要你创建一个事件代理来维持输入事件或者搭建重量级框架集群来进行处理。

15.4 测试 schema 演化和兼容性

根据任一事件流 schema 演化规则(参见 3.1.3 节),要确保所有输出 schema 与之前的 schema 兼容,你可以从 schema 注册表中提取 schema,并在代码提交过程中执行演化规则检查。一些应用程序 能使用 schema 生成工具在编译时从代码中定义的类或结构自动生成 schema,从而可自动与以前版本进行比较。

15.5 事件驱动型微服务的集成测试

微服务集成测试有两种主要形式:

本地集成测试和远程集成测试。

  • 本地集成测试指在生产环境的本地化副本上执行的测试,
  • 远程集成测试指在本地系统外部的环境中执行的微服务。

15.6 本地集成测试

本地集成测试允许进行大范围的功能性测试和非功能性测试。测试的形式是使用生产环境的本地副本,

本地集成测试的另一个显著好处是,你可以在相同的工作流中同时有效地测试事件驱动逻辑和"请求--响 应"逻辑。

15.6.1 在测试代码的运行时内创建临时环境

例如:Kafka Streams 应用程序的测试代码会启动自己的 Kafka 代理、schema 注册表和微服务拓扑实 例。然后,测试代码可以启动并停止拓扑实例、发布事件、等待响应、导致代理中断以及引发其他故障模式。一旦终止,所有组件都被终止,状态将被清除。

15.6.2 在测试代码外部创建临时环境

设置环境以执行这些测试的一种方法是只需在本地安装和配置所有必需的系统。这是一种低开销的方法, 特别是当你刚开始使用微服务时,但是如果每个团队成员都必须这样做,那么在他们每个人运行的版本略 有不同的情况下,调试就会变得成本高昂且复杂。

更灵活的方法是创建一个容器,其中安装并配置了所有必需的组件。所有想要以此方式测试应用程序的团 队都可以使用这个容器。

15.6.3 使用 mocking 和模拟器方法集成托管服务

使用简化本地集成测试环境可能也需要提供托管服务,比如托管的事件代理、重量级框架或 FaaS 平台。

需要有简化版的模拟依赖向

15.6.4 集成没有本地支持的远程服务

在生产中使用的一些服务可能根本没有任何可用的本地方案,这对开发和集成测试来说都是缺点。

缓解此问题通常需要与基础架构团队密切协调,以确保可以通过访问控制独立地调配测试环境,或者创建 一个供所有人使用的大型公共环境。

很多大型封闭源代码服务供应商正在努力提供本地的开发和测试方案,因此先行者们迟早都会 有可用的方案。

15.7 完全远程集成测试

完全远程集成测试使你能够执行在本地环境中难以执行的特定测试。例如,性能和负载测试对于确保被测 微服务实现其服务级别目标至关重要。事件处理吞吐量、"请求--响应"延迟、实例伸缩和故障恢复都可通过完全集成测试获得。

15.7.1 程序化创建临时集成测试环境

14.13 节研究了以程序化方式生成事件代理和计算资源管理器的优势。你可以使用这些工具来生成集成测试的临时环境。

在新搭建的环境中,下一个问题是缺少事件流和事件数据。

  1. 填充来自生产环境的事件
  1. 填充来自规划好的测试源的事件
  1. 使用 schema 创建模拟的事件

15.7.2 使用共享环境进行测试

另一个方案是创建一个单一的测试环境,其中包含一个共享的事件流池

优点 :很容易启动。 只需维护一个测试环境的基础设施。 与生产环境负载隔离。

缺点 :受制于"公地悲剧"。零碎和废弃的事件流会使你很难分辨哪些流对测试输入有效,哪些流只是以前未清理的测试的输出。

15.7.3 使用生产环境进行测试

你也可以在生产环境中测试微服务。微服务可以运转起来、从输入事件流中消费、应用业务逻辑并产生输出。

最常见的方法是让微服务使用自己指定的输出事件流和状态存储,这样就不会影响现有的生产系统。

当一个微服务的旧版本与测试中的新版本同时运行时,这一点尤为重要。

15.8 选择你的完全远程集成测试策略

微服务模块化的好处在于,不必只选择一种方法来执行测试。可以根据需要使用任一方案,为其他项目切 换到不同的方案,并随着需求的变化更新测试方法。对多集群事件代理的支持工具和事件复制功能的投资 将在很大程度上决定你的测试方案。

15.9 小结

事件驱动型微服务主要从事件流中获取其输入数据。可以通过多种方式创建和填充这些流,包括从生产环境中复制数据、管理特定的数据集以及基于 schema 自动生成事件。每种方法都有自己的优点和缺点,但 它们都依赖于支持性工具来创建、填充和管理这些事件流。

  1. 16 部署事件驱动型微服务

部署事件驱动型微服务富有挑战性。随着组织内微服务数量的增加,标准化部署流程的重要性也随之增加。

16.1 微服务部署的原则

  1. 团队部署自主权:
  • 团队应控制自己的测试和部署流程,并有权自行部署微服务。
  1. 标准化部署流程:
  • 确保所有服务的部署流程一致,通常通过持续集成(CI)框架实现。
  1. 提供支持性工具:
  • 提供自动化工具帮助团队重置消费者组偏移量、清除状态存储、检查更新schema演化及管理内部事件流。
  1. 考虑事件流再处理的影响:
  • 重新消费输入事件流可能导致下游系统高负载和过时事件处理,需谨慎评估对下游消费者的潜在影响,
  • 特别是可能引发的副作用(如误发旧促销邮件)。
  1. 遵守SLA:
  • 确保部署过程中的任何中断或负载激增不会违反服务级别协议(SLA),特别是在重建状态存储或再处理事件流时。
  1. 最小化依赖服务变更:
  • 尽量减少对其他服务API或数据模型的更改,以尊重其他团队的自主权,除非确实必要。
  1. 与下游消费者协商破坏性变更:
  • 对于不可避免的破坏性变更(如schema变更),提前与下游消费者沟通并制定迁移计划,确保部署前达成新的数据契约。

这些原则旨在保障部署流程的高效性、一致性和安全性,同时维护团队自治和服务间的良好协作。

16.2 微服务部署的架构组件

16.2.1 持续集成系统、持续交付系统和持续部署系统

持续集成系统、持续交付系统和持续部署系统允许在将代码变更提交到代码库时构建、测试和部署微服 务。这是大规模成功管理和部署微服务必须要付出的微服务税的一部分。

持续集成**(CI - Continuous Integration)**

  • 定义:持续集成是指频繁地(例如每天多次)将代码变更加入到主分支中,并通过自动化的构建和测试来验证这些变更不会引入问题。
  • 目标:尽早发现并解决集成问题,减少合并冲突,确保代码库的质量。

持续交付**(CD - Continuous Delivery)**

  • 定义:持续交付是在持续集成的基础上,确保代码库随时处于可部署状态,可以通过手动触发的方式快速、可靠地部署到生产环境。
  • 目标:保持代码库的高质量和稳定性,使任何版本都可以随时部署到生产环境中,但实际部署仍需人工确认。

持续部署**(CD - Continuous Deployment)**

  • 定义:持续部署是持续交付的进一步扩展,它不仅保持代码库随时可部署,而且每次有新的代码变更通过所有阶段的自动化测试后,会自动部署到生产环境。
  • 目标:实现从代码提交到生产的完全自动化流程,加速创新和反馈循环。

|----------------------------------------------------------------------|
| 事件驱动,持续部署在实践中是很难的。有状态的服务特别有挑战性,因为部署可能需要重建状态存储及 再处理事件流,这对依赖服务尤其具有干扰性。 |

16.2.2 CMS 和商业硬件

CMS 提供了管理、部署和控制容器化应用程序资源的方法

商业硬件提供了经济高效、灵活可靠的基础设施,非常适合事件驱动型微服务的部署和管理。

16.3 基本的全站式部署模式

基本的全站式部署模式如下:

  1. 提交代码(合并最新的代码到主分支,触发持续集成管道。)
  1. 执行自动化的单元测试和集成测试 。
  1. 运行部署前验证测试(事件流验证和schema 验证)
  1. 部署(在部署新的微服务之前需要停止当前部署的微服务)
  1. 运行部署后验证测试

16.4 滚动更新模式

滚动更新模式可用于在更新每个微服务实例时保持服务运行。

前提

没有对任何状态存储的破坏性变更;

没有对内部微服务拓扑的破坏性变更(特别对于使用轻量级框架的实现);

没有对内部事件 schema 的破坏性变更。

场景:

将新的字段添加到输入事件并反映在业务逻辑中;

消费新的输入流;

修复缺陷但无须重新处理。

|-------------------------------------------------------------------------------------------|
| 不再是同一时间停掉所有实例,而是每次只停一个实例。被停掉的实例接着进行更新并重启,这样在部署过程中会混合运行新实例和旧实例。此滚动更新意 味着在一小段时间内会同时执行新旧两种逻辑 |

16.5 破坏性的 schema 变更模式

部署破坏性的 schema 变更是一个相当简单的技术过程。困难的部分是重新协商 schema 定义、与干系人 沟通,以及协调部署和迁移计划。每一个步骤都需要各方之间的明确沟通和清晰的行动时间表。

16.5.1 通过两个事件流达到最终迁移

通过两个事件流达到最终迁移需要生产者同时用新旧格式写事件,然后将它们分别写到对应的流中。旧流 会被标记为弃用,而其消费者会在自己的时间周期内迁移到新流。

假设:

事件可以生成到新旧两个流中。

最终迁移不会导致下游的不一致性。(下游服务将继续消费两种不同的定义,但不会产生相应的影 响,或者这些影响将是有限的)。

16.5.2 同步迁移到新事件流

另一种方案是更新生产者以严格使用新格式创建事件,并停止向旧流提供更新。

假设:

事件定义变更足够重大,旧的事件不再可用 。(实体或事件的领域已经发生非常大的变化,以至于无 法同时维护新旧两种格式。)

必须同步进行迁移以消除下游的不一致性 。(。领域发生了重大变化,服务需要更新以确保能够满足业 务需求。否则,下游服务可能会出现严重的不一致。)

16.6 蓝绿部署模式

蓝绿部署的主要目标是在部署新功能时提供零中断时间。该模式主要用于同步的**"** 请求 -- 响应 " 微服务部署,因为它使得服务在更新时仍然能够继续进行同步请求.

|---------------------------------------------------------------------------|
| 当微服务响应于输入事件流并向输出流生成事件时,蓝绿部署不起作用 。这两个微服务在实体流的情况下将覆盖彼此的结果,在事件流的情况下将创建重复的事件。 |

16.7 小结

流水化微服务的部署需要组织支付微服务税并投资必要的部署系统。

持续集成管道是部署过程的一个关键部分。它们提供了搭建并执行测试、验证构建以及确保容器化服务完 成部署准备的框架。

部署微服务的方法有很多,最简单的是完全停止微服务并重新部署最新的代码。但是,这会导致严重的中 断时间,并且根据 SLA 的不同,这可能是不合适的。

  1. 17 结论

事件驱动型微服务架构提供了强大、灵活且定义良好的方法来解决业务问题

17.1 通信层

数据沟通结构带来了跨整个组织的重要业务事件的通用访问。

事件代理非常好地满足了这一需求,因为它们允许对数据进行严格的组织,可以近实时地传播更新,并且可以在大数据规模上运行。

成熟的数据通信层将数据的所有权和生产与数据的访问和消费分离开来。

任何服务都可以利用事件代理的持久性和弹性来使其数据高度可用,包括那些使用事件代理存储其内部状 态变更日志的服务。

17.2 业务领域和界限上下文

业务在特定的领域内执行操作,这个领域又可以分解为几个子领域。业务问题的解决方案是由界限上下文定义的,界限上下文标识了边界,这些边界包括与子域相关的输入、输出、事件、需求、流程和数据模型。

17.3 可共享的工具和基础设施

事件驱动型微服务需要对允许其大规模运行的系统和工具进行投资,这种投资被称为微服务税。事件代理 是系统的核心,因为它提供了服务之间的基本通信。

事件代理

schema 注册表和数据探测服务

CMS 持续集成、交付和部署服务

监控和日志服务

组织通常会从事件代理服务或 CMS 开始,然后根据需 要添加其他服务。

17.4 结构化事件

schema 在传达事件的意义方面起着关键作用。生产者必须确保根据 schema 来生产事件,而消费者必须确保会处理所消费事件的类型、范围和定 义。强定义的 schema 大大减少了消费者误解事件的机会。

schema 演化为事件和实体的变更提供了一种响应新业务需求的机制。它使生产者能够使用新的和修改过 的字段生成数据,同时还允许不关心变更的消费者继续使用旧的 schema 进行消费。

schema 还带来了一些有用的特性,比如代码生成和数据发现。

同时,schema 注册表提供了一种可搜索的方法来确定哪些数据属于哪个事件流,从而 更容易发现流的内容

17.5 数据解放和单一事实来源

一旦有了可用的数据通信层,就可以让关键业务数据进入里面。从组织的各种服务和数据存储中解放数据 是一个漫长的过程,将所有必要的数据放入事件代理需要一些时间。这是解耦系统并向事件驱动架构迈进 的关键一步。数据解放将数据的生产和所有权与下游消费者对数据的访问进行了解耦。

17.6 微服务

作为界限上下文的实现,微服务专注于解决界限上下文的 业务 问题,并进行相应的调整。业务需求变更是 微服务更新的主要驱动力,所有其他不相关的微服务则保持不变。

将重要的业务实体和事件放入事件代理中;

将事件代理作为单一事实来源使用;

避免服务间的直接调用。

17.7 微服务实现方案

对于构建事件驱动型微服务,有各种各样的方案可供选择,它们各有利弊。目前,

轻量级框架往往具有最 好的开箱即用功能。流可以物化到表并永久保存。

重量级框架提供了与轻量级框架类似的功能,但在处理独立性方面存在不足,因为它们需要单独的专用资 源集群。新的集群管理方案正变得越来越流行,比如与 Kubernetes(领先的 CMS 之一)直接集成。

BPC 模式和 FaaS 解决方案为许多编程语言和运行时提供了灵活的方案。这两种方案都受限于基础的消费 和生产模式。

17.8 测试

本地测试可以包含单元测试和集成测试,其中后者依赖于动态创建的事件代理、schema 注册表以及任何 其他测试微服务所需要的依赖项。

可以通过动态创建临时事件代理集群、使用生产环境事件流和事件的副本填充集群(排除信息安全问题) 并针对集群执行应用程序来进行生产环境集成测试。

还可以在生产环境部署之前提供冒烟测试,以确保没 有忽略任何内容。还可以在此环境中执行性能测试,测试单个实例的性能和应用程序水平伸缩的能力。

17.9 部署

大规模部署微服务要求微服务拥有者能够快速且简单地部署并回滚他们的服务。这种自主权允许团队独立 行动,消除了原本只负责部署的基础设施团队中存在的瓶颈。

部署过程必须考虑 SLA、状态重建和输入事件流的再消费。SLA 不只是中断时间的问题,还需要考虑部署 对所有下游消费者的影响,以及事件代理服务的健康情况。

17.10 结语

事件驱动型微服务及其数据通信层是应对大数据挑战的关键,提供了灵活性、专注性和解耦的优势,推动了更高效的数据处理和业务创新。

  • 数据增长挑战:随着数据量的飞速增长,传统的单一大型数据存储已无法满足多样化的需求。
  • 数据通信层的重要性:健壮且定义良好的数据通信层减轻了微服务的负担,使它们专注于自身的业务功能,而不是处理其他服务的数据和查询需求。
  • 灵活性与专注:事件驱动型微服务通过事件流处理大型和多样化数据集,提供了无与伦比的灵活性,让各业务部门能够专注于实现其特定业务目标所需的数据。
  • 解耦与扩展:事件代理促进了新服务的构建,与旧业务需求完全解耦,消除了访问边界,并降低了数据生产和分发的复杂性。
  • 未来趋势:数据通信层将继续扩展组织内数据访问的能力,确保任何服务或团队都能轻松获取所需的业务信息。

读书笔记

|-------------------------------------------------------------------------------------------------------------------------|
| TSD第三次架构培训 微服务:微服务面向职能拆分 DDD:DDD面向业务目标,通常会有事件驱动。 CORS:将操作和业务做拆分 MTA东软主架构 SBA service - base arc:面相服务 MSA : 微服务 EDA:事件驱动 |

灵感摘抄

引申阅读

|-------------------------|
| 在阅读中发现感兴趣的地方,拓展阅读的广度和深度 |

相关推荐
jackson凌3 小时前
【Java学习笔记】Java第一课,梦开始的地方!!!
java·笔记
I like Code?3 小时前
AntVG2可视化学习与开发笔记-React19(持续更新)
javascript·笔记·学习
那天的烟花雨5 小时前
android display 笔记(十一)surfaceflinger 如何将图层传到lcd驱动的呢?
android·笔记
你说你说你来说5 小时前
安卓开发Intent详细介绍和使用
android·笔记
jingjingjing11115 小时前
笔记:代码随想录算法训练营day67:Floyd 算法精讲、A * 算法精讲 (A star算法) 严重超时完结,不过,撒花
笔记
zhuyixiangyyds5 小时前
day28图像处理OpenCV
图像处理·笔记·学习
DaLi Yao6 小时前
【笔记】对抗训练-GAN
笔记
不爱吃于先生6 小时前
机器学习概述自用笔记(李宏毅)
人工智能·笔记·机器学习
辛姜_千尘红回7 小时前
AT_abc398_e [ABC398E] Tree Game 题解
c语言·c++·笔记·算法
*TQK*9 小时前
Java笔记5——面向对象(下)
java·笔记·学习