原文:
zh.annas-archive.org/md5/52026E2A45414F981753F74B874EEB00
译者:飞龙
第三章:应用微服务概念
微服务是好的,但如果没有得到妥善构思,也可能是一种恶。错误的微服务解释可能导致无法挽回的失败。
本章将探讨实际实施微服务的技术挑战。它还将提供成功微服务开发的关键设计决策指南。还将审查关于微服务的一些常见关注点的解决方案和模式。本章还将审查企业规模微服务开发中的挑战,以及如何克服这些挑战。更重要的是,将在最后建立一个微服务生态系统的能力模型。
在本章中,您将了解以下内容:
-
在开发微服务时需要考虑不同设计选择和模式之间的权衡
-
开发企业级微服务的挑战和反模式
-
微服务生态系统的能力模型
模式和常见设计决策
近年来,微服务已经获得了巨大的流行。它们已经成为架构师的首选,将 SOA 放在了后台。尽管承认微服务是开发可扩展的云原生系统的工具,但成功的微服务需要精心设计以避免灾难。微服务并非一刀切,也不是解决所有架构问题的通用解决方案。
一般来说,微服务是构建轻量级、模块化、可扩展和分布式系统的绝佳选择。过度设计、错误的用例和误解很容易将系统变成灾难。在选择正确的用例对于开发成功的微服务至关重要,同样重要的是通过进行适当的权衡分析做出正确的设计决策。在设计微服务时需要考虑许多因素,如下面的详细部分所述。
建立适当的微服务边界
关于微服务最常见的问题之一是关于服务的大小。微服务可以有多大(迷你单片)或多小(纳米服务),或者是否有合适大小的服务?大小真的很重要吗?
一个快速的答案可能是“每个微服务一个 REST 端点”,或者“少于 300 行代码”,或者“执行单一职责的组件”。但在我们选择这些答案之前,还有很多分析工作要做,以了解我们服务的边界。
领域驱动设计(DDD)定义了有界上下文的概念。有界上下文是一个较大的领域或系统的子域或子系统,负责执行特定的功能。
提示
在domainlanguage.com/ddd/
了解更多关于 DDD 的信息。
以下图表是领域模型的一个示例:
在金融后勤系统中,系统发票、会计、结算等代表不同的有界上下文。这些有界上下文是与业务能力密切相关的强烈隔离的领域。在金融领域,发票、会计和结算是不同的业务能力,通常由财务部门下的不同子单位处理。
有界上下文是确定微服务边界的好方法。每个有界上下文可以映射到一个单独的微服务。在现实世界中,有界上下文之间的通信通常耦合较少,而且经常是断开的。
尽管真实世界的组织边界是建立有界上下文的最简单机制,但由于组织结构内在问题,这些边界在某些情况下可能会被证明是错误的。例如,业务能力可以通过不同的渠道进行交付,如前台办事处、在线、漫游代理等。在许多组织中,业务单位可能是根据交付渠道而不是实际的基础业务能力进行组织的。在这种情况下,组织边界可能无法提供准确的服务边界。
自上而下的领域分解可能是建立正确有界上下文的另一种方式。
建立微服务边界没有银弹,通常这是非常具有挑战性的。在将单块应用程序迁移到微服务的情况下,建立边界要容易得多,因为现有系统的服务边界和依赖关系是已知的。另一方面,在绿地微服务开发中,依赖关系很难事先建立。
设计微服务边界的最实用方法是将手头的情景运行通过多种可能的选项,就像服务试纸测试一样。请记住,可能有多个条件匹配给定情景,这将导致权衡分析。
以下情景可能有助于定义微服务边界。
自主功能
如果正在审查的功能本质上是自主的,那么它可以被视为微服务边界。自主服务通常对外部功能的依赖较少。它们接受输入,使用内部逻辑和数据进行计算,并返回结果。所有实用功能,如加密引擎或通知引擎,都是直接的候选者。
接受订单、处理订单,然后通知卡车服务的交付服务是另一个自主服务的例子。基于缓存的座位可用性信息进行在线航班搜索是另一个自主功能的例子。
可部署单元的大小
大多数微服务生态系统将利用自动化,如自动集成、交付、部署和扩展。覆盖更广泛功能的微服务会导致更大的部署单元。大型部署单元在自动文件复制、文件下载、部署和启动时间方面存在挑战。例如,服务的大小随着它实现的功能密度的增加而增加。
一个良好的微服务确保其可部署单元的大小保持可管理。
最合适的功能或子域
分析从单块应用程序中分离出来的最有用的组件是非常重要的。这在将单块应用程序分解为微服务时特别适用。这可能基于资源密集性、所有权成本、业务利益或灵活性等参数。
在典型的酒店预订系统中,大约 50-60%的请求是基于搜索的。在这种情况下,移出搜索功能可能会立即带来灵活性、业务利益、成本降低、资源释放等。
多语言体系结构
微服务的一个关键特征是支持多语言体系结构。为了满足不同的非功能性和功能性要求,组件可能需要不同的处理方式。这可能是不同的体系结构、不同的技术、不同的部署拓扑等。当组件被识别出来时,要根据多语言体系结构的要求对它们进行审查。
在前面提到的酒店预订场景中,预订微服务可能需要事务完整性,而搜索微服务可能不需要。在这种情况下,预订微服务可能会使用 ACID 兼容的数据库,如 MySQL,而搜索微服务可能会使用最终一致的数据库,如 Cassandra。
选择性扩展
选择性扩展与先前讨论的多语言体系结构相关。在这种情况下,所有功能模块可能不需要相同级别的可扩展性。有时,根据可扩展性要求确定边界可能是合适的。
例如,在酒店预订场景中,搜索微服务必须比许多其他服务(如预订微服务或通知微服务)扩展得多,因为搜索请求的速度更快。在这种情况下,一个独立的搜索微服务可以在 Elasticsearch 或内存数据网格上运行,以获得更好的响应。
小而敏捷的团队
微服务使得敏捷开发成为可能,小而专注的团队开发饼图的不同部分。可能存在不同组织或不同地理位置的团队,或者技能水平不同的团队构建系统的部分的情况。例如,在制造业中,这种方法是常见的做法。
在微服务的世界中,这些团队中的每一个都构建不同的微服务,然后将它们组合在一起。尽管这不是分解系统的理想方式,但组织可能最终会陷入这种情况。因此,这种方法不能完全被排除。
在在线产品搜索场景中,一个服务可以根据客户的需求提供个性化的选项。这可能需要复杂的机器学习算法,因此需要一个专业团队。在这种情况下,这个功能可以由一个独立的专业团队构建为一个微服务。
单一责任
理论上,单一责任原则可以应用于方法、类或服务。然而,在微服务的背景下,并不一定对应于单个服务或端点。
更实际的方法可能是将单一责任转化为单一业务能力或单一技术能力。根据单一责任原则,一个责任不能被多个微服务共享。同样,一个微服务不应执行多个责任。
然而,也可能存在特殊情况,其中单个业务能力分布在多个服务之间。其中一种情况是管理客户档案,可能会出现使用两个不同的微服务来管理读取和写入的情况,使用命令查询责任分离(CQRS)方法来实现一些质量属性。
可复制性或可变性
创新和速度在 IT 交付中至关重要。微服务的边界应该被确定为每个微服务都可以轻松地从整个系统中分离出来,重新编写的成本最小。如果系统的一部分只是一个实验,最好将其作为一个微服务隔离起来。
一个组织可能会开发一个推荐引擎或客户排名引擎作为一个实验。如果业务价值没有实现,那么就放弃该服务,或者用另一个替换它。
许多组织遵循创业公司的模式,重视功能的实现和快速交付。这些组织可能不会过分担心架构和技术,而是关注哪些工具或技术可以更快地提供解决方案。组织越来越倾向于通过组合几个服务来开发最小可行产品(MVPs),并允许系统不断演进。在系统不断演进、服务逐渐重写或替换的情况下,微服务在这些情况下发挥着至关重要的作用。
耦合和内聚
耦合和内聚是决定服务边界的两个最重要的参数。微服务之间的依赖关系必须经过仔细评估,以避免高度耦合的接口。功能分解,以及建模依赖树,可以帮助确定微服务的边界。避免过于啰嗦的服务、过多的同步请求-响应调用以及循环同步依赖是三个关键点,因为这些很容易破坏系统。一个成功的方程是在微服务内保持高内聚性,在微服务之间保持松耦合。除此之外,确保事务边界不要跨微服务。一流的微服务将在接收到事件作为输入时做出反应,执行一些内部功能,最终发送另一个事件。作为计算功能的一部分,它可能会读取和写入数据到自己的本地存储。
将微服务视为产品
领域驱动设计还建议将一个有界上下文映射到一个产品。根据领域驱动设计,每个有界上下文都是一个理想的产品候选者。将微服务视为一个独立的产品。当微服务边界确定后,从产品的角度评估它们,看它们是否真的符合产品的标准。对于业务用户来说,从产品的角度思考边界会更容易。产品边界可能有许多参数,如针对的社区、部署灵活性、可销售性、可重用性等。
设计通信风格
微服务之间的通信可以设计为同步(请求-响应)或异步(发送并忘记)风格。
同步风格通信
以下图表显示了一个示例的请求/响应式服务:
在同步通信中,没有共享状态或对象。当调用者请求服务时,它传递所需的信息并等待响应。这种方法有许多优点。
应用程序是无状态的,从高可用性的角度来看,可以有许多活跃的实例运行,接受流量。由于没有其他基础设施依赖,如共享的消息服务器,管理开销更少。在任何阶段出现错误时,错误将立即传播回调用者,使系统保持一致状态,而不会损害数据完整性。
同步请求-响应通信的缺点是用户或调用者必须等待请求的过程完成。因此,调用线程将等待响应,因此这种方式可能会限制系统的可扩展性。
同步风格在微服务之间添加了硬依赖关系。如果服务链中的一个服务失败,整个服务链将失败。为了使服务成功,所有依赖的服务都必须正常运行。许多故障场景必须使用超时和循环来处理。
异步风格通信
以下图表是一个设计用于接受异步消息作为输入,并异步发送响应供其他消费者使用的服务:
异步风格基于反应式事件循环语义,解耦了微服务。这种方法提供了更高级别的可扩展性,因为服务是独立的,可以在内部生成线程来处理负载的增加。当负载过重时,消息将被排队在消息服务器中以供以后处理。这意味着如果其中一个服务减速,它不会影响整个链条。这提供了更高级别的服务之间的解耦,因此维护和测试将更简单。
缺点是它对外部消息服务器有依赖性。处理消息服务器的容错性是复杂的。消息通常使用主/备份语义。因此,处理消息系统的持续可用性更难实现。由于消息通常使用持久性,需要更高级别的 I/O 处理和调优。
如何决定选择哪种风格?
这两种方法都有各自的优点和限制。无法只使用一种方法开发系统。根据用例需要,需要结合两种方法。原则上,异步方法非常适合构建真正可扩展的微服务系统。然而,试图将所有东西都建模为异步会导致复杂的系统设计。
在终端用户点击 UI 获取配置文件详细信息的情境中,以下示例是什么样子的?
这可能是向后端系统发出简单查询以在请求-响应模型中获取结果。这也可以通过将消息推送到输入队列,并在输出队列中等待响应,直到收到给定关联 ID 的响应来以异步风格建模。然而,尽管我们使用异步消息传递,用户仍然在整个查询的持续时间内被阻塞。
另一个用例是用户点击 UI 搜索酒店,如下图所示:
这与先前的情景非常相似。然而,在这种情况下,我们假设这个业务功能在将酒店列表返回给用户之前会触发一系列内部活动。例如,当系统接收到这个请求时,它会计算客户排名,根据目的地获取优惠,根据客户偏好获取推荐,根据客户价值和收入因素优化价格等等。在这种情况下,我们有机会并行地执行许多这些活动,以便在向客户呈现结果之前汇总所有这些结果。如前图所示,几乎任何计算逻辑都可以插入到监听IN队列的搜索管道中。
在这种情况下,一个有效的方法是从同步请求响应开始,以后根据需要引入异步风格。
以下示例展示了完全异步风格的服务交互:
当用户点击预订功能时,服务被触发。同样,它是一种同步风格的通信。预订成功后,它会向客户的电子邮件地址发送消息,向酒店的预订系统发送消息,更新缓存库存,更新忠诚积分系统,准备发票,或许还有其他操作。与其让用户陷入长时间等待状态,更好的方法是将服务分解成片段。让用户等待,直到预订服务创建了一个预订记录。在成功完成后,将发布一个预订事件,并向用户返回确认消息。随后,所有其他活动将并行异步地发生。
在这三个例子中,用户都需要等待响应。使用新的 Web 应用程序框架,可以异步发送请求,并定义回调方法,或设置观察者以获取响应。因此,用户不会完全被阻止执行其他活动。
总的来说,在微服务世界中,异步风格总是更好的,但确定正确的模式应纯粹基于优点。如果在异步风格中建模事务没有优点,那么在找到吸引人的案例之前,请使用同步风格。在建模用户驱动的请求时,使用反应式编程框架以避免复杂性,以异步风格建模。
微服务的编排
可组合性是服务设计原则之一。这导致了谁负责组合服务的混乱。在 SOA 世界中,ESB 负责组合一组细粒度的服务。在一些组织中,ESB 扮演代理的角色,服务提供者自己组合和公开粗粒度的服务。在 SOA 世界中,处理这种情况有两种方法。
第一种方法是编排,如下图所示:
在编排方法中,多个服务被组合在一起以获得完整的功能。一个中央大脑充当编排者。如图所示,订单服务是一个组合服务,将编排其他服务。主进程可能有顺序和并行分支。每个任务将由原子任务服务完成,通常是一个 Web 服务。在 SOA 世界中,ESB 扮演编排的角色。编排的服务将由 ESB 作为组合服务公开。
第二种方法是协同,如下图所示:
在协同方法中,没有中央大脑。在这种情况下,一个事件,例如预订事件,由生产者发布,许多消费者等待事件,并独立对传入事件应用不同的逻辑。有时,事件甚至可以嵌套,其中消费者可以发送另一个事件,该事件将被另一个服务消费。在 SOA 世界中,调用者向 ESB 推送消息,下游流程将由消费服务自动确定。
微服务是自治的。这基本上意味着在理想情况下,完成其功能所需的所有组件都应该在服务内部。这包括数据库、内部服务的编排、内在状态管理等。服务端点提供粗粒度的 API。只要不需要外部接触点,这是完全可以的。但实际上,微服务可能需要与其他微服务交流以完成其功能。
在这种情况下,协同是连接多个微服务的首选方法。遵循自治原则,一个组件坐在微服务外部并控制流程并不是理想的选择。如果用例可以以协同方式建模,那将是处理情况的最佳方式。
但在所有情况下可能无法建模协同。这在下图中显示:
在上面的例子中,预订和客户是两个微服务,具有明确定义的功能责任。当预订希望在创建预订时获取客户偏好时,可能会出现这样的情况。在开发复杂系统时,这些是非常正常的情况。
我们能否将客户移到预订中,以便预订可以自行完成?如果根据各种因素将客户和预订确定为两个微服务,将客户移到预订可能不是一个好主意。在这种情况下,我们迟早会遇到另一个单片应用程序。
我们能否将预订客户的调用设置为异步?这个例子在下图中显示:
预订需要客户偏好才能进行,因此可能需要对客户进行同步阻塞调用。通过异步建模来进行适配实际上并没有意义。
我们能否只取出编排部分,然后创建另一个复合微服务,然后将预订和客户组合起来?
这在微服务内部组合多个组件的方法中是可以接受的。但创建一个复合微服务可能不是一个好主意。我们最终会创建许多与业务不对齐的微服务,这些微服务将不是自治的,并且可能会导致许多细粒度的微服务。
我们能否通过在预订中保留偏好数据的从属副本来复制客户偏好?
只要主服务器发生变化,变更就会传播。在这种情况下,预订可以使用客户偏好而无需进行调用。这是一个有效的想法,但我们需要仔细分析这一点。今天我们复制客户偏好,但在另一种情况下,我们可能希望联系客户服务,看看客户是否被列入了预订的黑名单。在决定复制哪些数据时,我们必须非常小心。这可能会增加复杂性。
微服务中有多少个端点?
在许多情况下,开发人员对每个微服务的端点数量感到困惑。问题实际上是是否限制每个微服务只有一个端点还是多个端点:
端点的数量并不是一个决定性因素。在某些情况下,可能只有一个端点,而在其他一些情况下,一个微服务可能有多个端点。例如,考虑一个传感器数据服务,它收集传感器信息,并具有两个逻辑端点:创建和读取。但为了处理 CQRS,我们可能会创建两个单独的物理微服务,就像前面图表中的预订一样。多语言架构可能是另一种情况,我们可能会将端点拆分成不同的微服务。
考虑到通知引擎,通知将会在事件发生时发送出去。通知的过程,比如数据准备、人员识别和传递机制,对于不同的事件是不同的。此外,我们可能希望在不同的时间窗口内以不同的方式扩展这些过程。在这种情况下,我们可能决定将每个通知端点拆分成一个单独的微服务。
在另一个例子中,积分微服务可能有多个服务,比如积分、兑换、转移和余额。我们可能不希望对这些服务进行不同对待。所有这些服务都是相互连接的,并且使用积分表进行数据操作。如果我们为每个服务创建一个端点,我们最终会陷入这样一种情况:许多细粒度的服务从同一个数据存储或复制的相同数据存储中访问数据。
简而言之,端点的数量不是一个设计决策。一个微服务可以承载一个或多个端点。为微服务设计适当的边界上下文更为重要。
每个虚拟机一个微服务,还是多个?
一个微服务可以通过复制部署来实现可扩展性和可用性,部署在多个虚拟机(VMs)中。这是一个不需要大脑的决定。问题是是否可以在一个虚拟机中部署多个微服务?这种方法有其利弊。这个问题通常在服务简单且流量较少时出现。
考虑一个例子,我们有一对微服务,总交易量少于 10 笔每分钟。还假设可用的最小虚拟机大小是 2 核 8GB 内存。进一步假设在这种情况下,一个 2 核 8GB 的虚拟机可以处理 10-15 笔每分钟的交易而不会出现性能问题。如果我们为每个微服务使用不同的虚拟机,可能不是成本有效的,因为许多供应商根据核心数收费。
解决这个问题的最简单方法是提出几个问题:
-
虚拟机是否有足够的容量来在高峰使用时运行这两个服务?
-
我们是否想要以不同的方式处理这些服务以实现 SLA(选择性扩展)?例如,对于可伸缩性,如果我们有一个全包式虚拟机,我们将不得不复制复制所有服务的虚拟机。
-
是否存在冲突的资源需求?例如,不同的操作系统版本,JDK 版本等。
如果您所有的答案都是否,那么也许我们可以从共部署开始,直到遇到改变部署拓扑的情况。然而,我们必须确保这些服务不共享任何东西,并且作为独立的操作系统进程运行。
话虽如此,在一个拥有成熟的虚拟化基础设施或云基础设施的组织中,这可能并不是一个巨大的问题。在这样的环境中,开发人员不需要担心服务运行的位置。开发人员甚至可能不考虑容量规划。服务将部署在计算云中。根据基础设施的可用性、SLA 和服务的性质,基础设施自我管理部署。AWS Lambda 就是这样一个服务的很好的例子。
规则引擎-共享还是嵌入?
规则是任何系统的重要组成部分。例如,一个资格服务可能在做出是或否决定之前执行多个规则。我们要么手工编写规则,要么使用规则引擎。许多企业在规则存储库中集中管理规则,并在中央执行它们。这些企业规则引擎主要用于为业务提供编写和管理规则以及从中央存储库重用规则的机会。Drools是一种流行的开源规则引擎。IBM、FICO 和 Bosch 是商业领域的一些先驱。这些规则引擎提高了生产率,实现了规则、事实、词汇的重用,并使用 rete 算法提供更快的规则执行。
在微服务的背景下,中央规则引擎意味着从微服务向中央规则引擎扩展调用。这也意味着服务逻辑现在在两个地方,一部分在服务内部,一部分在服务外部。然而,在微服务的背景下,目标是减少外部依赖:
如果规则足够简单,数量不多,仅在服务范围内使用,并且不向业务用户公开进行编写,那么手工编码业务规则可能比依赖企业规则引擎更好:
如果规则很复杂,限于服务上下文,并且不提供给业务用户,那么最好在服务内使用嵌入式规则引擎:
如果规则由业务管理和编写,或者规则很复杂,或者我们正在从其他服务领域重用规则,那么具有本地嵌入执行引擎的中央编写存储库可能是更好的选择。
请注意,这必须经过仔细评估,因为并非所有供应商都支持本地规则执行方法,可能存在技术依赖,比如只能在特定应用服务器内运行规则等。
BPM 和工作流的作用
业务流程管理(BPM)和智能业务流程管理(iBPM)是用于设计、执行和监控业务流程的工具套件。
BPM 的典型用例包括:
-
协调长时间运行的业务流程,其中一些流程由现有资产实现,而其他一些领域可能是利基领域,并且没有具体的流程实现。BPM 允许组合这两种类型,并提供端到端的自动化流程。这通常涉及系统和人员的交互。
-
以流程为中心的组织,比如那些实施了六西格玛的组织,希望监控其流程以持续改进效率。
-
采用自上而下的方法对业务流程进行再造,重新定义组织的业务流程。
BPM 适用于微服务世界的两种情况:
第一个场景是业务流程再造,或者说是将端到端的长时间运行的业务流程串联起来,正如前面所述。在这种情况下,BPM 在更高的层面上运行,它可以通过串联多个粗粒度的微服务、现有的遗留连接器和人员交互来自动化跨职能、长时间运行的业务流程。如前图所示,贷款批准 BPM 调用微服务以及遗留应用服务。它还整合了人员任务。
在这种情况下,微服务是实现子流程的无头服务。从微服务的角度来看,BPM 只是另一个消费者。在这种方法中需要注意避免接受来自 BPM 流程的共享状态以及将业务逻辑转移到 BPM 中:
第二个场景是监控流程,并优化其效率。这与完全自动化、异步编排的微服务生态系统密切相关。在这种情况下,微服务和 BPM 作为独立的生态系统运行。微服务在各种时间框架内发送事件,比如流程开始、状态变化、流程结束等。这些事件被 BPM 引擎用于绘制和监控流程状态。对于这种情况,我们可能不需要一个完整的 BPM 解决方案,因为我们只是模拟一个业务流程来监控其效率。在这种情况下,订单交付流程并不是 BPM 的实现,而更像是一个监控仪表板,用于捕获和显示流程的进展。
总之,BPM 仍然可以在更高的层面上用于组合多个微服务的情况,其中端到端的跨职能业务流程通过自动化系统和人员交互进行建模。一个更好、更简单的方法是拥有一个业务流程仪表板,微服务向其提供状态变化事件,就像第二个场景中提到的那样。
微服务能共享数据存储吗?
原则上,微服务应该抽象出展示、业务逻辑和数据存储。如果按照指南拆分服务,每个微服务逻辑上可以使用独立的数据库:
在前图中,产品和订单微服务共享一个数据库和一个数据模型。共享数据模型、共享模式和共享表在开发微服务时是灾难的根源。这在开始阶段可能还好,但在开发复杂的微服务时,我们往往会在数据模型之间添加关系,添加连接查询等。这可能导致物理数据模型紧密耦合。
如果服务只有少量表,可能不值得投资一个像 Oracle 数据库实例这样的完整数据库实例。在这种情况下,仅进行模式级别的分离就足够了:
可能存在一些情况,我们倾向于考虑为多个服务使用共享数据库。以企业级管理的客户数据存储库或主数据为例,客户注册和客户分割微服务在逻辑上共享相同的客户数据存储库。
如前图所示,在这种情况下的另一种方法是通过为这些服务添加一个本地事务数据存储来将微服务的事务数据存储与企业数据存储库分开。这将帮助服务在重新设计本地数据存储以优化其用途时具有灵活性。企业客户存储库在客户数据存储库发生任何更改时发送更改事件。同样,如果事务数据存储中的任何更改,这些更改都必须发送到中央客户存储库。
设置事务边界
操作系统中的事务用于通过将多个操作组合成一个原子块来维护存储在 RDBMS 中的数据的一致性。它们要么提交整个操作,要么回滚整个操作。分布式系统遵循分布式事务的概念,采用两阶段提交。如果异构组件(如 RPC 服务、JMS 等)参与事务,则特别需要这一点。
在微服务中是否有事务的位置?事务并不是坏事,但应该谨慎使用,通过分析我们要做什么来使用事务。
对于给定的微服务,可以选择像 MySQL 这样的 RDBMS 作为后备存储,以确保 100%的数据完整性,例如库存或库存管理服务,其中数据完整性至关重要。在微系统中使用本地事务定义事务边界是合适的。然而,在微服务环境中应避免分布式全局事务。需要进行适当的依赖性分析,以尽可能确保事务边界不跨越两个不同的微服务。
修改用例以简化事务要求
最终一致性比跨多个微服务的分布式事务是更好的选择。最终一致性减少了很多开销,但应用程序开发人员可能需要重新思考他们编写应用程序代码的方式。这可能包括重新设计函数、对操作进行排序以最小化故障、批量插入和修改操作、重新设计数据结构,最后是抵消效果的补偿操作。
一个经典的问题是酒店预订用例中的最后一个房间售罄的情况。如果只剩下一个房间,有多个客户预订这个可用的房间怎么办?有时业务模型的变化会使这种情况影响较小。我们可以设置一个“预订不足”配置文件,其中可预订房间的实际数量可以低于实际房间数量(可预订=可用-3),以预期会有一些取消。在这个范围内的任何预订都将被接受为“待确认”,只有在确认付款后才会向客户收费。预订将在一定的时间窗口内得到确认。
现在考虑一种情况,我们正在在像 CouchDB 这样的 NoSQL 数据库中创建客户配置文件。在传统的 RDBMS 方法中,我们首先插入客户,然后一次性插入客户的地址、配置文件详细信息,然后是偏好。在使用 NoSQL 时,我们可能不会执行相同的步骤。相反,我们可能准备一个包含所有细节的 JSON 对象,并一次性将其插入 CouchDB。在这种情况下,不需要显式的事务边界。
分布式事务场景
在微服务中,如果需要,理想的情况是在微服务内使用本地事务,并完全避免分布式事务。可能存在这样的情况,即在执行一个服务结束时,我们可能希望向另一个微服务发送消息。例如,假设旅行预订有一个轮椅请求。一旦预订成功,我们将不得不向另一个处理辅助预订的微服务发送一个轮椅预订的消息。预订请求本身将在本地事务上运行。如果发送此消息失败,我们仍然处于事务边界内,可以回滚整个事务。如果我们创建了一个预订并发送了消息,但在发送消息后遇到了预订错误,预订事务失败,随后预订记录被回滚?现在我们陷入了一个不必要创建孤立轮椅预订的情况:
我们可以通过几种方式来解决这种情况。第一种方法是延迟发送消息直到最后。这确保在发送消息后减少任何失败的机会。即使在发送消息后发生故障,也会运行异常处理例程,也就是我们发送一个补偿消息来撤销轮椅预订。
服务端点设计考虑
微服务的一个重要方面是服务设计。服务设计有两个关键元素:合同设计和协议选择。
合同设计
服务设计的首要原则是简单。服务应该设计为消费者消费。复杂的服务合同会降低服务的可用性。KISS(保持简单愚蠢)原则帮助我们更快地构建更高质量的服务,并降低维护和更换的成本。YAGNI(你不会需要它)是支持这一想法的另一个原则。预测未来的需求并构建系统实际上并不能未雨绸缪。这导致了大量的前期投资以及更高的维护成本。
进化式设计是一个很好的概念。只需进行足够的设计来满足当下的需求,并不断改变和重构设计以适应新功能的需求。话虽如此,除非有强有力的治理措施,否则这可能并不简单。
消费者驱动的合同(CDC)是支持进化式设计的一个很好的想法。在许多情况下,当服务合同发生变化时,所有消费应用都必须经过测试。这使得变更变得困难。CDC 有助于建立对消费应用的信心。CDC 倡导每个消费者以测试用例的形式向提供者提供他们的期望,以便提供者在服务合同发生变化时将它们用作集成测试。
波斯特尔定律在这种情况下也是相关的。波斯特尔定律主要涉及 TCP 通信;然而,这同样适用于服务设计。在服务设计方面,服务提供者在接受消费者请求时应尽可能灵活,而服务消费者应遵守与提供者达成的合同。
协议选择
在 SOA 世界中,HTTP/SOAP 和消息传递是服务交互的默认协议。微服务遵循相同的服务交互设计原则。松耦合也是微服务世界的核心原则之一。
微服务将应用程序分解为许多物理独立的可部署服务。这不仅增加了通信成本,还容易受到网络故障的影响。这也可能导致服务性能不佳。
面向消息的服务
如果我们选择异步通信方式,用户是断开连接的,因此响应时间不会直接受到影响。在这种情况下,我们可以使用标准的 JMS 或 AMQP 协议进行通信,JSON 作为有效载荷。通过 HTTP 进行消息传递也很受欢迎,因为它减少了复杂性。许多新进入消息服务的公司支持基于 HTTP 的通信。异步 REST 也是可能的,在调用长时间运行的服务时非常方便。
HTTP 和 REST 端点
对于互操作性、协议处理、流量路由、负载平衡、安全系统等方面,通过 HTTP 进行通信总是更好的。由于 HTTP 是无状态的,它更适合处理无关联的无状态服务。大多数开发框架、测试工具、运行时容器、安全系统等都更友好地支持 HTTP。
随着 REST 和 JSON 的流行和接受,它是微服务开发者的默认选择。HTTP/REST/JSON 协议栈使构建互操作系统变得非常容易和友好。HATEOAS 是一种新兴的设计模式,用于设计渐进式渲染和自助导航。正如前一章讨论的那样,HATEOAS 提供了一种将资源链接在一起的机制,以便消费者可以在资源之间导航。RFC 5988 - Web Linking 是另一个即将推出的标准。
优化的通信协议
如果服务的响应时间要求严格,那么我们需要特别关注通信方面。在这种情况下,我们可以选择替代协议,如 Avro、Protocol Buffers 或 Thrift 来进行服务之间的通信。但这限制了服务的互操作性。权衡是在通信开销与在多个服务中复制库之间。自定义二进制协议需要仔细评估,因为它们在消费者和生产者两端绑定了本地对象。这可能会导致类版本不匹配的问题,比如基于 Java 的 RPC 风格通信中的类版本不匹配问题。
API 文档
最后一点:一个好的 API 不仅要简单,还应该有足够的文档供消费者使用。今天有许多工具可用于记录 REST-based 服务,如 Swagger、RAML 和 API Blueprint。
处理共享库
微服务背后的原则是它们应该是自治的和自包含的。为了遵循这个原则,可能会出现需要复制代码和库的情况。这些可以是技术库或功能组件。
例如,对于航班升舱的资格将在办理登机手续时和登机时进行检查。如果办理登机手续和登机是两个不同的微服务,我们可能需要在两个服务中都复制资格规则。这是添加依赖与代码重复之间的权衡。
与添加额外的依赖相比,嵌入代码可能更容易,因为它能够更好地管理发布和提高性能。但这违反了 DRY 原则。
注意
DRY 原则
系统中的每一条知识都必须有一个单一、明确、权威的表示。
这种方法的缺点是,如果共享库出现错误或需要增强,就必须在多个地方进行升级。这可能不是一个严重的挫折,因为每个服务可以包含共享库的不同版本:
将共享库开发为另一个微服务本身的替代选项需要仔细分析。如果从业务能力的角度来看它并不合格作为一个微服务,那么它可能会增加更多的复杂性而不是有用性。权衡分析在通信开销与在多个服务中复制库之间。
微服务中的用户界面
微服务原则主张将微服务作为从数据库到表示的垂直切片:
实际上,我们得到了构建快速 UI 和移动应用程序的需求,这些应用程序整合了现有的 API。在现代情景中,这并不罕见,业务希望从 IT 获得快速的周转时间:
移动应用程序的渗透是这种方法的原因之一。在许多组织中,将有移动开发团队坐在业务团队旁边,通过结合和整合来自多个内部和外部来源的 API 开发快速移动应用程序。在这种情况下,我们可能只是暴露服务,并让移动团队按照业务的要求来实现。在这种情况下,我们将构建无头微服务,并让移动团队构建一个表示层。
另一个问题类别是业务可能希望构建面向社区的综合网络应用程序:
例如,业务可能希望开发一个面向机场用户的离港控制应用程序。离港控制网络应用程序可能具有办理登机手续、休息室管理、登机等功能。这些可能被设计为独立的微服务。但从业务的角度来看,所有这些都需要被整合到一个单一的网络应用程序中。在这种情况下,我们将不得不通过从后端整合服务来构建网络应用程序。
一种方法是构建一个容器网络应用程序或占位符网络应用程序,它链接到后端的多个微服务。在这种情况下,我们开发全栈微服务,但由此产生的屏幕可以嵌入到另一个占位符网络应用程序中。这种方法的一个优点是你可以有多个针对不同用户群体的占位符网络应用程序,如前图所示。我们可以使用 API 网关来避免这些交叉连接。我们将在下一节中探讨 API 网关。
微服务中使用 API 网关
随着像 AngularJS 这样的客户端 JavaScript 框架的发展,服务器被期望暴露 RESTful 服务。这可能会导致两个问题。第一个问题是合同期望不匹配。第二个问题是多次调用服务器来渲染页面。
我们从合同不匹配的情况开始。例如,GetCustomer
可能返回一个具有许多字段的 JSON:
Customer {
Name:
Address:
Contact:
}
在前面的情况下,Name
、Address
和Contact
是嵌套的 JSON 对象。但移动客户端可能只期望基本信息,比如名字和姓氏。在 SOA 世界中,ESB 或移动中间件完成了为客户端转换数据的工作。微服务的默认方法是获取Customer
的所有元素,然后客户端负责过滤元素。在这种情况下,网络的负担就在于网络。
我们可以考虑几种方法来解决这个问题:
Customer {
Id: 1
Name: /customer/name/1
Address: /customer/address/1
Contact: /customer/contact/1
}
在第一种方法中,最小的信息与链接一起发送,如 HATEOAS 部分所述。在前面的情况下,对于客户 ID1
,有三个链接,这将帮助客户端访问特定的数据元素。这个例子是一个简单的逻辑表示,不是实际的 JSON。在这种情况下,移动客户端将获得基本的客户信息。客户端进一步使用链接来获取额外所需的信息。
第二种方法是在客户端发起 REST 调用时使用的,它还将所需字段作为查询字符串的一部分发送。在这种情况下,客户端发送一个带有firstname
和lastname
作为查询字符串的请求,以表明客户端只需要这两个字段。缺点是它会导致复杂的服务器端逻辑,因为它必须基于字段进行过滤。服务器必须根据传入的查询发送不同的元素。
第三种方法是引入一定程度的间接性。在这种情况下,一个网关组件位于客户端和服务器之间,并根据消费者的规范转换数据。这是一个更好的方法,因为我们不会妥协后端服务契约。这导致了所谓的 UI 服务。在许多情况下,API 网关充当后端的代理,公开一组特定于消费者的 API:
微服务中使用 API 网关
我们可以以两种方式部署 API 网关。第一种是每个微服务一个 API 网关,如图A所示。第二种方法(图B)是为多个服务使用一个通用 API 网关。选择取决于我们要寻找什么。如果我们将 API 网关用作反向代理,那么可以使用诸如 Apigee、Mashery 等现成的网关作为共享平台。如果我们需要对流量塑形和复杂转换进行精细控制,那么每个服务的自定义 API 网关可能更有用。
相关问题是,我们将不得不从客户端向服务器发出许多调用。如果我们参考第一章中的度假示例,解密微服务,您会知道为了呈现每个小部件,我们必须向服务器发出调用。尽管我们只传输数据,但仍可能对网络造成重大负担。这种方法并不完全错误,因为在许多情况下,我们使用响应式设计和渐进式设计。数据将根据用户导航按需加载。为了做到这一点,客户端中的每个小部件都应以懒惰模式独立地向服务器发出调用。如果带宽是一个问题,那么 API 网关就是解决方案。API 网关充当中间人,从多个微服务中组合和转换 API。
在微服务中使用 ESB 和 iPaaS
从理论上讲,SOA 并不完全依赖于 ESB,但现实情况是,ESB 一直是许多 SOA 实施的核心。在微服务世界中,ESB 的角色将是什么?
总的来说,微服务是具有较小占地面积的完全云原生系统。微服务的轻量特性使得部署、扩展等自动化成为可能。相反,企业 ESB 具有沉重的特性,大多数商业 ESB 都不友好于云。ESB 的关键特性是协议调解、转换、编排和应用适配器。在典型的微服务生态系统中,我们可能不需要这些功能。
对于微服务相关的有限 ESB 功能,已经可以使用更轻量级的工具,如 API 网关。编排从中央总线转移到了微服务本身。因此,在微服务的情况下,不需要集中的编排能力。由于服务设置为接受更通用的消息交换样式,使用 REST/JSON 调用,因此不需要协议调解。我们从 ESB 中获得的最后一部分功能是连接回传统系统的适配器。在微服务的情况下,服务本身提供了具体的实现,因此不需要传统的连接器。因此,ESB 在微服务世界中没有自然的空间。
许多组织将 ESB 建立为其应用集成(EAI)的支柱。这些组织的企业架构政策是围绕 ESB 构建的。在使用 ESB 进行集成时,可能存在许多企业级政策,如审计、日志记录、安全性、验证等。然而,微服务倡导更加分散的治理。如果与微服务集成,ESB 将是一种过度杀伤力。
并非所有服务都是微服务。企业拥有遗留应用程序、供应商应用程序等。遗留服务使用 ESB 与微服务连接。ESB 仍然在遗留集成和供应商应用程序集成中占据一席之地。
随着云的发展,ESB 的能力已不足以管理云之间、云与本地等之间的集成。集成平台即服务(iPaaS)正在成为下一代应用集成平台,进一步减少了 ESB 的作用。在典型部署中,iPaaS 调用 API 网关来访问微服务。
服务版本考虑
当我们允许服务发展时,需要考虑的一个重要方面是服务版本控制。服务版本控制应该是提前考虑的,而不是事后的想法。版本控制帮助我们发布新服务而不会破坏现有的消费者。新旧版本将同时部署。
语义版本广泛用于服务版本控制。语义版本有三个组成部分:主要、次要和补丁。主要用于有破坏性变化时,次要用于向后兼容的变化,补丁用于向后兼容的错误修复。
当微服务中有多个服务时,版本控制可能会变得复杂。与操作级别相比,对服务级别进行版本控制总是更简单的。如果其中一个操作发生变化,服务将升级并部署到 V2。版本变化适用于服务中的所有操作。这是不可变服务的概念。
我们可以以三种不同的方式对 REST 服务进行版本控制:
-
URI 版本控制
-
媒体类型版本控制
-
自定义标头
在 URI 版本控制中,版本号包含在 URL 中。在这种情况下,我们只需要担心主要版本。因此,如果有次要版本变化或补丁,消费者不需要担心变化。将最新版本别名为非版本化的 URI 是一个好的做法,如下所示:
/api/v3/customer/1234
/api/customer/1234 - aliased to v3.
@RestController("CustomerControllerV3")
@RequestMapping("api/v3/customer")
public class CustomerController {
}
一个稍微不同的方法是将版本号作为 URL 参数的一部分:
api/customer/100?v=1.5
在媒体类型版本控制的情况下,版本由客户端在 HTTP Accept
标头中设置如下:
Accept: application/vnd.company.customer-v3+json
版本控制的一个不太有效的方法是在自定义标头中设置版本:
@RequestMapping(value = "/{id}", method = RequestMethod.GET, headers = {"version=3"})
public Customer getCustomer(@PathVariable("id") long id) {
//other code goes here.
}
在 URI 方法中,客户端消费服务很简单。但这也存在一些固有问题,比如嵌套 URI 资源的版本控制可能会很复杂。事实上,与媒体类型方法相比,迁移客户端稍微复杂,服务的多个版本存在缓存问题等。然而,这些问题并不足以让我们不选择 URI 方法。大多数大型互联网公司,如谷歌、Twitter、LinkedIn 和 Salesforce 都在采用 URI 方法。
设计跨域
在微服务中,不能保证服务将从相同的主机或相同的域运行。复合 UI Web 应用程序可能调用多个微服务来完成任务,这些微服务可能来自不同的域和主机。
CORS 允许浏览器客户端向托管在不同域上的服务发送请求。这在基于微服务的架构中是必不可少的。
一种方法是允许所有微服务允许来自其他受信任域的跨域请求。第二种方法是使用 API 网关作为客户端的单一受信任域。
处理共享参考数据
在拆分大型应用程序时,我们经常看到的一个常见问题是主数据或参考数据的管理。参考数据更像是不同微服务之间需要共享的数据。城市主数据、国家主数据等将被用于许多服务,如航班时刻表、预订等。
有几种方法可以解决这个问题。例如,在相对静态、永远不会改变的数据的情况下,每个服务都可以在所有微服务内部硬编码这些数据:
另一种方法,如前图所示,是将其构建为另一个微服务。这是一个很好、干净、整洁的方法,但缺点是每个服务可能需要多次调用主数据。如搜索和预订示例图中所示,有事务性微服务,它们使用地理微服务来访问共享数据:
另一种选择是将数据复制到每个微服务中。没有单一所有者,但每个服务都有其所需的主数据。更新时,所有服务都会更新。这非常有利于性能,但必须在所有服务中复制代码。在所有微服务之间保持数据同步也很复杂。如果代码库和数据简单或数据更为静态,这种方法是有意义的。
另一种方法类似于第一种方法,但每个服务都有所需数据的本地近缓存,该缓存将逐步加载。根据数据量,也可以使用 Ehcache 等本地嵌入式缓存或 Hazelcast 或 Infinispan 等数据网格。这是对依赖于主数据的大量微服务最优先的方法。
微服务和批量操作
由于我们已经将单片应用程序分解为更小、更专注的服务,不再可能跨微服务数据存储使用连接查询。这可能导致一种情况,即一个服务可能需要其他服务的许多记录来执行其功能。
例如,月度计费功能需要处理许多客户的发票才能进行计费。更复杂的是,发票可能有很多订单。当我们将计费、发票和订单分解为三个不同的微服务时,出现的挑战是计费服务必须查询发票服务以获取所有发票,然后对每张发票调用订单服务以获取订单。这不是一个好的解决方案,因为发送到其他微服务的调用次数很高:
我们可以考虑两种解决方法。第一种方法是在创建数据时预先聚合数据。创建订单时,会发送一个事件。收到事件后,计费微服务会在内部持续聚合数据以进行月度处理。在这种情况下,计费微服务无需外出处理。这种方法的缺点是数据重复。
第二种方法是,在无法进行预聚合时,使用批处理 API。在这种情况下,我们调用GetAllInvoices
,然后使用多个批次,每个批次进一步使用并行线程获取订单。Spring Batch 在这些情况下很有用。
微服务挑战
在前一节中,您了解了应采取的正确设计决策以及应用的权衡。在本节中,我们将回顾一些微服务的挑战,以及如何解决这些挑战以实现成功的微服务开发。
数据孤岛
微服务抽象出自己的本地事务存储,用于其自己的事务目的。存储类型和数据结构将针对微服务提供的服务进行优化。
例如,如果我们想要开发客户关系图,我们可以使用图形数据库如 Neo4j、OrientDB 等。使用类似 Elasticsearch 或 Solr 的索引搜索数据库进行预测性文本搜索,以便根据护照号码、地址、电子邮件、电话等任何相关信息找到客户可能是最好的。
这将使我们陷入将数据分散为异构数据岛的独特境地。例如,客户、忠诚积分、预订等是不同的微服务,因此使用不同的数据库。如果我们想要通过组合来自所有三个数据存储的数据进行近实时分析所有高价值客户,那该怎么办?这在单片应用中很容易,因为所有数据都存在于单个数据库中:
为了满足这一要求,需要一个数据仓库或数据湖。传统的数据仓库如 Oracle、Teradata 等主要用于批量报告。但是使用 NoSQL 数据库(如 Hadoop)和微批处理技术,可以通过数据湖的概念实现近实时分析。与传统的专为批量报告而构建的仓库不同,数据湖存储原始数据,而不假设数据将如何使用。现在真正的问题是如何将数据从微服务传输到数据湖中。
从微服务到数据湖或数据仓库的数据传输可以通过多种方式进行。传统的 ETL 可能是其中之一。由于我们允许通过 ETL 进行后门入口,并且打破了抽象,因此这不被认为是数据移动的有效方式。更好的方法是从微服务发送事件,例如客户注册、客户更新事件等。数据摄取工具消耗这些事件,并将状态更改适当地传播到数据湖。数据摄取工具是高度可扩展的平台,如 Spring Cloud Data Flow、Kafka、Flume 等。
日志记录和监控
日志文件是分析和调试的好信息。由于每个微服务都是独立部署的,它们会发出单独的日志,可能存储在本地磁盘上。这导致了日志的碎片化。当我们在多台机器上扩展服务时,每个服务实例可能会产生单独的日志文件。这使得通过日志挖掘极其困难来调试和理解服务的行为。
将订单、交付和通知作为三种不同的微服务进行检查,我们发现没有办法将跨越它们的客户交易相关联:
在实施微服务时,我们需要能够将每个服务的日志传输到一个集中管理的日志存储库。采用这种方法,服务不必依赖本地磁盘或本地 I/O。第二个优势是日志文件是集中管理的,并且可用于各种分析,如历史、实时和趋势。通过引入相关 ID,可以轻松跟踪端到端的交易。
对于大量的微服务以及多个版本和服务实例,很难找出哪个服务在哪个服务器上运行,这些服务的健康状况,服务依赖关系等。这在与特定或固定服务器集标记的单片应用中要容易得多。
除了了解部署拓扑和健康状况外,还需要面对识别服务行为、调试和识别热点的挑战。需要强大的监控能力来管理这样的基础设施。
我们将在第七章日志记录和监控微服务中涵盖日志记录和监控方面。
依赖管理
依赖管理是大规模微服务部署中的关键问题之一。我们如何识别并减少变更的影响?我们如何知道所有依赖服务是否正常运行?如果其中一个依赖服务不可用,服务会如何表现?
过多的依赖可能会给微服务带来挑战。以下是四个重要的设计方面:
-
通过正确设计服务边界来减少依赖关系。
-
尽量设计松散耦合的依赖关系来减少影响。此外,通过异步通信方式设计服务交互。
-
使用断路器等模式解决依赖问题。
-
使用可视化依赖图监控依赖关系。
组织文化
微服务实施中最大的挑战之一是组织文化。为了利用微服务的交付速度,组织应该采用敏捷开发流程、持续集成、自动化 QA 检查、自动交付流水线、自动化部署和自动基础设施配置。
遵循瀑布式开发或重量级发布管理流程的组织,以及发布周期不频繁的挑战是微服务开发的挑战。不足的自动化也是微服务部署的挑战。
简而言之,云和 DevOps 是微服务开发的支持要素。这些对于成功实施微服务至关重要。
治理挑战
微服务实施了分散的治理,这与传统的 SOA 治理形成鲜明对比。组织可能会发现很难适应这种变化,这可能会对微服务的开发产生负面影响。
分散治理模型带来了许多挑战。我们如何了解谁在使用服务?我们如何确保服务的重复使用?我们如何定义组织中可用的服务?我们如何确保执行企业政策?
首先要有一套标准、最佳实践和指南,说明如何实现更好的服务。这些应该以标准库、工具和技术的形式提供给组织。这确保了开发的服务是高质量的,并且它们是以一致的方式开发的。
第二个重要的考虑是有一个地方,所有利益相关者不仅可以看到所有服务,还可以看到它们的文档、合同和服务级别协议。Swagger 和 API Blueprint 通常用于处理这些要求。
运营开销
微服务部署通常会增加可部署单元和虚拟机(或容器)的数量。这会增加显着的管理开销并增加运营成本。
对于单个应用程序,在本地数据中心中专门使用一定数量的容器或虚拟机可能没有太多意义,除非业务利益非常高。成本通常随着规模经济而降低。在共享基础设施中部署大量微服务更有意义,因为这些微服务不是针对特定的虚拟机或容器。基础设施自动化、配置、容器化部署等能力对于大规模微服务部署至关重要。没有这种自动化,将导致显着的运营开销和成本增加。
对于许多微服务来说,可配置项(CIs)的数量变得太高,这些 CIs 部署在的服务器数量也可能是不可预测的。这使得在传统的配置管理数据库(CMDB)中管理数据变得极其困难。在许多情况下,动态发现当前运行的拓扑比静态配置的 CMDB 式部署拓扑更有用。
测试微服务
微服务也对服务的可测试性提出了挑战。为了实现完整的服务功能,一个服务可能依赖于另一个服务,而另一个服务又依赖于另一个服务,无论是同步还是异步。问题是我们如何测试端到端的服务以评估其行为?在测试时,依赖的服务可能可用,也可能不可用。
服务虚拟化或服务模拟是测试服务的技术之一,可以在没有实际依赖关系的情况下进行测试。在测试环境中,当服务不可用时,模拟服务可以模拟实际服务的行为。微服务生态系统需要服务虚拟化能力。然而,这可能无法完全保证,因为模拟服务可能无法模拟许多边缘情况,特别是当存在深层依赖关系时。
另一种方法,正如前面讨论的,是使用消费者驱动的合同。翻译后的集成测试用例可以涵盖服务调用的几乎所有边缘情况。
测试自动化、适当的性能测试和持续交付方法,如 A/B 测试、未来标志、金丝雀测试、蓝绿部署和红黑部署,都可以降低生产发布的风险。
基础设施供应
正如在操作开销下简要提到的,手动部署可能严重挑战微服务的部署。如果部署中存在手动元素,部署者或运维管理员应该了解运行拓扑,手动重新路由流量,然后逐个部署应用程序,直到所有服务都升级完成。随着许多服务器实例的运行,这可能导致重大的操作开销。此外,在这种手动方法中出现错误的机会很高。
微服务需要支持弹性云一样的基础设施,可以自动提供虚拟机或容器,自动部署应用程序,调整流量流向,将新版本复制到所有实例,并优雅地淘汰旧版本。自动化还负责根据需求添加容器或虚拟机来弹性扩展,并在负载低于阈值时缩减规模。
在具有许多微服务的大型部署环境中,我们可能还需要额外的工具来管理可以进一步自动启动或销毁服务的虚拟机或容器。
微服务能力模型
在我们总结本章之前,我们将根据本章描述的设计指南和常见模式和解决方案来审查微服务的能力模型。
以下图表描述了微服务的能力模型:
能力模型大致分为四个领域:
-
核心能力:这些是微服务本身的一部分
-
支持能力:这些是支持核心微服务实施的软件解决方案
-
基础设施能力:这些是成功实施微服务的基础设施级别期望
-
治理能力:这更多是过程、人员和参考信息
核心能力
核心能力的解释如下:
-
服务监听器(HTTP/消息):如果微服务启用了基于 HTTP 的服务端点,则 HTTP 监听器嵌入在微服务中,从而消除了对外部应用服务器的需求。HTTP 监听器在应用启动时启动。如果微服务基于异步通信,则会启动消息监听器而不是 HTTP 监听器。还可以考虑其他协议。如果微服务是定时服务,则可能没有任何监听器。Spring Boot 和 Spring Cloud Streams 提供了这种能力。
-
存储能力:微服务具有某种存储机制,用于存储与业务能力相关的状态或事务数据。这是可选的,取决于实现的能力。存储可以是物理存储(如 MySQL 等关系型数据库管理系统;Hadoop、Cassandra、Neo 4J、Elasticsearch 等 NoSQL 数据库),也可以是内存存储(如 Ehcache 等缓存,Hazelcast、Infinispan 等数据网格)。
-
业务能力定义:这是微服务的核心,业务逻辑在其中实现。这可以用任何适用的语言实现,如 Java、Scala、Conjure、Erlang 等。实现所需的所有业务逻辑将嵌入在微服务中。
-
事件溯源:微服务向外部世界发送状态更改,而不必真正担心这些事件的目标消费者。其他微服务、审计服务、复制服务或外部应用程序等都可以消费这些事件。这使得其他微服务和应用程序可以响应状态更改。
-
服务端点和通信协议:这些定义了外部消费者使用的 API。这些可以是同步端点或异步端点。同步端点可以基于 REST/JSON 或其他协议,如 Avro、Thrift、Protocol Buffers 等。异步端点通过 Spring Cloud Streams 支持的 RabbitMQ、其他消息服务器或其他消息样式实现,如 ZeroMQ。
-
API 网关:API 网关通过代理服务端点或组合多个服务端点提供一定程度的间接性。API 网关还可用于执行策略。它还可以提供实时负载平衡能力。市场上有许多 API 网关可用。Spring Cloud Zuul、Mashery、Apigee 和 3scale 是 API 网关提供商的一些例子。
-
用户界面:通常,用户界面也是微服务的一部分,用于用户与微服务实现的业务能力进行交互。这些可以用任何技术实现,并且与通道和设备无关。
基础设施能力
对于成功部署和管理大规模微服务,需要某些基础设施能力。在大规模部署微服务时,如果没有适当的基础设施能力,可能会面临挑战,并导致失败:
-
云:在传统的数据中心环境中,微服务的实施是困难的,因为需要长时间来配置基础设施。即使为每个微服务专门配置大量基础设施,也可能不具有成本效益。在数据中心内部管理它们可能会增加所有权成本和运营成本。云式基础设施更适合微服务部署。
-
容器或虚拟机:管理大型物理机器不具有成本效益,而且也很难管理。物理机器也很难处理自动容错。许多组织采用虚拟化技术,因为它能够充分利用物理资源,并提供资源隔离。它还减少了管理大型物理基础设施组件的开销。容器是虚拟机的下一代。VMWare、Citrix 等公司提供虚拟机技术。Docker、Drawbridge、Rocket 和 LXD 是一些容器技术。
-
集群控制和配置:一旦我们有大量容器或虚拟机,就很难自动管理和维护它们。集群控制工具在容器之上提供统一的操作环境,并在多个服务之间共享可用容量。Apache Mesos 和 Kubernetes 是集群控制系统的例子。
-
应用程序生命周期管理:应用程序生命周期管理工具有助于在启动新容器时调用应用程序,或在容器关闭时终止应用程序。应用程序生命周期管理允许脚本应用程序部署和发布。它自动检测故障情况,并对这些故障做出响应,从而确保应用程序的可用性。这与集群控制软件协同工作。Marathon 部分地满足了这一能力。
支持能力
支持能力并不直接与微服务相关,但对于大规模微服务开发至关重要。
-
软件定义的负载均衡器:负载均衡器应该足够智能,能够理解部署拓扑的变化,并做出相应的响应。这摆脱了在负载均衡器中配置静态 IP 地址、域别名或集群地址的传统方法。当新服务器添加到环境中时,它应该自动检测到,并通过避免任何手动交互将它们包含在逻辑集群中。同样,如果服务实例不可用,它应该从负载均衡器中移除。Ribbon、Eureka 和 Zuul 的组合在 Spring Cloud Netflix 中提供了这一能力。
-
中央日志管理:正如本章前面探讨的,需要一个能够集中所有服务实例发出的日志并带有相关性 ID 的能力。这有助于调试、识别性能瓶颈和预测分析。其结果反馈到生命周期管理器中,以采取纠正措施。
-
服务注册表:服务注册表为服务提供了一个运行时环境,使其能够在运行时自动发布其可用性。注册表将是了解任何时候服务拓扑的良好信息来源。Spring Cloud 的 Eureka、Zookeeper 和 Etcd 是一些可用的服务注册表工具。
-
安全服务:分布式微服务生态系统需要一个用于管理服务安全的中央服务器。这包括服务认证和令牌服务。基于 OAuth2 的服务广泛用于微服务安全。Spring Security 和 Spring Security OAuth 是构建这一能力的良好选择。
-
服务配置:所有服务配置应该按照十二要素应用程序原则进行外部化。一个集中的服务用于所有配置是一个不错的选择。Spring Cloud Config 服务器和 Archaius 是现成的配置服务器。
-
测试工具(反脆弱性、RUM 等):Netflix 使用 Simian Army 进行反脆弱性测试。成熟的服务需要持续的挑战来检验服务的可靠性,以及良好的备用机制。Simian Army 组件创建各种错误场景,以探索系统在故障情况下的行为。
-
监控和仪表板:微服务还需要强大的监控机制。这不仅仅是基础设施级别的监控,还包括服务级别的监控。Spring Cloud Netflix Turbine、Hysterix Dashboard 等提供服务级别信息。像 AppDynamic、New Relic、Dynatrace 以及 statd、Sensu、Spigo 等端到端监控工具可以为微服务监控增加价值。
-
依赖和 CI 管理:我们还需要工具来发现运行时拓扑、服务依赖关系,并管理可配置项。基于图形的 CMDB 是管理这些场景最明显的工具。
-
数据湖:正如本章前面讨论的,我们需要一种机制来合并存储在不同微服务中的数据,并进行近实时分析。数据湖是实现这一目标的不错选择。Spring Cloud Data Flow、Flume 和 Kafka 等数据摄取工具用于消费数据。HDFS、Cassandra 等用于存储数据。
-
可靠的消息传递:如果通信是异步的,我们可能需要可靠的消息传递基础设施服务,如 RabbitMQ 或任何其他可靠的消息传递服务。云消息传递或作为服务的消息传递是互联网规模基于消息的服务端点的流行选择。
流程和治理能力
拼图中的最后一块是微服务所需的流程和治理能力:
-
DevOps:成功实施微服务的关键是采用 DevOps。DevOps 通过支持敏捷开发、高速交付、自动化和更好的变更管理来补充微服务开发。
-
DevOps 工具:敏捷开发、持续集成、持续交付和持续部署的 DevOps 工具对于成功交付微服务至关重要。需要大量强调自动化功能、真实用户测试、合成测试、集成、发布和性能测试。
-
微服务仓库:微服务仓库是微服务的版本化二进制文件存放的地方。这可以是一个简单的 Nexus 仓库,也可以是一个容器仓库,比如 Docker 注册表。
-
微服务文档:正确记录所有微服务非常重要。Swagger 或 API Blueprint 有助于实现良好的微服务文档。
-
参考架构和库:参考架构在组织级别提供了蓝图,以确保服务按照一定的标准和指南以一致的方式开发。其中许多可以转化为许多可重用的库,以强制执行服务开发理念。
总结
在本章中,您学习了处理微服务开发中可能出现的实际场景。
您学习了各种解决常见微服务问题的解决方案选项和模式。我们审查了开发大规模微服务时遇到的许多挑战,以及如何有效解决这些挑战。
我们还为基于微服务的生态系统构建了一个能力参考模型。该能力模型有助于解决构建互联网规模微服务时的差距。本章学到的能力模型将是本书的支柱。其余章节将深入探讨能力模型。
在下一章中,我们将解决一个现实世界的问题,并使用微服务架构对其进行建模,以了解如何将我们的学习转化为实践。
第四章:微服务演进-案例研究
与 SOA 一样,微服务架构可以根据手头的问题在不同组织中有不同的解释。除非详细研究一个相当大的真实问题,否则微服务概念很难理解。
本章将介绍 BrownField 航空公司(BF),一个虚构的廉价航空公司,以及他们从单体乘客销售和服务(PSS)应用到下一代微服务架构的过程。本章将详细研究 PSS 应用,并解释从单体系统到基于微服务的架构的挑战、方法和转型步骤,遵循前一章中解释的原则和实践。
这个案例研究的目的是让我们尽可能接近实际情况,以便将架构概念确立下来。
在本章结束时,您将学到以下内容:
-
将单体系统迁移到基于微服务的真实案例,以 BrownField 航空公司的 PSS 应用为例
-
迁移单体应用程序到微服务的各种方法和过渡策略
-
使用 Spring 框架组件设计一个新的未来主义微服务系统来替代 PSS 应用程序
审查微服务能力模型
本章的示例探讨了第三章中讨论的微服务能力模型中的以下微服务能力:
-
HTTP 监听器
-
消息监听器
-
存储能力(物理/内存)
-
业务能力定义
-
服务端点和通信协议
-
用户界面
-
安全服务
-
微服务文档
在第二章中,使用 Spring Boot 构建微服务,我们独立探讨了所有这些能力,包括如何保护 Spring Boot 微服务。本章将基于一个真实案例构建一个全面的微服务示例。
提示
本章的完整源代码可在代码文件的第四章
项目中找到。
了解 PSS 应用
BrownField 航空公司是增长最快的低成本地区航空公司之一,从其枢纽直飞 100 多个目的地。作为一家初创航空公司,BrownField 航空公司从少数目的地和少量飞机开始运营。BrownField 开发了自己的 PSS 应用来处理他们的乘客销售和服务。
业务流程视图
这个用例为了讨论目的而大大简化了。以下图表中的流程视图显示了 BrownField 航空公司当前 PSS 解决方案涵盖的端到端乘客服务操作:
当前解决方案正在自动化某些面向客户的功能以及某些面向内部的功能。有两个面向内部的功能,Pre-flight和Post-flight。Pre-flight功能包括规划阶段,用于准备飞行计划、计划、飞机等。Post-flight功能由后勤部门用于收入管理、会计等。搜索和预订功能是在线座位预订流程的一部分,办理登机手续是在机场接受乘客的过程。办理登机手续也可以通过互联网向最终用户提供在线办理登机手续。
在前面的图表中,箭头开头的交叉标记表示它们是断开的,并且发生在不同的时间轴上。例如,乘客可以提前 360 天预订,而办理登机手续通常发生在飞机起飞前 24 小时。
功能视图
以下图表显示了 BrownField 航空公司 PSS 景观的功能构建块。每个业务流程及其相关的子功能都在一行中表示:
前面图表中显示的每个子功能都解释了它在整个业务流程中的作用。一些子功能参与了多个业务流程。例如,库存在搜索和预订中都有使用。为了避免任何复杂情况,这在图表中没有显示。数据管理和交叉子功能在许多业务功能中使用。
架构视图
为了有效管理端到端的乘客操作,BrownField 在近十年前开发了一款内部 PSS 应用程序。这款良好架构的应用程序是使用 Java 和 JEE 技术结合当时最先进的开源技术开发的。
整体架构和技术如下图所示:
架构有明确定义的边界。此外,不同的关注点被分隔到不同的层中。Web 应用程序被开发为N层、基于组件的模块化系统。功能通过以 EJB 端点形式定义的明确定义的服务契约相互交互。
设计视图
应用程序有许多逻辑功能分组或子系统。此外,每个子系统都有许多组件,组织如下图所示:
子系统通过使用 IIOP 协议进行远程 EJB 调用相互交互。事务边界跨越子系统。子系统内的组件通过本地 EJB 组件接口相互通信。理论上,由于子系统使用远程 EJB 端点,它们可以在不同的物理分离的应用服务器上运行。这是设计目标之一。
实施视图
以下图表中的实施视图展示了子系统及其组件的内部组织。图表的目的也是展示不同类型的构件:
在前面的图表中,灰色阴影框被视为不同的 Maven 项目,并转化为物理构件。子系统和组件都遵循“按接口编程”的原则进行设计。接口被打包为单独的 JAR 文件,以便客户端与实现分离。业务逻辑的复杂性被隐藏在领域模型中。本地 EJB 被用作组件接口。最后,所有子系统被打包到一个单一的 EAR 中,并部署在应用服务器中。
部署视图
应用程序的初始部署如下图所示,简单而直接:
Web 模块和业务模块被部署到单独的应用服务器集群中。通过向集群添加更多应用服务器来实现水平扩展应用程序。
零停机部署通过创建一个备用集群,并优雅地将流量转移到该集群来处理。一旦主集群被打补丁升级到新版本并重新投入使用,备用集群就会被销毁。大多数数据库更改都设计为向后兼容,但破坏性更改会导致应用程序中断。
巨石的消亡
PSS 应用程序表现良好,成功支持所有业务需求以及预期的服务水平。在最初的几年里,系统在业务的有机增长中没有任何问题。
业务在一段时间内实现了巨大的增长。车队规模显著增加,新目的地被添加到网络中。由于这种快速增长,预订数量增加,导致交易量急剧增加,达到最初估计的 200 到 500 倍。
痛点
业务的快速增长最终使应用程序承受了压力。出现了奇怪的稳定性问题和性能问题。新的应用程序发布开始破坏工作代码。此外,变更的成本和交付速度开始深刻影响业务运营。
进行了端到端架构审查,并暴露了系统的弱点以及许多故障的根本原因,如下所示:
-
稳定性:稳定性问题主要是由于线程阻塞,限制了应用服务器接受更多交易的能力。线程阻塞主要是由于数据库表锁。内存问题也是稳定性问题的另一个原因。还存在一些资源密集型操作的问题,这些问题影响了整个应用程序。
-
故障:故障窗口的增加主要是由于服务器启动时间的增加。这个问题的根本原因归结为 EAR 的体积过大。在任何故障窗口期间,消息堆积导致故障窗口后立即对应用的大量使用。由于一切都打包在一个单独的 EAR 中,任何小的应用代码更改都会导致完全重新部署。之前描述的零停机部署模型的复杂性,以及服务器启动时间的增加,都增加了故障的数量和持续时间。
-
敏捷性:随着时间的推移,代码的复杂性也大大增加,部分原因是在实施变更时缺乏纪律。因此,变更变得更难实施。此外,影响分析变得过于复杂。因此,不准确的影响分析经常导致修复破坏了工作代码。应用构建时间大大增加,从几分钟到几个小时,导致开发生产率不可接受的下降。构建时间的增加还导致构建自动化困难,并最终停止了持续集成(CI)和单元测试。
临时修复
性能问题部分通过在规模立方体中应用 Y 轴扩展方法来解决,如第一章 解密微服务中所述。全面的 EAR 部署到多个不相交的集群中。安装了软件代理,以选择性地将流量路由到指定的集群,如下图所示:
这有助于 BrownField 的 IT 扩展应用服务器。因此,稳定性问题得到了控制。然而,这很快在数据库层面导致了瓶颈。Oracle 的Real Application Cluster(RAC)被实施为解决这个问题的解决方案。
这种新的扩展模型减少了稳定性问题,但以增加的复杂性和所有权成本为代价。技术债务也随着时间的推移而增加,导致了一种状态,即完全重写是减少这种技术债务的唯一选择。
回顾
尽管应用程序架构良好,但功能组件之间存在明显的分离。它们松散耦合,通过标准化接口编程,并且具有丰富的领域模型。
显而易见的问题是,为什么这样一个设计良好的应用程序未能达到预期?架构师还能做些什么?
重要的是要了解随着时间的推移出现了什么问题。在本书的背景下,了解微服务如何避免这些情况再次发生也很重要。我们将在接下来的章节中研究其中一些情况。
共享数据
几乎所有功能模块都需要参考数据,例如航空公司的详细信息,飞机的详细信息,机场和城市的列表,国家,货币等等。例如,票价是根据出发地(城市)计算的,航班是在出发地和目的地(机场)之间的,办理登机手续是在出发机场(机场)进行的,等等。在某些功能中,参考数据是信息模型的一部分,而在另一些功能中,它用于验证目的。
这些参考数据大部分既不是完全静态的,也不是完全动态的。当航空公司开通新航线时,可能会增加一个国家、城市、机场等。当航空公司购买新飞机或更改现有飞机的座位配置时,飞机参考数据可能会发生变化。
参考数据的常见用法之一是根据某些参考数据过滤操作数据。例如,假设用户希望查看所有飞往某个国家的航班。在这种情况下,事件的流程可能如下:找到所选国家的所有城市,然后找到所有城市的机场,然后发送请求以获取所有飞往该国家中所识别的机场的航班列表。
在设计系统时,架构师考虑了多种方法。将参考数据作为独立子系统分离是考虑的选项之一,但这可能会导致性能问题。团队决定采用异常处理的方法来处理参考数据,与其他事务相比。考虑到前面讨论的查询模式的性质,该方法是将参考数据用作共享库。
在这种情况下,允许子系统直接访问参考数据,而不是通过 EJB 接口。这也意味着无论子系统如何,Hibernate 实体都可以将参考数据用作它们实体关系的一部分。
如前图所示,预订子系统中的Booking实体被允许使用参考数据实体,例如Airport,作为它们的关系的一部分。
单一数据库
尽管在中间层强制执行了足够的分离,但所有功能都指向单一数据库,甚至是相同的数据库架构。单一架构方法带来了一系列问题。
本地查询
Hibernate 框架提供了对底层数据库的良好抽象。它生成高效的 SQL 语句,在大多数情况下使用特定的方言针对数据库。然而,有时编写本地 JDBC SQL 可以提供更好的性能和资源效率。在某些情况下,使用本地数据库函数可以获得更好的性能。
单一数据库方法在开始时效果很好。但随着时间的推移,它为开发人员打开了一个漏洞,通过连接不同子系统拥有的数据库表。本地 JDBC SQL 是执行这一操作的良好工具。
以下图表显示了使用本地 JDBC SQL 连接两个子系统拥有的两个表的示例:
如前图所示,会计组件需要从预订组件获取给定城市当天的所有预订记录,以便进行日终结算。基于子系统的设计要求会计组件向预订组件发起服务调用,以获取给定城市的所有预订记录。假设这会导致N条预订记录。现在,对于每条预订记录,会计组件必须执行数据库调用,以查找与每条预订记录附加的票价代码相关的适用规则。这可能导致N+1个 JDBC 调用,效率低下。虽然可以使用批量查询或并行和批量执行等解决方法,但这将导致增加编码工作量和增加复杂性。开发人员通过本地 JDBC 查询来解决这个问题,作为一种易于实现的快捷方式。基本上,这种方法可以将调用次数从N+1减少到单个数据库调用,编码工作量最小。
这种习惯继续下去,许多 JDBC 本地查询连接跨多个组件和子系统的表。这不仅导致组件之间耦合度高,还导致代码难以发现和难以检测。
存储过程
由于使用单个数据库而出现的另一个问题是复杂存储过程的使用。一些在中间层编写的复杂数据中心逻辑性能不佳,导致响应缓慢、内存问题和线程阻塞问题。
为了解决这个问题,开发人员决定将一些复杂的业务逻辑从中间层移动到数据库层,通过在存储过程中直接实现逻辑。这个决定改善了一些交易的性能,并消除了一些稳定性问题。随着时间的推移,越来越多的存储过程被添加。然而,这最终破坏了应用程序的模块化。
领域边界
尽管领域边界已经建立,但所有组件都打包在一个单独的 EAR 文件中。由于所有组件都设置在单个容器上运行,开发人员可以自由引用这些边界之间的对象。随着时间的推移,项目团队发生了变化,交付压力增加,复杂性大大增加。开发人员开始寻找快速解决方案,而不是正确的解决方案。应用程序的模块化特性逐渐消失。
如下图所示,跨子系统边界创建了 Hibernate 关系:
微服务解救
BrownField 航空公司没有太多的改进机会来支持不断增长的业务需求。BrownField 航空公司希望以渐进式方法而不是革命性模式重新平台化系统。
在这些情况下,微服务是一种理想选择,可以在最小干扰业务的情况下转换传统的单块应用:
如前图所示,目标是转向基于微服务的架构,与业务能力对齐。每个微服务将包含数据存储、业务逻辑和表示层。
BrownField 航空公司采取的方法是构建多个面向特定用户群体的 Web 门户应用程序,如面向客户、前台和后台。这种方法的优势在于对建模的灵活性,以及对不同社区进行不同对待的可能性。例如,面向互联网的层的政策、架构和测试方法与面向内部网的 Web 应用程序不同。面向互联网的应用程序可以利用 CDN(内容传送网络)尽可能地将页面靠近客户端,而内部网应用程序可以直接从数据中心提供页面。
业务案例
在为迁移建立业务案例时,一个常见的问题是“微服务架构如何避免在另外五年内重新出现相同的问题?”
微服务提供了一系列的好处,你在第一章中学到了,但在这种情况下,重要的是列出其中一些关键的好处:
-
服务依赖:在从单片应用程序迁移到微服务时,依赖关系更为明确,因此架构师和开发人员更有能力避免破坏依赖关系,并未来保护依赖关系问题。来自单片应用程序的经验帮助架构师和开发人员设计一个更好的系统。
-
物理边界:微服务在所有领域都强制实施物理边界,包括数据存储、业务逻辑和表示层。由于它们的物理隔离,跨子系统或微服务的访问是真正受限制的。除了物理边界,它们甚至可以在不同的技术上运行。
-
选择性扩展:在微服务架构中,可以进行选择性的扩展。与单片场景中使用的 Y-比例方法相比,这提供了一种更具成本效益的扩展机制。
-
技术过时:技术迁移可以应用于微服务级别,而不是整体应用级别。因此,它不需要巨额投资。
规划演变
要打破拥有数百万行代码的应用程序并不简单,特别是如果代码具有复杂的依赖关系。我们如何打破它?更重要的是,我们从哪里开始,以及如何解决这个问题?
进化方法
解决这个问题的最佳方法是建立一个过渡计划,并逐渐将功能迁移到微服务。在每一步,都会在单片应用程序之外创建一个微服务,并将流量转移到新服务,如下图所示:
为了成功地运行这次迁移,需要从过渡的角度回答一些关键问题:
-
识别微服务的边界
-
为迁移优先考虑微服务
-
在过渡阶段处理数据同步
-
处理用户界面集成,与旧用户界面和新用户界面一起工作
-
在新系统中处理参考数据
-
测试策略以确保业务能力完整且正确重现
-
微服务开发的任何先决条件的识别,如微服务能力、框架、流程等
识别微服务边界
首要的活动是识别微服务的边界。这是问题中最有趣的部分,也是最困难的部分。如果边界的识别不正确,迁移可能会导致更复杂的可管理性问题。
就像在 SOA 中一样,服务分解是识别服务的最佳方式。然而,需要注意的是,分解停止于业务能力或有界上下文。在 SOA 中,服务分解进一步到原子、细粒度的服务级别。
顶部向下的方法通常用于域分解。在打破现有系统的情况下,自下而上的方法也很有用,因为它可以利用现有单片应用程序的许多实际知识、功能和行为。
先前的分解步骤将给出潜在的微服务列表。重要的是要注意,这不是最终的微服务列表,但它作为一个很好的起点。我们将通过一些过滤机制来得到最终的列表。在这种情况下,功能分解的第一步将类似于本章前面介绍的功能视图下显示的图表。
分析依赖关系
接下来的步骤是分析我们在上一节中创建的候选微服务之间的依赖关系。在这项活动结束时,将生成一个依赖图。
注意
这项工作需要一个由架构师、业务分析师、开发人员、发布管理和支持人员组成的团队。
生成依赖图的一种方法是列出遗留系统的所有组件并叠加依赖关系。这可以通过结合以下一种或多种方法来完成:
-
分析手动代码并重新生成依赖关系。
-
利用开发团队的经验重新生成依赖关系。
-
使用 Maven 依赖图。我们可以使用一些工具来重新生成依赖图,如 PomExplorer、PomParser 等。
-
使用性能工程工具,如 AppDynamics 来识别调用堆栈和依赖关系。
假设我们按照以下图表中显示的函数及其依赖关系进行复制:
不同模块之间存在许多来回的依赖关系。底层显示了跨模块使用的横切能力。在这一点上,模块更像是意大利面而不是自主单元。
接下来的步骤是分析这些依赖关系,并提出一个更好、更简化的依赖映射。
事件与查询相对
依赖关系可以是基于查询或基于事件的。基于事件的对可扩展系统更好。有时,可以将基于查询的通信转换为基于事件的通信。在许多情况下,这些依赖关系存在是因为业务组织是这样管理的,或者是由于旧系统处理业务情景的方式。
从先前的图表中,我们可以提取出收入管理和票价服务:
收入管理是一个用于根据预订需求预测计算最佳票价数值的模块。如果起始地和目的地之间的票价发生变化,收入管理将调用票价模块上的更新票价来更新票价模块中的相应票价。
另一种思考方式是,票价模块订阅了收入管理以获取票价变化,而收入管理在票价变化时发布。这种反应式编程方法通过这种方式给予了额外的灵活性,使得票价和收入管理模块可以保持独立,并通过可靠的消息传递系统进行连接。这种模式也可以应用于从办理登机到忠诚度和登机模块等许多其他情景。
接下来,检查 CRM 和 Booking 的情景:
这种情景与先前解释的情景略有不同。CRM 模块用于管理乘客投诉。当 CRM 收到投诉时,它会检索相应乘客的预订数据。实际上,投诉数量与预订数量相比可以忽略不计。如果我们盲目地应用先前的模式,即 CRM 订阅所有预订,我们会发现这是不划算的:
检查预订和预订模块之间的另一个场景。办理登机是否可以监听预订事件,而不是调用预订模块上的获取预订服务?这是可能的,但这里的挑战是,预订可以提前 360 天发生,而办理登机通常只在飞行起飞前 24 小时开始。提前 360 天在办理登机模块中复制所有预订和预订更改将不是一个明智的决定,因为办理登机直到飞行起飞前 24 小时才需要这些数据。
另一个选择是,当航班开放办理登机手续时(起飞前 24 小时),办理登机会调用预订模块上的服务,以获取给定航班的预订快照。一旦完成,办理登机可以订阅该航班的预订事件。在这种情况下,使用了基于查询和基于事件的组合方法。通过这样做,除了减少这两个服务之间的查询次数外,还减少了不必要的事件和存储。
简而言之,并没有一种政策适用于所有情况。每种情况都需要逻辑思维,然后应用最合适的模式。
事件而不是同步更新
除了查询模型,依赖关系也可以是更新事务。考虑收入管理和预订之间的情况:
为了对当前需求进行预测和分析,收入管理需要获取所有航班的所有预订。当前的方法如依赖图所示,收入管理有一个调用预订模块上的获取预订的定时作业,以获取自上次同步以来的所有增量预订(新预订和更改)。
另一种方法是在预订模块中即时将新预订和预订更改作为异步推送发送。相同的模式可以应用于许多其他场景,例如从预订到会计,从航班到库存,以及从航班到预订。在这种方法中,源服务将所有状态更改事件发布到主题。所有感兴趣的方都可以订阅此事件流并在本地存储。这种方法消除了许多硬连接,并保持系统松散耦合。
依赖关系在下一个图中描述:
在前面的图中所示的情况下,我们改变了依赖关系,并将它们转换为异步事件。
最后一个要分析的情况是预订模块向库存模块的更新库存调用:
当预订完成时,库存状态将通过减少存储在库存服务中的库存来更新。例如,当有 10 个经济舱座位可用时,在预订结束时,我们必须将其减少到 9 个。在当前系统中,预订和更新库存在同一事务边界内执行。这是为了处理只剩下一个座位的情况,而多个客户正在尝试预订。在新设计中,如果我们应用相同的事件驱动模式,将库存更新作为事件发送到库存可能会使系统处于不一致的状态。这需要进一步分析,我们将在本章后面解决这个问题。
挑战要求
在许多情况下,可以通过重新审视需求来实现目标状态:
有两个验证航班的调用,一个来自预订,另一个来自搜索模块。验证航班的调用是为了验证来自不同渠道的输入航班数据。最终目标是避免存储或服务不正确的数据。当客户进行航班搜索时,比如说“BF100”,系统会验证这个航班以查看以下内容:
-
这是否是一个有效的航班?
-
那天是否有这个特定日期的航班?
-
这次航班有没有设置任何预订限制?
另一种解决方法是根据这些给定条件调整航班的库存。例如,如果航班有限制,更新库存为零。在这种情况下,智能将保留在航班中,并持续更新库存。就搜索和预订而言,两者只是查找库存,而不是为每个请求验证航班。与原始方法相比,这种方法更有效。
接下来我们将审查支付用例。支付通常是一个独立的功能,因为安全约束的性质,如 PCIDSS 类似的标准。捕获支付的最明显方式是将浏览器重定向到支付服务中托管的支付页面。由于卡处理应用程序属于 PCIDSS 的范围,因此明智地删除支付服务的任何直接依赖关系。因此,我们可以删除预订到支付的直接依赖,并选择 UI 级别的集成。
挑战服务边界
在这一部分,我们将根据需求和依赖图,审查一些服务边界,考虑登记和其对座位和行李的依赖关系。
座位功能基于飞机座位分配的当前状态运行一些算法,并找出最佳方式来安置下一个乘客,以满足重量和平衡要求。这是基于一些预定义的业务规则。然而,除了登记,没有其他模块对座位功能感兴趣。从业务能力的角度来看,座位只是登记的一个功能,而不是一个独立的业务能力。因此,最好将这个逻辑嵌入到登记本身。
行李也是一样的。BrownField 有一个独立的行李处理系统。PSS 上下文中的行李功能是打印行李标签以及将行李数据存储在登记记录中。这个特定功能没有与任何业务能力相关联。因此,最好将这个功能移动到登记本身。
重新设计后,预订、搜索和库存功能如下图所示:
同样,库存和搜索更多地是预订模块的支持功能。它们与任何业务能力都不对齐。与之前的判断类似,最好将搜索和库存功能移动到预订中。假设,暂时将搜索、库存和预订移动到一个名为预订的单一微服务中。
根据 BrownField 的统计数据,搜索交易的频率比预订交易高 10 倍。此外,与预订相比,搜索不是一项产生收入的交易。由于这些原因,我们需要为搜索和预订采用不同的可扩展性模型。如果搜索交易突然激增,预订不应受到影响。从业务角度来看,为了保存有效的预订交易,放弃搜索交易更为可接受。
这是一个多语种需求的例子,它推翻了业务能力的对齐。在这种情况下,将搜索作为一个独立的服务,与预订服务分开更有意义。假设我们移除搜索。现在只有库存和预订留在预订中。现在搜索必须返回到预订中执行库存搜索。这可能会影响预订交易:
更好的方法是将库存与预订模块一起,并在搜索下保留库存的只读副本,同时通过可靠的消息系统持续同步库存数据。由于库存和预订都是同地的,这也解决了需要进行两阶段提交的需求。由于它们都是本地的,它们可以很好地与本地事务一起工作。
现在让我们挑战票价模块的设计。当客户在给定日期搜索 A 和 B 之间的航班时,我们希望同时显示航班和票价。这意味着我们的只读库存副本也可以同时组合票价和库存。搜索将订阅票价以获取任何票价变更事件。智能仍然留在票价服务中,但它不断将票价更新发送到搜索下的缓存票价数据。
最终依赖图
仍然有一些同步调用,暂时我们将保持它们不变。
通过应用所有这些变化,最终的依赖图将如下所示:
现在我们可以安全地将前图中的每个方框视为一个微服务。我们已经确定了许多依赖关系,并且将其中许多建模为异步的。整个系统基本上是以反应式风格设计的。在图中仍然显示了一些同步调用,如从办理登机手续获取批量、从 CRM 获取预订和从预订获取票价等,这些同步调用根据权衡分析实际上是必需的。
为迁移微服务设置优先级
我们已经确定了基于微服务的架构的第一次版本。下一步,我们将分析优先级,并确定迁移顺序。这可以通过考虑以下多个因素来完成:
- 依赖性:决定优先级的参数之一是依赖图。从服务依赖图中,具有较少依赖或根本没有依赖的服务易于迁移,而复杂的依赖关系则更难。具有复杂依赖关系的服务还需要将依赖模块与其一起迁移。
会计、忠诚度、CRM 和登机手续与预订和办理登机手续相比具有较少的依赖关系。具有高依赖性的模块在迁移时也会有更高的风险。
- 交易量:另一个可以应用的参数是分析交易量。迁移具有最高交易量的服务将减轻现有系统的负担。从 IT 支持和维护的角度来看,这将具有更多的价值。然而,这种方法的缺点是风险因素更高。
如前所述,搜索请求的数量是预订请求的十倍。在搜索和预订之后,办理登机手续的请求是交易量第三高的。
- 资源利用率:资源利用率是基于当前利用率来衡量的,例如 CPU、内存、连接池、线程池等。将资源密集型服务从传统系统中迁移出去可以为其他服务提供帮助。这有助于其余模块的更好运行。
航班、收入管理和会计是资源密集型服务,因为它们涉及数据密集型交易,如预测、计费、航班时间表更改等。
- 复杂性:复杂性可能是根据与服务相关的业务逻辑来衡量的,例如功能点、代码行数、表数、服务数等。与更复杂的模块相比,较不复杂的模块易于迁移。
与登机、搜索和办理登机手续服务相比,预订服务非常复杂。
- 业务重要性:业务重要性可以基于收入或客户体验。高度关键的模块提供更高的业务价值。
从业务角度来看,预订是最赚钱的服务,而办理登机手续是业务关键的,因为它可能导致航班延误,这可能导致收入损失以及客户不满意。
- 变更速度:变更速度表示在短时间内针对某个功能的变更请求数量。这意味着交付的速度和灵活性。具有高变更速度请求的服务比稳定模块更适合迁移。
统计数据显示,搜索、预订和票价经常发生变化,而办理登机手续是最稳定的功能。
- 创新:作为颠覆性创新过程的一部分的服务需要优先于基于更成熟业务流程的后勤功能。与在微服务世界应用创新相比,传统系统中的创新更难实现。
大多数创新都围绕搜索、预订、票价、收入管理和办理登机手续,而不是后勤会计。
根据 BrownField 的分析,搜索具有最高优先级,因为它需要创新,变更速度高,业务关键性较低,并且对业务和 IT 都有更好的缓解。搜索服务与传统系统没有最小的依赖性要求将数据同步回去。
迁移期间的数据同步
在过渡阶段,传统系统和新的微服务将并行运行。因此,保持两个系统之间的数据同步非常重要。
最简单的选择是使用任何数据同步工具在数据库级别同步两个系统之间的数据。当新旧系统都建立在相同的数据存储技术上时,这种方法效果很好。如果数据存储技术不同,复杂性将更高。这种方法的第二个问题是我们允许了一个后门入口,因此将微服务的内部数据存储暴露出去。这违反了微服务的原则。
在我们得出通用解决方案之前,让我们逐个案例来看待。以下图表显示了在搜索被移除后的数据迁移和同步方面:
迁移期间的数据同步
让我们假设我们使用 NoSQL 数据库来在搜索服务下保留库存和票价。在这种特殊情况下,我们只需要传统系统使用异步事件向新服务提供数据。我们将不得不对现有系统进行一些更改,以便发送票价变更或任何库存变更作为事件。搜索服务然后接受这些事件,并将它们本地存储到本地 NoSQL 存储中。
在复杂的预订服务的情况下,这会更加繁琐。
在这种情况下,新的预订微服务将库存变更事件发送到搜索服务。除此之外,传统应用还必须将票价变更事件发送到搜索。预订然后将新的预订服务存储在其 My SQL 数据存储中。
迁移期间的数据同步
最复杂的部分是预订服务,必须将预订事件和库存事件发送回传统系统。这是为了确保传统系统中的功能继续像以前一样工作。最简单的方法是编写一个更新组件,接受事件并更新旧的预订记录表,以便其他传统模块不需要进行更改。我们将继续进行此操作,直到没有任何传统组件在引用预订和库存数据。这将帮助我们最小化传统系统中的更改,从而减少失败的风险。
简而言之,单一方法可能不足以。需要基于不同模式的多管齐下的方法。
管理参考数据
将单体应用程序迁移到微服务的最大挑战之一是管理参考数据。一个简单的方法是将参考数据构建为另一个微服务,如下图所示:
在这种情况下,需要参考数据的人员应该通过微服务端点访问它。这是一个结构良好的方法,但可能会导致性能问题,就像在原始的旧系统中遇到的问题一样。
另一种方法是将参考数据作为所有管理和 CRUD 功能的微服务。然后在每个服务下创建一个近缓存,从主服务中逐步缓存数据。一个薄的参考数据访问代理库将被嵌入到这些服务中。参考数据访问代理抽象了数据是来自缓存还是远程服务的细节。
这在下一个图中有所描述。给定图中的主节点是实际的参考数据微服务:
挑战在于在主节点和从节点之间同步数据。对于那些频繁更改的数据缓存,需要订阅机制。
更好的方法是用内存数据网格替换本地缓存,如下图所示:
参考数据微服务将写入数据网格,而嵌入在其他服务中的代理库将具有只读 API。这消除了对数据订阅的要求,更加高效和一致。
用户界面和 Web 应用程序
在过渡阶段,我们必须同时保留旧的和新的用户界面。通常有三种一般性方法用于处理这种情况。
第一种方法是将旧的和新的用户界面作为独立的用户应用程序,彼此之间没有链接,如下图所示:
用户登录到新应用程序以及旧应用程序,就像两个不同的应用程序,它们之间没有单点登录(SSO)。这种方法简单,没有额外开销。在大多数情况下,除非针对两个不同的用户群体,否则业务可能不会接受这种方法。
第二种方法是将旧用户界面作为主要应用程序,然后在用户请求新应用程序的页面时将页面控件转移到新用户界面:
在这种情况下,由于旧应用程序和新应用程序都是在 Web 浏览器窗口中运行的 Web 应用程序,用户将获得无缝的体验。必须在旧和新用户界面之间实现 SSO。
第三种方法是直接将现有的旧用户界面集成到新的微服务后端,如下图所示:
在这种情况下,新的微服务被构建为无展示层的无头应用程序。这可能是具有挑战性的,因为它可能需要对旧的用户界面进行许多更改,比如引入服务调用、数据模型转换等。
在最后两种情况中的另一个问题是如何处理资源和服务的认证。
会话处理和安全性
假设新服务是基于 Spring Security 编写的,采用基于令牌的授权策略,而旧应用程序使用自定义构建的身份存储进行身份验证。
下图显示了如何在旧服务和新服务之间进行集成:
如前图所示,最简单的方法是使用 Spring Security 构建一个新的身份存储和认证服务作为一个新的微服务。这将用于我们所有未来的资源和服务保护,对于所有微服务。
现有的用户界面应用程序对新的身份验证服务进行身份验证,并获得一个令牌。这个令牌将被传递给新的用户界面或新的微服务。在这两种情况下,用户界面或微服务将调用身份验证服务来验证给定的令牌。如果令牌有效,那么用户界面或微服务接受调用。
问题在于,遗留身份存储必须与新的身份存储同步。
测试策略
从测试的角度来看,一个重要的问题是如何确保所有功能在迁移之前和之后都能正常工作?
在迁移或重构之前,应编写针对正在迁移的服务的集成测试用例。这可以确保一旦迁移完成,我们能够得到相同的预期结果,并且系统的行为保持不变。必须建立一个自动化的回归测试包,并且每次在新旧系统中进行更改时都必须执行。
对于每个服务,我们需要一个针对 EJB 端点的测试,另一个针对微服务端点的测试:
构建生态系统能力
在我们着手实际迁移之前,我们必须构建所有在能力模型下提到的微服务的能力,如第三章应用微服务概念中所记录的。这些是开发基于微服务的系统的先决条件。
除了这些能力,还需要预先构建某些应用功能,如参考数据、安全和 SSO,以及客户和通知。数据仓库或数据湖也是必需的先决条件。一个有效的方法是以增量方式构建这些能力,直到真正需要为止。
只有在必要的情况下才迁移模块。
在之前的章节中,我们已经研究了从单片应用转变为微服务的方法和步骤。重要的是要理解,除非真的需要,否则没有必要将所有模块迁移到新的微服务架构中。一个主要原因是这些迁移会产生成本。
我们将在这里审查一些这样的情景。BrownField 已经决定使用外部收入管理系统来取代 PSS 收入管理功能。BrownField 也正在将他们的会计功能集中化,因此,不需要迁移遗留系统的会计功能。在这一点上,迁移 CRM 并不会给业务增加太多价值。因此,决定将 CRM 保留在遗留系统中。业务计划作为他们的云策略的一部分转移到基于 SaaS 的 CRM 解决方案。还要注意,中途停止迁移可能会严重影响系统的复杂性。
目标架构
以下图表中的架构蓝图将之前的讨论整合成了一个架构视图。图表中的每个块代表一个微服务。阴影框是核心微服务,其他的是支持微服务。图表还显示了每个微服务的内部能力。用户管理已移至目标架构中的安全性下:
每个服务都有自己的架构,通常包括表示层、一个或多个服务端点、业务逻辑、业务规则和数据库。正如我们所看到的,我们使用不同的数据库选择,这些数据库更适合每个微服务。每个微服务都是自治的,服务之间的编排很少。大多数服务使用服务端点相互交互。
微服务的内部分层
在本节中,我们将进一步探讨微服务的内部结构。没有标准可供遵循微服务的内部架构。经验法则是在简单的服务端点背后抽象实现。
典型的结构看起来像下图所示:
UI 通过服务网关访问 REST 服务。API 网关可以是每个微服务一个,也可以是多个微服务一个,这取决于我们想要用 API 网关做什么。微服务可以暴露一个或多个 rest 端点。这些端点反过来连接到服务内的一个业务组件。业务组件然后借助领域实体执行所有业务功能。存储库组件用于与后端数据存储交互。
编排微服务
预订编排的逻辑和规则执行位于预订服务内。大脑仍然在预订服务内,以一个或多个预订业务组件的形式。在内部,业务组件编排其他业务组件或甚至外部服务暴露的私有 API:
如前图所示,预订服务内部调用更新其自己组件的库存,而不是调用票价服务。
这项活动是否需要编排引擎?这取决于需求。在复杂的情况下,我们可能需要同时做很多事情。例如,内部创建预订应用了许多预订规则,它验证票价,验证库存,然后才创建预订。我们可能希望并行执行它们。在这种情况下,我们可以使用 Java 并发 API 或反应式 Java 库。
在极其复杂的情况下,我们可以选择集成框架,如 Spring Integration 或 Apache Camel 的嵌入模式。
与其他系统集成
在微服务世界中,我们使用 API 网关或可靠的消息总线来与其他非微服务集成。
假设 BrownField 中有另一个系统需要预订数据。不幸的是,该系统无法订阅预订微服务发布的预订事件。在这种情况下,可以使用企业应用集成(EAI)解决方案,它监听我们的预订事件,然后使用本地适配器更新数据库。
管理共享库
某些业务逻辑在多个微服务中使用。在这种情况下,这些共享库将在两个微服务中复制。
处理异常
检查预订场景以了解不同的异常处理方法。在下面的服务序列图中,有三条用叉号标记的线。这些是异常可能发生的潜在区域:
预订和票价之间存在同步通信。如果票价服务不可用怎么办?如果票价服务不可用,将错误返回给用户可能会导致收入损失。另一种想法是信任作为传入请求的一部分的票价。当我们提供搜索时,搜索结果也将包含票价。当用户选择航班并提交时,请求将包含所选的票价。如果票价服务不可用,我们信任传入的请求,并接受预订。我们将使用断路器和一个备用服务,该服务仅以特殊状态创建预订,并将预订排队等待手动操作或系统重试。
如果创建预订失败怎么办?如果创建预订意外失败,更好的选择是将消息返回给用户。我们可以尝试替代选项,但这可能会增加系统的整体复杂性。对于库存更新也是如此。
在创建预订和更新库存的情况下,我们避免了创建预订后库存更新出现意外失败的情况。由于库存很关键,最好将创建预订和更新库存都放在本地事务中。这是可能的,因为这两个组件都在同一子系统下。
如果考虑登记场景,登记向登机和预订发送事件,如下图所示:
考虑一种情况,即在完成登记后立即发生登记服务失败。其他消费者处理了此事件,但实际的登记被回滚了。这是因为我们没有使用两阶段提交。在这种情况下,我们需要一种回滚该事件的机制。这可以通过捕获异常并发送另一个“取消登记”事件来实现。
在这种情况下,需要注意的是为了最小化补偿事务的使用,发送登记事件被移至登记事务的末尾。这减少了发送事件后失败的机会。
另一方面,如果登记成功,但发送事件失败怎么办?我们可以考虑两种方法。第一种方法是调用备用服务将其存储在本地,然后使用另一个扫描程序在以后的某个时间发送事件。甚至可以多次重试。这可能会增加更多的复杂性,并且在所有情况下可能不高效。另一种方法是将异常返回给用户,以便用户可以重试。然而,从客户参与的角度来看,这可能并不总是好的。另一方面,前一种选项对于系统的健康状况更好。需要进行权衡分析,以找出给定情况的最佳解决方案。
目标实现视图
下图表示了 BrownField PSS 微服务系统的实现视图:
如前图所示,我们正在实施四个微服务作为示例:搜索、票价、预订和登记。为了测试应用程序,使用了 Spring MVC 和 Thymeleaf 模板开发了一个网站应用程序。异步消息传递是通过 RabbitMQ 实现的。在此示例实现中,使用默认的 H2 数据库作为内存存储以进行演示。
本节中的代码演示了本章审查微服务能力模型部分中强调的所有功能。
实现项目
BrownField 航空公司的 PSS 微服务系统的基本实现有五个核心项目,如下表所总结。该表还显示了这些项目使用的端口范围,以确保整本书的一致性:
微服务 | 项目 | 端口范围 |
---|---|---|
预订微服务 | chapter4.book | 8060 -8069 |
办理登机手续微服务 | chapter4.checkin | 8070 -8079 |
航班微服务 | chapter4.fares | 8080 -8089 |
搜索微服务 | chapter4.search | 8090 -8099 |
网站 | chapter4.website | 8001 |
该网站是用于测试 PSS 微服务的 UI 应用程序。
在本示例中,所有微服务项目都遵循与下图中所示的包结构相同的模式:
以下是不同包及其用途的解释:
-
根文件夹(
com.brownfield.pss.book
)包含默认的 Spring Boot 应用程序。 -
component
包承载所有服务组件,业务逻辑在其中实现。 -
控制器包承载着 REST 端点和消息端点。控制器类在内部利用组件类进行执行。
-
entity
包含用于映射到数据库表的 JPA 实体类。 -
存储库类被打包在
repository
包中,并且基于 Spring Data JPA。
运行和测试项目
按照下面列出的步骤构建和测试本章开发的微服务:
- 使用 Maven 构建每个项目。确保
test
标志关闭。测试程序假定其他依赖服务正在运行。如果依赖服务不可用,则测试将失败。在我们的示例中,预订和票价有直接依赖关系。我们将学习如何在第七章, 记录和监控微服务中避免这种依赖关系:
mvn -Dmaven.test.skip=true install
- 运行 RabbitMQ 服务器:
rabbitmq_server-3.5.6/sbin$ ./rabbitmq-server
- 在单独的终端窗口中运行以下命令:
java -jar target/fares-1.0.jar
java -jar target/search-1.0.jar
java -jar target/checkin-1.0.jar
java -jar target/book-1.0.jar
java -jar target/website-1.0.jar
-
网站项目有一个
CommandLineRunner
,它在启动时执行所有测试用例。一旦所有服务成功启动,就在浏览器中打开http://localhost:8001
。 -
浏览器要求输入基本安全凭据。使用
guest
或guest123
作为凭据。本示例仅显示了具有基本身份验证机制的网站安全性。如第二章, 使用 Spring Boot 构建微服务中所述,可以使用 OAuth2 实现服务级安全性。 -
输入正确的安全凭据会显示以下屏幕。这是我们 BrownField PSS 应用程序的主屏幕:
-
提交按钮调用搜索微服务以获取满足屏幕上条件的可用航班。在搜索微服务启动时预先填充了一些航班。如有需要,编辑搜索微服务代码以输入额外的航班。
-
下一个截图显示了带有航班列表的输出屏幕。预订链接将带我们到所选航班的预订屏幕:
-
下一个截图显示了预订屏幕。用户可以输入乘客信息,并通过点击确认按钮来创建预订。这会调用预订微服务,以及内部的票价服务。它还会向搜索微服务发送一条消息:
-
如果预订成功,下一个确认屏幕将显示预订参考号码:
-
让我们测试办理登机手续微服务。可以通过点击屏幕顶部的办理登机手续菜单来完成。使用前一步获得的预订参考号码来测试办理登机手续。如下图所示:
-
在上一个屏幕上点击搜索按钮会调用 Booking 微服务,并检索预订信息。点击办理登机手续链接进行办理登机手续。这会调用办理登机微服务:
-
如果办理登机成功,它会显示确认消息,如下一张截图所示,并附有确认号。这是通过内部调用办理登机服务来完成的。办理登机服务向 Booking 发送消息以更新登机状态:
摘要
在本章中,我们使用基本的 Spring Boot 功能实现并测试了 BrownField PSS 微服务。我们学习了如何使用微服务架构处理真实用例。
我们审查了从单体应用程序向微服务的真实世界演变的各个阶段。我们还评估了多种方法的利弊,以及迁移单体应用程序时遇到的障碍。最后,我们解释了我们审查的用例的端到端微服务设计。还验证了一个完整的微服务实现的设计和实施。
在下一章中,我们将看到 Spring Cloud 项目如何帮助我们将开发的 BrownField PSS 微服务转换为互联网规模的部署。
第五章:使用 Spring Cloud 扩展微服务
为了管理互联网规模的微服务,需要比 Spring Boot 框架提供的能力更多。Spring Cloud 项目有一套专门构建的组件,可以轻松实现这些额外的能力。
本章将深入了解 Spring Cloud 项目的各个组件,如 Eureka、Zuul、Ribbon 和 Spring Config,将它们与第三章应用微服务概念中讨论的微服务能力模型进行对比。它将演示 Spring Cloud 组件如何帮助扩展上一章中开发的 BrownField 航空公司 PSS 微服务系统。
到本章结束时,您将了解以下内容:
-
Spring Config 服务器用于外部化配置
-
Eureka 服务器用于服务注册和发现
-
Zuul 作为服务代理和网关的相关性
-
自动微服务注册和服务发现的实现
-
Spring Cloud 消息传递用于异步微服务组合
审查微服务能力
本章的示例探讨了微服务能力模型中讨论的微服务能力模型中的以下微服务能力,应用微服务概念 第三章:
-
软件定义的负载均衡器
-
服务注册表
-
配置服务
-
可靠的云消息传递
-
API 网关
审查 BrownField 的 PSS 实现
在第四章微服务演进-案例研究中,我们使用 Spring 框架和 Spring Boot 为 BrownField 航空公司设计和开发了基于微服务的 PSS 系统。从开发的角度来看,实现是令人满意的,并且它可以满足低交易量的需求。然而,这对于部署具有数百甚至数千个微服务的大型企业规模部署来说还不够好。
在第四章微服务演进-案例研究中,我们开发了四个微服务:搜索、预订、票价和办理登机手续。我们还开发了一个网站来测试这些微服务。
到目前为止,我们在微服务实现中已经完成了以下工作:
-
每个微服务都公开一组 REST/JSON 端点,用于访问业务能力
-
每个微服务使用 Spring 框架实现特定的业务功能。
-
每个微服务使用 H2 作为内存数据库存储自己的持久数据
-
微服务使用 Spring Boot 构建,其中嵌入了 Tomcat 服务器作为 HTTP 监听器
-
RabbitMQ 被用作外部消息服务。搜索、预订和办理登机手续通过异步消息进行交互
-
Swagger 与所有微服务集成,用于记录 REST API。
-
开发了基于 OAuth2 的安全机制来保护微服务
什么是 Spring Cloud?
Spring Cloud 项目是 Spring 团队的一个总称项目,它实现了分布式系统所需的一组常见模式,作为一组易于使用的 Java Spring 库。尽管它的名字是 Spring Cloud,但它本身并不是一个云解决方案。相反,它提供了在开发应用程序时所需的一些关键能力,这些应用程序遵循十二要素应用程序原则,并且使用 Spring Cloud,开发人员只需专注于使用 Spring Boot 构建业务能力,并利用 Spring Cloud 提供的分布式、容错和自愈能力。
Spring Cloud 的解决方案对部署环境是不可知的,可以在桌面 PC 或弹性云中开发和部署。使用 Spring Cloud 开发的云就绪解决方案也是不可知的,并且可以在许多云提供商(如 Cloud Foundry、AWS、Heroku 等)之间进行移植。如果不使用 Spring Cloud,开发人员将最终使用云供应商原生提供的服务,导致与 PaaS 提供商的深度耦合。开发人员的另一个选择是编写大量样板代码来构建这些服务。Spring Cloud 还提供了简单易用的 Spring 友好 API,抽象了云提供商的服务 API,比如 AWS 通知服务的 API。
基于 Spring 的“约定优于配置”方法,Spring Cloud 默认所有配置,并帮助开发人员快速启动。Spring Cloud 还隐藏了复杂性,并提供简单的声明性配置来构建系统。Spring Cloud 组件的较小占用空间使其对开发人员友好,也使其易于开发云原生应用程序。
Spring Cloud 为开发人员提供了许多解决方案选择。例如,服务注册表可以使用流行的选项,如 Eureka、ZooKeeper 或 Consul 来实现。Spring Cloud 的组件相当解耦,因此开发人员可以灵活选择所需的内容。
注意
Spring Cloud 和 Cloud Foundry 有什么区别?
Spring Cloud 是一个用于开发互联网规模的 Spring Boot 应用程序的开发工具包,而 Cloud Foundry 是一个用于构建、部署和扩展应用程序的开源平台即服务。
Spring Cloud 发布
Spring Cloud 项目是一个包含不同组件组合的 Spring 项目。这些组件的版本在spring-cloud-starter-parent
BOM 中定义。
在本书中,我们依赖于 Spring Cloud 的Brixton.RELEASE
版本。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
spring-cloud-starter-parent
定义了其子组件的不同版本如下:
<spring-cloud-aws.version>1.1.0.RELEASE</spring-cloud-aws.version>
<spring-cloud-bus.version>1.1.0.RELEASE</spring-cloud-bus.version>
<spring-cloud-cloudfoundry.version>1.0.0.RELEASE</spring-cloud-cloudfoundry.version>
<spring-cloud-commons.version>1.1.0.RELEASE</spring-cloud-commons.version>
<spring-cloud-config.version>1.1.0.RELEASE</spring-cloud-config.version>
<spring-cloud-netflix.version>1.1.0.RELEASE</spring-cloud-netflix.version>
<spring-cloud-security.version>1.1.0.RELEASE</spring-cloud-security.version>
<spring-cloud-cluster.version>1.0.0.RELEASE</spring-cloud-cluster.version>
<spring-cloud-consul.version>1.0.0.RELEASE</spring-cloud-consul.version>
<spring-cloud-sleuth.version>1.0.0.RELEASE</spring-cloud-sleuth.version>
<spring-cloud-stream.version>1.0.0.RELEASE</spring-cloud-stream.version>
<spring-cloud-zookeeper.version>1.0.0.RELEASE </spring-cloud-zookeeper.version>
注意
Spring Cloud 发布的名称按字母顺序排列,从 A 开始,遵循伦敦地铁站的名称。Angel是第一个发布版本,Brixton是第二个发布版本。
Spring Cloud 组件
每个 Spring Cloud 组件都专门解决某些分布式系统功能。以下图表底部的灰色框显示了这些功能,放在这些功能上面的框展示了 Spring Cloud 子项目解决这些功能的能力:
Spring Cloud 的能力解释如下:
-
分布式配置:当有许多微服务实例在不同的配置文件下运行时,配置属性很难管理,比如开发、测试、生产等。因此,重要的是以受控的方式集中管理它们。分布式配置管理模块是为了外部化和集中微服务配置参数。Spring Cloud Config 是一个外部化配置服务器,使用 Git 或 SVN 作为后备存储库。Spring Cloud Bus 提供了对配置更改的支持,可以传播给多个订阅者,通常是一个微服务实例。另外,ZooKeeper 或 HashiCorp 的 Consul 也可以用于分布式配置管理。
-
路由:路由是一个 API 网关组件,主要用于类似于反向代理的功能,将消费者的请求转发给服务提供者。网关组件还可以执行基于软件的路由和过滤。Zuul 是一个轻量级的 API 网关解决方案,为开发人员提供了对流量整形和请求/响应转换的精细控制。
-
负载均衡:负载均衡能力需要一个软件定义的负载均衡器模块,它可以使用各种负载均衡算法将请求路由到可用的服务器。Ribbon 是一个支持这种能力的 Spring Cloud 子项目。Ribbon 可以作为一个独立的组件工作,也可以与 Zuul 集成并无缝地进行流量路由。
-
服务注册和发现:服务注册和发现模块使服务能够在服务可用并准备接受流量时以编程方式向存储库注册。微服务会公布它们的存在,并使它们可以被发现。消费者可以查找注册表以获取服务可用性和端点位置的视图。注册表在许多情况下更多地是一个垃圾场。但是注册表周围的组件使生态系统变得智能。在 Spring Cloud 下存在许多支持注册和发现能力的子项目。Eureka、ZooKeeper 和 Consul 是实现注册能力的三个子项目。
-
服务间调用:Spring Cloud 下的 Spring Cloud Feign 子项目提供了一种声明性的方式来以同步方式进行 RESTful 服务间调用。声明性方法允许应用程序使用 POJO(Plain Old Java Object)接口而不是低级 HTTP 客户端 API。Feign 在内部使用响应式库进行通信。
-
断路器:断路器子项目实现了断路器模式。当主要服务遇到故障时,断路器会断开电路,将流量转移到另一个临时的备用服务。当服务恢复正常时,它还会自动重新连接到主要服务。最后,它提供了一个监控仪表板,用于监控服务状态的变化。Spring Cloud Hystrix 项目和 Hystrix Dashboard 分别实现了断路器和仪表板。
-
全局锁、领导选举和集群状态:在处理大规模部署时,这种能力对于集群管理和协调是必需的。它还为各种目的提供全局锁,如序列生成。Spring Cloud Cluster 项目使用 Redis、ZooKeeper 和 Consul 实现了这些能力。
-
安全性:安全性能力是为了构建云原生分布式系统的安全性,使用外部授权提供者(如 OAuth2)。Spring Cloud Security 项目使用可定制的授权和资源服务器实现了这一能力。它还提供了 SSO 能力,在处理许多微服务时是必不可少的。
-
大数据支持:大数据支持能力是与大数据解决方案相关的数据服务和数据流所需的能力。Spring Cloud Streams 和 Spring Cloud Data Flow 项目实现了这些能力。Spring Cloud Data Flow 是 Spring XD 的重新设计版本。
-
分布式跟踪:分布式跟踪能力有助于跟踪和关联跨多个微服务实例的转换。Spring Cloud Sleuth 通过在各种分布式跟踪机制(如 Zipkin 和 HTrace)之上提供 64 位 ID 的支持来实现这一点。
-
分布式消息传递:Spring Cloud Stream 在可靠的消息传递解决方案(如 Kafka、Redis 和 RabbitMQ)之上提供了声明性的消息集成。
-
云支持:Spring Cloud 还提供了一组能力,它们在不同的云提供商(如 Cloud Foundry 和 AWS)之上提供各种连接器、集成机制和抽象。
Spring Cloud 和 Netflix OSS
许多用于微服务部署的 Spring Cloud 组件来自Netflix 开源软件(Netflix OSS)中心。Netflix 是微服务领域的先驱和早期采用者之一。为了管理大规模的微服务,Netflix 的工程师们提出了许多自制工具和技术来管理他们的微服务。这些工具和技术基本上是为了填补在 AWS 平台上管理 Netflix 服务时认识到的一些软件缺陷。后来,Netflix 将这些组件开源,并在 Netflix OSS 平台上提供给公众使用。这些组件在生产系统中被广泛使用,并在 Netflix 的大规模微服务部署中经过了实战测试。
Spring Cloud 为这些 Netflix OSS 组件提供了更高级别的抽象,使其更适合 Spring 开发人员使用。它还提供了一种声明性机制,与 Spring Boot 和 Spring 框架紧密集成和对齐。
为 BrownField PSS 设置环境
在本章中,我们将使用 Spring Cloud 的功能修改在第四章中开发的 BrownField PSS 微服务。我们还将研究如何使用 Spring Cloud 组件使这些服务达到企业级水平。
本章的后续部分将探讨如何使用 Spring Cloud 项目提供的一些开箱即用的功能,将在上一章中开发的微服务扩展到云规模部署。本章的其余部分将探讨 Spring Cloud 的功能,如使用 Spring Config 服务器进行配置,基于 Ribbon 的服务负载平衡,使用 Eureka 进行服务发现,使用 Zuul 进行 API 网关,最后,使用 Spring Cloud 消息传递进行基于消息的服务交互。我们将通过修改在第四章中开发的 BrownField PSS 微服务来演示这些功能,微服务演进-案例研究。
为了准备本章的环境,将项目导入并重命名(chapter4.*
为chapter5.*
)到一个新的 STS 工作空间中。
注意
本章的完整源代码可在代码文件的第五章
项目中找到。
Spring Cloud Config
Spring Cloud Config 服务器是一个外部化的配置服务器,应用程序和服务可以在其中存储、访问和管理所有运行时配置属性。Spring Config 服务器还支持配置属性的版本控制。
在之前的 Spring Boot 示例中,所有配置参数都是从打包在项目内的属性文件(application.properties
或application.yaml
)中读取的。这种方法很好,因为所有属性都从代码中移到了属性文件中。然而,当微服务从一个环境移动到另一个环境时,这些属性需要进行更改,这需要重新构建应用程序。这违反了十二要素应用程序原则之一,即倡导一次构建并将二进制文件移动到不同环境中。
更好的方法是使用配置文件。如在第二章中讨论的,配置文件用于将不同环境的不同属性进行分区。特定于配置文件的配置将被命名为application-{profile}.properties
。例如,application-development.properties
表示针对开发环境的属性文件。
然而,这种方法的缺点是配置静态地打包到应用程序中。对配置属性的任何更改都需要重新构建应用程序。
有多种方法可以将应用程序部署包中的配置属性外部化。可通过多种方式从外部源读取可配置属性:
-
从使用 JNDI 命名空间(
java:comp/env
)的外部 JNDI 服务器 -
使用 Java 系统属性(
System.getProperties()
)或使用-D
命令行选项 -
使用
PropertySource
配置:
@PropertySource("file:${CONF_DIR}/application.properties")
public class ApplicationConfig {
}
- 使用命令行参数指向外部位置的文件:
java -jar myproject.jar --spring.config.location=
JNDI 操作昂贵,缺乏灵活性,难以复制,并且没有版本控制。System.properties
对于大规模部署来说不够灵活。最后两个选项依赖于服务器上挂载的本地或共享文件系统。
对于大规模部署,需要一个简单而强大的集中式配置管理解决方案:
如前图所示,所有微服务都指向一个中央服务器以获取所需的配置参数。然后,这些微服务在本地缓存这些参数以提高性能。Config 服务器将配置状态更改传播给所有订阅的微服务,以便本地缓存的状态可以更新为最新更改。Config 服务器还使用配置文件来解析特定于环境的值。
如下截图所示,Spring Cloud 项目下有多个选项可用于构建配置服务器。Config Server、Zookeeper Configuration和Consul Configuration都是可选项。但本章将仅关注 Spring Config 服务器的实现:
Spring Config 服务器将属性存储在诸如 Git 或 SVN 之类的版本控制存储库中。Git 存储库可以是本地的或远程的。对于大规模分布式微服务部署,首选高可用的远程 Git 服务器。
Spring Cloud Config 服务器架构如下图所示:
如前图所示,嵌入在 Spring Boot 微服务中的 Config 客户端使用简单的声明性机制从中央配置服务器进行配置查找,并将属性存储到 Spring 环境中。配置属性可以是应用级配置,如每日交易限额,也可以是基础设施相关配置,如服务器 URL、凭据等。
与 Spring Boot 不同,Spring Cloud 使用一个引导上下文,这是主应用程序的父上下文。引导上下文负责从 Config 服务器加载配置属性。引导上下文寻找bootstrap.yaml
或bootstrap.properties
来加载初始配置属性。要使这在 Spring Boot 应用程序中工作,将application.*
文件重命名为bootstrap.*
。
接下来是什么?
接下来的几节演示了如何在实际场景中使用 Config 服务器。为了做到这一点,我们将修改我们的搜索微服务(chapter5.search
)以使用 Config 服务器。下图描述了这种情况:
在此示例中,搜索服务将通过传递服务名称在启动时读取 Config 服务器。在这种情况下,搜索服务的服务名称将是search-service
。为search-service
配置的属性包括 RabbitMQ 属性以及自定义属性。
注意
本节的完整源代码可在代码文件的chapter5.configserver
项目中找到。
设置 Config 服务器
创建新的 Config 服务器使用 STS 需要遵循以下步骤:
-
创建一个新的Spring Starter Project,并选择Config Server和Actuator,如下图所示:
-
设置一个 Git 存储库。这可以通过指向远程 Git 配置存储库来完成,比如
github.com/spring-cloud-samples/config-repo
上的存储库。这个 URL 是一个指示性的 URL,是 Spring Cloud 示例使用的 Git 存储库。我们将不得不使用我们自己的 Git 存储库。 -
或者,可以使用基于本地文件系统的 Git 存储库。在真实的生产场景中,建议使用外部 Git。本章中的配置服务器将使用基于本地文件系统的 Git 存储库进行演示。
-
输入下面列出的命令来设置本地 Git 存储库:
$ cd $HOME
$ mkdir config-repo
$ cd config-repo
$ git init .
$ echo message : helloworld > application.properties
$ git add -A .
$ git commit -m "Added sample application.properties"
application.properties with a message property and value helloworld is also created.
文件application.properties
是为演示目的而创建的。我们将在后续章节中更改这一点。
-
下一步是更改配置服务器中的配置,以使用在上一步中创建的 Git 存储库。为了做到这一点,将文件
application.properties
重命名为bootstrap.properties
: -
编辑新的
bootstrap.properties
文件的内容,使其与以下内容匹配:
server.port=8888
spring.cloud.config.server.git.uri: file://${user.home}/config-repo
端口8888
是配置服务器的默认端口。即使没有配置server.port
,配置服务器也应该绑定到8888
。在 Windows 环境中,文件 URL 需要额外的/
。
- 可选地,将自动生成的
Application.java
的默认包从com.example
重命名为com.brownfield.configserver
。在Application.java
中添加@EnableConfigServer
:
@EnableConfigServer
@SpringBootApplication
public class ConfigserverApplication {
-
通过右键单击项目并将其作为 Spring Boot 应用程序运行来运行配置服务器。
-
访问
http://localhost:8888/env
,以查看服务器是否正在运行。如果一切正常,这将列出所有环境配置。请注意,/env
是一个执行器端点。 -
检查
http://localhost:8888/application/default/master
,以查看特定于application.properties
的属性,这些属性是在之前的步骤中添加的。浏览器将显示在application.properties
中配置的属性。浏览器应该显示类似以下内容的内容:
{"name":"application","profiles":["default"],"label":"master","version":"6046fd2ff4fa09d3843767660d963866ffcc7d28","propertySources":[{"name":"file:///Users/rvlabs /config-repo /application.properties","source":{"message":"helloworld"}}]}
理解配置服务器 URL
在上一节中,我们使用了http://localhost:8888/application/default/master
来探索属性。我们如何解释这个 URL?
URL 中的第一个元素是应用程序名称。在给定的示例中,应用程序名称应该是application
。应用程序名称是给定应用程序的逻辑名称,使用 Spring Boot 应用程序的bootstrap.properties
中的spring.application.name
属性。每个应用程序必须有一个唯一的名称。配置服务器将使用名称来解析并从配置服务器存储库中获取适当的属性。应用程序名称有时也被称为服务 ID。如果有一个名为myapp
的应用程序,则配置存储库中应该有一个myapp.properties
来存储与该应用程序相关的所有属性。
URL 的第二部分表示配置文件。可以在存储库中为应用程序配置多个配置文件。配置文件可以在各种场景中使用。两个常见的场景是分隔不同的环境,如Dev
、Test
、Stage
、Prod
等,或者分隔服务器配置,如Primary
、Secondary
等。第一个表示应用程序的不同环境,而第二个表示部署应用程序的不同服务器。
配置文件名是将用于匹配存储库中文件名的逻辑名称。默认配置文件名为default
。要为不同的环境配置属性,我们必须根据以下示例配置不同的文件。在这个例子中,第一个文件是为开发环境而设,而第二个文件是为生产环境而设:
application-development.properties
application-production.properties
这些分别可以使用以下 URL 访问:
-
http://localhost:8888/application/development
-
http://localhost:8888/application/production
URL 的最后一部分是标签,默认情况下命名为master
。标签是一个可选的 Git 标签,如果需要的话可以使用。
简而言之,URL 基于以下模式:http://localhost:8888/{name}/{profile}/{label}
。
配置也可以通过忽略配置文件来访问。在前面的例子中,以下所有三个 URL 都指向相同的配置:
-
http://localhost:8888/application/default
-
http://localhost:8888/application/master
-
http://localhost:8888/application/default/master
有一个选项可以为不同的配置文件使用不同的 Git 存储库。这对于生产系统是有意义的,因为对不同存储库的访问可能是不同的。
从客户端访问配置服务器
在上一节中,已设置并通过 Web 浏览器访问了配置服务器。在本节中,将修改搜索微服务以使用配置服务器。搜索微服务将充当配置客户端。
按照以下步骤使用配置服务器而不是从application.properties
文件中读取属性:
- 将 Spring Cloud Config 依赖项和执行器(如果尚未就位)添加到
pom.xml
文件中。执行器对于刷新配置属性是强制性的:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
- 由于我们正在修改之前章节的 Spring Boot 搜索微服务,因此必须添加以下内容以包含 Spring Cloud 依赖项。如果项目是从头开始创建的,则不需要这样做:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
-
下一个屏幕截图显示了 Cloud starter 库选择屏幕。如果应用是从头开始构建的,请按照以下屏幕截图中显示的方式选择库:
-
将
application.properties
重命名为bootstrap.properties
,并添加应用程序名称和配置服务器 URL。如果配置服务器在本地主机上的默认端口(8888
)上运行,则配置服务器 URL 是非强制性的:
新的bootstrap.properties
文件将如下所示:
spring.application.name=search-service
spring.cloud.config.uri=http://localhost:8888
server.port=8090
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
search-service
是为搜索微服务指定的逻辑名称。这将被视为服务 ID。配置服务器将在存储库中查找search-service.properties
以解析属性。
- 为
search-service
创建一个新的配置文件。在创建 Git 存储库的config-repo
文件夹下创建一个新的search-service.properties
。请注意,search-service
是在bootstrap.properties
文件中为搜索微服务指定的服务 ID。将特定于服务的属性从bootstrap.properties
移动到新的search-service.properties
文件中。以下属性将从bootstrap.properties
中删除,并添加到search-service.properties
中:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
- 为了演示属性的集中配置和更改的传播,向属性文件中添加一个新的特定于应用程序的属性。我们将添加
originairports.shutdown
来临时将某个机场从搜索中移除。用户在搜索关闭列表中提到的机场时将不会得到任何航班:
originairports.shutdown=SEA
在此示例中,当使用SEA
作为起始地进行搜索时,我们将不返回任何航班。
- 通过执行以下命令将此新文件提交到 Git 存储库中:
git add –A .
git commit –m "adding new configuration"
- 最终的
search-service.properties
文件应如下所示:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
originairports.shutdown:SEA
chapter5.search
项目的bootstrap.properties
应如下所示:
spring.application.name=search-service
server.port=8090
spring.cloud.config.uri=http://localhost:8888
- 修改搜索微服务代码以使用配置参数
originairports.shutdown
。必须在类级别添加RefreshScope
注解,以允许在更改时刷新属性。在这种情况下,我们将在SearchRestController
类中添加一个刷新范围:
@RefreshScope
- 添加以下实例变量作为新属性的占位符,该属性刚刚添加到配置服务器中。
search-service.properties
文件中的属性名称必须匹配:
@Value("${originairports.shutdown}")
private String originAirportShutdownList;
- 更改应用程序代码以使用此属性。这是通过修改
search
方法来完成的:
@RequestMapping(value="/get", method = RequestMethod.POST)
List<Flight> search(@RequestBody SearchQuery query){
logger.info("Input : "+ query);
if(Arrays.asList(originAirportShutdownList.split(",")).contains(query.getOrigin())){
logger.info("The origin airport is in shutdown state");
return new ArrayList<Flight>();
}
return searchComponent.search(query);
}
search
方法被修改为读取参数originAirportShutdownList
,并查看请求的起始地是否在关闭列表中。如果匹配,则搜索方法将返回一个空的航班列表,而不是继续进行实际搜索。
-
启动配置服务器。然后启动 Search 微服务。确保 RabbitMQ 服务器正在运行。
-
修改
chapter5.website
项目以匹配bootstrap.properties
的内容,如下所示,以利用配置服务器:
spring.application.name=test-client
server.port=8001
spring.cloud.config.uri=http://localhost:8888
- 将
Application.java
中的CommandLineRunner
的run
方法更改为查询 SEA 作为起始机场:
SearchQuery = new SearchQuery("SEA","SFO","22-JAN-16");
- 运行
chapter5.website
项目。CommandLineRunner
现在将返回一个空的航班列表。服务器将打印以下消息:
The origin airport is in shutdown state
处理配置更改
本节将演示如何在发生更改时传播配置属性:
- 将
search-service.properties
文件中的属性更改为以下内容:
originairports.shutdown:NYC
在 Git 存储库中提交更改。刷新此服务的配置服务器 URL(http://localhost:8888/search-service/default
),并查看属性更改是否反映出来。如果一切正常,我们将看到属性更改。前面的请求将强制配置服务器再次从存储库中读取属性文件。
- 再次运行网站项目,并观察
CommandLineRunner
的执行。请注意,在这种情况下,我们没有重新启动 Search 微服务或配置服务器。服务将像以前一样返回一个空的航班列表,并且仍然会报错如下:
The origin airport is in shutdown state
这意味着更改不会反映在 Search 服务中,服务仍然使用旧版本的配置属性。
- 为了强制重新加载配置属性,请调用 Search 微服务的
/refresh
端点。这实际上是执行器的刷新端点。以下命令将向/refresh
端点发送一个空的 POST:
curl –d {} localhost:8090/refresh
- 重新运行网站项目,并观察
CommandLineRunner
的执行。这应该返回我们从 SEA 请求的航班列表。请注意,如果预订服务没有运行,网站项目可能会失败。
/refresh
端点将刷新本地缓存的配置属性,并从配置服务器重新加载新值。
用于传播配置更改的 Spring Cloud Bus
采用上述方法,可以在不重新启动微服务的情况下更改配置参数。当服务运行的实例只有一个或两个时,这是很好的。如果有很多实例会发生什么?例如,如果有五个实例,那么我们必须针对每个服务实例进行/refresh
。这绝对是一项繁琐的活动:
Spring Cloud Bus 提供了一种机制,可以在不知道有多少实例或它们的位置的情况下刷新多个实例之间的配置。当有许多微服务的服务实例运行或有许多不同类型的微服务运行时,这是特别方便的。这是通过将所有服务实例连接到单个消息代理来完成的。每个实例都订阅更改事件,并在需要时刷新其本地配置。通过调用任一实例的/bus/refresh
端点来触发此刷新,然后通过云总线和公共消息代理传播更改。
在此示例中,RabbitMQ 被用作 AMQP 消息代理。按照以下记录的步骤来实现这一点:
- 在
chapter5.search
项目的pom.xml
文件中添加一个新的依赖项,以引入 Cloud Bus 依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
-
Search 微服务还需要连接到 RabbitMQ,但这已经在
search-service.properties
中提供了。 -
重建并重新启动搜索微服务。在这种情况下,我们将从命令行运行两个搜索微服务实例,如下所示:
java -jar -Dserver.port=8090 search-1.0.jar
java -jar -Dserver.port=8091 search-1.0.jar
搜索服务的两个实例现在将运行,一个在端口8090
上,另一个在8091
上。
-
重新运行网站项目。这只是为了确保一切正常。此时,搜索服务应返回一个航班。
-
现在,使用以下值更新
search-service.properties
,并提交到 Git:
originairports.shutdown:SEA
- 运行以下命令来执行
/bus/refresh
。请注意,我们正在针对一个实例运行新的总线端点,本例中为8090
:
curl –d {} localhost:8090/bus/refresh
- 立即,我们将看到两个实例的以下消息:
Received remote refresh request. Keys refreshed [originairports.shutdown]
总线端点会将消息内部发送到消息代理,最终被所有实例消耗,重新加载其属性文件。也可以通过指定应用程序名称来对特定应用程序应用更改,如下所示:
/bus/refresh?destination=search-service:**
我们还可以通过将属性名称设置为参数来刷新特定属性。
为配置服务器设置高可用性
前几节探讨了如何设置配置服务器,允许实时刷新配置属性。但是,在这种架构中,配置服务器是一个单点故障。
在上一节建立的默认架构中存在三个单点故障。其中一个是配置服务器本身的可用性,第二个是 Git 仓库,第三个是 RabbitMQ 服务器。
以下图表显示了配置服务器的高可用性架构:
以下是架构机制和原理的解释:
配置服务器需要高可用性,因为如果配置服务器不可用,服务将无法引导。因此,需要冗余的配置服务器以实现高可用性。但是,在服务引导后,如果配置服务器不可用,应用程序可以继续运行。在这种情况下,服务将使用上次已知的配置状态运行。因此,配置服务器的可用性不像微服务的可用性那样关键。
为了使配置服务器具有高可用性,我们需要多个配置服务器实例。由于配置服务器是一个无状态的 HTTP 服务,可以并行运行多个配置服务器实例。根据配置服务器的负载,必须调整实例的数量。bootstrap.properties
文件无法处理多个服务器地址。因此,应配置多个配置服务器以在负载均衡器或本地 DNS 后运行,并具有故障转移和回退功能。负载均衡器或 DNS 服务器的 URL 将配置在微服务的bootstrap.properties
文件中。这是在假设 DNS 或负载均衡器具有高可用性并能够处理故障转移的情况下。
在生产场景中,不建议使用基于本地文件的 Git 仓库。配置服务器通常应该由高可用性的 Git 服务支持。可以通过使用外部高可用性的 Git 服务或高可用性的内部 Git 服务来实现。也可以考虑使用 SVN。
也就是说,已经引导的配置服务器始终能够使用配置的本地副本进行工作。因此,只有在需要扩展配置服务器时才需要高可用性的 Git。因此,这也不像微服务可用性或配置服务器可用性那样重要。
注意
设置高可用性的 GitLab 示例可在about.gitlab.com/high-availability/
找到。
RabbitMQ 也必须配置为高可用性。RabbitMQ 的高可用性仅需要动态推送配置更改到所有实例。由于这更多是离线受控活动,因此不需要与组件所需的高可用性相同。
RabbitMQ 高可用性可以通过使用云服务或本地配置的高可用性 RabbitMQ 服务来实现。
注意
为 Rabbit MQ 设置高可用性的步骤在www.rabbitmq.com/ha.html
中有记录。
监控配置服务器的健康状况
配置服务器只是一个 Spring Boot 应用程序,默认配置有一个执行器。因此,所有执行器端点都适用于配置服务器。可以使用以下执行器 URL 监控服务器的健康状况:http://localhost:8888/health
。
配置服务器用于配置文件
我们可能会遇到需要完整的配置文件(如logback.xml
)进行外部化的情况。配置服务器提供了一种配置和存储此类文件的机制。通过使用以下 URL 格式可以实现:/{name}/{profile}/{label}/{path}
。
名称、配置文件和标签的含义与之前解释的相同。路径表示文件名,例如logback.xml
。
完成使用配置服务器的更改
为了构建完成 BrownField 航空公司的 PSS 的能力,我们必须利用配置服务器来完成所有服务。在给定的示例中,chapter5.*
中的所有微服务都需要进行类似的更改,以便查找配置服务器以获取配置参数。
以下是一些关键的更改考虑:
- 预订组件中的票价服务 URL 也将被外部化:
private static final String FareURL = "/fares";
@Value("${fares-service.url}")
private String fareServiceUrl;
Fare = restTemplate.getForObject(fareServiceUrl+FareURL +"/get?flightNumber="+record.getFlightNumber()+"&flightDate="+record.getFlightDate(),Fare.class);
fares-service.url.
- 目前我们没有将搜索、预订和办理登机服务中使用的队列名称外部化。在本章后面,这些将被更改为使用 Spring Cloud Streams。
Feign 作为声明式 REST 客户端
Fare fare = restTemplate.getForObject(FareURL +"/get?flightNumber="+record.getFlightNumber()+"&flightDate="+record.getFlightDate(),Fare.class);
为了使用 Feign,首先需要更改pom.xml
文件以包含 Feign 依赖项,如下所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
对于新的 Spring Starter 项目,可以从 starter 库选择屏幕或start.spring.io/
中选择Feign。这在Cloud Routing下可用,如下截图所示:
下一步是创建一个新的FareServiceProxy
接口。这将充当实际票价服务的代理接口:
@FeignClient(name="fares-proxy", url="localhost:8080/fares")
public interface FareServiceProxy {
@RequestMapping(value = "/get", method=RequestMethod.GET)
Fare getFare(@RequestParam(value="flightNumber") String flightNumber, @RequestParam(value="flightDate") String flightDate);
}
FareServiceProxy
接口有一个@FeignClient
注解。此注解告诉 Spring 基于提供的接口创建一个 REST 客户端。值可以是服务 ID 或逻辑名称。url
表示目标服务运行的实际 URL。name
或value
是必需的。在这种情况下,由于我们有url
,因此name
属性是无关紧要的。
使用此服务代理调用票价服务。在预订微服务中,我们必须告诉 Spring 存在于 Spring Boot 应用程序中的 Feign 客户端,这些客户端需要被扫描和发现。这将通过在BookingComponent
的类级别添加@EnableFeignClients
来完成。可选地,我们也可以给出要扫描的包名。
更改BookingComponent
,并对调用部分进行更改。这就像调用另一个 Java 接口一样简单:
Fare = fareServiceProxy.getFare(record.getFlightNumber(), record.getFlightDate());
重新运行预订微服务以查看效果。
FareServiceProxy
接口中的票价服务的 URL 是硬编码的:url="localhost:8080/fares"
。
目前我们会保持这样,但在本章后面我们会进行更改。
用于负载均衡的 Ribbon
在以前的设置中,我们总是使用单个微服务实例运行。URL 在客户端和服务对服务调用中都是硬编码的。在现实世界中,这不是一种推荐的方法,因为可能会有多个服务实例。如果有多个实例,那么理想情况下,我们应该使用负载均衡器或本地 DNS 服务器来抽象实际实例位置,并在客户端中配置别名或负载均衡器地址。然后,负载均衡器接收别名,并将其解析为可用实例之一。通过这种方法,我们可以在负载均衡器后面配置尽可能多的实例。这也有助于我们处理对客户端透明的服务器故障。
这可以通过 Spring Cloud Netflix Ribbon 实现。Ribbon 是一个客户端负载均衡器,可以在一组服务器之间进行轮询负载平衡。Ribbon 库可能还有其他负载平衡算法。Spring Cloud 提供了一种声明性的方式来配置和使用 Ribbon 客户端。
如前图所示,Ribbon 客户端会查找配置服务器以获取可用微服务实例列表,并默认应用轮询负载平衡算法。
为了使用 Ribbon 客户端,我们将不得不将以下依赖项添加到pom.xml
文件中:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
在从头开始开发的情况下,可以从 Spring Starter 库或start.spring.io/
中选择。Ribbon 在Cloud Routing下可用:
更新预订微服务配置文件booking-service.properties
,以包含一个新属性,用于保留票价微服务的列表:
fares-proxy.ribbon.listOfServers=localhost:8080,localhost:8081
回到上一节中创建的FareServiceProxy
类并编辑,以使用 Ribbon 客户端,我们注意到@RequestMapping
注解的值从/get
更改为/fares/get
,以便我们可以轻松地将主机名和端口移动到配置中:
@FeignClient(name="fares-proxy")
@RibbonClient(name="fares")
public interface FareServiceProxy {
@RequestMapping(value = "fares/get", method=RequestMethod.GET)
现在我们可以运行两个 Fares 微服务实例。在8080
上启动一个,另一个在8081
上启动:
java -jar -Dserver.port=8080 fares-1.0.jar
java -jar -Dserver.port=8081 fares-1.0.jar
运行预订微服务。当预订微服务启动时,CommandLineRunner
会自动插入一条预订记录。这将进入第一个服务器。
运行网站项目时,它调用预订服务。这个请求将进入第二个服务器。
在预订服务上,我们看到以下跟踪,其中说有两个服务器被列入:
DynamicServerListLoadBalancer:{NFLoadBalancer:name=fares-proxy,current
list of Servers=[localhost:8080, localhost:8081],Load balancer stats=Zone stats: {unknown=[Zone:unknown; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},
Eureka 用于注册和发现
到目前为止,我们已经实现了外部化配置参数以及在许多服务实例之间的负载平衡。
基于 Ribbon 的负载平衡对于大多数微服务需求是足够的。然而,在一些情况下,这种方法存在一些不足之处:
-
如果有大量的微服务,并且我们想要优化基础设施利用率,我们将不得不动态更改服务实例的数量和相关服务器。在配置文件中预测和预配置服务器 URL 并不容易。
-
针对高度可扩展的微服务的云部署,静态注册和发现不是一个好的解决方案,考虑到云环境的弹性特性。
-
在云部署方案中,IP 地址是不可预测的,并且在文件中静态配置将会很困难。每次地址发生变化时,我们都必须更新配置文件。
Ribbon 方法部分地解决了这个问题。使用 Ribbon,我们可以动态更改服务实例,但是每当我们添加新的服务实例或关闭实例时,我们将不得不手动更新配置服务器。尽管配置更改将自动传播到所有所需的实例,但手动配置更改在大规模部署中将无法使用。在管理大规模部署时,尽可能地进行自动化是至关重要的。
为了弥补这一差距,微服务应该通过动态注册服务可用性来自我管理其生命周期,并为消费者提供自动化发现。
理解动态服务注册和发现
动态注册主要是从服务提供者的角度来看的。通过动态注册,当启动新服务时,它会自动在中央服务注册表中列出其可用性。同样,当服务停止服务时,它会自动从服务注册表中删除。注册表始终保持服务的最新信息,以及它们的元数据。
动态发现适用于服务消费者的角度。动态发现是指客户端查找服务注册表以获取服务拓扑的当前状态,然后相应地调用服务。在这种方法中,不是静态配置服务 URL,而是从服务注册表中获取 URL。
客户端可以保留注册表数据的本地缓存,以加快访问速度。一些注册表实现允许客户端监视他们感兴趣的项目。在这种方法中,注册表服务器中的状态更改将传播到感兴趣的各方,以避免使用过时的数据。
有许多选项可用于动态服务注册和发现。Netflix Eureka、ZooKeeper 和 Consul 作为 Spring Cloud 的一部分可用,如下所示:start.spring.io/
。Etcd 是 Spring Cloud 之外可用的另一个服务注册表,用于实现动态服务注册和发现。在本章中,我们将重点关注 Eureka 的实现:
理解 Eureka
Spring Cloud Eureka 也来自 Netflix OSS。Spring Cloud 项目为集成 Eureka 与基于 Spring 的应用程序提供了一种 Spring 友好的声明性方法。Eureka 主要用于自注册、动态发现和负载平衡。Eureka 在内部使用 Ribbon 进行负载平衡:
如前图所示,Eureka 由服务器组件和客户端组件组成。服务器组件是所有微服务注册其可用性的注册表。注册通常包括服务标识和其 URL。微服务使用 Eureka 客户端注册其可用性。消费组件也将使用 Eureka 客户端来发现服务实例。
当微服务引导启动时,它会联系 Eureka 服务器,并使用绑定信息宣布自己的存在。注册后,服务端点每 30 秒向注册表发送 ping 请求以更新其租约。如果服务端点在几次尝试中无法更新其租约,该服务端点将从服务注册表中移除。注册信息将被复制到所有 Eureka 客户端,以便客户端必须为每个请求去远程 Eureka 服务器。Eureka 客户端从服务器获取注册信息,并在本地进行缓存。之后,客户端使用该信息来查找其他服务。此信息会定期更新(每 30 秒),通过获取上次获取周期和当前获取周期之间的增量更新。
当客户端想要联系微服务端点时,Eureka 客户端根据请求的服务 ID 提供当前可用服务的列表。Eureka 服务器具有区域感知能力。在注册服务时也可以提供区域信息。当客户端请求服务实例时,Eureka 服务会尝试找到在同一区域运行的服务。然后,Ribbon 客户端会在 Eureka 客户端提供的这些可用服务实例之间进行负载均衡。Eureka 客户端和服务器之间的通信是使用 REST 和 JSON 进行的。
设置 Eureka 服务器
在本节中,我们将介绍设置 Eureka 服务器所需的步骤。
注意
本节的完整源代码可在代码文件的chapter5.eurekaserver
项目中找到。请注意,Eureka 服务器的注册和刷新周期需要长达 30 秒。因此,在运行服务和客户端时,请等待 40-50 秒。
- 启动一个新的 Spring Starter 项目,并选择Config Client,Eureka Server和Actuator:
Eureka 服务器的项目结构如下图所示:
请注意,主应用程序的名称为EurekaserverApplication.java
。
- 将
application.properties
重命名为bootstrap.properties
,因为这是使用 Config 服务器。与之前一样,在bootstrap.properties
文件中配置 Config 服务器的详细信息,以便它可以找到 Config 服务器实例。bootstrap.properties
文件将如下所示:
spring.application.name=eureka-server1
server.port:8761
spring.cloud.config.uri=http://localhost:8888
Eureka 服务器可以以独立模式或集群模式设置。我们将从独立模式开始。默认情况下,Eureka 服务器本身也是另一个 Eureka 客户端。当有多个 Eureka 服务器运行以实现高可用性时,这是特别有用的。客户端组件负责从其他 Eureka 服务器同步状态。通过配置eureka.client.serviceUrl.defaultZone
属性,Eureka 客户端将其对等方。
在独立模式中,我们将eureka.client.serviceUrl.defaultZone
指向同一个独立实例。稍后我们将看到如何以集群模式运行 Eureka 服务器。
- 创建一个
eureka-server1.properties
文件,并将其更新到 Git 存储库中。eureka-server1
是在上一步中应用的bootstrap.properties
文件中给出的应用程序的名称。如下所示,serviceUrl
指向同一个服务器。一旦添加了以下属性,就将文件提交到 Git 存储库中:
spring.application.name=eureka-server1
eureka.client.serviceUrl.defaultZone:http://localhost:8761/eureka/
eureka.client.registerWithEureka:false
eureka.client.fetchRegistry:false
- 更改默认的
Application.java
。在这个例子中,包也被重命名为com.brownfield.pss.eurekaserver
,类名改为EurekaserverApplication
。在EurekaserverApplication
中,添加@EnableEurekaServer
:
@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {
-
现在我们准备启动 Eureka 服务器。确保 Config 服务器也已启动。右键单击应用程序,然后选择Run As | Spring Boot App。应用程序启动后,在浏览器中打开
http://localhost:8761
以查看 Eureka 控制台。 -
在控制台中,请注意当前在 Eureka 中注册的实例下没有实例注册。由于没有启用 Eureka 客户端的服务,因此此时列表为空。
-
对我们的微服务进行一些更改将启用使用 Eureka 服务的动态注册和发现。为此,首先我们必须将 Eureka 依赖项添加到
pom.xml
文件中。如果服务是使用 Spring Starter 项目新建的,那么选择Config Client,Actuator,Web以及Eureka discovery客户端如下所示: -
由于我们正在修改我们的微服务,在它们的
pom.xml
文件中添加以下附加依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
- 必须在各自的配置文件中的
config-repo
下的所有微服务中添加以下属性。这将帮助微服务连接到 Eureka 服务器。更新完成后提交到 Git:
eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/
-
在各自的 Spring Boot 主类中的所有微服务中添加
@EnableDiscoveryClient
。这要求 Spring Boot 在启动时注册这些服务,以宣传它们的可用性。 -
启动除预订之外的所有服务器。由于我们在预订服务上使用了 Ribbon 客户端,当我们将 Eureka 客户端添加到类路径中时,行为可能会有所不同。我们将很快解决这个问题。
-
转到 Eureka URL(
http://localhost:8761
),您可以看到所有三个实例都正在运行:
是时候解决预订的问题了。我们将删除之前的 Ribbon 客户端,改用 Eureka。Eureka 在内部使用 Ribbon 进行负载平衡。因此,负载平衡行为不会改变。
- 删除以下依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
-
还要从
FareServiceProxy
类中删除@RibbonClient(name="fares")
注释。 -
将
@FeignClient(name="fares-service")
更新为匹配实际票价微服务的服务 ID。在这种情况下,fare-service
是配置在票价微服务的bootstrap.properties
中的服务 ID。这是 Eureka 发现客户端发送到 Eureka 服务器的名称。服务 ID 将用作 Eureka 服务器中注册的服务的键。 -
还要从
booking-service.properties
文件中删除服务器列表。使用 Eureka,我们将从 Eureka 服务器动态发现此列表:
fares-proxy.ribbon.listOfServers=localhost:8080, localhost:8081
-
启动预订服务。您将看到
CommandLineRunner
成功创建了一个预订,其中涉及使用 Eureka 发现机制调用票价服务。返回 URL 以查看所有注册的服务: -
更改网站项目的
bootstrap.properties
文件,以利用 Eureka 而不是直接连接到服务实例。在这种情况下,我们将不使用 Feign 客户端。相反,为了演示目的,我们将使用负载平衡的RestTemplate
。将这些更改提交到 Git 存储库:
spring.application.name=test-client
eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/
-
在
Application
类中添加@EnableDiscoveryClient
,使客户端具有 Eureka 意识。 -
编辑
Application.java
和BrownFieldSiteController.java
。添加三个RestTemplate
实例。这次,我们用@Loadbalanced
对它们进行注释,以确保我们使用 Eureka 和 Ribbon 的负载平衡功能。RestTemplate
无法自动注入。因此,我们必须提供以下配置条目:
@Configuration
class AppConfiguration {
@LoadBalanced
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Autowired
RestTemplate searchClient;
@Autowired
RestTemplate bookingClient;
@Autowired
RestTemplate checkInClient;
- 我们使用这些
RestTemplate
实例来调用微服务。用在 Eureka 服务器中注册的服务 ID 替换硬编码的 URL。在下面的代码中,我们使用服务名称search-service
、book-service
和checkin-service
,而不是显式的主机名和端口:
Flight[] flights = searchClient.postForObject("http://search-service/search/get", searchQuery, Flight[].class);
long bookingId = bookingClient.postForObject("http://book-service/booking/create", booking, long.class);
long checkinId = checkInClient.postForObject("http://checkin-service/checkin/create", checkIn, long.class);
- 现在我们准备运行客户端。运行网站项目。如果一切正常,网站项目的
CommandLineRunner
将成功执行搜索、预订和办理登机手续。也可以通过将浏览器指向http://localhost:8001
来测试相同的情况。
Eureka 的高可用性
The client URLs point to each other, forming a peer network as shown in the following configuration:
eureka-server1.properties
eureka.client.serviceUrl.defaultZone:http://localhost:8762/eureka/
eureka.client.registerWithEureka:false
eureka.client.fetchRegistry:false
eureka-server2.properties
eureka.client.serviceUrl.defaultZone:http://localhost:8761/eureka/
eureka.client.registerWithEureka:false
eureka.client.fetchRegistry:false
更新 Eureka 的bootstrap.properties
文件,并将应用程序名称更改为eureka
。由于我们使用了两个配置文件,根据启动时提供的活动配置文件,配置服务器将寻找eureka-server1
或eureka-server2
:
spring.application.name=eureka
spring.cloud.config.uri=http://localhost:8888
启动两个 Eureka 服务器的实例,server1
在8761
上,server2
在8762
上:
java -jar –Dserver.port=8761 -Dspring.profiles.active=server1 demo-0.0.1-SNAPSHOT.jar
java -jar –Dserver.port=8762 -Dspring.profiles.active=server2 demo-0.0.1-SNAPSHOT.jar
我们所有的服务仍然指向第一个服务器server1
。打开两个浏览器窗口:http://localhost:8761
和http://localhost:8762
。
启动所有微服务。打开8761
的那个将立即反映出更改,而另一个将需要 30 秒才能反映出状态。由于这两个服务器都在一个集群中,状态在这两个服务器之间是同步的。如果我们将这些服务器放在负载均衡器/DNS 后面,那么客户端将始终连接到其中一个可用的服务器。
完成此练习后,切换回独立模式进行剩余的练习。
Zuul 代理作为 API 网关
在大多数微服务实现中,内部微服务端点不会暴露在外部。它们被保留为私有服务。一组公共服务将使用 API 网关向客户端公开。有许多原因可以这样做:
-
只有一组精选的微服务是客户端所需的。
-
如果需要应用特定于客户端的策略,可以在一个地方应用它们,而不是在多个地方。这种情况的一个例子是跨域访问策略。
-
在服务端点上实现特定于客户端的转换很困难。
-
如果需要数据聚合,特别是在带宽受限的环境中避免多个客户端调用,那么中间需要一个网关。
Zuul 是一个简单的网关服务或边缘服务,非常适合这些情况。Zuul 也来自 Netflix 微服务产品系列。与许多企业 API 网关产品不同,Zuul 为开发人员提供了根据特定要求进行配置或编程的完全控制:
Zuul 代理在内部使用 Eureka 服务器进行服务发现,并使用 Ribbon 在服务实例之间进行负载平衡。
Zuul 代理还能够进行路由、监控、管理弹性、安全等。简单来说,我们可以将 Zuul 视为反向代理服务。使用 Zuul,我们甚至可以通过在 API 层覆盖它们来改变底层服务的行为。
设置 Zuul
与 Eureka 服务器和 Config 服务器不同,在典型的部署中,Zuul 是特定于一个微服务的。但是,也有部署方式,其中一个 API 网关覆盖多个微服务。在这种情况下,我们将为我们的每个微服务添加 Zuul:Search、Booking、Fare 和 Check-in:
注意
本节的完整源代码可在代码文件的chapter5.*-apigateway
项目中找到。
- 逐个转换微服务。从 Search API Gateway 开始。创建一个新的 Spring Starter 项目,并选择Zuul、Config Client、Actuator和Eureka Discovery:
search-apigateway
的项目结构如下图所示:
- 下一步是将 API 网关与 Eureka 和 Config 服务器集成。创建一个名为
search-apigateway.property
的文件,其中包含下面给出的内容,并提交到 Git 存储库。
此配置还设置了如何转发流量的规则。在这种情况下,API 网关上的任何请求都应该发送到search-service
的/api
端点:
spring.application.name=search-apigateway
zuul.routes.search-apigateway.serviceId=search-service
zuul.routes.search-apigateway.path=/api/**
eureka.client.serviceUrl.defaultZone:http://localhost:8761/eureka/
search-service
是搜索服务的服务 ID,并将使用 Eureka 服务器进行解析。
- 更新
search-apigateway
的bootstrap.properties
文件如下。在这个配置中没有什么新的内容——服务的名称、端口和 Config 服务器的 URL:
spring.application.name=search-apigateway
server.port=8095
spring.cloud.config.uri=http://localhost:8888
- 编辑
Application.java
。在这种情况下,包名和类名也更改为com.brownfield.pss.search.apigateway
和SearchApiGateway
。还要添加@EnableZuulProxy
以告诉 Spring Boot 这是一个 Zuul 代理:
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class SearchApiGateway {
-
将其作为 Spring Boot 应用程序运行。在此之前,请确保 Config 服务器、Eureka 服务器和 Search 微服务正在运行。
-
更改网站项目的
CommandLineRunner
以及BrownFieldSiteController
以利用 API 网关:
Flight[] flights = searchClient.postForObject("http://search-apigateway/api/search/get", searchQuery, Flight[].class);
在这种情况下,Zuul 代理充当反向代理,将所有微服务端点代理给消费者。在前面的例子中,Zuul 代理并没有增加太多价值,因为我们只是将传入的请求传递给相应的后端服务。
当我们有一个或多个以下要求时,Zuul 特别有用:
-
在网关上强制执行身份验证和其他安全策略,而不是在每个微服务端点上执行。网关可以在将请求传递给相关服务之前处理安全策略、令牌处理等。它还可以根据一些业务策略进行基本拒绝,例如阻止来自某些黑名单用户的请求。
-
商业洞察和监控可以在网关级别实施。收集实时统计数据,并将其推送到外部系统进行分析。这将很方便,因为我们可以在一个地方做到这一点,而不是在许多微服务中应用它。
-
API 网关在需要基于细粒度控制的动态路由的场景中非常有用。例如,根据“原始国家”等业务特定值发送请求到不同的服务实例。另一个例子是来自一个地区的所有请求都要发送到一组服务实例。还有一个例子是所有请求特定产品的请求都必须路由到一组服务实例。
-
处理负载削减和限流要求是另一种 API 网关非常有用的场景。这是当我们必须根据设置的阈值来控制负载,例如一天内的请求数。例如,控制来自低价值第三方在线渠道的请求。
-
Zuul 网关在细粒度负载均衡场景中非常有用。Zuul、Eureka 客户端和 Ribbon 共同提供对负载均衡需求的细粒度控制。由于 Zuul 实现只是另一个 Spring Boot 应用程序,开发人员可以完全控制负载均衡。
-
Zuul 网关在需要数据聚合要求的场景中也非常有用。如果消费者需要更高级别的粗粒度服务,那么网关可以通过代表客户端内部调用多个服务来内部聚合数据。当客户端在低带宽环境中工作时,这是特别适用的。
Zuul 还提供了许多过滤器。这些过滤器分为前置过滤器、路由过滤器、后置过滤器和错误过滤器。正如名称所示,这些过滤器在服务调用的生命周期的不同阶段应用。Zuul 还为开发人员提供了编写自定义过滤器的选项。为了编写自定义过滤器,需要从抽象的ZuulFilter
中扩展,并实现以下方法:
public class CustomZuulFilter extends ZuulFilter{
public Object run(){}
public boolean shouldFilter(){}
public int filterOrder(){}
public String filterType(){}
一旦实现了自定义过滤器,将该类添加到主上下文中。在我们的示例中,将其添加到SearchApiGateway
类中,如下所示:
@Bean
public CustomZuulFilter customFilter() {
return new CustomZuulFilter();
}
如前所述,Zuul 代理是一个 Spring Boot 服务。我们可以以我们想要的方式以编程方式定制网关。如下所示,我们可以向网关添加自定义端点,然后可以调用后端服务:
@RestController
class SearchAPIGatewayController {
@RequestMapping("/")
String greet(HttpServletRequest req){
return "<H1>Search Gateway Powered By Zuul</H1>";
}
}
在前面的情况下,它只是添加了一个新的端点,并从网关返回一个值。我们还可以进一步使用@Loadbalanced RestTemplate
来调用后端服务。由于我们有完全的控制权,我们可以进行转换、数据聚合等操作。我们还可以使用 Eureka API 来获取服务器列表,并实现完全独立的负载均衡或流量整形机制,而不是 Ribbon 提供的开箱即用的负载均衡特性。
Zuul 的高可用性
Zuul 只是一个无状态的带有 HTTP 端点的服务,因此我们可以拥有任意数量的 Zuul 实例。不需要亲和力或粘性。然而,Zuul 的可用性非常关键,因为从消费者到提供者的所有流量都通过 Zuul 代理。然而,弹性扩展要求并不像后端微服务那样关键,那里发生了所有繁重的工作。
Zuul 的高可用性架构取决于我们使用 Zuul 的场景。典型的使用场景包括:
-
当客户端 JavaScript MVC(如 AngularJS)从远程浏览器访问 Zuul 服务时。
-
另一个微服务或非微服务通过 Zuul 访问服务
在某些情况下,客户端可能没有能力使用 Eureka 客户端库,例如,基于 PL/SQL 编写的旧应用程序。在某些情况下,组织政策不允许 Internet 客户端处理客户端负载均衡。对于基于浏览器的客户端,可以使用第三方的 Eureka JavaScript 库。
这一切归结于客户端是否使用 Eureka 客户端库。基于此,我们可以通过两种方式设置 Zuul 的高可用性。
当客户端也是 Eureka 客户端时,Zuul 的高可用性
在这种情况下,由于客户端也是另一个 Eureka 客户端,因此可以像其他微服务一样配置 Zuul。Zuul 使用服务 ID 向 Eureka 注册自己。然后客户端使用 Eureka 和服务 ID 来解析 Zuul 实例:
在前面的图表中显示,Zuul 服务使用服务 ID 在 Eureka 中注册自己,在我们的情况下是search-apigateway
。Eureka 客户端使用 ID search-apigateway
请求服务器列表。Eureka 服务器根据当前的 Zuul 拓扑返回服务器列表。基于这个列表,Eureka 客户端选择一个服务器并发起调用。
如前所述,客户端使用服务 ID 来解析 Zuul 实例。在下面的情况中,search-apigateway
是在 Eureka 中注册的 Zuul 实例 ID:
Flight[] flights = searchClient.postForObject("http://search-apigateway/api/search/get", searchQuery, Flight[].class);
当客户端不是 Eureka 客户端时的高可用性
在这种情况下,客户端无法使用 Eureka 服务器进行负载均衡。如下图所示,客户端将请求发送到负载均衡器,然后负载均衡器识别正确的 Zuul 服务实例。在这种情况下,Zuul 实例将在负载均衡器后面运行,例如 HAProxy 或类似 NetScaler 的硬件负载均衡器:
微服务仍然会通过 Eureka 服务器由 Zuul 进行负载均衡。
为所有其他服务完成 Zuul
为了完成这个练习,为所有的微服务添加 API 网关项目(将它们命名为*-apigateway
)。需要完成以下步骤来实现这个任务:
-
为每个服务创建新的属性文件,并检入 Git 存储库。
-
将
application.properties
更改为bootstrap.properties
,并添加所需的配置。 -
在每个
*-apigateway
项目的Application.java
中添加@EnableZuulProxy
。 -
在每个
*-apigateway
项目的Application.java
文件中添加@EnableDiscoveryClient
。 -
可选地,更改默认生成的包名和文件名。
最终,我们将拥有以下 API 网关项目:
-
chapter5.fares-apigateway
-
chapter5.search-apigateway
-
chapter5.checkin-apigateway
-
chapter5.book-apigateway
用于响应式微服务的流
Spring Cloud Stream 提供了对消息基础设施的抽象。底层的消息实现可以是 RabbitMQ、Redis 或 Kafka。Spring Cloud Stream 提供了一个声明性的方法来发送和接收消息:
如前图所示,Cloud Stream 基于源和接收器的概念工作。源代表消息发送者的视角,而接收器代表消息接收者的视角。
在图中所示的例子中,发送者定义了一个名为Source.OUTPUT
的逻辑队列,发送者向其发送消息。接收者定义了一个名为Sink.INPUT
的逻辑队列,从中接收者检索消息。OUTPUT
到INPUT
的物理绑定通过配置进行管理。在这种情况下,两者链接到同一个物理队列——RabbitMQ 上的MyQueue
。因此,一端的Source.OUTPUT
指向MyQueue
,另一端的Sink.INPUT
指向相同的MyQueue
。
Spring Cloud 提供了在一个应用程序中使用多个消息提供程序的灵活性,例如将来自 Kafka 的输入流连接到 Redis 输出流,而无需管理复杂性。Spring Cloud Stream 是基于消息的集成的基础。Cloud Stream 模块子项目是另一个 Spring Cloud 库,提供了许多端点实现。
作为下一步,重新构建云流的微服务间消息通信。如下图所示,我们将在搜索微服务下定义一个连接到InventoryQ
的SearchSink
。预订将为发送库存更改消息定义一个BookingSource
连接到InventoryQ
。类似地,登记定义了一个用于发送登记消息的CheckinSource
。预订定义了一个接收器BookingSink
,用于接收消息,都绑定到 RabbitMQ 上的CheckinQ
队列:
在这个例子中,我们将使用 RabbitMQ 作为消息代理:
- 将以下 Maven 依赖项添加到预订、搜索和登记中,因为这三个模块使用消息传递:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
- 将以下两个属性添加到
booking-service.properties
中。这些属性将逻辑队列inventoryQ
绑定到物理inventoryQ
,逻辑checkinQ
绑定到物理checkinQ
:
spring.cloud.stream.bindings.inventoryQ.destination=inventoryQ
spring.cloud.stream.bindings.checkInQ.destination=checkInQ
- 将以下属性添加到
search-service.properties
中。这个属性将逻辑队列inventoryQ
绑定到物理inventoryQ
:
spring.cloud.stream.bindings.inventoryQ.destination=inventoryQ
- 将以下属性添加到
checkin-service.properties
中。这个属性将逻辑队列checkinQ
绑定到物理checkinQ
:
spring.cloud.stream.bindings.checkInQ.destination=checkInQ
-
将所有文件提交到 Git 存储库。
-
下一步是编辑代码。搜索微服务从预订微服务中消费消息。在这种情况下,预订是源,搜索是接收器。
在预订服务的Sender
类中添加@EnableBinding
。这将使 Cloud Stream 根据类路径中可用的消息代理库进行自动配置。在我们的情况下,这是 RabbitMQ。参数BookingSource
定义了用于此配置的逻辑通道:
@EnableBinding(BookingSource.class)
public class Sender {
- 在这种情况下,
BookingSource
定义了一个名为inventoryQ
的消息通道,它在配置中与 RabbitMQ 的inventoryQ
物理绑定。BookingSource
使用注解@Output
来指示这是输出类型的消息,即从模块发出的消息。这些信息将用于消息通道的自动配置:
interface BookingSource {
public static String InventoryQ="inventoryQ";
@Output("inventoryQ")
public MessageChannel inventoryQ();
}
- 我们可以使用 Spring Cloud Stream 提供的默认
Source
类,而不是定义自定义类,如果服务只有一个源和一个接收器:
public interface Source {
@Output("output")
MessageChannel output();
}
- 在发送器中定义一个基于
BookingSource
的消息通道。以下代码将注入一个名为inventory
的输出消息通道,该通道已在BookingSource
中配置:
@Output (BookingSource.InventoryQ)
@Autowired
private MessageChannel;
- 重新实现
BookingSender
中的send
消息方法:
public void send(Object message){
messageChannel.
send(MessageBuilder.withPayload(message).
build());
}
- 现在以与预订服务相同的方式将以下内容添加到
SearchReceiver
类中:
@EnableBinding(SearchSink.class)
public class Receiver {
- 在这种情况下,
SearchSink
接口将如下所示。这将定义它连接的逻辑接收器队列。在这种情况下,消息通道被定义为@Input
,以指示该消息通道用于接受消息:
interface SearchSink {
public static String INVENTORYQ="inventoryQ";
@Input("inventoryQ")
public MessageChannel inventoryQ();
}
- 修改搜索服务以接受此消息:
@ServiceActivator(inputChannel = SearchSink.INVENTORYQ)
public void accept(Map<String,Object> fare){
searchComponent.updateInventory((String)fare.
get("FLIGHT_NUMBER"),(String)fare.
get("FLIGHT_DATE"),(int)fare.
get("NEW_INVENTORY"));
}
- 我们仍然需要我们在配置文件中拥有的 RabbitMQ 配置来连接到消息代理:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
server.port=8090
- 运行所有服务,并运行网站项目。如果一切正常,网站项目将成功执行搜索、预订和办理登机手续功能。也可以通过浏览器指向
http://localhost:8001
进行测试。
总结 BrownField PSS 架构
以下图表显示了我们使用 Config 服务器、Eureka、Feign、Zuul 和 Cloud Streams 创建的整体架构。该架构还包括所有组件的高可用性。在这种情况下,我们假设客户端使用 Eureka 客户端库:
以下表格给出了项目及其监听的端口的摘要:
微服务 | 项目 | 端口 |
---|---|---|
预订微服务 | chapter5.book | 8060 到 8064 |
办理登机手续微服务 | chapter5.checkin | 8070 到 8074 |
票价微服务 | chapter5.fares | 8080 到 8084 |
搜索微服务 | chapter5.search | 8090 到 8094 |
网站客户端 | chapter5.website | 8001 |
Spring Cloud Config 服务器 | chapter5.configserver | 8888 /8889 |
Spring Cloud Eureka 服务器 | chapter5.eurekaserver | 8761 /8762 |
预订 API 网关 | chapter5.book-apigateway | 8095 到 8099 |
办理登机手续 API 网关 | chapter5.checkin-apigateway | 8075 到 8079 |
票价 API 网关 | chapter5.fares-apigateway | 8085 到 8089 |
搜索 API 网关 | chapter5.search-apigateway | 8065 到 8069 |
按照以下步骤进行最终运行:
-
运行 RabbitMQ。
-
使用根级别的
pom.xml
构建所有项目:
mvn –Dmaven.test.skip=true clean install
- 从各自的文件夹运行以下项目。在启动下一个服务之前,请记得等待 40 到 50 秒。这将确保依赖服务在我们启动新服务之前已注册并可用:
java -jar target/fares-1.0.jar
java -jar target/search-1.0.jar
java -jar target/checkin-1.0.jar
java -jar target/book-1.0.jar
java –jar target/fares-apigateway-1.0.jar
java –jar target/search-apigateway-1.0.jar
java –jar target/checkin-apigateway-1.0.jar
java –jar target/book-apigateway-1.0.jar
java -jar target/website-1.0.jar
- 打开浏览器窗口,指向
http://localhost:8001
。按照第四章中的运行和测试项目部分中提到的步骤进行操作,微服务演进-案例研究。
摘要
在本章中,您学习了如何使用 Spring Cloud 项目扩展十二要素 Spring Boot 微服务。然后,您学到的知识被应用到了我们在上一章中开发的 BrownField 航空公司的 PSS 微服务中。
然后,我们探讨了 Spring Config 服务器以外部化微服务的配置,以及部署高可用性的 Config 服务器的方法。我们还讨论了使用 Feign 进行声明式服务调用,研究了 Ribbon 和 Eureka 用于负载平衡、动态服务注册和发现的用法。通过实现 Zuul 来检查 API 网关的实现。最后,我们以使用 Spring Cloud Stream 进行响应式风格的微服务集成来结束。
BrownField 航空公司的 PSS 微服务现在可以在互联网规模上部署。其他 Spring Cloud 组件,如 Hyterix、Sleuth 等,将在第七章中介绍,微服务的日志记录和监控。下一章将演示自动缩放功能,扩展 BrownField PSS 实现。