Spring-微服务-全-

Spring 微服务(全)

原文:zh.annas-archive.org/md5/52026E2A45414F981753F74B874EEB00

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

微服务是一种架构风格和模式,复杂系统被分解为较小的服务,这些服务共同形成更大的业务服务。微服务是自治的、独立的,可以独立部署的服务。在当今世界,许多企业将微服务作为构建大型面向服务的企业应用程序的默认标准。

Spring 框架是开发者社区多年来广受欢迎的编程框架。Spring Boot 消除了需要使用笨重的应用容器,并提供了一种部署轻量级、无服务器应用程序的方法。Spring Cloud 结合了许多 Netflix OSS 组件,并提供了一个运行和管理大规模微服务的生态系统。它提供了诸如负载平衡、服务注册、监控、服务网关等功能。

然而,微服务也带来了自己的挑战,例如监控、管理、分发、扩展、发现等,特别是在大规模部署时。在不解决常见的微服务挑战的情况下采用微服务将导致灾难性的结果。本书最重要的部分是一个技术无关的微服务能力模型,它有助于解决所有常见的微服务挑战。

本书的目标是以务实的方法和指南为读者提供在大规模实施响应式微服务时的启示。本书将带领读者深入了解 Spring Boot、Spring Cloud、Docker、Mesos 和 Marathon。本书的读者将了解 Spring Boot 如何用于部署无服务器的自治服务,从而消除了使用笨重的应用服务器的需要。读者将学习不同的 Spring Cloud 功能,并了解 Docker 用于容器化以及 Mesos 和 Marathon 用于计算资源抽象和集群范围控制的用途。

我相信读者会喜欢本书的每一部分。而且,我真诚地相信本书通过成功构思微服务为您的业务增添了巨大的价值。在整本书中,我通过提供许多例子,包括旅行领域的案例研究,使用了微服务实施的实际方面。最终,您将学会如何使用 Spring 框架、Spring Boot 和 Spring Cloud 实施微服务架构。这些都是经过实战检验的强大工具,用于开发和部署可扩展的微服务。根据 Spring 的最新规范编写,借助本书的帮助,您将能够在短时间内构建现代的、互联网规模的 Java 应用程序。

本书涵盖的内容

《第一章》《解密微服务》为您介绍了微服务。本章涵盖了微服务的基本概念、它们的演变以及它们与面向服务的架构的关系,以及云原生和十二要素应用的概念。

《第二章》《使用 Spring Boot 构建微服务》介绍了如何使用 Spring 框架构建基于 REST 和消息的微服务,以及如何使用 Spring Boot 封装它们。此外,我们还将探索 Spring Boot 的一些核心功能。

《第三章》《应用微服务概念》详细解释了微服务实施的实际方面,详细说明了开发人员在企业级微服务中面临的挑战。这也将总结成功管理微服务生态系统所需的能力。

第四章,“微服务演进-案例研究”,将读者带入了一个真实的微服务演进案例研究,介绍了 BrownField 航空公司。使用案例研究,本章解释了如何应用前几章学到的微服务概念。

第五章,“使用 Spring Cloud 扩展微服务”,展示了如何使用 Spring Cloud 堆栈功能扩展先前的示例。它详细介绍了 Spring Cloud 的架构和不同组件以及它们如何集成在一起。

第六章,“微服务的自动扩展”,演示了使用简单的生命周期管理器来实现弹性和微服务的自我管理,通过编排服务网关来编排服务。它解释了在现实世界中如何向服务网关添加智能。

第七章,“微服务的日志记录和监控”,涵盖了在开发微服务时日志记录和监控方面的重要性。在这里,我们将详细介绍使用微服务时一些最佳实践,如使用开源工具实现集中式日志记录和监控功能,以及如何将它们与 Spring 项目集成。

第八章,“使用 Docker 容器化微服务”,解释了微服务上下文中的容器化概念。使用 Mesos 和 Marathon,本章演示了一个替代自定义生命周期管理器用于大规模部署的下一级实现。

第九章,“使用 Mesos 和 Marathon 管理 Docker 化的微服务”,解释了微服务的自动配置和部署。在这里,您还将学习如何在前面的示例中使用 Docker 容器进行大规模部署。

第十章,“微服务开发生命周期”,涵盖了微服务开发的过程和实践。本章还解释了 DevOps 和持续交付管道的重要性。

本书需要什么

第二章,“使用 Spring Boot 构建微服务”,介绍了 Spring Boot,需要以下软件组件来测试代码:

  • JDK 1.8

  • Spring Tool Suite 3.7.2 (STS)

  • Maven 3.3.1

  • Spring Framework 4.2.6.RELEASE

  • Spring Boot 1.3.5.RELEASE

  • spring-boot-cli-1.3.5.RELEASE-bin.zip

  • RabbitMQ 3.5.6

  • FakeSMTP

在第五章,“使用 Spring Cloud 扩展微服务”,您将了解 Spring Cloud 项目。除了前面提到的软件组件外,还需要以下软件组件:

  • Spring Cloud Brixton.RELEASE

在第七章,“微服务的日志记录和监控”中,我们将看看如何为微服务实现集中式日志记录。这需要以下软件堆栈:

  • Elasticsearch 1.5.2

  • kibana-4.0.2-darwin-x64

  • Logstash 2.1.2

在第八章,“使用 Docker 容器化微服务”,我们将演示如何使用 Docker 进行微服务部署。这需要以下软件组件:

  • Docker 版本 1.10.1

  • Docker Hub

第九章,“使用 Mesos 和 Marathon 管理 Docker 化的微服务”,使用 Mesos 和 Marathon 将 docker 化的微服务部署到可自动扩展的云中。为此需要以下软件组件:

  • Mesos 版本 0.27.1

  • Docker 版本 1.6.2

  • 本书主要面向寻求构建云就绪的互联网规模应用程序以满足现代业务需求的 Spring 开发人员。本书将通过检查许多真实用例和实际代码示例,帮助开发人员了解微服务的确切含义以及它们为何在当今世界中如此重要。开发人员将了解如何构建简单的 RESTful 服务,并将其有机地发展成真正的企业级微服务生态系统。

马拉松版本 0.15.3

警告或重要说明以如下方式显示在一个框中。

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以如下方式出现在文本中:“点击发出请求按钮。”

代码块设置如下:

本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是这些样式的一些示例以及它们的含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“可以在application.properties中设置以下属性来自定义与应用程序相关的信息。”

<parent>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.4.RELEASE</version>
</parent>

当我们希望引起您对代码块的特定部分的注意时,相关行或项目以粗体设置:

eureka-server2.properties
eureka.client.serviceUrl.defaultZone:http://localhost:8761/eureka/
eureka.client.registerWithEureka:false
eureka.client.fetchRegistry:false

任何命令行输入或输出都以以下方式编写:

$ java -jar fakeSMTP-2.0.jar

本书适合对象

注意

约定

提示和技巧以如下方式出现。

本书将对寻求使用 Spring 框架、Spring Boot 和 Spring Cloud 设计强大的互联网规模微服务,并使用 Docker、Mesos 和 Marathon 进行管理的架构师感兴趣。能力模型将帮助架构师制定甚至超越本书讨论的工具和技术的解决方案。

第一章:解密微服务

微服务是一种软件开发的架构风格和方法,以满足现代业务需求。微服务并非是创新,而是从以前的架构风格中演变而来。

我们将从更近距离地观察微服务架构从传统的大型架构演变而来。我们还将研究微服务的定义、概念和特点。最后,我们将分析微服务的典型用例,并建立微服务与其他架构方法(如面向服务的架构(SOA)和十二要素应用)之间的相似性和关系。十二要素应用定义了一套针对云平台开发应用程序的软件工程原则。

在本章中,您将学习以下内容:

  • 微服务的演进

  • 带有示例的微服务架构的定义

  • 微服务架构的概念和特点

  • 微服务架构的典型用例

  • 微服务与 SOA 和十二要素应用的关系

微服务的演进

微服务是继 SOA 之后越来越受欢迎的架构模式之一,与 DevOps 和云相辅相成。微服务的演进受到了现代业务中颠覆性数字创新趋势和过去几年技术演进的极大影响。我们将在本节中研究这两个因素。

业务需求作为微服务演进的催化剂

在这个数字转型的时代,企业越来越多地采用技术作为大幅增加其收入和客户群的关键推动力之一。企业主要利用社交媒体、移动、云、大数据和物联网作为实现颠覆性创新的工具。利用这些技术,企业找到了快速渗透市场的新途径,这严重挑战了传统的 IT 交付机制。

以下图表显示了传统开发和微服务在面对敏捷性、交付速度和规模等新企业挑战方面的状态。

业务需求作为微服务演进的催化剂

提示

微服务承诺比传统的大型应用程序更具敏捷性、交付速度和规模。

过去企业投资于数年的大型应用程序开发的时代已经过去了。企业不再有兴趣开发整合的应用程序来管理他们的端到端业务功能,就像几年前那样。

以下图表显示了传统的大型应用程序和微服务在周转时间和成本方面的比较。

业务需求作为微服务演进的催化剂

提示

微服务提供了一种开发快速敏捷应用程序的方法,从而降低总体成本。

例如,如今,航空公司或金融机构不再投资于重建他们的核心大型主机系统,以避免另一个庞大的怪物。零售商和其他行业也不再重建重量级供应链管理应用程序,比如他们传统的 ERP 系统。焦点已转向构建满足业务特定需求的快速解决方案,以最敏捷的方式进行。

让我们举个例子,一个在线零售商正在使用传统的大型应用程序。如果零售商希望通过根据客户的过往购物、偏好等个性化客户的产品来创新销售,并且还希望根据客户购买倾向来为客户提供产品,他们将快速开发个性化引擎或根据客户的即时需求提供优惠,并将其插入他们的传统应用程序中。

业务需求作为微服务演变的催化剂

如前面的图表所示,与其投资于重建核心遗留系统,这将通过将响应通过新功能传递来完成,如图中标有A的部分所示,或者通过修改核心遗留系统以调用这些功能作为处理的一部分,如图中标有B的部分所示。这些功能通常被编写为微服务。

这种方法为组织提供了大量的机会,以较低的成本在实验模式下快速尝试新功能。企业可以随后验证关键绩效指标,并根据需要修改或替换这些实现。

提示

现代架构期望最大化替换其部分并最小化替换其部分的成本。微服务方法是实现这一目标的手段。

技术作为微服务演变的催化剂

新兴技术也使我们重新思考构建软件系统的方式。例如,几十年前,我们甚至无法想象没有两阶段提交的分布式应用程序。后来,NoSQL 数据库使我们有了不同的思考方式。

同样,技术上的这种范式转变已经重塑了软件架构的所有层面。

HTML 5 和 CSS3 的出现以及移动应用程序的进步重新定位了用户界面。诸如 Angular、Ember、React、Backbone 等客户端 JavaScript 框架因其客户端渲染和响应式设计而广受欢迎。

随着云采用成为主流,平台即服务PaaS)提供商,如 Pivotal CF、AWS、Salesforce.com、IBM 的 Bluemix、RedHat 的 OpenShift 等,使我们重新思考构建中间件组件的方式。Docker 带来的容器革命从根本上影响了基础设施领域。如今,基础设施被视为一种商品服务。

集成景观也随着集成平台即服务iPaaS)的出现而发生了变化。Dell Boomi、Informatica、MuleSoft 等平台是 iPaaS 的例子。这些工具帮助组织将集成边界延伸到传统企业之外。

NoSQL 已经彻底改变了数据库领域。几年前,我们只有几种流行的数据库,都基于关系数据建模原则。今天我们有了一个很长的数据库列表:Hadoop、Cassandra、CouchDB、Neo 4j 等等。每个数据库都解决了特定的架构问题。

命令式架构演变

应用架构一直在与不断变化的业务需求和技术的发展一起不断演进。架构已经经历了古老的主机系统的演变,到完全抽象的云服务,比如 AWS Lambda。

提示

使用 AWS Lambda,开发人员现在可以将他们的“功能”放入一个完全托管的计算服务中。

aws.amazon.com/documentation/lambda/了解更多关于 Lambda 的信息。

不同的架构方法和风格,如主机、客户端服务器、N 层和面向服务的架构,在不同的时间段都很受欢迎。无论选择哪种架构风格,我们总是用来构建单片架构的一种或另一种形式。微服务架构的演变是现代业务需求(如敏捷性和交付速度)、新兴技术以及从以前一代架构中学到的结果。

命令式架构演变

微服务帮助我们打破了单片应用程序的边界,并构建了一个逻辑上独立的更小的系统,如前面的图表所示。

提示

如果我们将单片应用程序视为一组逻辑子系统,包含有物理边界,那么微服务就是一组没有封闭物理边界的独立子系统。

什么是微服务?

微服务是许多组织今天使用的一种架构风格,作为实现高度敏捷性、交付速度和规模的游戏规则改变者。微服务为我们提供了一种开发更加物理分离的模块化应用程序的方式。

微服务并非创造出来的。许多组织,如 Netflix、亚马逊和 eBay,成功地使用分而治之的技术,将其单片应用程序功能性地分割成更小的原子单元,每个单元执行单一功能。这些组织解决了他们在单片应用程序中遇到的一些问题。

在这些组织的成功之后,许多其他组织开始将这种模式作为重构其单片应用程序的常见模式。后来,倡导者将这种模式称为微服务架构。

微服务源自 Alistair Cockburn 提出的六边形架构的概念。六边形架构也被称为端口和适配器模式。

提示

alistair.cockburn.us/Hexagonal+architecture了解更多关于六边形架构。

微服务是一种建立 IT 系统的架构风格或方法,作为一组自治、自包含和松耦合的业务能力:

什么是微服务?

前面的图表描述了传统的 N 层应用程序架构,包括表示层、业务层和数据库层。模块ABC代表三种不同的业务能力。图表中的层代表架构关注点的分离。每个层包含与该层相关的所有三种业务能力。表示层包含所有三个模块的 Web 组件,业务层包含所有三个模块的业务组件,数据库托管所有三个模块的表。在大多数情况下,层是可以物理分开的,而层内的模块是硬连接的。

现在让我们来看看基于微服务的架构。

什么是微服务?

正如前面的图表所示,微服务架构中的边界是相反的。每个垂直切片代表一个微服务。每个微服务都有自己的表示层、业务层和数据库层。微服务是与业务能力对齐的。通过这样做,对一个微服务的更改不会影响其他微服务。

微服务的通信或传输机制没有标准。一般来说,微服务使用广泛采用的轻量级协议进行通信,如 HTTP 和 REST,或者消息协议,如 JMS 或 AMQP。在特定情况下,可以选择更优化的通信协议,如 Thrift、ZeroMQ、Protocol Buffers 或 Avro。

由于微服务更加符合业务能力,并且具有独立可管理的生命周期,它们是企业在采用 DevOps 和云时的理想选择。DevOps 和云是微服务的两个方面。

提示

DevOps 是一种 IT 重新调整,缩小传统 IT 开发和运营之间的差距,以提高效率。

了解更多关于 DevOps:

dev2ops.org/2010/02/what-is-devops/

微服务-蜂窝类比

蜂窝是一个理想的类比,用来代表演进的微服务架构。

微服务-蜂窝类比

在现实世界中,蜜蜂通过排列六边形蜂窝来建造蜂巢。它们从小开始,使用不同的材料来建造蜂窝。建造是基于当时可用的材料。重复的蜂窝形成了一个模式,并形成了一个坚固的结构。蜂巢中的每个蜂房都是独立的,但也与其他蜂房集成在一起。通过添加新的蜂房,蜂巢有机地增长成一个大而坚固的结构。每个蜂房内部的内容是抽象的,对外不可见。一个蜂房的损坏不会影响其他蜂房,蜜蜂可以重建这些蜂房而不影响整个蜂巢。

微服务的原则

在这一部分,我们将研究微服务架构的一些原则。这些原则在设计和开发微服务时是“必须具备”的。

每个服务只有一个责任

单一责任原则是 SOLID 设计模式的一部分所定义的原则之一。它指出一个单元应该只有一个责任。

提示

在这里了解更多关于 SOLID 设计模式的信息:

c2.com/cgi/wiki?PrinciplesOfObjectOrientedDesign

这意味着一个单元,无论是类、函数还是服务,都应该只有一个责任。在任何时候,两个单元都不应该共享一个责任,或者一个单元有多个责任。一个具有多个责任的单元表示紧密耦合。

每个服务只有一个责任

如前图所示,CustomerProductOrder是电子商务应用程序的不同功能。与其将它们全部构建到一个应用程序中,最好是有三个不同的服务,每个负责一个业务功能,这样对一个责任的更改不会影响其他责任。在前述情景中,CustomerProductOrder将被视为三个独立的微服务。

微服务是自治的

微服务是自包含的、独立部署的、自治的服务,完全负责业务能力及其执行。它们捆绑了所有依赖,包括库依赖,以及抽象物理资源的执行环境,如 Web 服务器、容器或虚拟机。

微服务和 SOA 之间的一个主要区别在于它们的自治级别。虽然大多数 SOA 实现提供服务级别的抽象,但微服务更进一步,抽象了实现和执行环境。

在传统的应用程序开发中,我们构建一个 WAR 或 EAR,然后将其部署到 JEE 应用服务器,如 JBoss、WebLogic、WebSphere 等。我们可能会将多个应用程序部署到同一个 JEE 容器中。在微服务方法中,每个微服务将被构建为一个 fat Jar,嵌入所有依赖项,并作为一个独立的 Java 进程运行。

微服务是自治的

微服务也可以获得自己的容器来执行,如前图所示。容器是可移植的、独立可管理的、轻量级的运行时环境。容器技术,如 Docker,是微服务部署的理想选择。

微服务的特点

本章前面讨论的微服务定义是任意的。倡导者和实践者对微服务有着强烈但有时不同的看法。对于微服务并没有一个单一、具体和普遍接受的定义。然而,所有成功的微服务实现都表现出一些共同的特征。因此,重要的是要理解这些特征,而不是坚持理论上的定义。本节详细介绍了一些共同的特征。

服务是一等公民

在微服务世界中,服务是一等公民。微服务将服务端点公开为 API,并抽象了所有实现细节。内部实现逻辑、架构和技术(包括编程语言、数据库、服务质量机制等)完全隐藏在服务 API 的后面。

此外,在微服务架构中,不再有应用程序开发;相反,组织专注于服务开发。在大多数企业中,这需要应用程序构建方式的重大文化转变。

客户资料微服务中,诸如数据结构、技术、业务逻辑等的内部细节被隐藏起来。它们不会暴露或对任何外部实体可见。访问是通过服务端点或 API 进行限制的。例如,客户资料微服务可以公开注册客户获取客户作为其他人与之交互的两个 API。

微服务中的服务特性

由于微服务在某种程度上类似于 SOA,因此在微服务中也适用于 SOA 中定义的许多服务特性。

以下是一些适用于微服务的服务特性:

  • 服务契约:与 SOA 类似,微服务通过明确定义的服务契约进行描述。在微服务世界中,JSON 和 REST 被普遍接受用于服务通信。在 JSON/REST 的情况下,有许多用于定义服务契约的技术。JSON Schema、WADL、Swagger 和 RAML 是一些例子。

  • 松耦合:微服务是独立的和松耦合的。在大多数情况下,微服务接受事件作为输入,并以另一个事件作为响应。消息传递、HTTP 和 REST 通常用于微服务之间的交互。基于消息的端点提供更高级别的解耦。

  • 服务抽象:在微服务中,服务抽象不仅是服务实现的抽象,还提供了所有库和环境细节的完全抽象,正如前面讨论的那样。

  • 服务重用:微服务是粗粒度可重用的业务服务。这些服务可以被移动设备和桌面渠道、其他微服务,甚至其他系统访问。

  • 无状态性:设计良好的微服务是无状态的,不共享任何共享状态或由服务维护的对话状态。如果需要维护状态,它们会在数据库中维护,可能是在内存中。

  • 服务可发现性:微服务是可发现的。在典型的微服务环境中,微服务会自我宣传其存在,并使自己可供发现。当服务终止时,它们会自动从微服务生态系统中退出。

  • 服务互操作性:服务是可互操作的,因为它们使用标准协议和消息交换标准。消息传递、HTTP 等被用作传输机制。在微服务世界中,REST/JSON 是开发可互操作服务最流行的方法。在需要进一步优化通信的情况下,可以使用其他协议,如 Protocol Buffers、Thrift、Avro 或 Zero MQ。然而,使用这些协议可能会限制服务的整体互操作性。

  • 服务可组合性:微服务是可组合的。服务可组合性是通过服务编排或服务编舞来实现的。

提示

有关 SOA 原则的更多细节可以在以下找到:

serviceorientation.com/serviceorientation/index

微服务是轻量级的

设计良好的微服务与单一业务能力对齐,因此它们只执行一个功能。因此,在大多数实现中,我们看到的共同特征是具有较小足迹的微服务。

在选择支持技术(如 Web 容器)时,我们必须确保它们也是轻量级的,以便整体占用空间保持可管理。例如,Jetty 或 Tomcat 作为微服务的应用容器,与 WebLogic 或 WebSphere 等更复杂的传统应用服务器相比更好。

与 VMWare 或 Hyper-V 等虚拟化技术相比,Docker 等容器技术还可以帮助我们尽可能地减少基础设施的占用空间。

微服务轻量级

如前图所示,微服务通常部署在 Docker 容器中,这些容器封装了业务逻辑和所需的库。这有助于我们在新机器上或完全不同的托管环境甚至不同的云提供商之间快速复制整个设置。由于没有物理基础设施依赖,容器化的微服务非常易于移植。

多语言架构的微服务

由于微服务是自治的,并且将一切都抽象在服务 API 后面,因此可能会有不同的微服务采用不同的架构。微服务实现中常见的一些特征包括:

  • 不同的服务使用相同技术的不同版本。一个微服务可能是基于 Java 1.7 编写的,另一个可能是基于 Java 1.8 的。

  • 不同的语言用于开发不同的微服务,例如一个微服务用 Java 开发,另一个用 Scala 开发。

  • 使用不同的架构,例如一个微服务使用 Redis 缓存来提供数据,而另一个微服务可能使用 MySQL 作为持久性数据存储。

多语言架构的微服务

在前面的例子中,由于酒店搜索预计具有高交易量和严格的性能要求,因此使用 Erlang 进行实现。为了支持预测搜索,使用 Elasticsearch 作为数据存储。同时,酒店预订需要更多的 ACID 事务特性。因此,它使用 MySQL 和 Java 进行实现。内部实现被隐藏在定义为 REST/JSON over HTTP 的服务端点后面。

微服务环境中的自动化

大多数微服务实现都是从开发到生产的最大程度自动化的。

由于微服务将单片应用程序分解为多个较小的服务,大型企业可能会看到微服务的激增。除非自动化到位,否则大量微服务很难管理。微服务的较小占用空间也有助于我们自动化微服务的开发到部署生命周期。总的来说,微服务是端到端自动化的,例如自动化构建、自动化测试、自动化部署和弹性扩展。

微服务环境中的自动化

如前图所示,在开发、测试、发布和部署阶段通常会应用自动化:

  • 开发阶段使用版本控制工具(如 Git)与持续集成CI)工具(如 Jenkins、Travis CI 等)进行自动化。这也可能包括代码质量检查和单元测试的自动化。微服务也可以实现每次代码提交时进行完整构建的自动化。

  • 测试阶段将使用 Selenium、Cucumber 和其他 AB 测试策略等测试工具进行自动化。由于微服务与业务能力对齐,因此相对于单片应用程序,自动化的测试用例数量较少,因此每次构建都可以进行回归测试。

  • 基础设施的配置是通过诸如 Docker 之类的容器技术进行的,再加上诸如 Chef 或 Puppet 之类的发布管理工具,以及诸如 Ansible 之类的配置管理工具。自动化部署使用诸如 Spring Cloud、Kubernetes、Mesos 和 Marathon 之类的工具进行处理。

具有支持生态系统的微服务

大多数大规模的微服务实现都有一个支持生态系统。生态系统的能力包括 DevOps 流程、集中式日志管理、服务注册表、API 网关、广泛的监控、服务路由和流量控制机制。

具有支持生态系统的微服务

当前图表中所代表的支持能力齐备时,微服务运行良好。

微服务是分布式和动态的

成功的微服务实现将逻辑和数据封装在服务内部。这导致了两种非常规的情况:分布式数据和逻辑以及分散的治理。

与将所有逻辑和数据整合到一个应用边界的传统应用程序相比,微服务将数据和逻辑分散化。每个服务都与特定的业务能力对齐,拥有自己的数据和逻辑。

微服务是分布式和动态的

前面图表中的虚线表示逻辑单片应用边界。当我们将其迁移到微服务时,每个微服务 A、B 和 C 都创建了自己的物理边界。

微服务通常不使用像 SOA 中那样的集中式治理机制。微服务实现的一个共同特点是它们不依赖于重量级的企业级产品,如企业服务总线(ESB)。相反,业务逻辑和智能被嵌入到服务本身中。

微服务是分布式和动态的

前面图表中显示了典型的 SOA 实现。购物逻辑完全在 ESB 中实现,通过编排由客户、订单和产品提供的不同服务。另一方面,在微服务方法中,购物本身将作为一个独立的微服务运行,以一种相当解耦的方式与客户、产品和订单进行交互。

SOA 实现严重依赖于静态注册表和存储库配置来管理服务和其他工件。微服务为此带来了更加动态的特性。因此,静态治理方法被视为在维护最新信息方面的负担。这就是为什么大多数微服务实现使用自动化机制从运行时拓扑动态构建注册表信息。

反脆弱性、快速失败和自愈

反脆弱性是 Netflix 成功实验的一种技术。这是现代软件开发中构建故障安全系统的最强大方法之一。

提示

反脆弱性概念是由纳西姆·尼古拉斯·塔勒布在他的书《反脆弱:从混乱中获益的事物》中引入的。

在反脆弱性实践中,软件系统不断受到挑战。软件系统通过这些挑战不断发展,并在一段时间内变得越来越擅长应对这些挑战。亚马逊的 GameDay 练习和 Netflix 的 Simian Army 是这种反脆弱性实验的很好例子。

快速失败是另一个用于构建容错、弹性系统的概念。这种理念主张期望系统出现故障,而不是构建永远不会出现故障的系统。重要的是系统能够多快地失败,以及如果失败了,它能够多快地从失败中恢复。采用这种方法,重点从“平均故障间隔时间”(MTBF)转移到“平均恢复时间”(MTTR)。这种方法的一个关键优势是,如果出现问题,系统会自行终止,并且下游功能不会受到压力。

自愈在微服务部署中通常被使用,系统会自动从失败中学习并进行调整。这些系统还可以防止未来的失败。

微服务示例

在实施微服务时,没有“一刀切”的方法。在本节中,将分析不同的示例来阐明微服务的概念。

假期门户网站示例

在第一个示例中,我们将回顾一个假期门户网站,Fly By Points。Fly By Points 收集了当客户通过在线网站预订酒店、航班或汽车时积累的积分。当客户登录 Fly By Points 网站时,他/她可以看到积累的积分、通过兑换积分可以获得的个性化优惠以及即将到来的旅行(如果有的话)。

假期门户网站示例

假设前一页是登录后的主页。Jeo有两次即将到来的旅行,四个个性化的优惠和 21,123 积分。当用户点击每个框时,将查询并显示详细信息。

假期门户网站采用了基于 Java Spring 的传统单体应用架构,如下所示:

假期门户网站示例

如前图所示,假期门户网站的架构是基于 Web 的模块化架构,各层之间有明确的分离。按照通常的做法,假期门户网站也部署为单个 WAR 文件,放在诸如 Tomcat 之类的 Web 服务器上。数据存储在一个全面的关系型数据库中。当业务增长、用户基数扩大并且复杂性增加时,交易量也会成比例增加。在这一点上,企业应该考虑将单体应用重新架构为微服务,以获得更快的交付速度、灵活性和可管理性。

假期门户网站示例

检查这个应用程序的简单微服务版本时,我们可以立即注意到架构中的一些事情:

  • 每个子系统现在都成为了一个独立的系统,即一个微服务。有三个微服务代表三个业务功能:TripsOffersPoints。每个微服务都有自己的内部数据存储和中间层。每个服务的内部结构保持不变。

  • 每个服务都封装了自己的数据库以及自己的 HTTP 监听器。与之前的模型相反,没有 Web 服务器或 WAR。相反,每个服务都有自己的嵌入式 HTTP 监听器,如 Jetty、Tomcat 等。

  • 每个微服务都会暴露一个 REST 服务来操作属于该服务的资源/实体。

假设呈现层是使用客户端 JavaScript MVC 框架(如 Angular JS)开发的。这些客户端框架能够直接调用 REST 调用。

当网页加载时,三个框,Trips、Offers 和 Points 将显示出来,包括积分、优惠数量和旅行次数等详细信息。每个框都会独立地通过 REST 对应的后端微服务进行异步调用。服务层之间没有依赖关系。当用户点击任何一个框时,屏幕将进行过渡并加载所点击项目的详细信息。这将通过对应微服务的另一个调用来完成。

基于微服务的订单管理系统

让我们再来看一个微服务示例:在线零售网站。在这种情况下,我们将更多地关注后端服务,比如处理订单事件的订单服务,当客户通过网站下订单时会生成订单事件:

基于微服务的订单管理系统

这个微服务系统完全基于反应式编程实践设计。

提示

在以下网址阅读有关响应式编程的更多信息:

www.reactivemanifesto.org

当事件发布时,许多微服务准备好在接收事件时启动。它们每一个都是独立的,不依赖于其他微服务。这种模型的优势在于我们可以不断添加或替换微服务以满足特定需求。

在上图中,显示了八个微服务。在订单事件到达时,发生以下活动:

  1. 订单服务在接收到订单事件时启动。订单服务创建订单并将详细信息保存到自己的数据库中。

  2. 如果订单成功保存,订单服务将创建并发布订单成功事件。

  3. 当订单成功事件到达时,一系列操作发生。

  4. 交付服务接受事件并放置交付记录以将订单交付给客户。这反过来生成交付事件并发布事件。

  5. 运输服务接收交付事件并处理。例如,运输服务创建运输计划。

  6. 客户通知服务向客户发送通知,通知客户订单已经下达。

  7. 库存缓存服务使用可用产品数量更新库存缓存。

  8. 库存重新订购服务检查库存限制是否足够,并在需要时生成补货事件。

  9. 客户积分服务根据此购买重新计算客户的忠诚积分。

  10. 客户账户服务更新客户账户中的订单历史记录。

在这种方法中,每个服务只负责一个功能。服务接受并生成事件。每个服务都是独立的,不知道自己的邻居。因此,邻居可以像蜂窝类比中提到的那样有机地增长。新服务可以根据需要添加。添加新服务不会影响任何现有服务。

旅行代理门户的示例

这第三个示例是一个简单的旅行代理门户应用程序。在这个例子中,我们将看到同步的 REST 调用以及异步事件。

在这种情况下,门户只是一个包含多个菜单项或链接的容器应用程序。当请求特定页面时,例如当单击菜单或链接时,它们将从特定的微服务加载。

旅行代理门户的示例

当客户请求预订时,内部发生以下事件:

  1. 旅行代理打开航班 UI,搜索航班,并为客户确定合适的航班。在幕后,航班 UI 是从航班微服务加载的。航班 UI 只与航班微服务内部的自己的后端 API 交互。在这种情况下,它通过 REST 调用航班微服务来加载要显示的航班。

  2. 然后,旅行代理通过访问客户 UI 查询客户详细信息。与航班 UI 类似,客户 UI 是从客户微服务加载的。客户 UI 中的操作将在客户微服务上调用 REST 调用。在这种情况下,通过调用客户微服务上的适当 API 加载客户详细信息。

  3. 然后,旅行代理检查客户的签证详细信息,以确定客户是否有资格前往所选国家。这也遵循前两点中提到的相同模式。

  4. 接下来,旅行代理使用预订微服务的预订 UI 进行预订,这再次遵循相同的模式。

  5. 支付页面是从支付微服务加载的。一般来说,支付服务有额外的约束条件,比如 PCIDSS 合规性(保护和加密数据在传输和静态数据)。微服务方法的优势在于,与单体应用相比,其他微服务不需要考虑 PCIDSS 的监管范围,而单体应用中整个应用都受 PCIDSS 规则的约束。支付也遵循前面描述的模式。

  6. 一旦预订提交,预订微服务调用航班服务来验证和更新航班预订。这种编排是作为预订微服务的一部分定义的。进行预订的智能也保存在预订微服务中。作为预订过程的一部分,它还验证、检索和更新客户微服务。

  7. 最后,预订微服务发送预订事件,通知服务接收并向客户发送通知。

这里的有趣因素是,我们可以在不影响其他微服务的情况下更改微服务的用户界面、逻辑和数据。

这是一种清晰而整洁的方法。可以通过组合不同微服务的不同屏幕来构建许多门户应用程序,特别是针对不同的用户群体。整体行为和导航将由门户应用程序控制。

除非页面是根据这种方法设计的,否则这种方法会面临许多挑战。请注意,网站布局和静态内容将由内容管理系统CMS)作为布局模板加载。或者,这些内容可以存储在 Web 服务器上。网站布局可能包含从微服务在运行时加载的 UI 片段。

微服务的好处

微服务相对于传统的多层、单片架构有许多优势。本节解释了微服务架构方法的一些关键优势。

支持多语言架构

有了微服务,架构师和开发人员可以为每个微服务选择合适的架构和技术。这样可以灵活地以更具成本效益的方式设计更合适的解决方案。

由于微服务是自治和独立的,每个服务可以使用自己的架构或技术,或者不同版本的技术运行。

以下是一个简单、实际的微服务多语言架构的示例。

支持多语言架构

有一个要求对所有系统交易进行审计,并记录诸如请求和响应数据、发起交易的用户、调用的服务等交易细节。

如前图所示,核心服务如订单和产品微服务使用关系型数据存储,而审计微服务将数据持久化在 Hadoop 文件系统(HDFS)中。在存储大数据量的情况下,关系型数据存储既不理想也不划算,比如审计数据。在单体架构中,应用通常使用一个共享的单一数据库来存储订单、产品和审计数据。

在这个例子中,审计服务是一个使用不同架构的技术微服务。同样,不同的功能服务也可以使用不同的架构。

在另一个例子中,可能有一个运行在 Java 7 上的预订微服务,而搜索微服务可能在 Java 8 上运行。同样,订单微服务可以用 Erlang 编写,而交付微服务可以使用 Go 语言。这些在单体架构中都是不可能的。

促进实验和创新

现代企业正在追求快速成功。微服务是企业实施颠覆性创新的关键推动因素之一,因为它们提供了实验和快速失败的能力。

由于服务相当简单且规模较小,企业可以承担尝试新的流程、算法、业务逻辑等。对于大型单片应用程序,实验并不容易;也不直接或成本效益高。企业必须花费巨资来构建或更改应用程序以尝试新的东西。使用微服务,可以编写一个小型微服务来实现目标功能,并以一种反应式的方式将其插入系统中。然后可以对新功能进行几个月的实验,如果新的微服务不如预期那样工作,我们可以更改或替换为另一个。与单片方法相比,变更成本将大大降低。

启用实验和创新

在另一个航空公司预订网站的示例中,航空公司希望在其预订页面上显示个性化的酒店推荐。这些推荐必须显示在预订确认页面上。

如前图所示,编写一个可以插入单片应用程序预订流程的微服务比在单片应用程序本身中包含此要求更方便。航空公司可以选择从一个简单的推荐服务开始,并不断用更新的版本替换,直到满足所需的准确性。

弹性和可选择性可扩展

由于微服务是较小的工作单元,它们使我们能够实现选择性的可伸缩性。

不同功能在应用程序中可能有不同的可伸缩性要求。作为单个 WAR 或 EAR 打包的单片应用程序只能作为一个整体进行扩展。当 I/O 密集型功能以高速数据流进行传输时,很容易降低整个应用程序的服务水平。

在微服务的情况下,每个服务都可以独立地进行横向或纵向扩展。由于可在每个服务上选择性地应用可伸缩性,因此与微服务方法相比,扩展的成本相对较低。

在实践中,有许多不同的方法可用于扩展应用程序,这在很大程度上取决于应用程序的架构和行为。Scale Cube主要定义了扩展应用程序的三种方法:

  • 通过水平克隆应用程序来扩展x

  • 通过分割不同的功能来扩展y

  • 通过分区或分片数据来扩展z

提示

在以下网站了解有关 Scale Cube 的更多信息:

theartofscalability.com/

当将y轴扩展应用于单片应用程序时,它将单片应用程序分解为与业务功能对齐的较小单元。许多组织成功地应用了这种技术,摆脱了单片应用程序。原则上,功能的结果单元符合微服务的特征。

例如,在典型的航空公司网站上,统计数据表明,航班搜索与航班预订的比例可能高达 500:1。这意味着每 500 次搜索交易就会有一次预订交易。在这种情况下,搜索需要比预订功能多 500 倍的可伸缩性。这是选择性扩展的理想用例。

弹性和可选择性可扩展

解决方案是将搜索请求和预订请求区分对待。在单片架构中,这只有在规模立方体的z扩展中才可能。然而,这种方法很昂贵,因为在z规模中,整个代码库都会被复制。

在前面的图表中,搜索和预订被设计为不同的微服务,以便搜索可以与预订不同比例地扩展。在图表中,搜索有三个实例,预订有两个实例。选择性的可伸缩性不仅限于实例的数量,如图表所示,还包括微服务的架构方式。在搜索的情况下,可以使用诸如 Hazelcast 之类的内存数据网格IMDG)作为数据存储。这将进一步提高搜索的性能和可伸缩性。当实例化一个新的搜索微服务实例时,将向 IMDG 集群添加一个额外的 IMDG 节点。预订不需要相同级别的可伸缩性。在预订的情况下,预订的两个实例都连接到同一个数据库实例。

允许替换

微服务是自包含的、独立的部署模块,使得可以用另一个类似的微服务替换一个微服务。

许多大型企业遵循购买与自建政策来实施软件系统。一个常见的情况是大部分功能在内部开发,而从外部专家购买某些特定的能力。这在传统的单片应用程序中存在挑战,因为这些应用程序组件高度内聚。试图将第三方解决方案插入单片应用程序会导致复杂的集成。而在微服务中,这不是事后想法。从架构上看,一个微服务可以很容易地被另一个内部开发的微服务或者来自第三方的微服务所替代。

允许替换

航空公司业务中的定价引擎是复杂的。不同航线的票价是使用复杂的数学公式计算的,称为定价逻辑。航空公司可以选择从市场上购买定价引擎,而不是自行开发产品。在单片架构中,定价是票价和预订的一个功能。在大多数情况下,定价、票价和预订都是硬编码的,几乎不可能分离。

在设计良好的微服务系统中,预订、票价和定价将是独立的微服务。替换定价微服务对其他任何服务的影响将最小,因为它们都是松散耦合和独立的。今天,它可能是第三方服务;明天,它可能很容易被另一个第三方或自行开发的服务所替代。

使有机系统建设成为可能

微服务帮助我们构建有机性质的系统。这在逐步将单片系统迁移到微服务时非常重要。

有机系统是指随着时间的推移,通过不断添加更多功能而横向增长的系统。在实践中,一个应用程序在其生命周期内增长得难以想象地庞大,而且在大多数情况下,应用程序的可管理性在同一时期内急剧减少。

微服务是关于独立可管理的服务。这使我们能够在需要时不断添加更多的服务,对现有服务的影响最小。构建这样的系统并不需要巨额资本投资。因此,企业可以将其作为运营支出的一部分不断建设。

多年前,一家航空公司建立了一个忠诚度系统,针对个人乘客。一切都很顺利,直到航空公司开始向其企业客户提供忠诚度福利。企业客户是以公司为单位分组的个人。由于当前系统的核心数据模型是扁平的,针对个人,企业环境需要对核心数据模型进行根本性的改变,因此需要大量的重塑,以满足这一要求。

使有机系统建设成为可能

如前图所示,在基于微服务的架构中,客户信息将由客户微服务管理,忠诚度将由忠诚度积分微服务管理。

在这种情况下,很容易添加一个新的企业客户微服务来管理企业客户。当一个公司注册时,个别成员将被推送到客户微服务中,以便像往常一样管理他们。企业客户微服务通过从客户微服务中聚合数据来提供企业视图。它还将提供支持企业特定业务规则的服务。采用这种方法,添加新服务对现有服务的影响将最小化。

帮助减少技术债务

由于微服务体积较小且依赖性较小,它们允许以最小成本迁移使用末期技术的服务。

技术变化是软件开发中的障碍之一。在许多传统的单片应用程序中,由于技术的快速变化,今天的下一代应用程序甚至在发布到生产之前就可能成为遗留系统。架构师和开发人员倾向于通过添加抽象层来保护技术变化。然而,实际上,这种方法并不能解决问题,反而导致了过度设计的系统。由于技术升级通常是风险和昂贵的,并且对业务没有直接回报,因此业务可能不愿意投资于减少应用程序的技术债务。

使用微服务,可以单独为每个服务更改或升级技术,而不是升级整个应用程序。

例如,将使用 EJB 1.1 和 Hibernate 编写的五百万行代码升级到 Spring、JPA 和 REST 服务几乎等同于重写整个应用程序。在微服务世界中,这可以逐步完成。

帮助减少技术债务

如前图所示,旧版本的服务在旧版本的技术上运行,而新服务开发可以利用最新的技术。与增强单片应用程序相比,使用末期技术迁移微服务的成本要低得多。

允许不同版本的共存

由于微服务将服务运行时环境与服务本身打包在一起,这使得同一环境中可以存在多个服务版本。

将出现需要同时运行同一服务的多个版本的情况。零停机推广,其中必须从一个版本平稳地切换到另一个版本,就是这样一个情况的例子,因为会有一个时间窗口,两个服务必须同时运行。对于单片应用程序来说,这是一个复杂的过程,因为在集群的一个节点中升级新服务是很麻烦的,例如,这可能导致类加载问题。金丝雀发布,其中新版本只发布给少数用户以验证新服务,是另一个需要多个服务版本共存的例子。

使用微服务,这两种情况都很容易管理。由于每个微服务都使用独立的环境,包括像 Tomcat 或 Jetty 嵌入式的服务监听器,因此可以发布多个版本并在没有太多问题的情况下进行平稳过渡。当消费者查找服务时,他们会寻找特定版本的服务。例如,在金丝雀发布中,新用户界面发布给用户 A。当用户 A 发送请求到微服务时,它会查找金丝雀发布版本,而所有其他用户将继续查找上一个生产版本。

在数据库层面需要注意确保数据库设计始终向后兼容,以避免破坏更改。

允许不同版本共存

如前图所示,客户服务的版本 1 和 2 可以共存,因为它们不会相互干扰,考虑到它们各自的部署环境。路由规则可以在网关处设置,以将流量转发到特定实例,如图所示。或者,客户端可以在请求本身中请求特定版本。在图中,网关根据请求的来源地选择版本。

支持自组织系统的构建

微服务帮助我们构建自组织系统。自组织系统支持将自动化部署,具有弹性,并展现自愈和自学习能力。

在良好架构的微服务系统中,服务不知道其他服务。它接受来自选定队列的消息并处理它。在处理结束时,它可能发送另一条消息,触发其他服务。这使我们可以将任何服务放入生态系统中,而无需分析对整个系统的影响。根据输入和输出,服务将自组织到生态系统中。不需要额外的代码更改或服务编排。没有中央大脑来控制和协调流程。

想象一下,现有的通知服务监听INPUT队列并将通知发送到SMTP服务器,如下图所示:

支持自组织系统的构建

假设以后需要引入一个个性化引擎,负责将消息的语言更改为客户的母语,以在发送给客户之前个性化消息,个性化引擎负责将消息的语言更改为客户的母语。

支持自组织系统的构建

使用微服务,将创建一个新的个性化微服务来执行此任务。输入队列将在外部配置服务器中配置为 INPUT,并且个性化服务将从 INPUT 队列中获取消息(之前由通知服务使用),并在完成处理后将消息发送到 OUTPUT 队列。通知服务的输入队列将然后发送到 OUTPUT。从下一刻起,系统将自动采用这种新的消息流。

支持事件驱动架构

微服务使我们能够开发透明的软件系统。传统系统通过本机协议相互通信,因此表现为黑匣子应用程序。业务事件和系统事件,除非明确发布,否则很难理解和分析。现代应用程序需要数据进行业务分析,以了解动态系统行为,并分析市场趋势,它们还需要响应实时事件。事件是数据提取的有用机制。

良好架构的微服务始终使用事件作为输入和输出。这些事件可以被任何服务利用。一旦提取,事件可以用于各种用例。

例如,业务希望实时查看按产品类型分类的订单速度。在单片系统中,我们需要考虑如何提取这些事件。这可能会对系统造成改变。

支持事件驱动架构

在微服务世界中,订单事件在订单创建时已经发布。这意味着只需要添加一个新的服务来订阅相同的主题,提取事件,执行请求的聚合,并推送另一个事件供仪表板消费。

启用 DevOps

微服务是 DevOps 的关键推动因素之一。DevOps 被广泛采用作为许多企业的实践,主要是为了提高交付速度和敏捷性。成功采用 DevOps 需要文化变革、流程变革以及架构变革。DevOps 主张具有敏捷开发、高速发布周期、自动化测试、自动化基础设施配置和自动化部署。

用传统的单片应用程序自动化所有这些过程是非常难以实现的。微服务并不是终极答案,但在许多 DevOps 实施中,微服务处于中心舞台。许多 DevOps 工具和技术也围绕着微服务的使用而不断发展。

考虑一个需要数小时才能完成完整构建并且需要 20 到 30 分钟才能启动应用程序的单片应用程序;可以看出这种应用程序不太适合 DevOps 自动化。很难在每次提交时自动化持续集成。由于大型的单片应用程序不太适合自动化,连续测试和部署也很难实现。

另一方面,小型微服务更易于自动化,并且因此更容易支持这些要求。

微服务还可以为开发提供更小、更专注的敏捷团队。团队将根据微服务的边界进行组织。

与其他架构风格的关系

现在我们已经看到了微服务的特点和好处,在本节中,我们将探讨微服务与其他密切相关的架构风格(如 SOA 和十二要素应用)的关系。

与 SOA 的关系

SOA 和微服务遵循类似的概念。在本章的前面,我们讨论了微服务是从 SOA 发展而来的,并且许多服务特点在这两种方法中都是共通的。

然而,它们是相同的还是不同的?

由于微服务是从 SOA 发展而来的,许多微服务的特点与 SOA 相似。让我们首先来看一下 SOA 的定义。

The Open Group联盟对 SOA 的定义如下:

“面向服务的架构(SOA)是一种支持服务定位的架构风格。服务定位是一种以服务和基于服务的开发思维方式以及服务的结果。

一个服务:

是一个可重复的业务活动的逻辑表示,具有指定的结果(例如,检查客户信用、提供天气数据、整合钻井报告)

它是自包含的。

它可能由其他服务组成。

对于服务的消费者来说,这是一个“黑匣子”。

我们也在微服务中观察到了类似的方面。那么,微服务有什么不同之处呢?答案是:这取决于情况。

对于上一个问题的答案可能是肯定的,也可能是否定的,这取决于组织及其对 SOA 的采用。SOA 是一个更广泛的术语,不同的组织以不同的方式来解决不同的组织问题。微服务和 SOA 之间的区别在于组织如何对待 SOA。

为了明确,将研究一些案例。

面向服务的集成

面向服务的集成是指许多组织使用的基于服务的集成方法。

面向服务的集成

许多组织主要使用 SOA 来解决其集成复杂性,也被称为集成意大利面。一般来说,这被称为面向服务的集成SOI)。在这种情况下,应用程序通过一个通用的集成层使用标准协议和消息格式进行通信,例如基于 SOAP/XML 的 Web 服务通过 HTTP 或 JMS。这些类型的组织专注于企业集成模式EIP)来建模其集成需求。这种方法严重依赖于重量级的 ESB,如 TIBCO Business Works、WebSphere ESB、Oracle ESB 等。大多数 ESB 供应商还打包了一套相关产品,如规则引擎、业务流程管理引擎等,作为 SOA 套件。这些组织的集成深深扎根于他们的产品中。他们要么在 ESB 层中编写繁重的编排逻辑,要么在服务总线中编写业务逻辑本身。在这两种情况下,所有企业服务都通过 ESB 部署和访问。这些服务通过企业治理模型进行管理。对于这样的组织,微服务与 SOA 完全不同。

遗留系统现代化

SOA 也用于在遗留应用程序之上构建服务层。

遗留系统现代化

另一类组织将在转型项目或遗留现代化项目中使用 SOA。在这种情况下,服务是在 ESB 层构建和部署的,通过 ESB 适配器连接到后端系统。对于这些组织来说,微服务与 SOA 是不同的。

面向服务的应用程序

一些组织在应用程序级别采用 SOA。

面向服务的应用程序

在这种方法中,轻量级的集成框架,如 Apache Camel 或 Spring Integration,被嵌入到应用程序中,用于处理与服务相关的横切能力,如协议转换、并行执行、编排和服务集成。由于一些轻量级集成框架具有本地 Java 对象支持,这样的应用程序甚至会使用本地普通旧 Java 对象POJO)服务进行集成和服务之间的数据交换。因此,所有服务都必须打包为一个单体 Web 存档。这样的组织可能会将微服务视为其 SOA 的下一个逻辑步骤。

使用 SOA 进行单体迁移

使用 SOA 进行单体迁移

最后一种可能性是在单体系统达到瓶颈后,将单体应用程序转换为更小的单元。他们会将应用程序分解为更小的、可以物理部署的子系统,类似于前面解释的y轴扩展方法,并将它们部署为 Web 服务器上的 Web 存档或部署为一些自制容器上的 JAR 文件。这些作为服务的子系统将使用 Web 服务或其他轻量级协议在服务之间交换数据。他们还将使用 SOA 和服务设计原则来实现这一点。对于这样的组织,他们可能倾向于认为微服务只是新瓶装旧酒。

与十二要素应用的关系

云计算是一种快速发展的技术之一。云计算承诺了许多好处,如成本优势、速度、灵活性和弹性。有许多云提供商提供不同的服务。他们降低了成本模型,使其对企业更具吸引力。不同的云提供商,如 AWS、微软、Rackspace、IBM、谷歌等,使用不同的工具、技术和服务。另一方面,企业意识到这个不断发展的战场,因此,他们正在寻找从锁定到单一供应商的风险降低的选择。

许多组织将它们的应用程序迁移到云中。在这种情况下,应用程序可能无法实现云平台所承诺的所有好处。有些应用程序需要进行彻底改造,而有些可能只需要在移动到云之前进行小的调整。这在很大程度上取决于应用程序的架构和开发方式。

例如,如果应用程序的生产数据库服务器 URL 被硬编码为应用程序 WAR 的一部分,那么在将应用程序移动到云之前,需要对其进行修改。在云中,基础设施对应用程序是透明的,特别是物理 IP 地址不能被假定。

我们如何确保应用程序,甚至微服务,可以在多个云提供商之间无缝运行,并利用云服务的优势,比如弹性?

在开发云原生应用程序时,遵循一定的原则是很重要的。

提示

云原生是指开发能够在云环境中高效工作的应用程序的术语,理解和利用云行为,比如弹性、基于利用率的计费、故障感知等。

由 Heroku 提出的 Twelve-Factor App 是一种描述现代云就绪应用程序所期望具备的特征的方法论。Twelve-Factor App 同样适用于微服务。因此,理解 Twelve-Factor App 是很重要的。

单一的代码库

代码库原则建议每个应用程序都有一个单一的代码库。可以部署多个相同代码库的实例,比如开发、测试和生产。代码通常在诸如 Git、Subversion 等源代码控制系统中进行管理。

单一的代码库

将相同的理念应用于微服务,每个微服务都应该有自己的代码库,并且这个代码库不与任何其他微服务共享。这也意味着一个微服务有且仅有一个代码库。

捆绑依赖

根据这一原则,所有应用程序应该将它们的依赖项与应用程序捆绑在一起。借助 Maven 和 Gradle 等构建工具,我们可以在pom.xml.gradle文件中明确管理依赖项,并使用诸如 Nexus 或 Archiva 之类的中央构建存储库将它们链接起来。这确保了版本的正确管理。最终的可执行文件将被打包为 WAR 文件或可执行的 JAR 文件,嵌入所有的依赖项。

捆绑依赖

在微服务的背景下,这是一个必须遵循的基本原则。每个微服务应该将所有所需的依赖项和执行库(如 HTTP 监听器等)捆绑在最终的可执行包中。

外部化配置

这一原则建议从代码中外部化所有配置参数。应用程序的配置参数在不同的环境中会有所不同,比如对外部系统的电子邮件 ID 或 URL 的支持,用户名、密码、队列名称等。这些在开发、测试和生产环境中都会有所不同。所有服务配置都应该被外部化。

外部化配置

同样的原则对于微服务也是显而易见的。微服务的配置参数应该从外部源加载。这也将有助于自动化发布和部署过程,因为这些环境之间唯一的区别是配置参数。

后端服务是可寻址的

所有后端服务都应该通过可寻址的 URL 访问。所有服务在其执行周期中需要与一些外部资源进行通信。例如,它们可能会监听或发送消息到消息系统,发送电子邮件,将数据持久化到数据库等等。所有这些服务都应该通过 URL 可达,而不需要复杂的通信要求。

后备服务是可寻址的

在微服务世界中,微服务要么通过消息系统发送或接收消息,要么可以接受或发送消息到其他服务 API。在常规情况下,这些要么是使用 REST 和 JSON 的 HTTP 端点,要么是基于 TCP 或 HTTP 的消息端点。

构建、发布和运行之间的隔离

这一原则主张在构建、发布和运行阶段之间进行强大的隔离。构建阶段指的是通过包含所有所需资产来编译和生成二进制文件。发布阶段指的是将二进制文件与特定于环境的配置参数相结合。运行阶段指的是在特定的执行环境中运行应用程序。流水线是单向的,因此不可能将运行阶段的更改传播回构建阶段。基本上,这也意味着不建议为生产进行特定的构建;而是必须通过流水线进行。

构建、发布和运行之间的隔离

在微服务中,构建将创建可执行的 JAR 文件,包括诸如 HTTP 监听器之类的服务运行时。在发布阶段,这些可执行文件将与发布配置(如生产 URL 等)相结合,创建一个发布版本,很可能是类似 Docker 的容器。在运行阶段,这些容器将通过容器调度程序部署到生产环境。

无状态,共享无事务处理

这一原则建议进程应该是无状态的并且不共享任何东西。如果应用程序是无状态的,那么它就是容错的,并且可以很容易地扩展。

所有微服务都应设计为无状态函数。如果有存储状态的要求,应该使用后备数据库或内存缓存来完成。

通过端口绑定公开服务

预期十二要素应用程序是自包含的。传统上,应用程序部署到服务器上:Web 服务器或应用服务器,如 Apache Tomcat 或 JBoss。十二要素应用程序不依赖外部 Web 服务器。HTTP 监听器,如 Tomcat 或 Jetty,必须嵌入到服务本身中。

端口绑定是微服务能够自主和自包含的基本要求之一。微服务将服务监听器嵌入到服务本身作为其一部分。

并发性以扩展规模

这一原则规定进程应该被设计为通过复制进程来扩展。这是在进程内使用线程之外的另一种方法。

在微服务世界中,服务被设计为扩展而不是扩大。x轴扩展技术主要用于通过启动另一个相同的服务实例来扩展服务。根据流量流动,服务可以弹性地扩展或收缩。此外,微服务可能利用并行处理和并发框架来进一步加快或扩展事务处理。

具有最小开销的可处置性

这一原则主张以最小的启动和关闭时间以及优雅的关闭支持构建应用程序。在自动化部署环境中,我们应该能够尽快启动或关闭实例。如果应用程序的启动或关闭需要相当长的时间,将对自动化产生不利影响。启动时间与应用程序的大小成正比。在针对自动扩展的云环境中,我们应该能够快速启动新实例。这也适用于推广新版本的服务。

在微服务上下文中,为了实现完全自动化,将应用程序的大小保持尽可能小,启动和关闭时间尽可能短是非常重要的。微服务还应考虑对象和数据的延迟加载。

开发和生产的对等性

这个原则强调了尽可能保持开发和生产环境的相同重要性。例如,让我们考虑一个具有多个服务或进程的应用程序,比如作业调度服务、缓存服务和一个或多个应用程序服务。在开发环境中,我们倾向于在一台机器上运行它们,而在生产环境中,我们将为每个进程提供独立的机器来运行。这主要是为了管理基础设施的成本。缺点是,如果生产环境出现问题,就没有相同的环境来重新生成和修复问题。

这个原则不仅适用于微服务,也适用于任何应用程序开发。

外部化日志

十二要素应用程序永远不会尝试存储或传输日志文件。在云中,最好避免本地 I/O。如果在特定基础设施中 I/O 速度不够快,可能会造成瓶颈。解决这个问题的方法是使用集中式日志框架。Splunk、Greylog、Logstash、Logplex 和 Loggly 是一些日志传输和分析工具的例子。推荐的方法是通过连接 logback appenders 将日志传输到一个中央存储库,并写入其中一个传输点。

在微服务生态系统中,这一点非常重要,因为我们正在将一个系统分解成许多较小的服务,这可能导致日志的去中心化。如果它们将日志存储在本地存储中,将极其难以在服务之间进行日志相关性的对比。

外部化日志

在开发中,微服务可以将日志流重定向到stdout,而在生产中,这些流将被日志传输器捕获,并发送到中央日志服务进行存储和分析。

打包管理进程

除了应用程序服务,大多数应用程序还提供管理任务。这个原则建议对应用程序服务和管理任务使用相同的发布包以及相同的环境。管理代码也应该与应用程序代码一起打包。

这个原则不仅适用于微服务,也适用于任何应用程序开发。

微服务用例

微服务并不是万能药,也不会解决当今世界的所有架构挑战。关于何时使用微服务并没有硬性规定或严格的指导方针。

微服务可能并不适用于每种用例。微服务的成功很大程度上取决于用例的选择。首要活动是对用例进行酸碱试验,以检验其是否符合微服务的好处。酸碱试验必须涵盖我们在本章前面讨论过的所有微服务的好处。对于给定的用例,如果没有可量化的好处,或者成本超过了好处,那么该用例可能不适合微服务。

让我们讨论一些常用的适合微服务架构的场景:

  • 由于需要改进可伸缩性、可管理性、灵活性或交付速度,迁移单片应用程序。另一个类似的情况是重写一个即将到期且被广泛使用的遗留应用程序。在这两种情况下,微服务都提供了一个机会。使用微服务架构,可以通过逐步将功能转换为微服务来重新平台化遗留应用程序。这种方法有好处。不需要巨额的前期投资,不会对业务造成重大干扰,也没有严重的业务风险。由于服务依赖关系已知,可以很好地管理微服务的依赖关系。

  • 诸如集成优化服务、预测服务、价格计算服务、预测服务、报价服务、推荐服务等的实用计算场景都是微服务的良好候选者。这些是独立的无状态计算单元,接受特定数据,应用算法,并返回结果。独立的技术服务,如通信服务、加密服务、认证服务等也是微服务的良好候选者。

  • 在许多情况下,我们可以构建无头业务应用程序或具有自主性质的服务,例如支付服务、登录服务、航班搜索服务、客户档案服务、通知服务等等。这些通常在多个渠道中被重复使用,因此很适合构建为微服务。

  • 可能存在微型或宏型应用程序,用于单一目的并执行单一职责。一个简单的时间跟踪应用程序就是这一类的例子。它所做的就是捕获时间、持续时间和执行的任务。常用的企业应用程序也是微服务的候选者。

  • 良好架构、响应式客户端 MVC web 应用程序的后端服务(后端即服务BaaS)场景)根据用户导航需求加载数据。在大多数情况下,数据可能来自于多个逻辑上不同的数据源,就像之前提到的 飞越点 示例一样。

  • 高度敏捷的应用程序、需要快速交付或上市时间、创新试点、选择进行 DevOps 的应用程序、创新型系统的应用程序等等也可以被视为微服务架构的潜在候选者。

  • 我们可以预期从微服务中获益的应用程序,例如多语言要求、需要 命令查询职责分离CQRS)等等,也是微服务架构的潜在候选者。

如果使用案例属于这些类别之一,它就是微服务架构的潜在候选者。

有一些情况下,我们应该考虑避免使用微服务:

  • 如果组织的政策被迫使用集中管理的重量级组件,例如 ESB 来托管业务逻辑,或者如果组织有任何其他阻碍微服务基本原则的政策,那么微服务就不是正确的解决方案,除非组织流程得到放松。

  • 如果组织的文化、流程等等是基于传统的瀑布交付模型、漫长的发布周期、矩阵团队、手动部署和繁琐的发布流程、没有基础设施供应等等,那么微服务可能不适合。这是康威定律的基础。这一定律指出组织结构与其创建的软件之间存在着强烈的联系。

提示

阅读更多关于康威定律的信息:

www.melconway.com/Home/Conways_Law.html

微服务的早期采用者

许多组织已经成功地踏上了微服务世界的旅程。在本节中,我们将研究一些微服务领域的先驱者,分析他们为什么这样做以及他们是如何做到的。最后我们将进行一些分析以得出一些结论:

  • Netflix(www.netflix.com):Netflix 是一家国际点播媒体流媒体公司,在微服务领域是先驱。Netflix 将大量开发传统单片代码的开发人员转变为生产微服务的较小开发团队。这些微服务共同工作,向数百万 Netflix 客户流媒体数字媒体。在 Netflix,工程师们从单片开始,经历了痛苦,然后将应用程序分解为松散耦合且与业务能力对齐的较小单元。

  • Uber(www.uber.com):Uber 是一家国际运输网络公司,于 2008 年开始使用单片架构和单一代码库。所有服务都嵌入到单片应用程序中。当 Uber 将业务从一个城市扩展到多个城市时,挑战开始了。Uber 随后通过将系统分解为较小的独立单元,转向基于 SOA 的架构。每个模块都交给不同的团队,并授权他们选择自己的语言、框架和数据库。Uber 在其生态系统中部署了许多使用 RPC 和 REST 的微服务。

  • Airbnb(www.airbnb.com):Airbnb 是提供可信住宿市场的世界领先公司,开始使用一个执行业务所需功能的单片应用程序。随着流量增加,Airbnb 面临可扩展性问题。单一代码库变得过于复杂,导致关注点分离不良,并出现性能问题。Airbnb 将其单片应用程序分解为在单独机器上运行的具有单独部署周期的独立代码库的较小部分。Airbnb 围绕这些服务开发了自己的微服务或 SOA 生态系统。

  • Orbitz(www.orbitz.com):Orbitz 是一个在线旅行门户,在 2000 年代开始使用单片架构,有 Web 层、业务层和数据库层。随着 Orbitz 业务的扩展,他们面临了单片分层架构的可管理性和可扩展性问题。Orbitz 随后经历了持续的架构变化。后来,Orbitz 将其单片应用程序分解为许多较小的应用程序。

  • eBay(www.ebay.com):eBay 是最大的在线零售商之一,于上世纪 90 年代末开始使用单片 Perl 应用程序和 FreeBSD 作为数据库。随着业务的增长,eBay 经历了扩展问题。它一直在投资改进其架构。在 2000 年代中期,eBay 转向基于 Java 和 Web 服务的较小分解系统。他们采用了数据库分区和功能分离以满足所需的可扩展性。

  • 亚马逊(www.amazon.com):亚马逊是最大的在线零售商之一,于 2001 年运行了一个基于 C++的大型单片应用程序。这个良好架构的单片应用程序基于分层架构,有许多模块化组件。然而,所有这些组件都紧密耦合。因此,亚马逊无法通过将团队分成较小的组来加快其开发周期。亚马逊随后将代码分离为独立的功能服务,用 Web 服务封装,并最终发展为微服务。

  • Gilt(www.gilt.com):Gilt 是一个在线购物网站,于 2007 年开始使用分层单片 Rails 应用程序和后端的 Postgres 数据库。与许多其他应用程序类似,随着流量增加,Web 应用程序无法提供所需的弹性。Gilt 通过引入 Java 和多语言持久性进行了架构改造。后来,Gilt 转向使用微服务概念的许多较小的应用程序。

  • Twitterwww.twitter.com):Twitter,最大的社交网站之一,于 2000 年代中期开始使用三层单片的 rails 应用程序。后来,当 Twitter 的用户基数增长时,他们经历了一次架构重构周期。通过这次重构,Twitter 从典型的 Web 应用程序转向了基于 API 的事件驱动核心。Twitter 使用 Scala 和 Java 开发具有多语言持久性的微服务。

  • 耐克www.nike.com):耐克,全球服装和鞋类领导者,将他们的单片应用程序转变为微服务。与许多其他组织类似,耐克也是运行在古老的遗留应用程序上,几乎不稳定。在他们的旅程中,耐克转向了重量级商业产品,目标是稳定遗留应用程序,但最终变成了昂贵的单片应用程序,难以扩展,发布周期长,并且需要太多手动工作来部署和管理应用程序。后来,耐克转向了基于微服务的架构,大大缩短了开发周期。

共同主题是单片迁移

当我们分析前述企业时,有一个共同的主题。所有这些企业都是从单片应用程序开始,并通过应用他们以前版本的学习和痛点,转变为微服务架构。

即使在今天,许多初创公司也是从单体开始,因为这样更容易开始、概念化,然后在需求出现时慢慢转向微服务。单片到微服务的迁移场景有一个额外的优势:它们有所有的信息提前准备好,可以随时进行重构。

然而,对于所有这些企业来说,单片转变的催化剂是不同的。一些共同的动机是缺乏可伸缩性、长时间的开发周期、流程自动化、可管理性以及业务模式的变化。

虽然单片迁移是毫无疑问的,但也有机会从头开始构建微服务。与其构建从头开始的系统,不如寻找为业务快速赢得的机会,例如为航空公司的端到端货物管理系统添加卡车服务,或者为零售商的忠诚度系统添加客户评分服务。这些可以作为独立的微服务实现,并与它们各自的单片应用程序交换消息。

另一个观点是,许多组织仅将微服务用于业务关键的客户参与应用程序,而将其余的遗留单片应用程序采取自己的轨迹。

另一个重要观察是,先前检查的大多数组织在微服务旅程中处于不同的成熟水平。当 eBay 在 2000 年代初从单片应用程序过渡时,他们将应用程序在功能上分割成更小、独立、可部署的单元。这些逻辑上划分的单元被封装在 Web 服务中。虽然单一责任和自治性是它们的基本原则,但这些架构受限于当时可用的技术和工具。像 Netflix 和 Airbnb 这样的组织构建了自己的能力来解决他们面临的具体挑战。总之,所有这些都不是真正的微服务,而是遵循相同特征的小型、与业务对齐的服务。

没有所谓的“确定或最终的微服务”状态。这是一个不断发展和成熟的过程。架构师和开发人员的口头禅是可替代性原则;建立一种最大限度地提高替换其部分的能力并最小化替换成本的架构。最重要的是,企业不应该只是追随炒作来开发微服务。

总结

在本章中,您通过一些例子了解了微服务的基础知识。

我们探讨了微服务从传统的单片应用程序的演变。我们研究了一些现代应用架构所需的原则和思维转变。我们还看了一下微服务的特点和好处以及使用案例。在本章中,我们建立了微服务与面向服务的架构和十二要素应用的关系。最后,我们分析了来自不同行业的一些企业的例子。

在下一章中,我们将开发一些示例微服务,以更清晰地了解本章的内容。

第二章:使用 Spring Boot 构建微服务

开发微服务不再那么乏味,这要归功于强大的 Spring Boot 框架。Spring Boot 是一个用于开发 Java 生产就绪微服务的框架。

本章将从上一章中解释的微服务理论转移到实际操作,通过审查代码示例来介绍 Spring Boot 框架,并解释 Spring Boot 如何帮助构建符合上一章讨论的原则和特征的 RESTful 微服务。最后,将回顾 Spring Boot 提供的一些功能,使微服务达到生产就绪状态。

在本章结束时,您将学到:

  • 设置最新的 Spring 开发环境

  • 使用 Spring 框架开发 RESTful 服务

  • 使用 Spring Boot 构建完全合格的微服务

  • 使用 Spring Boot 构建生产就绪的微服务的有用功能

建立开发环境

为了明确微服务的概念,将构建一对微服务。为此,假定已安装以下组件:

也可以使用其他 IDE,如 IntelliJ IDEA、NetBeans 或 Eclipse。同样,也可以使用其他构建工具,如 Gradle。假设 Maven 仓库、类路径和其他路径变量已正确设置以运行 STS 和 Maven 项目。

本章基于以下版本的 Spring 库:

  • Spring 框架4.2.6.RELEASE

  • Spring Boot1.3.5.RELEASE

提示

下载代码包的详细步骤在本书的前言中有提到。看一看。

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Spring-Microservices。我们还有其他丰富的书籍和视频代码包可供使用,网址为github.com/PacktPublishing/。去看看吧!

开发 RESTful 服务-传统方法

在深入研究 Spring Boot 之前,本示例将回顾传统的 RESTful 服务开发。

STS 将用于开发此 REST/JSON 服务。

注意

此示例的完整源代码可在本书的代码文件中的legacyrest项目中找到。

以下是开发第一个 RESTful 服务的步骤:

  1. 启动 STS 并为该项目设置一个工作区。

  2. 导航到File | New | Project

  3. 选择Spring Legacy Project,如下截图所示,然后点击Next开发 RESTful 服务-传统方法

  4. 选择Spring MVC Project,如下图所示,然后点击Next开发 RESTful 服务-传统方法

  5. 选择一个顶级包名称。本示例使用org.rvslab.chapter2.legacyrest作为顶级包。

  6. 然后,点击Finish

  7. 这将在 STS 工作区中创建一个名为legacyrest的项目。

在继续之前,需要编辑pom.xml

  1. 将 Spring 版本更改为4.2.6.RELEASE,如下所示:
<org.springframework-version>4.2.6.RELEASE</org.springframework-version>
  1. pom.xml文件中添加Jackson依赖项,用于 JSON 到 POJO 和 POJO 到 JSON 的转换。请注意,使用2.*.*版本以确保与 Spring 4 的兼容性。
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.6.4</version>
</dependency>
  1. 需要添加一些 Java 代码。在Java Resources下的legacyrest中,展开包并打开默认的HomeController.java文件:开发 RESTful 服务-传统方法

  2. 默认实现更加面向 MVC 项目。重写HomeController.java以响应 REST 调用返回 JSON 值将会奏效。生成的HomeController.java文件将类似于以下内容:

@RestController
public class HomeController {
  @RequestMapping("/")
  public Greet sayHello(){
    return new Greet("Hello World!");
  }
}
class Greet { 
  private String message;
  public Greet(String message) {
    this.message = message;
  }
  //add getter and setter
}

检查代码,现在有两个类:

  • Greet:这是一个简单的 Java 类,具有用于表示数据对象的 getter 和 setter。Greet类中只有一个属性,即message

  • HomeController.java:这只是一个 Spring 控制器 REST 端点,用于处理 HTTP 请求。

请注意,在HomeController中使用的注释是@RestController,它会自动注入@Controller@ResponseBody,并具有与以下代码相同的效果:

@Controller
@ResponseBody
public class HomeController { }
  1. 项目现在可以通过右键单击legacyrest,导航到Run As | Run On Server,然后选择默认服务器(Pivotal tc Server Developer Edition v3.1)来运行。

这应该会自动启动服务器并在 TC 服务器上部署 Web 应用程序。

如果服务器正常启动,控制台将显示以下消息:

INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'appServlet': initialization completed in 906 ms
May 08, 2016 8:22:48 PM org.apache.catalina.startup.Catalina start
INFO: Server startup in 2289 ms

  1. 如果一切正常,STS 将打开一个浏览器窗口到http://localhost:8080/legacyrest/,并在浏览器中显示 JSON 对象。右键单击并导航到legacyrest | Properties | Web Project Settings,并查看Context Root以识别 Web 应用程序的上下文根:开发 RESTful 服务-传统方法

另一个构建选项是使用 Maven。右键单击项目,导航到Run As | Maven install。这将在目标文件夹下生成chapter2-1.0.0-BUILD-SNAPSHOT.war。这个 war 文件可以部署在任何 Servlet 容器中,如 Tomcat、JBoss 等。

从传统的 Web 应用程序转向微服务

仔细检查前面的 RESTful 服务将会揭示这是否真的构成了微服务。乍一看,前面的 RESTful 服务是一个完全合格的可互操作的 REST/JSON 服务。然而,它在本质上并不是完全自治的。这主要是因为该服务依赖于底层的应用服务器或 Web 容器。在前面的例子中,一个 war 文件被明确创建并部署在 Tomcat 服务器上。

这是一种传统的开发 RESTful 服务的方法,作为 Web 应用程序。然而,从微服务的角度来看,人们需要一种机制来开发可执行的服务,即带有嵌入式 HTTP 监听器的自包含 JAR 文件。

Spring Boot 是一个工具,可以方便地开发这种类型的服务。Dropwizard 和 WildFly Swarm 是替代的无服务器 RESTful 堆栈。

使用 Spring Boot 构建 RESTful 微服务

Spring Boot 是 Spring 团队的一个实用框架,可以快速轻松地启动基于 Spring 的应用程序和微服务。该框架在决策制定方面采用了一种有见地的方法,从而减少了编写大量样板代码和配置所需的工作量。使用 80-20 原则,开发人员应该能够使用许多默认值快速启动各种 Spring 应用程序。Spring Boot 进一步为开发人员提供了定制应用程序的机会,通过覆盖自动配置的值。

Spring Boot 不仅提高了开发速度,还提供了一套生产就绪的运维功能,如健康检查和指标收集。由于 Spring Boot 掩盖了许多配置参数并抽象了许多底层实现,它在一定程度上减少了错误的机会。Spring Boot 根据类路径中可用的库识别应用程序的性质,并运行打包在这些库中的自动配置类。

许多开发人员错误地将 Spring Boot 视为代码生成器,但实际上并非如此。Spring Boot 只自动配置构建文件,例如 Maven 的 POM 文件。它还根据某些默认值设置属性,例如数据源属性。看一下以下代码:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <scope>runtime</scope>
</dependency>

例如,在前面的案例中,Spring Boot 知道项目设置为使用 Spring Data JPA 和 HSQL 数据库。它会自动配置驱动程序类和其他连接参数。

Spring Boot 的一个伟大成果之一是几乎消除了传统的 XML 配置的需求。Spring Boot 还通过将所有所需的运行时依赖项打包到一个大的可执行 JAR 文件中来实现微服务的开发。

开始使用 Spring Boot

Spring Boot 基于应用程序开发的不同方式有很多:

  • 使用 Spring Boot CLI 作为命令行工具

  • 使用 STS 等 IDE 提供 Spring Boot,这些都是默认支持的

  • 使用 Spring Initializr 项目在start.spring.io

本章将探讨这三种选项,开发各种示例服务。

使用 CLI 开发 Spring Boot 微服务

开发和演示 Spring Boot 功能的最简单方法是使用 Spring Boot CLI,一个命令行工具。执行以下步骤:

  1. 通过从repo.spring.io/release/org/springframework/boot/spring-boot-cli/1.3.5.RELEASE/spring-boot-cli-1.3.5.RELEASE-bin.zip下载spring-boot-cli-1.3.5.RELEASE-bin.zip文件来安装 Spring Boot 命令行工具。

  2. 将文件解压缩到您选择的目录中。打开终端窗口,并将终端提示更改为bin文件夹。

确保将bin文件夹添加到系统路径中,以便可以从任何位置运行 Spring Boot。

  1. 使用以下命令验证安装。如果成功,Spring CLI 版本将打印在控制台上:
$spring –-version
Spring CLI v1.3.5.RELEASE

  1. 作为下一步,将在 Groovy 中开发一个快速的 REST 服务,Spring Boot 默认支持。为此,使用任何编辑器复制并粘贴以下代码,并将其保存为myfirstapp.groovy在任何文件夹中:
@RestController
class HelloworldController {
    @RequestMapping("/")
    String sayHello() {
        "Hello World!"
    }
}
  1. 要运行这个 Groovy 应用程序,转到保存myfirstapp.groovy的文件夹,并执行以下命令。服务器启动日志的最后几行将类似于以下内容:
$spring run myfirstapp.groovy 

2016-05-09 18:13:55.351  INFO 35861 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-05-09 18:13:55.375  INFO 35861 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 24 ms

  1. 打开浏览器窗口,转到http://localhost:8080;浏览器将显示以下消息:

你好,世界!

没有创建 war 文件,也没有运行 Tomcat 服务器。Spring Boot 自动选择了 Tomcat 作为 Web 服务器,并将其嵌入到应用程序中。这是一个非常基本的、最小的微服务。在前面的代码中使用的@RestController注解将在下一个示例中进行详细讨论。

使用 STS 开发 Spring Boot Java 微服务

在本节中,将演示使用 STS 开发另一个基于 Java 的 REST/JSON Spring Boot 服务。

注意

本示例的完整源代码可作为本书的代码文件中的chapter2.bootrest项目获得。

  1. 打开 STS,在Project Explorer窗口中右键单击,导航到New | Project,然后选择Spring Starter Project,如下图所示,并单击Next使用 STS 开发 Spring Boot Java 微服务

Spring Starter Project 是一个基本模板向导,提供了许多其他启动库供选择。

  1. 将项目名称命名为chapter2.bootrest或者您选择的其他名称。选择打包方式为 JAR 非常重要。在传统的 web 应用中,会创建 war 文件然后部署到 servlet 容器,而 Spring Boot 会将所有依赖项打包到一个独立的、自包含的 JAR 文件中,并带有嵌入式 HTTP 监听器。

  2. Java 版本下选择 1.8。建议 Spring 4 应用程序使用 Java 1.8。更改其他 Maven 属性,如GroupArtifactPackage,如下图所示:使用 STS 开发 Spring Boot Java 微服务

  3. 完成后,点击下一步

  4. 向导将显示库选项。在这种情况下,由于正在开发 REST 服务,选择Web下的Web。这是一个有趣的步骤,告诉 Spring Boot 正在开发一个 Spring MVC web 应用程序,以便 Spring Boot 可以包含必要的库,包括 Tomcat 作为 HTTP 监听器和其他所需的配置:使用 STS 开发 Spring Boot Java 微服务

  5. 点击完成

这将在 STS 的项目资源管理器中生成一个名为chapter2.bootrest的项目:

使用 STS 开发 Spring Boot Java 微服务

  1. 花一点时间检查生成的应用程序。感兴趣的文件包括:
  • pom.xml

  • Application.java

  • Application.properties

  • ApplicationTests.java

检查 POM 文件

父元素是pom.xml文件中的一个有趣的方面。看一下以下内容:

<parent>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.4.RELEASE</version>
</parent>

spring-boot-starter-parent模式是 Maven 依赖管理使用的材料清单BOM)模式。BOM 是一种特殊的 POM 文件,用于管理项目所需的不同库版本。使用spring-boot-starter-parent POM 文件的优势在于开发人员无需担心找到不同库的兼容版本,比如 Spring、Jersey、JUnit、Logback、Hibernate、Jackson 等等。例如,在我们的第一个传统示例中,需要添加一个特定版本的 Jackson 库来与 Spring 4 一起使用。在这个示例中,这些都由spring-boot-starter-parent模式处理。

starter POM 文件具有一系列 Boot 依赖项、合理的资源过滤和 Maven 构建所需的合理插件配置。

提示

参考github.com/spring-projects/spring-boot/blob/1.3.x/spring-boot-dependencies/pom.xml查看 starter parent(版本 1.3.x)中提供的不同依赖项。如果需要,所有这些依赖项都可以被覆盖。

starter POM 文件本身不会向项目添加 JAR 依赖项,而是只会添加库版本。随后,当依赖项添加到 POM 文件中时,它们会引用该 POM 文件中的库版本。以下是一些属性的快照:

<spring-boot.version>1.3.5.BUILD-SNAPSHOT</spring-boot.version>
<hibernate.version>4.3.11.Final</hibernate.version>
<jackson.version>2.6.6</jackson.version>
<jersey.version>2.22.2</jersey.version>
<logback.version>1.1.7</logback.version>
<spring.version>4.2.6.RELEASE</spring.version>
<spring-data-releasetrain.version>Gosling-SR4</spring-data-releasetrain.version>
<tomcat.version>8.0.33</tomcat.version>

审查依赖部分,可以看到这是一个干净整洁的 POM 文件,只有两个依赖,如下所示:

<dependencies>
   <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

   <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
   </dependency>
</dependencies>

选择 web 后,spring-boot-starter-web会添加 Spring MVC 项目所需的所有依赖项。它还包括对 Tomcat 的依赖,作为嵌入式 HTTP 监听器。这提供了一种有效的方式来获取所有所需的依赖项作为一个单独的捆绑包。可以用其他库替换单个依赖项,例如用 Jetty 替换 Tomcat。

与 web 类似,Spring Boot 提供了许多spring-boot-starter-*库,比如amqpaopbatchdata-jpathymeleaf等等。

pom.xml文件中最后需要审查的是 Java 8 属性。默认情况下,父 POM 文件会添加 Java 6。建议将 Java 版本覆盖为 8 用于 Spring:

<java.version>1.8</java.version>

检查 Application.java

Spring Boot 默认在src/main/java下生成了一个org.rvslab.chapter2.Application.java类来引导,如下所示:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Application中只有一个main方法,按照 Java 约定,在启动时将被调用。main方法通过在SpringApplication上调用run方法来引导 Spring Boot 应用程序。将Application.class作为参数传递,告诉 Spring Boot 这是主要组件。

更重要的是,这是由@SpringBootApplication注解完成的。@SpringBootApplication注解是一个顶级注解,封装了另外三个注解,如下面的代码片段所示:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {

@Configuration注解提示包含的类声明一个或多个@Bean定义。@Configuration注解是元注解@Component的提示;因此,它是组件扫描的候选对象。

@EnableAutoConfiguration注解告诉 Spring Boot 根据类路径中可用的依赖项自动配置 Spring 应用程序。

检查 application.properties

默认的application.properties文件放置在src/main/resources下。这是一个重要的文件,用于配置 Spring Boot 应用程序的任何必需属性。目前,这个文件是空的,将在本章的后面一些测试用例中重新访问。

检查 ApplicationTests.java

要检查的最后一个文件是src/test/java下的ApplicationTests.java。这是一个占位符,用于针对 Spring Boot 应用程序编写测试用例。

要实现第一个 RESTful 服务,添加一个 REST 端点,如下所示:

  1. 可以编辑src/main/java下的Application.java,并添加一个 RESTful 服务实现。RESTful 服务与之前的项目中所做的完全相同。在Application.java文件的末尾添加以下代码:
@RestController
class GreetingController{
  @RequestMapping("/")
  Greet greet(){
    return new Greet("Hello World!");
  }
}
class Greet {
  private String message;
public Greet() {}

  public Greet(String message) {
    this.message = message;
  }
//add getter and setter
}
  1. 要运行,导航到Run As | Spring Boot App。Tomcat 将在8080端口上启动:检查 ApplicationTests.java

我们可以从日志中注意到:

  • Spring Boot 获得了自己的进程 ID(在本例中为41130

  • Spring Boot 会自动在本地主机的 Tomcat 服务器上启动,端口为8080

  1. 接下来,打开浏览器,指向http://localhost:8080。这将显示 JSON 响应,如下面的截图所示:检查 ApplicationTests.java

传统服务和这个服务之间的一个关键区别是,Spring Boot 服务是自包含的。为了更清楚地说明这一点,可以在 STS 之外运行 Spring Boot 应用程序。打开一个终端窗口,转到项目文件夹,并运行 Maven,如下所示:

$ maven install

这将在项目的目标文件夹下生成一个 fat JAR 文件。从命令行运行应用程序会显示:

$java -jar target/bootrest-0.0.1-SNAPSHOT.jar

正如大家所看到的,bootrest-0.0.1-SNAPSHOT.jar是自包含的,可以作为独立的应用程序运行。在这一点上,JAR 文件只有 13MB。尽管应用程序不过是一个简单的“Hello World”,但刚刚开发的 Spring Boot 服务实际上遵循了微服务的原则。

测试 Spring Boot 微服务

有多种方法可以测试 REST/JSON Spring Boot 微服务。最简单的方法是使用 Web 浏览器或指向 URL 的 curl 命令,如下所示:

curl http://localhost:8080

有许多工具可用于测试 RESTful 服务,例如 Postman、Advanced REST client、SOAP UI、Paw 等。

在这个例子中,为了测试服务,将使用 Spring Boot 生成的默认测试类。

ApplicatonTests.java中添加一个新的测试用例会导致:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTests {
  @Test
  public void testVanillaService() {
    RestTemplate restTemplate = new RestTemplate();
    Greet greet = restTemplate.getForObject("http://localhost:8080", Greet.class);
    Assert.assertEquals("Hello World!", greet.getMessage());
  }
}

请注意,在类级别添加了@WebIntegrationTest,并删除了@WebAppConfiguration@WebIntegrationTest注解是一个方便的注解,可以确保测试针对一个完全运行的服务器。或者,@WebAppConfiguration@IntegrationTest的组合将产生相同的结果。

还要注意,RestTemplate用于调用 RESTful 服务。RestTemplate是一个实用程序类,它抽象了 HTTP 客户端的底层细节。

要测试这一点,可以打开一个终端窗口,转到项目文件夹,并运行mvn install

使用 Spring Initializr 开发 Spring Boot 微服务–HATEOAS 示例

在下一个示例中,将使用 Spring Initializr 创建一个 Spring Boot 项目。Spring Initializr 是 STS 项目向导的一个可插拔替代品,并提供了一个 Web UI 来配置和生成 Spring Boot 项目。Spring Initializr 的一个优点是它可以通过网站生成一个项目,然后可以导入到任何 IDE 中。

在本示例中,将研究基于 REST 的服务的HATEOAS应用程序状态的超文本作为引擎)概念和HAL超文本应用语言)浏览器。

HATEOAS 是一种 REST 服务模式,其中导航链接作为有效负载元数据的一部分提供。客户端应用程序确定状态并遵循作为状态的一部分提供的过渡 URL。这种方法在响应式移动和 Web 应用程序中特别有用,其中客户端根据用户导航模式下载附加数据。

HAL 浏览器是一个方便的 API 浏览器,用于浏览hal+json数据。HAL 是一种基于 JSON 的格式,它建立了表示资源之间超链接的约定。HAL 有助于使 API 更具可探索性和可发现性。

注意

此示例的完整源代码可在本书的代码文件中的chapter2.boothateoas项目中找到。

以下是使用 Spring Initilizr 开发 HATEOAS 示例的具体步骤:

  1. 要使用 Spring Initilizr,转到start.spring.io使用 Spring Initializr 开发 Spring Boot 微服务–HATEOAS 示例

  2. 填写详细信息,例如是否为 Maven 项目,Spring Boot 版本,组和 artifact ID,如前所示,并点击切换到完整版本链接下的生成项目按钮。选择WebHATEOASRest Repositories HAL Browser。确保 Java 版本为 8,并且包类型选择为JAR使用 Spring Initializr 开发 Spring Boot 微服务–HATEOAS 示例

  3. 选择后,点击生成项目按钮。这将生成一个 Maven 项目,并将项目下载为 ZIP 文件到浏览器的下载目录中。

  4. 解压文件并将其保存到您选择的目录中。

  5. 打开 STS,转到文件菜单,点击导入使用 Spring Initializr 开发 Spring Boot 微服务–HATEOAS 示例

  6. 转到Maven | 现有 Maven 项目,然后点击下一步

  7. 点击根目录旁边的浏览,选择解压的文件夹。点击完成。这将把生成的 Maven 项目加载到 STS 的项目资源管理器中。

  8. 编辑Application.java文件,添加一个新的 REST 端点,如下所示:

@RequestMapping("/greeting")
@ResponseBody
public HttpEntity<Greet> greeting(@RequestParam(value = "name", required = false, defaultValue = "HATEOAS") String name) {
       Greet greet = new Greet("Hello " + name);
       greet.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel());

       return new ResponseEntity<Greet>(greet, HttpStatus.OK);
}
  1. 请注意,这与上一个示例中的GreetingController类相同。但是,这次添加了一个名为greeting的方法。在这个新方法中,定义了一个额外的可选请求参数,并将其默认为HATEOAS。以下代码将链接添加到生成的 JSON 代码中。在这种情况下,它将链接添加到相同的 API 中:
greet.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel());

为了做到这一点,我们需要将Greet类从ResourceSupport扩展,如下所示。其余的代码保持不变:

class Greet extends ResourceSupport{
  1. add方法是ResourceSupport中的一个方法。linkTomethodOn方法是ControllerLinkBuilder的静态方法,用于在控制器类上创建链接的实用程序类。methodOn方法将执行一个虚拟方法调用,而linkTo将创建一个指向控制器类的链接。在这种情况下,我们将使用withSelfRel将其指向自身。

  2. 这将基本上生成一个默认的链接/greeting?name=HATEOAS。客户端可以读取链接并发起另一个调用。

  3. 将其作为 Spring Boot 应用程序运行。一旦服务器启动完成,将浏览器指向http://localhost:8080

  4. 这将打开 HAL 浏览器窗口。在Explorer字段中,输入/greeting?name=World!并单击Go按钮。如果一切正常,HAL 浏览器将显示响应详细信息,如下面的屏幕截图所示:使用 Spring Initializr 开发 Spring Boot 微服务 - HATEOAS 示例

如屏幕截图所示,响应主体部分显示了一个带有href指向同一服务的链接。这是因为我们将引用指向自身。还要查看链接部分。self旁边的小绿色框是可导航链接。

在这个简单的例子中并没有太多意义,但在有许多相关实体的大型应用程序中可能会很方便。使用提供的链接,客户端可以轻松地在这些实体之间来回导航。

接下来是什么?

到目前为止,已经审查了许多基本的 Spring Boot 示例。本章的其余部分将从微服务开发的角度考虑一些重要的 Spring Boot 功能。在接下来的几节中,我们将看看如何处理动态可配置属性,更改默认的嵌入式 Web 服务器,为微服务添加安全性,并在处理微服务时实现跨源行为。

注意

此示例的完整源代码可作为本书的代码文件中的chapter2.boot-advanced项目获得。

Spring Boot 配置

在本节中,重点将放在 Spring Boot 的配置方面。已经开发的chapter2.bootrest项目将在本节中进行修改,以展示配置功能。复制并粘贴chapter2.bootrest并将项目重命名为chapter2.boot-advanced

了解 Spring Boot 自动配置

Spring Boot 使用约定优于配置,通过扫描类路径中可用的依赖库。对于 POM 文件中的每个spring-boot-starter-*依赖项,Spring Boot 执行默认的AutoConfiguration类。AutoConfiguration类使用*AutoConfiguration词法模式,其中*表示库。例如,JPA 存储库的自动配置是通过JpaRepositoriesAutoConfiguration完成的。

使用--debug运行应用程序以查看自动配置报告。以下命令显示了chapter2.boot-advanced项目的自动配置报告:

$java -jar target/bootadvanced-0.0.1-SNAPSHOT.jar --debug

以下是一些自动配置类的示例:

  • ServerPropertiesAutoConfiguration

  • RepositoryRestMvcAutoConfiguration

  • JpaRepositoriesAutoConfiguration

  • JmsAutoConfiguration

如果应用程序有特殊要求,并且您想完全控制配置,可以排除某些库的自动配置。以下是一个排除DataSourceAutoConfiguration的示例:

@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})

覆盖默认配置值

还可以使用application.properties文件覆盖默认配置值。STS 提供了一个易于自动完成的上下文帮助application.properties,如下面的屏幕截图所示:

覆盖默认配置值

在前面的屏幕截图中,server.port被编辑为设置为9090。再次运行此应用程序将在端口9090上启动服务器。

更改配置文件的位置

为了与十二要素应用程序保持一致,配置参数需要从代码中外部化。Spring Boot 将所有配置外部化到application.properties中。然而,它仍然是应用程序构建的一部分。此外,可以通过设置以下属性从包外部读取属性:

spring.config.name= # config file name  
spring.config.location= # location of config file

在这里,spring.config.location可以是本地文件位置。

以下命令使用外部提供的配置文件启动 Spring Boot 应用程序:

$java -jar target/bootadvanced-0.0.1-SNAPSHOT.jar --spring.config.name=bootrest.properties

读取自定义属性

在启动时,SpringApplication加载所有属性并将它们添加到 Spring Environment类中。在application.properties文件中添加自定义属性。在这种情况下,自定义属性的名称为bootrest.customproperty。将 Spring Environment类自动装配到GreetingController类中。编辑GreetingController类以从Environment中读取自定义属性,并添加日志语句以将自定义属性打印到控制台。

执行以下步骤来完成此操作:

  1. application.properties文件中添加以下属性:
bootrest.customproperty=hello
  1. 然后,编辑GreetingController类如下:
@Autowired
Environment env;

Greet greet(){
    logger.info("bootrest.customproperty "+ env.getProperty("bootrest.customproperty"));
    return new Greet("Hello World!");
}
  1. 重新运行应用程序。日志语句将在控制台中打印自定义变量,如下所示:
org.rvslab.chapter2.GreetingController   : bootrest.customproperty hello

使用.yaml 文件进行配置

作为application.properties的替代,可以使用.yaml文件。与平面属性文件相比,YAML 提供了类似 JSON 的结构化配置。

要查看此操作,请简单地将application.properties替换为application.yaml并添加以下属性:

server
  port: 9080

重新运行应用程序以查看端口在控制台中打印。

使用多个配置文件。

此外,可以有不同的配置文件,如开发、测试、暂存、生产等。这些是逻辑名称。使用这些,可以为不同的环境配置相同属性的不同值。当在不同环境中运行 Spring Boot 应用程序时,这非常方便。在这种情况下,从一个环境切换到另一个环境时不需要重新构建。

更新.yaml文件如下。Spring Boot 根据点分隔符对配置文件进行分组:

spring:
    profiles: development
server:
      port: 9090
---

spring:
    profiles: production
server:
      port: 8080

按照以下方式运行 Spring Boot 应用程序以查看配置文件的使用:

mvn -Dspring.profiles.active=production install
mvn -Dspring.profiles.active=development install

可以使用@ActiveProfiles注解以编程方式指定活动配置文件,这在运行测试用例时特别有用,如下所示:

@ActiveProfiles("test")

读取属性的其他选项

可以以多种方式加载属性,例如以下方式:

  • 命令行参数(-Dhost.port =9090)

  • 操作系统环境变量

  • JNDI (java:comp/env)

更改默认的嵌入式 Web 服务器

嵌入式 HTTP 监听器可以轻松自定义如下。默认情况下,Spring Boot 支持 Tomcat、Jetty 和 Undertow。在以下示例中,Tomcat 被替换为 Undertow:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

实现 Spring Boot 安全

保护微服务非常重要。在本节中,将审查一些保护 Spring Boot 微服务的基本措施,使用chapter2.bootrest来演示安全功能。

使用基本安全保护微服务

向 Spring Boot 添加基本身份验证非常简单。将以下依赖项添加到pom.xml中。这将包括必要的 Spring 安全库文件:

<dependency>
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

打开Application.java并在Application类中添加@EnableGlobalMethodSecurity。此注解将启用方法级安全性:

@EnableGlobalMethodSecurity
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

默认的基本身份验证假定用户为user。默认密码将在启动时打印在控制台上。或者,可以在application.properties中添加用户名和密码,如下所示:

security.user.name=guest
security.user.password=guest123

ApplicationTests中添加一个新的测试用例,测试安全服务的结果,如下所示:

  @Test
  public void testSecureService() {  
    String plainCreds = "guest:guest123";
    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization", "Basic " + new String(Base64.encode(plainCreds.getBytes())));
    HttpEntity<String> request = new HttpEntity<String>(headers);
    RestTemplate restTemplate = new RestTemplate();

    ResponseEntity<Greet> response = restTemplate.exchange("http://localhost:8080", HttpMethod.GET, request, Greet.class);
    Assert.assertEquals("Hello World!", response.getBody().getMessage());
  }

如代码所示,创建一个新的Authorization请求头,使用 Base64 编码用户名密码字符串。

使用 Maven 重新运行应用程序。请注意,新的测试用例通过了,但旧的测试用例出现了异常。早期的测试用例现在在没有凭据的情况下运行,结果服务器拒绝了请求,并显示以下消息:

org.springframework.web.client.HttpClientErrorException: 401 Unauthorized

使用 OAuth2 保护微服务

在本节中,我们将看一下 OAuth2 的基本 Spring Boot 配置。当客户端应用程序需要访问受保护的资源时,客户端向授权服务器发送请求。授权服务器验证请求并提供访问令牌。这个访问令牌对每个客户端到服务器的请求进行验证。来回发送的请求和响应取决于授权类型。

提示

oauth.net阅读有关 OAuth 和授权类型的更多信息。

在这个示例中将使用资源所有者密码凭据授权方法:

使用 OAuth2 保护微服务

在这种情况下,如前图所示,资源所有者提供客户端用户名和密码。然后客户端通过提供凭据信息向授权服务器发送令牌请求。授权服务器授权客户端并返回访问令牌。在每个后续请求中,服务器验证客户端令牌。

要在我们的示例中实现 OAuth2,请执行以下步骤:

  1. 首先,按照以下步骤更新pom.xml以添加 OAuth2 依赖:
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
  <version>2.0.9.RELEASE</version>
</dependency>
  1. 接下来,在Application.java文件中添加两个新的注释@EnableAuthorizationServer@EnableResourceServer@EnableAuthorizationServer注释创建一个授权服务器,其中包含一个内存存储库,用于存储客户端令牌并为客户端提供用户名、密码、客户端 ID 和密钥。@EnableResourceServer注释用于访问令牌。这将启用一个通过传入的 OAuth2 令牌进行身份验证的 Spring 安全过滤器。

在我们的示例中,授权服务器和资源服务器是相同的。然而,在实践中,这两者将分开运行。看一下以下代码:

@EnableResourceServer
@EnableAuthorizationServer
@SpringBootApplication
public class Application {
  1. 将以下属性添加到application.properties文件中:
security.user.name=guest
security.user.password=guest123
security.oauth2.client.clientId: trustedclient
security.oauth2.client.clientSecret: trustedclient123
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid
  1. 然后,添加另一个测试用例来测试 OAuth2,如下所示:
  @Test
  public void testOAuthService() {
        ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
        resource.setUsername("guest");
        resource.setPassword("guest123");
          resource.setAccessTokenUri("http://localhost:8080/oauth/token");
        resource.setClientId("trustedclient");
        resource.setClientSecret("trustedclient123");
        resource.setGrantType("password");

        DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();
        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resource, clientContext);

        Greet greet = restTemplate.getForObject("http://localhost:8080", Greet.class);

        Assert.assertEquals("Hello World!", greet.getMessage());
  }

如前面的代码所示,通过传递封装在资源详细信息对象中的资源详细信息来创建一个特殊的 REST 模板OAuth2RestTemplate。这个 REST 模板在 OAuth2 过程中处理访问令牌。访问令牌 URI 是令牌访问的端点。

  1. 使用mvn install重新运行应用程序。前两个测试用例将失败,而新的测试用例将成功。这是因为服务器只接受启用了 OAuth2 的请求。

这些是 Spring Boot 开箱即用提供的快速配置,但不足以达到生产级别。我们可能需要定制ResourceServerConfigurerAuthorizationServerConfigurer使其达到生产就绪。尽管如此,方法仍然是一样的。

为微服务启用跨源访问

当来自一个来源的客户端端网页应用程序从另一个来源请求数据时,通常会受到限制。启用跨源访问通常被称为CORS跨源资源共享)。

为微服务启用跨源访问

这个示例显示了如何启用跨源请求。对于微服务来说,由于每个服务都有自己的来源,很容易出现客户端端网页应用程序从多个来源消费数据的问题。例如,浏览器客户端访问来自 Customer 微服务的 Customer 和来自 Order 微服务的 Order History 的情况在微服务世界中非常常见。

Spring Boot 提供了一种简单的声明性方法来启用跨源请求。以下示例显示了如何启用微服务以启用跨源请求:

@RestController
class GreetingController{
  @CrossOrigin
  @RequestMapping("/")
  Greet greet(){
    return new Greet("Hello World!");
  }
}

默认情况下,所有的来源和标头都被接受。我们可以通过给予特定来源访问的方式进一步定制跨源注释。@CrossOrigin注释使方法或类能够接受跨源请求:

@CrossOrigin("http://mytrustedorigin.com")

可以使用WebMvcConfigurer bean 并定制addCorsMappings(CorsRegistry registry)方法来启用全局 CORS。

实现 Spring Boot 消息传递

在理想情况下,所有微服务之间的交互都应该使用发布-订阅语义进行异步处理。Spring Boot 提供了一种无忧的机制来配置消息传递解决方案:

实现 Spring Boot 消息传递

在这个例子中,我们将创建一个带有发送者和接收者的 Spring Boot 应用程序,它们都通过一个外部队列连接。执行以下步骤:

注意

这个例子的完整源代码可以在本书的代码文件中的chapter2.bootmessaging项目中找到。

  1. 使用 STS 创建一个新项目来演示这个功能。在这个例子中,不要选择Web,而是在I/O下选择AMQP实现 Spring Boot 消息传递

  2. 这个例子也需要 Rabbit MQ。从www.rabbitmq.com/download.html下载并安装最新版本的 Rabbit MQ。

本书中使用的是 Rabbit MQ 3.5.6。

  1. 按照网站上记录的安装步骤进行操作。准备就绪后,通过以下命令启动 RabbitMQ 服务器:
$./rabbitmq-server

  1. application.properties文件进行配置更改,以反映 RabbitMQ 的配置。以下配置使用 RabbitMQ 的默认端口、用户名和密码:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
  1. src/main/java目录下的Application.java文件中添加一个消息发送组件和一个org.springframework.amqp.core.Queue类型的名为TestQ的队列。RabbitMessagingTemplate是发送消息的一种便捷方式,它将抽象出所有的消息语义。Spring Boot 提供了所有的样板配置来发送消息:
@Component 
class Sender {
  @Autowired
  RabbitMessagingTemplate template;
  @Bean
  Queue queue() {
    return new Queue("TestQ", false);
  }
  public void send(String message){
    template.convertAndSend("TestQ", message);
  }
}
  1. 要接收消息,只需要使用@RabbitListener注解。Spring Boot 会自动配置所有必需的样板配置:
@Component
class Receiver {
    @RabbitListener(queues = "TestQ")
    public void processMessage(String content) {
       System.out.println(content);
    }
}
  1. 这个练习的最后一部分是将发送者连接到我们的主应用程序,并实现CommandLineRunnerrun方法来启动消息发送。当应用程序初始化时,它会调用CommandLineRunnerrun方法,如下所示:
@SpringBootApplication
public class Application implements CommandLineRunner{

  @Autowired
  Sender sender;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
      sender.send("Hello Messaging..!!!");
    }
}
  1. 将应用程序作为 Spring Boot 应用程序运行并验证输出。以下消息将打印在控制台上:
Hello Messaging..!!!

开发全面的微服务示例

到目前为止,我们考虑的例子不过是一个简单的“Hello world”。结合我们所学到的知识,本节演示了一个端到端的 Customer Profile 微服务实现。Customer Profile 微服务将展示不同微服务之间的交互。它还演示了具有业务逻辑和基本数据存储的微服务。

在这个例子中,将开发两个微服务,Customer Profile 和 Customer Notification 服务:

开发全面的微服务示例

如图所示,Customer Profile 微服务公开了用于创建、读取、更新和删除(CRUD)客户以及用于注册客户的注册服务的方法。注册过程应用了某些业务逻辑,保存了客户资料,并向 Customer Notification 微服务发送了一条消息。Customer Notification 微服务接受了注册服务发送的消息,并使用 SMTP 服务器向客户发送了一封电子邮件。异步消息传递用于将 Customer Profile 与 Customer Notification 服务集成起来。

Customer 微服务类的领域模型图如下所示:

开发全面的微服务示例

CustomerController在图中是 REST 端点,调用一个组件类CustomerComponent。组件类/bean 处理所有业务逻辑。CustomerRepository是一个 Spring data JPA repository,用于处理Customer实体的持久化。

注意

此示例的完整源代码可作为本书代码文件中的chapter2.bootcustomerchapter2.bootcustomernotification项目获得。

  1. 创建一个新的 Spring Boot 项目,并将其命名为chapter2.bootcustomer,与之前的方式相同。在启动模块选择屏幕中选择如下屏幕截图中的选项:开发全面的微服务示例

这将创建一个带有 JPA、REST 存储库和 H2 作为数据库的 Web 项目。H2 是一个微型的内存嵌入式数据库,可以轻松演示数据库功能。在现实世界中,建议使用适当的企业级数据库。此示例使用 JPA 定义持久性实体和 REST 存储库来公开基于 REST 的存储库服务。

项目结构将类似于以下屏幕截图:

开发全面的微服务示例

  1. 通过添加名为Customer的实体类来开始构建应用程序。为简单起见,Customer实体类只添加了三个字段:自动生成的id字段,nameemail。看一下以下代码:
@Entity
class Customer {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  private String name;
  private String email;
  1. 添加一个存储库类来处理客户的持久化处理。CustomerRepository扩展了标准的 JPA 存储库。这意味着所有 CRUD 方法和默认查找方法都由 Spring Data JPA 存储库自动实现,如下所示:
@RepositoryRestResource
interface CustomerRespository extends JpaRepository <Customer,Long>{
  Optional<Customer> findByName(@Param("name") String name);
}

在这个示例中,我们向存储库类添加了一个新的方法findByName,它基本上根据客户名称搜索客户,并在有匹配名称时返回Customer对象。

  1. @RepositoryRestResource注解通过 RESTful 服务启用存储库访问。这也将默认启用 HATEOAS 和 HAL。由于 CRUD 方法不需要额外的业务逻辑,我们将其保留为没有控制器或组件类的状态。使用 HATEOAS 将帮助我们轻松地浏览客户存储库方法。

请注意,没有在任何地方添加配置来指向任何数据库。由于 H2 库在类路径中,所有配置都是由 Spring Boot 根据 H2 自动配置默认完成的。

  1. 通过添加CommandLineRunner来初始化存储库并插入一些客户记录,更新Application.java文件,如下所示:
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
  CommandLineRunner init(CustomerRespository repo) {
  return (evt) ->  {
    repo.save(new Customer("Adam","adam@boot.com"));
    repo.save(new Customer("John","john@boot.com"));
  repo.save(new Customer("Smith","smith@boot.com"));
    repo.save(new Customer("Edgar","edgar@boot.com"));
    repo.save(new Customer("Martin","martin@boot.com"));
    repo.save(new Customer("Tom","tom@boot.com"));
    repo.save(new Customer("Sean","sean@boot.com"));
  };
  }
}
  1. CommandLineRunner被定义为一个 bean,表示当它包含在SpringApplication中时应该运行。这将在启动时向数据库插入六个样本客户记录。

  2. 此时,将应用程序作为 Spring Boot 应用程序运行。打开 HAL 浏览器,并将浏览器指向http://localhost:8080

  3. 资源管理器部分,指向http://localhost:8080/customers,然后点击Go。这将在 HAL 浏览器的响应主体部分列出所有客户。

  4. 资源管理器部分,输入http://localhost:8080/customers?size=2&page=1&sort=name,然后点击Go。这将自动在存储库上执行分页和排序,并返回结果。

由于页面大小设置为2,并且请求了第一页,它将以排序顺序返回两条记录。

  1. 查看链接部分。如下屏幕截图所示,它将方便地导航firstnextprevlast。这些是通过存储库浏览器自动生成的 HATEOAS 链接完成的:开发全面的微服务示例

  2. 还可以通过选择适当的链接,如http://localhost:8080/customers/2,来探索客户的详细信息。

  3. 作为下一步,添加一个控制器类CustomerController来处理服务端点。在这个类中只有一个端点/register,用于注册客户。如果成功,它将返回Customer对象作为响应,如下所示:

@RestController
class CustomerController{

  @Autowired
  CustomerRegistrar customerRegistrar;

  @RequestMapping( path="/register", method = RequestMethod.POST)
  Customer register(@RequestBody Customer customer){
    return customerRegistrar.register(customer);
  }
}
  1. 添加了一个CustomerRegistrar组件来处理业务逻辑。在这种情况下,组件中只添加了最少的业务逻辑。在这个组件类中,注册客户时,我们只会检查数据库中是否已经存在客户名称。如果不存在,我们将插入一个新记录,否则,我们将发送一个错误消息,如下所示:
@Component 
class CustomerRegistrar {

  CustomerRespository customerRespository;

  @Autowired
  CustomerRegistrar(CustomerRespository customerRespository){
    this.customerRespository = customerRespository;
  }

  Customer register(Customer customer){
    Optional<Customer> existingCustomer = customerRespository.findByName(customer.getName());
    if (existingCustomer.isPresent()){
      throw new RuntimeException("is already exists");
    } else {
      customerRespository.save(customer); 
    }
    return customer;
  }
}
  1. 重新启动 Boot 应用程序,并通过 URL http://localhost:8080 使用 HAL 浏览器进行测试。

  2. Explorer字段指向http://localhost:8080/customers。在Links部分查看结果:开发全面的微服务示例

  3. 点击self旁边的NON-GET选项。这将打开一个表单来创建一个新的客户:开发全面的微服务示例

  4. 填写表格,并按照图中所示更改操作。点击发出请求按钮。这将调用注册服务并注册客户。尝试给出重复的名称以测试负面情况。

  5. 让我们通过将客户通知服务集成到通知客户的示例的最后部分来完成示例。当注册成功时,通过异步调用客户通知微服务向客户发送电子邮件。

  6. 首先更新CustomerRegistrar以调用第二个服务。这是通过消息传递完成的。在这种情况下,我们注入了一个Sender组件,通过将客户的电子邮件地址传递给发送者,向客户发送通知,如下所示:

@Component 
@Lazy
class CustomerRegistrar {

  CustomerRespository customerRespository;
  Sender sender;

  @Autowired
  CustomerRegistrar(CustomerRespository customerRespository, Sender sender){
    this.customerRespository = customerRespository;
    this.sender = sender;
  }

  Customer register(Customer customer){
    Optional<Customer> existingCustomer = customerRespository.findByName(customer.getName());
    if (existingCustomer.isPresent()){
      throw new RuntimeException("is already exists");
    } else {
      customerRespository.save(customer); 
      sender.send(customer.getEmail());
    } 
    return customer;
  }
}
  1. 发送者组件将基于 RabbitMQ 和 AMQP。在本例中,RabbitMessagingTemplate被用作上一个消息示例中所探讨的方式;请看以下内容:
@Component 
@Lazy
class Sender {

  @Autowired
  RabbitMessagingTemplate template;

  @Bean
  Queue queue() {
    return new Queue("CustomerQ", false);
  }

  public void send(String message){
    template.convertAndSend("CustomerQ", message);
  }
}

@Lazy注解是一个有用的注解,它有助于增加启动时间。这些 bean 只有在需要时才会被初始化。

  1. 我们还将更新application.property文件,以包括与 Rabbit MQ 相关的属性,如下所示:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
  1. 我们准备发送消息。为了消费消息并发送电子邮件,我们将创建一个通知服务。为此,让我们创建另一个 Spring Boot 服务,chapter2.bootcustomernotification。在创建 Spring Boot 服务时,请确保选择了AMQPMail启动器库。AMQPMail都在I/O下。

  2. chapter2.bootcustomernotification项目的包结构如下所示:开发全面的微服务示例

  3. 添加一个Receiver类。Receiver类等待客户端的消息。这将接收客户资料服务发送的消息。在收到消息时,它会发送一封电子邮件,如下所示:

@Component
class Receiver {  
  @Autowired
  Mailer mailer;

  @Bean
  Queue queue() {
    return new Queue("CustomerQ", false);
  }

  @RabbitListener(queues = "CustomerQ")
    public void processMessage(String email) {
       System.out.println(email);
       mailer.sendMail(email);
    }
}
  1. 添加另一个组件来向客户发送电子邮件。我们将使用JavaMailSender通过以下代码发送电子邮件:
@Component 
class Mailer {
  @Autowired
  private  JavaMailSender  javaMailService;
    public void sendMail(String email){
      SimpleMailMessage mailMessage=new SimpleMailMessage();
      mailMessage.setTo(email);
      mailMessage.setSubject("Registration");
      mailMessage.setText("Successfully Registered");
      javaMailService.send(mailMessage);
    }
}

在幕后,Spring Boot 会自动配置JavaMailSender所需的所有参数。

  1. 要测试 SMTP,需要一个 SMTP 的测试设置来确保邮件发送出去。在本例中,将使用 FakeSMTP。您可以从nilhcem.github.io/FakeSMTP下载 FakeSMTP。

  2. 下载fakeSMTP-2.0.jar后,通过执行以下命令运行 SMTP 服务器:

$ java -jar fakeSMTP-2.0.jar

这将打开一个 GUI 来监视电子邮件消息。点击监听端口文本框旁边的启动服务器按钮。

  1. 使用以下配置参数更新application.properties以连接到 RabbitMQ 以及邮件服务器:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

spring.mail.host=localhost
spring.mail.port=2525
  1. 我们准备测试我们的微服务端到端。启动两个 Spring Boot 应用程序。打开浏览器,并通过 HAL 浏览器重复客户端创建步骤。在这种情况下,提交请求后,我们将能够在 SMTP GUI 中看到电子邮件。

在内部,客户资料服务异步调用客户通知服务,后者又将电子邮件消息发送到 SMTP 服务器:

开发一个全面的微服务示例

Spring Boot actuator

前面的部分探讨了开发微服务所需的大部分 Spring Boot 功能。在本节中,将探讨 Spring Boot 的一些适用于生产的操作方面。

Spring Boot actuator 提供了一个出色的开箱即用的机制,用于监控和管理生产中的 Spring Boot 应用程序:

注意

此示例的完整源代码可在本书的代码文件中的chapter2.bootactuator项目中找到。

  1. 创建另一个Spring Starter Project,命名为chapter2.bootactuator。这次,在Ops下选择WebActuators。与chapter2.bootrest项目类似,添加一个带有greet方法的GreeterController端点。

  2. 启动应用程序作为 Spring Boot 应用程序。

  3. 将浏览器指向localhost:8080/actuator。这将打开 HAL 浏览器。然后,查看Links部分。

Links部分下有许多链接可用。这些链接是由 Spring Boot actuator 自动公开的:

Spring Boot actuator

一些重要的链接列举如下:

  • dump:执行线程转储并显示结果

  • mappings:列出所有 HTTP 请求映射

  • info:显示有关应用程序的信息

  • health:显示应用程序的健康状况

  • autoconfig:显示自动配置报告

  • metrics:显示从应用程序收集的不同指标

使用 JConsole 进行监控

或者,我们可以使用 JMX 控制台查看 Spring Boot 信息。从 JConsole 连接到远程 Spring Boot 实例。Boot 信息将显示如下:

使用 JConsole 进行监控

使用 SSH 进行监控

Spring Boot 提供了使用 SSH 远程访问 Boot 应用程序的功能。以下命令从终端窗口连接到 Spring Boot 应用程序:

$ ssh -p 2000 user@localhost

可以通过在application.properties文件中添加shell.auth.simple.user.password属性来自定义密码。更新后的application.properties文件将类似于以下内容:

shell.auth.simple.user.password=admin

通过前面的命令连接时,可以访问类似的 actuator 信息。以下是通过 CLI 访问的指标信息示例:

  • help:列出所有可用选项

  • dashboard:这是一个显示大量系统级信息的有趣功能

配置应用程序信息

可以在application.properties中设置以下属性来自定义与应用程序相关的信息。添加后,重新启动服务器并访问 actuator 的/info端点以查看更新后的信息,如下所示:

info.app.name=Boot actuator
info.app.description= My Greetings Service
info.app.version=1.0.0

添加自定义健康模块

向 Spring Boot 应用程序添加新的自定义模块并不复杂。为了演示这一特性,假设如果一个服务在一分钟内获得超过两个事务,那么服务器状态将被设置为服务外。

为了自定义这一点,我们必须实现HealthIndicator接口并重写health方法。以下是一个快速而简单的实现来完成这项工作:

class TPSCounter {
  LongAdder count;
  int threshold = 2;
  Calendar expiry = null; 

  TPSCounter(){
    this.count = new LongAdder();
    this.expiry = Calendar.getInstance();
    this.expiry.add(Calendar.MINUTE, 1);
  }

  boolean isExpired(){
    return Calendar.getInstance().after(expiry);
  }

  boolean isWeak(){
    return (count.intValue() > threshold);
  }

  void increment(){
     count.increment();
  }
}

上述类是一个简单的 POJO 类,用于在窗口中维护事务计数。isWeak方法检查特定窗口中的事务是否达到了其阈值。isExpired方法检查当前窗口是否已过期。increment方法简单地增加计数器值。

下一步,实现我们的自定义健康指示器类TPSHealth。通过扩展HealthIndicator来完成:

@Component
class TPSHealth implements HealthIndicator {
  TPSCounter counter;

@Override
    public Health health() {
        boolean health = counter.isWeak(); // perform some specific health check
        if (health) {
            return Health.outOfService().withDetail("Too many requests", "OutofService").build();
        }
        return Health.up().build();
    }

    void updateTx(){
    if(counter == null || counter.isExpired()){
      counter = new TPSCounter();

    }
    counter.increment();
    }
}

health方法检查计数器是否弱。弱计数器意味着服务处理的事务比其可以处理的要多。如果它是弱的,它将把实例标记为服务外。

最后,我们将把TPSHealth自动装配到GreetingController类中,然后在greet方法中调用health.updateTx(),如下所示:

  Greet greet(){
    logger.info("Serving Request....!!!");
    health.updateTx(); 
    return new Greet("Hello World!");
  }

转到 HAL 浏览器中的/health端点,并查看服务器的状态。

现在,打开另一个浏览器,指向http://localhost:8080,并多次调用服务。返回/health端点并刷新以查看状态。它应该更改为服务外。

在此示例中,除了收集健康状态之外,没有采取其他行动,即使状态为服务外,新的服务调用仍将继续。但是,在现实世界中,程序应读取/health端点并阻止进一步的请求发送到此实例。

构建自定义指标

类似于健康状况,还可以自定义指标。以下示例显示了如何添加计数器服务和计量器服务,仅用于演示目的:

  @Autowired   
  CounterService counterService;

  @Autowired
  GaugeService gaugeService;

在问候方法中添加以下方法:

  this.counterService.increment("greet.txnCount");
  this.gaugeService.submit("greet.customgauge", 1.0);

重新启动服务器,转到/metrics以查看已添加的新计量器和计数器是否已反映在其中。

记录微服务

传统的 API 文档方法是编写服务规范文档或使用静态服务注册表。对于大量的微服务,很难保持 API 文档的同步。

微服务可以用许多方式记录。本节将探讨如何使用流行的 Swagger 框架记录微服务。以下示例将使用 Springfox 库生成 REST API 文档。Springfox 是一组 Java 和 Spring 友好的库。

创建一个新的Spring Starter Project,并在库选择窗口中选择Web。将项目命名为chapter2.swagger

注意

此示例的完整源代码可在本书的代码文件中的chapter2.swagger项目中找到。

由于 Springfox 库不是 Spring 套件的一部分,请编辑pom.xml并添加 Springfox Swagger 库依赖项。将以下依赖项添加到项目中:

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.3.1</version>
</dependency>  
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.3.1</version>
</dependency>

创建一个类似于之前创建的服务的 REST 服务,但还要添加@EnableSwagger2注释,如下所示:

@SpringBootApplication
@EnableSwagger2
public class Application {

这就是基本的 Swagger 文档所需的全部内容。启动应用程序,并将浏览器指向http://localhost:8080/swagger-ui.html。这将打开 Swagger API 文档页面:

记录微服务

如图所示,Swagger 列出了问候控制器上可能的操作。单击GET操作。这将展开GET行,提供尝试操作的选项。

摘要

在本章中,您了解了 Spring Boot 及其构建生产就绪应用程序的关键功能。

我们探讨了上一代 Web 应用程序,以及 Spring Boot 如何使开发人员更容易开发完全合格的微服务。我们还讨论了服务之间的异步基于消息的交互。此外,我们探讨了如何通过实际示例实现微服务所需的一些关键功能,例如安全性、HATEOAS、跨源、配置等。我们还看了 Spring Boot 执行器如何帮助运营团队,以及如何根据需要自定义它。最后,还探讨了记录微服务 API。

在下一章中,我们将更深入地研究在实施微服务时可能出现的一些实际问题。我们还将讨论一个能力模型,该模型在处理大型微服务实施时对组织有所帮助。

第三章:应用微服务概念

微服务是好的,但如果没有得到妥善构思,也可能是一种恶。错误的微服务解释可能导致无法挽回的失败。

本章将探讨实际实施微服务的技术挑战。它还将提供成功微服务开发的关键设计决策指南。还将审查关于微服务的一些常见关注点的解决方案和模式。本章还将审查企业规模微服务开发中的挑战,以及如何克服这些挑战。更重要的是,将在最后建立一个微服务生态系统的能力模型。

在本章中,您将了解以下内容:

  • 在开发微服务时需要考虑不同设计选择和模式之间的权衡

  • 开发企业级微服务的挑战和反模式

  • 微服务生态系统的能力模型

模式和常见设计决策

近年来,微服务已经获得了巨大的流行。它们已经成为架构师的首选,将 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 解决方案,因为我们只是模拟一个业务流程来监控其效率。在这种情况下,订单交付流程并不是 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: 
}

在前面的情况下,NameAddressContact是嵌套的 JSON 对象。但移动客户端可能只期望基本信息,比如名字和姓氏。在 SOA 世界中,ESB 或移动中间件完成了为客户端转换数据的工作。微服务的默认方法是获取Customer的所有元素,然后客户端负责过滤元素。在这种情况下,网络的负担就在于网络。

我们可以考虑几种方法来解决这个问题:

Customer {
  Id: 1
  Name: /customer/name/1
  Address: /customer/address/1
  Contact: /customer/contact/1
}

在第一种方法中,最小的信息与链接一起发送,如 HATEOAS 部分所述。在前面的情况下,对于客户 ID1,有三个链接,这将帮助客户端访问特定的数据元素。这个例子是一个简单的逻辑表示,不是实际的 JSON。在这种情况下,移动客户端将获得基本的客户信息。客户端进一步使用链接来获取额外所需的信息。

第二种方法是在客户端发起 REST 调用时使用的,它还将所需字段作为查询字符串的一部分发送。在这种情况下,客户端发送一个带有firstnamelastname作为查询字符串的请求,以表明客户端只需要这两个字段。缺点是它会导致复杂的服务器端逻辑,因为它必须基于字段进行过滤。服务器必须根据传入的查询发送不同的元素。

第三种方法是引入一定程度的间接性。在这种情况下,一个网关组件位于客户端和服务器之间,并根据消费者的规范转换数据。这是一个更好的方法,因为我们不会妥协后端服务契约。这导致了所谓的 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-flightPost-flightPre-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 ClusterRAC)被实施为解决这个问题的解决方案。

这种新的扩展模型减少了稳定性问题,但以增加的复杂性和所有权成本为代价。技术债务也随着时间的推移而增加,导致了一种状态,即完全重写是减少这种技术债务的唯一选择。

回顾

尽管应用程序架构良好,但功能组件之间存在明显的分离。它们松散耦合,通过标准化接口编程,并且具有丰富的领域模型。

显而易见的问题是,为什么这样一个设计良好的应用程序未能达到预期?架构师还能做些什么?

重要的是要了解随着时间的推移出现了什么问题。在本书的背景下,了解微服务如何避免这些情况再次发生也很重要。我们将在接下来的章节中研究其中一些情况。

共享数据

几乎所有功能模块都需要参考数据,例如航空公司的详细信息,飞机的详细信息,机场和城市的列表,国家,货币等等。例如,票价是根据出发地(城市)计算的,航班是在出发地和目的地(机场)之间的,办理登机手续是在出发机场(机场)进行的,等等。在某些功能中,参考数据是信息模型的一部分,而在另一些功能中,它用于验证目的。

这些参考数据大部分既不是完全静态的,也不是完全动态的。当航空公司开通新航线时,可能会增加一个国家、城市、机场等。当航空公司购买新飞机或更改现有飞机的座位配置时,飞机参考数据可能会发生变化。

参考数据的常见用法之一是根据某些参考数据过滤操作数据。例如,假设用户希望查看所有飞往某个国家的航班。在这种情况下,事件的流程可能如下:找到所选国家的所有城市,然后找到所有城市的机场,然后发送请求以获取所有飞往该国家中所识别的机场的航班列表。

在设计系统时,架构师考虑了多种方法。将参考数据作为独立子系统分离是考虑的选项之一,但这可能会导致性能问题。团队决定采用异常处理的方法来处理参考数据,与其他事务相比。考虑到前面讨论的查询模式的性质,该方法是将参考数据用作共享库。

在这种情况下,允许子系统直接访问参考数据,而不是通过 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 应用程序

在过渡阶段,我们必须同时保留旧的和新的用户界面。通常有三种一般性方法用于处理这种情况。

第一种方法是将旧的和新的用户界面作为独立的用户应用程序,彼此之间没有链接,如下图所示:

用户界面和 Web 应用程序

用户登录到新应用程序以及旧应用程序,就像两个不同的应用程序,它们之间没有单点登录(SSO)。这种方法简单,没有额外开销。在大多数情况下,除非针对两个不同的用户群体,否则业务可能不会接受这种方法。

第二种方法是将旧用户界面作为主要应用程序,然后在用户请求新应用程序的页面时将页面控件转移到新用户界面:

用户界面和 Web 应用程序

在这种情况下,由于旧应用程序和新应用程序都是在 Web 浏览器窗口中运行的 Web 应用程序,用户将获得无缝的体验。必须在旧和新用户界面之间实现 SSO。

第三种方法是直接将现有的旧用户界面集成到新的微服务后端,如下图所示:

用户界面和 Web 应用程序

在这种情况下,新的微服务被构建为无展示层的无头应用程序。这可能是具有挑战性的,因为它可能需要对旧的用户界面进行许多更改,比如引入服务调用、数据模型转换等。

在最后两种情况中的另一个问题是如何处理资源和服务的认证。

会话处理和安全性

假设新服务是基于 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。

运行和测试项目

按照下面列出的步骤构建和测试本章开发的微服务:

  1. 使用 Maven 构建每个项目。确保test标志关闭。测试程序假定其他依赖服务正在运行。如果依赖服务不可用,则测试将失败。在我们的示例中,预订和票价有直接依赖关系。我们将学习如何在第七章, 记录和监控微服务中避免这种依赖关系:
mvn -Dmaven.test.skip=true install

  1. 运行 RabbitMQ 服务器:
rabbitmq_server-3.5.6/sbin$ ./rabbitmq-server

  1. 在单独的终端窗口中运行以下命令:
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

  1. 网站项目有一个CommandLineRunner,它在启动时执行所有测试用例。一旦所有服务成功启动,就在浏览器中打开http://localhost:8001

  2. 浏览器要求输入基本安全凭据。使用guestguest123作为凭据。本示例仅显示了具有基本身份验证机制的网站安全性。如第二章, 使用 Spring Boot 构建微服务中所述,可以使用 OAuth2 实现服务级安全性。

  3. 输入正确的安全凭据会显示以下屏幕。这是我们 BrownField PSS 应用程序的主屏幕:运行和测试项目

  4. 提交按钮调用搜索微服务以获取满足屏幕上条件的可用航班。在搜索微服务启动时预先填充了一些航班。如有需要,编辑搜索微服务代码以输入额外的航班。

  5. 下一个截图显示了带有航班列表的输出屏幕。预订链接将带我们到所选航班的预订屏幕:运行和测试项目

  6. 下一个截图显示了预订屏幕。用户可以输入乘客信息,并通过点击确认按钮来创建预订。这会调用预订微服务,以及内部的票价服务。它还会向搜索微服务发送一条消息:运行和测试项目

  7. 如果预订成功,下一个确认屏幕将显示预订参考号码:运行和测试项目

  8. 让我们测试办理登机手续微服务。可以通过点击屏幕顶部的办理登机手续菜单来完成。使用前一步获得的预订参考号码来测试办理登机手续。如下图所示:运行和测试项目

  9. 在上一个屏幕上点击搜索按钮会调用 Booking 微服务,并检索预订信息。点击办理登机手续链接进行办理登机手续。这会调用办理登机微服务:运行和测试项目

  10. 如果办理登机成功,它会显示确认消息,如下一张截图所示,并附有确认号。这是通过内部调用办理登机服务来完成的。办理登机服务向 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 的能力解释如下:

  • 分布式配置:当有许多微服务实例在不同的配置文件下运行时,配置属性很难管理,比如开发、测试、生产等。因此,重要的是以受控的方式集中管理它们。分布式配置管理模块是为了外部化和集中微服务配置参数。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.propertiesapplication.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对于大规模部署来说不够灵活。最后两个选项依赖于服务器上挂载的本地或共享文件系统。

对于大规模部署,需要一个简单而强大的集中式配置管理解决方案:

Spring Cloud Config

如前图所示,所有微服务都指向一个中央服务器以获取所需的配置参数。然后,这些微服务在本地缓存这些参数以提高性能。Config 服务器将配置状态更改传播给所有订阅的微服务,以便本地缓存的状态可以更新为最新更改。Config 服务器还使用配置文件来解析特定于环境的值。

如下截图所示,Spring Cloud 项目下有多个选项可用于构建配置服务器。Config ServerZookeeper ConfigurationConsul Configuration都是可选项。但本章将仅关注 Spring Config 服务器的实现:

Spring Cloud Config

Spring Config 服务器将属性存储在诸如 Git 或 SVN 之类的版本控制存储库中。Git 存储库可以是本地的或远程的。对于大规模分布式微服务部署,首选高可用的远程 Git 服务器。

Spring Cloud Config 服务器架构如下图所示:

Spring Cloud Config

如前图所示,嵌入在 Spring Boot 微服务中的 Config 客户端使用简单的声明性机制从中央配置服务器进行配置查找,并将属性存储到 Spring 环境中。配置属性可以是应用级配置,如每日交易限额,也可以是基础设施相关配置,如服务器 URL、凭据等。

与 Spring Boot 不同,Spring Cloud 使用一个引导上下文,这是主应用程序的父上下文。引导上下文负责从 Config 服务器加载配置属性。引导上下文寻找bootstrap.yamlbootstrap.properties来加载初始配置属性。要使这在 Spring Boot 应用程序中工作,将application.*文件重命名为bootstrap.*

接下来是什么?

接下来的几节演示了如何在实际场景中使用 Config 服务器。为了做到这一点,我们将修改我们的搜索微服务(chapter5.search)以使用 Config 服务器。下图描述了这种情况:

接下来是什么?

在此示例中,搜索服务将通过传递服务名称在启动时读取 Config 服务器。在这种情况下,搜索服务的服务名称将是search-service。为search-service配置的属性包括 RabbitMQ 属性以及自定义属性。

注意

本节的完整源代码可在代码文件的chapter5.configserver项目中找到。

设置 Config 服务器

创建新的 Config 服务器使用 STS 需要遵循以下步骤:

  1. 创建一个新的Spring Starter Project,并选择Config ServerActuator,如下图所示:设置 Config 服务器

  2. 设置一个 Git 存储库。这可以通过指向远程 Git 配置存储库来完成,比如github.com/spring-cloud-samples/config-repo上的存储库。这个 URL 是一个指示性的 URL,是 Spring Cloud 示例使用的 Git 存储库。我们将不得不使用我们自己的 Git 存储库。

  3. 或者,可以使用基于本地文件系统的 Git 存储库。在真实的生产场景中,建议使用外部 Git。本章中的配置服务器将使用基于本地文件系统的 Git 存储库进行演示。

  4. 输入下面列出的命令来设置本地 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是为演示目的而创建的。我们将在后续章节中更改这一点。

  1. 下一步是更改配置服务器中的配置,以使用在上一步中创建的 Git 存储库。为了做到这一点,将文件application.properties重命名为bootstrap.properties设置配置服务器

  2. 编辑新的bootstrap.properties文件的内容,使其与以下内容匹配:

server.port=8888
spring.cloud.config.server.git.uri: file://${user.home}/config-repo

端口8888是配置服务器的默认端口。即使没有配置server.port,配置服务器也应该绑定到8888。在 Windows 环境中,文件 URL 需要额外的/

  1. 可选地,将自动生成的Application.java的默认包从com.example重命名为com.brownfield.configserver。在Application.java中添加@EnableConfigServer
@EnableConfigServer
@SpringBootApplication
public class ConfigserverApplication {
  1. 通过右键单击项目并将其作为 Spring Boot 应用程序运行来运行配置服务器。

  2. 访问http://localhost:8888/env,以查看服务器是否正在运行。如果一切正常,这将列出所有环境配置。请注意,/env是一个执行器端点。

  3. 检查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 的第二部分表示配置文件。可以在存储库中为应用程序配置多个配置文件。配置文件可以在各种场景中使用。两个常见的场景是分隔不同的环境,如DevTestStageProd等,或者分隔服务器配置,如PrimarySecondary等。第一个表示应用程序的不同环境,而第二个表示部署应用程序的不同服务器。

配置文件名是将用于匹配存储库中文件名的逻辑名称。默认配置文件名为default。要为不同的环境配置属性,我们必须根据以下示例配置不同的文件。在这个例子中,第一个文件是为开发环境而设,而第二个文件是为生产环境而设:

application-development.properties
application-production.properties

这些分别可以使用以下 URL 访问:

URL 的最后一部分是标签,默认情况下命名为master。标签是一个可选的 Git 标签,如果需要的话可以使用。

简而言之,URL 基于以下模式:http://localhost:8888/{name}/{profile}/{label}

配置也可以通过忽略配置文件来访问。在前面的例子中,以下所有三个 URL 都指向相同的配置:

有一个选项可以为不同的配置文件使用不同的 Git 存储库。这对于生产系统是有意义的,因为对不同存储库的访问可能是不同的。

从客户端访问配置服务器

在上一节中,已设置并通过 Web 浏览器访问了配置服务器。在本节中,将修改搜索微服务以使用配置服务器。搜索微服务将充当配置客户端。

按照以下步骤使用配置服务器而不是从application.properties文件中读取属性:

  1. 将 Spring Cloud Config 依赖项和执行器(如果尚未就位)添加到pom.xml文件中。执行器对于刷新配置属性是强制性的:
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
  </dependency>
  1. 由于我们正在修改之前章节的 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>
  1. 下一个屏幕截图显示了 Cloud starter 库选择屏幕。如果应用是从头开始构建的,请按照以下屏幕截图中显示的方式选择库:从客户端访问配置服务器

  2. 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以解析属性。

  1. 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
  1. 为了演示属性的集中配置和更改的传播,向属性文件中添加一个新的特定于应用程序的属性。我们将添加originairports.shutdown来临时将某个机场从搜索中移除。用户在搜索关闭列表中提到的机场时将不会得到任何航班:
originairports.shutdown=SEA

在此示例中,当使用SEA作为起始地进行搜索时,我们将不返回任何航班。

  1. 通过执行以下命令将此新文件提交到 Git 存储库中:
git add –A .
git commit –m "adding new configuration" 

  1. 最终的search-service.properties文件应如下所示:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
originairports.shutdown:SEA
  1. chapter5.search项目的bootstrap.properties应如下所示:
spring.application.name=search-service
server.port=8090
spring.cloud.config.uri=http://localhost:8888
  1. 修改搜索微服务代码以使用配置参数originairports.shutdown。必须在类级别添加RefreshScope注解,以允许在更改时刷新属性。在这种情况下,我们将在SearchRestController类中添加一个刷新范围:
@RefreshScope
  1. 添加以下实例变量作为新属性的占位符,该属性刚刚添加到配置服务器中。search-service.properties文件中的属性名称必须匹配:
  @Value("${originairports.shutdown}")
  private String originAirportShutdownList;
  1. 更改应用程序代码以使用此属性。这是通过修改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,并查看请求的起始地是否在关闭列表中。如果匹配,则搜索方法将返回一个空的航班列表,而不是继续进行实际搜索。

  1. 启动配置服务器。然后启动 Search 微服务。确保 RabbitMQ 服务器正在运行。

  2. 修改chapter5.website项目以匹配bootstrap.properties的内容,如下所示,以利用配置服务器:

spring.application.name=test-client
server.port=8001
spring.cloud.config.uri=http://localhost:8888
  1. Application.java中的CommandLineRunnerrun方法更改为查询 SEA 作为起始机场:
SearchQuery = new SearchQuery("SEA","SFO","22-JAN-16");
  1. 运行chapter5.website项目。CommandLineRunner现在将返回一个空的航班列表。服务器将打印以下消息:
The origin airport is in shutdown state

处理配置更改

本节将演示如何在发生更改时传播配置属性:

  1. search-service.properties文件中的属性更改为以下内容:
originairports.shutdown:NYC

在 Git 存储库中提交更改。刷新此服务的配置服务器 URL(http://localhost:8888/search-service/default),并查看属性更改是否反映出来。如果一切正常,我们将看到属性更改。前面的请求将强制配置服务器再次从存储库中读取属性文件。

  1. 再次运行网站项目,并观察CommandLineRunner的执行。请注意,在这种情况下,我们没有重新启动 Search 微服务或配置服务器。服务将像以前一样返回一个空的航班列表,并且仍然会报错如下:
The origin airport is in shutdown state

这意味着更改不会反映在 Search 服务中,服务仍然使用旧版本的配置属性。

  1. 为了强制重新加载配置属性,请调用 Search 微服务的/refresh端点。这实际上是执行器的刷新端点。以下命令将向/refresh端点发送一个空的 POST:
curl –d {} localhost:8090/refresh

  1. 重新运行网站项目,并观察CommandLineRunner的执行。这应该返回我们从 SEA 请求的航班列表。请注意,如果预订服务没有运行,网站项目可能会失败。

/refresh端点将刷新本地缓存的配置属性,并从配置服务器重新加载新值。

用于传播配置更改的 Spring Cloud Bus

采用上述方法,可以在不重新启动微服务的情况下更改配置参数。当服务运行的实例只有一个或两个时,这是很好的。如果有很多实例会发生什么?例如,如果有五个实例,那么我们必须针对每个服务实例进行/refresh。这绝对是一项繁琐的活动:

用于传播配置更改的 Spring Cloud Bus

Spring Cloud Bus 提供了一种机制,可以在不知道有多少实例或它们的位置的情况下刷新多个实例之间的配置。当有许多微服务的服务实例运行或有许多不同类型的微服务运行时,这是特别方便的。这是通过将所有服务实例连接到单个消息代理来完成的。每个实例都订阅更改事件,并在需要时刷新其本地配置。通过调用任一实例的/bus/refresh端点来触发此刷新,然后通过云总线和公共消息代理传播更改。

在此示例中,RabbitMQ 被用作 AMQP 消息代理。按照以下记录的步骤来实现这一点:

  1. chapter5.search项目的pom.xml文件中添加一个新的依赖项,以引入 Cloud Bus 依赖项:
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
  1. Search 微服务还需要连接到 RabbitMQ,但这已经在search-service.properties中提供了。

  2. 重建并重新启动搜索微服务。在这种情况下,我们将从命令行运行两个搜索微服务实例,如下所示:

java -jar -Dserver.port=8090  search-1.0.jar 
java -jar -Dserver.port=8091  search-1.0.jar

搜索服务的两个实例现在将运行,一个在端口8090上,另一个在8091上。

  1. 重新运行网站项目。这只是为了确保一切正常。此时,搜索服务应返回一个航班。

  2. 现在,使用以下值更新search-service.properties,并提交到 Git:

originairports.shutdown:SEA
  1. 运行以下命令来执行/bus/refresh。请注意,我们正在针对一个实例运行新的总线端点,本例中为8090
curl –d {} localhost:8090/bus/refresh

  1. 立即,我们将看到两个实例的以下消息:
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下可用,如下截图所示:

Feign 作为声明式 REST 客户端

下一步是创建一个新的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。namevalue是必需的。在这种情况下,由于我们有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 客户端会查找配置服务器以获取可用微服务实例列表,并默认应用轮询负载平衡算法。

为了使用 Ribbon 客户端,我们将不得不将以下依赖项添加到pom.xml文件中:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

在从头开始开发的情况下,可以从 Spring Starter 库或start.spring.io/中选择。Ribbon 在Cloud Routing下可用:

用于负载平衡的 Ribbon

更新预订微服务配置文件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

如前图所示,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 秒。

  1. 启动一个新的 Spring Starter 项目,并选择Config ClientEureka ServerActuator设置 Eureka 服务器

Eureka 服务器的项目结构如下图所示:

设置 Eureka 服务器

请注意,主应用程序的名称为EurekaserverApplication.java

  1. 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 服务器。

  1. 创建一个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
  1. 更改默认的Application.java。在这个例子中,包也被重命名为com.brownfield.pss.eurekaserver,类名改为EurekaserverApplication。在EurekaserverApplication中,添加@EnableEurekaServer
@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {
  1. 现在我们准备启动 Eureka 服务器。确保 Config 服务器也已启动。右键单击应用程序,然后选择Run As | Spring Boot App。应用程序启动后,在浏览器中打开http://localhost:8761以查看 Eureka 控制台。

  2. 在控制台中,请注意当前在 Eureka 中注册的实例下没有实例注册。由于没有启用 Eureka 客户端的服务,因此此时列表为空。

  3. 对我们的微服务进行一些更改将启用使用 Eureka 服务的动态注册和发现。为此,首先我们必须将 Eureka 依赖项添加到pom.xml文件中。如果服务是使用 Spring Starter 项目新建的,那么选择Config ClientActuatorWeb以及Eureka discovery客户端如下所示:设置 Eureka 服务器

  4. 由于我们正在修改我们的微服务,在它们的pom.xml文件中添加以下附加依赖项:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
  1. 必须在各自的配置文件中的config-repo下的所有微服务中添加以下属性。这将帮助微服务连接到 Eureka 服务器。更新完成后提交到 Git:
eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/
  1. 在各自的 Spring Boot 主类中的所有微服务中添加@EnableDiscoveryClient。这要求 Spring Boot 在启动时注册这些服务,以宣传它们的可用性。

  2. 启动除预订之外的所有服务器。由于我们在预订服务上使用了 Ribbon 客户端,当我们将 Eureka 客户端添加到类路径中时,行为可能会有所不同。我们将很快解决这个问题。

  3. 转到 Eureka URL(http://localhost:8761),您可以看到所有三个实例都正在运行:设置 Eureka 服务器

是时候解决预订的问题了。我们将删除之前的 Ribbon 客户端,改用 Eureka。Eureka 在内部使用 Ribbon 进行负载平衡。因此,负载平衡行为不会改变。

  1. 删除以下依赖项:
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
  1. 还要从FareServiceProxy类中删除@RibbonClient(name="fares")注释。

  2. @FeignClient(name="fares-service")更新为匹配实际票价微服务的服务 ID。在这种情况下,fare-service是配置在票价微服务的bootstrap.properties中的服务 ID。这是 Eureka 发现客户端发送到 Eureka 服务器的名称。服务 ID 将用作 Eureka 服务器中注册的服务的键。

  3. 还要从booking-service.properties文件中删除服务器列表。使用 Eureka,我们将从 Eureka 服务器动态发现此列表:

fares-proxy.ribbon.listOfServers=localhost:8080, localhost:8081
  1. 启动预订服务。您将看到CommandLineRunner成功创建了一个预订,其中涉及使用 Eureka 发现机制调用票价服务。返回 URL 以查看所有注册的服务:设置 Eureka 服务器

  2. 更改网站项目的bootstrap.properties文件,以利用 Eureka 而不是直接连接到服务实例。在这种情况下,我们将不使用 Feign 客户端。相反,为了演示目的,我们将使用负载平衡的RestTemplate。将这些更改提交到 Git 存储库:

spring.application.name=test-client
eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/
  1. Application类中添加@EnableDiscoveryClient,使客户端具有 Eureka 意识。

  2. 编辑Application.javaBrownFieldSiteController.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;
  1. 我们使用这些RestTemplate实例来调用微服务。用在 Eureka 服务器中注册的服务 ID 替换硬编码的 URL。在下面的代码中,我们使用服务名称search-servicebook-servicecheckin-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);
  1. 现在我们准备运行客户端。运行网站项目。如果一切正常,网站项目的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-server1eureka-server2

spring.application.name=eureka
spring.cloud.config.uri=http://localhost:8888

启动两个 Eureka 服务器的实例,server18761上,server28762上:

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:8761http://localhost:8762

启动所有微服务。打开8761的那个将立即反映出更改,而另一个将需要 30 秒才能反映出状态。由于这两个服务器都在一个集群中,状态在这两个服务器之间是同步的。如果我们将这些服务器放在负载均衡器/DNS 后面,那么客户端将始终连接到其中一个可用的服务器。

完成此练习后,切换回独立模式进行剩余的练习。

Zuul 代理作为 API 网关

在大多数微服务实现中,内部微服务端点不会暴露在外部。它们被保留为私有服务。一组公共服务将使用 API 网关向客户端公开。有许多原因可以这样做:

  • 只有一组精选的微服务是客户端所需的。

  • 如果需要应用特定于客户端的策略,可以在一个地方应用它们,而不是在多个地方。这种情况的一个例子是跨域访问策略。

  • 在服务端点上实现特定于客户端的转换很困难。

  • 如果需要数据聚合,特别是在带宽受限的环境中避免多个客户端调用,那么中间需要一个网关。

Zuul 是一个简单的网关服务或边缘服务,非常适合这些情况。Zuul 也来自 Netflix 微服务产品系列。与许多企业 API 网关产品不同,Zuul 为开发人员提供了根据特定要求进行配置或编程的完全控制:

Zuul 代理作为 API 网关

Zuul 代理在内部使用 Eureka 服务器进行服务发现,并使用 Ribbon 在服务实例之间进行负载平衡。

Zuul 代理还能够进行路由、监控、管理弹性、安全等。简单来说,我们可以将 Zuul 视为反向代理服务。使用 Zuul,我们甚至可以通过在 API 层覆盖它们来改变底层服务的行为。

设置 Zuul

与 Eureka 服务器和 Config 服务器不同,在典型的部署中,Zuul 是特定于一个微服务的。但是,也有部署方式,其中一个 API 网关覆盖多个微服务。在这种情况下,我们将为我们的每个微服务添加 Zuul:Search、Booking、Fare 和 Check-in:

注意

本节的完整源代码可在代码文件的chapter5.*-apigateway项目中找到。

  1. 逐个转换微服务。从 Search API Gateway 开始。创建一个新的 Spring Starter 项目,并选择ZuulConfig ClientActuatorEureka Discovery设置 Zuul

search-apigateway的项目结构如下图所示:

设置 Zuul

  1. 下一步是将 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 服务器进行解析。

  1. 更新search-apigatewaybootstrap.properties文件如下。在这个配置中没有什么新的内容——服务的名称、端口和 Config 服务器的 URL:
spring.application.name=search-apigateway
server.port=8095
spring.cloud.config.uri=http://localhost:8888
  1. 编辑Application.java。在这种情况下,包名和类名也更改为com.brownfield.pss.search.apigatewaySearchApiGateway。还要添加@EnableZuulProxy以告诉 Spring Boot 这是一个 Zuul 代理:
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class SearchApiGateway {
  1. 将其作为 Spring Boot 应用程序运行。在此之前,请确保 Config 服务器、Eureka 服务器和 Search 微服务正在运行。

  2. 更改网站项目的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 实例:

当客户端也是 Eureka 客户端时,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 客户端时的高可用性

微服务仍然会通过 Eureka 服务器由 Zuul 进行负载均衡。

为所有其他服务完成 Zuul

为了完成这个练习,为所有的微服务添加 API 网关项目(将它们命名为*-apigateway)。需要完成以下步骤来实现这个任务:

  1. 为每个服务创建新的属性文件,并检入 Git 存储库。

  2. application.properties更改为bootstrap.properties,并添加所需的配置。

  3. 在每个*-apigateway项目的Application.java中添加@EnableZuulProxy

  4. 在每个*-apigateway项目的Application.java文件中添加@EnableDiscoveryClient

  5. 可选地,更改默认生成的包名和文件名。

最终,我们将拥有以下 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的逻辑队列,从中接收者检索消息。OUTPUTINPUT的物理绑定通过配置进行管理。在这种情况下,两者链接到同一个物理队列——RabbitMQ 上的MyQueue。因此,一端的Source.OUTPUT指向MyQueue,另一端的Sink.INPUT指向相同的MyQueue

Spring Cloud 提供了在一个应用程序中使用多个消息提供程序的灵活性,例如将来自 Kafka 的输入流连接到 Redis 输出流,而无需管理复杂性。Spring Cloud Stream 是基于消息的集成的基础。Cloud Stream 模块子项目是另一个 Spring Cloud 库,提供了许多端点实现。

作为下一步,重新构建云流的微服务间消息通信。如下图所示,我们将在搜索微服务下定义一个连接到InventoryQSearchSink。预订将为发送库存更改消息定义一个BookingSource连接到InventoryQ。类似地,登记定义了一个用于发送登记消息的CheckinSource。预订定义了一个接收器BookingSink,用于接收消息,都绑定到 RabbitMQ 上的CheckinQ队列:

用于响应式微服务的流

在这个例子中,我们将使用 RabbitMQ 作为消息代理:

  1. 将以下 Maven 依赖项添加到预订、搜索和登记中,因为这三个模块使用消息传递:
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
  1. 将以下两个属性添加到booking-service.properties中。这些属性将逻辑队列inventoryQ绑定到物理inventoryQ,逻辑checkinQ绑定到物理checkinQ
spring.cloud.stream.bindings.inventoryQ.destination=inventoryQ
spring.cloud.stream.bindings.checkInQ.destination=checkInQ
  1. 将以下属性添加到search-service.properties中。这个属性将逻辑队列inventoryQ绑定到物理inventoryQ
spring.cloud.stream.bindings.inventoryQ.destination=inventoryQ
  1. 将以下属性添加到checkin-service.properties中。这个属性将逻辑队列checkinQ绑定到物理checkinQ
spring.cloud.stream.bindings.checkInQ.destination=checkInQ
  1. 将所有文件提交到 Git 存储库。

  2. 下一步是编辑代码。搜索微服务从预订微服务中消费消息。在这种情况下,预订是源,搜索是接收器。

在预订服务的Sender类中添加@EnableBinding。这将使 Cloud Stream 根据类路径中可用的消息代理库进行自动配置。在我们的情况下,这是 RabbitMQ。参数BookingSource定义了用于此配置的逻辑通道:

@EnableBinding(BookingSource.class)
public class Sender {
  1. 在这种情况下,BookingSource定义了一个名为inventoryQ的消息通道,它在配置中与 RabbitMQ 的inventoryQ物理绑定。BookingSource使用注解@Output来指示这是输出类型的消息,即从模块发出的消息。这些信息将用于消息通道的自动配置:
interface BookingSource {
    public static String InventoryQ="inventoryQ"; 
    @Output("inventoryQ")
    public MessageChannel inventoryQ();      
}
  1. 我们可以使用 Spring Cloud Stream 提供的默认Source类,而不是定义自定义类,如果服务只有一个源和一个接收器:
public interface Source {
  @Output("output")
  MessageChannel output();
}
  1. 在发送器中定义一个基于BookingSource的消息通道。以下代码将注入一个名为inventory的输出消息通道,该通道已在BookingSource中配置:
  @Output (BookingSource.InventoryQ)
  @Autowired
  private MessageChannel;
  1. 重新实现BookingSender中的send消息方法:
public void send(Object message){
  messageChannel.
    send(MessageBuilder.withPayload(message).
    build());
}
  1. 现在以与预订服务相同的方式将以下内容添加到SearchReceiver类中:
@EnableBinding(SearchSink.class)
public class Receiver {
  1. 在这种情况下,SearchSink接口将如下所示。这将定义它连接的逻辑接收器队列。在这种情况下,消息通道被定义为@Input,以指示该消息通道用于接受消息:
interface SearchSink {
    public static String INVENTORYQ="inventoryQ"; 
    @Input("inventoryQ")
    public MessageChannel inventoryQ();
}
  1. 修改搜索服务以接受此消息:
@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"));
}
  1. 我们仍然需要我们在配置文件中拥有的 RabbitMQ 配置来连接到消息代理:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
server.port=8090
  1. 运行所有服务,并运行网站项目。如果一切正常,网站项目将成功执行搜索、预订和办理登机手续功能。也可以通过浏览器指向 http://localhost:8001 进行测试。

总结 BrownField PSS 架构

以下图表显示了我们使用 Config 服务器、Eureka、Feign、Zuul 和 Cloud Streams 创建的整体架构。该架构还包括所有组件的高可用性。在这种情况下,我们假设客户端使用 Eureka 客户端库:

总结 BrownField PSS 架构

以下表格给出了项目及其监听的端口的摘要:

微服务 项目 端口
预订微服务 chapter5.book 80608064
办理登机手续微服务 chapter5.checkin 80708074
票价微服务 chapter5.fares 80808084
搜索微服务 chapter5.search 80908094
网站客户端 chapter5.website 8001
Spring Cloud Config 服务器 chapter5.configserver 8888/8889
Spring Cloud Eureka 服务器 chapter5.eurekaserver 8761/8762
预订 API 网关 chapter5.book-apigateway 80958099
办理登机手续 API 网关 chapter5.checkin-apigateway 80758079
票价 API 网关 chapter5.fares-apigateway 80858089
搜索 API 网关 chapter5.search-apigateway 80658069

按照以下步骤进行最终运行:

  1. 运行 RabbitMQ。

  2. 使用根级别的 pom.xml 构建所有项目:

mvn –Dmaven.test.skip=true clean install 

  1. 从各自的文件夹运行以下项目。在启动下一个服务之前,请记得等待 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

  1. 打开浏览器窗口,指向 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 实现。

第六章:自动缩放微服务

Spring Cloud 提供了必要的支持,以便在规模上部署微服务。为了充分发挥类似云的环境的全部功能,微服务实例还应能够根据流量模式自动扩展和收缩。

本章将详细介绍如何通过有效使用从 Spring Boot 微服务收集的执行器数据来控制部署拓扑,从而使微服务能够弹性增长和收缩,并实现一个简单的生命周期管理器。

在本章结束时,您将学习以下主题:

  • 自动缩放的基本概念和不同的自动缩放方法

  • 在微服务的上下文中,生命周期管理器的重要性和能力

  • 检查自定义生命周期管理器以实现自动缩放

  • 从 Spring Boot 执行器中以编程方式收集统计信息,并将其用于控制和塑造传入流量

审查微服务能力模型

本章将涵盖微服务能力模型中讨论的应用生命周期管理能力,该能力在第三章中讨论,应用微服务概念,如下图所示:

审查微服务能力模型

在本章中,我们将看到生命周期管理器的基本版本,这将在后续章节中得到增强。

使用 Spring Cloud 扩展微服务

在第五章中,使用 Spring Cloud 扩展微服务,您学习了如何使用 Spring Cloud 组件扩展 Spring Boot 微服务。我们实现的 Spring Cloud 的两个关键概念是自注册和自发现。这两个能力使得微服务部署自动化。通过自注册,微服务可以在实例准备好接受流量时,通过向中央服务注册表注册服务元数据来自动宣传服务的可用性。一旦微服务注册,消费者就可以通过发现注册表服务实例来从下一刻开始消费新注册的服务。注册表是这种自动化的核心。

这与传统 JEE 应用服务器采用的传统集群方法有很大不同。在 JEE 应用服务器的情况下,服务器实例的 IP 地址在负载均衡器中更多地是静态配置的。因此,在互联网规模的部署中,集群方法并不是自动缩放的最佳解决方案。此外,集群还带来其他挑战,例如它们必须在所有集群节点上具有完全相同的二进制版本。还有可能一个集群节点的故障会因节点之间的紧密依赖关系而影响其他节点。

注册表方法将服务实例解耦。它还消除了在负载均衡器中手动维护服务地址或配置虚拟 IP 的需要:

使用 Spring Cloud 扩展微服务

如图所示,在我们的自动化微服务部署拓扑中有三个关键组件:

  • Eureka是微服务注册和发现的中央注册组件。消费者和提供者都使用 REST API 来访问注册表。注册表还保存服务元数据,如服务标识、主机、端口、健康状态等。

  • Eureka客户端与Ribbon客户端一起提供客户端动态负载平衡。消费者使用 Eureka 客户端查找 Eureka 服务器,以识别目标服务的可用实例。Ribbon 客户端使用此服务器列表在可用的微服务实例之间进行负载平衡。类似地,如果服务实例停止服务,这些实例将从 Eureka 注册表中移除。负载均衡器会自动对这些动态拓扑变化做出反应。

  • 第三个组件是使用 Spring Boot 开发的微服务实例,并启用了执行器端点。

然而,这种方法存在一个缺陷。当需要额外的微服务实例时,需要手动启动一个新实例。在理想情况下,启动和停止微服务实例也需要自动化。

例如,当需要添加另一个搜索微服务实例来处理流量增加或负载突发情况时,管理员必须手动启动一个新实例。同样,当搜索实例一段时间处于空闲状态时,需要手动将其从服务中移除,以实现最佳的基础设施使用。这在服务在按使用量付费的云环境中尤为重要。

理解自动扩展的概念

自动扩展是一种根据资源使用情况自动扩展实例的方法,以通过复制要扩展的服务来满足 SLA。

系统会自动检测流量增加,启动额外的实例,并使它们可用于处理流量。同样,当流量减少时,系统会自动检测并通过将活动实例从服务中收回来减少实例数量:

理解自动扩展的概念

如前图所示,通常使用一组预留机器来进行自动扩展。

由于许多云订阅都是基于按使用量付费的模式,因此在针对云部署时,这是一种必要的能力。这种方法通常被称为弹性。它也被称为动态资源配置和取消配置。自动扩展是一种针对具有不同流量模式的微服务的有效方法。例如,会计服务在月末和年末流量会很高。永久预留实例来处理这些季节性负载是没有意义的。

在自动扩展方法中,通常有一个资源池,其中有一些备用实例。根据需求,实例将从资源池移动到活动状态,以满足多余的需求。这些实例没有预先标记为任何特定的微服务,也没有预先打包任何微服务二进制文件。在高级部署中,Spring Boot 二进制文件可以根据需要从 Nexus 或 Artifactory 等存储库中下载。

自动扩展的好处

实施自动扩展机制有许多好处。在传统部署中,管理员针对每个应用程序预留一组服务器。通过自动扩展,不再需要这种预分配。这种预分配的服务器可能导致服务器利用不足。在这种情况下,即使相邻服务需要额外的资源,空闲服务器也无法利用。

对于数百个微服务实例,为每个微服务预分配固定数量的服务器并不划算。更好的方法是为一组微服务预留一定数量的服务器实例,而不是预先分配或标记它们与微服务相关。根据需求,一组服务可以共享一组可用资源。通过这种方式,微服务可以通过最佳地利用资源在可用的服务器实例之间动态移动:

自动扩展的好处

如前图所示,M1微服务有三个实例,M2有一个实例,M3有一个实例正在运行。还有另一台服务器保持未分配。根据需求,未分配的服务器可以用于任何微服务:M1M2M3。如果M1有更多的服务请求,那么未分配的实例将用于M1。当服务使用量下降时,服务器实例将被释放并移回池中。稍后,如果M2的需求增加,同一服务器实例可以使用M2激活。

自动扩展的一些关键好处包括:

  • 它具有高可用性和容错性:由于存在多个服务实例,即使一个失败,另一个实例也可以接管并继续为客户提供服务。这种故障转移对消费者来说是透明的。如果此服务没有其他实例可用,自动扩展服务将识别此情况并启动另一台带有服务实例的服务器。由于启动或关闭实例的整个过程是自动的,因此服务的整体可用性将高于没有自动扩展的系统。没有自动扩展的系统需要手动干预以添加或删除服务实例,在大规模部署中将很难管理。

例如,假设有两个预订服务实例正在运行。如果流量增加,通常情况下,现有实例可能会过载。在大多数情况下,整套服务将被堵塞,导致服务不可用。在自动扩展的情况下,可以快速启动新的预订服务实例。这将平衡负载并确保服务可用性。

  • 它增加了可伸缩性:自动扩展的关键好处之一是水平可伸缩性。自动扩展允许我们根据流量模式自动选择性地扩展或缩减服务。

  • 它具有最佳的使用和节省成本:在按使用量付费的订阅模型中,计费是基于实际资源利用率的。采用自动扩展方法,实例将根据需求启动和关闭。因此,资源得到了最佳利用,从而节省成本。

  • 它优先考虑某些服务或服务组:通过自动扩展,可以优先考虑某些关键交易而不是低价值交易。这将通过从低价值服务中移除实例并重新分配给高价值服务来实现。这也将消除低优先级交易在高价值交易因资源不足而受阻时大量利用资源的情况。自动扩展的好处

例如,预订报告服务以两个实例运行,如前图所示。假设预订服务是一个收入生成服务,因此价值高于报告服务。如果对预订服务的需求更大,那么可以设置策略将一个报告服务从服务中移除,并释放此服务器供预订服务使用。

不同的自动扩展模型

自动扩展可以应用于应用程序级别或基础设施级别。简而言之,应用程序扩展是通过仅复制应用程序二进制文件进行扩展,而基础设施扩展是复制整个虚拟机,包括应用程序二进制文件。

应用程序的自动扩展

在这种情况下,扩展是通过复制微服务而不是底层基础设施(如虚拟机)来完成的。假设有一组可用于扩展微服务的 VM 或物理基础设施。这些 VM 具有基本镜像,以及诸如 JRE 之类的任何依赖项。还假设微服务在性质上是同质的。这样可以灵活地重用相同的虚拟或物理机器来运行不同的服务:

自动扩展应用程序

如前图所示,在场景 A中,VM3用于Service 1,而在场景 B中,相同的VM3用于Service 2。在这种情况下,我们只交换了应用程序库,而没有交换底层基础设施。

这种方法可以更快地实例化,因为我们只处理应用程序二进制文件,而不是底层的虚拟机。切换更容易更快,因为二进制文件体积较小,也不需要操作系统启动。然而,这种方法的缺点是,如果某些微服务需要操作系统级调整或使用多语言技术,那么动态交换微服务将不会有效。

云中的自动扩展

与前一种方法相比,在这种情况下,基础设施也是自动配置的。在大多数情况下,这将根据需求创建新的 VM 或销毁 VM:

自动扩展基础设施

如前图所示,保留实例是作为具有预定义服务实例的 VM 映像创建的。当对Service 1有需求时,VM3被移动到活动状态。当对Service 2有需求时,VM4被移动到活动状态。

如果应用程序依赖于基础设施级别的参数和库,例如操作系统,这种方法是有效的。此外,这种方法对于多语言微服务更好。缺点是 VM 镜像的重量级和启动新 VM 所需的时间。在这种情况下,与传统的重量级虚拟机相比,轻量级容器(如 Docker)更受青睐。

云中的自动扩展

弹性或自动扩展是大多数云提供商的基本功能之一。云提供商使用基础设施扩展模式,如前一节所讨论的。这些通常基于一组池化的机器。

例如,在 AWS 中,这是基于引入具有预定义 AMI 的新 EC2 实例。AWS 支持使用自动扩展组来进行自动扩展。每个组都设置了最小和最大数量的实例。AWS 确保在这些范围内根据需求进行实例扩展。在可预测的流量模式下,可以根据时间表配置预配。AWS 还提供了应用程序自定义自动扩展策略的能力。

Microsoft Azure 还支持根据 CPU、消息队列长度等资源利用率进行自动扩展。IBM Bluemix 支持根据 CPU 使用率进行自动扩展。

其他 PaaS 平台,如 CloudBees 和 OpenShift,也支持 Java 应用程序的自动扩展。Pivotal Cloud Foundry 通过 Pivotal Autoscale 支持自动扩展。扩展策略通常基于资源利用率,如 CPU 和内存阈值。

有一些组件在云顶部运行,并提供细粒度的控制来处理自动扩展。Netflix Fenzo、Eucalyptus、Boxfuse 和 Mesosphere 是这一类组件中的一些。

自动扩展方法

自动扩展是通过考虑不同的参数和阈值来处理的。在本节中,我们将讨论通常应用于决定何时扩展或缩小的不同方法和策略。

根据资源约束进行扩展

这种方法是基于通过监控机制收集的实时服务指标。通常,资源扩展方法是基于机器的 CPU、内存或磁盘做出决策。也可以通过查看服务实例本身收集的统计数据来实现,比如堆内存使用情况。

典型的策略可能是当机器的 CPU 利用率超过 60%时,启动另一个实例。同样,如果堆大小超过一定阈值,我们可以添加一个新实例。资源利用率低于设定阈值时,也可以缩减计算能力。这是通过逐渐关闭服务器来实现的:

受资源约束的扩展

在典型的生产场景中,不会在第一次阈值违规时创建额外的服务。最合适的方法是定义一个滑动窗口或等待期。

以下是一些例子:

  • 响应滑动窗口的一个例子是,如果特定交易的 60%响应时间在 60 秒的采样窗口中一直超过设定的阈值,就增加服务实例

  • CPU 滑动窗口中,如果 CPU 利用率在 5 分钟的滑动窗口中一直超过 70%,那么会创建一个新实例

  • 异常滑动窗口的一个例子是,如果在 60 秒的滑动窗口中有 80%的交易或连续 10 次执行导致特定系统异常,比如由于线程池耗尽而导致连接超时,那么会创建一个新的服务实例

在许多情况下,我们会将实际预期的阈值设定为较低的阈值。例如,不是将 CPU 利用率阈值设定为 80%,而是设定为 60%,这样系统有足够的时间来启动一个实例,而不会停止响应。同样,在缩减规模时,我们会使用比实际阈值更低的阈值。例如,我们将使用 40%的 CPU 利用率来缩减规模,而不是 60%。这样可以让我们有一个冷却期,以便在关闭实例时不会出现资源竞争。

基于资源的扩展也适用于服务级参数,如服务的吞吐量、延迟、应用程序线程池、连接池等。这些也可以是在应用程序级别,比如基于内部基准测试的服务实例中处理的销售订单数量。

特定时间段的扩展

基于时间的扩展是一种根据一天、一个月或一年的某些时段来扩展服务的方法,以处理季节性或业务高峰。例如,一些服务可能在办公时间内经历更多的交易,而在非办公时间内交易数量明显较少。在这种情况下,白天,服务会自动扩展以满足需求,并在非办公时间自动缩减:

特定时间段的扩展

全球许多机场对夜间着陆施加限制。因此,与白天相比,夜间在机场办理登机手续的乘客数量较少。因此,在夜间减少实例数量是成本有效的。

基于消息队列长度的扩展

当微服务基于异步消息传递时,这种方法特别有用。在这种方法中,当队列中的消息超过一定限制时,会自动添加新的消费者:

基于消息队列长度的扩展

这种方法是基于竞争消费者模式。在这种情况下,一组实例用于消费消息。根据消息阈值,会添加新实例来消费额外的消息。

基于业务参数的扩展

在这种情况下,增加实例是基于某些业务参数的,例如,在处理销售结束交易之前立即启动一个新实例。一旦监控服务接收到预先配置的业务事件(例如销售结束前 1 小时),将会预先启动一个新实例,以预期大量交易。这将根据业务规则提供基于细粒度控制的扩展:

基于业务参数的扩展

预测自动扩展

预测扩展是一种新的自动扩展范式,不同于传统的基于实时指标的自动扩展。预测引擎将采用多个输入,例如历史信息,当前趋势等,来预测可能的流量模式。根据这些预测进行自动扩展。预测自动扩展有助于避免硬编码规则和时间窗口。相反,系统可以自动预测这些时间窗口。在更复杂的部署中,预测分析可能使用认知计算机制来预测自动扩展。

在突发流量激增的情况下,传统的自动扩展可能无法帮助。在自动扩展组件能够对情况做出反应之前,激增已经发生并损害了系统。预测系统可以理解这些情况并在它们实际发生之前进行预测。一个例子是在计划的停机后立即处理一大堆请求。

Netflix Scryer 是这样一个系统的例子,它可以提前预测资源需求。

自动扩展 BrownField PSS 微服务

在本节中,我们将研究如何增强第五章中开发的微服务,使用 Spring Cloud 扩展微服务,以实现自动扩展。我们需要一个组件来监视某些性能指标并触发自动扩展。我们将称这个组件为生命周期管理器

服务生命周期管理器或应用程序生命周期管理器负责检测扩展需求并相应地调整实例数量。它负责动态启动和关闭实例。

在本节中,我们将研究一个原始的自动扩展系统,以了解基本概念,这将在后面的章节中得到增强。

自动扩展系统所需的功能

典型的自动扩展系统具有以下图表中显示的功能:

自动扩展系统所需的功能

在微服务的自动扩展生态系统中涉及的组件如下所述:

  • 微服务:这些是一组正在运行的微服务实例,它们不断发送健康和指标信息。或者,这些服务公开执行器端点以进行指标收集。在前面的图表中,这些被表示为微服务 1微服务 4

  • 服务注册表:服务注册表跟踪所有服务、它们的健康状态、它们的元数据和它们的端点 URI。

  • 负载均衡器:这是一个客户端负载均衡器,它查找服务注册表以获取有关可用服务实例的最新信息。

  • 生命周期管理器:生命周期管理器负责自动扩展,具有以下子组件:

  • 指标收集器:指标收集单元负责从所有服务实例收集指标。生命周期管理器将汇总这些指标。它还可以保持一个滑动时间窗口。这些指标可以是基础设施级别的指标,例如 CPU 使用率,也可以是应用程序级别的指标,例如每分钟的交易数。

  • 扩展策略:扩展策略只是指示何时扩展和缩小微服务的一组规则,例如,在 5 分钟的滑动时间窗口内,CPU 使用率超过 60%的 90%。

  • 决策引擎:决策引擎负责根据汇总的指标和扩展策略做出扩展或缩减的决策。

  • 部署规则:部署引擎使用部署规则来决定部署服务时要考虑哪些参数。例如,服务部署约束可能要求实例必须分布在多个可用区域,或者服务需要至少 4GB 的内存。

  • 部署引擎:基于决策引擎的决策,部署引擎可以启动或停止微服务实例,或通过改变服务的健康状态来更新注册表。例如,它将健康状态设置为“暂时停用”以暂时移除服务。

使用 Spring Boot 实现自定义生命周期管理器

本节介绍的生命周期管理器是一个最小实现,用于理解自动扩展的能力。在后面的章节中,我们将使用容器和集群管理解决方案来增强这个实现。Ansible、Marathon 和 Kubernetes 是一些有用的工具,用于构建这种能力。

在本节中,我们将使用 Spring Boot 为第五章中开发的服务实现一个应用级自动扩展组件,使用 Spring Cloud 扩展微服务

理解部署拓扑

以下图表显示了 BrownField PSS 微服务的示例部署拓扑:

理解部署拓扑

如图所示,有四台物理机器。从四台物理机器创建了八个虚拟机。每台物理机器能够承载两个虚拟机,每个虚拟机能够运行两个 Spring Boot 实例,假设所有服务具有相同的资源需求。

四台虚拟机VM1VM4是活动的,用于处理流量。VM5VM8被保留用于处理可扩展性。VM5VM6可以用于任何微服务,并且也可以根据扩展需求在微服务之间切换。冗余服务使用来自不同物理机器创建的虚拟机,以提高容错性。

我们的目标是在流量增加时使用四个虚拟机VM5VM8扩展任何服务,并在负载不足时缩减。我们解决方案的架构如下。

理解执行流程

请查看以下流程图:

理解执行流程

如前图所示,以下活动对我们很重要:

  • Spring Boot 服务代表了诸如搜索、预订、票价和办理登机等微服务。这些服务在启动时会自动将端点详细信息注册到 Eureka 注册表。这些服务启用了执行器,因此生命周期管理器可以从执行器端点收集指标。

  • 生命周期管理器服务实际上就是另一个 Spring Boot 应用程序。生命周期管理器具有一个指标收集器,它运行一个后台作业,定期轮询 Eureka 服务器,并获取所有服务实例的详细信息。然后,指标收集器调用 Eureka 注册表中注册的每个微服务的执行器端点,以获取健康和指标信息。在真实的生产场景中,采用订阅方法进行数据收集更好。

  • 通过收集的指标信息,生命周期管理器执行一系列策略,并根据这些策略决定是否扩展或缩减实例。这些决策要么是在特定虚拟机上启动特定类型的新服务实例,要么是关闭特定实例。

  • 在关闭的情况下,它使用执行器端点连接到服务器,并调用关闭服务来优雅地关闭一个实例。

  • 在启动新实例的情况下,生命周期管理器的部署引擎使用扩展规则并决定在哪里启动新实例以及启动实例时要使用的参数。然后,它使用 SSH 连接到相应的 VM。一旦连接,它通过传递所需的约束作为参数来执行预安装的脚本(或将此脚本作为执行的一部分)。此脚本从中央 Nexus 存储库中获取应用程序库,其中保存了生产二进制文件,并将其初始化为 Spring Boot 应用程序。端口号由生命周期管理器参数化。目标机器上需要启用 SSH。

在本例中,我们将使用TPM每分钟事务数)或RPM每分钟请求数)作为决策的采样指标。如果搜索服务的 TPM 超过 10,那么它将启动一个新的搜索服务实例。同样,如果 TPM 低于 2,其中一个实例将被关闭并释放回池中。

在启动新实例时,将应用以下策略:

  • 任何时候的服务实例数应该至少为 1,最多为 4。这也意味着至少一个服务实例将始终处于运行状态。

  • 定义了一个扩展组,以便在不同物理机器上创建一个新实例的 VM 上。这将确保服务在不同的物理机器上运行。

这些策略可以进一步增强。生命周期管理器理想情况下提供通过 REST API 或 Groovy 脚本自定义这些规则的选项。

生命周期管理器代码演示

我们将看一下如何实现一个简单的生命周期管理器。本节将演示代码,以了解生命周期管理器的不同组件。

提示

完整的源代码在代码文件中的第六章项目中可用。chapter5.configserverchapter5.eurekaserverchapter5.searchchapter5.search-apigateway分别复制并重命名为chapter6.*

执行以下步骤来实现自定义生命周期管理器:

  1. 创建一个新的 Spring Boot 应用程序,并将其命名为chapter6.lifecyclemanager。项目结构如下图所示:生命周期管理器代码演示

此示例的流程图如下图所示:

生命周期管理器代码演示

此图的组件在此处详细解释。

  1. 创建一个MetricsCollector类,其中包含以下方法。在 Spring Boot 应用程序启动时,将使用CommandLineRunner调用此方法,如下所示:
public void start(){
  while(true){ 
    eurekaClient.getServices().forEach(service -> {        System.out.println("discovered service "+ service);
      Map metrics = restTemplate.getForObject("http://"+service+"/metrics",Map.class);
      decisionEngine.execute(service, metrics);
    });  
  }    
}

前面的方法查找在 Eureka 服务器中注册的服务并获取所有实例。在现实世界中,实例应该发布指标到一个共同的地方,指标聚合将在那里发生,而不是轮询。

  1. 以下的DecisionEngine代码接受指标并应用特定的扩展策略来确定服务是否需要扩展:
  public boolean execute(String serviceId, Map metrics){
  if(scalingPolicies.getPolicy(serviceId).execute(serviceId, metrics)){    
      return deploymentEngine.scaleUp(deploymentRules.getDeploymentRules(serviceId), serviceId);  
    }
    return false;
  }
  1. 根据服务 ID,将挑选并应用与服务相关的策略。在这种情况下,TpmScalingPolicy中实现了最小 TPM 扩展策略,如下所示:
public class TpmScalingPolicy implements ScalingPolicy {
  public boolean execute(String serviceId, Map metrics){
    if(metrics.containsKey("gauge.servo.tpm")){
      Double tpm = (Double) metrics.get("gauge.servo.tpm");
      System.out.println("gauge.servo.tpm " + tpm);
      return (tpm > 10);
    }
    return false;
  }
}
  1. 如果策略返回trueDecisionEngine将调用DeploymentEngine来启动另一个实例。DeploymentEngine使用DeploymentRules来决定如何执行扩展。规则可以强制执行最小和最大实例数,在哪个区域或机器上启动新实例,新实例所需的资源等。DummyDeploymentRule只需确保最大实例数不超过 2。

  2. 在这种情况下,DeploymentEngine使用 JCraft 的JSchJava Secure Channel)库来 SSH 到目标服务器并启动服务。这需要以下额外的 Maven 依赖项:

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.53</version>
</dependency>
  1. 当前的 SSH 实现足够简单,因为我们将在未来的章节中更改它。在这个例子中,DeploymentEngine通过 SSH 库向目标机器发送以下命令:
 String command ="java -jar -Dserver.port=8091 ./work/codebox/chapter6/chapter6.search/target/search-1.0.jar";

与 Nexus 的集成是通过目标机器使用带有 Nexus CLI 的 Linux 脚本或使用curl来完成的。在这个例子中,我们不会探索 Nexus。

  1. 下一步是更改搜索微服务以公开一个新的 TPM 量规。我们必须更改之前开发的所有微服务以提交这个额外的指标。

本章我们只会检查搜索,但为了完成它,所有服务都必须更新。为了获得 gauge.servo.tpm 指标,我们必须在所有微服务中添加 TPMCounter

以下代码计算了一个滑动窗口内的交易次数:

class TPMCounter {
  LongAdder count;
  Calendar expiry = null; 
  TPMCounter(){
    reset();
  }  
  void reset (){
    count = new LongAdder();
    expiry = Calendar.getInstance();
    expiry.add(Calendar.MINUTE, 1);
  }
  boolean isExpired(){
    return Calendar.getInstance().after(expiry);
  }
  void increment(){
     if(isExpired()){
       reset();
     }
     count.increment();
  }
}
  1. 以下代码需要添加到SearchController中以设置tpm值:
class SearchRestController {
  TPMCounter tpm = new TPMCounter();
  @Autowired
  GaugeService gaugeService;
   //other code 
  1. 以下代码来自SearchRestController的 get REST 端点(搜索方法),它将tpm值作为量规提交给执行器端点:
tpm.increment();
gaugeService.submit("tpm", tpm.count.intValue()); 

运行生命周期管理器

执行以下步骤来运行前一节中开发的生命周期管理器:

  1. 编辑DeploymentEngine.java并更新密码以反映机器的密码,如下所示。这是 SSH 连接所需的:
session.setPassword("rajeshrv");
  1. 通过从根文件夹(第六章)运行 Maven 来构建所有项目,使用以下命令:
mvn -Dmaven.test.skip=true clean install

  1. 然后,按以下方式运行 RabbitMQ:
./rabbitmq-server

  1. 确保配置服务器指向正确的配置存储库。我们需要为生命周期管理器添加一个属性文件。

  2. 从各自的项目文件夹运行以下命令:

java -jar target/config-server-0.0.1-SNAPSHOT.jar
java -jar target/eureka-server-0.0.1-SNAPSHOT.jar
java -jar target/lifecycle-manager-0.0.1-SNAPSHOT.jar
java -jar target/search-1.0.jar
java -jar target/search-apigateway-1.0.jar
java -jar target/website-1.0.jar

  1. 一旦所有服务都启动了,打开浏览器窗口并加载 http://localhost:8001

  2. 连续执行 11 次航班搜索,在一分钟内依次执行。这将触发决策引擎实例化搜索微服务的另一个实例。

  3. 打开 Eureka 控制台(http://localhost:8761)并观察第二个SEARCH-SERVICE。一旦服务器启动,实例将如下所示出现:运行生命周期管理器

摘要

在本章中,您了解了在部署大规模微服务时自动缩放的重要性。

我们还探讨了自动缩放的概念以及自动缩放的不同模型和方法,例如基于时间、基于资源、基于队列长度和预测性的方法。然后我们审查了生命周期管理器在微服务环境中的作用并审查了它的能力。最后,我们通过审查一个简单的自定义生命周期管理器的示例实现来结束本章,该示例是在 BrownField PSS 微服务环境中。

自动缩放是处理大规模微服务时所需的重要支持能力。我们将在第九章中讨论生命周期管理器的更成熟的实现,使用 Mesos 和 Marathon 管理 Docker 化的微服务

下一章将探讨对于成功的微服务部署至关重要的日志记录和监控能力。

第七章:微服务的日志记录和监控

由于互联网规模微服务部署的分布式特性,最大的挑战之一是对单个微服务进行日志记录和监控。通过相关不同微服务发出的日志来跟踪端到端事务是困难的。与单片应用程序一样,没有单一的监控窗格来监视微服务。

本章将介绍微服务部署中日志记录和监控的必要性和重要性。本章还将进一步探讨解决日志记录和监控的挑战和解决方案,涉及多种潜在的架构和技术。

通过本章结束时,您将了解以下内容:

  • 日志管理的不同选项、工具和技术

  • 在跟踪微服务中使用 Spring Cloud Sleuth

  • 微服务端到端监控的不同工具

  • 使用 Spring Cloud Hystrix 和 Turbine 进行电路监控

  • 使用数据湖来实现业务数据分析

审查微服务能力模型

在本章中,我们将从第三章中讨论的微服务能力模型中探讨以下微服务能力:

  • 中央日志管理

  • 监控和仪表板

  • 依赖管理(监控和仪表板的一部分)

  • 数据湖

审查微服务能力模型

了解日志管理的挑战

日志只是来自运行进程的事件流。对于传统的 JEE 应用程序,有许多框架和库可用于日志记录。Java Logging(JUL)是 Java 本身提供的一个选项。Log4j、Logback 和 SLF4J 是其他一些流行的日志记录框架。这些框架支持 UDP 和 TCP 协议进行日志记录。应用程序将日志条目发送到控制台或文件系统。通常采用文件回收技术来避免日志填满所有磁盘空间。

日志处理的最佳实践之一是在生产环境中关闭大部分日志条目,因为磁盘 IO 的成本很高。磁盘 IO 不仅会减慢应用程序的速度,还会严重影响可伸缩性。将日志写入磁盘还需要高磁盘容量。磁盘空间不足的情况可能导致应用程序崩溃。日志框架提供了在运行时控制日志以限制打印内容的选项。这些框架大多提供对日志控制的细粒度控制。它们还提供在运行时更改这些配置的选项。

另一方面,如果适当分析,日志可能包含重要信息并具有很高的价值。因此,限制日志条目基本上限制了我们理解应用程序行为的能力。

从传统部署到云部署后,应用程序不再锁定到特定的预定义机器。虚拟机和容器不是与应用程序硬连接的。用于部署的机器可能会不时更改。此外,诸如 Docker 之类的容器是短暂的。这基本上意味着不能依赖磁盘的持久状态。一旦容器停止并重新启动,写入磁盘的日志就会丢失。因此,我们不能依赖本地机器的磁盘来写入日志文件。

正如我们在第一章中讨论的那样,解密微服务,十二要素应用程序的原则之一是避免应用程序自身路由或存储日志文件。在微服务的情况下,它们将在隔离的物理或虚拟机上运行,导致日志文件分散。在这种情况下,几乎不可能跟踪跨多个微服务的端到端事务:

了解日志管理的挑战

如图所示,每个微服务都会向本地文件系统发出日志。在这种情况下,微服务 M1 调用 M3。这些服务将它们的日志写入自己的本地文件系统。这使得难以关联和理解端到端的事务流。此外,如图所示,有两个 M1 的实例和两个 M2 的实例在两台不同的机器上运行。在这种情况下,很难实现对服务级别的日志聚合。

集中式日志解决方案

为了解决前面提到的挑战,传统的日志解决方案需要认真重新思考。新的日志解决方案除了解决前面提到的挑战外,还应该支持以下总结的能力:

  • 能够收集所有日志消息并对其进行分析

  • 能够关联和跟踪端到端的交易

  • 能够保留日志信息以进行趋势分析和预测的更长时间段

  • 消除对本地磁盘系统的依赖能力

  • 能够聚合来自多个来源的日志信息,如网络设备、操作系统、微服务等

解决这些问题的方法是集中存储和分析所有日志消息,而不管日志的来源是什么。新日志解决方案采用的基本原则是将日志存储和处理与服务执行环境分离。与在微服务执行环境中存储和处理大量日志消息相比,大数据解决方案更适合存储和处理大量日志消息。

在集中式日志解决方案中,日志消息将从执行环境发货到中央大数据存储。日志分析和处理将使用大数据解决方案进行处理:

集中式日志解决方案

如前面的逻辑图所示,集中式日志解决方案中有许多组件,如下所示:

  • 日志流:这些是源系统输出的日志消息流。源系统可以是微服务、其他应用程序,甚至是网络设备。在典型的基于 Java 的系统中,这相当于流式处理 Log4j 日志消息。

  • 日志发货人:日志发货人负责收集来自不同来源或端点的日志消息。然后,日志发货人将这些消息发送到另一组端点,例如写入数据库,推送到仪表板,或将其发送到流处理端点进行进一步的实时处理。

  • 日志存储:日志存储是存储所有日志消息以进行实时分析、趋势分析等的地方。通常,日志存储是一个能够处理大数据量的 NoSQL 数据库,例如 HDFS。

  • 日志流处理器:日志流处理器能够分析实时日志事件以进行快速决策。流处理器会采取行动,如向仪表板发送信息、发送警报等。在自愈系统的情况下,流处理器甚至可以采取行动来纠正问题。

  • 日志仪表板:仪表板是用于显示日志分析结果的单一窗格,如图表和图形。这些仪表板是为运营和管理人员准备的。

这种集中式方法的好处是没有本地 I/O 或阻塞磁盘写入。它也不使用本地机器的磁盘空间。这种架构在根本上类似于大数据处理的 Lambda 架构。

注意

要了解更多关于 Lambda 架构的信息,请访问lambda-architecture.net

每条日志消息中都需要有上下文、消息和关联 ID。上下文通常包括时间戳、IP 地址、用户信息、进程详细信息(如服务、类和函数)、日志类型、分类等。消息将是简单的自由文本信息。关联 ID 用于建立服务调用之间的链接,以便跨微服务的调用可以被追踪。

日志解决方案的选择

有多种选择可用于实现集中式日志记录解决方案。这些解决方案使用不同的方法、架构和技术。重要的是要了解所需的功能,并选择满足需求的正确解决方案。

云服务

有许多云日志服务可用,例如 SaaS 解决方案。

Loggly 是最受欢迎的基于云的日志服务之一。Spring Boot 微服务可以使用 Loggly 的 Log4j 和 Logback appender 直接将日志消息流式传输到 Loggly 服务中。

如果应用程序或服务部署在 AWS 上,AWS CloudTrail 可以与 Loggly 集成进行日志分析。

Papertrial、Logsene、Sumo Logic、Google Cloud Logging 和 Logentries 是其他基于云的日志解决方案的例子。

云日志服务通过提供简单易集成的服务,消除了管理复杂基础设施和大型存储解决方案的开销。然而,在选择云日志服务时,延迟是需要考虑的关键因素之一。

现成的解决方案

有许多专门设计的工具,可以在本地数据中心或云中安装,提供端到端的日志管理功能。

Graylog 是流行的开源日志管理解决方案之一。Graylog 使用 Elasticsearch 进行日志存储,使用 MongoDB 作为元数据存储。Graylog 还使用 GELF 库进行 Log4j 日志流式传输。

Splunk 是一种流行的商业工具,用于日志管理和分析。与其他解决方案使用日志流式传输相比,Splunk 使用日志文件传输方法来收集日志。

最佳集成

最后一种方法是选择最佳的组件并构建自定义的日志解决方案。

日志收集器

有一些日志收集器可以与其他工具结合使用,构建端到端的日志管理解决方案。不同的日志收集工具之间的功能有所不同。

Logstash 是一个强大的数据管道工具,可用于收集和传输日志文件。Logstash 充当代理,提供一种接受来自不同来源的流数据并将其同步到不同目的地的机制。Log4j 和 Logback appender 也可以用于将日志消息直接从 Spring Boot 微服务发送到 Logstash。Logstash 的另一端连接到 Elasticsearch、HDFS 或任何其他数据库。

Fluentd 是另一个与 Logstash 非常相似的工具,Logspout 也是如此,但后者更适合基于 Docker 容器的环境。

日志流处理器

流处理技术可选择用于即时处理日志流。例如,如果 404 错误持续作为对特定服务调用的响应发生,这意味着服务出现了问题。这种情况必须尽快处理。在这种情况下,流处理器非常有用,因为它们能够对传统的反应式分析无法处理的某些事件流做出反应。

用于流处理的典型架构是将 Flume 和 Kafka 与 Storm 或 Spark Streaming 结合在一起。Log4j 具有 Flume appender,用于收集日志消息。这些消息被推送到分布式 Kafka 消息队列中。流处理器从 Kafka 收集数据,并在发送到 Elasticsearch 和其他日志存储之前即时处理它们。

Spring Cloud Stream、Spring Cloud Stream 模块和 Spring Cloud Data Flow 也可用于构建日志流处理。

日志存储

实时日志消息通常存储在 Elasticsearch 中。Elasticsearch 允许客户端基于文本索引进行查询。除了 Elasticsearch,HDFS 也常用于存储归档的日志消息。MongoDB 或 Cassandra 用于存储月度聚合交易计数等摘要数据。离线日志处理可以使用 Hadoop 的 MapReduce 程序来完成。

仪表板

中央日志解决方案所需的最后一部分是仪表板。用于日志分析的最常用的仪表板是基于 Elasticsearch 数据存储的 Kibana。Graphite 和 Grafana 也用于显示日志分析报告。

自定义日志实现

之前提到的工具可以用来构建自定义端到端的日志解决方案。自定义日志管理最常用的架构是 Logstash、Elasticsearch 和 Kibana 的组合,也称为 ELK 堆栈。

注意

本章的完整源代码可在代码文件的“第七章”项目下找到。将chapter5.configserverchapter5.eurekaserverchapter5.searchchapter5.search-apigatewaychapter5.website复制到一个新的 STS 工作空间中,并将它们重命名为chapter7.*

以下图显示了日志监控流程:

自定义日志实现

在本节中,将研究使用 ELK 堆栈的自定义日志解决方案的简单实现。

按照以下步骤实现用于日志记录的 ELK 堆栈:

  1. www.elastic.co下载并安装 Elasticsearch、Kibana 和 Logstash。

  2. 更新 Search 微服务(chapter7.search)。审查并确保 Search 微服务中有一些日志语句。日志语句并不特别,只是使用slf4j进行简单的日志记录。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
  //other code goes here
  private static final Logger logger = LoggerFactory.getLogger(SearchRestController.class);
//other code goes here

logger.info("Looking to load flights...");
for (Flight flight : flightRepository.findByOriginAndDestinationAndFlightDate("NYC", "SFO", "22-JAN-16")) {
      logger.info(flight.toString());
}
  1. 在 Search 服务的pom.xml文件中添加logstash依赖项,以将logback集成到 Logstash 中,如下所示:
<dependency>
  <groupId>net.logstash.logback</groupId>
  <artifactId>logstash-logback-encoder</artifactId>
  <version>4.6</version>
</dependency>
  1. 此外,通过以下行将logback版本降级以与 Spring 1.3.5.RELEASE 兼容:
<logback.version>1.1.6</logback.version>
  1. 覆盖默认的 Logback 配置。可以通过在src/main/resources下添加一个新的logback.xml文件来完成,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>localhost:4560</destination>
        <!-- encoder is required -->
        <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
    </appender>
  <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="stash" />
  </root>
</configuration>

前面的配置通过添加一个新的 TCP 套接字appender来覆盖默认的 Logback 配置,该套接字将所有日志消息流式传输到在端口4560上监听的 Logstash 服务。重要的是要添加一个编码器,如前面的配置中所述。

  1. 创建如下代码所示的配置,并将其存储在logstash.conf文件中。该文件的位置不重要,因为在启动 Logstash 时将作为参数传递。此配置将从在4560端口上监听的套接字接收输入,并将输出发送到在9200端口上运行的 Elasticsearch。 stdout是可选的,并设置为 debug:
input {
  tcp {
     port => 4560
     host => localhost
  }
}
output {
elasticsearch { hosts => ["localhost:9200"] }
  stdout { codec => rubydebug }
}
  1. 从各自的安装文件夹运行 Logstash、Elasticsearch 和 Kibana,如下所示:
./bin/logstash -f logstash.conf
./bin/elasticsearch
./bin/kibana

  1. 运行 Search 微服务。这将调用单元测试用例,并导致打印前面提到的日志语句。

  2. 转到浏览器,访问 Kibana,网址为http://localhost:5601

  3. 转到“Settings” | “Configure an index pattern”,如下所示:自定义日志实现

  4. 转到“Discover”菜单查看日志。如果一切顺利,我们将看到 Kibana 截图如下。请注意,日志消息显示在 Kibana 屏幕上。

Kibana 提供了开箱即用的功能,可以使用日志消息构建摘要图表和图形:

自定义日志实现

使用 Spring Cloud Sleuth 进行分布式跟踪

前一节通过集中日志数据解决了微服务的分布式和碎片化日志问题。通过集中的日志解决方案,我们可以将所有日志存储在一个中央位置。然而,要跟踪端到端的事务仍然几乎是不可能的。为了进行端到端跟踪,跨越微服务的事务需要有一个相关 ID。

Twitter 的 Zipkin、Cloudera 的 HTrace 和 Google 的 Dapper 系统是分布式跟踪系统的例子。Spring Cloud 使用 Spring Cloud Sleuth 库在这些系统之上提供了一个包装组件。

分布式跟踪使用跨度跟踪的概念。跨度是一个工作单元;例如,调用一个服务由一个 64 位的跨度 ID 标识。一组跨度形成一个类似树状结构的跟踪。使用跟踪 ID,可以跟踪端到端的调用:

使用 Spring Cloud Sleuth 进行分布式跟踪

如图所示,微服务 1调用微服务 2微服务 2调用微服务 3。在这种情况下,如图所示,相同的跟踪 ID 在所有微服务之间传递,可以用来跟踪端到端的事务。

为了演示这一点,我们将使用搜索 API 网关和搜索微服务。必须在搜索 API 网关(chapter7.search-apigateway)中添加一个新的端点,该端点在内部调用搜索服务以返回数据。如果没有跟踪 ID,几乎不可能追踪或链接来自网站到搜索 API 网关到搜索微服务的调用。在这种情况下,只涉及两到三个服务,而在复杂的环境中,可能有许多相互依赖的服务。

按照以下步骤使用 Sleuth 创建示例:

  1. 更新搜索和搜索 API 网关。在此之前,需要将 Sleuth 依赖项添加到各自的 POM 文件中,可以通过以下代码完成:
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
  1. 在构建新服务的情况下,选择SleuthWeb,如下所示:使用 Spring Cloud Sleuth 进行分布式跟踪

  2. 在搜索服务以及 Logback 配置中添加 Logstash 依赖,如前面的示例所示。

  3. 接下来是在 Logback 配置中添加两个属性:

<property name="spring.application.name" value="search-service"/>
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [${spring.application.name}] [trace=%X{X-Trace-Id:-},span=%X{X-Span-Id:-}] [%15.15t] %-40.40logger{39}: %m%n"/>

第一个属性是应用程序的名称。在这里给出的名称是服务 ID:在搜索和搜索 API 网关中分别是search-servicesearch-apigateway。第二个属性是一个可选的模式,用于打印带有跟踪 ID 和跨度 ID 的控制台日志消息。前面的改变需要应用到两个服务中。

  1. 在 Spring Boot 应用程序类中添加以下代码片段,以指示 Sleuth 何时开始一个新的跨度 ID。在这种情况下,使用AlwaysSampler表示每次调用服务时都必须创建跨度 ID。这个改变需要应用在两个服务中:
  @Bean
    public AlwaysSampler defaultSampler() {
      return new AlwaysSampler();
    }
  1. 在搜索 API 网关中添加一个新的端点,该端点将调用搜索服务,如下所示。这是为了演示跟踪 ID 在多个微服务之间的传播。网关中的这个新方法通过调用搜索服务返回机场的操作中心,如下所示:
  @RequestMapping("/hubongw")
  String getHub(HttpServletRequest req){
    logger.info("Search Request in API gateway for getting Hub, forwarding to search-service ");
    String hub = restTemplate.getForObject("http://search-service/search/hub", String.class);
    logger.info("Response for hub received,  Hub "+ hub);
    return hub; 
  }
  1. 在搜索服务中添加另一个端点,如下所示:
  @RequestMapping("/hub")
  String getHub(){
    logger.info("Searching for Hub, received from search-apigateway ");
    return "SFO"; 
  }
  1. 添加后,运行两个服务。使用浏览器(http://localhost:8095/hubongw)在网关的新中心(/hubongw)端点上进行访问。

如前所述,搜索 API 网关服务运行在8095上,搜索服务运行在8090上。

  1. 查看控制台日志以查看打印的跟踪 ID 和跨度 ID。第一个打印来自搜索 API 网关,第二个来自搜索服务。请注意,在这两种情况下,跟踪 ID 都是相同的,如下所示:
2016-04-02 17:24:37.624 [search-apigateway] [trace=8a7e278f-7b2b-43e3-a45c-69d3ca66d663,span=8a7e278f-7b2b-43e3-a45c-69d3ca66d663] [io-8095-exec-10] c.b.p.s.a.SearchAPIGatewayController    : Response for hub received,  Hub DXB

2016-04-02 17:24:37.612 [search-service] [trace=8a7e278f-7b2b-43e3-a45c-69d3ca66d663,span=fd309bba-5b4d-447f-a5e1-7faaab90cfb1] [nio-8090-exec-1] c.b.p.search.component.SearchComponent  : Searching for Hub, received from search-apigateway
  1. 打开 Kibana 控制台并使用控制台中打印的跟踪 ID 搜索跟踪 ID。在这种情况下,它是 8a7e278f-7b2b-43e3-a45c-69d3ca66d663。如下面的截图所示,使用跟踪 ID,可以跟踪跨多个服务的服务调用:使用 Spring Cloud Sleuth 进行分布式跟踪

监控微服务

微服务是真正的分布式系统,具有流动的部署拓扑。如果没有复杂的监控系统,运维团队可能会在管理大规模微服务时遇到麻烦。传统的单片应用部署仅限于已知服务、实例、机器等。这比可能在不同机器上运行的大量微服务实例更容易管理。更复杂的是,这些服务会动态改变其拓扑。集中式日志记录能力只解决了问题的一部分。运维团队了解运行时部署拓扑和系统行为至关重要。这需要比集中式日志记录更多的东西。

一般应用程序监控更多是一组指标、聚合和它们对某些基线值的验证。如果有服务级别的违规,监控工具会生成警报并将其发送给管理员。对于数百甚至数千个相互连接的微服务,传统的监控实际上并没有真正提供真正的价值。在大规模微服务中实现一刀切的监控或使用单一视图监控所有东西并不容易实现。

微服务监控的主要目标之一是从用户体验的角度了解系统的行为。这将确保端到端的行为是一致的,并符合用户的预期。

监控挑战

与分散的日志记录问题类似,监控微服务的关键挑战在于微服务生态系统中有许多移动部分。

典型问题总结如下:

  • 统计数据和指标分散在许多服务、实例和机器中。

  • 可能会使用异构技术来实现微服务,这使得事情变得更加复杂。单一的监控工具可能无法提供所有所需的监控选项。

  • 微服务部署拓扑是动态的,无法预先配置服务器、实例和监控参数。

许多传统监控工具适用于监控单片应用程序,但在监控大规模、分布式、相互关联的微服务系统方面表现不佳。许多传统监控系统是基于代理的,需要在目标机器或应用程序实例上预先安装代理。这带来了两个挑战:

  • 如果代理需要与服务或操作系统进行深度集成,那么在动态环境中将很难管理。

  • 如果这些工具在监控或为应用程序进行仪器化时增加了开销,可能会导致性能问题

许多传统工具需要基线指标。这些系统使用预设规则,例如如果 CPU 利用率超过 60% 并保持在这个水平 2 分钟,那么应该向管理员发送警报。在大规模的互联网部署中,预先配置这些值非常困难。

新一代的监控应用程序通过自学习应用程序的行为并设置自动阈值值。这使管理员免于进行这种乏味的任务。自动基线有时比人类预测更准确:

监控挑战

如图所示,微服务监控的关键领域包括:

  • 指标来源和数据收集器:在源头进行指标收集,可以通过服务器将指标信息推送到中央收集器,也可以通过嵌入轻量级代理来收集信息。数据收集器从不同来源收集监控指标,如网络、物理机器、容器、软件组件、应用程序等。挑战在于使用自动发现机制而不是静态配置来收集这些数据。

这可以通过在源机器上运行代理、从源头流式传输数据或定期轮询来完成。

  • 指标的聚合和关联:需要聚合能力来聚合从不同来源收集的指标,如用户交易、服务、基础设施、网络等。聚合可能具有挑战性,因为它需要一定程度上理解应用程序的行为,如服务依赖关系、服务分组等。在许多情况下,这些是根据来源提供的元数据自动制定的。

通常,这是由一个中间人接受指标来完成的。

  • 处理指标和可操作见解:一旦数据被聚合,下一步就是进行测量。通常使用设定的阈值进行测量。在新一代监控系统中,这些阈值是自动发现的。监控工具然后分析数据并提供可操作的见解。

这些工具可能使用大数据和流分析解决方案。

  • 警报、操作和仪表板:一旦发现问题,就必须通知相关人员或系统。与传统系统不同,微服务监控系统应能够实时采取行动。积极的监控对于实现自愈至关重要。仪表板用于显示 SLA、KPI 等。

仪表板和警报工具能够满足这些要求。

微服务监控通常有三种方法。实际上,需要结合这些方法才能有效监控:

  • 应用性能监控APM):这更多地是一种传统的系统指标收集、处理、警报和仪表板呈现的方法。这些更多来自系统的角度。应用拓扑发现和可视化是许多 APM 工具实施的新功能。不同 APM 提供商之间的能力有所不同。

  • 合成监控:这是一种技术,用于使用在生产环境或类似生产环境中的多个测试场景进行端到端交易来监控系统的行为。收集数据以验证系统的行为和潜在热点。合成监控还有助于了解系统的依赖关系。

  • 实时用户监控RUM)或用户体验监控:这通常是一个基于浏览器的软件,记录真实用户的统计数据,如响应时间、可用性和服务水平。对于微服务,由于发布周期更频繁、拓扑结构更动态,用户体验监控更为重要。

监控工具

有许多工具可用于监控微服务。许多工具之间也存在重叠。监控工具的选择实际上取决于需要监控的生态系统。在大多数情况下,需要多个工具来监控整个微服务生态系统。

本节的目标是让我们熟悉一些常见的微服务友好的监控工具:

  • AppDynamics、Dynatrace 和 New Relic 是 Gartner Magic Quadrant 2015 年 APM 领域的顶级商业供应商。这些工具对微服务友好,可以在单个控制台中有效支持微服务监控。Ruxit、Datadog 和 Dataloop 是其他专为基本上友好的分布式系统而构建的商业产品。多个监控工具可以使用插件向 Datadog 提供数据。

  • 云供应商都有自己的监控工具,但在许多情况下,这些监控工具本身可能不足以进行大规模微服务监控。例如,AWS 使用 CloudWatch,Google Cloud Platform 使用 Cloud Monitoring 来收集来自各种来源的信息。

  • 一些数据收集库,如 Zabbix、statd、collectd、jmxtrans 等,以较低的级别收集运行时统计数据、指标、量规和计数。通常,这些信息被馈送到数据收集器和处理器,如 Riemann、Datadog 和 Librato,或者仪表板,如 Graphite。

  • Spring Boot Actuator 是收集微服务指标、量规和计数的好工具,正如我们在《使用 Spring Boot 构建微服务》的第二章中讨论的那样。Netflix Servo 是一种类似于 Actuator 的度量收集器,QBit 和 Dropwizard 度量也属于同一类度量收集器。所有这些度量收集器都需要聚合器和仪表板来促进全尺寸监控。

  • 通过日志进行监控是一种流行但不太有效的微服务监控方法。在这种方法中,正如在前一节中讨论的那样,日志消息从各种来源(如微服务、容器、网络等)传送到一个中央位置。然后,我们可以使用日志文件来跟踪交易、识别热点等。Loggly、ELK、Splunk 和 Trace 是这一领域的候选者。

  • Sensu 是开源社区中用于微服务监控的流行选择。Weave Scope 是另一个工具,主要针对容器化部署。Spigo 是一个专为微服务监控系统,与 Netflix 堆栈紧密结合。

  • Pingdom、New Relic Synthetics、Runscope、Catchpoint 等提供了在实时系统上进行合成交易监控和用户体验监控的选项。

  • Circonus 更多地被归类为 DevOps 监控工具,但也可以进行微服务监控。Nagios 是一种流行的开源监控工具,但更多地属于传统的监控系统。

  • Prometheus 提供了一个时间序列数据库和可视化 GUI,可用于构建自定义监控工具。

监控微服务的依赖关系

当有大量具有依赖关系的微服务时,重要的是要有一个监控工具,可以显示微服务之间的依赖关系。静态配置和管理这些依赖关系并不是一种可扩展的方法。有许多有用的工具可以监控微服务的依赖关系,如下所示:

  • 像 AppDynamics、Dynatrace 和 New Relic 这样的监控工具可以绘制微服务之间的依赖关系。端到端事务监控也可以跟踪事务依赖关系。其他监控工具,如 Spigo,也对微服务依赖管理很有用。

  • CMDB 工具,如 Device42 或专门的工具,如 Accordance,对于管理微服务的依赖关系非常有用。Veritas Risk Advisor(VRA)也对基础设施发现非常有用。

  • 使用图形数据库(如 Neo4j)进行自定义实现也是有用的。在这种情况下,微服务必须预先配置其直接和间接的依赖关系。在服务启动时,它会发布并与 Neo4j 数据库交叉检查其依赖关系。

Spring Cloud Hystrix 用于容错微服务

本节将探讨 Spring Cloud Hystrix 作为一种容错和延迟容忍微服务实现的库。Hystrix 基于失败快速快速恢复原则。如果服务出现问题,Hystrix 有助于隔离它。它通过回退到另一个预先配置的回退服务来快速恢复。Hystrix 是 Netflix 的另一个经过实战检验的库。Hystrix 基于断路器模式。

注意

msdn.microsoft.com/en-us/library/dn589784.aspx上阅读有关断路器模式的更多信息。

在本节中,我们将使用 Spring Cloud Hystrix 构建一个断路器。执行以下步骤来更改 Search API Gateway 服务,以将其与 Hystrix 集成:

  1. 更新 Search API Gateway 服务。为服务添加 Hystrix 依赖项。如果从头开始开发,请选择以下库:Spring Cloud Hystrix for fault-tolerant microservices

  2. 在 Spring Boot 应用程序类中,添加@EnableCircuitBreaker。这个命令将告诉 Spring Cloud Hystrix 为这个应用程序启用断路器。它还公开了用于指标收集的/hystrix.stream端点。

  3. 为 Search API Gateway 服务添加一个组件类,其中包含一个方法;在这种情况下,这是用@HystrixCommand注释的getHub。这告诉 Spring 这个方法容易失败。Spring Cloud 库包装这些方法以处理容错和延迟容忍,通过启用断路器。Hystrix 命令通常跟随一个回退方法。在失败的情况下,Hystrix 会自动启用提到的回退方法,并将流量转移到回退方法。如下面的代码所示,在这种情况下,getHub将回退到getDefaultHub

@Component  
class SearchAPIGatewayComponent { 
  @LoadBalanced
  @Autowired 
  RestTemplate restTemplate;

  @HystrixCommand(fallbackMethod = "getDefaultHub")
  public String getHub(){
    String hub = restTemplate.getForObject("http://search-service/search/hub", String.class);
    return hub;
  }
  public String getDefaultHub(){
    return "Possibily SFO";
  }
}
  1. SearchAPIGatewayControllergetHub方法调用SearchAPIGatewayComponentgetHub方法,如下所示:
@RequestMapping("/hubongw") 
String getHub(){
  logger.info("Search Request in API gateway for getting Hub, forwarding to search-service ");
  return component.getHub(); 
}
  1. 这个练习的最后一部分是构建一个 Hystrix 仪表板。为此,构建另一个 Spring Boot 应用程序。在构建此应用程序时,包括 Hystrix、Hystrix 仪表板和执行器。

  2. 在 Spring Boot 应用程序类中,添加@EnableHystrixDashboard注释。

  3. 启动 Search 服务、Search API Gateway 和 Hystrix 仪表板应用程序。将浏览器指向 Hystrix 仪表板应用程序的 URL。在本例中,Hystrix 仪表板在端口9999上启动。因此,打开 URLhttp://localhost:9999/hystrix

  4. 将显示类似于以下屏幕截图的屏幕。在 Hystrix 仪表板中,输入要监视的服务的 URL。

在这种情况下,Search API Gateway 正在端口8095上运行。因此,hystrix.stream的 URL 将是http://localhost:8095/hytrix.stream,如下所示:

Spring Cloud Hystrix for fault-tolerant microservices

  1. Hystrix 仪表板将显示如下:Spring Cloud Hystrix for fault-tolerant microservices

提示

请注意,至少必须执行一个事务才能看到显示。这可以通过访问http://localhost:8095/hubongw来实现。

  1. 通过关闭 Search 服务创建一个故障场景。请注意,当访问 URLhttp://localhost:8095/hubongw时,将调用回退方法。

  2. 如果连续失败,则断路器状态将更改为打开。这可以通过多次访问上述 URL 来实现。在打开状态下,原始服务将不再被检查。Hystrix 仪表板将显示断路器的状态为打开,如下面的屏幕截图所示。一旦断路器打开,系统将定期检查原始服务的状态以进行恢复。当原始服务恢复时,断路器将恢复到原始服务,并且状态将设置为关闭Spring Cloud Hystrix for fault-tolerant microservices

注意

要了解每个参数的含义,请访问 Hystrix wiki github.com/Netflix/Hystrix/wiki/Dashboard

使用 Turbine 聚合 Hystrix 流

在上一个示例中,我们的微服务的/hystrix.stream端点在 Hystrix 仪表板中给出。Hystrix 仪表板一次只能监视一个微服务。如果有许多微服务,则 Hystrix 仪表板指向的服务必须每次切换要监视的微服务时更改。一次只查看一个实例是很繁琐的,特别是当有多个微服务实例或多个微服务时。

我们必须有一种机制来聚合来自多个/hystrix.stream实例的数据,并将其合并成单个仪表板视图。Turbine 正是这样做的。Turbine 是另一个服务器,它从多个实例收集 Hystrix 流,并将它们合并成一个/turbine.stream实例。现在,Hystrix 仪表板可以指向/turbine.stream以获取合并信息:

使用 Turbine 聚合 Hystrix 流

提示

Turbine 目前仅适用于不同的主机名。每个实例必须在单独的主机上运行。如果您在同一主机上本地测试多个服务,则更新主机文件(/etc/hosts)以模拟多个主机。完成后,必须配置bootstrap.properties如下:

eureka.instance.hostname: localdomain2

此示例展示了如何使用 Turbine 监视多个实例和服务之间的断路器。在此示例中,我们将使用搜索服务和搜索 API 网关。Turbine 内部使用 Eureka 来解析配置用于监视的服务 ID。

执行以下步骤来构建和执行此示例:

  1. Turbine 服务器可以作为另一个 Spring Boot 应用程序创建,使用 Spring Boot Starter 选择 Turbine 以包括 Turbine 库。

  2. 创建应用程序后,在主 Spring Boot 应用程序类中添加@EnableTurbine。在此示例中,Turbine 和 Hystrix 仪表板都配置为在同一个 Spring Boot 应用程序上运行。通过向新创建的 Turbine 应用程序添加以下注释,可以实现这一点:

@EnableTurbine
@EnableHystrixDashboard
@SpringBootApplication
public class TurbineServerApplication {
  1. 将以下配置添加到.yaml或属性文件中,以指向我们感兴趣监视的实例:
spring:
   application:
     name : turbineserver
turbine:
   clusterNameExpression: new String('default')
   appConfig : search-service,search-apigateway
server:
  port: 9090
eureka:
  client:
    serviceUrl:
       defaultZone: http://localhost:8761/eureka/
  1. 上述配置指示 Turbine 服务器查找 Eureka 服务器以解析search-servicesearch-apigateway服务。search-servicesearch-apigateways服务 ID 用于向 Eureka 注册服务。Turbine 使用这些名称通过与 Eureka 服务器检查来解析实际的服务主机和端口。然后,它将使用此信息从每个实例中读取/hystrix.stream。Turbine 然后读取所有单独的 Hystrix 流,将它们聚合,并在 Turbine 服务器的/turbine.stream URL 下公开它们。

  2. 集群名称表达式指向默认集群,因为在此示例中没有进行显式集群配置。如果手动配置了集群,则必须使用以下配置:

turbine:
  aggregator:
    clusterConfig: [comma separated clusternames]
  1. 将搜索服务的SearchComponent更改为添加另一个断路器,如下所示:
  @HystrixCommand(fallbackMethod = "searchFallback")
  public List<Flight> search(SearchQuery query){
  1. 此外,在搜索服务的主应用程序类中添加@EnableCircuitBreaker

  2. 将以下配置添加到搜索服务的bootstrap.properties中。这是因为所有服务都在同一主机上运行:

Eureka.instance.hostname: localdomain1
  1. 同样,在搜索 API 网关服务的bootstrap.properties中添加以下内容。这是为了确保两个服务使用不同的主机名:
eureka.instance.hostname: localdomain2
  1. 在此示例中,我们将运行两个search-apigateway实例:一个在localdomain1:8095上,另一个在localdomain2:8096上。我们还将在localdomain1:8090上运行一个search-service实例。

  2. 使用命令行覆盖运行微服务以管理不同的主机地址,如下所示:

java -jar -Dserver.port=8096 -Deureka.instance.hostname=localdomain2 -Dserver.address=localdomain2 target/chapter7.search-apigateway-1.0.jar
java -jar -Dserver.port=8095 -Deureka.instance.hostname=localdomain1 -Dserver.address=localdomain1 target/chapter7.search-apigateway-1.0.jar
java -jar -Dserver.port=8090 -Deureka.instance.hostname=localdomain1 -Dserver.address=localdomain1 target/chapter7.search-1.0.jar

  1. 通过将浏览器指向http://localhost:9090/hystrix来打开 Hystrix 仪表板。

  2. 与其给出/hystrix.stream,这次我们将指向/turbine.stream。在这个例子中,Turbine 流正在9090上运行。因此,在 Hystrix 仪表板中要给出的 URL 是http://localhost:9090/turbine.stream

  3. 通过打开浏览器窗口并访问以下两个 URL 来触发一些事务:http://localhost:8095/hubongwhttp://localhost:8096/hubongw

完成后,仪表板页面将显示getHub服务。

  1. 运行chapter7.website。使用网站http://localhost:8001执行搜索事务。

在执行前面的搜索之后,仪表板页面将显示search-service。如下截图所示:

使用 Turbine 聚合 Hystrix 流

正如我们在仪表板中所看到的,search-service来自 Search 微服务,而getHub来自 Search API 网关。由于我们有两个 Search API 网关的实例,getHub来自两个主机,由Hosts 2表示。

使用数据湖进行数据分析

与分段日志和监控的情景类似,分段数据是微服务架构中的另一个挑战。分段数据在数据分析中带来了挑战。这些数据可能用于简单的业务事件监控、数据审计,甚至从数据中推导出业务智能。

数据湖或数据中心是处理这种情况的理想解决方案。事件源架构模式通常用于将状态和状态变化作为事件与外部数据存储共享。当状态发生变化时,微服务将状态变化作为事件发布。感兴趣的各方可以订阅这些事件,并根据自己的需求进行处理。中央事件存储也可以订阅这些事件,并将它们存储在大数据存储中进行进一步分析。

常用的数据处理架构如下图所示:

使用数据湖进行数据分析

从微服务生成的状态变化事件——在我们的案例中是SearchBookingCheck-In事件——被推送到分布式高性能消息系统,如 Kafka。数据摄取服务,如 Flume,可以订阅这些事件并将其更新到 HDFS 集群中。在某些情况下,这些消息将通过 Spark Streaming 实时处理。为了处理事件的异构来源,Flume 也可以在事件源和 Kafka 之间使用。

Spring Cloud Streams、Spring Cloud Streams 模块和 Spring Data Flow 也是用于高速数据摄取的替代方案。

总结

在本章中,您了解了处理互联网规模微服务时日志记录和监控所面临的挑战。

我们探讨了集中式日志记录的各种解决方案。您还了解了如何使用 Elasticsearch、Logstash 和 Kibana(ELK)实现自定义集中式日志记录。为了理解分布式跟踪,我们使用 Spring Cloud Sleuth 升级了 BrownField 微服务。

在本章的后半部分,我们深入探讨了微服务监控解决方案所需的能力以及监控的不同方法。随后,我们检查了一些可用于微服务监控的工具。

通过 Spring Cloud Hystrix 和 Turbine 进一步增强了 BrownField 微服务,以监控服务间通信的延迟和故障。示例还演示了如何使用断路器模式在发生故障时回退到另一个服务。

最后,我们还提到了数据湖的重要性以及如何在微服务环境中集成数据湖架构。

微服务管理是我们在处理大规模微服务部署时需要解决的另一个重要挑战。下一章将探讨容器如何帮助简化微服务管理。

第八章:使用 Docker 容器化微服务

在微服务的上下文中,容器化部署是锦上添花。它通过自包含底层基础设施来帮助微服务更加自治,从而使微服务与云中立。

本章将介绍虚拟机镜像的概念和相关性,以及微服务的容器化部署。然后,本章将进一步使读者熟悉使用 Spring Boot 和 Spring Cloud 开发的 BrownField PSS 微服务构建 Docker 镜像。最后,本章还将介绍如何在类生产环境中管理、维护和部署 Docker 镜像。

通过本章结束时,您将了解以下内容:

  • 容器化概念及其在微服务上下文中的相关性

  • 构建和部署微服务作为 Docker 镜像和容器

  • 以 AWS 作为基于云的 Docker 部署的示例

审查微服务能力模型

在本章中,我们将探讨第三章中讨论的微服务能力模型中的以下微服务能力:

  • 容器和虚拟机

  • 私有/公共云

  • 微服务仓库

该模型如下图所示:

审查微服务能力模型

了解 BrownField PSS 微服务中的空白

在第五章使用 Spring Cloud 扩展微服务中,BrownField PSS 微服务使用 Spring Boot 和 Spring Cloud 开发。这些微服务部署为版本化的 fat JAR 文件,特别是在本地开发机器上的裸金属上。

在第六章微服务自动扩展中,通过自定义生命周期管理器添加了自动扩展能力。在第七章日志和监控微服务中,通过集中日志和监控解决方案解决了围绕日志和监控的挑战。

我们的 BrownField PSS 实施仍然存在一些空白。到目前为止,该实施尚未使用任何云基础设施。专用机器,如传统的单片应用部署,不是部署微服务的最佳解决方案。自动化,如自动配置、按需扩展、自助服务和基于使用量的付款,是管理大规模微服务部署所需的基本能力。一般来说,云基础设施提供所有这些基本能力。因此,具有前述能力的私有或公共云更适合部署互联网规模的微服务。

此外,在裸金属上运行一个微服务实例并不划算。因此,在大多数情况下,企业最终会在单个裸金属服务器上部署多个微服务。在单个裸金属上运行多个微服务可能会导致“吵闹的邻居”问题。在同一台机器上运行的微服务实例之间没有隔离。因此,部署在单台机器上的服务可能会占用其他服务的空间,从而影响其性能。

另一种方法是在虚拟机上运行微服务。然而,虚拟机的性能较重。因此,在物理机上运行许多较小的虚拟机并不高效。这通常会导致资源浪费。在共享虚拟机以部署多个服务的情况下,我们将面临与前述共享裸金属相同的问题。

在基于 Java 的微服务的情况下,共享 VM 或裸机来部署多个微服务也会导致在微服务之间共享 JRE。这是因为在我们的 BrownField PSS 抽象中创建的 fat JAR 仅包含应用程序代码及其依赖项,而不包括 JRE。在安装在机器上的 JRE 上进行任何更新都会对部署在该机器上的所有微服务产生影响。同样,如果特定微服务需要 OS 级参数、库或调整,则在共享环境中很难对其进行管理。

一个微服务原则坚持认为它应该是自包含的,并通过完全封装其端到端运行时环境来实现自主性。为了符合这一原则,所有组件,如操作系统、JRE 和微服务二进制文件,都必须是自包含和隔离的。实现这一点的唯一选择是遵循每个 VM 部署一个微服务的方法。然而,这将导致虚拟机的利用率不足,并且在许多情况下,由于这种情况而产生的额外成本可能会抵消微服务的好处。

什么是容器?

容器并不是革命性的、开创性的概念。它们已经实践了相当长的时间。然而,由于广泛采用云计算,世界正在见证容器的重新进入。传统虚拟机在云计算领域的缺陷也加速了容器的使用。像Docker这样的容器提供商大大简化了容器技术,这也使得容器技术在当今世界得到了广泛的应用。最近 DevOps 和微服务的流行也促成了容器技术的重生。

那么,什么是容器?容器在操作系统之上提供了私有空间。这种技术也被称为操作系统虚拟化。在这种方法中,操作系统的内核提供了隔离的虚拟空间。这些虚拟空间中的每一个被称为一个容器或虚拟引擎VE)。容器允许进程在主机操作系统之上的隔离环境中运行。多个容器在同一主机上运行的表示如下:

什么是容器?

容器是构建、运输和运行组件化软件的简单机制。通常,容器打包了运行应用程序所必需的所有二进制文件和库。容器保留自己的文件系统、IP 地址、网络接口、内部进程、命名空间、操作系统库、应用程序二进制文件、依赖项和其他应用程序配置。

组织使用数十亿个容器。此外,许多大型组织都在大力投资容器技术。Docker 遥遥领先于竞争对手,得到了许多大型操作系统供应商和云提供商的支持。LmctfySystemdNspawnRocketDrawbridgeLXDKurmaCalico是其他一些容器化解决方案。开放容器规范也正在开发中。

VM 和容器之间的区别

几年前,Hyper-VVMWareZen等 VM 是数据中心虚拟化的热门选择。企业通过实施虚拟化而节省了成本,而不是传统的裸机使用。它还帮助许多企业以更加优化的方式利用其现有基础设施。由于 VM 支持自动化,许多企业发现他们在虚拟机上的管理工作更少。虚拟机还帮助组织获得应用程序运行的隔离环境。

乍一看,虚拟化和容器化表现出完全相同的特征。然而,总的来说,容器和虚拟机并不相同。因此,在虚拟机和容器之间进行苹果对苹果的比较是不公平的。虚拟机和容器是两种不同的技术,解决虚拟化的不同问题。这种差异可以从以下图表中看出:

虚拟机和容器之间的区别

与容器相比,虚拟机的操作级别要低得多。虚拟机提供硬件虚拟化,如 CPU、主板、内存等。虚拟机是一个独立的单元,内嵌操作系统,通常称为客户操作系统。虚拟机复制整个操作系统,并在虚拟机内部运行,不依赖于主机操作系统环境。由于虚拟机嵌入了完整的操作系统环境,因此它们在性质上比较笨重。这既是优势也是劣势。优势在于虚拟机为在虚拟机上运行的进程提供了完全隔离。劣势在于它限制了在裸机上启动虚拟机的数量,因为虚拟机的资源需求。

虚拟机的大小直接影响其启动和停止时间。由于启动虚拟机会启动操作系统,因此虚拟机的启动时间通常较长。虚拟机更适合基础设施团队,因为管理虚拟机需要较低水平的基础设施能力。

在容器世界中,容器不会模拟整个硬件或操作系统。与虚拟机不同,容器共享主机内核和操作系统的某些部分。在容器的情况下,没有客户操作系统的概念。容器在主机操作系统的顶部直接提供了一个隔离的执行环境。这既是它的优势也是劣势。优势在于它更轻,更快。由于同一台机器上的容器共享主机操作系统,容器的整体资源利用率相当小。因此,与笨重的虚拟机相比,可以在同一台机器上运行许多较小的容器。由于同一主机上的容器共享主机操作系统,也存在一些限制。例如,在容器内部无法设置 iptables 防火墙规则。容器内的进程与在同一主机上运行的不同容器的进程完全独立。

与虚拟机不同,容器镜像在社区门户网站上是公开可用的。这使得开发人员的生活变得更加轻松,因为他们不必从头开始构建镜像;相反,他们现在可以从认证来源获取基础镜像,并在下载的基础镜像上添加额外的软件组件层。

容器的轻量化特性也为自动化构建、发布、下载、复制等提供了大量机会。通过几个命令下载、构建、运行容器或使用 REST API 使容器更加适合开发人员。构建一个新的容器不会超过几秒钟。容器现在也是持续交付流水线的一部分。

总之,容器相对于虚拟机有许多优势,但虚拟机也有其独特的优势。许多组织同时使用容器和虚拟机,例如在虚拟机上运行容器。

容器的优势

我们已经考虑了容器相对于虚拟机的许多优势。本节将解释容器的整体优势,超越虚拟机的优势:

  • 自包含:容器将必要的应用程序二进制文件和它们的依赖项打包在一起,以确保在开发、测试或生产等不同环境之间没有差异。这促进了十二要素应用程序和不可变容器的概念。Spring Boot 微服务捆绑了所有必需的应用程序依赖项。容器通过嵌入 JRE 和其他操作系统级别的库、配置等,进一步扩展了这一边界。

  • 轻量级:总的来说,容器体积小,占用空间少。最小的容器 Alpine 大小不到 5MB。使用 Alpine 容器和 Java 8 打包的最简单的 Spring Boot 微服务只有大约 170MB 的大小。虽然大小仍然偏大,但比通常几 GB 的 VM 镜像要小得多。容器的较小占用空间不仅有助于快速启动新容器,还使构建、部署和存储更加容易。

  • 可扩展:由于容器镜像体积小,在启动时没有操作系统引导,容器通常更快地启动和关闭。这使得容器成为云友好的弹性应用程序的热门选择。

  • 可移植:容器在不同机器和云提供商之间提供可移植性。一旦容器构建完成所有依赖项,它们可以在多台机器或多个云提供商之间移植,而不依赖于底层机器。容器可以从桌面移植到不同的云环境。

  • 较低的许可成本:许多软件许可条款是基于物理核心的。由于容器共享操作系统,并且在物理资源级别上没有虚拟化,因此在许可成本方面具有优势。

  • DevOps:容器的轻量级占用空间使得容易自动化构建,并从远程存储库发布和下载容器。这使得在敏捷和 DevOps 环境中易于使用,通过与自动交付流水线集成。容器还支持“构建一次”的概念,通过在构建时创建不可变容器,并在多个环境之间移动它们。由于容器并不深入基础设施,多学科的 DevOps 团队可以将容器作为日常生活的一部分进行管理。

  • 版本控制:容器默认支持版本。这有助于构建有版本的工件,就像有版本的存档文件一样。

  • 可重用:容器镜像是可重用的工件。如果一个镜像是通过组装一些库来实现某个目的,它可以在类似的情况下被重复使用。

  • 不可变的容器:在这个概念中,容器在使用后被创建和销毁。它们永远不会被更新或打补丁。不可变的容器在许多环境中被使用,以避免部署单元的补丁复杂性。打补丁会导致无法追踪和无法一致地重新创建环境。

微服务和容器

微服务和容器之间没有直接关系。微服务可以在没有容器的情况下运行,容器可以运行单片应用程序。然而,微服务和容器之间存在一个甜蜜点。

容器适用于单片应用程序,但单片应用程序的复杂性和大小可能会削弱容器的一些优势。例如,使用单片应用程序可能不容易快速启动新容器。除此之外,单片应用程序通常具有本地环境依赖,如本地磁盘、与其他系统的独立依赖等。这些应用程序很难通过容器技术进行管理。这就是微服务与容器相辅相成的地方。

以下图表显示了在同一主机上运行的三个多语言微服务,并共享相同的操作系统,但抽象了运行时环境:

微服务和容器

当管理许多多语言微服务时,容器的真正优势可以看出来,例如,一个微服务用 Java 编写,另一个微服务用 Erlang 或其他语言编写。容器帮助开发人员以平台和技术无关的方式打包任何语言或技术编写的微服务,并统一分布到多个环境中。容器消除了处理多语言微服务的不同部署管理工具的需求。容器不仅抽象了执行环境,还抽象了如何访问服务。无论使用何种技术,容器化的微服务都会暴露 REST API。一旦容器启动运行,它就会绑定到某些端口并暴露其 API。由于容器是自包含的,并在服务之间提供完全的堆栈隔离,在单个 VM 或裸金属上,可以以统一的方式运行多个异构微服务并处理它们。

Docker 简介

前面的部分讨论了容器及其优势。容器已经在业界使用多年,但 Docker 的流行使容器有了新的前景。因此,许多容器定义和观点都源自 Docker 架构。Docker 如此受欢迎,以至于容器化甚至被称为dockerization

Docker 是一个基于 Linux 内核构建、运输和运行轻量级容器的平台。Docker 默认支持 Linux 平台。它还支持 Mac 和 Windows,使用Boot2Docker,它运行在 Virtual Box 之上。

亚马逊EC2 容器服务ECS)在 AWS EC2 实例上对 Docker 有开箱即用的支持。Docker 可以安装在裸金属上,也可以安装在传统的虚拟机上,如 VMWare 或 Hyper-V。

Docker 的关键组件

Docker 安装有两个关键组件:Docker 守护程序Docker 客户端。Docker 守护程序和 Docker 客户端都作为单个二进制文件分发。

以下图表显示了 Docker 安装的关键组件:

Docker 的关键组件

Docker 守护程序

Docker 守护程序是运行在主机上的服务器端组件,负责构建、运行和分发 Docker 容器。Docker 守护程序暴露 API 供 Docker 客户端与守护程序交互。这些 API 主要是基于 REST 的端点。可以想象 Docker 守护程序是运行在主机上的控制器服务。开发人员可以以编程方式使用这些 API 来构建自定义客户端。

Docker 客户端

Docker 客户端是一个远程命令行程序,通过套接字或 REST API 与 Docker 守护程序进行交互。CLI 可以在与守护程序相同的主机上运行,也可以在完全不同的主机上运行,并远程连接到守护程序。Docker 用户使用 CLI 构建、运输和运行 Docker 容器。

Docker 概念

Docker 架构围绕着一些概念构建:镜像、容器、注册表和 Dockerfile。

Docker 镜像

Docker 的一个关键概念是镜像。Docker 镜像是操作系统库、应用程序及其库的只读副本。一旦创建了镜像,它就保证在任何 Docker 平台上运行而不需要修改。

在 Spring Boot 微服务中,Docker 镜像打包了操作系统,如 Ubuntu、Alpine、JRE 和 Spring Boot fat 应用程序 JAR 文件。它还包括运行应用程序和暴露服务的指令:

Docker 镜像

如图所示,Docker 镜像基于分层架构,其中基本镜像是 Linux 的各种版本之一。如前图所示,每个层都添加到具有前一个镜像作为父层的基本镜像层中。Docker 使用联合文件系统的概念将所有这些层组合成单个镜像,形成单个文件系统。

在典型情况下,开发人员不会从头开始构建 Docker 镜像。操作系统的镜像,或其他常见的库,如 Java 8 镜像,都可以从可信任的来源公开获取。开发人员可以在这些基本镜像的基础上开始构建。在 Spring 微服务中,基本镜像可以是 JRE 8,而不是从 Ubuntu 等 Linux 发行版镜像开始。

每次重新构建应用程序时,只有更改的层会被重新构建,其余层保持不变。所有中间层都被缓存,因此,如果没有更改,Docker 会使用先前缓存的层并在其上构建。在同一台机器上运行具有相同类型基本镜像的多个容器将重用基本镜像,从而减小部署的大小。例如,在主机上,如果有多个使用 Ubuntu 作为基本镜像运行的容器,它们都会重用相同的基本镜像。这也适用于发布或下载镜像时:

Docker 镜像

如图所示,图像中的第一层是称为bootfs的引导文件系统,类似于 Linux 内核和引导加载程序。引导文件系统充当所有图像的虚拟文件系统。

在引导文件系统之上,放置了操作系统文件系统,称为rootfs。根文件系统向容器添加了典型的操作系统目录结构。与 Linux 系统不同,在 Docker 的情况下,rootfs处于只读模式。

根据需求,其他所需的镜像被放置在rootfs之上。在我们的情况下,这些是 JRE 和 Spring Boot 微服务的 JAR 文件。当容器被初始化时,会在所有其他文件系统之上放置一个可写文件系统以供进程运行。进程对底层文件系统所做的任何更改都不会反映在实际容器中。相反,这些更改会被写入可写文件系统。这个可写文件系统是易失性的。因此,一旦容器停止,数据就会丢失。因此,Docker 容器是短暂的。

Docker 内部打包的基本操作系统通常是 OS 文件系统的最小副本。实际上,运行在其上的进程可能并不使用整个 OS 服务。在 Spring Boot 微服务中,很多情况下,容器只是启动一个 CMD 和 JVM,然后调用 Spring Boot 的 fat JAR。

Docker 容器

Docker 容器是 Docker 镜像的运行实例。容器在运行时使用主机操作系统的内核。因此,它们与在同一主机上运行的其他容器共享主机内核。Docker 运行时确保容器进程使用内核功能(如cgroups和操作系统的内核namespace)分配其自己的隔离进程空间。除了资源隔离,容器还有自己的文件系统和网络配置。

实例化的容器可以具有特定的资源分配,如内存和 CPU。从相同镜像初始化的容器可以具有不同的资源分配。Docker 容器默认获得独立的子网网关。网络有三种模式。

Docker 注册表

Docker 注册表是 Docker 镜像发布和下载的中心位置。URL hub.docker.com是 Docker 提供的中央注册表。Docker 注册表有公共镜像,可以下载并用作基本注册表。Docker 还有私有镜像,专门针对在 Docker 注册表中创建的帐户。Docker 注册表的截图如下所示:

Docker 注册表

Docker 还提供Docker Trusted Registry,可用于在本地部署注册表。

Dockerfile

Dockerfile 是一个包含构建 Docker 镜像的指令的构建或脚本文件。Dockerfile 中可以记录多个步骤,从获取基本镜像开始。Dockerfile 是一个通常命名为 Dockerfile 的文本文件。docker build命令查找 Dockerfile 以获取构建指令。可以将 Dockerfile 比作 Maven 构建中使用的pom.xml文件。

在 Docker 中部署微服务

本节将通过展示如何为我们的 BrownField PSS 微服务构建容器来实现我们的学习。

注意

本章的完整源代码可在代码文件的第八章项目中找到。将chapter7.configserverchapter7.eurekaserverchapter7.searchchapter7.search-apigatewaychapter7.website复制到新的 STS 工作区,并将它们重命名为chapter8.*

执行以下步骤来构建 BrownField PSS 微服务的 Docker 容器:

  1. 从官方 Docker 网站www.docker.com安装 Docker。

按照所选操作系统的下载和安装说明,使用开始链接。安装后,使用以下命令验证安装:

$docker –version
Docker version 1.10.1, build 9e83765

  1. 在本节中,我们将看看如何将Search(chapter8.search)微服务,Search API Gateway(chapter8.search-apigateway)微服务和Website(chapter8.website) Spring Boot 应用程序 docker 化。

  2. 在进行任何更改之前,我们需要编辑bootstrap.properties,将配置服务器 URL 从 localhost 更改为 IP 地址,因为在 Docker 容器内无法解析 localhost。在现实世界中,这将指向 DNS 或负载均衡器,如下所示:

spring.cloud.config.uri=http://192.168.0.105:8888

注意

用您的机器的 IP 地址替换 IP 地址。

  1. 同样,在 Git 存储库上编辑search-service.properties,将 localhost 更改为 IP 地址。这适用于 Eureka URL 以及 RabbitMQ URL。更新后提交回 Git。您可以通过以下代码执行此操作:
spring.application.name=search-service
spring.rabbitmq.host=192.168.0.105
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
orginairports.shutdown:JFK
eureka.client.serviceUrl.defaultZone: http://192.168.0.105:8761/eureka/
spring.cloud.stream.bindings.inventoryQ=inventoryQ
  1. 通过取消注释以下行来更改 RabbitMQ 配置文件rabbitmq.config,以提供对 guest 的访问。默认情况下,guest 只能从本地主机访问:
    {loopback_users, []}

rabbitmq.config的位置对于不同的操作系统是不同的。

  1. 在 Search 微服务的根目录下创建一个 Dockerfile,如下所示:
FROM frolvlad/alpine-oraclejdk8
VOLUME /tmp
ADD  target/search-1.0.jar search.jar
EXPOSE 8090
ENTRYPOINT ["java","-jar","/search.jar"]

以下是对 Dockerfile 内容的快速检查:

  • FROM frolvlad/alpine-oraclejdk8:这告诉 Docker 构建使用特定的alpine-oraclejdk8版本作为此构建的基本镜像。frolvlad表示定位alpine-oraclejdk8镜像的存储库。在这种情况下,它是使用 Alpine Linux 和 Oracle JDK 8 构建的镜像。这将帮助我们将应用程序层叠在基本镜像之上,而无需自己设置 Java 库。在这种情况下,由于此镜像在我们的本地镜像存储中不可用,Docker 构建将继续从远程 Docker Hub 注册表下载此镜像。

  • VOLUME /tmp:这允许容器访问主机机器中指定的目录。在我们的情况下,这指向 Spring Boot 应用程序为 Tomcat 创建工作目录的tmp目录。tmp目录对于容器来说是一个逻辑目录,间接指向主机的一个本地目录。

  • ADD target/search-1.0.jar search.jar: 这将应用程序二进制文件添加到容器中,并指定目标文件名。在这种情况下,Docker 构建将target/search-1.0.jar复制到容器中作为search.jar

  • EXPOSE 8090: 这是告诉容器如何进行端口映射。这将8090与内部 Spring Boot 服务的外部端口绑定。

  • ENTRYPOINT ["java","-jar", "/search.jar"]: 这告诉容器在启动时要运行哪个默认应用程序。在这种情况下,我们指向 Java 进程和 Spring Boot fat JAR 文件来启动服务。

  1. 下一步是从存储 Dockerfile 的文件夹运行docker build。这将下载基础镜像,并依次运行 Dockerfile 中的条目,如下所示:
docker build –t search:1.0 .

这个命令的输出将如下所示:

在 Docker 中部署微服务

  1. 对 Search API Gateway 和 Website 重复相同的步骤。

  2. 创建完镜像后,可以通过输入以下命令来验证。这个命令将列出镜像及其详细信息,包括镜像文件的大小:

docker images

输出将如下所示:

在 Docker 中部署微服务

  1. 接下来要做的是运行 Docker 容器。可以使用docker run命令来完成这个操作。这个命令将加载并运行容器。在启动时,容器调用 Spring Boot 可执行 JAR 来启动微服务。

在启动容器之前,请确保 Config 和 Eureka 服务器正在运行:

docker run --net host -p 8090:8090 -t search:1.0
docker run --net host -p 8095:8095 -t search-apigateway:1.0
docker run --net host -p 8001:8001 -t website:1.0

前面的命令启动了 Search 和 Search API Gateway 微服务以及网站。

在这个例子中,我们使用主机网络(--net host)而不是桥接网络,以避免 Eureka 注册到 Docker 容器名称。这可以通过覆盖EurekaInstanceConfigBean来纠正。从网络角度来看,主机选项比桥接选项更少隔离。主机与桥接的优势和劣势取决于项目。

  1. 一旦所有服务都完全启动,可以使用docker ps命令进行验证,如下面的屏幕截图所示:在 Docker 中部署微服务

  2. 下一步是将浏览器指向http://192.168.99.100:8001。这将打开 BrownField PSS 网站。

注意 IP 地址。这是 Docker 机器的 IP 地址,如果你在 Mac 或 Windows 上使用 Boot2Docker 运行。在 Mac 或 Windows 上,如果不知道 IP 地址,则输入以下命令来找出默认机器的 Docker 机器 IP 地址:

docker-machine ip default

如果 Docker 在 Linux 上运行,那么这就是主机 IP 地址。

BookingFaresCheck-in和它们各自的网关微服务应用相同的更改。

在 Docker 上运行 RabbitMQ

由于我们的示例也使用了 RabbitMQ,让我们探讨如何将 RabbitMQ 设置为 Docker 容器。以下命令从 Docker Hub 拉取 RabbitMQ 镜像并启动 RabbitMQ:

docker run –net host rabbitmq3

确保*-service.properties中的 URL 已更改为 Docker 主机的 IP 地址。在 Mac 或 Windows 的情况下,应用之前的规则来找出 IP 地址。

使用 Docker 注册表

Docker Hub 提供了一个集中存储所有 Docker 镜像的位置。这些镜像可以存储为公共或私有。在许多情况下,由于安全相关的问题,组织会在本地部署自己的私有注册表。

执行以下步骤来设置和运行本地注册表:

  1. 以下命令将启动一个注册表,将注册表绑定到端口5000上:
docker run -d -p 5000:5000 --restart=always --name registry registry:2

  1. search:1.0标记到注册表,如下所示:
docker tag search:1.0 localhost:5000/search:1.0

  1. 然后,通过以下命令将镜像推送到注册表:
docker push localhost:5000/search:1.0

  1. 从注册表中拉取镜像,如下所示:
docker pull localhost:5000/search:1.0

设置 Docker Hub

在上一章中,我们使用了本地 Docker 注册表。本节将展示如何设置和使用 Docker Hub 来发布 Docker 容器。这是一个方便的机制,可以全球访问 Docker 镜像。在本章的后面部分,Docker 镜像将从本地机器发布到 Docker Hub,并从 EC2 实例下载。

为此,创建一个公共 Docker Hub 账户和一个存储库。对于 Mac,按照以下 URL 的步骤进行:docs.docker.com/mac/step_five/

在本例中,使用brownfield用户名创建了 Docker Hub 账户。

在这种情况下,注册表充当微服务存储库,其中所有 docker 化的微服务将被存储和访问。这是微服务能力模型中解释的能力之一。

将微服务发布到 Docker Hub

要将 docker 化的服务推送到 Docker Hub,请按照以下步骤进行。第一条命令标记 Docker 镜像,第二条命令将 Docker 镜像推送到 Docker Hub 存储库:

docker tag search:1.0brownfield/search:1.0
docker push brownfield/search:1.0

要验证容器镜像是否已发布,请转到 Docker Hub 存储库https://hub.docker.com/u/brownfield

对所有其他 BrownField 微服务也重复此步骤。在此步骤结束时,所有服务将被发布到 Docker Hub。

云上的微服务

微服务能力模型中提到的能力之一是使用云基础设施进行微服务。在本章的前面部分,我们还探讨了使用云进行微服务部署的必要性。到目前为止,我们还没有将任何东西部署到云上。由于我们总共有八个微服务——Config-serverEureka-server、Turbine、RabbitMQ、Elasticsearch、Kibana 和 Logstash——在我们的整体 BrownField PSS 微服务生态系统中,很难在本地机器上运行所有这些微服务。

在本书的其余部分,我们将使用 AWS 作为云平台来部署 BrownField PSS 微服务。

在 AWS EC2 上安装 Docker

在本节中,我们将在 EC2 实例上安装 Docker。

本例假设读者熟悉 AWS,并且在 AWS 上已经创建了一个账户。

执行以下步骤在 EC2 上设置 Docker:

  1. 启动一个新的 EC2 实例。在这种情况下,如果我们必须同时运行所有实例,可能需要一个大实例。本例使用t2.large

在本例中,使用以下 Ubuntu AMI 镜像:ubuntu-trusty-14.04-amd64-server-20160114.5 (ami-fce3c696)

  1. 连接到 EC2 实例并运行以下命令:
sudo apt-get update 
sudo apt-get install docker.io

  1. 上述命令将在 EC2 实例上安装 Docker。使用以下命令验证安装:
docker version

在 EC2 上运行 BrownField 服务

在本节中,我们将在创建的 EC2 实例上设置 BrownField 微服务。在这种情况下,构建设置在本地桌面机器上,并且二进制文件将部署到 AWS。

执行以下步骤在 EC2 实例上设置服务:

  1. 通过以下命令安装 Git:
sudo apt-get install git

  1. 在任意文件夹上创建一个 Git 存储库。

  2. 更改配置服务器的bootstrap.properties,指向为本例创建的适当 Git 存储库。

  3. 更改所有微服务的bootstrap.properties,指向使用 EC2 实例的私有 IP 地址的配置服务器。

  4. 将本地 Git 存储库中的所有*.properties复制到 EC2 Git 存储库并执行提交。

  5. 更改*.properties文件中的 Eureka 服务器 URL 和 RabbitMQ URL,以匹配 EC2 私有 IP 地址。完成后将更改提交到 Git。

  6. 在本地机器上重新编译所有项目,并为searchsearch-apigatewaywebsite微服务创建 Docker 镜像。将它们全部推送到 Docker Hub 注册表。

  7. 从本地机器复制配置服务器和 Eureka 服务器的二进制文件到 EC2 实例。

  8. 在 EC2 实例上设置 Java 8。

  9. 然后,按顺序执行以下命令:

java –jar config-server.jar 
java –jar eureka-server.jar 
docker run –net host rabbitmq:3
docker run --net host -p 8090:8090 rajeshrv/search:1.0
docker run --net host -p 8095:8095 rajeshrv/search-apigateway:1.0
docker run --net host -p 8001:8001 rajeshrv/website:1.0

  1. 通过打开网站的 URL 并执行搜索来检查所有服务是否正常工作。请注意,在这种情况下我们将使用公共 IP 地址:http://54.165.128.23:8001

更新生命周期管理器

在第六章中,自动缩放微服务,我们考虑了一个生命周期管理器来自动启动和停止实例。我们使用 SSH 并执行 Unix 脚本来在目标机器上启动 Spring Boot 微服务。使用 Docker,我们不再需要 SSH 连接,因为 Docker 守护程序提供了基于 REST 的 API 来启动和停止实例。这极大地简化了生命周期管理器的部署引擎组件的复杂性。

在本节中,我们不会重写生命周期管理器。总的来说,我们将在下一章中替换生命周期管理器。

容器化的未来 - 单内核和强化安全

容器化仍在不断发展,但采用容器化技术的组织数量近年来有所增加。虽然许多组织正在积极采用 Docker 和其他容器技术,但这些技术的缺点仍在于容器的大小和安全问题。

目前,Docker 镜像通常很大。在一个弹性自动化的环境中,容器经常被创建和销毁,大小仍然是一个问题。更大的大小表示更多的代码,更多的代码意味着更容易受到安全漏洞的影响。

未来绝对在小型容器中。Docker 正在研究单内核,轻量级内核可以在低功率的物联网设备上运行 Docker。单内核不是完整的操作系统,但它们提供了支持部署应用程序所需的基本库。

容器的安全问题被广泛讨论和辩论。关键的安全问题围绕用户命名空间隔离或用户 ID 隔离。如果容器在根目录上,则可以默认获取主机的根权限。使用来自不受信任来源的容器镜像是另一个安全问题。Docker 正在尽快弥合这些差距,但有许多组织使用虚拟机和 Docker 的组合来规避一些安全顾虑。

总结

在本章中,您了解了在处理互联网规模的微服务时需要具有云环境的必要性。

我们探讨了容器的概念,并将其与传统虚拟机进行了比较。您还学习了 Docker 的基础知识,我们解释了 Docker 镜像、容器和注册表的概念。在微服务的背景下解释了容器的重要性和好处。

然后,本章转向了一个实际示例,通过将 BrownField 微服务 docker 化。我们演示了如何在 Docker 上部署之前开发的 Spring Boot 微服务。通过探索本地注册表以及 Docker Hub 来推送和拉取 docker 化的微服务,您学习了注册表的概念。

作为最后一步,我们探讨了如何在 AWS 云环境中部署一个 dockerized 的 BrownField 微服务。

第九章:使用 Mesos 和 Marathon 管理 docker 化的微服务

在互联网规模的微服务部署中,要管理成千上万个 docker 化的微服务并不容易。必须有一个基础设施抽象层和一个强大的集群控制平台,才能成功地管理互联网规模的微服务部署。

本章将解释在云环境中部署大规模微服务时,需要使用 Mesos 和 Marathon 作为基础设施抽象层和集群控制系统,以实现优化的资源使用。本章还将提供在云环境中设置 Mesos 和 Marathon 的逐步方法。最后,本章将演示如何在 Mesos 和 Marathon 环境中管理 docker 化的微服务。

在本章结束时,您将学到:

  • 需要有一个抽象层和集群控制软件

  • 从微服务的角度看 Mesos 和 Marathon

  • 使用 Mesos 和 Marathon 管理 docker 化的 BrownField 航空公司 PSS 微服务

审查微服务能力模型

在本章中,我们将探讨微服务能力模型中的集群控制和供应微服务能力,该模型在第三章中讨论了应用微服务概念

审查微服务能力模型

缺失的部分

在第八章中,我们讨论了如何将 BrownField 航空公司的 PSS 微服务 docker 化。Docker 帮助打包了 JVM 运行时和应用程序的 OS 参数,这样在将 docker 化的微服务从一个环境移动到另一个环境时就不需要特别考虑。Docker 提供的 REST API 简化了生命周期管理器与目标机器在启动和停止构件时的交互。

在大规模部署中,有数百甚至数千个 Docker 容器,我们需要确保 Docker 容器以自己的资源约束运行,例如内存、CPU 等。除此之外,可能还会为 Docker 部署设置规则,例如不应在同一台机器上运行容器的复制副本。此外,需要建立一种机制,以最佳地利用服务器基础设施,避免产生额外成本。

有些组织处理数十亿个容器。手动管理它们几乎是不可能的。在大规模 Docker 部署的情况下,需要回答一些关键问题:

  • 如何管理成千上万的容器?

  • 如何监视它们?

  • 在部署构件时,我们如何应用规则和约束?

  • 如何确保我们正确利用容器以获得资源效率?

  • 如何确保至少在任何时候运行一定数量的最小实例?

  • 如何确保依赖服务正在运行?

  • 如何进行滚动升级和优雅迁移?

  • 如何回滚故障部署?

所有这些问题都指向了需要解决两个关键能力的需求,这两个能力如下:

  • 提供统一抽象的集群抽象层,覆盖许多物理或虚拟机器。

  • 一个集群控制和初始化系统,以智能地管理集群抽象之上的部署

生命周期管理器理想地处理这些情况。可以向生命周期管理器添加足够的智能来解决这些问题。但是,在尝试修改生命周期管理器之前,重要的是更深入地了解集群管理解决方案的作用。

为什么集群管理很重要

由于微服务将应用程序分解为不同的微应用程序,许多开发人员请求更多的服务器节点进行部署。为了正确管理微服务,开发人员倾向于每个 VM 部署一个微服务,这进一步降低了资源利用率。在许多情况下,这导致 CPU 和内存的过度分配。

在许多部署中,微服务的高可用性要求迫使工程师为冗余添加越来越多的服务实例。实际上,尽管它提供了所需的高可用性,但这将导致服务器实例的资源利用不足。

一般来说,与单片应用程序部署相比,微服务部署需要更多的基础设施。由于基础设施成本的增加,许多组织未能看到微服务的价值:

为什么集群管理很重要

为了解决之前提到的问题,我们需要一个具备以下功能的工具:

  • 自动化一系列活动,如高效地将容器分配给基础设施,并使开发人员和管理员对此保持透明

  • 为开发人员提供一个抽象层,使他们可以在不知道要使用哪台机器来托管他们的应用程序的情况下,部署他们的应用程序到数据中心

  • 针对部署工件设置规则或约束

  • 为开发人员和管理员提供更高级别的灵活性,同时减少管理开销,或许还能减少人为干预

  • 通过最大限度地利用可用资源来以成本效益的方式构建、部署和管理应用程序

容器在这个背景下解决了一个重要问题。我们选择的任何具备这些功能的工具都可以以统一的方式处理容器,而不考虑底层的微服务技术。

集群管理的作用是什么?

典型的集群管理工具帮助虚拟化一组机器,并将它们作为单个集群进行管理。集群管理工具还帮助在机器之间移动工作负载或容器,同时对消费者保持透明。技术布道者和实践者使用不同的术语,如集群编排、集群管理、数据中心虚拟化、容器调度器、容器生命周期管理、容器编排、数据中心操作系统等。

许多这些工具目前既支持基于 Docker 的容器,也支持非容器化的二进制部署,比如独立的 Spring Boot 应用程序。这些集群管理工具的基本功能是将实际的服务器实例与应用程序开发人员和管理员抽象出来。

集群管理工具帮助自助服务和基础设施的预配,而不是要求基础设施团队分配所需的具有预定义规格的机器。在这种自动化的集群管理方法中,机器不再提前预配和预分配给应用程序。一些集群管理工具还帮助在许多异构机器或数据中心之间虚拟化数据中心,并创建一个弹性的、类似私有云的基础设施。集群管理工具没有标准的参考模型。因此,供应商之间的功能差异很大。

集群管理软件的一些关键功能总结如下:

  • 集群管理:它将一组虚拟机和物理机作为单个大型机器进行管理。这些机器在资源能力方面可能是异构的,但它们基本上都是运行 Linux 操作系统的机器。这些虚拟集群可以在云上、本地或两者的组合上形成。

  • 部署:它处理大量机器的应用程序和容器的自动部署。它支持应用程序容器的多个版本,还支持跨大量集群机器的滚动升级。这些工具还能够处理故障推广的回滚。

  • 可扩展性:它处理应用程序实例的自动和手动扩展,以优化利用率为主要目标。

  • 健康:它管理集群、节点和应用程序的健康状况。它会从集群中删除故障机器和应用实例。

  • 基础设施抽象化:它将开发人员与应用程序部署的实际机器抽象出来。开发人员不需要担心机器、其容量等等。完全由集群管理软件决定如何调度和运行应用程序。这些工具还将机器细节、其容量、利用率和位置从开发人员那里抽象出来。对于应用程序所有者来说,这些等同于一个具有几乎无限容量的单个大型机器。

  • 资源优化:这些工具的固有行为是以高效的方式在一组可用的机器上分配容器工作负载,从而降低所有权成本。可以有效地使用简单到极其复杂的算法来提高利用率。

  • 资源分配:它根据资源可用性和应用程序开发人员设置的约束来分配服务器。资源分配基于这些约束、亲和规则、端口需求、应用程序依赖性、健康状况等。

  • 服务可用性:确保服务在集群中的某个地方正常运行。在发生机器故障时,集群控制工具会自动通过在集群中的其他机器上重新启动这些服务来处理故障。

  • 灵活性:这些工具能够快速地将工作负载分配给可用资源,或者在资源需求发生变化时将工作负载移动到其他机器上。还可以根据业务的关键性、业务优先级等设置约束,重新调整资源。

  • 隔离:其中一些工具可以直接提供资源隔离。因此,即使应用程序未经容器化,仍然可以实现资源隔离。

用于资源分配的算法种类繁多,从简单算法到复杂算法,再到机器学习和人工智能。常用的算法包括随机算法、装箱算法和分散算法。根据资源可用性设置的约束将覆盖默认算法:

集群管理的作用是什么?

上图显示了这些算法如何填充可用的机器部署。在这种情况下,演示了两台机器:

  • 分散:此算法在可用的机器上均匀分配工作负载。这在图A中显示。

  • 装箱:此算法尝试逐个填充数据机器,并确保最大限度地利用机器。在使用按需付费的云服务时,装箱算法尤其有效。这在图B中显示。

  • 随机:此算法随机选择机器,并在随机选择的机器上部署容器。这在图C中显示。

有可能使用认知计算算法,如机器学习和协同过滤来提高效率。诸如超额分配之类的技术允许通过为高优先级任务分配未充分利用的资源来更好地利用资源,例如为收入产生服务分配最佳努力任务,如分析、视频、图像处理等。

与微服务的关系

如果微服务基础架构没有得到适当的配置,很容易导致过度的基础架构,从而增加拥有成本。正如前面所讨论的,具有集群管理工具的类似云的环境对于处理大规模微服务时实现成本效益至关重要。

使用 Spring Cloud 项目进行加速的 Spring Boot 微服务是利用集群管理工具的理想候选工作负载。由于基于 Spring Cloud 的微服务不知道位置,这些服务可以在集群中的任何位置部署。每当服务启动时,它们会自动注册到服务注册表并宣布其可用性。另一方面,消费者始终在注册表中查找可用的服务实例。这样,应用程序支持完全流动的结构,而不预设部署拓扑。通过 Docker,我们能够抽象运行时,使服务能够在任何基于 Linux 的环境中运行。

与虚拟化的关系

集群管理解决方案在许多方面与服务器虚拟化解决方案不同。集群管理解决方案作为应用程序组件运行在 VM 或物理机上。

集群管理解决方案

市场上有许多集群管理软件工具可用。对它们进行苹果对苹果的比较是不公平的。尽管没有一对一的组件,但它们之间在功能上有许多重叠的领域。在许多情况下,组织使用一个或多个这些工具的组合来满足他们的需求。

以下图表显示了微服务环境下集群管理工具的位置:

集群管理解决方案

在本节中,我们将探讨市场上可用的一些流行的集群管理解决方案。

Docker Swarm

Docker Swarm 是 Docker 的本地集群管理解决方案。Swarm 与 Docker 有本地和更深层次的集成,并公开与 Docker 远程 API 兼容的 API。Docker Swarm 在逻辑上将一组 Docker 主机分组,并将它们作为单个大型 Docker 虚拟主机进行管理。与应用程序管理员和开发人员决定将容器部署在哪个主机不同,这个决策将被委托给 Docker Swarm。Docker Swarm 将根据装箱和扩展算法决定使用哪个主机。

由于 Docker Swarm 基于 Docker 的远程 API,对于已经使用 Docker 的人来说,其学习曲线比任何其他容器编排工具都要窄。然而,Docker Swarm 是市场上相对较新的产品,它只支持 Docker 容器。

Docker Swarm 使用管理器节点的概念。管理器是管理人员与 Docker 容器进行交互和调度的单一点。节点是部署和运行 Docker 容器的地方。

Kubernetes

Kubernetes(k8s)来自谷歌的工程,使用 Go 语言编写,并在谷歌进行了大规模部署的实战测试。与 Swarm 类似,Kubernetes 帮助管理跨节点集群的容器化应用程序。Kubernetes 帮助自动化容器部署、调度和容器的可伸缩性。Kubernetes 支持许多有用的功能,例如自动渐进式部署、版本化部署以及容器的弹性,如果容器由于某种原因失败。

Kubernetes 架构具有主节点节点Pods的概念。主节点和节点一起形成一个 Kubernetes 集群。主节点负责在多个节点之间分配和管理工作负载。节点只是一个虚拟机或物理机。节点进一步细分为 Pods。一个节点可以托管多个 Pods。一个或多个容器被分组并在一个 Pod 内执行。Pods 还有助于管理和部署共同服务以提高效率。Kubernetes 还支持标签的概念,作为键值对来查询和找到容器。标签是用户定义的参数,用于标记执行共同类型工作负载的某些类型的节点,例如前端 Web 服务器。部署在集群上的服务获得一个单一的 IP/DNS 来访问该服务。

Kubernetes 对 Docker 有开箱即用的支持;然而,与 Docker Swarm 相比,Kubernetes 的学习曲线更陡峭。RedHat 作为其 OpenShift 平台的一部分,为 Kubernetes 提供商业支持。

Apache Mesos

Mesos 是由加州大学伯克利分校最初开发的开源框架,被 Twitter 大规模使用。Twitter 主要使用 Mesos 来管理庞大的 Hadoop 生态系统。

Mesos 与之前的解决方案略有不同。Mesos 更像是一个资源管理器,依赖其他框架来管理工作负载的执行。Mesos 位于操作系统和应用程序之间,提供了一个逻辑机器集群。

Mesos 是一个分布式系统内核,它将许多计算机逻辑分组和虚拟化为一个大型机器。Mesos 能够将多种异构资源分组到一个统一的资源集群上,应用程序可以在其上部署。因此,Mesos 也被称为在数据中心构建私有云的工具。

Mesos 具有主节点从节点的概念。与之前的解决方案类似,主节点负责管理集群,而从节点运行工作负载。Mesos 内部使用 ZooKeeper 进行集群协调和存储。Mesos 支持框架的概念。这些框架负责调度和运行非容器化应用程序和容器。Marathon,Chronos 和 Aurora 是用于调度和执行应用程序的流行框架。Netflix Fenzo 是另一个开源的 Mesos 框架。有趣的是,Kubernetes 也可以用作 Mesos 框架。

Marathon 支持 Docker 容器以及非容器化应用程序。Spring Boot 可以直接在 Marathon 中配置。Marathon 提供了许多开箱即用的功能,例如支持应用程序依赖关系,将应用程序分组以扩展和升级服务,启动和关闭健康和不健康的实例,推出推广,回滚失败的推广等。

Mesosphere 为 Mesos 和 Marathon 提供商业支持,作为其 DCOS 平台的一部分。

Nomad

HashiCorp 的 Nomad 是另一个集群管理软件。Nomad 是一个集群管理系统,它抽象了较低级别的机器细节和它们的位置。Nomad 的架构与之前探讨的其他解决方案相比更简单。Nomad 也更轻量级。与其他集群管理解决方案类似,Nomad 负责资源分配和应用程序的执行。Nomad 还接受用户特定的约束,并根据此分配资源。

Nomad 具有服务器的概念,所有作业都由其管理。一个服务器充当领导者,其他充当跟随者。Nomad 具有任务的概念,这是最小的工作单位。任务被分组成任务组。一个任务组有在相同位置执行的任务。一个或多个任务组或任务被管理为作业

Nomad 支持许多工作负载,包括 Docker,开箱即用。Nomad 还支持跨数据中心的部署,并且具有区域和数据中心感知能力。

舰队

Fleet 是 CoreOS 的集群管理系统。它在较低级别上运行,并在 systemd 之上工作。Fleet 可以管理应用程序依赖关系,并确保所有所需的服务在集群中的某个地方运行。如果服务失败,它会在另一个主机上重新启动服务。在分配资源时可以提供亲和性和约束规则。

Fleet 具有引擎代理的概念。在集群中任何时候只有一个引擎,但有多个代理。任务提交给引擎,代理在集群机器上运行这些任务。

Fleet 也支持 Docker。

使用 Mesos 和 Marathon 进行集群管理

正如我们在前一节中讨论的,有许多集群管理解决方案或容器编排工具可供选择。不同的组织根据其环境选择不同的解决方案来解决问题。许多组织选择 Kubernetes 或带有 Marathon 等框架的 Mesos。在大多数情况下,Docker 被用作默认的容器化方法来打包和部署工作负载。

在本章的其余部分,我们将展示 Mesos 如何与 Marathon 一起提供所需的集群管理能力。许多组织使用 Mesos,包括 Twitter、Airbnb、Apple、eBay、Netflix、PayPal、Uber、Yelp 等。

深入了解 Mesos

Mesos 可以被视为数据中心内核。DCOS 是 Mesos 的商业版本,由 Mesosphere 支持。为了在一个节点上运行多个任务,Mesos 使用资源隔离概念。Mesos 依赖于 Linux 内核的cgroups来实现类似容器方法的资源隔离。它还支持使用 Docker 进行容器化隔离。Mesos 支持批处理工作负载以及 OLTP 类型的工作负载:

深入了解 Mesos

Mesos 是一个在 Apache 许可下的开源顶级 Apache 项目。Mesos 将 CPU、内存和存储等较低级别的计算资源从较低级别的物理或虚拟机中抽象出来。

在我们研究为什么需要 Mesos 和 Marathon 之前,让我们先了解 Mesos 架构。

Mesos 架构

以下图表显示了 Mesos 的最简单的架构表示。Mesos 的关键组件包括一个 Mesos 主节点,一组从属节点,一个 ZooKeeper 服务和一个 Mesos 框架。Mesos 框架进一步分为两个组件:调度程序和执行程序:

Mesos 架构

前面图表中的方框解释如下:

  • 主节点:Mesos 主节点负责管理所有 Mesos 从属节点。Mesos 主节点从所有从属节点获取资源可用性信息,并负责根据特定资源策略和约束适当地填充资源。Mesos 主节点从所有从属机器中抢占可用资源,并将它们汇集为一个单一的大型机器。主节点根据这个资源池向运行在从属机器上的框架提供资源。

为了实现高可用性,Mesos 主节点由 Mesos 主节点的备用组件支持。即使主节点不可用,现有任务仍然可以执行。但是,在没有主节点的情况下无法调度新任务。主节点备用节点是等待活动主节点故障并在故障发生时接管主节点角色的节点。它使用 ZooKeeper 进行主节点领导者选举。领导者选举必须满足最低法定人数要求。

  • 从属节点:Mesos 从属节点负责托管任务执行框架。任务在从属节点上执行。Mesos 从属节点可以以键值对的形式启动,例如数据中心=X。这在部署工作负载时用于约束评估。从属机器与 Mesos 主节点共享资源可用性。

  • ZooKeeper:ZooKeeper 是 Mesos 中使用的集中协调服务器,用于协调 Mesos 集群中的活动。在 Mesos 主节点故障的情况下,Mesos 使用 ZooKeeper 进行领导者选举。

  • 框架:Mesos 框架负责理解应用程序的约束,接受主节点的资源提供,并最终在主节点提供的从属资源上运行任务。Mesos 框架由两个组件组成:框架调度程序和框架执行程序:

  • 调度程序负责注册到 Mesos 并处理资源提供

  • 执行程序在 Mesos 从属节点上运行实际程序

框架还负责执行某些策略和约束。例如,一个约束可以是,假设最少有 500MB 的 RAM 可用于执行。

框架是可插拔组件,可以用另一个框架替换。框架工作流程如下图所示:

Mesos 架构

在前面的工作流程图中表示的步骤如下所述:

  1. 框架向 Mesos 主节点注册并等待资源提供。调度程序可能有许多任务在其队列中等待执行,具有不同的资源约束(例如,在此示例中为任务 A 到 D)。在这种情况下,任务是安排的工作单元,例如 Spring Boot 微服务。

  2. Mesos 从属将可用资源提供给 Mesos 主节点。例如,从属会广告其机器上可用的 CPU 和内存。

  3. 然后,Mesos 主节点根据设置的分配策略创建资源提供,并将其提供给框架的调度组件。分配策略确定资源将提供给哪个框架以及将提供多少资源。可以通过插入额外的分配策略来自定义默认策略。

  4. 基于约束、能力和策略的调度框架组件可能接受或拒绝资源提供。例如,如果资源不足,框架会拒绝资源提供,根据设置的约束和策略。

  5. 如果调度程序组件接受资源提供,它将提交一个或多个任务的详细信息给 Mesos 主节点,每个任务都有资源约束。例如,在这个例子中,它准备好提交任务 A 到 D。

  6. Mesos 主节点将任务列表发送给资源可用的从属。安装在从属机器上的框架执行程序组件会接收并运行这些任务。

Mesos 支持许多框架,例如:

  • 用于长时间运行的进程(例如 Web 应用程序)的 Marathon 和 Aurora

  • 用于大数据处理的 Hadoop、Spark 和 Storm

  • 用于批处理调度的 Chronos 和 Jenkins

  • 用于数据管理的 Cassandra 和 Elasticsearch

在本章中,我们将使用 Marathon 来运行 docker 化的微服务。

Marathon

Marathon 是 Mesos 框架实现之一,可以运行容器和非容器执行。Marathon 特别设计用于长时间运行的应用程序,例如 Web 服务器。Marathon 确保使用 Marathon 启动的服务即使 Mesos 上托管的从属失败也能继续可用。这将通过启动另一个实例来完成。

Marathon 是用 Scala 编写的,具有高度可扩展性。Marathon 提供 UI 以及 REST API 与 Marathon 交互,例如启动、停止、扩展和监视应用程序。

与 Mesos 类似,Marathon 的高可用性是通过运行指向 ZooKeeper 实例的多个 Marathon 实例来实现的。其中一个 Marathon 实例充当领导者,其他实例处于待机模式。如果领先的主节点失败,将进行领导者选举,并确定下一个活动主节点。

Marathon 的一些基本特性包括:

  • 设置资源约束

  • 应用程序的扩展、缩减和实例管理

  • 应用程序版本管理

  • 启动和关闭应用程序

Marathon 的一些高级功能包括:

  • 滚动升级、滚动重启和回滚

  • 蓝绿部署

为 BrownField 微服务实现 Mesos 和 Marathon

在本节中,将部署在 AWS 云中并使用 Mesos 和 Marathon 进行管理的 docker 化的 Brownfield 微服务,该微服务在第八章中开发。

为了演示目的,解释中只涵盖了三个服务(搜索搜索 API 网关网站):

为 BrownField 微服务实现 Mesos 和 Marathon

目标状态实现的逻辑架构如上图所示。该实现使用多个 Mesos 从属实例来执行 docker 化的微服务,其中包括一个 Mesos 主节点。使用 Marathon 调度程序组件来调度 docker 化的微服务。docker 化的微服务托管在 Docker Hub 注册表上。docker 化的微服务使用 Spring Boot 和 Spring Cloud 实现。

以下图表显示了物理部署架构:

为 BrownField 微服务实现 Mesos 和 Marathon

如上图所示,在本示例中,我们将使用四个 EC2 实例:

  • EC2-M1:这个托管了 Mesos 主节点、ZooKeeper、Marathon 调度程序和一个 Mesos 从属实例

  • EC2-M2:这个托管了一个 Mesos 从属实例

  • EC2-M3:这个托管了另一个 Mesos 从属实例

  • EC2-M4:这个托管了 Eureka、配置服务器和 RabbitMQ

对于真正的生产设置,需要多个 Mesos 主节点以及多个 Marathon 实例来实现容错。

设置 AWS

启动四个将用于此部署的t2.micro EC2 实例。所有四个实例必须在同一个安全组中,以便实例可以使用它们的本地 IP 地址相互看到。

以下表格显示了机器详细信息和 IP 地址,仅供参考和链接后续指令:

设置 AWS

实例 ID 私有 DNS/IP 公有 DNS/IP
i-06100786 ip-172-31-54-69.ec2.internal``172.31.54.69 ec2-54-85-107-37.compute-1.amazonaws.com``54.85.107.37
i-2404e5a7 ip-172-31-62-44.ec2.internal``172.31.62.44 ec2-52-205-251-150.compute-1.amazonaws.com``52.205.251.150
i-a7df2b3a ip-172-31-49-55.ec2.internal``172.31.49.55 ec2-54-172-213-51.compute-1.amazonaws.com``54.172.213.51
i-b0eb1f2d ip-172-31-53-109.ec2.internal``172.31.53.109 ec2-54-86-31-240.compute-1.amazonaws.com``54.86.31.240

根据您的 AWS EC2 配置替换 IP 和 DNS 地址。

安装 ZooKeeper、Mesos 和 Marathon

在部署中将使用以下软件版本。本节中的部署遵循前一节中解释的物理部署架构:

  • Mesos 版本 0.27.1

  • Docker 版本 1.6.2,构建 7c8fca2

  • Marathon 版本 0.15.3

注意

有关设置 ZooKeeper、Mesos 和 Marathon 的详细说明,请参阅open.mesosphere.com/getting-started/install/

执行以下步骤进行最小化安装 ZooKeeper、Mesos 和 Marathon 以部署 BrownField 微服务:

  1. 作为先决条件,所有机器上必须安装 JRE 8。执行以下命令:
sudo apt-get -y install oracle-java8-installer

  1. 通过以下命令在所有标记为 Mesos 从属实例的机器上安装 Docker:
sudo apt-get install docker

  1. 打开终端窗口并执行以下命令。这些命令设置了用于安装的存储库:
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E56151BF
DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
CODENAME=$(lsb_release -cs)
# Add the repository
echo "deb http://repos.mesosphere.com/${DISTRO} ${CODENAME} main" | \
 sudo tee /etc/apt/sources.list.d/mesosphere.list
sudo apt-get -y update

  1. 执行以下命令安装 Mesos 和 Marathon。这也将安装 Zookeeper 作为依赖项:
sudo apt-get -y install mesos marathon

在为 Mesos slave 执行保留的三个 EC2 实例上重复上述步骤。作为下一步,必须在为 Mesos 主节点标识的机器上配置 ZooKeeper 和 Mesos。

配置 ZooKeeper

连接到为 Mesos 主节点和 Marathon 调度器保留的机器。在这种情况下,172.31.54.69将用于设置 ZooKeeper、Mesos 主节点和 Marathon。

ZooKeeper 需要进行两个配置更改,如下:

  1. 第一步是将/etc/zookeeper/conf/myid设置为介于1255之间的唯一整数,如下所示:
Open vi /etc/zookeeper/conf/myid and set 1\. 

  1. 下一步是编辑/etc/zookeeper/conf/zoo.cfg。更新文件以反映以下更改:
# specify all zookeeper servers
# The first port is used by followers to connect to the leader
# The second one is used for leader election
server.1= 172.31.54.69:2888:3888
#server.2=zookeeper2:2888:3888
#server.3=zookeeper3:2888:3888

用相关的私有 IP 地址替换 IP 地址。在这种情况下,我们将只使用一个 ZooKeeper 服务器,但在生产场景中,需要多个服务器以实现高可用性。

配置 Mesos

对 Mesos 配置进行更改,以指向 ZooKeeper,设置仲裁,并通过以下步骤启用 Docker 支持:

  1. 编辑/etc/mesos/zk以设置以下值。这是为了将 Mesos 指向 ZooKeeper 实例进行仲裁和领导者选举:
zk:// 172.31.54.69:2181/mesos 
  1. 编辑/etc/mesos-master/quorum文件,并将值设置为1。在生产场景中,可能需要最少三个仲裁:
vi /etc/mesos-master/quorum

  1. 默认的 Mesos 安装不支持 Mesos slave 上的 Docker。为了启用 Docker,更新以下mesos-slave配置:
echo 'docker,mesos' > /etc/mesos-slave/containerizers

作为服务运行 Mesos、Marathon 和 ZooKeeper

所有必需的配置更改都已实施。启动 Mesos、Marathon 和 Zookeeper 的最简单方法是将它们作为服务运行,如下所示:

  • 以下命令启动服务。服务需要按以下顺序启动:
sudo service zookeeper start
sudo service mesos-master start
sudo service mesos-slave start
sudo service marathon start

  • 在任何时候,可以使用以下命令来停止这些服务:
sudo service zookeeper stop
sudo service mesos-master stop
sudo service mesos-slave stop
sudo service marathon stop

  • 一旦服务启动并运行,使用终端窗口验证服务是否正在运行:作为服务运行的 Mesos、Marathon 和 ZooKeeper

在命令行中运行 Mesos slave

在这个例子中,我们将使用命令行版本来调用 Mesos slave,以展示额外的输入参数,而不是使用 Mesos slave 服务。停止 Mesos slave,并使用此处提到的命令行来重新启动 slave:

$sudo service mesos-slave stop

$sudo /usr/sbin/mesos-slave  --master=172.31.54.69:5050 --log_dir=/var/log/mesos --work_dir=/var/lib/mesos --containerizers=mesos,docker --resources="ports(*):[8000-9000, 31000-32000]"

所使用的命令行参数解释如下:

  • --master=172.31.54.69:5050:此参数用于告诉 Mesos slave 连接到正确的 Mesos 主节点。在这种情况下,只有一个主节点在172.31.54.69:5050运行。所有的 slave 都连接到同一个 Mesos 主节点。

  • --containerizers=mesos,docker:此参数用于启用对 Docker 容器执行以及在 Mesos slave 实例上的非容器化执行的支持。

  • --resources="ports(*):[8000-9000, 31000-32000]:此参数表示 slave 在绑定资源时可以提供两个端口范围。3100032000是默认范围。由于我们使用以8000开头的端口号,因此很重要告诉 Mesos slave 也允许从8000开始暴露端口。

执行以下步骤来验证 Mesos 和 Marathon 的安装:

  1. 在所有为 slave 指定的三个实例上执行前面步骤中提到的命令来启动 Mesos slave。由于它们都连接到同一个主节点,因此可以在所有三个实例上使用相同的命令。

  2. 如果 Mesos slave 成功启动,控制台中将出现类似以下的消息:

I0411 18:11:39.684809 16665 slave.cpp:1030] Forwarding total oversubscribed resources

上述消息表明 Mesos slave 开始定期向 Mesos 主节点发送资源可用性的当前状态。

  1. 打开http://54.85.107.37:8080来检查 Marathon UI。用 EC2 实例的公共 IP 地址替换 IP 地址:在命令行中运行 Mesos slave

由于目前尚未部署任何应用程序,因此 UI 的应用程序部分为空。

  1. 打开运行在端口5050上的 Mesos UI,访问http://54.85.107.37:5050在命令行中运行 Mesos 从属

控制台的从属部分显示有三个已激活的 Mesos 从属可用于执行。它还表明没有活动任务。

准备 BrownField PSS 服务

在上一节中,我们成功地设置了 Mesos 和 Marathon。在本节中,我们将看看如何部署之前使用 Mesos 和 Marathon 开发的 BrownField PSS 应用程序。

注意

本章的完整源代码可在代码文件的第九章项目中找到。将chapter8.configserverchapter8.eurekaserverchapter8.searchchapter8.search-apigatewaychapter8.website复制到一个新的 STS 工作区,并将它们重命名为chapter9.*

  1. 在部署任何应用程序之前,我们必须在其中一个服务器上设置配置服务器、Eureka 服务器和 RabbitMQ。按照第八章中描述的在 EC2 上运行 BrownField 服务部分中描述的步骤,使用 Docker 容器化微服务。或者,我们可以在前一章中用于此目的的相同实例上使用。

  2. 将所有bootstrap.properties文件更改为反映配置服务器的 IP 地址。

  3. 在部署我们的服务之前,微服务需要进行一些特定的更改。当在 BRIDGE 模式下运行 docker 化的微服务时,我们需要告诉 Eureka 客户端要使用的主机名。默认情况下,Eureka 使用实例 ID进行注册。然而,这并不有用,因为 Eureka 客户端将无法使用实例 ID 查找这些服务。在上一章中,使用了 HOST 模式而不是 BRIDGE 模式。

主机名设置可以使用eureka.instance.hostname属性来完成。然而,在特定情况下在 AWS 上运行时,另一种方法是在微服务中定义一个 bean 来获取 AWS 特定的信息,如下所示:

@Configuration
class EurekaConfig { 
@Bean
    public EurekaInstanceConfigBean eurekaInstanceConfigBean() {
    EurekaInstanceConfigBean config = new EurekaInstanceConfigBean(new InetUtils(new InetUtilsProperties()));
AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka");
        config.setDataCenterInfo(info);
        info.getMetadata().put(AmazonInfo.MetaDataKey.publicHostname.getName(), info.get(AmazonInfo.MetaDataKey.publicIpv4));
        config.setHostname(info.get(AmazonInfo.MetaDataKey.localHostname));       
config.setNonSecurePortEnabled(true);
config.setNonSecurePort(PORT); 
config.getMetadataMap().put("instanceId",  info.get(AmazonInfo.MetaDataKey.localHostname));
return config;
}

上述代码使用亚马逊主机信息使用 Netflix API 提供了自定义的 Eureka 服务器配置。该代码使用私有 DNS 覆盖了主机名和实例 ID。端口从配置服务器中读取。该代码还假定每个服务一个主机,以便端口号在多次部署中保持不变。这也可以通过在运行时动态读取端口绑定信息来覆盖。

上述代码必须应用于所有微服务。

  1. 使用 Maven 重新构建所有微服务。构建并推送 Docker 镜像到 Docker Hub。三个服务的步骤如下所示。对所有其他服务重复相同的步骤。在执行这些命令之前,工作目录需要切换到相应的目录:
docker build -t search-service:1.0 .
docker tag search-service:1.0 rajeshrv/search-service:1.0
docker push rajeshrv/search-service:1.0

docker build -t search-apigateway:1.0 .
docker tag search-apigateway:1.0 rajeshrv/search-apigateway:1.0
docker push rajeshrv/search-apigateway:1.0

docker build -t website:1.0 .
docker tag website:1.0 rajeshrv/website:1.0
docker push rajeshrv/website:1.0

部署 BrownField PSS 服务

Docker 镜像现在已发布到 Docker Hub 注册表。执行以下步骤来部署和运行 BrownField PSS 服务:

  1. 在专用实例上启动配置服务器、Eureka 服务器和 RabbitMQ。

  2. 确保 Mesos 服务器和 Marathon 正在配置 Mesos 主服务器的机器上运行。

  3. 按照之前描述的在所有机器上运行 Mesos 从属的命令行来运行 Mesos 从属。

  4. 此时,Mesos Marathon 集群已经启动并准备好接受部署。可以通过为每个服务创建一个 JSON 文件来进行部署,如下所示:

{
  "id": "search-service-1.0",
  "cpus": 0.5,
  "mem": 256.0,
  "instances": 1,
  "container": {
   "docker": {
    "type": "DOCKER",
      "image": "rajeshrv/search-service:1.0",
       "network": "BRIDGE",
       "portMappings": [
        {  "containerPort": 0, "hostPort": 8090 }
      ]
    }
  }
}

上述 JSON 代码将存储在search.json文件中。同样,也为其他服务创建一个 JSON 文件。

JSON 结构解释如下:

  • id:这是应用程序的唯一 ID。这可以是一个逻辑名称。

  • cpusmem:这为应用程序设置了资源约束。如果资源提供不满足这个资源约束,Marathon 将拒绝来自 Mesos 主服务器的资源提供。

  • instances:这决定了要启动多少个此应用程序的实例。在前面的配置中,默认情况下,一旦部署,它就会启动一个实例。Marathon 在任何时候都会保持所述实例的数量。

  • container:此参数告诉 Marathon 执行器使用 Docker 容器进行执行。

  • image:这告诉 Marathon 调度器要使用哪个 Docker 镜像进行部署。在这种情况下,它将从 Docker Hub 仓库rajeshrv下载search-service:1.0镜像。

  • network:此值用于 Docker 运行时建议在启动新的 Docker 容器时使用的网络模式。这可以是 BRIDGE 或 HOST。在这种情况下,将使用 BRIDGE 模式。

  • portMappings:端口映射提供了如何映射内部和外部端口的信息。在前面的配置中,主机端口设置为8090,这告诉 Marathon 执行器在启动服务时使用8090。由于容器端口设置为0,相同的主机端口将分配给容器。如果主机端口值为0,Marathon 会选择随机端口。

  1. 还可以使用 JSON 描述符进行额外的健康检查,如下所示:
"healthChecks": [
    {
      "protocol": "HTTP",
      "portIndex": 0,
      "path": "/admin/health",
      "gracePeriodSeconds": 100,
      "intervalSeconds": 30,
      "maxConsecutiveFailures": 5
    }
  ]
  1. 创建并保存此 JSON 代码后,使用 Marathon 的 REST API 将其部署到 Marathon:
curl -X POST http://54.85.107.37:8080/v2/apps -d @search.json -H "Content-type: application/json"

对所有其他服务也重复此步骤。

上述步骤将自动将 Docker 容器部署到 Mesos 集群,并启动服务的一个实例。

审查部署

具体步骤如下:

  1. 打开 Marathon UI。如下图所示,UI 显示所有三个应用程序都已部署,并处于运行状态。它还指示1 个 1实例处于运行状态:审查部署

  2. 访问 Mesos UI。如下图所示,有三个活动任务,全部处于运行状态。它还显示了这些服务运行的主机:审查部署

  3. 在 Marathon UI 中,点击正在运行的应用程序。以下屏幕截图显示了search-apigateway-1.0应用程序。在实例选项卡中,显示了服务绑定的 IP 地址和端口:审查部署

扩展应用程序按钮允许管理员指定需要多少个服务实例。这可用于扩展和缩减实例。

  1. 打开 Eureka 服务器控制台,查看服务的绑定情况。如屏幕截图所示,当服务注册时,AMI可用区会反映出来。访问http://52.205.251.150:8761审查部署

  2. 在浏览器中打开http://54.172.213.51:8001,验证Website应用程序。

生命周期管理器的位置

生命周期管理器在第六章中介绍,具有根据需求自动扩展或缩减实例的能力。它还具有根据策略和约束条件在一组机器上决定部署何处和如何部署应用程序的能力。生命周期管理器的能力如下图所示:

生命周期管理器的位置

Marathon 具有根据策略和约束条件管理集群和集群部署的能力。可以使用 Marathon UI 更改实例的数量。

我们的生命周期管理器和 Marathon 之间存在冗余的能力。有了 Marathon,就不再需要 SSH 工作或机器级脚本。此外,部署策略和约束条件可以委托给 Marathon。Marathon 提供的 REST API 可以用于启动扩展功能。

Marathon 自动缩放是 Mesosphere 的一个自动缩放的概念验证项目。Marathon 自动缩放提供基本的自动缩放功能,如 CPU、内存和请求速率。

重写生命周期管理器与 Mesos 和 Marathon

我们仍然需要一个定制的生命周期管理器来收集来自 Spring Boot 执行器端点的指标。如果缩放规则超出了 CPU、内存和缩放速率,定制的生命周期管理器也很方便。

以下图表显示了使用 Marathon 框架更新的生命周期管理器:

重写生命周期管理器与 Mesos 和 Marathon

在这种情况下,生命周期管理器收集来自不同 Spring Boot 应用程序的执行器指标,将它们与其他指标结合起来,并检查特定的阈值。根据缩放策略,决策引擎通知缩放引擎是缩小还是扩大。在这种情况下,缩放引擎只是一个 Marathon REST 客户端。这种方法比我们早期使用 SSH 和 Unix 脚本的原始生命周期管理器实现更清洁、更整洁。

技术元模型

我们已经涵盖了使用 BrownField PSS 微服务的许多内容。以下图表通过将所有使用的技术汇总到技术元模型中来总结了这一点。

技术元模型

总结

在本章中,您了解了集群管理和初始化系统在大规模高效管理 docker 化微服务的重要性。

在深入研究 Mesos 和 Marathon 之前,我们探讨了不同的集群控制或集群编排工具。我们还在 AWS 云环境中实施了 Mesos 和 Marathon,以演示如何管理为 BrownField PSS 开发的 docker 化微服务。

在本章末尾,我们还探讨了生命周期管理器在 Mesos 和 Marathon 中的位置。最后,我们基于 BrownField PSS 微服务实现,总结了本章的技术元模型。

到目前为止,我们已经讨论了成功实施微服务所需的所有核心和支持技术能力。成功的微服务实施还需要超越技术的流程和实践。下一章,也是本书的最后一章,将涵盖微服务的流程和实践视角。

第十章:微服务开发生命周期

软件开发生命周期SDLC)类似,了解微服务开发生命周期流程的各个方面对于成功实施微服务架构至关重要。

本章将重点关注使用 BrownField 航空公司 PSS 微服务示例的开发过程和实践。此外,本章将描述在符合 DevOps 实践的情况下,构建开发团队、开发方法论、自动化测试和微服务的持续交付的最佳实践。最后,本章将通过阐明参考架构在微服务的分散治理方法中的重要性来结束。

通过本章结束时,您将了解以下主题:

  • 在微服务开发的背景下审查 DevOps

  • 定义微服务的生命周期和相关流程

  • 围绕互联网规模的微服务的开发、测试和部署的最佳实践

审查微服务能力模型

本章将涵盖在第三章 应用微服务概念中讨论的微服务能力模型中的以下微服务能力:

  • DevOps

  • DevOps 工具

  • 参考架构和库

  • 测试工具(反脆弱、RUM 等)

审查微服务能力模型

精益 IT 的新口号-DevOps

我们在第二章 使用 Spring Boot 构建微服务中讨论了 DevOps 的定义。以下是对 DevOps 定义的快速回顾。

Gartner 对 DevOps 的定义如下:

"DevOps 代表了 IT 文化的变革,侧重于通过采用敏捷、精益实践在系统导向方法的背景下实现快速的 IT 服务交付。DevOps 强调人(和文化),并致力于改善运营和开发团队之间的协作。DevOps 实施利用技术-特别是可以利用越来越可编程和动态的基础设施的自动化工具,从生命周期的角度来看。"

DevOps 和微服务是独立发展的。第一章 解密微服务,探讨了微服务的演变。在本节中,我们将回顾 DevOps 的演变,然后看看 DevOps 如何支持微服务的采用。

在数字颠覆的时代,为了支持现代业务,IT 组织必须掌握两个关键领域:交付速度和价值驱动的交付。这显然是除了精通领先技术之外的事情。

许多 IT 组织未能掌握这种变革,导致业务用户感到沮丧。为了克服这种情况,许多业务部门开始在其控制下启动自己的影子 IT 或隐形 IT。然后一些聪明的 IT 组织采用了精益 IT 模型来应对这些情况。

然而,许多组织仍然因为传统系统和流程的沉重包袱而在这一转变中挣扎。Gartner 提出了速度分层应用策略的概念。Gartner 认为,只有某些类型的应用程序或某些业务领域需要高速。Gartner 将其称为创新系统。创新系统需要比记录系统更快的创新。由于创新系统需要快速创新,因此对于这类应用程序来说,精益的 IT 交付模型是必不可少的。从业者将精益的 IT 模型宣扬为 DevOps。

组织采用的两种关键策略来采用 DevOps。

一些组织将 DevOps 定位为填补现有流程中的空白的过程。这些组织采用了 DevOps 旅程的渐进策略。采用路径从敏捷开发开始,然后逐步采用持续集成、自动化测试和发布到生产环境,然后采用所有 DevOps 实践。这类组织面临的挑战是实现全部益处的时间以及由于传统流程而导致的混合文化。

因此,许多组织采取了颠覆性的方法来采用 DevOps。这将通过将 IT 分成两层甚至两个不同的 IT 单元来实现。IT 的高速层使用 DevOps 风格的实践,从而彻底改变组织的文化,与传统流程和实践没有关联。将识别出一个选择性应用集群,并将其移至基于业务价值的新 IT:

精益 IT 的新口号-DevOps

DevOps 的目的不仅仅是降低成本。它还使业务能够通过快速将想法转化为产品来打破竞争对手。DevOps 以多种方式攻击传统 IT 问题,如此处所述。

减少浪费

DevOps 流程和实践本质上加快了交付速度,从而提高了质量。交付速度是通过减少 IT 浪费来实现的。这是通过避免对业务或期望的业务结果没有价值的工作来实现的。IT 浪费包括软件缺陷、生产力问题、流程开销、决策时间滞后、报告层面的时间、内部治理、过度估计等等。通过减少这些浪费,组织可以根本性地提高交付速度。主要通过采用敏捷流程、工具和技术来减少浪费。

自动化每一个可能的步骤

通过自动化手动执行的任务,可以显著提高交付速度和交付质量。自动化的范围从规划到客户反馈。自动化减少了将业务想法转化为产品的时间。这也减少了许多手动的门禁检查、官僚决策等。自动化的监控机制和反馈回到开发工厂,得到修复并快速转移到生产环境。

价值驱动交付

DevOps 通过价值驱动交付减少了 IT 与业务之间的差距。价值驱动交付通过理解真正的业务价值,将 IT 与业务紧密对齐,并通过快速交付这些价值来帮助业务,从而获得竞争优势。这类似于影子 IT 概念,其中 IT 与业务部门共同办公,并快速交付业务需求,而不是等待重型项目投资交付周期。

传统上,IT 与业务部门部分脱节,并且以 IT 关键绩效指标为主,比如成功项目交付的数量,而在新模式中,IT 与业务共享关键绩效指标。举例来说,一个新的 IT 关键绩效指标可能是 IT 帮助业务实现销售订单增长 10%,或者导致客户获取增长 20%。这将把 IT 的组织定位从仅仅是一个支持组织转变为业务伙伴。

弥合开发和运营之间的鸿沟

传统上,IT 在开发和运营方面有不同的团队。在许多情况下,它们通过逻辑障碍进行区分。DevOps 减少了开发和运营团队之间的差距,从而潜在地减少了浪费并提高了质量。多学科团队共同解决手头的问题,而不是互相指责。

通过 DevOps,运维团队将对开发团队开发的服务和应用程序有相当好的了解。同样,开发团队将对应用程序使用的基础设施组件和配置有很好的掌握。因此,运维团队可以根据服务行为做出决策,而不是在设计基础设施组件时强制执行标准组织政策和规则。这最终将有助于 IT 组织提高产品质量以及解决事件和问题管理的时间。

在 DevOps 世界中,通过高速变更的自动化实现交付速度,通过自动化和人员实现质量。通过效率、交付速度、质量和创新能力实现业务价值。通过自动化、生产力和减少浪费实现成本降低。

会见三人组-微服务、DevOps 和云

这三者-云、微服务和 DevOps-针对一组共同的目标:交付速度、业务价值和成本效益。所有三者可以独立存在和发展,但它们相互补充,以实现期望的共同目标。着手其中任何一个的组织自然倾向于考虑其他两个,因为它们密切相关:

会见三人组-微服务、DevOps 和云

许多组织从 DevOps 作为组织实践开始,以实现高速发布周期,但最终转向微服务架构和云。微服务和云不一定需要支持 DevOps。然而,自动化大型单片应用程序的发布周期并没有太多意义,在许多情况下,这是不可能实现的。在这种情况下,实施 DevOps 时,微服务架构和云将会很有用。

如果我们抛硬币,云不需要微服务架构来实现其好处。然而,要有效地实施微服务,云和 DevOps 都是必不可少的。

总之,如果一个组织的目标是以成本效益的方式实现高速交付和质量,这三者共同可以带来巨大的成功。

云作为微服务的自助服务基础设施

云的主要驱动力是提高敏捷性和降低成本。通过减少基础设施的配置时间,可以提高交付速度。通过最佳利用基础设施,可以降低成本。因此,云直接有助于实现交付速度和成本的双重目标。

如第九章中所讨论的,使用 Mesos 和 Marathon 管理 Docker 化的微服务,如果没有具有集群管理软件的云基础设施,部署微服务时很难控制基础设施成本。因此,具有自助服务能力的云对于微服务实现其全部潜力的好处至关重要。在微服务的背景下,云不仅帮助抽象物理基础设施,还提供了动态配置和自动部署的软件 API。这被称为基础设施即代码软件定义基础设施

DevOps 作为微服务的实践和流程

微服务是一种能够快速交付的架构风格。然而,单独的微服务无法提供期望的好处。一个基于微服务的项目,交付周期为 6 个月,无法实现期望的交付速度或业务敏捷性。微服务需要一套支持的交付实践和流程,才能有效实现其目标。

DevOps 是微服务交付的基础流程和实践的理想候选者。DevOps 的流程和实践与微服务架构的理念相契合。

微服务开发的实践要点

为了成功交付微服务,需要考虑一系列从开发到交付的实践,包括 DevOps 理念。在前几章中,您了解了微服务的不同架构能力。在本节中,我们将探讨微服务开发的非架构方面。

理解业务动机和价值

微服务不应该只是为了实现一种小众的架构风格而被使用。在选择微服务作为某个问题的架构解决方案之前,了解业务价值和业务关键绩效指标非常重要。对业务动机和业务价值的深刻理解将帮助工程师们以成本效益的方式专注于实现这些目标。

业务动机和价值应该证明选择微服务的合理性。此外,使用微服务,业务价值应该从业务角度实现。这将避免 IT 投资于微服务,但业务没有利用微服务所带来的任何好处的情况。在这种情况下,基于微服务的开发将成为企业的负担。

从项目开发到产品开发的思维方式转变

如第一章中所讨论的,解密微服务,微服务更加符合产品开发。使用微服务交付的业务能力应被视为产品。这也符合 DevOps 理念。

项目开发和产品开发的思维方式是不同的。产品团队总是有一种所有权感,并对他们所生产的产品负责。因此,产品团队总是努力提高产品的质量。产品团队不仅负责交付软件,还负责产品的生产支持和维护。

产品团队通常直接与他们正在开发产品的业务部门联系在一起。一般来说,产品团队既有 IT 代表又有业务代表。因此,产品思维与实际业务目标密切相关。产品团队随时了解他们为实现业务目标所增加的价值。产品的成功直接取决于产品所获得的业务价值。

由于高速发布周期,产品团队总是对他们的交付感到满意,并且总是努力改进。这给团队带来了更多的积极动力。

在许多情况下,典型的产品团队是为长期而获得资金支持并保持完整性的。因此,产品团队的凝聚力更强。由于团队规模较小,这些团队专注于根据他们日常的学习经验改进他们的流程。

产品开发中的一个常见陷阱是 IT 人员代表业务参与产品团队。这些 IT 代表可能并不完全理解业务愿景。此外,他们可能没有权力代表业务做出决定。这种情况可能导致与业务的不一致,并很快导致失败。

同时,考虑团队的协同工作也很重要,业务和 IT 代表在同一地方工作。团队的协同工作增加了 IT 和业务团队之间的联系,减少了沟通成本。

选择开发理念

不同的组织采取不同的方法来开发微服务,无论是迁移还是新开发。选择适合组织的方法非常重要。有各种各样的方法可供选择,本节将解释其中的一些。

设计思维

设计思维是一种主要用于创新型开发的方法。这是一种从最终用户的角度探索系统的方法:客户看到什么,以及他们如何体验解决方案。然后基于观察、模式、直觉和访谈建立一个故事。

设计思维通过采用一些理论、逻辑推理和对问题的假设,快速设计解决方案。在达成一致的解决方案之前,这些概念通过头脑风暴得到扩展。

一旦解决方案确定,就会快速建立原型,以考虑客户对其的反应,然后相应地调整解决方案。当团队获得满意的结果时,下一步是扩大产品规模。请注意,原型可能是代码形式,也可能不是。

设计思维使用以人为中心的思维,包括情感、共鸣、直觉和想象力。在这种方法中,即使是已知的问题,解决方案也会被重新思考,以找到创新和更好的解决方案。

创业模式

越来越多的组织都在遵循创业哲学来提供解决方案。组织内部创建创业团队,使命是提供特定的解决方案。这些团队远离日常组织活动,专注于实现他们的使命。

许多初创公司都以一个小而专注的团队开始——一个高度凝聚的单位。这个团队不担心他们如何实现事情;相反,他们关注的是他们想要实现什么。一旦他们有了一个产品,团队就会考虑正确的构建和扩展方式。

这种方法通过首先考虑生产来实现快速交付。这种方法的优势在于团队不受组织治理和政治挑战的干扰。团队被授权进行创新思考,创新并交付成果。通常,这些团队表现出更高的所有权水平,这是成功的关键因素之一。这些团队只采用足够的流程和纪律来推动解决方案。他们还采用快速失败的方法,尽早进行调整。

敏捷实践

最常用的方法是敏捷开发方法。在这种方法中,软件以增量、迭代的方式交付,使用敏捷宣言中提出的原则。这种开发方法使用敏捷方法,如 Scrum 或 XP。敏捷宣言定义了敏捷软件开发团队应该关注的四个关键点:

  • 个体和互动胜过流程和工具

  • 可工作的软件胜过详尽的文档

  • 与合同谈判相比,与客户合作

  • 响应变化胜过遵循计划

注意

敏捷软件开发的 12 个原则可以在www.agilemanifesto.org/principles.html找到。

使用最小可行产品的概念

不论之前解释的开发哲学如何,对于为了速度和敏捷性开发微服务系统来说,识别最小可行产品(MVP)是至关重要的。

Eric Ries 在开创精益创业运动时,将 MVP 定义为:

“最小可行产品是新产品的那个版本,它允许团队以最少的努力收集关于客户的最大量的验证学习。”

MVP 方法的目标是快速构建一个展示软件最重要方面的软件。MVP 方法实现了一个想法的核心概念,并可能选择那些为业务增加最大价值的功能。它有助于获得早期反馈,然后在构建重型产品之前进行必要的调整。

MVP 可能是一个针对有限用户群体的成熟服务,或者是针对更广泛用户群体的部分服务。来自客户的反馈在 MVP 方法中非常重要。因此,将 MVP 发布给真实用户非常重要。

克服遗留热点

在着手微服务开发之前,了解组织中的环境和政治挑战是很重要的。

在微服务中,通常会直接或间接地依赖其他遗留应用程序。直接遗留集成的常见问题是遗留应用程序的开发周期缓慢。例如,一个创新的铁路预订系统依赖于一个古老的事务处理设施TPF)来实现一些核心后端功能,比如预订。当将遗留的单片应用迁移到微服务时,这种情况尤其常见。在许多情况下,遗留系统继续以非敏捷方式进行开发,发布周期较长。在这种情况下,微服务开发团队可能无法因为与遗留系统的耦合而快速移动。集成点可能会严重拖累微服务的开发。组织政治挑战使情况变得更糟。

没有解决这个问题的万能方法。文化和流程差异可能是一个持续的问题。许多企业都会对这些遗留系统进行重点关注和投资,以支持快速移动的微服务。对这些遗留平台进行有针对性的 C 级干预可以减少开销。

解决数据库方面的挑战

自动化是微服务开发的关键。自动化数据库是许多微服务开发中的关键挑战之一。

在许多组织中,数据库管理员在数据库管理中扮演着关键角色,他们喜欢以不同的方式处理他们控制的数据库。保密性和数据访问控制也被认为是数据库管理员集中管理所有数据的原因。

许多自动化工具专注于应用逻辑。因此,许多开发团队完全忽视了数据库自动化。忽视数据库自动化可能严重影响整体效益,并可能使微服务开发偏离轨道。

为了避免这种情况,必须像应用程序一样对待数据库,具有适当的源代码控制和变更管理。在选择数据库时,考虑自动化也是一个关键因素。

在 NoSQL 数据库的情况下,数据库自动化要容易得多,但在传统的关系数据库管理系统中很难管理。数据库生命周期管理DLM)作为一个概念在 DevOps 世界中很受欢迎,特别是用于处理数据库自动化。诸如 DBmaestro、Redgate DLM、Datical DB 和 Delphix 等工具支持数据库自动化。

建立自组织团队

微服务开发中最重要的活动之一是建立正确的开发团队。正如许多 DevOps 流程所建议的,一个小而专注的团队总是能够取得最好的结果。

建立自组织团队

由于微服务与业务能力对齐,并且是相当松散耦合的产品,因此最好为每个微服务设立一个专门的团队。可能会出现同一团队拥有来自同一业务领域的多个微服务的情况,代表相关能力。这些通常由微服务的耦合和大小决定。

团队规模是建立有效的微服务开发团队的重要因素。一般的观念是,团队规模不应超过 10 人。最佳交付的推荐规模在 4 到 7 之间。Amazon.com 的创始人 Jeff Bezos 提出了双比萨团队的理论。杰夫的理论认为,如果团队规模变大,就会面临沟通问题。较大的团队需要达成共识,这会导致增加浪费。大团队也会失去所有权和责任。一个标准是产品负责人应该有足够的时间与团队中的个人交谈,让他们理解他们正在交付的价值。

团队被期望在构思、分析、开发和支持服务方面承担完全的责任。来自 Amazon.com 的 Werner Vogels 称之为你构建它,你运行它。根据 Werner 的理论,开发人员更加关注开发质量的代码,以避免意外的支持电话。团队成员包括全栈开发人员和运维工程师。这样的团队完全了解所有领域。开发人员了解运营,运营团队了解应用程序。这不仅减少了团队之间的混乱,还提高了质量。

团队应该具备多学科技能,以满足交付服务所需的所有能力。理想情况下,团队不应依赖外部团队来交付服务的各个组成部分。相反,团队应该是自给自足的。然而,在大多数组织中,挑战在于稀缺的专业技能。例如,组织中可能没有很多图数据库的专家。解决这个问题的一种常见方法是使用顾问的概念。顾问是专家,并被聘用来获得团队面临的特定问题的专业知识。一些组织还使用共享或平台团队来提供一些常见的能力。

团队成员应该完全了解产品,不仅从技术角度,还从业务案例和业务 KPI 的角度。团队应该在交付产品以及共同实现业务目标方面拥有集体所有权。

敏捷软件开发也鼓励建立自组织团队。自组织团队作为一个紧密的单位,找到了作为团队实现目标的方法。团队自动调整自己并分配责任。团队成员是自我管理的,并有权在日常工作中做出决策。这种团队的沟通和透明度非常重要。这强调了需要共同定位和合作,以实现高带宽的沟通:

建立自组织团队

在前面的图表中,微服务 A微服务 B都代表相关的业务能力。自组织团队平等对待团队中的每个人,团队内部没有太多的等级和管理开销。在这种情况下,管理会很薄。团队中不会有太多指定的垂直技能,比如团队负责人、UX 经理、开发经理、测试经理等等。在典型的微服务开发中,共享的产品经理、共享的架构师和共享的人员经理足以管理不同的微服务团队。在一些组织中,架构师也承担交付的责任。

自组织团队具有一定程度的自治权,并且有权以快速和敏捷的方式做出决策,而不必等待长时间运行的官僚决策过程,这种过程存在于许多企业中。在许多情况下,企业架构和安全性被视为事后考虑。然而,从一开始就将它们纳入是很重要的。在赋予团队最大自由度的同时,对开发人员的决策能力进行全面自动化的 QA 和合规性同样重要,以确保偏差能够尽早被捕捉到。

团队之间的沟通很重要。然而,在理想的世界中,它应该仅限于微服务之间的接口。团队之间的集成理想情况下应该通过消费者驱动的合同来处理,以测试脚本的形式,而不是通过描述各种场景的大型接口文档。当服务不可用时,团队应该使用模拟服务实现。

构建自助式云

在着手进行微服务之前,一个关键方面是构建一个云环境。当只有少量服务时,可以通过手动将它们分配到一组预先指定的虚拟机来轻松管理它们。

然而,微服务开发人员需要的不仅仅是一个 IaaS 云平台。团队中的开发人员和运维工程师都不应该担心应用程序部署在何处以及部署的最佳方式。他们也不应该担心如何管理容量。

这种复杂程度需要一个具有自助式能力的云平台,就像我们在第九章中讨论的那样,使用 Mesos 和 Marathon 管理 Docker 化的微服务,使用 Mesos 和 Marathon 集群管理解决方案。在第八章中讨论的容器化部署,使用 Docker 容器化微服务,在管理和端到端自动化中也很重要。构建这种自助式云生态系统是微服务开发的先决条件。

构建微服务生态系统

正如我们在第三章中讨论的能力模型,应用微服务概念,微服务需要许多其他能力。在实施大规模微服务之前,所有这些能力都应该就位。

这些能力包括服务注册、发现、API 网关和外部化配置服务。所有这些都由 Spring Cloud 项目提供。集中日志记录、监控等能力也是微服务开发的先决条件。

定义 DevOps 风格的微服务生命周期流程

DevOps 是微服务开发的最合适实践。已经实践 DevOps 的组织不需要另一种实践来进行微服务开发。

在本节中,我们将探讨微服务开发的生命周期。我们将从微服务的角度探索 DevOps 流程和实践,而不是为微服务重新发明流程。

在探索 DevOps 流程之前,让我们澄清一些 DevOps 世界中常用的术语:

  • 持续集成CI):这在指定环境中连续自动化应用程序构建和质量检查,可以按时间触发方式或开发人员提交方式进行。CI 还将代码指标发布到中央仪表板,以及将二进制构件发布到中央存储库。CI 在敏捷开发实践中很受欢迎。

  • 持续交付CD):这自动化了从构思到生产的软件交付实践。在非 DevOps 模型中,这以前被称为应用生命周期管理ALM)。持续交付的一个常见解释是它是持续集成的下一个演进,它将 QA 周期整合到集成管道中,并使软件准备好发布到生产环境。需要手动操作才能将其移动到生产环境。

  • 持续部署:这是一种自动化将应用程序二进制文件部署到一个或多个环境的方法,通过管理二进制文件的移动和相关配置参数。持续部署也被认为是持续交付的下一个演进,通过将自动发布流程整合到持续交付管道中。

  • 应用发布自动化ARA):ARA 工具帮助监视和管理端到端的交付管道。ARA 工具使用 CI 和 CD 工具,并管理发布管理批准的附加步骤。ARA 工具还能够在不同环境中部署发布,并在部署失败时回滚。ARA 提供了一个完全协调的工作流管道,通过整合许多专门的工具来实现交付生命周期,如存储库管理、质量保证、部署等。XL Deploy 和 Automic 是一些 ARA 工具。

以下图表显示了微服务开发的 DevOps 流程:

定义 DevOps 风格的微服务生命周期流程

现在让我们进一步探讨微服务开发的这些生命周期阶段。

价值驱动的规划

价值驱动的规划是敏捷开发实践中使用的术语。在微服务开发中,价值驱动的规划非常重要。在价值驱动的规划中,我们将确定要开发哪些微服务。最重要的是确定对业务价值最高和风险最低的需求。在从头开始开发微服务时,会使用 MVP 理念。在从单块到微服务的迁移中,我们将使用第三章中提供的指南,应用微服务概念,来确定首先采取哪些服务。选择的微服务预期能够精确地为业务提供预期的价值。作为价值驱动的规划的一部分,必须确定用于衡量这个价值的业务 KPI。

敏捷开发

一旦确定了微服务,开发必须采用敏捷方法,遵循敏捷宣言的原则。大多数组织在微服务开发中使用 Scrum 方法论。

持续集成

持续集成步骤应该自动构建各个团队成员产生的源代码并生成二进制文件。重要的是只构建一次,然后将二进制文件移动到后续阶段。持续集成还作为构建管道的一部分执行各种 QA,如代码覆盖率、安全检查、设计准则和单元测试用例。持续集成通常将二进制文件交付给二进制文件存储库,并将二进制文件部署到一个或多个环境中。部分功能测试也作为持续集成的一部分进行。

持续测试

一旦持续集成生成了二进制文件,它们就会被移动到测试阶段。在这个阶段,将启动完全自动化的测试循环。自动化测试作为测试阶段的一部分也很重要。自动化测试有助于提高可交付成果的质量。测试可能会在多个环境中进行,这取决于测试的类型。这可能从集成测试环境到生产环境,甚至在生产环境中进行测试。

持续发布

持续发布到生产环境负责实际部署、基础设施供应和发布。通过应用一定的规则,二进制文件会自动发货并部署到生产环境。许多组织在暂存环境中停止自动化,并使用手动批准步骤来移动到生产环境。

持续监控和反馈

持续监控和反馈阶段是敏捷微服务开发中最重要的阶段。在 MVP 场景中,这个阶段提供了有关 MVP 的初始接受情况的反馈,并评估了所开发服务的价值。在功能添加场景中,这进一步提供了有关用户如何接受这个新功能的见解。根据反馈,服务进行调整,然后重复相同的周期。

自动化持续交付管道

在前一节中,我们讨论了微服务开发的生命周期。生命周期阶段可以根据组织的需求以及应用程序的性质进行调整。在本节中,我们将看一下一个样本持续交付管道以及实施样本管道的工具集。

有许多工具可用于构建端到端管道,无论是在开源空间还是商业空间。组织可以选择他们选择的产品来连接管道任务。

提示

参考 XebiaLabs 周期表,了解构建持续交付管道的工具参考。网址为xebialabs.com/periodic-table-of-devops-tools/

最初设置管道可能会很昂贵,因为它们需要许多工具集和环境。组织可能无法立即从实施交付管道中获得成本效益。此外,构建管道需要高性能资源。大型构建管道可能涉及数百台机器。从一个端到另一个端将更改移动到管道需要数小时。因此,对于不同的微服务需要不同的管道非常重要。这也有助于解耦不同微服务的发布。

在管道中,应该使用并行处理在不同环境中执行测试。并且尽可能并行执行测试用例是很重要的。因此,根据应用程序的性质设计管道是很重要的。没有一刀切的情况。

管道的重点在于端到端的自动化,从开发到生产,并且在出现问题时快速失败。

以下管道是微服务的一个指示性示例,并探讨了在开发微服务管道时应考虑的不同能力:

自动化持续交付管道

持续交付管道阶段在以下部分进行了解释。

开发

开发阶段从开发的角度来看有以下活动。本节还指出了一些可以在开发阶段使用的工具。这些工具是除了规划、跟踪和沟通工具(如敏捷 JIRA、Slack 等)之外,敏捷开发团队使用的。看一下以下内容:

  • 源代码:开发团队需要 IDE 或开发环境来编写源代码。在大多数组织中,开发人员可以自由选择他们想要的 IDE。话虽如此,IDE 可以与许多工具集成以检测违反指南的情况。一般来说,Eclipse IDE 具有用于静态代码分析和代码矩阵的插件。SonarQube 是一个集成其他插件的例子,例如用于代码约定的 Checkstyle,用于检测不良实践的 PMD,用于检测潜在错误的 FindBugs,以及用于代码覆盖率的 Cobertura。还建议使用 Eclipse 插件,如 ESVD、Find Security Bugs、SonarQube Security Rules 等,以检测安全漏洞。

  • 单元测试用例:开发团队还使用 JUnit、NUnit、TestNG 等工具生成单元测试用例。单元测试用例针对组件、存储库、服务等进行编写。这些单元测试用例与本地 Maven 构建集成。针对微服务端点的单元测试用例(服务测试)作为回归测试包。如果使用 AngularJS 编写 Web UI,可以使用 Karma 进行测试。

  • 消费者驱动契约:开发人员还编写 CDC 来测试与其他微服务的集成点。契约测试用例通常以 JUnit、NUnit、TestNG 等形式编写,并添加到前面步骤中提到的服务测试包中。

  • 模拟测试:开发人员还编写模拟来模拟集成端点以执行单元测试用例。通常使用 Mockito、PowerMock 等进行模拟测试。一旦确定了服务契约,部署基于契约的模拟服务是一个良好的做法。这对于后续阶段的服务虚拟化是一个简单的机制。

  • 行为驱动设计(BDD):敏捷团队还使用 BDD 工具(如 Cucumber)编写 BDD 场景。通常,这些场景针对微服务契约或微服务 Web 应用程序公开的用户界面。这些场景分别使用 JUnit 和 Selenium WebDriver 的 Cucumber。不同的场景用于功能测试、用户旅程测试以及验收测试。

  • 源代码存储库:源代码存储库是开发的一部分。开发人员通过 IDE 插件将他们的代码提交到中央存储库。许多组织使用每个微服务一个存储库的常见模式。这样可以阻止其他微服务开发人员修改其他微服务或根据其他微服务的内部表示编写代码。Git 和 Subversion 是流行的源代码存储库选择。

  • 构建工具:构建工具(如 Maven 或 Gradle)用于管理依赖关系和构建目标工件,例如 Spring Boot 服务。构建本身集成了许多情况,例如基本质量检查、安全检查和单元测试用例、代码覆盖率等。这些与 IDE 类似,特别是当开发人员不使用 IDE 时。作为 IDE 的一部分考虑的工具也作为 Maven 插件可用。开发团队在项目的 CI 阶段之前不使用 Docker 等容器。所有工件都必须正确进行版本控制以适应每一次更改。

  • 工件存储库:工件存储库在开发过程中扮演着关键角色。工件存储库是存储所有构建工件的地方。工件存储库可以是 Artifactory、Nexus 或类似产品。

  • 数据库模式:Liquibase 和 Flyway 通常用于管理、跟踪和应用数据库更改。Maven 插件允许与 Liquibase 或 Flyway 库进行交互。模式更改被版本化和维护,就像源代码一样。

持续集成

一旦代码提交到存储库,下一个阶段——持续集成,就会自动开始。这是通过配置 CI 管道来实现的。该阶段使用存储库快照构建源代码并生成可部署的构件。不同的组织使用不同的事件来启动构建。CI 启动事件可能是每次开发人员提交或基于时间窗口,如每天、每周等。

CI 工作流程是该阶段的关键方面。持续集成工具,如 Jenkins、Bamboo 等,扮演着编排构建管道的中心角色。该工具配置了要调用的活动工作流程。工作流程会自动执行配置的步骤,如构建、部署和 QA。在开发人员提交或一定频率上,CI 启动工作流程。

在持续集成工作流程中进行以下活动:

  1. 构建和 QA:工作流程监听 Git Webhooks 以进行提交。一旦检测到变化,第一步是从存储库下载源代码。对下载的快照源代码进行构建。作为构建的一部分,会自动执行许多 QA 检查,类似于在开发环境中执行的 QA。这些包括代码质量检查、安全检查和代码覆盖率。许多 QA 都是使用诸如 SonarQube 之类的工具进行的,还有前面提到的插件。它还收集诸如代码覆盖率等代码指标,并将其发布到用于分析的中央数据库。使用 OWASP ZAP Jenkins 的插件执行额外的安全检查。作为构建的一部分,还会执行 JUnit 或类似的用于编写测试用例的工具。如果 Web 应用程序支持 Karma 进行 UI 测试,Jenkins 也能够运行使用 Karma 编写的 Web 测试。如果构建或 QA 失败,它会按照系统中的配置发送警报。

  2. 打包:一旦构建和 QA 通过,CI 会创建一个可部署的软件包。在我们的微服务案例中,它会生成 Spring Boot 独立的 JAR。建议在集成构建的过程中构建 Docker 镜像。这是我们构建二进制构件的唯一地方。构建完成后,它会将不可变的 Docker 镜像推送到 Docker 注册表。这可以是在 Docker Hub 上或私有的 Docker 注册表上。在这个阶段,正确地对容器进行版本控制非常重要。

  3. 集成测试:Docker 镜像被移动到集成环境,进行回归测试(服务测试)等。该环境具有其他依赖的微服务功能,如 Spring Cloud、日志记录等。所有依赖的微服务也存在于此环境中。如果实际的依赖服务尚未部署,将使用服务虚拟化工具,如 MockServer。或者,由各自的开发团队将服务的基本版本推送到 Git。一旦成功部署,Jenkins 会触发服务测试(针对服务的 JUnits)、一组在 Selenium WebDriver 中编写的端到端健全性测试(在 Web 的情况下)以及使用 OWASP ZAP 进行安全测试。

自动化测试

在宣布构建准备投入生产之前,需要执行许多类型的测试作为自动交付过程的一部分。测试可能通过将应用程序移动到多个环境中进行。每个环境都专门用于特定类型的测试,如验收测试、性能测试等。这些环境都受到充分的监控,以收集相应的指标。

在复杂的微服务环境中,测试不应该被视为最后一分钟的门禁检查;相反,测试应该被视为提高软件质量以及避免最后一分钟失败的一种方式。左移测试是一种尽早将测试移到发布周期中的方法。自动化测试将软件开发转变为每天开发和每天测试的模式。通过自动化测试用例,我们将避免手动错误以及完成测试所需的工作。

CI 或 ARA 工具用于在多个测试环境中移动 Docker 镜像。一旦部署在环境中,测试用例将根据环境的目的执行。默认情况下,会执行一组理智测试来验证测试环境。

在本节中,我们将涵盖自动交付流程中所需的所有测试类型,而不管环境如何。我们已经将一些测试类型视为开发和集成环境的一部分。在本节的后面,我们还将对测试用例与执行环境进行映射。

自动化候选测试

在本节中,我们将探讨在设计端到端交付流程时候自动化的不同类型的测试。关键的测试类型描述如下。

自动化理智测试

在从一个环境迁移到另一个环境时,建议运行一些理智测试,以确保所有基本事项都正常工作。这是使用 JUnit 服务测试、Selenium WebDriver 或类似工具创建的测试包。重要的是要仔细识别和编写所有关键的服务调用。特别是如果微服务使用同步依赖进行集成,最好考虑这些场景,以确保所有依赖服务也正常运行。

回归测试

回归测试确保软件的更改不会破坏系统。在微服务环境中,回归测试可以在服务级别(Rest API 或消息端点)进行,并使用 JUnit 或类似的框架编写,如前所述。当依赖服务不可用时,会使用服务虚拟化。Karma 和 Jasmine 可用于 Web UI 测试。

在微服务用于 Web 应用程序背后的情况下,使用 Selenium WebDriver 或类似工具准备回归测试包,并在 UI 级别进行测试,而不是专注于服务端点。或者,也可以使用 BDD 工具,如 Cucumber 与 JUnit 或 Cucumber 与 Selenium WebDriver,来准备回归测试包。CI 工具如 Jenkins 或 ARA 用于自动触发回归测试包。还有其他商业工具,如 TestComplete,也可以用于构建回归测试包。

自动化功能测试

功能测试用例通常针对消费微服务的 UI。这些是基于用户故事或功能的业务场景。这些功能测试在每次构建时执行,以确保微服务的性能符合预期。

BDD 通常用于开发功能测试用例。通常在 BDD 中,业务分析师用特定领域的语言但用简单的英语编写测试用例。然后开发人员添加脚本来执行这些场景。自动化的 Web 测试工具如 Selenium WebDriver 在这种情况下非常有用,还有 BDD 工具如 Cucumber,JBehave,SpecFlow 等。在无头微服务的情况下使用 JUnit 测试用例。有一些流水线将回归测试和功能测试结合为一个步骤,并使用相同的测试用例。

自动化验收测试

这与前面的功能测试用例非常相似。在许多情况下,自动接受测试通常使用屏幕剧本或旅程模式,并应用于 Web 应用程序级别。在构建测试用例时使用客户视角,而不是特性或功能。这些测试模拟用户流程。

BDD 工具,如 Cucumber、JBehave 和 SpecFlow 通常与 JUnit 或 Selenium WebDriver 一起在这些场景中使用,如前面的场景中所讨论的。功能测试和接受测试中测试用例的性质是不同的。通过将它们与 Jenkins 集成,可以实现接受测试包的自动化。市场上还有许多其他专门的自动接受测试工具。FitNesse 就是这样一种工具。

性能测试

将性能测试自动化作为交付管道的一部分非常重要。这将性能测试从门控模型转变为交付管道的一个组成部分。通过这样做,可以在构建周期的早期阶段识别瓶颈。在一些组织中,性能测试仅针对重大发布进行,但在其他组织中,性能测试是管道的一部分。性能测试有多种选择。诸如 JMeter、Gatling、Grinder 等工具可用于负载测试。这些工具可以集成到 Jenkins 工作流程中进行自动化。然后可以使用 BlazeMeter 等工具进行测试报告。

应用性能管理工具,如 AppDynamics、New Relic、Dynatrace 等,作为交付管道的一部分提供质量指标。这可以在性能测试环境中使用这些工具来完成。在某些管道中,这些工具被集成到功能测试环境中,以获得更好的覆盖范围。Jenkins 具有插件来获取测量数据。

真实用户流程模拟或旅程测试

这是另一种测试形式,通常用于分期和生产环境。这些测试在分期和生产环境中持续运行,以确保所有关键交易的执行符合预期。这比典型的 URL ping 监控机制更有用。通常类似于自动接受测试,这些测试用例模拟用户在现实世界中发生的旅程。这也有助于检查依赖的微服务是否正常运行。这些测试用例可以是接受测试用例的一个子集,或者使用 Selenium WebDriver 创建的测试包。

自动化安全测试

确保自动化不违反组织的安全政策非常重要。安全是最重要的事情,为了速度而牺牲安全是不可取的。因此,将安全测试作为交付管道的一部分是很重要的。一些安全评估已经集成在本地构建环境以及集成环境中,例如 SonarQube、Find Security Bugs 等。一些安全方面作为功能测试用例的一部分。诸如 BDD-Security、Mittn 和 Gauntlt 等工具是遵循 BDD 方法的其他安全测试自动化工具。可以使用 ImmuniWeb 等工具进行 VAPT。OWASP ZAP 和 Burp Suite 是安全测试中的其他有用工具。

探索性测试

探索性测试是测试人员或业务用户采用的手动测试方法,用于验证他们认为自动化工具可能无法捕捉的特定场景。测试人员以任何他们想要的方式与系统交互,而不带有偏见。他们利用自己的智慧来识别他们认为某些特殊用户可能会探索的场景。他们还通过模拟某些用户行为进行探索性测试。

A/B 测试、金丝雀测试和蓝绿部署

将应用程序移至生产时,通常会应用 A/B 测试、蓝绿部署和金丝雀测试。A/B 测试主要用于审查变更的有效性以及市场对变更的反应。新功能会向一定数量的用户推出。金丝雀发布是将新产品或功能先移至特定社区,然后再完全推向所有客户。蓝绿部署是从 IT 角度来看的一种部署策略,用于测试服务的新版本。在这种模式下,蓝色和绿色版本在某个时间点同时运行,然后从一个版本优雅地迁移到另一个版本。

其他非功能测试

在生产之前执行高可用性和抗脆弱性测试(故障注入测试)也很重要。这有助于开发人员发现在真实生产场景中可能发生的未知错误。通常通过破坏系统的组件来理解它们的故障转移行为来完成。这也有助于测试系统中的断路器和备用服务。在这些情况下,Simian Army 等工具非常有用。

在生产中进行测试

在生产中进行测试(TiP)与其他环境一样重要,因为我们只能模拟到一定程度。通常有两种类型的测试针对生产环境执行。第一种方法是以连续的方式运行真实用户流或旅程测试,模拟各种用户操作。这是使用 Real User Monitoring(RUM)工具之一,如 AppDynamics 自动化的。第二种方法是窃听生产中的消息,将它们在暂存环境中执行,然后将结果与生产环境中的结果进行比较。

抗脆弱性测试

通常在与生产环境相同的预生产环境中进行抗脆弱性测试,甚至在生产环境中通过在环境中制造混乱来观察应用程序如何响应并从这些情况中恢复。随着时间的推移,应用程序能够自动从大多数故障中恢复。Simian Army 是 Netflix 的一个这样的工具。Simian Army 是专为 AWS 环境构建的一套产品。Simian Army 用于使用一组自主猴子在预生产或生产环境中制造混乱的破坏性测试。混沌猴、清洁工猴和一致性猴是 Simian Army 的一些组件。

目标测试环境

针对不同的测试环境和这些环境中执行的测试类型如下:

  • 开发环境:开发环境用于测试编码风格检查、不良实践、潜在错误、单元测试和基本安全扫描。

  • 集成测试环境:集成环境用于跨多个微服务的单元测试和回归测试。一些基本的与安全相关的测试也在集成测试环境中执行。

  • 性能和诊断:性能测试在性能测试环境中执行。应用程序性能测试工具部署在这些环境中,以收集性能指标并识别瓶颈。

  • 功能测试环境:功能测试环境用于执行健全性测试和功能测试包。

  • UAT 环境:UAT 环境具有健全性测试、自动接受测试包和用户旅程模拟。

  • 暂存:预生产环境主要用于健全性测试、安全性、抗脆弱性、网络测试等。它也用于用户旅程模拟和探索性测试。

  • 生产:用户旅程模拟和 RUM 测试在生产环境中持续执行。

在多个环境中提供适当的数据以支持测试用例是最大的挑战。Delphix 是一种有效处理跨多个环境的测试数据的工具。

持续部署

持续部署是将应用程序部署到一个或多个环境,并相应地配置和提供这些环境的过程。如第九章中所讨论的,使用 Mesos 和 Marathon 管理 Docker 化的微服务,基础设施配置和自动化工具促进了部署自动化。

从部署的角度来看,一旦所有质量检查成功完成,发布的 Docker 镜像会自动移动到生产环境。在这种情况下,生产环境必须是基于云的,具有 Mesos 或 Marathon 等集群管理工具。必须具有监控能力的自助式云环境是强制性的。

集群管理和应用程序部署工具确保应用程序依赖项得到正确部署。这会自动部署所有需要的依赖项,以防缺失。它还确保在任何时间点都运行最少数量的实例。在发生故障时,它会自动回滚部署。它还会优雅地回滚升级。

Ansible、Chef 或 Puppet 是将配置和二进制文件移动到生产环境中的有用工具。可以使用 Ansible playbook 概念来启动具有 Marathon 和 Docker 支持的 Mesos 集群。

监控和反馈

一旦应用程序在生产环境中部署,监控工具会持续监控其服务。监控和日志管理工具收集和分析信息。根据反馈和所需的纠正措施,信息被反馈给开发团队采取纠正措施,并通过流水线将更改推回生产。诸如 APM、Open Web Analytics、Google Analytics、Webalizer 等工具对监控 Web 应用程序非常有用。真实用户监控应提供端到端的监控。QuBit、Boxever、Channel Site、MaxTraffic 等工具也对分析客户行为非常有用。

自动化配置管理

配置管理也必须从微服务和 DevOps 的角度重新思考。使用新的配置管理方法,而不是使用传统的静态配置的 CMDB。手动维护 CMDB 已不再是一个选择。静态管理的 CMDB 需要大量单调的任务来维护条目。同时,由于部署拓扑的动态性质,以一致的方式维护数据是非常困难的。

CMDB 的新样式会根据运行拓扑自动创建配置项配置。这些应该是基于发现的,以获取最新的信息。新的 CMDB 应该能够管理裸金属、虚拟机和容器。

微服务开发治理、参考架构和库

拥有整体企业参考架构和微服务开发的标准工具集非常重要,以确保开发以一致的方式进行。这有助于个别微服务团队遵守某些最佳实践。每个团队可能会确定适合其开发的专业技术和工具。在多语言微服务开发中,显然有不同团队使用多种技术。然而,他们必须遵守总体原则和实践。

为了快速取得成功并利用时间表,微服务开发团队在某些情况下可能会偏离这些做法。只要团队在他们的待办事项中添加重构任务,这是可以接受的。在许多组织中,尽管团队试图从企业中重复使用某些东西,但重复使用和标准化通常是事后想到的。

重要的是要确保服务在企业中被编目并可见。这提高了微服务的重复使用机会。

摘要

在本章中,您了解了微服务与 DevOps 之间的关系。我们还审查了开发微服务时的一些实践要点。最重要的是,您了解了微服务开发生命周期。

在本章的后面,我们还研究了如何将微服务交付流水线从开发自动化到生产。作为其中的一部分,我们研究了一些在自动化微服务交付流水线时有帮助的工具和技术。最后,我们强调了微服务治理中参考架构的重要性。

将本书涵盖的微服务概念、挑战、最佳实践和各种能力结合起来,是开发成功的大规模微服务的完美配方。

posted @ 2024-05-24 10:56  绝不原创的飞龙  阅读(169)  评论(0)    收藏  举报