
第 1 章 为什么用事件驱动型微服务
媒介即信息。
——Marshall McLuhan
Marshall McLuhan 认为,影响人类并给社会带来根本性变革的不是媒介的内容,而是与媒介的互动过程。在我们的集体参与下,报纸、广播、电视、互联网、即时通信和社交媒体改变了人类的互动方式以及社会结构。
对于计算机系统架构来说同样如此。只需回顾计算机技术的发明史就会发现,网络通信、关系型数据库、大数据开发和云计算等显著地改变了系统架构的构建方式和工作方式。其中的每一项发明不仅改变了技术在各软件项目中的使用方式,也改变了各类组织、团队和人们相互交流的方式。从中心化的大型机到分布式的移动应用,每种新的媒介都彻底改变了人们与计算的关系。
异步生产和消费事件的媒介已被现代技术彻底改变。这些事件如今可以被大规模地永久存储,并且能被任何服务按需多次消费。可以根据需要轻松地获取和释放计算资源,这让微服务的创建和管理变得简单。微服务可以根据自身的需要存储和管理数据,并且能够达到之前仅限于基于批处理的大数据解决方案的数据规模。对事件驱动媒介的这些改进已经产生了深远的影响,不仅改变了计算机体系结构,也彻底重塑了团队、个人和组织创建系统和业务的方式。
1.1 什么是事件驱动型微服务
微服务和微服务类型的架构已经存在很多年了,它们有许多不同的形式和名字。面向服务的架构(service-oriented architecture,SOA)通常由多个相互直接同步通信的微服务构成。消息传递架构使用可被消费的事件在微服务之间进行异步通信。基于事件的通信当然不算新颖,但大规模并实时地处理大数据集是新的需求,而这要求对旧的架构类型进行改进。
在现代的事件驱动型微服务架构中,系统通过发布和消费事件来通信。这些事件并不会像消息传递系统中那样在某次消费的时候就被销毁,而是可被其他多个消费者按需消费。这是一个重要的区别,因为正是这个特性使得本书所要介绍的真正强大的模式成为可能。
微服务本身是微小且定制的,旨在帮助组织实现必要的业务目标。“微小”的一种典型定义是能够在两周内实现,另一种定义是服务应该(在概念上)像是出自一个人的头脑。这些服务从输入事件流中消费事件,运行它们特定的业务逻辑,并且可能发布自己的输出事件,提供用于“请求–响应”访问的数据,与某个第三方 API 通信或者执行其他必需的操作。正如本书将要详细介绍的,这些服务可以是有状态的,也可以是无状态的;可以复杂,也可以简单;可以被实现为长期运行的独立应用程序,也可以通过“函数即服务”作为函数执行。
事件流和微服务的组合形成了贯穿整个组织的业务活动的相连图。传统的计算机体系结构由多个单体应用和单体应用间的通信组成,具有相似的图形结构。这两种图形结构如图 1-1 所示。
图 1-1:微服务和单体应用的图形结构
为了确定如何使这种图形结构高效运行,要看两个主要部分:节点和连接。本章会依次对它们进行介绍。
1.2 领域驱动设计和界限上下文
领域驱动设计由 Eric Evans 在他的同名图书《领域驱动设计:软件核心复杂性应对之道》1 中提出,书中介绍了一些构建事件驱动型微服务所必需的概念。鉴于已有丰富的文章(“What is the Domain Model in Domain Driven Design?”)、图书(《实现领域驱动设计》)和博客资源讨论了这一主题,本节仅做简单介绍。
1该书已由人民邮电出版社出版,详见图灵社区。——编者注
以下概念支撑了领域驱动设计。
领域
某个企业所涉足并提供解决方案的问题空间。它涵盖了这个企业必须关注的所有内容,包括规则、流程、想法、特定于业务的术语,以及所有跟这个问题空间相关的东西,无论该企业是否与之相关。领域的存在跟企业是否存在无关。
子领域
子领域是主领域的一个部分。每个子领域都聚焦于一个特定的职责子集,并且通常反映了企业的部分组织结构(如仓储、销售和工程部门)。一个子领域本身可视为一个领域。跟领域一样,子领域也属于问题空间。
领域(子领域)模型
这是一种用于商业目的的对实际领域的抽象。在领域中对企业最重要的部分和属性被用来生成模型。一个企业的主领域模型可通过其提供给客户的产品、客户与产品交互的接口以及企业实现既定目标的各种流程和功能来识别。模型往往需要随着领域的改变和业务优先级的变化而不断完善。领域模型是解决方案空间的一部分,因为它是企业用来解决问题的框架。
界限上下文
界限上下文是与子领域相关的逻辑边界,包括输入、输出、事件、需求、流程和数据模型。虽然在理想情况下,界限上下文会和子领域保持完全一致,但是历史遗留的系统、技术债和第三方集成等因素通常会带来一些例外。界限上下文也是解决方案空间的一个属性,对微服务如何相互交互有着重要的影响。
界限上下文应该是高内聚的。上下文内部的运作应该是紧密相关的,绝大部分的通信发生在内部而不会跨越边界。拥有高内聚的职责可以缩小设计范围和简化实现。
界限上下文之间的关联应该是低耦合的,因为一个界限上下文内部的更改应该最小化或消除对相邻上下文的影响。低耦合可以确保在某个上下文内的需求变更不会向相邻的上下文传播大量依赖变更。
1.2.1 运用领域模型和界限上下文
每个组织都会在其自身和外部世界之间形成一个单独的领域。每个人在这个组织内的工作都在支持这个领域的需求。
领域可以被划分为多个子领域,例如,对于以技术为中心的公司来说,会有一个工程部门、一个销售部门和一个客户支持部门。每个子领域有其自身的需求和职责,并且其本身可以进一步划分。这个划分的过程会不断重复,直到子领域模型的颗粒度变得足够细和具有可操作性,并且开发团队可将其实现为小型的独立服务。界限上下文是围绕着这些子领域建立的,并且是创建微服务的基础。
1.2.2 保持界限上下文与业务需求一致
产品的业务需求在其生命周期内发生变化是很平常的事情,这可能是由于组织变更或新特性的要求所致。很少有公司需要在没有业务需求变更的情况下去变更既有产品的底层实现。这就是为什么界限上下文应该围绕着业务需求而不是技术需求去构建。
保持界限上下文和业务需求一致,可以让开发团队以高内聚、低耦合的方式对微服务的实现做出变更。这为团队提供了设计和实现特定业务需求解决方案的自主权,可以大大减少团队之间的依赖,并且让每个团队都高度聚焦于自身需求。
相反,根据技术需求来调整微服务是有问题的。这种模式通常见于设计不当的同步点对点微服务和传统的单体式计算系统中,在这些系统中不同的团队负责应用程序中特定的技术分层。按照技术保持一致性的主要问题在于,它将实现业务功能的职责分散到不同的界限上下文中,这会涉及多个具有不同排期和职责的团队。因为没有一个团队单独对实现某个解决方案负责,所以每个服务都要跨越团队和 API 边界,这使得变更很困难,而且成本很高。一个看起来无害的变更、缺陷或者故障服务,却可能对使用这种技术系统的所有服务的业务服务能力产生严重的连锁反应。事件驱动型微服务很少采用保持技术一致性的做法,并且应该尽可能地完全避免这种做法。清除跨领域的技术方案和团队间的相互依赖可以降低系统对变更的敏感度。
图 1-2 展示了两种场景:左边的单一权责结构和右边的跨领域权责结构。在单一权责结构中,团队是完全围绕着两个独立的业务需求(界限上下文)来组织的,并且可以完全控制其应用层和数据层。在右边的图中,团队是根据技术需求来组织的,应用层和数据层是分开管理的。这就在不同的团队之间建立了显式的依赖,进而在不同的业务需求之间产生了隐式的依赖。
图 1-2:对齐业务上下文与对齐技术上下文
围绕着业务需求对事件驱动型微服务架构建模是更好的选择,尽管这种方法也有一些折中之处。代码可能会被复制多次,并且许多服务可能使用相似的数据访问模式。产品开发者可能会尝试与其他产品共享数据资源或耦合边界的方式来减少复制行为。在这些例子中,从长远来看,后续的紧耦合可能比重复代码和存储冗余数据带来更高的成本。这些权衡点将在本书中详细讨论。
保持界限上下文之间的低耦合,并聚焦于最小化上下文之间的依赖。这将使得界限上下文的实现可以按需变更,而不会进一步破坏很多(或任何)其他系统。
此外,每个团队可能都需要拥有全栈的专业知识,这个要求会因需要特定的技能集和访问权限而变得复杂。组织应该实施最通用的需求,这样这些垂直团队就能进行自支持,同时可以根据需要在跨团队中提供更专业的技能集。这些最佳实践会在第 14 章中详细介绍。
1.3 沟通结构
要达成目标,一个组织的团队、系统和人员必须相互沟通。这些沟通形成了一个相互连接的依赖拓扑结构,被称为沟通结构。沟通结构主要有 3 种,每一种都影响着企业的运作方式。
1.3.1 业务沟通结构
如图 1-3 所示,业务沟通结构决定了团队和部门之间的沟通方式,每个团队和部门都由分配给它的主要需求和职责驱动。例如,工程部开发软件产品,销售部将其卖给客户,支持部则负责确保顾客和客户满意。从主要业务部门到个人贡献者的工作,团队的组织及其目标的配置都遵循此结构。业务需求、需求在团队间的分配以及团队的组成都会随着时间而改变,这会极大地影响业务沟通结构和实现沟通结构之间的关系。
图 1-3:业务沟通结构的例子
1.3.2 实现沟通结构
实现沟通结构(参见图 1-4)是由组织确定的子领域模型相关的数据和逻辑。它确立了业务流程、数据结构和系统设计,以便快速高效地执行业务操作。因为实现重新定义的业务需求需要重写逻辑,所以这会导致业务沟通结构灵活性的折中。这些重写通常是对子领域模型和相关代码的迭代改进,随着时间的推移,它们反映了为满足新的业务需求而在实现上进行的演进。
图 1-4:实现沟通结构的例子
软件工程中对应实现沟通结构的典型例子是单体数据库应用程序。应用程序中的业务逻辑通过函数调用或共享状态来进行内部通信。继而,这类单体应用被用来满足业务沟通结构所确定的业务需求。
1.3.3 数据沟通结构
数据沟通结构(参见图 1-5)是在业务之间,特别是实现之间进行数据通信的过程。虽然在日常事务中经常使用包括电子邮件、即时通信和会议的数据沟通结构来传达业务的变更,但在大部分软件实现中,数据沟通结构是缺失的。它的作用通常临时体现在系统跟系统间发生联系的时候。实现沟通结构经常扮演双重角色,除了实现自身需求,还包含数据通信功能。这就给公司的成长和变化造成了很多问题,其影响会在下一节中进行评估。
图 1-5:临时性的数据沟通结构的例子
1.3.4 康威定律和沟通结构
设计系统的组织……受到约束,产生的设计是这些组织的沟通结构的副本。
—— Melvin Conway,“How Do Committees Invent?”(1968 年 4 月)
这句话被称为康威定律,它表明一个团队将根据其组织的沟通结构来构建产品。业务沟通结构将人员组织成团队,这些团队通常生产由他们的团队边界限定的产品。实现沟通结构提供了对给定产品的子领域数据模型的访问,但是较弱的数据通信能力限制了其对其他产品的访问。
因为领域概念跨越了不同业务,所以同一个组织里的不同界限上下文之间通常需要对方的领域数据。实现沟通结构一般不擅长提供这种通信机制,虽然它们在满足自身界限上下文的需求时表现得很出色。它们会以两种方式来影响产品设计。首先,由于在整个组织内传递所需的领域数据很低效,它们不鼓励创建逻辑独立的新产品。其次,它们会提供能轻松访问现有领域数据的方式,但存在要不断扩展领域以满足新业务需求的风险。这种特殊的模式体现在单体设计中。
数据沟通结构在组织设计和构建产品的过程中扮演着关键角色,但对于许多组织来说这种结构是长期缺失的。如前所述,实现沟通结构除了自身的角色外,还经常扮演数据沟通结构的角色。
一些组织试图通过其他实现方式来提升访问领域数据的能力,但这些努力有其自身的缺点。例如,尽管共享数据库的做法会促进反模式并且通常无法进行充分扩展以满足所有的性能要求,但还是经常被使用。数据库方案也许仅仅提供了一个只读副本,但是这可能会不必要地暴露其内部数据模型。批处理程序能够将数据转存到文件中以被其他程序读取,但这种方法会造成数据一致性和多信息源的问题。最后,所有这些解决方案都会导致实现之间的紧耦合,并进一步将系统架构强化为点对点的关系。
如果你发现在组织中访问数据很困难,或者由于所有数据都存在于单独的实现中,你的产品不得不不断扩大其涵盖的范围,那你可能正受到糟糕的数据沟通结构的影响。这个问题会随着组织的发展、新产品的开发以及对访问通用领域数据需求的增加而进一步扩大。
1.4 传统计算中的沟通结构
一个组织的沟通结构极大地影响了工程实现的创建方式。在团队的层面也是如此:团队的沟通结构影响了其为所负责的特定业务需求所构建的解决方案。下面来看看具体实践。
考虑这样的场景:有一个团队负责一个单独的服务及支持该服务的单独的数据库。团队成员很乐于提供他们的业务功能,一切都很美好。有一天团队领导带来了一个新的业务需求。这个需求跟该团队正在做的业务是相关的,可能将其加到现有的服务里就可以。但是,它与当前服务的差异程度也足以让团队考虑去开发一个新服务。
团队目前处在一个十字路口:是用新服务来实现新的业务需求,还是简单地将其加到现有的服务中去?下面进一步看看他们的选择。
1.4.1 选项1:创建一个新服务
如果业务需求与原服务差异足够大,那么可以将其放入新的服务。但是数据呢?新的业务功能需要一些旧数据,但那些数据目前限定在现有的服务内。此外,团队并没有真正的启动新的、完全独立的服务的流程。另外,团队越来越大,公司也在快速成长。如果团队在将来不得不被拆分,那么拥有模块化和独立的系统可能会让划分职权更加容易。
这种方法存在一些风险。团队必须找到一种从原有数据库中复制合适的数据到新数据库中的方法。他们需要确保不会暴露内部工作逻辑,并且对数据结构的变更不会影响任何其他复制了他们数据的团队。此外,被复制的数据通常是过时的,因为他们只能每 30 分钟实时同步一次生产数据,这样才不会给数据库带来大量查询,进而造成过载。复制数据的连接需要被监控和维护,以保证它的正确运行。
让新服务启动并运转起来也有风险。他们需要管理两套数据库、两个服务,并且要为它们建立日志记录、监控、测试、部署和回滚流程。他们还必须注意同步任何数据结构的变更,以免影响相关的系统。
1.4.2 选项2:将它加入现有服务中
另一个选项是在现有服务里创建新的数据结构和业务逻辑。所需的数据已经在当前的数据库中,日志记录、监控、测试、部署和回滚流程也早已经被定义和使用。团队很熟悉这个系统并且能正确地实现逻辑,他们的单体模式支持这种服务设计方法。
这种方法也有一些风险,尽管它们更不易被察觉。随着变更的不断发生,实现的边界可能会变得模糊,特别是因为不同的模块通常被捆绑在同一个代码库里。通过跨边界和直接跨模块耦合来快速添加特性实在太容易了。能够快速迭代是一个很大的优势,但这是以高耦合、低内聚和缺少模块化为代价的。虽然团队可以防范这种情况,但这需要出色的规划和对边界的严格遵守,而在面对紧张的排期、缺乏经验和服务权责改变等情况时,这些要求往往会被搁置一边。
1.4.3 两种选项的利弊
大部分团队会选择第二个选项,即添加功能到现有的服务中。这个选择并没有错,单体架构是很有用且强大的结构,能够为企业带来巨大的价值。第一个选项首先会遇到与传统计算相关的两个问题:
● 难以可靠地访问另一个系统的数据,特别是在大规模访问和实时访问的情况下;
● 创建和管理新的服务会带来很大的开销和风险,特别是在组织没有处理此类事情的既定方法的情况下。
访问本地数据通常比访问另一个数据存储中的数据简单。要获取任何封装在属于其他团队的数据存储中的数据都是很难的,因为这需要跨越实现和业务沟通边界。随着数据、连接数和性能需求的增长,这会变得越来越难以维护和扩展。
虽然复制必要的数据是值得的,但并非万无一失。这个模型鼓励许多直接的点对点的耦合,而随着组织的发展、业务单元和所有者的变化,以及产品的成熟和逐渐淘汰,这些耦合将变得难以维护。它创建了一种严格的技术依赖关系,即两个团队(存储数据的团队和复制数据的团队)的实现沟通结构要求它们在更改数据时进行同步工作。这需要很大的投入来确保不会过度暴露一个实现的内部数据模型,以免其他系统与之紧密耦合。扩展性、性能和系统可用性通常是这两个系统存在的问题,因为数据的复制查询可能给数据源系统带来不可持续的负载。在紧急事故发生之前,失败的同步过程可能并不会被察觉。部落文化可能导致一个正在复制数据的团队认为这就是原始数据。
被复制的数据在查询完成和数据传输完成时总是有些过时的。数据集越大,来源越复杂,副本与数据源不同步的可能性就越大。当系统期望彼此拥有完美的、最实时的数据时,这会是个问题,尤其是在就这些数据进行相互通信时。例如,由于数据过时,一个报表服务可能会上报与计费服务不同的值。这会对服务质量、报表、分析和基于金钱的决策制定产生严重的传导影响。
无法在整个公司内正确地传播数据并非由于概念上的根本性缺陷。恰恰相反,这是由于数据沟通结构的弱化或缺失。在前面的场景中,团队的实现沟通结构作为相当有限的数据沟通结构在执行着双重任务。
事件驱动型微服务的一个原则是核心业务数据应该易于获得,并且可以由任何需要它的服务使用。这将用一个正式的数据沟通结构替代此场景中的临时数据沟通结构。对于这个假设的团队,这种数据沟通结构能够消除从其他系统获取数据的大部分困难点。
1.4.4 团队场景(续)
该团队最终决定采用第二个选项,在同一服务内加入新特性。这种做法简单便捷,并且从那之后他们已经实现了很多新特性。随着公司的成长,团队也在壮大,经过一年时间,是时候考虑将原团队拆分为两个更小、更聚焦的团队了。
现在必须指定每个新团队负责之前服务中的某些业务功能。每个团队的业务需求都基于最需要关注的业务领域进行了极好的划分。但是,要拆分实现沟通结构并不那么容易。就如之前的情况,似乎两个团队都需要大量相同的数据来满足各自的需求。新的问题出现了。
● 哪个团队应该拥有哪些数据?
● 数据应该存储在哪里?
● 两个团队都需要修改值的数据怎么办?
团队领导们觉得最好继续共享原有的服务,他们可以各自负责处理不同的部分。这需要更多的跨团队沟通和同步工作,而这些工作可能会导致生产效率降低。并且,如果将来他们的规模再扩大一倍,又该怎么办呢?或者,如果业务需求变化使得他们再也无法用相同的数据结构去满足需求,又该如何?
1.4.5 冲突的压力
原有的团队中有两个冲突的压力。要保持所有数据在一个服务里面,以使添加新功能更加快捷和简便,代价就是扩大实现沟通结构。最终,随着团队规模的扩大,需要拆分业务沟通结构——紧跟而来的是要给新的团队重新分配业务需求。但是,实现沟通结构在当前模式下无法支持重新分配,它需要被分解成适当的组件。这两种方法都没有可伸缩性,并且都需要做一些不同的事情。这些问题都来自同一个根源:一种在实现沟通结构之间弱化的、不明确的数据通信方式。
1.5 事件驱动的沟通结构
事件驱动方法提供了一种替代传统的实现行为和数据沟通结构的选项。基于事件的通信不是“请求–响应”通信的简单替代品,而是服务之间完全不同的通信方式。一个事件流的数据沟通结构从数据访问场景中解耦了数据的生产和所有。服务之间不再通过一个“请求–响应”API 发生联系,而是通过在事件流里定义的事件数据(这个过程在第 3 章中有更详细的描述)发生联系。生产者的职责仅限于生产定义良好的数据到它们各自的事件流中。
1.5.1 事件是通信的基础
所有可共享的数据都被发布到一系列事件流中,构成了一个连续、规范的叙述,详细描述了组织中发生的所有事情。这成了系统间相互通信的渠道。大部分事物能作为事件进行通信,从简单的某个事情的发生到复杂的、有状态的记录。事件就是数据。它们不仅仅是表明数据已经准备就绪的信号,也不仅仅是从一个实现向另一个实现直接进行数据传输的方法。相反,它们既是数据存储又是服务间异步通信的一种方式。
1.5.2 事件流提供了单一事实来源
事件流里的每个事件都是事实的一个状态,所有这些状态合起来构成了单一事实来源——这是组织内所有系统相互通信的基础。沟通结构的好坏取决于其信息的真实性,因此组织采用事件流叙述作为单一事实来源是至关重要的。如果有些团队选择在其他位置放置一份有冲突的数据,那么事件流作为组织数据通信主干的功能将显著降低。
1.5.3 消费者执行自己的建模和查询
基于事件的数据沟通结构不同于扩大化的实现沟通结构,因为它不能提供数据查询和查找功能。所有的业务和应用逻辑必须被封装在事件的生产者和消费者内部。
数据访问和建模需求被完全转移到了消费者一方,每个消费者需要从源事件流中获取他们自己的事件副本。任何查询复杂度也会从数据所有者的实现沟通结构转移到消费者的实现沟通结构。消费者全权负责来自多个事件流的数据混合、特定的查询功能,或者其他特定业务的实现逻辑。生产者和消费者都不必为了数据通信方式而提供查询机制、数据传输机制、API(应用编程接口)和跨团队的服务。它们现在的责任仅限于解决当前的界限上下文的需求。
1.5.4 整个组织的数据沟通得到改善
数据沟通结构的使用是一种逆转,所有可共享的数据都暴露在了实现沟通结构外部。不是所有的数据都必须共享,因此也不是所有的数据都需要发布到一系列事件流中。但是,任何其他团队或服务感兴趣的数据都必须发布到公共事件流集合中,这样数据的生产和所有就变得完全解耦了。这提供了系统架构中长期缺失的正式的数据沟通结构,并且更好地遵守了“高内聚,低耦合”的界限上下文原则。
应用程序现在可以访问原本很难通过点到点连接获得的数据了。新的服务可以简单地从典型事件流中获得所需的数据,创建它们自己的模型和状态,并执行任何必要的业务功能,而无须依赖与其他服务的直接点到点连接或 API。这就释放了组织在任何产品中更高效地使用海量数据的潜力,甚至可以以独特而强大的方式整合来自不同产品的数据。
1.5.5 高可访问的数据利于业务变更
事件流包含对业务操作至关重要的核心领域事件。虽然各团队可能重组,项目也会一个接一个地开展,但是重要的核心领域数据可以随时提供给任何有需要的新产品,这个能力独立于任何具体的实现沟通结构。这为业务提供了空前的灵活性,因为访问核心领域事件不再依赖于任何具体的实现。
1.6 异步的事件驱动型微服务
事件驱动型微服务支持业务逻辑转换和操作以满足界限上下文的需求。这些应用的任务是满足这些需求并向其他下游消费者发布任何必要的事件。下面是使用事件驱动型微服务的主要优点。
颗粒化
服务可以恰当地映射界限上下文,并且在业务需求发生变化时可被轻易重写。
可伸缩性
单独的服务可以根据需要进行扩缩容。
技术灵活性
服务可以使用最合适的语言和技术,也可以使用最前沿的技术进行简单的原型开发。
业务需求灵活性
颗粒化微服务的所有权很容易进行重组。与大型服务相比,它们有更少的跨团队依赖,并且组织可以快速响应业务需求的变化,否则这些变化会受到数据访问障碍的阻碍。
松耦合
事件驱动型微服务耦合于领域数据而不是特定的实现 API。数据 schema 能极大地改进管理数据变更的方式,这部分内容会在第 3 章中详细介绍。
持续交付支持
发布一个小型的、模块化的微服务很容易,并且可在需要时将其回滚。
高可测试性
微服务的依赖比大型单体服务少,因此可以更容易地模拟所需的测试端点并保证适当的代码覆盖率。
使用事件驱动型微服务的示例团队
现在回到前面那个团队早期做决策的时候,这次他们使用的是事件驱动的数据沟通结构。
一个新的业务需求被提交给了这个团队。此需求与该团队当前负责的产品相关,但它们的差异程度也足以将其实现到单独的一个服务里。将其添加到当前的服务是否违反了单一权责原则且扩大了当前所定义的界限上下文?或者它就是对现有服务的一次简单扩展,可能添加了一些新的相关数据和功能?
之前所说的技术问题,比如确定从哪里获取数据和如何接收数据、批处理的同步,以及需要实现同步 API 等,在采用事件驱动型微服务后都基本被消除了。该团队可以启动一个新的微服务并从事件流中获取必要的数据。如果需要,可以一直回溯到事件流中最开始时的数据。团队完全可以整合用于其他服务的公共数据,只要这些数据只用于实现新的界限上下文需求。这些数据的存储和结构完全由该团队来决定,他们可以选择保留哪些字段和抛弃哪些字段。
业务风险也得到了缓解,因为小型的、细粒度的服务允许由单一团队来负责,团队可以根据需要进行规模调整和重组。当团队变得太大而无法在单一业务所有者下进行管理时,可以根据需要拆分并重新分配微服务的权责。事件数据的所有权跟着生产事件的服务移动,并且所做出的组织决策会减少将来执行任务时所需的跨团队通信。
如果创建新服务和获取所需数据的开销很小,那么微服务的特性就防止了意大利面式代码和扩大化的单体应用的出现。可伸缩性的关注点现在聚焦于单个事件处理服务,这些服务可以根据需要变更 CPU、内存、磁盘和实例计数的规模。剩余的伸缩需求被转移到了数据沟通结构上,数据沟通结构必须确保能够处理服务从事件流中消费和往事件流生产事件所产生的各种负载。
然而,要实现这一切,团队需要确保数据确实存在于数据沟通结构中,并且他们必须有轻松启动和管理一系列微服务的方法。这就需要整个组织接受事件驱动型微服务架构。
1.7 同步式微服务
微服务可以通过事件(本书所建议的方法)实现成异步的形式,或者实现成同步的形式(同步服务通常出现在面向服务的架构中)。同步的微服务通常采用“请求–响应”的方法来实现,服务间通信直接通过 API 来满足业务需求。
1.7.1 同步式微服务的缺点
同步式微服务有很多问题,这使得它们难以大规模地使用。这并不是说一个公司不能用同步式微服务来获得成功,Netflix、Lyft、Uber 和 Facebook 等公司的成功就证明了这一点。但是也有许多公司使用过时的、混乱的意大利面式代码和单体应用发了大财,所以不要把公司的最终成功与底层架构的质量混为一谈。市面上有许多介绍如何实现同步式微服务的图书,可以读读它们以更好地理解同步方法。2
2例如由 Sam Newman 所著的《微服务设计》以及由 Kasun Indrasiri 和 Prabath Siriwardena 合著的 Microservices for the Enterprise。
此外,请注意,无论是点到点的“请求–响应”型微服务还是异步的事件驱动型微服务,严格来说都没有绝对的优劣之分。在一个组织里它们都有自己的一席之地,因为有些任务更适合采用其中的一种模式。然而,我以及我的许多同行和同事的经验表明,事件驱动型微服务架构提供了无与伦比的灵活性,这是同步的“请求–响应”型微服务所不具备的。当你继续阅读本书时可能会同意我的观点,至少你会了解它们的优缺点。
以下是同步的“请求–响应”型微服务的几大缺点。
点到点的耦合
同步的微服务依赖其他服务来帮助它们执行业务任务。那些服务同样有自己的依赖服务,而这些依赖服务又有自己的依赖服务,以此类推。这会导致过度的扇出,且难以跟踪哪些服务要为实现业务逻辑的特定部分负责。服务之间的连接数量多得惊人,这会进一步增强现有的沟通结构并使得未来的变更更加困难。
有依赖的可伸缩性
你自身服务的扩容能力依赖于所有依赖服务的扩容能力,并且直接与通信的扇出程度有关。实现技术可能是可伸缩性的瓶颈。高度变化的负载和激增的请求模式会使情况更加复杂,所有这些都需要在整个体系架构内同步处理。
服务故障的处理
如果一个依赖服务关闭,则必须做出如何处理这个异常的决策。生态系统中的微服务越多,决定如何处理停机、何时重试、何时失败以及何时恢复以确保数据一致性就越困难。
API 版本和依赖管理
多个 API 定义和服务版本通常需要同时存在。强制让客户端升级到最新的 API 并不总是可行或可取的。这会增加跨多个服务协调 API 更改请求的复杂性,特别是当它们伴随着对底层数据结构的更改时。
数据访问耦合于实现
同步的微服务在访问外部数据时会遇到所有跟传统的服务相同的问题。虽然有减少访问外部数据需求的服务设计策略,但微服务通常还是需要访问来自其他服务的通用数据。这就把数据访问和可伸缩性的责任重新放在了实现沟通结构上。
分布式的单体应用
服务可以被组合成一个分布式的单体应用,在它们之间进行许多相互交织的调用。这种情况通常出现在团队分解一个单体应用,并决定使用同步的点到点调用来模拟这个单体应用内部的边界时。点对点的服务可以很容易地模糊界限上下文的边界,因为对远程系统的函数调用可以一行一行地插入现有单体式代码。
测试
集成测试可能很困难,因为每个服务都需要完全可操作的依赖项,而这些依赖项又需要自己的依赖项。在单元测试中将它们截取出来可能是可行的,但并不足以满足更广泛的测试需求。
1.7.2 同步式微服务的优点
同步式微服务有许多不可否认的优点。一些数据访问模式很适合直接的“请求–响应”耦合,比如验证用户身份和 AB 测试中的上报。与外部第三方解决方案的集成大多数时候使用同步机制,并且通常使用 HTTP 以提供一种灵活的、与语言无关的通信机制。
跟踪跨多个系统的操作在同步环境下会比异步环境下更简单。详细的日志可以显示在哪些系统上调用了哪些函数,从而实现业务操作的高度可调试性和可见性。
承载 Web 和移动体验的服务通常由“请求–响应”设计提供支持,无论它们是同步还是异步性质。客户会收到完全满足了其需求的及时响应。
经验因素也是非常重要的,尤其是当今市场上的许多开发者往往对同步、单体类型的编码更有经验。总的来说,这使得获取熟悉同步系统的人才比获取熟悉异步事件驱动开发的人才更容易。
一个公司的架构很少完全基于事件驱动型微服务。混合式架构肯定会成为常态,同步和异步的解决方案会根据问题空间的需要进行并行部署。
1.8 小结
沟通结构指导着在组织的整个生命周期中如何创建和管理软件。数据沟通结构通常是开发不完全且临时的,但是,如事件驱动系统所体现的那样,引入一组持久的、易于访问的领域事件可以使得更小的、专门构建的实现得以应用。