C-8-和--NET-Core3-微服务实用手册-全-

C#8 和 .NET Core3 微服务实用手册(全)

原文:Hands-On Microservices with C# 8 and .NET Core 3

协议:CC BY-NC-SA 4.0

零、前言

这本书的目的是让你对现代软件开发中的微服务有一个广泛的了解,同时也用具体的例子来深入了解更多的细节.NET Core 应用接口。这本书涵盖了微服务的理论,然后是对用于开发虚拟应用的模式的高级理解,其中我们将涵盖解决方案中使用的概念,这些解决方案可以轻松配置为在云中或内部运行。

值得注意的是,微服务架构确实具有关键优势,尤其是在支持敏捷改进和复杂风险应用的传输方面。

然而,对于如何在微软生态系统中实现微服务,尤其是如何利用 Azure 和.NET Core 框架。这本书试图填补这一空白。

虽然微服务架构的许多方面不需要详细的解释,但我们试图总结这些概念,并提供微软文档链接供进一步阅读。在本书中,在我们开始开发我们想象中的应用之前,我们将涵盖开发基于微服务架构的应用所需的每一部分。

从一个简单的应用编程接口项目和整体应用开始,这本书描述了您可以执行的过渡到微服务的各种步骤。为了使代码示例更简单,本书使用了.NET Core 应用接口。最后一章包括一个应用,显示了整本书的关键。

这本书是给谁的

本书的目标读者包括希望在 Azure 云中找到设计高性能应用的一站式最佳实践的应用开发人员。这本书适用于 Azure 的所有开发者。它还面向希望了解和理解微服务体系结构并在其.NET Core 应用。这本书的编写/更新方式涵盖了从新手到高级用户。对于完全不熟悉微服务或者对这种体系结构方法只有理论上的了解,并且希望获得实用视角以便更好地管理其应用复杂性的开发人员来说,这是理想的选择。

这本书涵盖了什么

第 1 章微服务简介,讨论了微服务的基础,回顾了一个整体应用,并揭示了它的局限性。您还将学习如何开始向微服务架构过渡。

第 2 章重构整块,讨论应用的当前堆栈。它还包括的功能.NET Core 和 C#8,演示了如何实现独立的微服务,并研究了微服务之间的通信。

第三章服务间的有效通信,涵盖了服务间的通信,包括同步和异步通信,以及 Azure 服务总线的概述。此外,本章借助于集成模式检查集成。在概述 Azure 服务结构之后,我们将了解 Docker 和容器,以及 Azure Kubernetes 服务、物联网集线器和物联网边缘的概述。

第 4 章用微软单元测试框架测试微服务,涵盖了各种类型的服务及其差异。在这里,您将使用微软单元测试框架、Moq 和 ASP.NET Core 应用编程接口实现测试方法。

第 5 章使用 Docker 部署微服务,涵盖了部署范例并解释了部署术语。

第 6 章使用 Azure Active Directory 保护微服务揭示了通过使用 Azure Active Directory 部署示例应用来保护微服务的概念。

第 7 章监控微服务,涵盖仪器仪表和遥测,其次是监控策略、日志记录和云中监控。

第 8 章使用 Azure 扩展微服务,探讨可扩展性、基础架构扩展和微服务扩展。本章还概述了 Azure Redis 缓存。

第 9 章反应式微服务介绍,借助代码示例为大家介绍反应式微服务。

第 10 章设计模式和最佳实践,涵盖了帮助构建微服务的高级模式,以及聚合器、DDD、API 网关、共享数据微服务模式、反腐败层模式和 BFF。

第 11 章构建微服务应用,研究了开发真实应用的各种方法。

第 12 章微服务架构总结着眼于未来应用如何通过遵循微服务方法而发展。

附录,解释了 API 网关模式和后端到前端模式的优缺点,帮助我们了解它们的最佳实践。

充分利用这本书

本书假设您对 SOA、RESTful web 服务、API、服务器/客户端体系结构有一定的了解.NET Core、ASP.NET Core 和 C#。本书涵盖了高级主题、基本概念以及 Kubernetes 和 IoT Edge 等技术的概述。这本书的内容旨在帮助您开始开发基于微服务的应用。也可以作为综合指南。使用工具箱类比,这本书为现代应用开发人员提供了大量的工具,从低级代码设计发展到更高级的体系结构,以及当今基于微服务的应用开发中常用的重要概念、模式和最佳实践。

本书将涵盖以下要点:

  • 微服务的细节,包括整体与 SOA 和微服务架构的深入细节
  • 使用 C#8 和 ASP.NET Core 3 的示例
  • 当前应用堆栈和通过虚拟应用开发微服务的新堆栈的概述
  • 使用演示应用深入讨论设计模式和最佳实践

您需要安装带有最新更新的 Visual Studio 2019(最好是社区版)。所有代码示例都使用进行了测试。Windows 操作系统上的. NET Core 3.1。然而,它们也应该适用于未来的版本发布。

| 书中介绍的软件/硬件 | 操作系统要求 |
| ASP.NET Core 3.1 | Windows 操作系统 |
| C# 8 | Windows 操作系统 |
| SQL Server 2008R2 | Windows 操作系统 |
| SQL Server 2017 | Windows 操作系统 |

如果您正在使用本书的数字版本,我们建议您自己键入代码或通过 GitHub 存储库访问代码(下一节中提供了链接)。这样做将帮助您避免任何与复制/粘贴代码相关的潜在错误。

Few of the code example uses Angular 8  to showcase the UI part. There is no code for these component as these are only for UI and you would require to setup Angular 8 on your Windows OS.

下载示例代码文件

你可以从你在www.packt.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册将文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com
  2. 选择“支持”选项卡。
  3. 点击代码下载和勘误表。
  4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。

下载文件后,请确保使用最新版本的解压缩文件夹:

  • 视窗系统的 WinRAR/7-Zip
  • zipeg/izp/un ARX for MAC
  • 适用于 Linux 的 7-Zip/PeaZip

这本书的代码包也托管在 GitHub 上,网址为 https://GitHub . com/PacktPublishing/hand-On-microservice-with-CSharp-8-and-。网络核心 3 第三版。如果代码有更新,它将在现有的 GitHub 存储库中更新。

我们还有来自丰富的图书和视频目录的其他代码包,可在【https://github.com/PacktPublishing/】获得。看看他们!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:https://static . packt-cdn . com/downloads/9781789617948 _ color images . pdf

使用的约定

本书通篇使用了许多文本约定。

CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。下面是一个例子:“前面的代码用预先指定的值声明了两个readonly属性。”

代码块设置如下:

Range book = 1..4;
var res = Books[book] ;
Console.WriteLine($"\tElement of array using Range: Books[{book}] => {Books[book]}");

当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示:

private static readonly int num1=5;
private static readonly int num2=6;

任何命令行输入或输出都编写如下:

dotnet --info

粗体:表示一个新的术语,一个重要的单词,或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”

Warnings or important notes appear like this. Tips and tricks appear like this.

取得联系

我们随时欢迎读者的反馈。

一般反馈:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们customercare@packtpub.com

勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的图书,点击勘误表提交链接,并输入详细信息。

盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com联系我们,并提供材料链接。

如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com

复习

请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!

更多关于 Packt 的信息,请访问packt.com

一、微服务简介

本章的重点是让您熟悉微服务。将您的应用分割成许多服务既不是面向服务架构的特征,也不是微服务的特征。然而,微服务结合了服务设计和来自 SOA 世界的最佳实践,以及一些新兴的实践,例如隔离部署、语义版本控制、提供轻量级服务以及多语种编程中的服务发现。我们实施微服务来满足业务功能,从而缩短上市时间并提高灵活性。

我们将在本章中讨论以下主题:

  • 微服务的起源
  • 讨论微服务
  • 了解微服务架构
  • 微服务的优势
  • SOA 与微服务
  • 理解整体建筑风格的问题
  • . NET 堆栈标准化的挑战
  • Azure 服务结构概述

在这一章中,我们将熟悉分层单片体系结构带来的问题。我们还将讨论在整体世界中这些问题的解决方案。在这一章的最后,我们将能够把一个单一的应用分解成一个微服务架构。

技术要求

本章包含各种代码示例来解释这些概念。代码保持简单,仅用于演示目的。

要运行和执行代码,您需要以下先决条件:

  • Visual Studio 2019
  • 。网络核心 3.1

要安装和运行这些代码示例,您需要安装 Visual Studio 2019(首选 IDE)。为此,请从安装说明中提到的下载链接下载 Visual Studio 2019(社区版,免费):https://docs . Microsoft . com/en-us/visualstudio/install/install-Visual Studio。Visual Studio 安装有多个版本。我们使用的是视窗操作系统。

如果你没有.NET Core 3.1 安装完毕,可以从以下链接下载:https://www.microsoft.com/net/download/windows

The complete source code is available here: https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2001

微服务的起源

在讨论细节之前,我们应该探索微服务的起源或者任何新的框架、语言等等。微服务是一个流行词,我们应该意识到这种架构风格是如何发展到现在这个趋势的。有几个原因让你熟悉任何语言或框架的起源。要知道的最重要的事情如下:

  • 特定的语言或框架是如何进入语境的。
  • 谁是微服务新趋势架构风格的幕后推手?
  • 它是在什么地方建立的。

现在我们来讨论一下微服务的起源。术语微服务在 2011 年年中的一次软件架构师研讨会上首次被使用。2012 年 3 月,詹姆斯·刘易斯提出了他关于微服务的一些想法。到 2013 年底,来自 IT 行业的各种团体开始讨论微服务,到 2014 年,这个概念变得足够流行,被认为是大型企业的有力竞争者。

微服务没有官方定义。对这个术语的理解纯粹是基于用例和过去的讨论。

2014 年,詹姆斯·刘易斯和马丁·福勒走到一起,提供了一些真实世界的例子,并展示了微服务(参考http://martinfowler.com/microservices/)。

The development of a single application by combining small services (running on their processes) is called a microservice architectural style. Each service carries business capabilities and is independently deployed. Moreover, these services can be written in different languages and have different database storage.

微服务的微软官方文档页面(参考https://docs . Microsoft . com/en-us/azure/architecture/guide/architecture-style/微服务)对微服务架构风格的定义如下:

"Microservices architecture is a bunch of services, where each service is independently deployed and should implement a single business capability."

看到刘易斯和福勒在这里定义的所有属性是非常重要的。他们将微服务定义为一种架构风格,开发人员可以利用这种风格来开发单个应用,业务逻辑分布在一堆小服务中,每个服务都有自己的持久存储功能。此外,请注意它的属性:它可以独立部署,可以在自己的进程中运行,是一种轻量级的通信机制,并且可以用不同的编程语言编写。

我们想强调这个具体的定义,因为它是整个概念的关键。随着我们的前进,当我们读完这本书时,所有的部分都会合在一起。目前,我们将详细研究微服务。

讨论微服务

我们已经讨论了微服务的一些定义;现在让我们详细讨论一下。

简而言之,微服务架构消除了 SOA 的大部分缺点。它也比 SOA 服务更面向代码(我们将在接下来的章节中详细讨论这一点)。

在我们继续理解架构之前,让我们讨论导致其存在的两个重要架构:

  • 整体建筑风格
  • 前进速度

我们大多数人都知道,当我们开发一个企业应用时,我们必须选择一种合适的架构风格。然后,在不同的阶段,初始模式被进一步改进和调整,以适应各种挑战,例如部署复杂性、庞大的代码库和可伸缩性问题。这正是单体架构风格如何演变成 SOA,然后导致微服务的原因。

探索整体架构

单体建筑风格是一种传统的建筑类型,在信息技术行业得到了广泛的应用。monolithic 一词并不新鲜,它是从 Unix 世界借用来的。在 Unix 中,大多数命令作为独立程序存在,其功能不依赖于任何其他程序。如下图所示,应用中可以有不同的组件,包括:

  • 用户界面:这处理所有的用户交互,同时用 HTML、JSON 或任何其他首选的数据交换格式(在网络服务的情况下)进行响应。
  • 业务逻辑:这包括应用于以用户输入、事件和数据库形式接收的输入的所有业务规则。
  • 数据库访问:这包含了为了查询和持久化对象而访问数据库的完整功能。一个被广泛接受的规则是,它是通过业务模块使用的,而不是直接通过面向用户的组件。

使用这种架构构建的软件是独立的。我们可以想象一个。包含各种组件的. NET 程序集,如下图所示:

因为软件在这里是独立的,所以它的组件是相互关联和相互依赖的。即使其中一个模块中的简单代码更改也可能会破坏其他模块中的主要功能。这将导致我们需要测试整个应用的场景。由于业务严重依赖其企业应用框架,这一时间可能非常关键。

让所有组件紧密耦合会带来另一个挑战:每当我们执行或编译这样的软件时,所有组件都应该可用,否则构建将会失败。参考上图,该图代表单片架构,是独立的或单一的.NET 程序集项目。然而,单体架构也可能有多个程序集。这意味着,即使一个业务层(程序集、数据访问层程序集等)是分离的,但它们都将聚集在一起,在运行时作为一个进程运行。

用户界面依赖于其他组件的直接销售和库存,其方式类似于所有其他相互依赖的组件。在这种情况下,如果没有这些组件中的任何一个,我们将无法执行这个项目。升级它们的过程将会更加复杂,因为我们将不得不考虑其他需要修改代码的组件。这将导致比实际变更所需的更多的开发时间。

部署这样的应用将成为另一个挑战。在部署过程中,我们必须确保每个组件都被正确部署。如果我们不这样做,我们可能会在生产环境中面临许多问题。

如前所述,如果我们使用单体架构风格开发应用,我们可能会面临以下挑战:

  • 大代码库:这是一个代码行比注释多很多的场景。由于组件是互连的,我们将不得不处理重复的代码库。
  • 业务模块太多:这是针对同一系统内的模块。
  • 代码库复杂性:由于其他模块或服务需要修复,这导致代码被破坏的几率更高。
  • 复杂的代码部署:您可能会遇到需要整个系统部署的微小变化。
  • 影响整个系统的一个模块故障:这是针对相互依赖的模块。
  • 可扩展性:这是整个系统需要的,而不仅仅是其中的模块。
  • 模块间依赖:这是由于紧耦合。如果任何模块的操作需要,这将导致重大变化。

Tight coupling is a scenario in which one class is intended for many responsibilities, or in other words, when classes (mostly a group of classes) are dependent on each other.

  • 螺旋式上升的开发时间:这是由于代码的复杂性和相互依赖性。
  • 无法轻松适应新技术:在这种情况下,整个系统都需要升级。

如前所述,如果我们想要减少开发时间,简化部署,并提高企业应用软件的可维护性,我们应该避免传统或单片架构。因此,我们将着眼于 SOA。

面向服务的架构

在前一节中,我们讨论了单片架构及其局限性。我们还讨论了为什么它不适合我们的企业应用需求。为了克服这些问题,我们应该采取模块化的方法,将组件分开,使它们独立出来.NET 程序集。以上图中描述的方式使用一个服务或多个服务的系统被称为面向服务的体系结构 e ( SOA )。

The main difference between SOA and monolithic architecture is not one or multiple assemblies. As the service in SOA runs as a separate process, SOA scales better in comparison.

我们来讨论一下模块化架构,也就是 SOA。这是一种著名的架构风格,其中企业应用被设计为服务的集合。这些服务可能是 RESTful 或 ASMX 网络服务。为了更详细地理解 SOA,让我们首先讨论服务。

了解服务

在这种情况下,服务是 SOA 的一个基本概念。它们可以是一段代码、一个程序或向其他系统组件提供功能的软件。这段代码可以直接与数据库交互,也可以通过另一个服务间接交互。此外,它可以被客户端直接消费,其中客户端可以是网站、桌面应用、移动应用或任何其他设备应用。下图显示了各种客户端可以通过网络、桌面、移动或任何其他设备使用服务。服务可以在后端有或没有数据库支持:

服务是指由其他系统(通常称为客户端 / 客户端应用)公开使用的一种功能。如前所述,这可以用一段代码、一个程序或软件来表示。这种服务通常通过 HTTP 传输协议公开。然而,HTTP 协议不是一个限制因素,可以选择一个被认为适合该场景的协议。

下图中,服务-直销直接与数据库交互,三个不同的客户端,即 Web桌面移动正在消费服务。另一方面,我们有客户消费服务合作伙伴销售,这与服务渠道合作伙伴进行数据库访问。

产品销售服务是一组与客户端应用交互并直接或通过另一项服务提供数据库访问的服务,在本例中为服务–渠道合作伙伴。在服务-直销的案例中,如下图所示,它为网络商店、桌面应用和移动应用提供功能。该服务还与数据库进行交互,执行各种任务,即获取和保存数据。

通常,服务通过通信通道(通常是 HTTP 协议)与其他系统交互。这些服务可能部署在同一台服务器上,也可能不部署在单台服务器上:

在上图中,我们已经计划了一个 SOA 示例场景。这里有很多细节需要注意,让我们开始吧。首先,我们的服务可以分布在不同的物理机器上。这里,服务直销托管在两台独立的机器上。可能不是整个业务功能,而是只有一部分驻留在服务器 1 上,其余部分驻留在服务器 2 上。类似地,服务合作伙伴销售似乎在服务器 3服务器 4 上有相同的安排。然而,这并没有阻止服务渠道合作伙伴服务器 5服务器 6 上作为一个完整的集合进行托管。

我们将在以下几节中详细讨论 SOA。

让我们回忆一下单片架构。在这种情况下,我们没有使用它,因为它限制了代码的可重用性;它是一个独立的组件,所有组件都是相互连接和相互依赖的。对于部署,在这种情况下,在我们选择了 SOA 之后,我们将不得不部署我们的完整项目(参考前面的图表和后续的讨论)。现在,由于使用了这种架构风格,我们拥有代码可重用性和易于部署的优势。让我们根据前面的图表来检查这一点:

  • 复用性:多个客户端可以消费该服务。这也可以由其他服务同时消费。例如OrderService被 web 和移动客户端消费。OrderService现在也可以被报告仪表板用户界面使用。
  • 无状态:服务不会在客户端请求之间保持任何状态。这意味着服务不知道或不关心后续的请求来自已经/还没有发出前一个请求的客户端。
  • 基于契约:接口使得任何服务在实现和消费两方面都是技术不可知的。它们还使它不受底层功能中代码更新的影响。
  • 可扩展性:一个系统可以向上扩展,SOA 可以通过适当的负载平衡单独集群。
  • 可升级性:推出新功能或引入现有功能的新版本非常容易。系统不会阻止您保留同一业务功能的多个版本。

本节讲述了 SOA,我们还讨论了服务的概念以及它们如何影响架构。接下来,我们将继续学习微服务架构的所有知识。

理解微服务架构

微服务架构是一种开发包含一组较小服务的单个应用的方法。这些服务相互独立,并在各自的流程中运行。这些服务的一个重要优势是它们可以独立开发和部署。换句话说,我们可以说微服务是隔离我们的服务的一种方式,这样它们就可以在设计、开发、部署和升级的环境中彼此完全独立地处理。

在单个应用中,我们有一个由用户界面、直销和库存组成的独立组件。在微服务体系结构中,应用的服务部分更改为以下描述:

在这里,业务组件被分离成单独的服务。这些独立的服务现在是更小的单元,早些时候存在于单体架构中的自包含组件中。直销和库存服务都是相互独立的,虚线描绘了它们在同一个生态系统中的存在,还没有被限制在一个范围内。

参考下图,描述用户与不同 API 的交互:

从前面的图中,很明显我们的用户界面可以与任何服务交互。当用户界面调用任何服务时,不需要干预它。两个服务相互独立,不知道用户界面何时会调用另一个服务。这两个服务对自己的操作负责,而不对整个系统的任何其他部分负责。尽管我们更接近我们预期的微服务架构的布局。请注意,布局的先前表示并不完全是预期微服务架构的完整可视化。

In microservice architecture, services are small, independent units with their own persistent stores.

现在让我们应用这个最后的更改,这样每个服务都有自己的数据库来保存必要的数据。参考下图:

在这里,用户界面正在与服务交互,这些服务有自己独立的存储。在这种情况下,当用户界面调用直接销售服务时,直接销售的业务流程独立于库存服务中包含的任何数据或逻辑来执行。

使用微服务提供的解决方案有很多好处,包括:

  • 更小的代码库:每个服务都很小,因此作为一个单元更容易开发和部署。
  • 独立环境的易用性:随着服务的分离,所有开发人员独立工作,独立部署,没有人关心模块依赖。

随着微服务架构的采用,单片应用现在可以利用相关的优势,因为它们现在可以轻松扩展,并使用服务独立部署。

微服务中的消息传递

在处理微服务架构时,仔细考虑消息机制的选择是非常重要的。如果忽略这一方面,它可能会损害设计微服务架构的整个目的。在单片应用中,这不是问题,因为组件的业务功能是通过函数调用来调用的。另一方面,这是通过松散耦合的 web 服务级消息传递特性来实现的,其中服务主要基于 SOAP。在微服务消息机制的情况下,这应该是简单和轻量级的。

在微服务架构的各种框架或协议之间做出选择没有固定的规则。不过,这里有几点值得考虑。首先,它应该实现起来足够简单,不会给系统增加任何复杂性。其次,它应该非常轻量级,记住微服务架构可能严重依赖服务间消息传递这一事实。让我们继续考虑同步和异步消息传递的选择,以及不同的消息传递格式。

同步消息

同步消息传递是指系统期望从服务中得到及时的响应,系统等待直到从服务中收到响应。剩下的就是微服务最受欢迎的选择。这很简单,支持 HTTP 请求-响应,因此几乎没有空间去寻找替代方案。这也是大多数微服务实现使用 HTTP(基于 API 的样式)的原因之一。

异步消息传递

异步消息传递是指系统没有立即从服务中得到及时的响应,并且系统可以继续处理而不阻塞该调用。

让我们将这个消息传递概念融入到我们的应用中,看看它将如何改变我们的应用的工作和外观:

在上图中,当系统与销售数据库和/或库存数据库服务交互时,用户会得到响应,并将数据提取或推送到各自的数据库。用户(通过用户界面)对相应服务的呼叫不会阻止来自相同或不同用户的新呼叫。

消息格式

在过去的几年里,使用 MVC 之类的东西让我迷上了 JSON 格式。您也可以考虑 XML。使用应用编程接口风格的资源,这两种格式在 HTTP 上都可以。如果您需要使用二进制消息格式,也可以使用。我们不推荐任何特定的格式;您可以继续使用您喜欢的消息格式。

使用微服务

社区已经探索了大量的模式和架构,其中一些越来越受欢迎。每种解决方案都有各自的优缺点,因此对于公司来说,快速响应基本需求变得越来越重要,例如可扩展性、高性能和易于部署。任何一个方面如果不能以成本效益的方式实现,都很容易对大型企业产生负面影响,从而在盈利和非盈利企业之间产生巨大差异。

We will discuss scalability in detail in Chapter 8, Scaling Microservices with Azure.

在这种架构风格的帮助下,涉众可以确保他们的设计免受前面提到的问题的影响。同样重要的是要考虑这样一个事实,即在尊重所涉时间的同时,以具有成本效益的方式实现这一目标。

让我们看看微服务架构是如何工作的。

微服务架构的工作原理

微服务架构是一种架构风格,它将应用构建为松散耦合服务的集合。这些服务可以相互通信,也可以相互独立。基于微服务的应用的整体工作架构取决于用于开发应用的各种模式。例如,微服务可以基于后端或前端模式。我们将在第 10 章、设计模式和最佳实践中讨论各种模式。

到目前为止,我们已经讨论了微服务架构的各个方面,现在我们可以描述它是如何工作的;我们可以根据我们的设计方法使用任何组合,或者预测适合的模式。以下是使用微服务架构的一些好处:

  • 在当前的编程时代,每个人都应该遵循所有的 SOLID 原则。几乎所有的语言都是面向对象编程 ( OOP )。
  • 最好的方法是向其他或外部组件公开功能,允许任何其他编程语言使用该功能,而不遵守任何特定的用户界面(即,诸如 web 服务、API、REST 服务等服务)。
  • 整个系统按照一种不相互关联或不相互依赖的协作方式工作。
  • 每个组件都有自己的责任。换句话说,组件只负责一个功能。
  • 它用分离概念分离代码,分离的代码是可重用的。

微服务的优势

现在,让我们探索和讨论作为微服务相对于 SOA 和单体架构的优势的各种因素:

  • 扩展的性价比:你不需要投入很多就可以让整个应用可扩展。就购物车而言,我们可以简单地对产品搜索模块和订单处理模块进行负载平衡,同时省去不太常用的操作服务,如库存管理、订单取消和交货确认。
  • 清晰的代码边界:代码应该与组织的部门层级相匹配。在大型企业中,不同的部门赞助产品开发,这可能是一个巨大的优势。
  • 更容易的代码更改:代码的完成方式不依赖于其他模块的代码,只实现孤立的功能。如果做得好,那么一个微服务中的变化影响另一个微服务的机会是最小的。
  • 轻松部署:由于整个应用更像是一组相互隔离的生态系统,如果需要,可以一次部署一个微服务。其中任何一个的失败都不会使整个系统瘫痪。
  • 技术适配:你可以一夜之间将单个微服务或者一大堆微服务移植到不同的技术上,而你的用户甚至不知道。记得维护那些服务合同。
  • 分布式系统:这里隐含的意思,但需要提醒一句。确保异步调用使用良好,同步调用不会真正阻塞整个信息流。很好地使用数据分区。我们稍后会在本章的数据分区部分谈到这一点,所以现在不要担心。
  • 快速市场反应:世界竞争是一个确定的优势。如果您对新功能请求或系统内新技术的采用反应迟缓,用户往往会很快失去兴趣。

到目前为止,我们已经介绍了 SOA 和微服务架构。我们已经详细讨论了每一个。我们也看到了各自是如何独立的。在下一节中,我们将了解微服务和 SOA 之间的区别。

SOA 与微服务

如果你对微服务和 SOA 都没有完整的理解,你会混淆两者。从表面上看,微服务的特性和优势听起来几乎像是 SOA 的细长版本,许多专家认为,事实上,不需要像微服务这样的附加术语,SOA 可以实现微服务布局的所有属性。然而,事实并非如此。存在足够的差异,可以在技术上隔离它们。

SOA 的底层通信系统固有地存在以下问题:

  • 事实上,在 SOA 中开发的系统依赖于它的组件,组件之间是相互作用的。所以,不管你怎么努力,它最终都会在消息队列中面临瓶颈。
  • SOA 的另一个焦点是命令式组合。这样,我们就失去了使一个代码单元相对于面向对象程序可重用的途径。

我们都知道组织在基础设施上的支出越来越多。企业越大,开发的应用的所有权问题就越复杂。随着利益相关方数量的增加,满足他们所有不断变化的业务需求变得不可能。

In SOA, the development of services can be managed and organized within multiple teams. On the other hand, services can be developed, operated, and deployed independently when working with microservices. This helps to deploy new versions of services easily.

SOA uses an enterprise service bus (ESB) for communication; an ESB can be the reason for communication failures and can impact the entire application. This could happen in a scenario where one service is slowing down and communication is delayed, hampering the workings of the entire application. On the other hand, it would not be a problem in microservices; in the case of independent services, if one service is down, then only that microservice will be affected. In the case of interdependent services, if one of the services is down, then only a particular service(s) will be affected. The other microservices will continue to handle requests.

Data storage is common/sharable in the case of SOA. On the other hand, each service can have independent data storage in microservices.

这是微服务明显与众不同的地方。虽然云开发不在我们目前的讨论范围内,但说微服务架构的可扩展性、模块化和适应性可以通过使用云平台轻松扩展,这并不会损害我们。是时候改变了。

让我们看看微服务架构的先决条件。

微服务架构的先决条件

了解微服务架构实现所产生的生态系统非常重要。微服务的影响不仅仅是运营前的。任何选择微服务架构的组织的变化都是如此深刻,以至于如果他们没有做好准备来处理它们,用不了多久优势就会变成劣势。

在同意采用微服务架构后,明智的做法是具备以下先决条件:

  • 部署和 QA :需求会变得更加苛刻,从开发需求的转变会更快。这将要求您尽快部署和测试。如果只是少量的服务,那么这就不成问题了。然而,如果服务的数量不断增加,它可能会很快挑战现有的基础设施和实践。例如,您的质量保证和试运行环境可能不再足以测试开发团队返回的构建数量。
  • 开发运营团队的一个协作平台:随着应用走向公共领域,用不了多久,开发对抗 QA 的老套剧本又要上演了。这一次的不同之处在于,业务将处于危险之中。因此,您需要准备好以自动化方式快速响应,以便在需要时识别根本原因。
  • 监控框架:随着微服务数量的不断增加,您将很快需要一种方法来监控整个系统的功能和运行状况,以发现任何可能的瓶颈或问题。如果没有任何方法来监控已部署的微服务的状态以及由此产生的业务功能,任何团队都不可能采取主动部署方法。

本节解释了基于微服务架构的应用的先决条件。有了它们,下一节将帮助我们理解单片的问题.NET 基于堆栈的应用。

理解整体建筑风格的问题

在这一节中,我们将讨论单片集成电路的所有问题.NET 基于堆栈的应用。在单芯片应用中,核心问题是:扩展单芯片应用很困难。最终得到的应用拥有非常大的代码库,并在可维护性、部署和修改方面带来了挑战。在接下来的几节中,我们将了解扩展,然后我们将通过遵循扩展属性来应对部署挑战。

. NET 堆栈标准化的挑战

在单片应用技术中,堆栈依赖阻止了外部世界最新技术的引入。当前的堆栈带来了挑战,因为 web 服务本身也会受到这些挑战的困扰:

  • 安全性:由于对强认证方案没有明确共识,无法通过 web 服务识别用户。想象一下,一个银行应用发送包含用户凭据的未加密数据。所有提供免费无线网络的机场、咖啡馆和公共场所都很容易成为身份盗窃和其他网络犯罪的受害者。
  • 响应时间:虽然 web 服务本身在整体架构上提供了一定的灵活性,但是由于服务本身需要很长的处理时间,这种灵活性很快就会减少。因此,在这种情况下,web 服务没有任何问题。事实上,一个单一的应用需要大量的代码;复杂的逻辑使得 web 服务的响应时间很长,因此是不可接受的。
  • 吞吐率:这个偏高,结果阻碍了后续的操作。对于结帐操作来说,依靠调用库存 web 服务来搜索几百万条记录并不是一个坏主意。但是,当相同的库存服务为搜索整个门户的主要产品提供服务时,可能会导致业务损失。10 次呼叫中有一次服务呼叫失败将意味着业务的转化率降低 10%。
  • 频繁宕机:由于 web 服务是整个整体生态系统的一部分,因此每次升级或应用出现故障时,它们必然会停机且不可用。这意味着外部世界对应用 web 服务的任何 B2B 依赖的存在将进一步使决策复杂化,从而导致停机。这使得系统的较小升级看起来很昂贵;因此,这进一步增加了待定系统升级的积压。
  • 技术采用:为了采用或升级技术栈,需要对整个应用进行升级、测试和部署,因为模块是相互依赖的,项目的整个代码库都会受到影响。考虑支付网关模块使用需要合规性相关框架升级的组件。开发团队别无选择,只能升级框架本身,并仔细检查整个代码库,以抢先识别任何代码中断。当然,这仍然不排除生产崩溃,但这很容易让即使是最好的架构师和经理失眠。
  • 可用性:服务运行的时间百分比。
  • 响应时间:服务响应所花费的时间。
  • 吞吐量:处理请求的速率。

容错

单片应用具有很高的模块依赖性,因为它们紧密耦合。不同的模块以模块内的方式利用功能,由于其级联效应,即使单个模块故障也会导致系统停机。我们都知道,一个用户得不到产品搜索的结果,远没有整个系统崩溃那么严重。

使用 web 服务的解耦传统上是在架构级别尝试的。对于数据库级策略,ACID 已经被依赖了很长时间。让我们进一步研究这两点:

  • Web 服务:在目前的单体应用中,由于使用 Web 服务导致客户体验下降。即使客户尝试下订单,由于 web 服务响应时间长,甚至服务本身完全失败等原因,也可能导致无法成功下订单。甚至没有一次失败是可以接受的,因为用户倾向于记住他们最后的经历,并假设可能会重复。这不仅会导致可能的销售损失,还会失去未来的商业前景。Web 服务故障会导致依赖它们的系统出现级联故障。
  • ACID : ACID原子性、一致性、隔离性和持久性的缩写;这是数据库中的一个重要概念。它是存在的,但它是好事还是坏事要根据综合表现的总和来判断。它处理数据库级别的故障,毫无疑问,它确实提供了一些防止数据库错误蔓延的保障。同时,每个 ACID 操作都会妨碍/延迟其他组件/模块的操作。需要非常仔细地判断它造成的伤害大于益处的程度。

将要过渡到微服务的单一应用面临着与安全性、响应时间、可伸缩性相关的各种挑战,此外,它的模块高度依赖于彼此。当试图处理一个标准应用时,这些都是很大的挑战,尤其是一个单一的应用,它应该用于大量的用户。对于我们的单片应用来说,这里的主要和重要的一点是可伸缩性,这将在下一节中讨论。

系统的缩放特性

各种因素,如不同通信手段的可获得性、信息的便捷获取和开放的世界市场,正在导致企业快速增长,同时实现多样化。随着这种快速增长,越来越需要适应不断增长的客户群。扩展是任何企业在努力迎合不断增长的用户群时面临的最大挑战之一。

可扩展性描述系统/程序处理不断增加的工作负载的能力。换句话说,可伸缩性是指系统/程序的伸缩能力。

在开始下一部分之前,让我们详细讨论扩展,因为这将是我们练习的一个组成部分,因为我们正在努力从单块架构过渡到微服务。

有两种主要的可伸缩性策略或类型:

  1. 垂直缩放或放大
  2. 水平扩展或横向扩展

我们可以通过采用其中一种策略来扩展我们的应用。让我们进一步讨论这两种类型的扩展,看看如何扩展我们的应用。

垂直缩放或放大

在纵向扩展中,我们分析现有的应用,找出模块中由于执行时间较长而导致应用变慢的部分。提高代码的效率可能是一种策略,这样可以减少内存消耗。这个减少内存消耗的练习可以针对特定的模块或整个应用。另一方面,由于该策略涉及明显的挑战,我们可以向现有的信息技术基础架构添加更多资源,例如升级内存或添加更多磁盘驱动器,而不是更改应用。垂直缩放中的这两种路径都有其有益程度的限制。在一个特定的时间点之后,最终的收益将趋于平稳。请务必记住,这种扩展需要停机。

水平扩展或横向扩展

在水平扩展中,我们深入挖掘对整体性能有较大影响的模块,例如高并发性;这将使我们的应用能够为我们不断增长的用户群提供服务,用户群现在已经达到百万大关。我们还实现了负载平衡来处理更多的工作。向集群添加更多服务器的选项不需要停机,这是一个明显的优势。每个案例都有所不同,因此电力、许可证和冷却的额外成本是否值得,以及达到什么程度,将根据具体情况进行评估。

扩展将在第 8 章中详细介绍。

部署挑战

当前的应用也面临部署挑战。它被设计为一个整体应用,订单模块中的任何更改都需要重新部署整个应用。这很耗时,而且每次变更都必须重复整个周期,这意味着这可能是一个频繁的周期。在这种情况下,缩放只能是一个遥远的梦想。

正如关于扩展当前存在部署挑战的应用所讨论的那样,这些应用需要我们部署整个程序集,模块是相互依赖的,这是. NET 的单个程序集应用。一次性部署整个应用也使测试我们应用的整个功能成为强制性的。这种做法的影响将是巨大的:

  • 高风险部署:一次性部署整个解决方案或应用会带来高风险,因为所有模块都将被部署,即使其中一个模块发生了单次更改。
  • 更长的测试时间:由于我们必须部署完整的应用,我们将不得不测试整个应用的功能。没有测试我们不能直播。由于更高的相互依赖性,更改可能会导致其他模块出现问题。
  • 计划外停机:完整的生产部署需要代码进行全面测试,因此我们需要安排生产部署。这是一项耗时的任务,会导致长时间停机。虽然是计划停机,但在此期间,由于系统不可用,业务和客户都会受到影响;这可能会给企业造成收入损失。
  • 生产 bug:无 bug 部署将是任何项目经理的梦想。然而,这远非现实,每个团队都害怕这种错误部署的可能性。单片应用与这个场景没有什么不同,解决生产缺陷说起来容易做起来难。如果之前的错误没有解决,情况只会变得更加复杂。

组织一致性

在单片应用中,拥有庞大的代码库并不是您将面临的唯一挑战。拥有一个庞大的团队来处理这样的代码库是影响业务和应用增长的另一个问题。

要调整一个组织,最关心的因素是团队的目标。对于所有团队成员来说,团队目标应该相同,这一点非常重要:

  • 同一个目标:在一个团队中,所有的团队成员都有同一个目标,那就是每天结束时及时无 bug 的交付。然而,在当前的应用中有一个大的代码库意味着单一的架构风格对于团队成员来说不是一个舒适的领域。由于相互依赖的代码和相关的可交付成果,团队成员是相互依赖的,在代码中体验到的同样的效果也存在于开发团队中。在这里,每个人都在争先恐后地努力完成工作。互相帮助或尝试新事物的问题不会出现。简而言之,团队不是自组织的。

Roy Osherove defined three stages of a team as: survival phase—no time to learn, l****earning phase—learning to solve your own problems, and s****elf-organizing phase—facilitate, and experiment.

  • 不同的视角:开发团队由于功能增强、bug 修复或模块相互依赖等原因,在交付件上花费了太多的时间,阻碍了开发的简易性。质量保证团队依赖于开发团队,开发团队有自己的问题。一旦开发人员开始处理 bug、修复或特性增强,质量保证团队就会陷入困境。没有单独的环境或构建可供质量保证团队进行测试。这种延迟会阻碍整体交付,客户或最终用户将无法按时获得新功能或修复。

模块性

在我们的单片应用中,我们可能有一个Order模块,Order模块的变化会影响Stock模块,以此类推。正是模块化的缺失导致了这种情况。

这也意味着我们不能在另一个模块中重用一个模块的功能。代码没有被分解成可重用的结构化片段,以节省时间和精力。代码模块内没有隔离,因此没有通用代码可用。

业务在增长,客户在突飞猛进。来自不同地区的新客户或现有客户在使用该应用时有不同的偏好。有些人喜欢访问网站,但其他人更喜欢使用移动应用。该系统的结构意味着我们不能在一个网站和一个移动应用之间共享组件。这使得为企业推出移动/设备应用成为一项具有挑战性的任务。因此,由于客户更喜欢移动应用,企业损失惨重,业务受到影响。

困难在于替换使用第三方库的应用组件、支付网关等外部系统以及外部订单跟踪系统。在当前风格的整体架构应用中替换旧组件是一项乏味的工作。例如,如果我们考虑升级消耗外部订单跟踪系统的模块库,那么整个更改将会非常困难。此外,用另一个支付网关取代我们的支付网关将是一项复杂的任务。

在前面的任何场景中,每当我们升级组件时,我们都会升级应用中的所有内容,这需要对系统进行完整的测试,并且需要大量的停机时间。除此之外,升级可能会导致生产错误,这需要重复整个开发、测试和部署周期。

大数据库

我们当前的应用有一个庞大的数据库,包含一个带有大量索引的模式。就微调性能而言,这种结构是一项具有挑战性的工作:

  • 单一模式:数据库中的所有实体都集中在一个名为dbo的单一模式下。这再次阻碍了业务,因为对于属于不同模块的各种表的单一模式存在混淆。例如,客户表和供应商表属于同一个模式,即dbo
  • 众多的存储过程:目前数据库中有大量的存储过程,其中也包含了相当大一部分的业务逻辑。有些计算是在存储过程中执行的。因此,当涉及到优化这些存储过程或将它们分解成更小的单元时,这些存储过程被证明是一项令人困惑的任务。

无论何时计划部署,团队都必须密切关注每个数据库更改。同样,这是一项耗时的工作,往往比构建和部署工作本身更加复杂。

大型数据库有其自身的局限性。在我们的整体应用中,我们有一个单一的模式数据库。这有很多存储过程和函数;所有这些都会影响数据库的性能。

在接下来的部分中,我们将讨论各种解决方案和其他方法来克服这些问题。但在此之前,我们需要先了解微服务的前提条件,然后再去挖掘这种架构风格。

微服务的先决条件

为了更好地理解微服务,让我们来看看 FlixOne Inc .的一个假想例子,以这个例子为基础,我们可以详细讨论所有的概念,看看准备好微服务是什么样子的。

FlixOne 是一家遍布印度的电子商务玩家。他们正以非常快的速度增长,同时实现业务多元化。他们已经建立了他们现有的系统.NET 框架,这是一个传统的三层架构。他们有一个庞大的数据库,是这个系统的核心,在他们的生态系统中还有外围应用。其中一个这样的应用是为他们的销售和物流团队准备的,它恰好是一个安卓应用。这些应用连接到其集中式数据中心,并面临性能问题。FlixOne 有一个由外部顾问支持的内部开发团队。参考下图:

上图描述了我们当前应用的广义,它是一个单一的.NET 程序集应用。在这里,我们有用于搜索和订购产品、跟踪订单和结账的用户界面。现在看下图:

上图仅描述了我们的购物 购物车模块。该应用是用 C#、MVC5 和实体框架构建的,它有一个单一的项目应用。这个图只是我们应用架构的图示概述。这个应用是基于网络的,可以从任何浏览器访问。最初,任何使用 HTTP 协议的请求都会到达使用 MVC5 和 jQuery 开发的用户界面。对于购物车活动,UI 与购物车模块交互,该模块是与数据库层交互的业务逻辑层(用 C#编写)。我们将数据存储在数据库中。

应用的功能概述

在这里,我们将了解 FlixOne 书店应用的功能概述。这只是为了可视化我们的应用。以下是应用的简化功能概述,显示了从主页结账的过程:

在当前应用中,客户登录到主页,在那里他们可以看到特色/突出显示的书籍。他们还可以选择搜索书籍项目。在获得期望的结果后,顾客可以选择书籍项目并将其添加到他们的购物车中。顾客可以在最后结账前核实图书项目。一旦客户决定结账,现有的购物车系统会将他们重定向到外部支付网关,支付您需要为购物车中的图书支付的指定金额。

如前所述,我们的应用是一个整体应用;它的结构是作为一个单元来开发和部署的。这个应用有一个仍在增长的庞大代码库。小更新需要一次部署整个应用。

在本节中,我们讨论了应用的功能概述。我们仍然需要分析和应对挑战,并为当前的挑战找到最佳解决方案。那么,接下来让我们讨论这些事情。

应对当前挑战的解决方案

业务增长迅速,因此我们决定在另外 20 个城市开设我们的电子商务网站。然而,我们仍然面临着现有应用的挑战,并努力为现有用户群提供适当的服务。在这种情况下,在我们开始过渡之前,我们应该让我们的单片应用为过渡到微服务做好准备。

在第一种方法中,购物车模块将被分离成更小的模块,然后您将能够使这些模块以及外部或第三方软件相互作用:

这个提议的解决方案对于我们现有的应用来说是不够的,尽管开发人员能够划分代码并重用它。然而,业务逻辑的内部处理将保持与用户界面或数据库交互的方式相同。新代码将与用户界面和数据库层交互,而数据库仍然保持为同一个旧的单一数据库。随着我们的数据库保持不可分割和紧密耦合的层,必须更新和部署整个代码库的问题仍然存在。所以,这个方案不适合解决我们的问题。

处理部署问题

在上一节中,我们讨论了当前面临的部署挑战.NET 单片应用。在本节中,让我们来看看如何通过在相同的环境中进行一些实践来克服这些挑战.NET 堆栈。

用我们的.NET 单片应用,我们的部署由 XCOPY 部署组成。在将我们的模块划分为不同的子模块后,我们可以借助这些子模块来适应部署策略。我们可以简单地部署我们的业务逻辑层或一些常见的功能。我们可以适应持续的集成和部署。XCOPY 部署是一个将所有文件复制到服务器的过程;它主要用于 web 项目。

制造更好的单片应用

现在,我们已经了解了现有单片应用的所有挑战,新的应用应该能够更好地应对新的变化。随着我们的扩张,我们不能错过获得新客户的机会。如果我们不能克服挑战,那么我们也会失去商业机会。让我们讨论几个点来解决这些问题。

引入依赖注入

我们的模块是相互依赖的,因此我们面临着一些问题,例如代码的可重用性和由于一个模块中的变化而导致的未解决的 bug。这些是部署挑战。为了解决这些问题,让我们分离我们的应用,这样我们就能够将模块分成子模块。我们可以划分我们的Order模块,让它实现接口,这可以从构造函数开始。

Dependency injection (DI) is a design pattern and provides a technique so that you can make a class independent of its dependencies. It can be achieved by decoupling an object from its creation.

下面是一个简短的代码片段,展示了我们如何将它应用到现有的整体应用中。下面的代码示例显示了我们的Order类,其中我们使用构造函数注入:

using System;
using System.Collections.Generic;
using FlixOne.BookStore.Models;

namespace FlixOne.BookStore.Common
{
    public class Order : IOrder
    {
        private readonly IOrderRepository _orderRepository;
        public Order() => _orderRepository = new OrderRepository();
        public Order(IOrderRepository orderRepository) => _orderRepository = orderRepository;
        public IEnumerable<OrderModel> Get() => _orderRepository.GetList();
        public OrderModel GetBy(Guid orderId) => _orderRepository.Get(orderId);
    }
}

Inversion of control, or IoC, is the way in which objects do not create other objects on whom they rely to do their work.

在前面的代码片段中,我们抽象了我们的Order模块,使得它可以使用IOrder接口。之后,我们的Order类实现了IOrder接口,通过使用控制反转,我们创建了一个对象,因为这是在控制反转的帮助下自动解决的。

此外,IOrderRepository的代码片段如下:

using FlixOne.BookStore.Models;
using System;
using System.Collections.Generic;

namespace FlixOne.BookStore.Common
{
    public interface IOrderRepository
    {
        IEnumerable<OrderModel> GetList();
        OrderModel Get(Guid orderId);
    }
}

我们有以下OrderRepository的代码片段,它实现了IOrderRepository接口:

using System;
using System.Collections.Generic;
using System.Linq;
using FlixOne.BookStore.Models;

namespace FlixOne.BookStore.Common
{
    public class OrderRepository : IOrderRepository
    {
        public IEnumerable<OrderModel> GetList() => DummyData();
        public OrderModel Get(Guid orderId) => DummyData().FirstOrDefault(x => x.OrderId == orderId);
        }
}

在前面的代码片段中,我们有一个名为DummyData()的方法,用于为我们的示例代码创建Order数据。

以下是显示DummyData()方法的代码片段:

private IEnumerable<OrderModel> DummyData()
        {
            return new List<OrderModel>
            {
                new OrderModel
                {
                    OrderId = new Guid("61d529f5-a9fd-420f-84a9-
                                        ab86f3eaf8ad"),
                    OrderDate = DateTime.Now,
                    OrderStatus = "In Transit"
                },
                ...
            };
        }

在这里,我们试图展示我们的Order模块是如何抽象的。在前面的代码片段中,我们为订单返回了默认值(使用示例数据),只是为了演示实际问题的解决方案。

最后,我们的表示层(MVC 控制器)将使用可用的方法,如下面的代码片段所示:

using FlixOne.BookStore.Common;
using System;
using System.Web.Mvc;

namespace FlixOne.BookStore.Controllers
{
    public class OrderController : Controller
    {
        private readonly IOrder _order;
        public OrderController() => _order = new Order();
        public OrderController(IOrder order) => _order = order;
        // GET: Order
        public ActionResult Index() => View(_order.Get());
        // GET: Order/Details/5
        public ActionResult Details(string id)
        {
            var orderId = Guid.Parse(id);
            var orderModel = _order.GetBy(orderId);
            return View(orderModel);
        }
    }
}

下图是一个类图,描述了我们的接口和类如何相互关联,以及它们如何公开它们的方法、属性等:

在这里,我们再次使用构造函数注入,其中IOrder被传递并初始化了Order类。因此,所有的方法在我们的控制器中都是可用的。

走到这一步意味着我们已经克服了一些问题,包括:

  • 减少模块依赖:随着IOrder在我们应用中的引入,我们已经减少了Order模块的相互依赖。这样,如果我们需要在该模块中添加或移除任何东西,那么其他模块将不会受到影响,因为IOrder仅由Order模块实现。假设我们想对我们的Order模块进行增强;这不会影响我们的Stock模块。这样,我们减少了模块的相互依赖性。
  • 引入代码可重用性:如果需要获取任何应用模块的订单详细信息,可以使用IOrder类型轻松实现。
  • 代码可维护性的提升:我们现在已经把我们的模块划分成子模块或者类和接口。我们现在可以以这样一种方式来构造我们的代码,即所有的类型(也就是说,所有的接口)都放在一个文件夹下,并遵循存储库的结构。有了这种结构,我们将更容易安排和维护代码。
  • 单元测试:我们目前的单片应用没有任何一种单元测试。随着接口的引入,我们现在可以轻松地执行单元测试,并轻松地采用测试驱动开发的系统。

数据库重构

正如上一节所讨论的,我们的应用数据库非常庞大,并且依赖于单个模式。重构时应该考虑这个庞大的数据库。为了重构我们的应用数据库,我们遵循以下几点:

  • 模式修正:一般情况下(不需要),我们的模式描述了我们的模块。如前几节所述,我们庞大的数据库只有一个模式(现在是dbo ),,代码或表的每个部分都不应该与dbo相关。可能有几个模块会与特定的表交互。例如,我们的Order模块应该包含一些相关的模式名,比如Order。所以,每当我们需要使用这些表时,我们可以用它们自己的模式来使用它们,而不是一般的dbo模式。这不会影响与如何从数据库中检索数据相关的任何功能,但它会以这样一种方式来构造或排列我们的表,即我们能够识别每个表并将其与其特定模块相关联。当我们处于将单一应用转换为微服务的阶段时,这个练习将非常有帮助。参考下图描述数据库的订单模式库存模式:

在上图中,我们看到了数据库模式是如何逻辑分离的。由于我们的订单模式库存模式属于同一个数据库,因此在物理上没有分离。因此,在这里,我们将在逻辑上而不是物理上分离数据库模式。

我们也可以以我们的用户为例;并非所有用户都是管理员或属于特定的区域、地区或地区。然而,我们的用户表应该以这样一种方式来构造,即我们应该能够通过表名或它们的构造方式来识别用户。在这里,我们可以根据区域来构建我们的用户表。我们应该将用户表映射到一个区域表,这样它就不会影响或改变现有的代码库。

  • 将业务逻辑从存储过程转移到代码中:在当前的数据库中,我们有数千行存储过程,其中包含大量的业务逻辑。我们应该将业务逻辑转移到我们的代码库中。在我们的整体应用中,我们使用实体框架;在这里,我们可以避免创建存储过程,并且我们可以将所有的业务逻辑写成代码。

数据库分片和分区

说到数据库分片和分区,我们选择数据库分片。在这里,我们将把它分成更小的数据库。这些较小的数据库将部署在单独的服务器上:

通常,数据库分片被简单地定义为大型数据库的无共享分区方案。这样,我们可以实现更高水平的高性能和可扩展性。分片这个词来自分片和传播,这意味着将一个数据库分成块(分片),并将其传播到不同的服务器。

Sharding can come in different forms. One would be splitting customers and orders into different databases, but one could also split customers into multiple databases for optimization. For instance, customers A-G, customers H-P, and customers Q-Z (based on surname).

上图是如何将我们的数据库划分成更小的数据库的示意图。请看下图:

上图说明我们的应用现在有了一个更小的数据库,或者每个服务都有自己的数据库。

德沃普斯文化

在前几节中,我们讨论了团队面临的挑战和问题。在这里,我们将为 DevOps 团队提出一个解决方案:应该强调开发团队与另一个运营团队的协作。我们还应该建立一个开发、质量保证和基础设施团队合作的系统。

自动化

基础架构设置可能是一项非常耗时的工作,当基础架构准备就绪时,开发人员仍然处于空闲状态。他们需要一些时间才能加入团队并做出贡献。基础架构设置的过程不应该阻止开发人员提高工作效率,因为这会降低整体工作效率。这应该是一个自动化的过程。使用 Chef 或 PowerShell,我们可以轻松创建虚拟机,并在需要时快速增加开发人员数量。这样,我们的开发人员就可以在加入团队的第一天开始工作。

Chef 是一个 DevOps 工具,它提供了一个自动化和管理基础设施的框架。PowerShell 可以用来创建我们的 Azure 机器和设置 Azure DevOps(以前的 TFS)。

测试

我们将引入自动化测试,作为我们在部署期间进行测试时所面临问题的解决方案。在解决方案的这一部分,我们必须将我们的测试方法划分如下:

  • 采用测试驱动开发 ( TDD )。使用 TDD,开发人员在编写实际代码之前编写测试。这样,他们将测试自己的代码。测试是另一段代码,可以验证功能是否按预期工作。如果发现任何功能不满足测试代码,则相应的单元测试失败。这个功能很容易修复,因为你知道这就是问题所在。为了实现这一点,我们可以利用框架,如微软测试或单元测试。
  • 质量保证团队可以使用脚本来自动化他们的任务。他们可以利用 QTP 或硒框架创建脚本。

版本控制

当前的系统没有任何类型的版本控制系统,所以如果在变更期间发生了什么,就没有办法恢复。为了解决这个问题,我们需要引入一个版本控制机制。在我们的例子中,这应该是 Azure DevOps 或者 Git。通过使用版本控制,如果发现我们的更改破坏了某些功能或者在应用中引入了任何意外行为,我们现在可以恢复它。我们现在能够在个人层面上跟踪在这个应用上工作的团队成员所做的更改。然而,在我们的单片应用中,我们没有能力做到这一点。

部署

在我们的应用中,部署是一个巨大的挑战。为了解决这个问题,我们将引入持续集成 ( CI )。在这个过程中,我们需要设置一个 CI 服务器。随着 CI 的引入,整个过程实现了自动化。一旦代码被任何团队成员使用版本控制 Azure DevOps 或 Git 签入,在我们的例子中,CI 过程就开始起作用了。这确保了新代码被构建,单元测试与集成测试一起运行。在成功构建或其他情况下,团队会被告知结果。这使团队能够快速响应问题。

接下来,我们继续进行连续部署。这里,我们介绍各种环境,即开发环境、试运行环境、质量保证环境等等。现在,只要代码被任何团队成员签入,CI 就会开始工作。这将调用单元/集成测试套件,构建系统,并将其推到我们设置的各种环境中。这样,开发团队为质量保证提供合适构建的周转时间被减少到了最低限度。

作为一个整体应用,我们面临着与部署相关的各种挑战,这些挑战也会影响开发团队。我们已经讨论了配置项/光盘,并了解了部署是如何工作的。

下一节将介绍如何识别单块架构中可能导致问题的分解候选对象。

确定单片中的分解候选项

我们现在已经清楚地确定了当前 FlixOne 应用架构及其生成的代码给开发团队带来的各种问题。我们也理解开发团队不能接受哪些业务挑战,以及为什么。

这并不是团队能力不够——这只是代码。让我们继续前进,看看专注于 FlixOne 应用各个部分的最佳策略,我们需要将这些部分转移到微服务风格的架构中。我们需要知道,我们有一个具有整体架构的候选人,这在以下某个方面会带来问题:

  • 集中部署:虽然这是在整个过程的最后阶段,但它需要更多的尊重,这是理所当然的。这里需要理解的是,这个因素从识别和设计的初始阶段就塑造和定义了整个开发策略。这里有一个例子:企业要求你解决两个同等重要的问题。其中一个问题可能需要您对更多的相关模块进行测试,而另一个问题的解决方案可能允许您通过有限的测试逃脱惩罚。不得不做出这样的选择是错误的,企业不应该有这样做的选择。
  • 代码复杂度:拥有更小的团队是这里的关键。您应该能够为与单一功能相关的变更分配小型开发团队。小团队由一两个成员组成。除此之外,还需要一个项目经理。这意味着一些东西在模块之间比它应该的更加相互依赖。
  • 技术采用:你应该可以在不破坏任何东西的情况下,将组件升级到更新的版本或者不同的技术。如果你必须考虑依赖技术的组件,你有不止一个候选。即使您不得不担心这个组件所依赖的模块,您仍然会有多个候选模块。我记得我的一个客户有一个专门的团队来测试正在发布的技术是否适合他们的需求。我后来了解到,他们实际上会移植其中一个模块,并测量整个系统的性能影响、工作量要求和周转时间。不过,我不同意这一点。
  • 高资源:在我看来,一个系统中的所有东西,从内存、CPU 时间、I/O 需求,都应该被认为是一个模块。如果任何一个模块花费的时间更长,和/或出现的频率更高,就应该挑出它。在任何涉及高于正常内存的操作中,处理时间会阻塞延迟,输入/输出会让系统等待;这对我们来说是件好事。
  • 人类依赖:如果跨模块移动团队成员看起来工作量太大,那么你就有更多的候选人。开发人员很聪明,但如果他们在大型系统上挣扎,那不是他们的错。将系统分解成更小的单元,开发人员将会更加舒适和高效。

这一部分帮助我们理解了单体架构面临的问题。接下来,我们将继续讨论这种架构的一些优势。

重要的微服务优势

我们已经完成了确定迁移到微服务的候选人的第一步。仔细研究微服务提供的相应优势是值得的。让我们在以下几节中了解它们。

技术独立性

随着每个微服务相互独立,我们现在有能力为每个微服务使用不同的技术。支付网关可能使用最新的.NET 框架,而产品搜索可以转移到任何其他编程语言。

整个应用可以基于用于数据存储的 SQL Server,而库存可以基于 NoSQL。灵活性是无限的。

相互依赖消除

由于我们试图在每个微服务中实现独立的功能,因此很容易在每个微服务中添加新功能、修复 bug 或升级技术。这对其他微服务没有影响。现在,您有了垂直代码隔离,这使您能够执行所有这些操作,并且部署速度仍然一样快。

这还没有结束。FlixOne 团队现在有能力在现有支付网关的基础上发布一个新的支付网关选项。两个支付网关可以共存,直到团队和企业所有者都对报告感到满意。这就是这个建筑的巨大力量发挥作用的地方。

与业务目标保持一致

了解某些功能更难处理或更耗时并不一定是企业所有者的顾虑。他们的责任是继续推动和发展业务。开发团队应该成为实现业务目标的支持网络,而不是路障。

理解快速响应业务需求和适应营销趋势不是微服务的副产品,而是目标,这一点非常重要。

用更小的团队实现这些目标的能力只会让微服务更适合企业所有者。

成本效益

每个微服务都成为企业的投资,因为它可以很容易地被其他微服务消费,而不必一次又一次地重做相同的代码。每次重用一个微服务,都可以通过避免测试和部署该部分来节省时间。

用户体验得到增强,因为停机时间要么被消除,要么被减少到最小。

易于扩展

有了垂直隔离,每个微服务都向整个系统提供特定的服务,就很容易扩展。不仅对缩放候选对象的识别更容易,而且成本更低。这是因为我们只扩展了整个微服务生态系统的一部分。

这项工作对企业来说可能是成本密集型的。因此,业务团队现在可以选择先扩展哪个微服务的优先级;这个决定不再是开发团队的选择。

安全

安全性类似于传统分层架构提供的安全性;微服务可以同样容易地得到保护。不同的配置可以用来保护不同的微服务。您可以将微服务生态系统的一部分放在防火墙后面,另一部分用于用户加密。面向 Web 的微服务的安全性可以不同于其他微服务。您可以根据选择、技术或预算来满足您的需求。

数据管理

在大多数单一应用中,拥有一个数据库是很常见的。几乎总是有一个数据库架构师或指定的所有者负责其完整性和维护。任何需要更改数据库的应用增强都必须通过这条路径。对我来说,这从来都不是一件容易的事情。这进一步减缓了应用增强、可伸缩性和技术采用的过程。

因为每个微服务都有自己的独立的数据库,所以与数据库中所需更改相关的决策可以很容易地委托给各自的团队。我们不必担心对系统其他部分的影响,因为不会有任何影响。

同时,数据库的这种分离带来了团队变得自组织的可能性。他们现在可以开始实验了。

例如,团队现在可以考虑使用 Azure Table 存储或 Redis 的 Azure Cache 来存储大量的产品目录,而不是数据库,就像目前正在做的那样。团队现在不仅可以进行实验,而且他们的经验也可以很容易地在整个系统中复制,就像其他团队以他们方便的时间表的形式所要求的那样。

事实上,没有什么能阻止 FlixOne 团队现在的创新和同时使用多种可用技术,然后比较现实世界中的性能并做出最终决定。一旦每个微服务都有了自己的数据库,FlixOne 就会变成这样:

在上图中,每个服务都有自己的数据库,并且具有可伸缩性;清单服务有缓存(Redis 服务器)。

将单片应用与微服务集成

无论何时选择从单一架构转向微服务风格的架构,该计划的时间和成本轴都会带来一些阻力。业务评估可能会禁止移动整体应用中没有为过渡提供业务案例的部分。

如果我们从一开始就开发应用,情况会有所不同。然而,在我看来,这也是微服务的力量。对整个单芯片架构的正确评估可以安全地识别随后要移植的单芯片器件。

我们必须防范集成的风险,以确保这些孤立的部分不会在未来给其他微服务带来问题。

虽然我们讨论了单块应用的各个部分,但我们的目标是协作创建它们,以便它们可以在基于微服务架构风格的应用遵循的模式上相互通信。为了实现这一点,需要开发原始单片应用的各种模式和技术堆栈。例如,如果我们已经使用了事件驱动的模式,我们的整体应用应该遵循这种模式,这样它就可以消费和发布事件。为了实现或遵守这种模式,我们应该管理我们的单片应用的代码,这基本上包括对现有代码进行更改的开发工作。

同样,如果需要使用应用编程接口网关模式,那么我们应该确保我们的网关应该足够了,这样它就可以与整体应用进行通信。要实现这一点可能有点复杂或棘手,因为现有的单一应用不具备公开网络服务(RESTful)的功能。这也会给开发团队带来压力,要求他们对现有代码进行修改,使应用符合网关的标准。将有很大的机会进行代码更改来添加或更新 RESTful 服务,因为这些服务很容易被网关使用。为了克服这种负担,我们可以创建一个单独的微服务,这样我们就可以避免源代码中的重大变化。

在这一节中,我们借助各种方法讨论了单片应用的集成,例如领域驱动模式、API Gateway 模式等。下一节讨论 Azure 服务结构。

Azure 服务结构概述

当我们谈论微服务时.NET Core 世界,Azure 服务结构是一个广泛用于微服务的名称。在本节中,我们将讨论结构服务。

这是一个帮助我们轻松打包、部署和管理可扩展且可靠的微服务的平台(容器也类似于 Docker)。由于复杂的基础设施问题,有时很难专注于作为开发人员的主要职责。在 Azure 服务结构的帮助下,开发人员无需担心基础设施问题。Azure 服务结构提供了各种技术,并作为一个捆绑包提供,具有 Azure SQL 数据库、宇宙数据库、微软 Power BI、Azure 事件中心、Azure 物联网中心和许多其他核心服务的功能。

根据官方文档(https://docs . Microsoft . com/en-us/Azure/Service-Fabric/Service-Fabric-overview),我们可以对 Azure Service Fabric 进行如下定义:

  • 服务结构—任何操作系统、任何云:您只需要创建一个服务结构集群,它将在 Azure(云)或内部、Linux 或 Windows 服务器上运行。此外,您还可以在其他公共云上创建集群。
  • 服务结构:无状态和有状态微服务。在服务结构的帮助下,您可以将应用构建为无状态或有状态的。

根据其官方文档(https://docs.microsoft.com/en-us/azure/service-fabric/),我们可以如下定义无状态微服务:

"Stateless microservices are not in the mutable state outside a request and its response from the service, for example Azure Cloud Services worker roles (Cloud Services should be avoided during development of new services). Stateful microservices are in a mutable state beyond the request and its response."

对应用生命周期管理的全面支持:在服务结构的帮助下,获得完整应用生命周期的支持,包括开发、部署等。

您可以开发一个可扩展的应用。更多信息请参考:https://docs . Microsoft . com/en-us/azure/service-fabric/service-fabric-application-life cycle

您可以开发高度可靠、无状态和有状态的微服务。

Cloud Services should be avoided during the development of new services.

有不同的服务结构编程模型,超出了本章的范围。更多信息请参考:https://docs . Microsoft . com/en-us/azure/service-fabric/service-fabric-choose-framework

本节的目的是概述 Azure 服务结构,然后介绍无状态微服务。我们已经看到 Azure 服务结构支持开发可扩展的应用。

摘要

在这一章中,我们详细讨论了微服务架构风格、它的历史,以及它与它的前辈、单体架构和 SOA 的不同之处。我们进一步定义了单块架构在处理大型系统时面临的各种挑战。可伸缩性和可重用性是 SOA 相对于单一架构的一些明显优势。

我们还讨论了单片架构的局限性,包括通过实现真实的单片应用来扩展问题。微服务架构风格通过减少代码相关性和隔离任何一个微服务处理的数据集大小来解决所有这些问题。为此,我们利用了依赖注入和数据库重构。我们还进一步探索了自动化、配置项和部署。这些很容易让开发团队让业务发起人选择首先响应哪个行业趋势。这将带来成本效益、更好的业务响应、及时的技术采用、有效的扩展以及消除人员依赖。最后,我们讨论了 Azure 服务结构,并了解了服务结构及其不同的编程模型。

在下一章中,我们将继续将我们现有的应用转换为微服务风格的架构,并对我们的知识进行测试。我们将通过讨论新的技术堆栈(C#、EF 等)将我们的单片应用转换为微服务。我们还将介绍 seam 的概念,并讨论微服务通信。

问题

  1. 什么是微服务?
  2. 您能定义 Azure 服务结构吗?
  3. 什么是数据库分片?
  4. 什么是 TDD,开发者为什么要采用这个?
  5. 你能详细说明依赖注入(DI)吗?

二、重构整体

在前一章中,我们讨论了分层整体建筑的问题。本章旨在讨论单片到基于微服务的应用的过渡。我们将在这一章开始讨论我们的新技术堆栈(C#和实体框架),然后讨论好服务的特性,以及我们如何从现有系统中重构它们,并为产品和订单构建单独的微服务。

我们将涵盖以下主题:

  • 了解当前和新的技术堆栈
  • 微服务的规模
  • 什么是好的服务?
  • 理解接缝的概念
  • 微服务之间的通信
  • 重新审视 FlixOne 案例研究

技术要求

本章包含解释这些概念的各种代码示例。代码保持简单,仅用于演示目的。

要运行和执行代码,以下是先决条件:

  • Visual Studio 2019
  • 。网络核心 3.1

要运行这些代码示例,您需要安装 Visual Studio 2019(首选的 IDE)。为此,请从安装说明中提到的下载链接下载 Visual Studio 2019:https://docs . Microsoft . com/en-us/Visual Studio/install/install-Visual Studio。有多个版本的 Visual Studio 可供安装,包括免费版本的 Visual Studio Community。我们使用的是视窗操作系统。

如果你没有.NET Core 3.1 安装完毕,可以从这个链接下载:https://www.microsoft.com/net/download/windows

The complete source code is available here: https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2002

了解当前和新的技术堆栈

在我们继续讨论实现微服务的概念之前,值得一提的是我们将用来实现这些微服务的核心概念、语言和工具。在本章中,我们将对这些主题进行概述。

讨论–当前堆栈(C#、EF 和 SQL Server)

在本节中,我们将讨论我们在演示应用中使用的技术堆栈。

C# 8

C#是微软开发的一种编程语言,写这本书时的当前版本是 C# 8。这种面向对象和面向组件的语言始于 2002 年。当前版本有各种新特性,如 ValueTuple、解构器、模式匹配和 switch 语句本地函数。

我们不会详细讨论这些特性,因为这超出了本书的范围。详见https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/

实体框架核心

实体框架核心 ( EF 核心)是微软实体框架的跨平台版本,是目前最流行的对象关系映射器 ( 表单)之一。

ORM is a technique that helps you to query and manipulate data, according to the required business output.

英孚核心支持各种数据库。这里有完整的数据库列表:https://docs.microsoft.com/en-us/ef/core/providers/index。EF Core 的当前版本(撰写本文时)为 3.1(https://devblogs . Microsoft . com/dotnet/公告-实体-框架-Core-3-1-和-实体-框架-6-4/ )。

Visual Studio 2019

Visual Studio 是最好的 IDEs 之一,由微软创建。它使开发人员能够使用流行的语言以各种方式工作,如 C#、VB.NET 和 F#。Visual Studio 2019 当前版本为 VS16.4(https://docs . Microsoft . com/en-us/visualstudio/releases/2019/releases-notes)。要找到最新版本的 Visual Studio,请查看官方页面:https://docs . Microsoft . com/en-us/Visual Studio/install/update-Visual Studio?view=vs-2019

An IDE is an integrated development environment, a software application that provides a facility for programmers to write programs by using programming languages. For more information, visit https://en.wikipedia.org/wiki/Integrated_development_environment.

微软也为 macOS 发布了 Visual Studio,Visual Studio 有很多新功能。更多信息请参考https://www.visualstudio.com/vs/whatsnew/。在这本书里,所有的例子都是用 Visual Studio 2019 写的。您也可以在以下链接下载免费的社区版:https://www.visualstudio.com/.

搜寻配置不当的

微软 SQL Server ( MSSQL )是一个软件应用,是一个关系数据库管理系统。它主要用作数据库软件来存储和检索数据;它建立在结构化查询语言(SQL):http://searchsqlserver.techtarget.com/definition/SQL之上。

当前版本——即 SQL Server 2019——更加健壮,可以在 Windows 和 Linux 上使用。您可以从这里获得 SQL Server 2019:https://www.microsoft.com/en-IN/sql-server/sql-server-2019。请注意,我们将在本书中使用 SQL Server 2008 R2 或更高版本。

在开始阅读本书的实际操作说明之前,让我们先了解一下中的新功能.NET Core 和 C# 8.0,我们将在本书中利用它们。以下两个部分更像是对中新特性的探索.NET Core 3.1在 C# 8.0 中的新特性。我们将为这些特性添加深入的细节和说明。如果您已经了解新功能,可以跳过这些部分。

中的新功能.NET Core

在前一节中,我们概述了当前的技术堆栈。本节和下一节的目的是简要介绍即将推出的技术堆栈的预期特性.NET Core 3.1 和 C# 8.0),并利用我们的代码示例中的新功能。请注意,在写这本书的时候.NET Core 3.1 刚刚发布。

的释放.NET Core 3.1 是一个小版本,由早期版本的修复组成.NET Core 3.0。因此,在本节中,我们将重点讨论的新功能.NET Core 3.0。

.NET Core 3.1 is announced as a Long Term Support (LTS) release for three years.

.NET Core 3.0 应该会让桌面应用的开发人员兴奋不已。这个版本带来了最大的增强,现在它支持 Windows 桌面应用的开发。目前,此增强功能仅适用于在 Windows 机器上工作的用户,因为支持仅限于 Windows。支持包括开发视窗窗体和视窗演示基金会(WPF)应用。

我想你已经安装了.NET Core 3.1;如果还没有,请重访技术要求部分。

验证的安装.NET Core 3.1,可以查看当前安装版本的信息.NET Core。为此,请打开命令提示符并键入以下命令:

dotnet --info

前面的命令将告诉您有关已安装的的完整信息.NET Core 版本,如下图截图所示:

前面的截图显示了所有可用的信息.NET Core 版本。我们版本的.NET Core 是 3.1.100。

现在让我们看看如何利用我们之前提到的新功能。如果您想开发 Windows 桌面应用,那么您可以使用命令行界面 ( 命令行界面 ) dotnet命令或 Visual Studio 2019 来创建 WPF 和 Windows 窗体应用。

要从命令行界面开始,请执行以下步骤:

  1. 打开您的命令提示符或 Windows PowerShell(我们将使用 PowerShell)。
  2. 转到您想要的文件夹(在那里您想要创建一个新项目)——在我们的例子中,我们将使用Chapter02文件夹。
  3. 传递以下命令创建一个 Windows 窗体应用:
dotnet new winforms

前面的代码片段如下:

  • dotnet new:这将初始化一个有效的.NET Core 项目。
  • winforms:这是一个在初始化有效时被调用的模板.NET Core 项目。

If you want to use the VB language instead of C#, you just need to pass the preceding command with the language as dotnet new winforms -lang VB.

使用WinForms模板处理前面的命令并创建一个 Windows 窗体应用。下面的截图描述了项目步骤的创建:

我们已经创建了一个WinForms应用。要运行此项目,请传递以下命令:

dotnet run

前面的命令将运行项目并显示项目的输出。我们没有添加任何内容,因此我们将看到空白的 Windows 窗体,如下图所示:

  1. 传递以下命令创建一个 Windows 窗体应用:
dotnet new wpf

前面的命令使用wpf模板创建了一个 WPF 应用。下面的截图显示了创建应用的步骤:

我们已经创建了一个wpf应用。要运行此项目,请传递以下命令:

dotnet run

前面的命令将运行项目并显示项目的输出。我们没有添加任何内容,因此我们将看到空白的 WPF 表单,如下图所示:

可以使用更多带有dotnet new命令的模板;如果您不确定模板名称,只需传递以下命令:

dotnet new -l

前面的命令以表格形式给出了所有可用模板的列表。请参考下面的截图,其中显示了可用的模板:

前面的截图显示了可用的预安装模板;如果要安装任何附加模板,可以使用dotnet new -i <Tempate>命令。上一个快照包含以下内容:

  • Templates:这是模板的完整名称。
  • Short Name:这是与dotnet new命令一起使用的模板名称。
  • Language:这是支持模板的编程语言。模板的默认语言显示在括号[ ]中。
  • Tags:表示模板的类型。例如,Web标签指的是 ASP.NET Core(网络)项目模板。

dotnet new命令有各种选项,如下表所示:

| [计]选项 | 描述 | 句法 |
| -h--help | 这为命令提供了帮助,并提供了关于命令及其使用的信息。 | dotnet new -h |
| -l--list | 这给出了模板的完整列表。 | dotnet new wpf -l
dotnet new -l |
| -n--name | 这是使用特定模板创建的项目/应用名称。如果未指定名称,则使用当前文件夹的名称。 | dotnet new wpf -n "MyProjName" |
| -i-install | 这将安装一个新模板。 | dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0 |
| -lang--language | 这指定了在其中创建项目的语言。 | dotnet new winforms -lang VB |

如果您使用的是 VS16.4,并且有早期版本的.NET Core 3.0 和/或.NET Core3.1 安装后,您可能会看到以下屏幕。在这种情况下,版本、WinFormsWPF设计器支持不可用。如果您尝试打开我们之前创建的项目,您会注意到一个设计器错误,如下图所示。当您尝试打开窗体 1 的设计布局时,会出现此错误:

上一张截图中显示的问题可以在经典设计师的帮助下解决.NET 框架)。通过这种变通方法,我们可以看到一个设计者,并可以安排或设计我们的表单布局。这个话题超出了本书的范围,我们就不多赘述了。

To get more details and to know how we can fix the designer issue, you can refer to the official GitHub repository: https://github.com/dotnet/winforms/blob/master/Documentation/winforms-designer.md.

此修复程序随 Visual Studio Preview 一起提供(在撰写本书时);您至少需要 VS16.5 才能查看 WinForms 或 WPF 的设计器。您可以通过安装 VS16.5 进行检查。要确保您从 Visual Studio 选项中标记了预览 SDK,请转到工具|选项|环境|预览功能中的设置,然后确保您选中了使用预览窗口窗体设计器进行.NET Core 应用,如下图所示:

您需要重新启动 Visual Studio,打开 Windows 项目,然后在要在设计器中打开的文件名上单击“Shift+F7”。

C# 8.0 中的新特性

在本节中,我们将讨论 C# 8.0 中尚未发布的新特性(在撰写本书时)。在我们的代码示例中,我们使用了 C# 8.0 的最新预览版。

在开始讨论新特性之前,我们应该首先启用对 C#8.0 语言的支持。

使用 Visual Studio 支持 C# 8.0 语言

如果没有最新安装的 Visual Studio(参考技术要求,按照以下步骤启用对 C# 8.0 的支持:

打开项目属性,然后打开构建选项卡,然后单击高级...按钮,靠近页面底部。选择 C# 8.0(测试版),如下图所示,或者从“常规”组下的“语言版本”下拉列表中选择任何最新版本。下面的截图说明了每一步:

如果安装了最新版本的 Visual Studio,您将看到以下窗口:

根据前面的截图,Visual Studio 不允许我们更改/选择 C#版本。这是因为已经选择了 C#版本。C#编译器已经根据我们项目的目标框架为我们确定了默认语言版本。

下表显示了编译器如何选择默认版本:

| 目标框架 | C#语言版本 |
| .NET Core 3.x | Eight |
| .NET Core 2.x | Seven point three |
| .NET 标准(所有版本) | Seven point three |
| .NET 框架(所有版本) | Seven point three |

If a project targets a preview framework, then the compiler accepts all of the language syntaxes from the latest preview version.

现在,我们讨论一下 C# 8.0 的新特性。

指数和范围

作为一项新功能,现在开发人员可以根据数组的索引或指定范围轻松获取数组的元素值。这可以通过引入两个运算符来实现:^运算符代表从末尾开始的索引,..运算符代表范围。下面的截图代表了我们的一系列图书名称:

上一张截图显示了Books []每个元素的值,以及索引位置。

To showcase the feature, we have created a console app, using Visual Studio 2019.

在早期版本的 C#中,只能通过数组的第一个索引获取数组第一个元素的值;在这种情况下,我们将编写以下代码:

Console.WriteLine($"\tFirst element of array (index from start): Books{{0}} => {Books[0]}");

借助 C# 8.0 的一个新特性,现在可以使用^运算符获取元素值,它从末尾开始取索引和计数。我们的Books[]阵总长度为6。因此,为了获得第一个元素的值,我们将编写以下代码:

Console.WriteLine($"\tFirst element of array (index from end): Books{{^6}} => {Books[^6]}");

现在,运行代码,您将获得如下截图所示的输出:

在早期版本的 C#中,当我们需要一个数组的所有元素的值时,我们必须迭代数组的元素,从第 0 个索引开始,直到数组的长度。在 C# 8.0 中,当我们需要打印一个数组的值时,我们可以使用一个范围,在从开始索引值到结束索引值的范围内。在..算子的帮助下,现在我们可以这样得到Book []的值:Book [1..4]

为了使用这个特性,让我们编写以下代码:

//Print using range
Range book = 1..4;
var res =  Books[book] ;
Console.WriteLine($"\tElement of array using Range: Books[{book}] => {Books[book]}");

在前面的代码中,我们已经声明了一个范围值为14book变量,然后我们使用该变量打印范围内Books[]的值。运行代码并查看输出;输出显示在下面的截图中:

在上一个截图的高亮行中,你可能已经注意到我们的输出不如预期;我们得到System.String[]作为输出,但是我们期待数组的值。前面的输出是正确的——我们将Books[]的值存储在一个新的变量book中,并且我们必须迭代该变量来获得值。

要验证这一点,请在调试模式下运行前面的代码(从 Visual Studio 2019 开始),方法是按下 F5 功能键或使用菜单选项:调试|开始调试。确保您在正确的位置放置了一个断点来查看调试结果。以下截图是在调试过程中捕获的:

为了获取这些值,我们修改了前面的代码,下面是获取输出的代码:

//Print using range
Range book = 1..4;
var books =  Books[book] ;
Console.WriteLine($"\n\tElement of array using Range: Books[{book}] => {books}\n");
foreach (var b in books)
{
    Console.WriteLine($"\t{b}");
}    

现在,运行前面的代码,您应该会看到如下截图所示的输出:

根据我们的范围,前面的截图给出了准确的输出。有更多的指数和范围场景,但我们没有涵盖所有内容,因为它不在本书的范围内。

只读成员

有了这个特性,我们现在可以在结构中定义一个readonly成员。这意味着该成员不能被修改,也不能从任何成员函数中更改。static readonly成员的值只能在运行时从静态构造函数更改。readonly字段不可修改的重要性在于,它将有助于避免整个代码中的任何意外更改或错误分配。这在我们有默认值并希望在运行时更改字段的情况下也很重要。

以下代码定义了readonly成员:

private static readonly int num1=5;
private static readonly int num2=6;

前面的代码声明了两个预先赋值的readonly成员。您可以编写一个方法来使用这些成员的值——类似于public static int Add => num1 + num2;

运行代码,您应该会看到如下截图所示的输出:

前面的截图显示了我们Add方法的输出。此方法不应声明为只读,否则会给你一个错误。例如,以下代码会给您一个错误:

public readonly static int Add => num1 + num2;

前面的代码不起作用,如果您构建代码,它会给您一个错误。考虑以下截图:

您将看到一条消息,如前面的截图所示。

默认接口方法

有了这个特性,现在,开发人员可以在接口中添加带有主体的方法。之前我们必须在后续的类中显式地编写接口方法的功能。但是现在,我们将获得这些默认接口方法的优势,当这些方法在后续类中被调用时,我们可以从中获得预定义的结果。为了更好地理解这个特性,请考虑下面的代码:

public interface IProduct
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string ProductDesc() => $"Book:{Name} has Price:{Price}";
}

在前面的代码中,我们声明了一个IProduct接口,它有以下内容:

  • Id:这是唯一的产品 ID。
  • Name:这是产品名称。
  • Price:这是产品价格。
  • ProductDesc:这是我们界面的默认方法,总是会产生结果;在我们的例子中,它将返回string作为这个方法的结果。此外,该方法适用于所有实现IProduct接口的类。

让我们在Product类上实现IProduct接口。考虑以下代码:

public class Product : IProduct
{
    public Product(int id, string name, decimal price)
    {
        Id = id;
        Name = name;
        Price = price;
    }
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

除了IProduct接口的属性之外,我们的Product类没有定义任何额外的东西。这个类中没有额外的方法。ProductDesc()方法适用于本课程。考虑下面的代码,让我们在代码中使用这个默认方法:

IProduct product = new Product(1, "Design Patterns", 350.00M);
Console.WriteLine($"\t{product.ProductDesc()}");

在前面的代码中,我们声明了一个IProduct类型的product变量,然后简单地调用了ProductDesc()方法。这将产生如下截图所示的输出:

IProduct接口中定义的ProductDesc()方法适用于我们的Product类。

在本节中,我们讨论了 C# 8.0 的一些特性。请注意,我们还添加了已随 C# 8.0 预览版 5 发布的功能。然后我们使用 C#8.0 更新了特性;在 C# 8.0 的正式版本中,有可能会排除一些功能,或者添加更多功能。在这里,我概述了 C# 8.0 的预期特性,但是为了完整地引用这些特性,我建议查看 C#语言的官方文档。

现在,让我们详细看看微服务的规模,以及这如何有助于构建它们。

微服务的规模

在我们开始构建我们的微服务之前,我们应该清楚它们的一些基本方面,例如在调整我们的微服务时要考虑哪些因素,以及如何确保它们与系统的其他部分隔离开来。

顾名思义,微服务应该是微的。但是什么是微?微服务都是关于大小和粒度的。为了更好地理解这一点,让我们考虑一下在第 1 章微服务简介中讨论的应用。

我们希望在这个项目上工作的团队在他们的代码方面始终保持同步。当我们发布完整的项目时,保持同步更加重要。我们首先需要将我们的应用及其特定部分分解成主服务的更小的功能/部分。让我们讨论微服务高级隔离需要考虑的因素:

  • 需求变化带来的风险:一个微服务的需求变化应该独立于其他微服务。在这种情况下,我们将把我们的软件隔离/分割成更小的服务,这样,如果一个服务中有任何需求变化,它们将独立于另一个微服务。
  • 功能改变:我们将把很少改变的功能从可以频繁修改的依赖功能中分离出来。例如,在我们的应用中,客户模块通知功能很少会改变。然而,其相关模块,如Order,更有可能在其生命周期中有频繁的业务变更。
  • 团队变化:我们还应该考虑以一个团队可以独立于所有其他团队工作的方式隔离模块。如果让一个新开发人员富有成效的过程——关于这些模块中的任务——不依赖于团队之外的人,这意味着我们处于有利地位。
  • 技术变化:技术使用需要在每个模块内部垂直隔离。一个模块不应依赖于另一个模块的技术或组件。我们应该严格隔离在不同技术或堆栈中开发的模块,或者将它们转移到一个公共平台作为最后的手段。

我们的主要目标不应该是使服务尽可能小。相反,我们的目标应该是隔离已识别的有界上下文,并保持它很小。

现在我们知道了微服务的大小。接下来,让我们学习什么是好的服务。

什么是好的服务?

在微服务被概念化之前,每当我们想到企业应用集成,中间件看起来是最可行的选择。软件供应商提供了企业服务总线 ( ESB ,这是中间件的最佳选择之一。

除了考虑这些解决方案,我们的主要优先事项应该是架构特性。当微服务到来时,中间件不再是一个考虑因素。相反,重点转移到业务问题的思考以及如何借助架构解决这些问题。

为了使服务易于开发人员和用户使用和维护,服务必须具有以下特征(我们也可以将这些视为好服务的特征):

  • 标准数据格式:好的服务应该遵循标准化的数据格式,同时与其他组件交换服务或系统。中使用的最流行的数据格式.NET 栈是 XML 和 JSON。
  • 标准通信协议:好的服务应该遵守标准的通信格式,比如 SOAP 和 REST。
  • 松耦合:好的服务最重要的一个特点就是跟松耦合。当服务松散耦合时,我们不必担心变化。一项服务的更改不会影响其他服务。

本节帮助我们了解什么使服务变得好以及如何变得好。我们接下来将讨论领域驱动设计及其对微服务的重要性。

DDD 及其对微服务的重要性

领域驱动设计 ( DDD )是设计复杂系统的方法和过程。在本节中,我们将简要讨论 DDD 以及它在微服务环境中的重要性。

领域模型设计

领域设计的主要目标是理解确切的领域问题,然后起草一个可以用任何语言或一组技术编写的模型。例如,在我们的 FlixOne 书店应用中,我们需要了解订单管理和库存管理。

以下是领域驱动模型的一些特征:

  • 一个领域模型应该关注一个特定的业务模型,而不是多个业务模型。
  • 它应该是可重用的。
  • 它应该被设计成可以以松散耦合的方式调用,而不像系统的其他部分。
  • 它应该独立于持久性实现进行设计。
  • 它应该从一个项目中被拉到另一个位置,所以它不应该基于任何基础设施框架。

接下来,让我们看看这个模型对微服务有多重要。

微服务的重要性

DDD 是蓝图,可以通过微服务来实现。换句话说,一旦 DDD 完成,我们就可以使用微服务来实现它。这就像在我们的应用中,我们可以轻松地实现订单服务、库存服务、跟踪服务等等。

一旦你满意地处理了过渡过程,就应该进行一个简单的练习。这将帮助您验证微服务的大小是否足够小。每个系统都是独一无二的,有自己的复杂程度。考虑到您的域的这些级别,您需要有一个可以相互对话的域对象的最大数量的基线。如果有任何服务没有达到这个评估标准,那么您就有可能再次评估您的过渡。然而,不要带着特定的数字进入这个练习;你总是可以放松。只要你正确地遵循了所有的步骤,系统对你来说应该没问题。

如果您觉得这个基线过程对您来说很难实现,您可以通过遍历每个微服务中的所有接口和类来走另一条路线。考虑到我们已经遵循的所有步骤,以及行业标准的编码准则,任何对系统不熟悉的人都应该能够理解它的目的。

您还可以执行另一个简单的测试来检查是否实现了服务的正确垂直隔离。您可以部署它们中的每一个,并让它们与仍然不可用的其余服务一起使用。如果你的服务上线了,并继续监听收到的请求,你可以拍拍自己的背。

隔离部署能力可以带来许多好处。独立部署它们的能力允许它们中的主机进入自己的独立进程。它还允许您利用云的力量以及您能想到的任何其他混合托管模式。你也可以自由选择不同的技术。

了解了 DDD 之后,我们现在可以继续讨论 seam 的概念。

理解接缝的概念

微服务的核心是独立于系统的其他部分来处理特定功能的能力。这转化为前面讨论的所有优势,例如减少模块依赖性、代码可重用性、更容易的代码维护和更好的部署。

在我看来,在微服务的实现过程中,应该保持相同的属性。为什么将单片迁移到微服务的整个过程会很痛苦,而且不如使用微服务本身有回报?请记住,过渡不能一蹴而就,需要精心规划。许多有能力的解决方案架构师在展示他们的高能力团队时有不同的方法。答案不仅在于已经提到的几点,还在于企业本身的风险。

这是完全可以实现的。然而,我们必须正确识别我们的方法来实现它。否则,将单一应用转换为微服务的整个过程可能会非常糟糕。让我们看看这是如何做到的。

模块相互依赖

当试图将一个单一的应用转换成一个微服务风格的架构时,这应该永远是一个起点。识别并挑选应用中其他模块最不依赖的部分,也是最不依赖它们的部分。

理解这一点非常重要,通过识别应用的这些部分,您不只是试图挑选最不具挑战性的部分来处理。但是,与此同时,您已经确定了接缝,这是最容易看到的变化部分。这些是应用的一部分,我们将首先在其中执行必要的更改。这使我们能够将这部分代码与系统的其他部分完全隔离开来。它应该准备好成为微服务的一部分,或者在本练习的最后阶段进行部署。

尽管已经发现了这些漏洞,但实现微服务式开发的能力还有点远。在这一节中,我们介绍了 seam 的概念,然后我们看到了什么是模块相互依赖。这是一个好的开始。然而,基于微服务的应用需要更多的模式,我们需要了解这项技术。在接下来的部分,我们将讨论这项技术。

技术

这里需要双管齐下。首先,您必须确定应用基础框架的哪些不同特性正在被利用。例如,这种区别可以是基于对某些数据结构、正在执行的进程间通信或报告生成活动的严重依赖的实现。这是容易的部分。

然而,作为第二步,我建议您变得更加自信,并选择使用不同于当前使用的技术类型的代码片段。例如,可能有一段代码依赖于简单的数据结构或基于 XML 的持久性。在系统中识别此类行李,并将其标记为过渡。这种双管齐下的方法需要非常谨慎。做出过于雄心勃勃的选择可能会让你走上一条类似于我们一直试图完全避免的道路。

对于最终的微服务风格的架构应用来说,其中一些部分可能看起来不太有希望。现在仍然应该处理它们。最终,它们将允许您轻松执行过渡。技术和框架在任何应用的开发中都起着重要的作用,但是当我们处理基于微服务架构风格的应用时,技术是最重要的。

这一节解释了技术;在下一节中,我们将讨论团队结构。

团队结构

随着识别过程的每一次迭代,这个因素变得越来越重要。可能会有基于不同理由的不同团队,例如他们的技术技能、地理位置或安全要求(员工对自由职业者)。

如果有一部分功能需要特定的技能组合,那么您可能会考虑另一个可能的 seam 候选人。团队可以由不同程度的这些分化因素组成。作为向微服务过渡的一部分,使他们能够独立工作的明显差异可以进一步优化他们的生产力。

这也可以以保护公司知识产权的形式提供好处;将应用的特定部分外包给顾问并不少见。允许顾问或合作伙伴仅在特定模块上为您提供帮助的能力使流程更加简单和安全。

团队及其成员对于任何应用的执行都非常重要。在基于微服务的应用中,我们为团队提供了灵活性,任何人都可以在不干扰他人任务或活动的情况下处理单个服务。为了了解这是如何发生的,在下一节中,我们将讨论数据库。

数据库

任何企业系统的核心和灵魂都是它的数据库。这是系统在任何一天的最大资产。在这样的演习中,它也是整个系统中最脆弱的部分。(难怪每当你要求数据库架构师做哪怕是最小的更改时,他们听起来都很刻薄和碍眼。)它们的域由数据库表和存储过程定义。

它们的域的健康状况也是由引用完整性和执行各种事务所需的时间来判断的。我不再责怪建筑师做得过头了。他们这样做是有原因的:他们过去的经历。是时候改变这种状况了。让我告诉你,这并不容易,因为当我们走上这条路时,我们将不得不使用一种完全不同的方法来处理数据完整性。

您可能会认为最简单的方法是一次性划分整个数据库,但事实并非如此。这可能会导致我们一直试图避免的局面。让我们看看如何更有效地进行这项工作。

在模块依赖关系分析之后,当您继续挑选片段时,请确定用于与数据库交互的数据库结构。这里需要执行两个步骤。首先,检查是否可以隔离代码中要分解的数据库结构,然后将其与新定义的垂直边界对齐。其次,确定需要什么来分解底层数据库结构。

如果分解底层数据结构似乎很困难,也不要担心。如果它似乎涉及到您还没有开始转移到微服务的其他模块,这是一个好的迹象。不要让数据库的变化定义了您将挑选并迁移到微服务风格架构的模块;反过来说。这确保了当数据库发生变化时,依赖于变化的代码已经准备好吸收变化。

这确保了当您已经忙于修改依赖于这部分数据库的代码时,您不会再挑起数据完整性的战斗。然而,这样的数据库结构应该引起您的注意,以便接下来选择依赖于它们的模块。这将允许您一次轻松完成所有相关模块向微服务的转移。参考下图:

这里,我们还没有破坏数据库。相反,作为第一步的一部分,我们已经简单地将数据库访问部分分成了几层。

我们在这里所做的是将代码数据结构映射到数据库,以便它们不再相互依赖。让我们看看当我们移除外键关系时,这一步将如何进行。

如果我们可以将用于访问数据库的代码结构与数据库结构一起转换,我们将节省时间。这种方法可能因系统而异,并且会受到我们个人偏见的影响。如果您的数据库结构变化似乎正在影响尚未标记为过渡的模块,现在就继续。

当您分解这个数据库表或将其与另一个部分结构合并时,您还需要了解什么样的更改是可以接受的。最重要的是不要回避打破那些外来的关键关系。这听起来可能与我们维护数据完整性的传统方法有很大不同。

然而,在重组数据库以适应微服务架构时,移除外键关系是最基本的挑战。请记住,微服务意味着独立于其他服务。如果与系统的其他部分存在外键关系,这就使得它依赖于拥有该部分数据库的服务。参考下图:

作为第二步的一部分,我们保留了数据库表中的外键字段,但删除了外键约束。因此, ORDER 表仍然保存着关于 ProductID 的信息,但是外键关系现在被破坏了。参考下图:

这就是我们的微服务式架构最终的样子。中央数据库将被移走,取而代之的是每个服务都有自己的数据库。因此,通过分离代码中的数据结构和移除外键关系,我们已经准备好最终进行更改。上图中微服务的连接边界表示服务间的通信。

执行了这两个步骤后,您的代码现在可以将 ORDERPRODUCT 拆分为单独的服务,每个服务都有自己的数据库。

如果这里的讨论让你对迄今为止安全执行的所有交易感到困惑,那么你并不孤单。交易挑战的结果无论如何都不小,值得重点关注。稍后我们将详细介绍这一点。

在本节中,我们讨论了一个数据库,我们称之为应用的主存储。我们还可以将单个数据库分解成每个服务可以使用的小数据库,因此我们可以说每个服务有一个数据库。

在我们更进一步之前,数据库中还有另一部分成为无人区。这是主数据,或者说是静态数据,有些人可能会这样称呼它。

主数据

处理主数据取决于您的个人选择和系统特定要求。如果您看到主数据在很长一段时间内都不会改变,并且占据了少量的记录,那么您最好使用配置文件甚至代码枚举。

这需要有人在发生更改时偶尔推出配置文件。然而,这在未来仍然留下了空白。由于系统的其余部分将依赖于这一个模块,它将负责这些更新。如果该模块运行不正常,依赖它的系统的其他部分也会受到负面影响。

另一种选择是将主数据打包到一个单独的服务中。通过服务交付主数据将提供这样的优势,即服务可以立即了解变化,并了解使用它的能力。

需要时,请求此服务的过程可能与读取配置文件的过程没有太大区别。它可能会慢一些,但是只需要做必要的次数。

此外,您还可以支持不同的主数据集。维护每年都不同的产品集是相当容易的。有了微服务架构风格,将来独立于任何类型的外部依赖总是一个好主意。

在本节中,我们讨论了主数据。当我们与数据交互时,事务起着重要的作用,所以在下一节中,我们将讨论它们。

处理

随着外键的消失以及数据库被分成更小的部分,我们需要设计自己的机制来处理数据完整性。在这里,我们需要考虑到并非所有服务都将在其各自的数据存储范围内成功完成事务的可能性。

一个很好的例子是用户订购特定的产品。接受订单时,有足够的数量可供订购。但是,在记录订单时,由于某种原因,产品服务无法记录订单。我们还不知道是因为数量不足还是系统内部的其他通信故障。这里有两种可能的选择。我们一个一个来讨论。

第一种选择是再试一次,并在以后某个时间执行事务的剩余部分。这将要求我们以跨服务跟踪单个事务的方式编排整个事务。因此,必须跟踪导致为多个服务执行事务的每个事务。如果其中一个没有通过,就应该重试。这可能适用于长期运营。

但是,对于其他操作,这可能会导致真正的问题。如果操作持续时间不长,而您仍然决定重试,结果将导致锁定其他事务或使事务等待,这意味着不可能完成它。

另一种选择是取消分散在各种服务中的整个事务集。这意味着在整个交易集的任何阶段的单个失败将导致所有先前交易的逆转。

这是一个需要最大限度谨慎的领域,是时候好好投资了。只有当事务在任何微服务风格的架构应用中被很好地规划时,才能保证稳定的结果。

当我们处理数据库操作时,事务非常重要;这些操作可以是提取、插入等等。在事务的帮助下,如果任何操作失败,我们可以回滚事务范围内的流程中的完整操作。

现在,让我们继续了解微服务之间的通信。

微服务之间的通信

在前一节中,我们将Order模块分为订单服务,并讨论了如何分解ORDERPRODUCT表之间的外键关系。

在一个单一的应用中,我们有一个单一的存储库,它查询数据库以从ORDERPRODUCT表中获取记录。然而,在我们即将推出的微服务应用中,我们将在订单服务产品服务之间隔离存储库。每个服务都有各自的数据库,每个服务只能访问自己的数据库。订单服务只能访问订单数据库,而产品服务只能访问产品数据库订单服务不允许访问产品数据库,反之亦然。

We will discuss communication between microservices in Chapter 3, Effective Communication between Services, in detail.

下图显示了使用应用编程接口网关与不同服务的交互:

在上图中,我们可以看到我们的 UI 正在通过 API 网关订单服务产品服务进行交互。这两种服务在物理上是相互分离的,并且这些服务之间没有直接的交互。以这种方式执行的通信也被称为基于应用编程接口网关模式的通信。

应用编程接口网关只是一个中间层,用户界面可以通过它与微服务交互。它还提供了一个更简单的界面,并简化了使用这些服务的过程。它根据需要为不同的客户端(浏览器和桌面)提供不同的粒度级别。

我们可以说,它向移动客户端提供粗粒度的 API,向桌面客户端提供细粒度的 API,并且它可以在引擎盖下使用高性能网络,以提供一些重要的吞吐量。

我们可以很容易地将粒度定义为(另请参见https://software engineering . stackexchange . com/questions/385313/什么是粒度):

"...a system is broken down into small parts; large systems can further be broken or torn down to finer parts."

应用编程接口网关对微服务的好处

毫无疑问,API 网关对微服务是有益的。使用应用编程接口网关,您可以执行以下操作:

  • 通过应用编程接口网关调用服务
  • 减少客户端和应用之间的往返行程
  • 客户端可以在一个地方访问不同的应用编程接口,由网关隔离

它为客户提供了灵活性,使他们可以在需要时与不同的服务进行交互。这样,根本不需要公开完整的/所有的服务。应用编程接口网关是完整应用编程接口管理的一个组成部分。在我们的解决方案中,我们将使用 Azure API Management,我们将在第 3 章服务之间的有效通信中进一步解释。

应用编程接口网关与应用编程接口管理

在前一节中,我们讨论了应用编程接口网关如何对其客户端隐藏实际的应用编程接口,然后简单地将调用从这些客户端重定向到实际的应用编程接口。应用编程接口管理解决方案提供了一个完整的管理系统来管理其外部消费者的所有应用编程接口。所有的应用编程接口管理解决方案,如 Azure 应用编程接口管理(https://docs.microsoft.com/en-us/azure/api-management/)都提供了各种功能,例如:

  • 设计
  • 发展
  • 安全
  • 出版
  • 可量测性
  • 监视
  • 分析
  • 货币铸造

接下来,让我们重温 FlixOne 案例研究,以便更好地理解。

重新审视 FlixOne 案例研究

在前一章中,我们看了一个虚构公司 FlixOne Inc .的例子,该公司在电子商务领域运营,并拥有自己的.NET 单片应用:FlixOne 书店。

我们已经讨论了以下内容:

  • 如何隔离代码
  • 如何隔离数据库
  • 如何反规范化数据库
  • 如何开始过渡
  • 可用的重构方法

前面几点很重要,因为我们正在将我们的整体应用转换为基于微服务的应用。在第 1 章微服务简介中,我们已经讨论了为什么我们想要构建基于微服务的应用。在现有的电子商务市场中,需要应用的需求、频繁的更新和 100%的正常运行时间(应用的可用性)。

在接下来的部分中,我们将开始将. NET 整体转换为微服务应用。

先决条件

我们将使用以下工具和技术,同时将我们的整体应用转变为微服务风格的体系结构:

  • Visual Studio 2019 或更高版本
  • C# 8.0
  • ASP.NET Core MVC/网络应用编程接口
  • 实体框架核心
  • SQL Server 2008 R2 或更高版本

过渡到我们的产品服务

我们已经有了我们的产品模块。我们现在将收回这个模块,从一个新的 ASP.NET Core MVC 项目开始。为此,请遵循我们在前面章节和第 1 章微服务简介中讨论的所有步骤。让我们检查一下我们将使用的技术和数据库:

  • 技术栈:我们的产品服务已经选择了这个;我们将和 ASP.NET 芯、c#实体框架 ( EF )等一起走。微服务可以使用不同的技术堆栈编写,并且可以由不同技术创建的客户端使用。对于我们的产品服务,我们将选择 ASP.NET 芯。
  • 数据库:我们已经在第 1 章微服务介绍中讨论过这个问题,当时我们讨论了一个单一应用和分离它的数据库。在这里,我们将使用 SQL Server,数据库模式将是Product而不是dbo

我们的产品数据库是独立的。我们将在产品服务中使用该数据库,如下图所示:

我们为我们的产品服务创建了一个独立的产品数据库。我们没有迁移整个数据集。在接下来的部分中,我们还将讨论产品数据库迁移。

迁移很重要,因为我们有许多 FlixOne 书店客户的现有记录。我们不能忽略这些记录,它们需要迁移到我们修改过的结构中。我们开始吧。

迁移

在前面的部分中,我们分离了我们的产品数据库,以确保它只被我们的产品服务使用。我们还选择了我们选择的技术堆栈来构建我们的微服务(产品服务)。

在本节中,我们将讨论如何迁移我们现有的代码和数据库,以确保它们适合我们的新架构风格。

代码迁移

代码迁移不仅仅涉及从现有的单一应用中拉出几层代码,然后将其与我们新创建的产品服务捆绑在一起。为了实现这一点,你需要实现你所学到的一切,直到现在。在现有的单片应用中,我们有一个单一的存储库,它对所有模块都是通用的。但是,对于微服务,我们将分别为每个模块创建存储库,并保持它们彼此隔离:

在上图中,产品服务有一个产品存储库,它进一步与其指定的数据存储交互,命名为产品数据库。我们现在将进一步讨论微组件。它们只不过是应用(微服务)的独立部分,即公共类和业务功能。这里值得注意的是产品存储库本身是微服务世界中的一个微组件。

在我们的最终产品服务中,这将在 ASP.NET Core 完成,我们将与模型和控制器一起创建我们的 RESTful 应用编程接口。让我们简单描述一下这两者:

  • 模型:这是一个表示产品服务中数据的对象。在我们的例子中,识别的模型被堆叠到产品和类别字段中。在我们的代码中,模型只不过是一组简单的 C#类。当我们用英孚核心来说话时,它们通常被称为普通旧 CLR 对象 ( POCOs )。POCOs 只不过是没有任何数据访问功能的简单实体。
  • 控制器:这是一个简单的 C#类,继承了Microsoft.AspNetCore.Mvc命名空间的抽象类控制器。它处理 HTTP 请求,并负责创建要发回的 HTTP 响应。在我们的产品服务中,我们有一个产品控制器来处理一切。

让我们按照一步一步的方法来创建我们的产品服务。

创建我们的项目

正如前面几节已经决定的,我们将使用 Visual Studio 在 ASP.NET Core 3.0 或 C# 8.0 中创建ProductService。让我们看看要做到这一点需要哪些步骤:

  1. 启动 Visual Studio。
  2. 通过导航到文件|新建|项目来创建新项目。
  3. 从可用的模板选项中,选择 ASP.NET Core 网络应用,然后单击下一步。以下屏幕截图显示了“创建新项目”窗口:

  1. 输入项目名称为FlixOne.BookStore.ProductService,点击确定。

  2. 从模板屏幕中,选择网络应用(模型-视图-控制器),并确保您已选择.NET Core 和 ASP.NET Core 3.1,如下图所示:

  1. 将其余选项保留为默认选项,然后单击创建。新的解决方案应该如下图所示:

  1. 在解决方案资源管理器中,右键单击(或按下 Alt + 进入)项目,然后单击属性。
  2. 在属性窗口中,单击构建|高级。语言版本是根据框架版本自动选择的。我们的框架是.NET Core 3.0,所以我们的语言应该是 C# 8.0,如下图截图所示:

Please make sure you select the latest version of C# 8.0. The use of older versions might be troublesome.

添加模型

在我们的单片应用中,我们还没有任何模型类。因此,让我们按照要求添加一个新模型。

要添加新模型,如果项目中不存在,则添加一个新文件夹并命名为Models。在解决方案资源管理器中,右键单击项目,然后单击添加|新建文件夹:

将所有模型类放入名为Models的文件夹中没有硬性规定。事实上,我们可以将模型类放在应用中项目的任何地方。我们遵循这种做法,因为它从文件夹名称中变得不言自明。同时,它很容易识别出这个文件夹是用于模型类的。

要添加新的ProductCategory类(这些类将代表我们的概念验证操作),请执行以下操作:

  1. 右键点击Models文件夹,选择【添加|新项目|类】,命名为Product;重复此步骤,添加另一个类,并将其命名为Category

  2. 现在,将描述我们的产品数据库列名的属性分别添加到ProductCategory表中。

There is no restriction, regarding having the property name match the table column name. It is just a general practice.

下面的代码片段描述了我们的Product模型类的样子:

using System;
namespace FlixOne.BookStore.ProductService.Models
{
    public class Product
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Image { get; set; }
        public decimal Price { get; set; }
        public Guid CategoryId { get; set; }
        public virtual Category Category { get; set; }
    }
}

前面的代码示例表示一个产品模型,它包含以下内容:

  • Id全球唯一标识符 ( GUID )并且代表记录 ID。
  • Name是字符串类型的属性,保存产品名称。
  • Description是一个字符串类型的属性,保存了产品的完整描述。
  • Image是一个字符串类型的属性,包含一个 Base64 字符串。
  • Price是十进制类型的属性,保存产品的价格。
  • CategoryId为 GUID 它保存产品类别的记录标识。
  • Category为虚拟财产,包含产品类别的完整信息。

下面的代码片段显示了我们的Category.cs模型类的样子:

using System;
using System.Collections.Generic;
namespace FlixOne.BookStore.ProductService.Models
{
    public class Category
    {
        public Category()
        {
            Products = new List<Product>();
        }
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public IEnumerable<Product> Products { get; set; }
    }
}

前面的代码代表了我们的类别模型,它包含以下内容:

  • Id是一个 GUID,代表一个记录 ID。
  • Name是字符串类型属性,保存类别名称。
  • Description是一个字符串类型属性,保存了类别的完整描述。
  • Products是属于当前记录类别的所有产品的集合。

让我们在代码示例中添加一个存储库,这样我们就可以进行一些实际的数据库调用。

添加存储库

在我们的整体应用中,我们在整个项目中都有一个公共的存储库。在ProductService中,通过遵循到目前为止所学的所有原则,我们将创建微组件,这意味着封装数据层的独立存储库。

A repository is nothing but a simple C# class that contains the logic to retrieve data from the database and map it to the model.

添加存储库就像执行以下步骤一样简单:

  1. 新建一个文件夹,然后命名为Persistence
  2. 添加IProductRepository接口和一个将实现IProductRepository接口的ProductRepository类。
  3. 同样,我们命名文件夹Persistence是为了遵循易于识别的一般原则。

以下代码片段概述了IProductRepository界面:

using System;
using System.Collections.Generic;
using FlixOne.BookStore.ProductService.Models;

namespace FlixOne.BookStore.ProductService.Persistence
{
  public interface IProductRepository
 {
 void Add(Product product);
 IEnumerable<Product> GetAll();
 Product GetBy(Guid id);
 void Remove(Guid id);
    void Update(Product product);
  }
}

我们的IProductRepository界面有所有需要的方法:

  • Add:这个方法负责添加一个新产品。
  • GetAll获取所有产品记录,返回产品集合。
  • GetBy根据给定的产品标识获取一个产品。
  • Remove删除特定记录。
  • Update负责更新现有记录。

下一个代码片段提供了ProductRepository类的概述(它仍然没有任何实现,并且还没有与数据库进行任何交互):

using FlixOne.BookStore.ProductService.Contexts;
using FlixOne.BookStore.ProductService.Models;
using Microsoft.EntityFrameworkCore;

 namespace FlixOne.BookStore.ProductService.Persistence
 {
 public class ProductRepository : IProductRepository
 {
 public void Add(Product Product)
 {
 throw new NotImplementedException();
 }
 public IEnumerable<Product> GetAll()
 {
       throw new NotImplementedException();
     }
     public Product GetBy(Guid id)
     {
       throw new NotImplementedException();
     }
 public void Remove(Guid id)
     {
       throw new NotImplementedException();
     }
     public void Update(Product Product)
     {
       throw new NotImplementedException();
     }
   }
 }

前面的代码实现了IProductRepository接口。该代码示例用于演示目的,因此我们没有向实现的方法添加定义。

接下来,让我们看看如何使用Startup.csConfigureServices注册我们的存储库。

注册存储库

对于ProductService,我们将使用内置的依赖注入支持与 ASP.NET Core。为此,请遵循以下简单步骤:

  1. 打开Startup.cs
  2. 将存储库添加到ConfigureServices方法中。应该是这样的:
using FlixOne.BookStore.ProductService.Persistence;
public void ConfigureServices(IServiceCollection services)
{
 // Add framework services.

  services.AddSingleton<IProductRepository, 
  ProductRepository>();
}

在前面的代码中,我们将我们的存储库注册为由提供的控制反转 ( IoC )的单一服务.NET Core 框架。

在接下来的部分中,我们将讨论我们的产品控制器,并了解如何在代码中添加控制器。

添加产品控制器

最后,我们已经到了可以继续添加控制器类的阶段。该控制器将负责用适用的 HTTP 响应来响应传入的 HTTP 请求。如果你想知道该怎么做,你可以看到HomeController类,因为它是 ASP.NET Core 模板提供的默认类。

右键点击controllers文件夹,选择添加|新建项目选项,选择 API 控制器类。命名为ProductController。在这里,我们将利用单片应用中的任何代码和功能。回到遗留代码,看看您在那里执行的操作;你可以借它们来上我们的ProductController课。参考以下截图:

在我们对ProductController进行了所需的修改之后,它应该看起来类似于这样:

 using Microsoft.AspNetCore.Mvc;
 using FlixOne.BookStore.ProductService.Persistence;
 namespace FlixOne.BookStore.ProductService.Controllers
 {
   [Route("api/[controller]")]
   public class ProductController : Controller
   {
     private readonly IProductRepository _ProductRepository;
     public ProductController(IProductRepository ProductRepository)
     {
       _ProductRepository = ProductRepository;
     }
   }
 }

在前一节中,我们为 IoC 注册了ProductRepository,在ProductController中,我们正在使用存储库。在前面的代码示例中,我们使用了构造函数注入,并且使用了带有IProductRepository类型参数的参数化构造函数。

产品服务应用编程接口

在我们的单片应用中,对于Product模块,我们正在执行以下操作:

  • 添加新的Product模块
  • 更新现有的Product模块
  • 删除现有的Product模块
  • 检索Product模块

现在,我们将创建ProductService;我们需要以下 API:

| 原料药资源 | 描述 |
| GET / api / Product | 获取产品列表 |
| GET / api / Product / {id} | 获取产品 |
| PUT / api / Product / {id} | 更新现有产品 |
| DELETE / api / Product / {id} | 删除现有产品 |
| POST / api / Product | 添加新产品 |

接下来,我们将看看如何为这些添加 EF Core 支持。

添加英孚核心支持

在进一步之前,我们需要添加 EF Core 支持,这样我们的服务就可以与产品数据库进行交互。到目前为止,我们还没有向存储库中添加任何可以与数据库交互的方法。要添加 EF Core 支持,我们需要添加 EF Core 的sqlserver包(我们正在添加sqlserver包,因为我们使用 SQL Server 作为我们的 DB 服务器)。打开获取包管理器(工具|获取包管理器|管理获取包)。

打开 NuGet 包,搜索Microsoft.EntityFrameworkCore.SqlServer:

前面的截图显示了Microsoft.EntityFrameworkCore.SqlServer的搜索结果作为输入。

EF Core DbContext

在前一节中,我们添加了支持 SQL Server 的 EF Core 3.1 包;现在我们需要创建一个上下文,这样我们的模型就可以与我们的产品数据库交互。我们有ProductCategory两种型号,你可以参考以下步骤:

  1. 添加新文件夹,然后命名为Contexts—不强制添加新文件夹。

  2. Contexts文件夹中,添加一个新的 C#类并命名为ProductContext。我们正在为ProductDatabase创建DbContext,所以为了使它在这里相似,我们创建ProductContext

  3. 确保ProductContext类继承了DbContext类。

  4. 进行更改,我们的ProductContext类将如下所示:

 using FlixOne.BookStore.ProductService.Models;
 using Microsoft.EntityFrameworkCore;
 namespace FlixOne.BookStore.ProductService.Contexts
 {
   public class ProductContext : DbContext
   {
     public ProductContext(DbContextOptions<
     ProductContext>options): base(options)
     { }
     public ProductContext()
     { }
     public DbSet<Product> Products { get; set; }
     public DbSet<Category> Categories { get; set; }
   }
 }

我们已经创建了上下文,但是这个上下文独立于产品数据库。我们需要添加一个提供者和连接字符串,这样ProductContext就可以和我们的数据库对话了。

  1. 再一次,打开Startup.cs文件,在ConfigureServcies方法下,为我们的英孚核心支持添加SQL Server db提供商。一旦添加了提供商的ConfigureServcies方法,我们的Startup.cs文件将如下所示:
 public void ConfigureServices(IServiceCollection services)
 {
   // Add framework services.

   services.AddSingleton<IProductRepository, ProductRepository>();
   services.AddDbContext<ProductContext>(o =>o.UseSqlServer
   (Configuration.GetConnectionString("ProductsConnection" )));
 }
  1. 打开appsettings.json文件,然后添加需要的数据库连接字符串。在我们的提供商中,我们已经将连接密钥设置为ProductConnection。现在,添加以下代码以设置具有相同键的连接字符串(将Data Source更改为您的数据源):
 {
   "ConnectionStrings": 
   {
     "ProductConnection":
     "Data Source=.SQLEXPRESS;Initial Catalog=ProductsDB;
     IntegratedSecurity=True;MultipleActiveResultSets=True"
   }
 }

前面的代码包含连接我们的应用和数据库的连接字符串。

英孚核心迁移

虽然我们已经创建了产品数据库,但我们不应低估 EF Core 迁移的力量。EF Core 迁移将有助于我们对数据库执行任何未来的修改。这种修改的形式可以是简单的字段添加或对数据库结构的任何其他更新。我们每次都可以简单地依靠这些 EF Core 迁移命令来为我们进行必要的更改。要利用这一功能,请执行以下简单步骤:

  1. 转到工具|否获取包管理器|包管理器控制台。
  2. 从包管理器控制台运行以下命令:
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.Design 
  1. 要启动迁移,请运行以下命令:
 Add-Migration ProductDB

需要注意的是,这只能在第一次执行(当我们还没有通过这个命令创建数据库时)。

  1. 现在,只要您的模型有任何变化,只需执行以下命令:
Update-Database

从前面的命令中,我们可以更新数据库。

至此,我们完成了我们的ProductDatabase创造。现在,是时候迁移我们现有的数据库了。

数据库迁移

从旧数据库迁移到现有数据库有许多不同的方法。我们的整体应用目前有一个巨大的数据库,也包含大量的记录。简单地使用数据库 SQL 脚本是不可能迁移它们的。

我们需要显式创建一个脚本来迁移数据库及其所有数据。另一种选择是根据需要创建数据库包。根据数据和记录的复杂性,您可能需要创建多个数据包,以确保数据正确迁移到我们新创建的数据库ProductDB

在本节中,我们学习了数据迁移,并在我们想象的应用中找到了数据迁移的范围。一般来说,SQL 脚本足以进行数据库(模式和数据)迁移。但是如果数据库很大,即使我们已经将它分解成每个服务的小数据库,我们在进行数据库迁移时也需要采取更多的预防措施。

在下一节中,我们将重新访问应用存储库和控制器,看看我们的应用如何与数据库交互。

重访存储库和控制器

我们现在准备通过新创建的存储库来促进模型和数据库之间的交互。对ProductRepository进行适当的修改后,会是这样的:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using FlixOne.BookStore.ProductService.Contexts;
using FlixOne.BookStore.ProductService.Models;
namespace FlixOne.BookStore.ProductService.Persistence
 {
   public class ProductRepository : IProductRepository
   {
     private readonly ProductContext _context;
     public ProductRepository(ProductContext context)
     {
       _context = context;
     }
     public void Add(Product Product)
     {
       _context.Add(Product);
       _context.SaveChanges();
     }
     public IEnumerable<Product> GetAll() =>
     _context.Products.Include(c => c.Category).ToList();
     ...
   }
 }

在前面的代码中,我们使用构造函数注入来初始化_context字段。此外,Add方法将新产品插入我们的数据库。类似地,GetAll方法返回我们数据库中所有可用产品的集合。

在这一节中,我们回顾了想象中的应用的代码,并讨论了使用存储库从应用获取、添加和执行其他操作到数据库的流程。

在我们的应用中,模型代表我们的数据库表,视图模型是为视图提供输出的模型。在下一节中,我们将讨论这个视图模型。

引入视图模型

models文件夹中添加一个新的类并命名为ProductViewModel。我们这样做是因为,在我们的单片应用中,每当我们搜索一个产品时,它都应该显示在其产品类别中。

A ViewModel contains the various properties to hold/represent the data. This data is displayed on our View of the application. ViewModel doesn't need to have all read/write properties. This data is meant to be shown to the end user on the UI page. This is not a domain model; in our case, we have ProductViewModel as our ViewModel and Product as our domain model.

为了支持这一点,我们需要将必要的字段合并到视图模型中。我们的ProductViewModel课会是这样的:

 using System;
 namespace FlixOne.BookStore.ProductService.Models
 {
   public class ProductViewModel
   {
     public Guid ProductId { get; set; }
     public string ProductName { get; set; }
     public string ProductDescription { get; set; }
     public string ProductImage { get; set; }
     public decimal ProductPrice { get; set; }
     public Guid CategoryId { get; set; }
     public string CategoryName { get; set; }
     public string CategoryDescription { get; set; }
   }
 }

从前面的代码中,我们看到ProductViewModel由以下内容组成:

  • ProductId包含产品的 GUID。
  • ProductName包含产品名称。
  • ProductImage包含产品形象。
  • ProductPrice包含产品价格。
  • CategoryId代表当前产品类别的 GUID。
  • CategoryName代表类别名称。
  • CategoryDescription给出了类别的完整描述。

我们已经看到了我们的ProductViewModel是如何组成的;这是一个与视图绑定并在屏幕上为最终用户生成结果的工具。

我们的ProductViewModel进入画面并获取值,同时我们的控制器的动作方法正在被使用。在下一节中,我们将讨论ProductController

重温产品控制器

最后,我们准备为ProductService创建一个 RESTful API。更改完成后,ProductController如下图:

 using System.Linq;
 using FlixOne.BookStore.ProductService.Models;
 using FlixOne.BookStore.ProductService.Persistence;
 using Microsoft.AspNetCore.Mvc;
 namespace FlixOne.BookStore.ProductService.Controllers
 {
   [Route("api/[controller]")]
   public class ProductController : Controller
   {
     private readonly IProductRepository _productRepository;
     public ProductController(IProductRepository 
     productRepository) => _productRepository = productRepository;

    [HttpGet]
    [Route("productlist")]
    public IActionResult GetList() => new
    OkObjectResult(_productRepository.GetAll().
    Select(ToProductvm).ToList());

    [HttpGet]
    [Route("product/{productid}")]
    public IActionResult Get(string productId)
 {
 var productModel = _productRepository.GetBy(new Guid(productId));
      return new OkObjectResult(ToProductvm(productModel));
    }

     ...
   }
 }

我们已经完成了创建网络应用编程接口所需的所有任务。现在,我们需要调整一些东西,以便客户端可以获得关于我们的 web APIs 的信息。因此,在接下来的部分中,我们将把 Swagger 支持添加到我们的 web API 文档中。

添加斯瓦格支持

我们在我们的应用编程接口文档中使用了斯瓦格。我们不会在这里深入讨论斯瓦格的细节,因为这超出了本书的范围。Swagger 是一个工具,建立在 OpenAPI 规范之上,它帮助我们轻松地记录我们的 API。在斯瓦格的帮助下,我们可以轻松地为各种应用编程接口/服务创建文档。这些文档对于将要使用这些 API 的最终用户非常有用。

Swagger is a famous open source library that provides documentation for web APIs. Refer to the official link, https://swagger.io/, for more information.

使用斯瓦格添加文档非常容易。请遵循以下步骤:

  1. 打开“获取包管理器”。
  2. 搜索Swashbuckle.AspNetCore包。
  3. 选择软件包,然后安装软件包:

  1. 它将安装以下内容:
    • Swashbuckle。AspNetCore
    • Swashbuckle。AspNetCore . Swagger
    • Swashbuckle。AspNetCore.SwaggerGen
    • Swashbuckle。AspNetCore.SwaggerUI

这显示在下面的截图中:

  1. 打开Startup.cs文件,转到 ConfigureServices 方法,添加以下几行来注册 Swagger 生成器:
//Register Swagger
services.AddSwaggerGen(swagger =>
{
    swagger.SwaggerDoc("v1", new Info { Title = "Product APIs", Version = "v1" });
});
  1. 接下来,在 Configure 方法中,添加以下代码:
app.UseSwagger();

app.UseSwaggerUI(option =>
{
 option.SwaggerEndpoint("/swagger/v1/swagger.json", "Product API V1");
});
  1. F5 ,然后运行应用;你会得到一个默认页面。
  2. 通过在网址中添加swagger打开斯瓦格文档。所以,网址是http://localhost:44338/swagger/:

前面的截图显示了产品应用编程接口资源,您可以在 Swagger 文档页面中尝试这些应用编程接口。

最后,我们完成了整体的过渡.NET 应用到微服务中,我们讨论了ProductService的逐步过渡。这个应用还有更多步骤:

  • 微服务如何通信:这将在第三章服务之间的有效通信中讨论。

  • 如何测试微服务:这将在第 4 章用微软单元测试框架测试微服务中讨论。

  • 部署微服务:这将在第 5 章使用 Docker 部署微服务中讨论。

  • 我们如何确保我们的微服务是安全的,以及我们如何监控我们的微服务:这将在第 6 章使用 Azure 活动目录第 7 章监控微服务中讨论。

  • 如何扩展微服务:这将在第 8 章用 Azure 扩展微服务中讨论。

在斯瓦格的帮助下,我们展示了我们产品服务的全部应用编程接口,并讨论了应用编程接口文档。

这整个部分为我们提供了一个完整的工作产品应用编程接口,从它的基础到文档。有了这些,我们重新审视了我们想象中的应用的整体。我们已经看到了我们的应用如何与数据库交互,以及我们的用户界面如何提供各种值。在这一部分,我们还创建了我们的产品服务。同样,我们现在可以创建其他所需的服务。为了使我们的代码更简单,我们以产品服务为例,并在本书中对此进行了详细阐述。

摘要

在本章中,我们从高层次讨论了可用于识别和隔离微服务的不同因素。我们还讨论了好服务的各种特征。谈到 DDD,我们了解到它在微服务领域的重要性。

此外,我们详细分析了如何通过各种参数正确实现微服务的垂直隔离。我们借鉴了之前对单片应用及其解决方案在微服务中带来的挑战的理解,并了解到我们可以利用模块相互依赖性、技术利用率和团队结构等因素来识别接缝,并以有组织的方式执行从单片架构到微服务的过渡。

很明显,在这个过程中,数据库会带来明显的挑战。然而,我们通过使用一个简单的策略,确定了我们仍然可以如何执行这个过程,并且我们讨论了可能的方法来做到这一点。然后我们确定,随着外键的减少/移除,事务可以以完全不同的方式处理。

从单块到有限上下文,我们进一步应用我们的知识将 FlixOne 应用转变为微服务架构。

在下一章中,我们将通过涵盖集成模式和 Azure 服务结构来讨论服务之间的通信。

问题

  1. 重构一个整体应用时,我们应该考虑哪些因素?
  2. C# 8.0 的默认接口方法有哪些?
  3. 我们为什么要用斯瓦格?

进一步阅读

三、服务之间的有效通信

在前一章中,我们使用. NET 单片应用开发了微服务。这些服务相互独立,位于不同的服务器上。有什么更好的服务间通信方式,其中一个服务与另一个服务进行交互和通信?在微服务中,每个服务可能相互独立,也可能不相互独立。例如,结帐服务可能需要产品服务,但产品服务可能不需要结帐服务。在这种情况下,服务之间的通信非常重要。我们将详细讨论某些支持服务间通信的模式。

在本章中,我们将讨论有助于我们促进这种交流的各种模式和方法。我们还将介绍使用 Azure 服务结构和 Kubernetes 的集成模式。

在本章中,我们将涵盖以下主题:

  • 了解服务之间的通信
  • 了解集成模式
  • 了解蔚蓝服务结构
  • 具有立方体的微服务
  • 构建微服务应用时考虑物联网

技术要求

本章包含各种代码示例来解释一些概念。代码将很简单,只是为了演示。

要运行和执行代码,您需要以下先决条件:

  • Visual Studio 2019
  • 。网络核心设置和运行

要运行这些代码示例,您需要安装 Visual Studio 2019 或更高版本(我们首选的 IDE)。为此,请遵循以下说明:

  1. https://docs . Microsoft . com/en-us/Visual Studio/install/install-Visual Studio下载 Visual Studio 2019(社区免费)。
  2. 按照操作系统的安装说明进行操作。Visual Studio 有多个版本。我们使用的是视窗操作系统。

如果你没有.NET Core 3.1 安装好了,可以去https://www.microsoft.com/net/download/windows下载设置。

The complete source code is available here: https://github.com/PacktPublishing/Hands-On-Microservices-with-C-8-and-.NET-Core-3.0.

让我们开始第一部分,它是关于理解服务之间的通信。

了解服务之间的通信

在. NET 单片应用的情况下,如果需要访问第三方组件或外部服务,我们使用 HTTP 客户端或另一个客户端框架来访问资源。在第二章重构整块中,我们开发了产品服务,使其能够独立工作。但是这对我们的应用没有好处;我们需要一些服务来相互交互。

这是一个挑战—让服务相互通信。产品服务和订单服务都托管在不同的服务器上。这两个服务器彼此独立,都基于 REST,并且都有自己的端点,它们可以从这些端点相互通信。(当一个服务与另一个服务交互时,反之亦然,我们称之为服务间通信)。

服务之间有几种通信方式;让我们简单讨论一下:

  • 同步:在这种情况下,客户端向远程服务(称为服务)请求特定的功能,然后等待直到得到响应:

在上图(图示视图,不完整)中,您可以看到我们不同的微服务相互通信。我们所有的服务都是 RESTful 的。它们基于 ASP.NET Core 网络应用编程接口。在下一节中,我们将讨论如何调用服务。这就是所谓的同步方法,客户端必须等待服务的响应。在这种情况下,客户端必须等到得到完整的响应。

  • 异步:在这种情况下,客户端向远程服务(称为服务)请求特定的功能。客户不会等待,但会关心响应。异步完成分配给他们的任务,这也适用于我们的日常生活。例如,如果我们正在做早餐,那么我们会遵循某些任务;也就是说,我们可以准备我们的茶,煮一个鸡蛋,等等。现在,让我们以想象中的应用为例。在这里,我们试图添加一个产品。用户指示系统这样做,并将数据值传递给控制器。然后,控制器调用存储库,存储库使用实体框架的上下文保存数据。我们将在接下来的章节中详细讨论这一点。

合作方式

在前一节中,我们讨论了两种不同的服务交互模式。这些模式是合作的风格。让我们来看看它们:

  • 请求/响应:这种情况下,客户端发送请求,等待服务器的响应。这是同步通信的一种实现。然而,请求/响应不仅仅是同步通信的实现;我们也可以将它用于异步通信。让我们看一个例子来理解这一点。在 第二章**重构整块中,我们开发了ProductService。该服务包括GetProduct方法,是同步的。每当客户端调用此方法时,都必须等待响应:
[HttpGet]
[Route("GetProduct")]
public IActionResult Get() => 
return new
OkObjectResult(_productRepository.GetAll().ToViewModel());

根据前面的代码片段,每当客户端调用这个方法(请求这个方法)时,他们都必须等待响应。换句话说,他们必须等到ToViewModel()扩展方法被执行:

[HttpGet]
[Route("GetProductSync")]
public IActionResult GetIsStillSynchronous()
{
   var task = Task.Run(async() => await
   _productRepository.GetAllAsync());
   return new OkObjectResult(task.Result.ToViewModel());
}

在前面的代码片段中,我们可以看到我们的方法是这样实现的,即每当客户端发出请求时,他们都必须等到async方法被执行。在前面的代码中,我们使用async作为sync。看看task.Result.ToViewModel(),它正在返回前面代码中的响应,我们可以看到.Result是我们的代码sync的来源。

为了缩短我们的代码,我们在已经存在的代码中添加了一些扩展方法,我们在第 2 章重构整体中写道:

namespace FlixOne.BookStore.ProductService.Helpers
{
    public static class Transpose
    {
        public static ProductViewModel ToViewModel(
                               this Product product)
        {
            return new ProductViewModel
            {
                CategoryId = product.CategoryId,
                CategoryDescription = product.Category.Description,
                CategoryName = product.Category.Name,
                ProductDescription = product.Description,
                ProductId = product.Id,
                ProductImage = product.Image,
                ProductName = product.Name,
                ProductPrice = product.Price
            };
        }

        public static IEnumerable<ProductViewModel> ToViewModel(this IEnumerable<Product> products) => products.Select(ToViewModel).ToList();
    }
}

总之,我们可以说请求/响应的协作风格并不意味着它只能同步实现;我们也可以为此使用异步调用。

  • 基于事件的:这种协作风格的实现纯粹是异步的。这是一种实现方法,发出事件的客户端不知道如何反应。在前一节中,我们以同步的方式讨论了ProductService。让我们看一个用户/客户如何下单的例子。以下流程图是购书功能的图示概述:

上图显示了以下内容:

从概念上讲,这看起来很容易;然而,当我们讨论实现微服务时,我们讨论的是单独托管的服务,它们有自己的 REST API、数据库等。这现在听起来更复杂了。涉及许多方面,例如,一个服务如何在一个或多个服务成功响应时调用另一个服务。下图显示了这种事件驱动架构的样子:

在上图中,我们可以看到订单服务执行时发票服务产品服务被触发。这些服务调用其他内部异步方法来完成它们的功能。

We are using Azure API management as our API gateway. In the upcoming sections, we will discuss this in detail.

这一部分是关于服务之间的通信,我们了解了各种通信方式,然后是关于协作的讨论。

在下一节中,我们将讨论如何实现我们的应用所需的各种集成模式。

了解集成模式

到目前为止,我们已经讨论了服务间通信,并通过使用同步和异步通信完成了ProductService的实际实现。我们还实现了微服务,使用不同风格的协作。我们的 FlixOne 书店(按照微服务架构风格开发)需要更多的交互,这意味着它需要更多的模式。在本节中,我们将帮助您理解它们。

The complete application of the FlixOne bookstore can be found in Chapter 11Building a Microservice Application.

应用编程接口网关

协作风格部分,我们讨论了两种可以用来促进微服务之间相互通信的风格。我们的应用分为各种微服务:

  • 产品服务
  • 订单服务
  • 发票服务
  • 客户服务

在我们的 FlixOne 书店(用户界面)中,我们需要展示一些细节:

  • 书名、作者姓名、价格、折扣等等
  • 有效
  • 书评
  • 图书评级
  • 出版商排名和其他出版商信息

在我们检查实现之前,让我们讨论一下 API 网关。

应用编程接口网关是所有客户端的单一入口点。它充当客户端应用和服务之间的代理。在我们的例子中,我们使用 Azure API 管理(APIM) 作为我们的 API 网关。

Please refer to the Appendix for more details and implementation of an API gateway and the related BFF pattern.

应用编程接口网关负责以下功能:

  • 接受应用编程接口调用并将它们路由到我们的后端
  • 验证应用编程接口密钥、JWT 令牌和证书
  • 通过 Azure AD 和 OAuth 2.0 访问令牌支持身份验证
  • 强制实施使用配额和费率限制
  • 在不修改代码的情况下,动态转换我们的应用编程接口
  • 缓存后端响应,无论它们设置在哪里
  • 出于分析目的记录呼叫元数据

Refer to Azure API Management (https://docs.microsoft.com/en-us/azure/api-management/) to find out more about the process of setting up the API Azure portal and working with REST APIs.

下图显示了作为应用编程接口网关的 Azure 应用编程接口管理:

在前面的流程图中,我们有不同的客户端,如移动应用、桌面应用和网络应用,它们使用微服务。

我们的客户不知道在哪个服务器上可以找到我们的服务。应用编程接口网关提供自己服务器的地址,并使用有效的Ocp-Apim-Subscription-Key在内部验证来自客户端的请求。

Additional steps must be taken to force traffic through APIM. With the respective URL, it is possible to bypass this by going directly to the backend, if it is exposed externally.

我们的ProductService有一个 REST API。它包含以下资源:

| API 资源 | 描述 |
| GET /api/product | 获取产品列表 |
| GET /api/product/{id} | 获取产品 |
| PUT /api/product/{id} | 更新现有产品 |
| DELETE /api/product/{id} | 删除现有产品 |
| POST /api/product | 添加新产品 |

我们已经创建了ProductClient,这是一个. NET 控制台应用。它通过提供订阅密钥向 Azure 应用编程接口管理发出请求。下面是这个的代码片段:

namespace FlixOne.BookStore.ProductClient
{
   class Program
   {
      private const string ApiKey = "myAPI Key";
      private const string BaseUrl = "http://localhost:3097/api";
      static void Main(string[] args)
      {
         GetProductList("/product/GetProductAsync");
         //Console.WriteLine("Hit ENTER to exit...");
         Console.ReadLine();
      }
      private static async void GetProductList(string resource)
      {
         using (var client = new HttpClient())
         {
            var queryString =
            HttpUtility.ParseQueryString(string.Empty);

            client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-
            Key", ApiKey);

            var uri = $"{BaseUrl}{resource}?{queryString}";

            //Get asynchronous response for further usage
            var response = await client.GetAsync(uri);
            Console.WriteLine(response);
          }
       }
    }
 }

在前面的代码中,我们的客户端请求一个 REST API 来获取所有的产品。下面是代码中出现的术语的简要描述:

| BaseUrl | 这是代理服务器的地址。 |
| Ocp-Apim-Subscription-Key | 这是应用编程接口管理为客户选择的特定产品分配的密钥。 |
| Resource | 这是我们的应用编程接口资源,通过 Azure 应用编程接口管理进行配置。它将不同于我们实际的 REST API 资源。 |
| Response | 这是指对特定请求的响应。在我们的例子中,这是默认的 JSON 格式。 |

由于我们使用 Azure 应用编程接口管理作为应用编程接口网关,我们将获得某些好处:

  • 我们可以从单个平台管理我们的各种 APIs 例如,ProductServiceOrderService等服务可以被很多客户端轻松管理和调用。
  • 因为我们使用的是应用编程接口管理,它不仅为我们提供了代理服务器;它还允许我们为我们的 API 创建和维护文档。
  • 它提供了一个内置的工具,这样我们就可以为配额、输出格式和格式转换定义各种策略,例如从 XML 到 JSON,反之亦然。

因此,在应用编程接口网关的帮助下,我们可以获得一些伟大的功能。

事件驱动模式

微服务架构使用每个服务一个数据库的模式,这意味着它为每个依赖或独立的服务包含一个独立的数据库:

  • 依赖服务:我们的应用需要一些外部服务(第三方服务或组件等)和/或内部服务(这些是我们自己的服务)才能按预期工作或运行。例如,结账服务需要客户服务,还需要外部(第三方)服务来验证客户身份(如印度客户的 Aadhaar 身份证或美国客户的 SSN 身份证)。这里,我们的结账服务是一个依赖服务,因为它需要两个服务(一个内部服务和一个外部服务)才能按预期运行。如果服务依赖的任何或所有服务不能正常工作(服务不能正常工作的原因有很多,包括网络故障、未处理的异常等),依赖服务就不能工作。
  • 独立服务:在我们的应用中,我们有不依赖任何其他服务正常工作的服务。这种服务被称为独立服务,它们可以自我托管。我们的客户服务不需要任何其他服务来正常运行,这意味着它是一个独立的服务,不需要任何其他服务的输入。不过,这并不意味着我们的其他服务不依赖于客户服务;其他服务可能依赖也可能不依赖于此服务。

主要的挑战是维护业务事务,以确保这些服务之间的数据一致性。例如,如下图所示,客户服务要知道结账服务何时以及如何运作,它需要客户服务的功能。一个应用中可能有多个服务(服务可能是自托管的)。在我们的例子中,当结账-服务被触发并且客户服务没有被调用时,那么我们的应用将如何识别客户的详细信息?

ASP.NET WebHooks(https://docs.microsoft.com/en-us/aspnet/webhooks/)也可以用来提供事件通知。

为了克服我们已经讨论过的相关问题和挑战(对于结账-服务客户服务,我们可以使用事件驱动模式(或最终的一致性方法)并使用分布式事务。

Distributed transactions are where data flows on more than one network. These transactions are capable of updating this data on networked computer systems. These also detect and roll back the subsequent operations, if any fail.

下图描述了事件驱动模式在我们的应用中的实际实现,其中产品-服务订阅事件,事件管理器管理所有事件:

在事件驱动模式中,我们以这样一种方式实现服务:每当一个服务更新其数据时,它就发布一个事件,而另一个服务(依赖服务)订阅这个事件。现在,每当一个依赖服务接收到一个事件,它就更新它的数据。这样,如果需要,我们的依赖服务可以获取和更新它们的数据。上图显示了服务如何订阅和发布事件的概述。这里,事件管理器可以是运行在服务上的程序,也可以是帮助您管理订阅者和发布者的所有事件的中介。

它登记发布者的事件,并在特定事件发生/触发时通知订阅者。它还可以帮助您形成队列并等待事件。在我们的实现中,我们将使用 Azure 服务总线队列。

让我们考虑一个例子。在我们的应用中,这是我们的服务发布和接收事件的方式:

  • 客户服务为用户执行一些检查,即登录检查、客户详细信息检查等。进行这些必要的检查后,服务会发布一个名为CustomerVerified的事件。
  • 结账-服务接收该事件,并在执行必要的操作后,发布名为ReadyToCheckout的事件。
  • 订单服务收到此事件并更新数量。
  • 结账一完成,结账服务就会发布一个事件。从外部服务(无论是CheckedoutSuccess还是CheckedoutFailed)收到的任何结果都由结账-服务使用。
  • InventoryService收到这些事件时,它会更新数据,以确保该项被添加或删除。

通过使用事件驱动模式,服务可以自动更新数据库并发布事件。

事件源模式

这种模式帮助我们确保每当状态改变时,服务都会发布一个事件。在这种模式中,我们将业务实体(产品、客户等)视为一系列状态变化事件。事件商店保存事件,这些事件可用于订阅或作为其他服务。这种模式简化了我们的任务,因为我们不需要同步数据模型和业务领域。它提高了性能、可扩展性和响应能力。这只是定义了一种方法,通过一系列事件来指示我们如何处理数据上的各种操作。这些事件被记录在商店里。事件表示对数据进行的一组更改,例如InvoiceCreated

下图描述了事件将如何为 ORDERSERVICE 工作:

上图显示了以下实现:

  1. 命令从用户界面发出一本书进行订购。
  2. 订单服务查询(来自事件商店,并用CreateOrder事件填充结果。
  3. 然后,命令处理程序引发一个事件来订购图书。
  4. 我们的服务执行相关操作。
  5. 最后,系统将事件附加到事件存储中。

最终一致性模式

最终一致性是数据一致性方法的实现。这意味着实现,因此该系统将是一个具有高可用性的可扩展系统。

"The distributed system needs to be specified as having eventual consistency as an explicit requirement. Eventual consistency comes from the systems that exhibit scalability and high availability."

根据该分布式数据,商店服从一致性、可用性(网络)、分区容差 ( CAP )定理。CAP 定理也被称为布鲁尔定理。根据这个定理,在分布式系统中,我们只能从以下三个中选择两个:

  • 一致性(丙)
  • 可用性(一)
  • 分区容差

以我们想象的系统为例,它是高可用的(A),高度一致的(C),并且没有分区(CA)。当我们要求并进行分区(P)时,我们的系统有多达 n 个分区,或者我们说我们在持续地对我们的系统进行分区。在这种情况下,这是一个非常复杂的场景,数据很难到达或覆盖所有分区。这就是为什么当我们进行分区时,我们要么使系统高度可用(AP),要么使系统高度一致(CP)。

补偿交易

补偿事务允许我们回滚或撤消我们在一系列步骤中执行的所有任务。假设一个或多个服务已经实现了一系列操作,并且其中一个或多个已经失败。我们的下一步会是什么?我们会颠倒所有的步骤,还是承诺一个半完成的功能?

在我们的案例中,客户订购了一本书,ProductService将订购的书标记为暂时售出。订单确认后,OrderService调用外部服务完成支付流程。如果付款失败,我们将需要撤销我们之前的任务,这意味着我们将不得不检查ProductService,以便它将特定的书标记为未售出。

竞争消费者

竞争消费者允许我们为多个并发消费者处理消息,以便他们在同一个通道上接收这些消息。这个应用是用来处理大量请求的。

下图显示了此的实现:

这个消息流是通过将一个消息传递系统传递给另一个服务(消费者服务)来实现的,这个服务可以异步处理。

这个场景可以通过使用 Azure 服务总线队列来实现。我们将在下一节讨论这个问题。

了解蔚蓝服务结构

Azure 服务结构是一个分布式系统的平台;它帮助我们轻松管理可扩展的微服务。它克服了开发人员和基础设施人员面临的各种挑战。Azure Service Fabric 是一个分布式系统平台,它使我们能够轻松地打包、部署和管理可扩展且可靠的微服务和容器。

服务结构体系结构

服务结构是服务的集合。每个集合被分成不同的子系统。每个子系统都有自己特定的职责,允许我们编写以下类型的应用:

  • 可扩展的应用
  • 可管理的应用
  • 可测试的应用

主要子系统构成了服务结构体系结构,如下图所示:

底层的第一层,即传输子系统,负责在服务结构集群中的节点之间提供安全的通信通道。让我们更详细地看一下所有这些子系统:

  • 传输子系统为集群内和集群间的通信提供了通信通道。用于通信的通道由 X509 证书或 Windows 安全保护。该子系统支持单向和请求-响应通信模式。联盟子系统使用这些信道来广播和多播消息。该子系统位于服务结构内部,开发人员不能直接将其用于应用编程。
  • 联盟子系统负责将虚拟机或物理机逻辑分组在一起,形成服务结构集群。该子系统使用传输子系统提供的通信基础设施来实现这种分组。这有助于服务结构更高效地管理资源。该子系统的主要职责包括故障检测、领导选举和路由。子系统在为集群分配的节点上形成环形拓扑。在系统中实现了令牌租用机制和心跳检查,以检测故障、执行领导者选举并实现一致的路由。
  • 托管在平台上的服务的可靠性子系统通过管理集群中跨节点的故障转移、复制和资源平衡来确保。这个子系统中的复制器逻辑负责跨服务的多个实例复制状态。该子系统的主要任务是维护服务部署中主副本和辅助副本之间的一致性。它与故障转移单元和重新配置代理交互,以便了解需要复制什么。
  • 管理子系统处理已经部署在服务结构集群上的工作负载的应用生命周期管理。应用开发人员可以通过管理 API 或 PowerShell cmdlets 访问管理子系统的功能,以调配、部署、升级或取消调配应用。所有这些操作都可以在不停机的情况下执行。管理子系统有三个关键组件:集群管理器、运行状况管理器和映像存储。群集管理器与故障转移管理器交互。同时,可靠性子系统中的资源管理器部署可用节点的应用,同时考虑放置约束。它负责应用的生命周期,从资源调配到取消资源调配。它还与运行状况管理器集成,在服务升级期间执行运行状况检查。顾名思义,运行状况管理器负责监控应用、服务、节点、分区和副本的运行状况。它还负责汇总健康状态并将其存储在集中的健康存储中。API 暴露在这个系统之外,用于查询健康事件,以便它们可以执行纠正措施。这些应用编程接口可以返回特定集群资源的原始事件或聚合健康数据。映像存储负责持久化和分发部署在服务结构集群上的应用二进制文件。
  • 托管子系统负责管理节点范围内的应用部署。集群管理器向托管子系统发出信号,通知它要在特定节点上管理的应用部署。然后,托管子系统管理该节点上应用的生命周期。它与可靠性子系统和管理子系统交互,以确保每个部署的健康。
  • 通信子系统提供服务发现功能,并提供使用命名服务的集群内消息传递功能。命名服务用于在集群中定位服务。它还允许用户安全地与集群中的任何节点通信、检索服务元数据和管理服务属性。命名服务还公开了 API,允许用户解析每个服务的网络位置,尽管它们是动态放置的。
  • 可测试性子系统为开发人员、部署工程师和测试人员提供了一个工具列表,这样他们就可以引入受控故障并运行测试场景,以验证已经部署在服务结构上的服务的状态转换和行为。当一个集群被提供或者当一个故障动作或测试场景已经启动一个命令时,故障分析服务自动启动

在接下来的章节中,我们将更详细地讨论服务结构,在这里我们将详细介绍编排器和编程模型。

讨论管弦乐队

简而言之,编排器是一个用于管理服务部署的自动化软件。该软件包旨在从顶级用户那里抽象出有关配置、部署、故障处理、扩展和优化其管理的应用的复杂性。例如,关联度编排应该准备好使用指定服务实例数量的配置,以运行和执行基于多个高级因素的服务部署任务。这些因素包括群集期间节点上的资源可用性、放置限制等。管弦乐队可以对故障处理和服务恢复收费。如果集群中的一个节点出现故障,安排者必须优雅地处理这个问题,同时保证服务的便捷性。

服务结构集群资源管理器可以是在我们的集群内运行的中央服务。它管理集群内服务的指定状态,以及相关的资源消耗和放置规则。我们将在下一节讨论这个问题。

服务结构编程模型概述

服务结构提供了我们编写和管理服务的多种方式。服务可以选择使用服务结构 API 来充分利用平台的特性和应用框架。服务也可以是以任何语言编写的任何编译过的可执行程序,或者是运行在服务结构集群上的容器中的代码。让我们现在来看看这些:

  • Guest 可执行文件是一个现有的可执行文件,可以用任何语言编写,可以作为服务在应用中运行。这些客户可执行文件没有直接使用服务结构 API。Visual Studio 允许我们在服务结构集群上部署来宾可执行文件。
  • 容器是完整文件系统的一部分,包含工具、运行时和系统库。
  • 可靠的服务是一个轻量级框架,用于编写与 Service Fabric 平台集成并受益于该平台功能的服务。可靠的服务提供了一组最小的 API,允许服务结构运行时管理服务的生命周期,以便它们可以与运行时交互。应用框架很小,因此您可以完全控制设计和实现选择。它也可以用来托管任何其他应用框架,如 ASP.NET Core。
  • ASP.NET Core是一个开源的跨平台框架,我们可以用它来构建网络应用、物联网应用和移动后端。服务结构与 ASP.NET Core 集成,因此我们可以编写无状态和有状态的 ASP.NET Core 应用,利用可靠集合和服务结构的高级编排功能。
  • 可靠参与者构建在可靠服务之上,是一个基于参与者设计模式实现虚拟参与者模式的应用框架。可靠参与者框架使用独立的计算和状态单元,并带有一个称为参与者的单线程执行。可靠参与者框架为参与者提供内置通信,并预设状态持久性和扩展配置。因为可靠参与者是建立在可靠服务上的应用框架,所以它与服务结构平台完全集成,并且受益于该平台提供的全套功能。

本节的目的是讨论服务结构,并提供其编程模型的概述。在下一节中,我们将学习如何实现关于竞争消费者的信息。

实施关于竞争消费者的信息

竞争消费者部分,我们讨论了竞争消费者是传递信息的一种方式。在本节中,我们将讨论 Azure 服务总线并实现 Azure 消息队列。

蔚蓝服务巴士

在事件驱动模式中,我们讨论了发布和订阅事件。我们使用事件管理器来管理所有事件。在本节中,我们将学习 Azure 服务总线如何管理事件,并提供与微服务一起工作的工具。它像信息传递服务一样工作,并且它使服务之间的通信完美无缺。在我们的例子中,每当服务需要交换信息时,它们将使用该服务进行通信。

Azure 服务总线提供两种主要类型的服务:

  • 代理通信:这项服务也被称为雇佣服务。它的工作原理类似于现实世界中的邮政服务。每当一个人想要发送消息或信息时,他/她可以向另一个人发送一封信。有了邮局,我们可以以信件、包裹、礼物等形式发送各种类型的信息。当我们使用代理通信类型时,我们自己不必费心传递消息,因为它确保我们的消息被传递,即使发送者和接收者都不在线。这是一个消息传递平台,包含队列、主题、订阅等组件。
  • 非代理通讯:这类似于打电话。在这种情况下,呼叫者(发送者)呼叫一个人(接收者),而没有任何指示他/她是否将应答该呼叫的确认。在这里,发送方发送信息,并依靠接收方接收通信,并将消息传递回发送方。请看下图,它展示了 Azure 服务总线:

请参见微软 Azure 服务总线的文档。这里有一个描述:

"Service Bus is a cloud service and is shared by multiple users. To get started with it, you need to create a namespace and define the communication mechanism."

上图是 Azure 服务总线的示意图,描述了四种不同的通信机制。每个人在连接应用时都有自己的喜好:

  • 队列:这些队列就像经纪人一样,允许单向通信。
  • 主题:与队列类似,主题提供单向通信,但单个主题可以有多个订阅。
  • 中继:这些不像队列和话题那样存储任何消息。相反,它们提供双向通信并将消息传递给应用。
  • 通知中心:这将消息从服务器应用分发到平台上的客户端设备。

Azure 消息队列

Azure 队列是使用 Azure 表的云存储帐户。它们允许我们在应用之间对消息进行排队。Azure 队列存储有一组不同于 Azure 服务总线的功能。在接下来的部分中,我们将实现消息队列,这是 Azure 服务总线的一部分。

在本节中,我们已经讨论了 Azure 消息队列,通过遵循 Azure 服务总线,它允许我们收集关于竞争消费者的信息。在下一节中,我们将实现关于服务结构的信息。

在服务结构上实现信息

在本节中,我们将通过创建以下内容来查看 Azure 服务总线队列的实际实现:

  • 服务总线命名空间
  • 服务总线消息队列
  • 向其发送消息的控制台应用
  • 接收消息的控制台应用

先决条件

我们需要以下内容来实施此解决方案:

  • Visual Studio 2019 或更高版本
  • 有效的 Azure 订阅

如果您没有 Azure 套餐,您可以通过在https://azure.microsoft.com/en-us/free/注册免费获得一个。

现在您已经拥有了一切,请按照以下步骤开始:

  1. 登录蔚蓝门户(https://portal.azure.com/)。
  2. 在左侧导航栏中,单击服务总线。如果此选项不可用,您可以通过单击“更多服务”找到它。
  3. 单击添加。这将打开“创建命名空间”对话框:

  1. 在“创建命名空间”对话框中,输入一个命名空间(命名空间应该是全局唯一的),如flixone。接下来选择定价层,即BasicStandardPremium
  2. 选择您的订阅。
  3. 选择现有资源或创建新资源。
  4. 选择要承载命名空间的位置。完成后,单击创建。
  5. 打开一个新创建的命名空间(我们刚刚创建了flixone)。
  6. 现在,单击共享访问策略。
  7. 点击 RootManageSharedAccessKey,如下图截图所示:

  1. 点击flixone命名空间主对话框中的【队列】,如下图所示:

  1. 在“策略:RootManageSharedAccessKey”窗口中,记下主键连接字符串,以便以后使用。

  2. 点击名称添加队列(比如flixonequeue)。然后,单击创建(我们使用 REST 值作为默认值),如下图所示:

前面的截图是“创建队列”对话框的截图。在“创建队列”对话框中,我们可以创建队列。例如,在这里,我们正在创建一个名为 flixonequeue 的队列。可以通过访问“队列”对话框来验证队列。

现在,我们准备创建发送方和接收方应用。

向队列发送消息

在本节中,我们将创建一个控制台应用,它将实际向队列发送消息。要创建此应用,请执行以下步骤:

  1. 创建一个新的控制台应用,然后使用 Visual Studio 的新项目(C#)模板将其命名为FlixOne.BookStore.MessageSender:

  1. 右键单击项目,添加 Microsoft Azure 服务总线 NuGet 包。
  2. 使用以下代码将消息发送到队列。您的Program.cs文件将包含以下MainAsync()方法:
private static async Task MainAsync()
{
    const int numberOfMessagesToSend = 10;
    _client = new QueueClient(_connectionString, _queuename);
    WriteLine("Starting...");
    await SendMessagesAsync(numberOfMessagesToSend);
    WriteLine("Ending...");
    WriteLine("Press any key...");
    ReadKey();
    await _client.CloseAsync();
}

在前面的代码中,我们通过提供已经在 Azure 门户中设置的ConnectionStringQueueName来创建队列客户端。这段代码调用SendMessagesAsync()方法,该方法接受一个包含需要发送的消息数的参数。

  1. 创建一个SendMessagesAsync()方法,并添加以下代码:
private static async Task SendMessagesAsync(int numberOfMessagesToSend)
{
    try
    {
        for (var index = 0; index < numberOfMessagesToSend; index++)
        {
            var customMessage = $"#{index}:A message from 
                            FlixOne.BookStore.MessageSender.";
            var message = new Message(Encoding.UTF8.GetBytes(
                                      customMessage));
            WriteLine($"Sending message: {customMessage}");
            await _client.SendAsync(message);
        }
    }
    catch (Exception exception)
    {
        WriteLine($"Weird! It's exception with message:
                   {exception.Message}");
    }
}
  1. 运行程序并等待一段时间。您将获得以下输出:

  1. 转到 Azure 门户,然后转到创建的队列,检查它是否显示消息。在下面的截图中,我们可以看到 flixonequeue 的概述,其中我们可以看到活动消息计数和更多信息:

前面的截图来自 Azure 门户,是 flixonequeue(服务总线)的概览屏幕。目前,我们有 10 条消息(活动消息数)。

添加配置设置

在上一节中,我们使用了ConnectionStringQueueName的常数值。如果我们需要更改这些设置,我们必须对代码进行更改。然而,为什么我们要为这么小的变化进行代码更改呢?为了克服这种情况,我们将使用配置设置。

在本节中,我们将借助Microsoft.Extensions.Configuration命名空间中的IConfigurationRoot来添加配置。

  1. 首先,右键单击项目,然后单击管理 NuGet 包。搜索Microsoft.Extensions.Configuration NuGet 包,如下图截图所示:

  1. 现在,搜索Microsoft.Extensions.Configuration.Json NuGet 包并选择它:

  1. 将以下ConfigureBuilder()方法添加到Program.cs文件中:
private static IConfigurationRoot ConfigureBuilder()
{
   return new ConfigurationBuilder()
 .SetBasePath(Directory.GetCurrentDirectory())
 .AddJsonFile("appsettings.json")
 .Build();
}
  1. 现在,将appsettings.json文件添加到项目中,包括以下属性:
{
   "connectionstring":
   "Endpoint=sb://flixone.servicebus.windows.net/;
   SharedAccessKeyName=
   RootManageSharedAccessKey;SharedAccessKey=
   BvQQcB5FhNxidcgEhhpuGmi/
   XEqvGho9GmHH4yjsTg4=",
   "QueueName": "flixonequeue"
}
  1. 将以下代码添加到main()方法中:
var builder = ConfigureBuilder();
_connectionString = builder["connectionstring"];
_queuename = builder["queuename"];

通过添加前面的代码,我们可以从.json文件中获取connectionstringqueuename。现在,如果我们需要更改这些字段中的任何一个,我们不需要对代码文件进行更改。

从队列接收消息

在本节中,我们将创建一个控制台应用,它将从队列中接收消息。要创建此应用,请执行以下步骤:

  1. 创建一个新的控制台应用(在 C#中)并命名为FlixOne.BookStore.MessageReceiver
  2. 添加 Azure 服务总线的 NuGet 包(我们在上一个应用中添加的),包括Microsoft.Extensions.ConfigurationMicrosoft.Extensions.Configuration.Json
  3. 使用以下代码从 Azure 总线服务队列接收消息。这里,你的program.cs文件包含了ProcessMessagesAsync()方法:
 static async Task ProcessMessagesAsync(Message message,
 CancellationToken token)
 {
    WriteLine($"Received message: #
    {message.SystemProperties.SequenceNumber}
    Body:{Encoding.UTF8.GetString(message.Body)}");
    await _client.CompleteAsync
    (message.SystemProperties.LockToken);
 }
  1. 运行应用,然后查看结果:

  1. 控制台窗口将显示消息及其标识。现在,转到 Azure 门户并验证消息。它应该显示零条消息,如下图所示:

前面的例子演示了如何使用 Azure 总线服务为我们的微服务发送/接收消息。

我们讨论了通过覆盖一个小应用来实现服务结构上的信息。通过这样做,我们可以使用容器连接服务。我们将在下一节更详细地讨论这一点。

实现关于容器的信息

容器是整个文件系统的一部分。顾名思义,它包含工具、运行时和系统库。容器与同一主机上的其他容器共享其主机操作系统和内核。围绕集装箱的技术并不新鲜。长期以来,它一直是 Linux 生态系统的一部分。由于最近围绕它的基于微服务的讨论,容器技术再次成为人们关注的焦点。请注意,它可以在谷歌、亚马逊和网飞上运行。

Chapter 5

Deploying Microservices with Docker

服务结构上的容器

在前一节中,我们了解到服务结构将服务部署为流程。但是,我们也可以在容器中部署服务。服务结构支持在 Linux 和 Windows 服务器上部署容器,它还支持 Hyper-V 隔离模式。

在本节中,我们将讨论使用 Docker 部署微服务的先决条件和执行计划。

先决条件

要使用示例应用,您应该在系统上设置以下先决条件:

执行

在本节中,我们将简要列出执行点。我们将在第 5 章用 Docker 部署微服务中编写一个完整的应用。以下是执行点:

  • 创建新应用(我们将创建产品微服务)
  • 创建 Docker 映像
  • 将服务结构添加到前面的项目中
  • 配置服务结构微服务的通信
  • 部署服务结构容器应用

Chapter 5

Deploying Microservices with Docker

在本节中,我们了解了什么是服务结构,并通过查看消息队列、讨论容器以及如何使用 Docker 部署微服务来奠定应用的基础。

下一节将帮助我们理解 Kubernetes 的微服务。

具有立方体的微服务

在前一节中,我们讨论了服务结构和 Docker 容器,并讨论了它们在微服务中的实现步骤。在这里,我们将通过查看 Azure Kubernetes 服务 ( AKS )的概述来与 Kubernetes 讨论微服务。

Azure Kubernetes 服务概述

AKS 基于开源的谷歌 Kubernetes。这是一项在微软 Azure 公共云上提供的服务。因为它是一个托管容器编排服务,所以您可以管理容器(Docker)和基于容器的应用。

https://github.com/kubernetes/kubernetes

https://azure.microsoft.com/mediahandler/files/resourcefiles/phippy-goes-to-the-zoo/Phippy%20Goes%20To%20The%20Zoo_MSFTonline.pdf

AKS 有许多功能,对于已经在其生产环境中发布的应用非常有用。这些功能包括服务命名和发现、负载平衡、应用运行状况检查、水平自动缩放和滚动更新。

重要概念

在开始实施 AKS 之前,我们应该了解一些概念。这些重要的概念将帮助我们充分利用 AKS 的潜力。

Kubernetes 有一个基本单位叫做豆荚。pod 可以包含一个或多个容器,这些容器共享相同的资源,这些资源保证位于主机上。容器部署在 pod 内部,可以通过 localhost 定位。因为这些都是基于集群的,所以每个 pod 在集群中都有自己的 IP 地址。

这些豆荚(豆荚的集合)统称为服务。默认情况下,服务暴露在集群内部,但也可以暴露给集群外部的外部 IP 地址。我们可以使用以下任何可用的特征来公开它:

  • ClusterIP
  • NodePort
  • LoadBalancer
  • ExternalName

Kubernetes 控制器有一种特定类型的控制器,称为复制控制器。在此帮助下,我们可以通过在集群中运行指定数量的 pod 副本来处理复制和扩展。如果底层节点崩溃,这也可以取代 pod。

易于部署和管理微服务

AKS 提供了各种功能,可以帮助我们在负载平衡、应用运行状况检查、水平自动缩放和滚动更新方面简化微服务的部署和管理。

下图显示了 AKS 中包含的微服务:

上图显示了以下过程:

  1. 开发人员使用 Visual Studio 将更改提交给 GitHub。

  2. 变更提交,GitHub 将构建推/触发到 Azure DevOps (以前称为 VSTS )。

  3. 在这一步中,DevOps 打包我们的微服务(也称为微服务容器),并将其发送到 Azure 容器注册表。

  4. 在这一步中,容器(打包的微服务)被部署到 Azure Kubernetes 服务集群中。

  5. 用户在应用和网站的帮助下访问这些微服务。

  6. 借助 Azure 活动目录,我们可以防止未经授权的访问。

  7. 数据库在这里是必不可少的,因为微服务从各种各样的数据库中保存和获取数据,例如,SQL 数据库、Azure CosmosDB 或 MySQL 的 Azure 数据库。

  8. 我们有一个专门针对管理员的独立管理门户。

在接下来的章节中,我们将使用 sidecar 和 ambassador 模式来理解我们的应用所需的部署和日志记录,并且我们将看到这些模式如何有助于容器。

边车图案

这个图案的名字指的是摩托车的边车。你可以想象,在一辆摩托车上安装的边车的帮助下,我们可以携带很多东西,而这些东西是我们不可能直接用摩托车携带的。类似地,sidecar 应用在执行不依赖于主应用的外围任务时非常有帮助,但是有助于监视、审核或记录主应用。

sidecar 应用可以是第三方应用;公用事业服务/组件;审计、记录或监控应用;等等。这种模式还为我们提供了将它作为单独的组件或服务进行部署的选项。如果主应用由于不可避免的情况而受阻,例如异常或网络故障,那么我们的 sidecar 应用将是必需的。

让我们考虑以下图表:

上图清楚地显示了 sidecar 模式的实现,其中我们将日志保存到主应用的 blob 存储(文件系统)中,并且我们的 sidecar 应用被发送到这些日志中。在这里,我们扩展了主应用的一个特性,即在 sidecar 应用的帮助下,简单地将日志保存在文件系统中,在这里我们可以应用逻辑并分析发送的日志。

何时使用边车模式

这种模式有很多优点。主应用和 sidecar 应用都可以用不同的语言编写,所以没有必要考虑不同的语言或运行时来帮助保持 sidecar 的完整性。主应用可用的资源可以由 sidecar 访问。当 sidecar 应用与主应用通信时,延迟非常小,例如,如果我们将单个 pod 用于主应用和 sidecar 应用。

侧车模式在以下情况下很有用:

  • 当组件或 sidecar 应用及其逻辑/功能可以被使用不同框架用不同语言编写的应用使用时
  • 当组件或功能必须与应用位于同一主机上时

在以下情况下,最好避免这种模式:

  • 当您想要优化进程间通信时。
  • 部署多个应用会产生成本;如果预算有限,不要尝试这种模式。
  • 如果主服务独立于主应用并需要优化,那么这不是您需要的模式。

应遵循的最佳实践

通常,这种模式用于容器,并被称为侧车容器。在应用中实现这种模式时,我们应该遵循规定的最佳实践。以下实践只是基本的最佳实践,可能还有更多特定于应用的实践:

  • 容器应该非常适合边车模式。
  • 在设计 sidecar 应用/组件时,我们应该牢记进程间通信。
  • 当我们决定制作一个 sidecar 应用时,我们应该确保我们已经决定我们需要的 sidecar 功能不会由一个单独的/独立的服务或一个经典的守护程序来实现。

在本节中,我们讨论了使用 sidecar 模式实现日志记录。同样,我们有大使模式,我们将在下一节讨论。

大使模式

大使模式几乎与我们刚刚讨论的边车模式相同。唯一不同的是,每个请求都是通过容器来的,容器也被称为大使(有时也称为代理容器)。换句话说,我们可以说,如果没有大使模式,主应用就无法联系外部世界(用户界面、客户端和/或远程服务)。

让我们考虑以下图表:

上图清楚地显示了大使模式的实现,其中主应用和大使容器部署在同一个主机上。现在,只要主应用与远程服务交互(这些服务可能是外部 API、用户界面等等),它就通过大使或代理容器进行交互。在这里,大使充当代理,将所有网络请求路由到主应用。

何时使用大使模式

大使模式在以下情况下很有用:

  • 当我们拥有用多种语言或框架开发的多种服务/功能,并且我们需要构建公共客户端连接时
  • 当支持遗留应用或难以修改的应用时
  • 当我们想要标准化和扩展仪器时

在以下情况下最好避免这种模式:

  • 如果有一个高效的网络是优先事项,如果延迟会影响各种操作并且无法处理。
  • 如果客户端的单一语言使用连接功能。更好的选择可能是将它作为一个客户端库,作为一个包分发给开发团队。
  • 当连接特性不能被一般化并且需要与客户端应用更深的集成时。

应遵循的最佳实践

一旦我们开始实施大使模式,就应该遵循一些最佳实践。虽然大使模式和边车模式几乎一样,但是在实现的时候要小心。以下是支持此模式最佳实践的建议要点:

  • 每当我们使用代理或通过任何外部组件路由网络请求时,都会增加延迟。因此,当我们使用大使模式时,我们应该记住延迟开销。
  • 有了这个模式的实现,我们应该考虑是实现断路器还是重试大使,因为这样做可能不安全,除非所有操作都是幂等的。
  • 这种模式最重要的考虑因素是我们如何打包和部署代理。

大使模式帮助我们建立一个连接客户端,将所有网络请求从外部服务路由到主应用。存在延迟的可能性,但是我们在选择这种模式时必须小心,尤其是当我们想要实现的特性没有其他替代方案时。

构建微服务应用时考虑物联网

如今,微服务越来越多地被用作创建企业应用的首选方式。微服务是支持网络、移动和物联网(包括可穿戴设备)等一系列平台和设备的理想选择。在这种情况下,我们的应用 FlixOne 与物联网设备连接。我们可以使用各种组件,比如监视器,它可以用来流式传输数据,可穿戴设备可以用来捕获数据,等等。

物联网是一个具有许多重要用例和巨大潜力的领域。微软 Azure 提供托管云服务,例如连接、监控和控制数十亿物联网资产的物联网 ( 物联网)。为了简化这一点,物联网解决方案有一个或多个运行在云中的物联网设备和一个或多个后端服务,它们相互通信。在构建物联网解决方案时,我们考虑的是大规模企业解决方案。

每个系统都包含一些使系统可靠的基本部分。同样的概念适用于任何物联网解决方案。这些主要部分如下:

  • 设备
  • 后端服务
  • 两者之间的交流

然而,物联网系统的开发带来了巨大的成本,需要在硬件、基础设施和应用方面进行大量投资。

将物联网与微服务结合使用,提供了一种减少基础设施使用和成本的方法。此外,容器化有效地帮助部署微服务,具有最佳的资源利用率,并且它通过各种框架监控整个基础架构,减少了我们的运营开销。因此,在构建物联网解决方案时,需要考虑开发部分,原因如下:

  • 架构:规划和选择你的架构,是构建物联网应用的基础和重要步骤之一。例如,医疗物联网应用的物联网架构将由三个阶段组成:物理阶段(使用传感器等设备)、通信阶段(使用设备收集数据并将其发送给应用)和应用阶段(接收数据并执行各种操作,如分析)。
  • 拓扑和协议:通信协议的选择非常重要,关系到你业务需求的实现。对于设备端通信,Azure IoT Hub 允许的协议包括 MQTT、AMQP、HTTPS 等。星型拓扑是 Azure 中首选的网络拓扑之一。集线器是 Azure 中的虚拟网络,它充当内部设备和 Azure 物联网网络之间的中心连接点。
  • 可扩展性:在开发企业应用的时候,可扩展性始终是重中之重,当我们讨论 IoT 应用的时候,这一点就更加重要了。随着时间的推移,数据和应用的使用量往往会大幅增加,因此我们应该相当重视扩展。Azure 物联网中心提供扩展功能(基本层和标准层),我们可以根据业务需求从中进行选择。
  • 安全性:安全性始终是一个关注点,当我们使用多个设备时,我们应该更加小心我们的安全性实现。确保安全的最佳方法是确保每个连接的设备都有自己的身份验证。
  • 测试部署:设备要测试好再部署。我们应该验证每个设备都与其物联网集线器连接并通信。性能测试(或负载测试)将是分析物联网应用的最佳方式。

在接下来的章节中,我们将讨论物联网中心和物联网边缘。

物联网枢纽概述

物联网中心是托管在云上的服务(受管);它就像一个中介,位于物联网应用和应用管理的设备之间。

当我们使用 Azure 物联网时,我们有能力在云上托管的后端系统的帮助下,在物联网应用和与之相关联的数百万台设备之间进行通信。例如,您可以将遥测数据从您的设备发送到 Azure IoT Hub 通过我们在云中的应用,我们可以根据业务模型执行分析或其他操作。我们不涉及这个例子的实现部分,因为它超出了本书的范围。不过完整实现可以参考这个链接:https://docs . Microsoft . com/en-us/azure/IOT-hub/quick start-send-telemetry-dotnet

此外,任何设备都可以与物联网集线器虚拟连接,并且它将支持双向通信:设备到云和云到设备。物联网中心允许我们管理可扩展、功能全面的物联网解决方案,因此我们可以做到以下几点:

  • 扩展解决方案:物联网集线器提供了扩展众多连接设备的设施。与扩展解决方案相关的重要一点是,它提供了不同的扩展选项,因为它认识到每个物联网解决方案都是不同的。如前所述,根据物联网解决方案的要求,您可以选择基本价格层或标准价格层。
  • 保护通信:物联网集线器还可以促进安全的通信通道,因为每个连接的设备都有自己的身份验证。
  • 与其他设备集成:IoT Hub 最棒的地方就是可以连接各种 Azure 服务,像 Azure 事件网格、Azure 逻辑 app、Azure 流分析等等。

物联网边缘概述

Azure 物联网边缘有助于设备的业务逻辑和分析。这样,我们就不需要为数据管理费心了。我们只需要专注于业务逻辑和工作解决方案。它还有助于打包业务逻辑,并帮助我们进入标准容器,以便我们能够横向扩展物联网解决方案。由于业务逻辑是在容器中维护的,我们可以将这些容器部署到任何设备上,然后从云中监控它们。

Azure 物联网边缘由三个组件组成:

  • 物联网边缘模块:Azure 物联网边缘模块是由 Azure 物联网边缘管理的最小计算单元。它可以包含不同的 Azure 服务,例如 Azure 流分析。根据业务需求,它还可以包含定制的代码。
  • 物联网边缘运行时:该模块在物联网边缘设备上启用定制逻辑和云逻辑。它执行管理和通信操作。它还有助于在设备上安装和更新工作负载。
  • 基于云的界面:它提供了从任何地方远程管理和监控物联网设备的设施。

要开发、部署和维护这些模块,需要以下四个要素:

  • 模块映像:这是一个定义一个模块的多个软件的包。
  • 模块实例:这是物联网边缘运行时发起的实例;实例是执行模块映像的特定代码或计算。
  • 模块标识:这是存储在物联网集线器上的安全信息(包括任何其他所需信息)。
  • 模块双胞胎:这包括元数据和配置信息,以 JSON 文件的形式;它存储在物联网集线器上。

本节的目的是向您概述微服务物联网的各种考虑因素。在这里,我们讨论了物联网中心和物联网边缘,我们理解一个是托管服务,而另一个允许我们添加业务逻辑并对物联网设备进行分析。

摘要

服务间的通信可以通过同步或异步通信来实现,这是一种协作方式。微服务应该有异步 API。应用编程接口网关是一个代理服务器,允许各种客户端与应用编程接口交互。作为一个应用编程接口网关,Azure 应用编程接口管理提供了大量的功能,我们可以使用这些功能来管理和托管各种 RESTful 应用编程接口。有多种模式可以帮助我们与微服务进行通信。

通过使用 Azure 总线服务,我们可以使用 Azure 总线服务消息队列轻松管理和处理服务间通信。(服务可以使用它轻松地在它们之间发送或接收消息。)最终的一致性都是关于具有高可伸缩性的可伸缩系统,可以用 CAP 定理来证明。

在下一章中,我们将讨论各种测试策略,这样我们就可以测试我们的应用并构建在微服务架构风格上。

问题

  1. 什么是同步通信和异步通信?
  2. 什么是集成模式?
  3. 什么是事件驱动模式,为什么它对微服务如此重要?
  4. 什么是 CAP 定理?

进一步阅读

四、用微软单元测试框架测试微服务

质量保证或测试是以多种方式评估系统、程序或应用的好方法。有时,系统需要对其进行测试以识别错误代码,而在其他情况下,我们可能需要它来评估我们系统的业务合规性。测试可能因系统而异,也可能有很大的不同,这取决于应用的架构风格。一切都取决于我们如何战略性地对待我们的测试计划。例如,测试一个整体.NET 应用不同于测试 SOA 或微服务。

本章的目的是了解测试策略和我们可以使用的不同类型的测试。我们将学习如何在微软单元测试框架和 Moq(一个开源的、友好的嘲讽框架)的帮助下实现单元测试。

在本章中,我们将涵盖以下主题:

  • 测试微服务应用
  • 了解我们的测试策略
  • 测试金字塔
  • 微服务测试的类型
  • 测试微服务应用

技术要求

本章包含各种代码示例,以解释其中的概念。代码将很简单,只是为了演示。

要运行和执行代码,您需要以下先决条件:

  • Visual Studio 2019
  • 。网络核心设置

安装 Visual Studio 2019:

要运行这些代码示例,您需要安装 Visual Studio 2019 或更高版本(首选 IDE)。为此,请遵循以下说明:

  1. https://docs . Microsoft . com/en-us/Visual Studio/install/install-Visual Studio下载 Visual Studio 2019(社区免费)。
  2. 遵循系统的安装说明。Visual Studio 有多个版本。我们使用的是视窗操作系统。

设置.NET Core 3.1:

如果你没有.NET Core 3.1 安装完毕,可以从https://www.microsoft.com/net/download/windows下载。

The complete source code is available at https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2004.

测试微服务应用

测试微服务可能是一项具有挑战性的工作,因为它不同于我们如何测试使用传统架构风格构建的应用。测试一个. NET 单片应用比测试一个微服务稍微容易一点,微服务提供了实现独立性和短的交付周期。

让我们结合我们的.NET 单片应用,我们没有利用持续集成和部署。当测试与持续集成和部署相结合时,它会变得更加复杂。对于微服务,我们需要了解每个服务的测试,以及这些测试之间的区别。另外,请注意,自动化测试并不意味着我们不会执行任何手动测试。

以下是使微服务测试成为一项复杂而具有挑战性的任务的几件事:

  • 对于一个企业系统,微服务可能有多个一起或单独工作的服务,因此它们可能很复杂。
  • 微服务旨在针对多个客户端(但不总是),因此它们涉及更复杂的用例。
  • 微服务架构风格的每个组件/服务都是独立的,所以测试它们有点复杂,因为它们需要单独测试,也需要作为一个完整的系统进行测试。
  • 可能有独立的团队在独立的组件/服务上工作,这些组件/服务可能需要相互交互。因此,测试不仅应该涵盖内部服务,还应该涵盖外部服务。这使得测试微服务的工作更具挑战性和复杂性。
  • 微服务中的每个组件/服务都被设计为独立工作,但是它们可能必须访问公共/共享数据,其中每个服务负责修改自己的数据库。因此,测试微服务将变得更加复杂,因为服务需要使用对其他服务的 API 调用来访问数据,这进一步增加了对其他服务的依赖。这种类型的测试必须使用模拟测试来处理。

本节帮助我们了解如何测试微服务。在下一节中,我们将讨论测试的各种挑战以及如何应对这些挑战。

利用微服务应对测试挑战

在上一节中,我们讨论了测试微服务是一项复杂且具有挑战性的工作。在本节中,我们将讨论一些要点,这些要点将表明如何进行各种测试来帮助我们克服这些挑战。我们现在来看一些:

  • 一个单元测试框架,比如微软单元测试框架,提供了一个工具,我们可以用它来测试独立组件的独立操作。为了确保所有的测试都通过,并且任何新的功能或更改都不会破坏任何东西(如果任何功能破坏了,那么相关的单元测试就会失败),这些测试可以在每次代码编译时运行。
  • 为了确保响应与客户或消费者的期望一致,可以使用消费者驱动的合同测试。
  • 服务使用来自外部方或其他服务的数据,并且可以通过设置负责处理数据的服务端点来测试它们。通过这样做,我们可以使用一个模拟框架或库,比如Moq,在集成过程中模拟这些端点。

在下一节中,我们将研究一些可以帮助我们克服所有这些挑战的测试策略。

理解测试策略

正如我们在第 1 章微服务简介技术要求一节中提到的,部署和质量保证要求可能非常苛刻。有效处理这种情况的唯一方法是先发制人的计划。在早期的需求收集和设计阶段,我一直倾向于让质量保证团队参与进来。在微服务的情况下,有必要在架构组和质量保证组之间进行紧密的协作。质量保证团队的投入不仅会有帮助,而且他们将能够制定策略来有效地测试微服务。

测试策略仅仅是描述完整测试方法的地图或概要计划。不同的系统需要不同的测试方法。不可能实现一个纯粹的测试方法,对于一个使用较新方法开发的系统,而不是较早开发的系统。测试策略应该对每个人都很清楚,这样创建的测试可以帮助团队中的非技术成员(比如涉众)理解系统是如何工作的。这样的测试可以是自动化的,以简单地测试业务流程,或者它们可以是手动测试,这可以由在用户验收测试系统上工作的用户来执行。

测试策略或方法有以下技术:

  • 主动:这是一个早期的方法,它试图在从初始测试设计创建构建之前修复缺陷。
  • 反应式:在这种方法中,一旦编码完成,测试就开始了。

在本节中,我们研究了可用于测试的策略,并讨论了主动和被动测试方法。在下一节中,我们将看看测试金字塔以及测试方法是如何流动的。

测试金字塔

测试金字塔是一种策略或方法,用来定义应该在微服务中测试什么。换句话说,我们可以说它帮助我们定义了微服务的测试范围。测试金字塔的概念是由 Mike Cohn 在 2009 年创建的。测试金字塔有多种口味;不同的作者描述了这一点,指出了他们是如何放置测试范围或确定测试范围的优先级的。下图描述了迈克·科恩定义的概念:

测试金字塔展示了设计良好的测试策略是如何构建的。当我们仔细观察它时,我们可以很容易地看到我们应该如何遵循微服务的测试方法(请注意,测试金字塔并不特定于微服务)。让我们从金字塔的底部开始。我们可以看到测试范围仅限于使用单元测试。一旦我们移动到顶部,我们的测试范围就扩展到更广的范围,在那里我们可以执行完整的系统测试。

让我们详细讨论这些层(采用自下而上的方法):

  • 单元测试:这些测试基于微服务的架构风格,测试应用的小功能。
  • 服务测试:这些是测试独立服务或与另一个/外部服务通信的服务的测试。
  • 系统测试:这些测试有助于测试整个系统,包括用户界面的一个方面。这些是端到端的测试。

有趣的一点是,顶层测试,也就是系统测试,编写和维护起来既慢又昂贵。另一方面,底层测试,也就是单元测试,速度相对较快,成本也较低。

测试金字塔是关于可以在应用的不同层上完成的各种测试。在本节中,我们按照测试金字塔讨论了单元、服务和系统测试。在下一节中,我们将详细讨论这些测试,并将涵盖各种类型的微服务测试。

微服务测试的类型

在前一节中,我们讨论了测试方法或测试策略。这些策略帮助我们决定如何继续测试系统。测试策略为我们提供了各种类型的测试,可以帮助我们测试整个系统。在本节中,我们将讨论各种类型的微服务测试。

单元测试

单元测试通常测试单个函数调用,以确保测试程序的最小部分。这些测试旨在验证特定功能,而不考虑其他组件。

以下是帮助验证特定功能的各种组件:

  • 当组件被分解成小的、独立的、应该被独立测试的部分时,测试将变得更加复杂。在这里,测试策略派上了用场,它们确保系统的最佳质量保证得以实现。当涉及到测试驱动开发 ( TDD )方法时,我们增加了更多的动力。我们将借助单元测试部分中的一个例子来讨论这一点,该部分是实际测试部分的一个小节。

You can learn and practice TDD, with the help of Katas at https://github.com/garora/TDD-Katas.

  • 单元测试可以是任何规模的;单元测试没有明确的规模。一般来说,这些测试是在类级别编写的。
  • 较小的单元测试有利于测试复杂系统的每一个可能的功能。

组件(服务)测试

组件测试(或服务测试)是一种绕过用户界面直接测试应用编程接口(在我们的例子中,是 ASP.NET Core 网络应用编程接口)的方法。使用这个测试,我们确认一个单独的服务没有任何代码错误,并且它在功能方面工作正常。

测试服务并不意味着它是一个独立的服务。此服务可能正在与外部服务交互。在这种情况下,我们不应该调用实际的服务,而是使用模拟和存根方法。这样做的原因在我们的座右铭中:测试代码并确保它没有错误。在我们的例子中,我们将使用moq框架来模拟我们的服务。

谈到组件或服务测试,有几件事值得注意:

  • 因为我们需要验证服务的功能,所以这类测试应该是小而快速的。
  • 借助嘲讽,我们不需要处理实际的数据库;因此,测试执行时间更少或名义上更长。
  • 这些测试的范围比单元测试更广泛。

集成测试

在单元测试中,我们测试单个代码单元。在组件或服务测试中,我们通过依赖外部或第三方组件来测试模拟服务。然而,微服务中的集成测试可能有点挑战性,因为我们测试协同工作的组件。在这里,应该进行服务调用,以便它们与外部服务集成。在这个测试策略中,我们确保系统正常工作,并且服务按预期运行。在我们的例子中,我们有各种各样的微服务,其中一些依赖于外部服务。

例如,库存服务依赖于订单服务,因为一旦客户成功订购了特定的商品,就会从库存中减少特定数量的商品。在这个场景中,当我们测试 StockService 时,我们应该模拟 OrderService。我们的目标应该是测试 StockService,而不是与 OrderService 通信。我们不直接测试任何服务的数据库。

合同测试

合同测试是一种方法,其中每个服务调用都独立地验证响应。如果有任何服务是依赖的,那么依赖关系就会被存根化。这样,服务在不与任何其他服务交互的情况下运行。这是一个集成测试,允许我们检查外部服务的合同。这遵循了一个称为消费者驱动契约的概念(我们将在下一节详细讨论这一点)。

例如,CustomerService允许新客户在 FlixOne 商店注册。我们不会在数据库中存储新客户的数据。我们首先验证客户数据,以检查黑名单或欺诈用户列表,等等。这个过程通过调用由另一个团队或完全由第三方维护的外部服务来实现。如果有人更改了这个外部服务的合同,我们的测试仍然会通过,因为这样的更改不应该影响我们的测试,因为我们取消了这个外部服务的合同。

微服务可以有几个独立的服务,它们可以相互通信,也可以不相互通信。为了测试这些服务,我们应该有某种模式或机制来确保和验证交互。为此,我们可以使用消费者驱动的合同。

消费者驱动的合同

在微服务中,我们有几个独立的服务,或者需要相互通信的服务。除此之外,从用户的角度来看(这里,用户是一个开发人员,他正在使用被引用的应用编程接口),他们知道服务,以及它是否有几个客户端/消费者/用户。这些客户可以有相同或不同的需求。

消费者驱动的契约指的是一种模式,它指定并验证客户端/消费者和 API 所有者(应用)之间的所有交互。在这里,消费者驱动意味着客户/消费者用定义的格式指定它所要求的交互类型。另一方面,应用接口所有者(应用服务)必须同意这些合同,并确保它们不会违反这些合同。下图是消费者驱动合同的示意图,其中合同介于消费者和其提供商之间:

这些是合同:

  • 提供商合同:这只是对 API 所有者(应用)提供的服务的完整描述。斯瓦格的文档可以用于我们的 REST API(网络 API)。
  • 消费者合同:这是对消费者/客户将如何利用提供商合同的描述。
  • 消费者驱动合同:这是对 API 所有者如何满足消费者/客户合同的描述。

如何实现消费者驱动的测试

在微服务的情况下,实现消费者驱动的测试比. NET 单片应用更具挑战性。这是因为,在单片应用中,我们可以直接使用任何单元测试框架,比如 MS tests 或者 NUnit,但是我们不能在微服务架构中直接这样做。在微服务中,我们不仅需要模拟方法调用,还需要模拟通过 HTTP 或 HTTPS 调用的服务本身。

为了实现消费者驱动的测试,我们可以使用各种工具。一个著名的开源工具.NET frameworks 是PactNet(https://github.com/SEEK-Jobs/pact-net,而另一个为。网芯是Pact.Net 芯(https://github.com/garora/pact-net-core)。这些是基于协定(https://docs.pact.io/标准。在本章的最后,我们将研究消费者驱动的契约测试。

契约核心如何帮助我们实现目标

在消费者驱动的测试中,我们的目标是确保我们能够测试依赖于其他/外部服务或与之通信的所有服务和内部组件。

Pact-net-core 的编写方式保证了合同的履行。以下是它如何帮助我们实现目标的几点:

  • 执行速度非常快。
  • 它有助于确定故障原因。
  • Pact 不需要单独的环境来管理自动化测试集成。

我们需要遵循两个步骤来与 Pact 合作:

  1. 定义期望:这里,消费者团队要定义合同。在下图中,Pact 帮助记录消费者合同,该合同将在重放时得到验证:

  1. 验证期望:作为下一步的一部分,将合同交给提供商团队,然后实施提供商服务来履行同样的义务。在下图中,我们展示了如何在提供者端重放契约,以实现定义的契约:

在这一节中,我们研究了消费者驱动的合同,这有助于我们在名为 Pact-net 的开源工具的帮助下减轻微服务架构的挑战。在消费者驱动的契约的帮助下,我们可以确保我们可以在没有来自服务的实际输入的情况下工作。为了了解它是如何工作的,我们将研究性能测试。

性能试验

这是非功能测试的一种形式,它的主要座右铭是不验证代码或测试代码的健康状况。这意味着根据各种度量,即可伸缩性、可靠性等,确保系统运行良好。

以下是不同的技术或性能测试类型,它们帮助我们在各个方面测试系统,以便我们可以测试其性能:

  • 负载测试:这是我们在特定负载的各种情况下测试系统行为的过程。这还包括关键事务、数据库负载、应用服务器等。
  • 压力测试:这是一种对系统进行回归测试,找出系统容量上限的方法。它还决定了系统在当前负载高于预期最大负载的情况下的行为。
  • 浸泡测试:这也叫耐力测试。在此测试中,主要目的是监视内存利用率、内存泄漏或影响系统性能的各种因素。
  • 尖峰测试:这是一种确保系统能够承受工作负载的方法。确定性能的最佳任务之一是突然增加用户负载。

端到端(用户界面/功能)测试

端到端、用户界面或功能测试是为整个系统执行的测试,包括整个服务和数据库。这些测试增加了测试的范围。这是最高级别的测试,它包括前端集成,并以终端用户使用系统的方式测试系统。这个测试类似于最终用户在系统上的工作方式。

社交单元测试和孤立单元测试

社交单元测试是那些包含具体合作者和跨边界的测试。它们不是孤立的测试。单独测试确保一个类的方法被测试。社交测试并不新鲜。马丁·福勒将这个概念详细解释为一个单元测试(https://martinfowler.com/bliki/UnitTest.html)。让我们更详细地看看这两个:

  • 社交测试:这是一个让我们知道应用正在按预期工作的测试。这是一个环境,在这个环境中,其他应用可以正常运行,并产生预期的结果。它还针对相同的环境测试新功能/方法的功能,包括其他软件。社交测试类似于系统测试,因为这些测试的行为类似于系统测试。
  • 孤立的单元测试:顾名思义,你可以使用这些测试以孤立的方式执行单元测试,通过执行存根和嘲讽。我们可以使用存根用一个具体的类来执行单元测试。

单元测试是确保应用代码得到良好测试的一种方式;通过编写代码来测试代码。单元测试是由开发人员编写的,因此这些测试可以确保代码正常工作。通过调整这些测试,我们可以确保我们的应用在特定的环境中运行良好。

本节概述了在某些环境中,我们可以用来验证类方法和应用功能的不同测试。但是,我们需要在测试期间使用数据时了解流的测试功能,因为我们不建议您使用实际数据。正因为如此,我们需要了解如何制作模型。在下一节中,我们将讨论存根和模拟。

存根和模拟

有时,我们需要测试有无数据的流程。在我们需要进行测试的情况下,我们不能使用实际数据,因为不建议在测试期间使用实际数据(一个原因是您不想更改数据)。这同样适用于我们不使用数据的测试;在这种情况下,我们需要验证输出。为了进行这些测试,我们需要一种安排,在这种安排中,我们可以创建一个具有一些标准输出的框架,而不依赖于正在测试的方法的输入。这可以通过存根和模拟来完成,其中存根是表示响应/结果或输出的对象,但是输入中的任何更改都不会影响存根。另一方面,模拟对象是包含被测试方法的预期输出的假对象。

存根是测试过程中对调用的固定响应;嘲笑意在设定期望。请参见以下描述:

  • 存根:存根对象不依赖于接收到的输入,这意味着响应或结果不会因正确或不正确的输入而受阻。最初,我们创建旨在成为对象的存根。
  • 模仿:模仿的对象不是真实的,可能永远都是假的。使用这个,您可以测试可以调用的方法,并告诉我们单元测试是失败了还是通过了。换句话说,我们可以说模拟对象只是我们实际对象的复制品。在下面的代码中,我们使用moq框架来实现一个模拟对象:
 [Fact]
 public void Get_Returns_ActionResults()
 {
    // Arrange
    var mockRepo = new Mock<IProductRepository>();
    mockRepo.Setup(repo => repo.GetAll().
    ToViewModel()).Returns(GetProducts());
    var controller = new ProductController(mockRepo.Object);
    // Act
    var result = controller.Get();
    // Assert
    var viewResult = Assert.IsType<OkObjectResult>(result);
    var model = Assert.IsAssignableFrom<
    IEnumerable<ProductViewModel>>(viewResult.Value);
    Assert.Equal(2, model.Count());
 }

在前面的代码示例中,我们模拟了我们的存储库IProductRepository,并且验证了模拟的结果。

本节的目的是涵盖我们可以用于基于微服务的应用的各种测试。在下一节中,我们将通过使用 FlixOne 书店应用中的更多代码示例来更详细地理解这些术语。

测试微服务应用

到目前为止,我们已经讨论了测试策略和各种类型的微服务测试。我们还讨论了如何测试以及测试什么。在本节中,我们将看到一些测试正在进行。我们将按照以下最低要求实施这些测试:

  • Visual Studio 2019
  • 。网络核心 3.1
  • C# 8.0
  • 迅驰和微软测试
  • 混合氧化物框架

为测试项目做准备

首先,我们将测试我们的微服务应用 FlixOne 书店。在一些代码示例的帮助下,我们将学习如何执行单元测试、存根和嘲讽。

We created the FlixOne bookstore application in Chapter 2, Refactoring the Monolith.

在我们开始编写测试之前,我们应该在现有的应用中建立一个测试项目。对此,我们可以遵循几个简单的步骤:

  1. 从解决方案资源管理器的“使用 Visual Studio”中,右键单击解决方案,然后单击添加|新建项目,如下图所示:

  1. 从添加新项目模板中,搜索.NET Core 并选择 xUnit 测试项目(.NET Core):

  1. 提供一个有意义的名称,例如FlixOne.BookStore.ProductService.UnitTests:

  1. 我们项目的默认语言版本是 C# 8.0(参考第二章重构整块,了解更多关于选择语言版本的信息)。

我们的项目结构应该如下所示:

前面的截图展示了我们测试项目的结构,也就是 flixone . Booker . productservice . unittests,它有两个文件夹:Fake 和 Services。ProductData.cs 文件包含假数据,属于“假”文件夹。另一方面,ProductTest.cs 文件属于服务文件夹,包含单元测试。

单元测试

让我们测试一下ProductService,确保我们的服务返回产品数据没有失败。在这里,我们将使用假物体。请遵循以下步骤:

  1. 从测试项目中删除默认的UniTest.cs文件。
  2. 添加一个新文件夹,然后在FlixOne.BookStore.ProductService.UnitTests项目中将其命名为Fake
  3. Fake文件夹下,添加ProductData.cs类,然后添加如下代码:
 public IEnumerable<ProductViewModel> GetProducts()
  {
    var productVm = new List<ProductViewModel>
    {
      new ProductViewModel
      {
        CategoryId = Guid.NewGuid(),
        CategoryDescription = "Category Description",
        CategoryName = "Category Name",
        ProductDescription = "Product Description",
        ProductId = Guid.NewGuid(),
        ProductImage = "Image full path",
        ProductName = "Product Name",
        ProductPrice = 112M
      },
      new ProductViewModel
      {
        CategoryId = Guid.NewGuid(),
        CategoryDescription = "Category Description-01",
        CategoryName = "Category Name-01",
        ProductDescription = "Product Description-01",
        ProductId = Guid.NewGuid(),
        ProductImage = "Image full path",
        ProductName = "Product Name-01",
        ProductPrice = 12M
      }
    };
    return productVm;
  }

前面的代码片段正在创建ProductViewModel类型的伪数据。

在下面的代码块中,我们通过制作ProductViewModelProduct两个列表来创建假数据:

  public IEnumerable<Product> GetProductList()
  {
    return new List<Product>
    {
      new Product
      {
        Category = new Category(),
        CategoryId = Guid.NewGuid(),
        Description = "Product Description-01",
        Id = Guid.NewGuid(),
        Image = "image full path",
        Name = "Product Name-01",
        Price = 12M
      },
      new Product
      {
        Category = new Category(),
        CategoryId = Guid.NewGuid(),
        Description = "Product Description-02",
        Id = Guid.NewGuid(),
        Image = "image full path",
        Name = "Product Name-02",
        Price = 125M
      }
    };
  }

前面的代码显示了两个列表。这些列表包含假数据来测试ProductViewModelProduct的输出。

  1. Services文件夹添加到FlixOne.BookStore.ProductService.UnitTests项目中。
  2. Services文件夹下,添加ProductTests.cs类。
  3. 打开 NuGet 管理器,然后搜索添加moq,如下图截图所示:

  1. 将以下代码添加到ProductTests.cs类中:
public class ProductTests
{
    [Fact]
    public void Get_Returns_ActionResults()
    {
      // Arrange
      var mockRepo = new Mock<IProductRepository>();
      mockRepo.Setup(repo => repo.GetAll()).
      Returns(new ProductData().GetProductList());
      var controller = new ProductController(mockRepo.Object);
      // Act
      var result = controller.GetList();
      // Assert
      var viewResult = Assert.IsType<OkObjectResult>(result);
      var model = Assert.IsAssignableFrom<IEnumerable<
      ProductViewModel>>(viewResult.Value);
      Assert.NotNull(model);
      Assert.Equal(2, model.Count());
    }
}

在前面的代码中,这是一个单元测试示例,我们正在模仿我们的存储库,并测试我们的网络应用编程接口控制器的输出。本测试基于 AAA 技术;如果您在设置过程中遇到模拟数据,它将被通过。

在本节中,我们通过对ProductService执行单元测试来介绍单元测试,并通过使用假对象来验证其方法输出。现在,我们需要学习如何测试集成一个或两个模块/块的方法或代码块。为此,我们需要了解集成测试。

集成测试

ProductService中,让我们确保我们的服务返回产品数据没有失败。在我们继续之前,我们必须添加一个新项目和后续的测试类。请按照以下步骤操作:

  1. 右键单击解决方案,然后单击添加项目。

  2. 从“添加新项目”窗口中,选择“测试项目”(.NET Core),如下图截图所示:

  1. 提供一个有意义的名字;例如FlixOne.BookStore.ProductService.IntegrationTests:

  1. 添加appsettings.json文件,然后添加以下代码:
 {
   "ConnectionStrings": 
   {
     "ProductConnection": "Data Source=.;Initial
     Catalog=ProductsDB;Integrated
     Security=True;MultipleActiveResultSets=True"
   },
   "buildOptions": 
   {
     "copyToOutput": 
     {
       "include": [ "appsettings.json" ]
     }
   }
 } 
  1. 打开FlixOne.BookStore.ProductService项目的Startup.cs文件。

  2. 现在,使ConfigureServicesConfigure方法无效。我们这样做是为了在我们的TestStartup.cs类中覆盖这些方法。这些方法如下所示:

public virtual void ConfigureServices(IServiceCollection services)
{
   services.AddTransient<IProductRepository,
   ProductRepository>();
   services.AddDbContext<ProductContext>(
   o => o.UseSqlServer(Configuration.
   GetConnectionString("ProductConnection")));
   services.AddMvc();
   //Code ommited
}
public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
 if (env.IsDevelopment())
 {
 app.UseDeveloperExceptionPage();
 app.UseBrowserLink();
 }
 else
 {
 app.UseExceptionHandler("/Home/Error");
 }
 app.UseStaticFiles();
 app.UseMvc(routes =>
 {
 routes.MapRoute(name: "default",
 template: "{controller=Home}/{action=
 Index}/{id?}");
 });
 app.UseSwaggerUI(op =>
 {
 op.SwaggerEndpoint("/swagger/v1/swagger.json", 
 "Product API V1");
 });
}
  1. 添加一个名为Services的新文件夹。
  2. 增加TestStartup.cs类。
  3. 打开 NuGet 管理器。搜索并添加微软。AspNetCore.TestHost 包,如下图截图所示:

  1. TestStartup.cs中添加以下代码:
public class TestStartup : Startup
{
   public TestStartup(IConfiguration 
   configuration) : base(configuration)
   { }
   public override void ConfigureServices
   (IServiceCollection services)
   {
   //mock context
   services.AddDbContext<ProductContext>(
   o => o.UseSqlServer(Configuration.
   GetConnectionString("ProductConnection")));
   services.AddMvc();
   }
 public override void Configure(IApplicationBuilder
 app, IHostingEnvironment env)
   { }
}
  1. Services文件夹下,添加一个新的ProductTest.cs类,然后在其中添加以下代码:
public class ProductTest
{
  public ProductTest()
  {
    // Arrange
    var webHostBuilder = new WebHostBuilder()
    .UseStartup<TestStartup>();
    var server = new TestServer(webHostBuilder);
    _client = server.CreateClient();
  }
  private readonly HttpClient _client;
  [Fact]
  public async Task ReturnProductList()
  {
    // Act
    var response = await _client.GetAsync
    ("api/product/productlist"); //change per //setting
    response.EnsureSuccessStatusCode();
    var responseString = await response.Content.
    ReadAsStringAsync();
    // Assert
    Assert.NotEmpty(responseString);
  }
}
  1. 要验证这些测试,请右键单击解决方案,然后选择“运行测试”(如果您面临 NuGet 包错误,请确保恢复所有包)。

在前面的代码示例中,我们正在检查一个简单的测试。我们试图通过使用HttpClient设置一个客户端来验证服务的响应。如果响应为空,测试将失败。

摘要

测试微服务与测试以传统架构风格构建的应用有点不同。在. NET 单片应用中,与微服务相比,测试稍微容易一些,并且它提供了实现独立性和短的交付周期。微服务在测试时面临挑战。

借助测试金字塔概念,我们可以制定测试程序的策略。就测试金字塔而言,我们可以很容易地看到,单元测试允许我们测试一个类的一个小函数,并且耗时更少。另一方面,测试金字塔的顶层范围很大,有系统测试或端到端测试,这些测试既耗时又非常昂贵。消费者驱动的契约是测试微服务的非常有用的方法。Pact-net 是一个用于此目的的开源工具。最后,我们进行了实际的测试实现。

在下一章中,我们将学习如何使用 Docker 部署微服务应用。我们将详细讨论延续集成和延续部署。

问题

  1. 什么是单元测试?
  2. 开发人员为什么要坚持测试驱动开发?
  3. 什么是存根和模拟对象?
  4. 什么是测试金字塔?
  5. 什么是消费者测试?
  6. 我们如何在基于微服务的应用中使用消费者测试?

进一步阅读

以下是一些参考资料,将增强您的测试知识:

五、使用 Docker 部署微服务

整体和微服务架构风格都有不同的部署挑战。就……而言.NET 单片应用,部署经常使用 Xcopy。Xcopy 部署是将所需的文件和文件夹部署(粘贴)到服务器中。单词 Xcopy 来源于微软磁盘操作系统(MS-DOS)的 Xcopy 命令。微服务部署带来了一系列不同的挑战。在交付微服务应用时,持续集成和持续部署是关键实践。此外,承诺更大隔离边界的容器和工具链技术对于微服务部署和扩展至关重要。

在本章中,我们将讨论微服务部署的基本原理,以及新兴实践(如 CI/CD 工具和容器)对微服务部署的影响。我们还将通过简单的部署。Docker 容器中的核心服务。

本章结束时,您将了解以下主题:

  • 单一应用部署挑战
  • 理解部署术语
  • 微服务部署成功的先决条件
  • 微服务部署的隔离要求
  • 对新部署模式的需求
  • 出路——集装箱
  • 介绍 Docker
  • 使用 Docker 的微服务部署示例

技术要求

本章包含各种代码示例,以便更详细地解释手头的概念。代码很简单,只是为了演示。

要运行和执行此代码,您需要以下先决条件:

  • Visual Studio 2019
  • 。网络核心设置和运行

要安装和运行这些代码示例,您需要安装 Visual Studio 2019(我们首选的 IDE)。为此,请遵循以下步骤:

  1. https://docs . Microsoft . com/en-us/Visual Studio/install/install-Visual Studio下载 Visual Studio 2019(社区免费)。
  2. 遵循适用于您的操作系统的安装说明。Visual Studio 安装有多个版本。我们使用的是视窗操作系统。

设置.NET Core 3.1:

如果你没有.NET Core 3.1 安装完毕,可以从https://www.microsoft.com/net/download/windows下载。

The source code is available at https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2005.

部署是应用/软件开发生命周期中最重要和最关键的步骤之一。部署基于单片和微服务的应用都有其挑战。在本章中,我们将讨论与应用部署相关的挑战。

单一应用部署挑战

单片应用是指所有的数据库和业务逻辑都绑定在一起并打包成一个系统的应用。一般来说,由于单个应用是作为一个包来部署的,因此部署有些简单,但很痛苦,原因如下:

  • 部署和发布作为一个单一的概念:部署构建工件和实际制作最终用户可用的特性之间没有区别。更常见的情况是,版本与其环境相结合。这增加了部署新功能的风险。
  • 全有或全无部署 : 全有或全无部署会增加应用宕机和故障的风险。在回滚的情况下,团队无法交付预期的新特性和修补程序,或者必须发布服务包来交付正确的功能。

A Hotfix, also known as a Quickfix, is a single or cumulative package (generally called a patch). It contains fixes for issues/bugs that are found in production and that must be fixed before the next major release.

  • 中央数据库作为单点故障 : 在单体应用中,大型集中式数据库是单点故障。这个数据库通常很大,很难分解。这导致平均恢复时间 ( MTTR )和平均故障间隔时间 ( 平均故障间隔时间)增加。
  • 部署和发布是大事件 : 由于应用中的小变化,整个应用可能会被部署。这给开发人员和运营团队带来了巨大的时间和精力投入。参与其中的团队需要协作才能成功发布。当许多团队分布在全球各地,并且他们在开发和发布阶段工作时,这就变得更加困难。这些类型的部署/发布需要大量的手动步骤。这对最终用户产生了影响,他们不得不面对应用宕机。如果你熟悉这些类型的部署,那么你也会熟悉所谓的作战室中的马拉松会议,以及会议桥中无休止的缺陷分类会议。
  • 上市时间 : 对系统进行任何更改都变得更加困难。在这样的环境中,执行任何业务变更都需要时间。这使得应对市场力量变得困难——企业也可能失去市场份额。

借助微服务架构,我们可以解决其中一些挑战。这种体系结构为服务部署提供了更大的灵活性和隔离性。事实证明,它提供了更快的周转时间和急需的业务敏捷性。

当我们使用单片应用时,尤其是在部署方面,我们将面临新的挑战。本节详细描述了我们可能面临的各种挑战。在下一节中,我们将了解微服务使用的部署术语。

理解部署术语

微服务部署术语是指我们在应用发布之前进行代码更改的地方。在本节中,我们将讨论部署术语的所有这些步骤,如下所示:

  • 构建:在构建阶段,服务源编译没有任何错误,并且通过了所有相应的单元测试。这个阶段产生构建工件。

  • 持续集成 ( CI ):每次开发人员提交任何变更时,CI 都会强制重新构建整个应用。编译应用代码,并对其运行一套全面的自动化测试。这种做法源于大型团队中频繁集成代码的问题。基本的想法是保持增量,或者对软件的改变很小。这提供了软件处于工作状态的信心。即使开发人员的签入破坏了系统,也很容易修复它。

  • 部署:部署的前提条件包括硬件配置和安装基本操作系统以及的正确版本.NET 框架。下一步是在生产中,通过不同的阶段来促进这些构建工件。这两部分的结合称为部署阶段。在大多数单一应用中,部署阶段和发布阶段没有区别。

  • 持续部署 ( CD ):在 CD 中,每个成功的构建都部署到生产中。从技术团队的角度来看,光盘更重要。在光盘下,还有一些其他的实践,例如自动化单元测试、标记、版本号的版本化以及变更的可追溯性。通过持续交付,技术团队确保通过各种较低的环境推向生产的变更在生产中按预期工作。通常,这些都很小,部署非常快。

  • 连续发货:连续发货不同于 CD。CD 通常由技术团队使用,而持续交付更侧重于尽早向客户提供部署的代码。为了确保客户获得正确的产品,在连续交付中,每一个构建都必须通过所有的质量保证检查。一旦产品通过这些检查,就由业务涉众来选择何时发布它。

  • 构建和部署管道:构建和部署管道是通过自动化实现持续交付的一部分。这是一个由步骤组成的工作流,通过它代码被提交给源存储库。在部署管道的另一端,产生了用于发布的工件。可能组成构建和部署管道的一些步骤如下:

    • 单元测试
    • 集成测试
    • 代码覆盖和静态分析
    • 回归测试
    • 部署到临时环境
    • 负载/压力测试
    • 部署到发布存储库
  • 发布:面向最终用户的业务特性被称为特性的发布。为了发布一个特性或服务,相关的构建工件应该提前部署。通常,功能切换管理功能的发布。如果在生产中未打开功能标志(也称为功能切换),则称为指定功能的暗发布。

在本节中,我们讨论了部署术语,并了解了部署的各个阶段,例如 CI/CD。为了进行完美的部署,我们需要一些基本的东西,所有这些都将在下一节中讨论。

微服务部署成功的先决条件

任何建筑风格都有一套相关的模式和实践可以遵循。微服务架构风格也没什么不同。通过采用以下实践,微服务实现更有可能成功:

  • 自给自足的团队:亚马逊是 SOA 和微服务架构的先驱,遵循两个披萨法则。这意味着一个微服务团队的成员不会超过 7-10 人(两个披萨可以养活的人数)。这些团队成员将具备所有必要的技能和角色;例如,开发、运营和业务分析师。这样的服务团队处理微服务的开发、运营和管理。
  • CI 和 CD :较小的自给自足的团队,他们可以频繁地整合他们的工作,是微服务成功的前兆。这个建筑不像一块巨石那么简单。然而,自动化和定期推动代码升级的能力允许团队处理复杂性。工具,如 Azure DevOps (原Team Foundation Online Services(TFS)Visual Studio Team Services(VSTS)Team city)和 Jenkins 等,都是这个空间中相当受欢迎的工具链。
  • 作为代码的基础设施:用代码表示硬件和基础设施组件(比如网络)的想法是新的。它帮助我们使部署环境(如集成、测试和生产)看起来完全相同。这意味着开发人员和测试工程师将能够在较低的环境中轻松重现生产缺陷。使用 CFEngine、Chef、Puppet、Ansible 和 PowerShell DSC 等工具,您可以将整个基础架构编写为代码。通过这种范式转换,您还可以将您的基础架构置于版本控制系统之下,并将其作为部署中的工件进行交付。
  • 云计算的利用:云计算是采用微服务的一大催化剂。不过,微服务部署并不是强制性的。云计算具有近乎无限的规模、弹性和快速配置能力。显而易见,云是微服务的天然盟友。因此,掌握 Azure 云的知识和经验将有助于您采用微服务。

为了进行完美无瑕的部署,我们需要一些基本组件。在本节中,我们讨论了部署的所有先决条件。在下一节中,我们将讨论微服务部署的隔离要求。

微服务部署的隔离要求

2012 年,Heroku 云平台的联合创始人 Adam Wiggins 提出了 12 条基本原则。这些原则讨论了从想法到部署定义新的现代 web 应用。这套原则现在被称为 12 因素 app 。这些原则为发展成微服务架构的新架构风格铺平了道路。12 因素应用的原则之一如下:

"Execute the app as one or more stateless processes" 
                                                                                                                                                                                    – Adam Wiggins

在这里,服务基本上是无状态的(除了数据库,它充当状态存储)。无共享原则也适用于所有模式和实践。为了实现规模和敏捷性,我们不需要任何东西,除了组件的隔离。

在微服务领域,这种隔离原则以下列方式应用:

  • 服务团队:围绕服务打造自给自足的团队。实际上,团队将能够做出所有必要的决策来开发和支持他们所负责的微服务。
  • 源码控制隔离:每个微服务的源码库都会是独立的。它不会共享任何源代码、文件等。在微服务领域跨服务复制一些代码是可以的。
  • 构建阶段隔离:每个微服务的构建和部署管道应该保持隔离。构建和部署管道甚至可以在并行、隔离和部署的服务中运行。因此,应该扩展 CI-CD 工具,以更快的速度支持不同的服务和管道。
  • 释放阶段隔离:每个微服务都要和其他服务隔离释放。生产环境中也可能存在具有不同版本的相同服务。
  • 部署阶段隔离:这是隔离最重要的部分。传统的整体部署是通过裸机服务器完成的。随着虚拟化技术的进步,虚拟服务器已经取代了裸机服务器。

一般来说,单块的标准发布过程如下所示:

考虑到这些隔离级别,微服务构建和部署管道可能如下所示:

在这一节中,我们讨论并理解了隔离的原理。对于许多微服务来说,纯粹的 Xcopy 部署是行不通的。在下一节中,我们将讨论对新部署范例的需求。

对新部署模式的需求

应用的最高隔离级别可以通过添加新的物理机或裸机服务器来实现,这样就有了一个拥有自己操作系统的服务器来管理所有系统资源。这在遗留应用中是经常发生的事情,但对于现代应用来说并不实用。现代应用是庞大的系统。这些系统的一些例子包括亚马逊、网飞和耐克,甚至是传统的金融银行,如荷兰国际集团。这些系统托管在数万台服务器上。这些类型的现代应用需要超可扩展性,以便它们能够为其数百万用户服务。对于微服务架构来说,建立一个新的服务器没有任何意义,只是在其上运行一个小服务。

随着新的 CPU 架构突破,出现的选项之一是虚拟机。虚拟机通过虚拟机管理程序技术抽象出操作系统的所有硬件交互。虚拟机管理程序允许我们在一台物理机上运行多台机器或服务器。需要注意的一个重要点是,所有虚拟机都从物理主机资源中获得自己的一部分隔离系统资源。

这仍然是运行应用的良好隔离环境。虚拟化带来了为整个应用提升服务器的基本原理。在这样做的同时,它保持了组件的隔离;这有助于我们利用数据中心的备用计算机资源。它提高了我们数据中心的效率,同时满足了应用的公平隔离需求。

然而,虚拟化本身无法支持微服务的某些需求。在 12 因素原则下,亚当·维金斯也谈到了这一点:

"The twelve-factor app's processes are disposable, meaning they can be started or stopped at a moment's notice. This facilitates fast elastic scaling, rapid deployment of code or config changes, and robustness of production deploys." 
                                                                                                                                                                               - Adam Wiggins

这个原则对于微服务架构风格很重要。因此,对于微服务,我们必须确保服务启动得更快。在这种情况下,让我们假设每个虚拟机有一个服务。如果我们想旋转这个服务,它需要旋转虚拟机;但是,虚拟机的启动时间很长。还会有很多集群部署。这意味着我们的服务肯定会以集群的形式分布。

这也意味着虚拟机可能需要在集群中的一个节点上启动并引导。这也是虚拟机启动时间的问题。这并没有带来我们所期望的微服务的效率。

现在,剩下的唯一选择是使用操作系统进程模型,它具有更快的启动时间。过程编程模型早已为人所知,但即使是过程也是有代价的。他们没有被很好地隔离;它们共享系统资源,以及操作系统的内核。

对于微服务,我们需要更好的隔离部署模型和新的部署范式。答案是用容器技术创新。一个很好的考虑因素是容器技术位于虚拟化和操作系统的流程模型之间。我们将在下一节中了解这一点。

出路——集装箱

容器技术对 Linux 世界来说并不陌生。容器基于 Linux 的 LXC 技术。在这一节中,我们将学习容器在微服务中是多么重要。

什么是容器?

容器是完整文件系统中的一个软件。它包含运行代码、运行时、系统工具和系统库所需的一切——任何可以安装在服务器上的东西。这保证了软件将始终以相同的方式运行,而不管其环境如何。容器与同一主机上的其他容器共享其主机操作系统和内核。围绕容器的技术并不新鲜;长期以来,它一直是 Linux 生态系统的一部分。

容器对虚拟机的适用性

让我们理解容器和虚拟机之间的区别。从表面上看,两者都是可以用来实现隔离和虚拟化的工具。

虚拟机和容器之间的架构差异可以在下图中看到:

我们可以看到,就虚拟机而言,有一个主机操作系统,以及一个内核和一个虚拟机管理程序层。托管应用必须引入自己的操作系统和环境。然而,在容器中,容器化技术层作为一个单独的层,它在不同的应用之间共享。

Containers don't provide the same level of isolation as VMs. For containers, if you run services for multiple customers, you shouldn't run customer_1 and customer_2 on the same Docker host. With VMs, this isn't an issue. Also, remember performance issues—if you don't configure resource limits on your containers, it's possible for one bad container to bring down the others.

这消除了对客户操作系统的需求。因此,容器中的应用占用空间更小,隔离级别更高。鼓励您使用容器进行微服务部署的另一个方面是,与部署在虚拟机上的相同应用相比,我们可以在同一台物理机上打包更多的应用。这有助于我们实现更大的规模经济优势,并提供了虚拟机优势的比较。

容器还有一点需要注意,它们也可以在虚拟机上运行。这意味着在物理服务器上安装虚拟机是可以的。该虚拟机充当许多容器的主机。这取决于主机的 CPU 及其对嵌套虚拟化的支持。

运营团队心态的转变

微软的比尔·贝克提出了一个宠物和牛的类比,他将其应用于数据中心的服务器。好吧。老实说,我们关心我们的宠物。我们爱他们,向他们表达爱意,我们也给他们取名。我们想到他们的卫生;如果它们生病了,我们会带它们去看兽医。我们这样照顾我们的牛吗?我们没有;这是因为我们不太关心牛。

同样的类比也适用于服务器和容器。在 DevOps 之前,服务器管理员关心服务器。他们曾经命名那些服务器机器,并且也有专门的维护停机时间等等。有了 DevOps 实践,比如代码和容器化这样的基础设施,容器就像牛一样被对待。作为运营团队,我们不需要关心它们,因为容器的寿命很短。它们可以在集群中快速启动,也可以快速拆除。当您处理容器时,请始终记住这个类比。就日常操作而言,期望容器的旋转和拆卸是正常的做法。

这个类比改变了我们对微服务部署以及它如何支持容器化的看法。

容器是新的二进制文件

作为一名使用微服务的. NET 开发人员,您将面临一个新的现实:容器是新的二进制文件。使用 Visual Studio,我们可以编译.NET 程序,编译后,Visual Studio 生成.NET 程序集,即 dll 或 exe。我们将编译器发出的这一组相关联的 dll 和 exe 部署到服务器上。

It was Steve Lasker, a Principal Program Manager at Microsoft, who first called containers the new binaries of deployment.

简而言之,我们的部署单元是以集合的形式。不再是了!我们还有.NET 程序生成 exe 和 dll,但是我们的部署单元在微服务世界中已经改变了。现在,它是一个容器。我们仍然会将程序编译成程序集。这些组件将被推到集装箱,并确保它们已准备好运输。

当我们浏览这些代码时,您会理解这个概念。我们作为.NET 开发人员,有能力(我可以说必要性)装运集装箱。除此之外,容器部署的另一个优点是它消除了不同操作系统甚至不同语言和运行时之间的障碍。

它在你的机器上工作吗?让我们运送你的机器!

通常,我们从开发者那里听到很多这样的话:嗯,它在我的机器上工作!这通常发生在生产中存在不可重现的缺陷时。由于容器是不可变和可组合的,因此有可能消除开发和生产环境之间的配置阻抗。

介绍 Docker

码头工人(https://www.docker.com/)一直是推广应用容器化的主要力量。Docker 之于容器就像谷歌之于搜索引擎。有时,人们甚至使用容器和 Docker 作为同义词。微软已经与 Docker 建立了合作伙伴关系,并且正在通过开源为 Docker 平台和工具做出积极贡献。这使得 Docker 对我们来说非常重要.NET 开发人员。

Docker 是一个非常重要的话题,意义重大,任何严肃的.NET 开发人员应该了解一下。然而,由于时间和范围的限制,我们将在这里仅仅触及 Docker 生态系统的表面。我们强烈建议您通读帕克特出版公司提供的 Docker 书籍。

If you want to safely try and learn about Docker without installing it on your machine, you can do so by going to https://KataCoda.com.

现在,让我们专注于 Docker 平台的一些术语和工具。这对于下一部分至关重要:

  • Docker 图像:这是一个模板,包含创建 Docker 容器的说明。只能看说明书;您不能向该模板添加自己的说明,因为它是只读的。它由一个单独的文件系统、相关的库等组成。在这里,映像始终是只读的,并且可以运行完全相同的抽象和底层主机差异。可以合成一个 Docker 图像,这样一层在另一层之上。Docker 图像的这种可组合性可以与分层蛋糕的类比进行比较。跨不同容器使用的 Docker 映像可以重用。这也有助于减少使用相同基础映像的应用的部署占用空间。
  • Docker 注册表:你可以把这个和 Windows 注册表(作为一个层次数据库)或者一个软件产品(作为一个众多程序的库)关联起来。类似地,Docker 注册表包含各种图像,如果可以检索或使用一组图像信息,则称之为图像库。您可以在同一个服务器上拥有一个公共或私有注册表,其中 Docker 守护程序或 Docker 客户端位于完全独立的服务器上。

The Windows Registry is a database that stores information about the internal or low-level settings of the Microsoft Windows operating system.

  • Docker hub :这是一个存储图像的公共注册表。位于http://hub.docker.com
  • Dockerfile : Dockerfile 是一个构建或脚本文件,包含我们可以用来构建 Docker 映像的指令。可以在 Dockerfile 中记录多个步骤,从获取基础映像开始。
  • Docker 容器:Docker 映像的一个实例称为 Docker 容器。
  • Docker compose:Docker compose 允许您在单个文件中定义应用的组件——它们的容器、配置、链接和卷。一个命令就可以设置一切并启动应用。它是应用的架构/依赖关系图。
  • Docker swarm : Swarm 是容器节点协同工作的 Docker 服务。它运行定义数量的副本任务实例,副本任务本身就是 Docker 映像。

现在,让我们看看 Docker 生态系统的各个组件,并了解 Docker 工作流在软件开发生命周期中的意义。

部署示例应用

为了支持这个工作流,我们需要一个配置项工具和一个配置管理工具。为了便于说明,我们采用了 Azure DevOps (以前的Visual Studio Team Services(VSTS)构建服务进行持续集成,采用 Azure DevOps 发布管理进行持续交付。对于任何其他工具或部署模式,工作流将保持不变。

下图描述了这一点:

以下是 Docker 微服务部署的要求:

  1. 代码被签入 Azure DevOps 存储库。如果这是项目的第一次签入,它将与项目的 Dockerfile 一起完成。
  2. 前面的签入触发了 Azure DevOps,因此它可以从源代码构建服务并运行单元/集成测试。
  3. 如果测试成功,Azure DevOps 将构建一个 Docker 映像,并将其推送到 Docker 注册表。Azure DevOps 发布服务将映像部署到 Azure 容器服务中。
  4. 如果质量保证测试也通过了,Azure DevOps 被用来升级容器,这样它就可以在生产中部署和启动。

The usual .NET CI-CD tools, such as TeamCity and Octopus Deploy (their capabilities are in the alpha stage), have features that can produce a Docker container as a build artifact and then deploy it to production.

在下一节中,我们将学习微服务部署如何在 Docker 中工作。

使用 Docker 的微服务部署示例

现在我们已经掌握了所有的要点,我们可以开始编码,看看事情是如何运作的。我们在这里以产品目录服务为例,我们将把它部署为一个 Docker 容器。运行附带的源代码后,您应该能够在 Docker 容器中成功运行产品目录服务。

在您的机器上设置 Docker

如果你从来没有在你的机器上使用过 Docker,或者如果你之前没有任何使用 Docker 的经验,那么不要担心!在本节中,我们将介绍这些步骤,以便您可以在 20 或 30 分钟内使用 Docker。

先决条件

要在您的机器上设置 Docker,您需要执行以下操作:

  1. 安装微软 Visual Studio 2019(https://www . visualstudio . com/downloads/downloads-Visual-Studio-vs)。
  2. 安装.NET Core 3.1(https://www.microsoft.com/net/download/core)。
  3. 为 Windows 安装 Docker,以便您可以在本地运行 Docker 容器(https://www.docker.com/products/docker#/windows)。

We are using Docker Community Edition for Windows to demonstrate this example.

  1. 安装后,您的系统需要重新启动。

  2. 重新启动系统后,如果系统上未启用 Hyper-V 功能,Windows Docker 将提示您启用该功能。单击“确定”在您的系统上启用 Hyper-V 功能(需要重新启动系统):

  1. 安装 Windows Docker 后,右键单击系统托盘中的 Docker 图标,然后单击设置。然后,选择共享驱动器:

现在,我们可以创建一个应用。

创建 ASP.NET Core 网络应用

在前几节中,我们讨论了容器、在机器上设置 Docker 以及使用 Docker 进行部署。现在,是时候看一个例子了,这样我们就可以实现到目前为止所学的一切。为此,请遵循以下步骤:

  1. 通过导航到文件|新项目|创建新项目.NET Core 并选择 ASP.NET Core Web 应用,如下图截图所示:

  1. 在新 ASP.NET Core 网络应用窗口中,选择。网络核心和 ASP.NET Core 3.1:

  1. 从创建新的 ASP.NET Core 网络应用窗口中,选择.NET Core 和 ASP.NET Core 3.1。从可用模板中选择网络应用(模型-视图-控制器):

  1. 选中启用 Docker 支持(在右侧)。
  2. 因为我们使用的是 Windows,所以选择它作为操作系统(如果你还没有安装 Docker,你需要为 Windows 安装 Docker)。
  3. 单击创建继续。

前面的步骤将在 Docker 的支持下创建FlixOne.BookStore.ProductService项目。下面的截图显示了我们项目的结构:

以下文件将被添加到项目中:

  • Dockerfile:ASP.NET Core 应用的 docker file 基于microsoft / aspnetcore图像(https://hub.docker.com/r/microsoft/aspnetcore/)。这张图片包括 ASP.NET Core 数字获取软件包,这些软件包已经预先安装,以提高启动性能。构建 ASP.NET Core 应用时,Dockerfile FROM指令(命令)指向 Docker 集线器上最近的microsoft / dotnet映像(https://hub.docker.com/r/microsoft/dotnet/)。以下是模板提供的默认代码片段:
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-nanoserver-1903 
AS base WORKDIR /app 
EXPOSE 80 EXPOSE 443 
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime
WORKDIR /app
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "FlixOne.BookStore.ProductService.dll"]

前面的代码基本上是一组指令。让我们看一下这些说明中包含的一些属性:

为了在容器的端口80上公开我们的Product服务,我们可以使用EXPOSE ENTRYPOINT,它指定了当容器启动时我们用来执行的命令。在我们的例子中,我们有ProductService,我们的切入点是["dotnet", "FlixOne.BookStore.ProductService.dll"]

让我们看看 Docker 所需的各种组件/配置文件:

  • Docker-compose.yml:这是基本的合成文件,用于定义可以用Docker-compose构建/运行构建和运行的图像集合。
  • Docker-compose.dev.debug.yml:当您的配置设置为调试模式时,这是一个用于迭代更改的附加合成文件。Visual Studio 将调用-f docker-compose.yml-f docker-compose.dev.debug.yml将其合并。Visual Studio 开发工具使用此编写文件。
  • Docker-compose.dev.release.yml:这是一个附加的合成文件,我们可以用它来调试我们的发布定义。它将独立加载调试器,这样就不会改变产品映像的内容。
  • docker-compose.yml文件包含项目运行时创建的图像的名称。

现在,我们在 Docker 容器中拥有了运行/启动服务所需的一切。在我们继续之前,请参考第 2 章重构整体,并添加完整的代码(即控制器、存储库等),这样项目的结构看起来如下:

现在,您所要做的就是按下 F5 并在容器中启动您的服务。这是将您的服务放入容器中最简单易行的方法。一旦您的微服务被容器化,您就可以使用 Azure DevOps(以前是 Visual Studio Team Services)和 Azure 容器服务将您的容器部署到 Azure 云中。

在本节中,我们已经部署了一个示例应用,并讨论了使用 Docker 作为容器的部署。我们还学习了如何在本地机器上启用 Docker。

摘要

微服务部署对我们来说是一次激动人心的旅程。为了成功交付微服务,我们应该遵循部署最佳实践。在讨论使用自动化工具进行部署之前,我们需要重点关注实现微服务的隔离要求。通过成功的微服务部署实践,我们可以快速交付业务变化。从自给自足的团队到持续交付,不同的隔离需求为我们提供了敏捷性和可扩展性,这是实现微服务的基础。

容器化是迄今为止我们拥有的最重要的创新技术之一,我们必须利用它来部署微服务。将 Azure 云与 Docker 相结合将有助于我们提供微服务所期望的规模和隔离。借助 Docker,我们可以轻松实现更高的应用密度,这意味着降低我们的云基础架构成本。我们还看到了用 Visual Studio 和 Docker 启动这些部署是多么容易。

在下一章中,我们将研究微服务安全性。我们将讨论用于身份验证的 Azure 活动目录,如何利用 OAuth 2.0,以及如何使用 Azure API 管理来保护 API 网关。

问题

  1. 什么是 Docker 图像,为什么它如此重要?

  2. 什么是 Docker 存储库?

  3. 什么是 Docker 容器?

  4. 什么是码头工人中心?

  5. 我可以在 Docker 文件中使用 JSON 代替 YAML 吗?如果是,如何做到?

  6. 你能解释一下下面的词吗,都和集装箱有关:FROMCOPYWORKDIREXPOSEENTRYPOINT

  7. 你能编写一个简单的 ASP.NET web 应用,在Product Services的帮助下,以表格形式显示AddDeleteUpdate产品吗?

进一步阅读

六、使用 Azure 活动目录保护微服务

安全性是 web 应用最重要的交叉问题之一。不幸的是,如今知名网站的数据泄露似乎司空见惯。考虑到这一点,信息和应用安全对于 web 应用来说变得至关重要。出于同样的原因,安全应用不应再是事后的想法。安全是组织中每个人的责任。

与微服务相比,单片应用有更大的攻击表面积;然而,微服务本质上是分布式系统。此外,原则上,微服务是相互隔离的,因此与单片应用相比,实施良好的微服务更加安全。与微服务相比,单块有不同的攻击媒介。微服务架构风格迫使我们在安全的背景下进行不同的思考。让我提前告诉你,微服务安全是一个很难理解和实现的复杂领域。

在我们深入研究微服务安全之前,让我们了解一下我们的方法。我们将更加关注认证 ( 认证)和授权 ( 认证)—在本章中统称为认证—如何工作,我们将讨论中可用的选项.NET 生态系统。

我们还将探索 Azure 应用编程接口管理服务及其作为应用编程接口网关的适用性.NET 的微服务环境,以及 Azure API Management 如何通过其安全特性帮助我们保护微服务。然后,我们将简要介绍不同的外围方面,它们具有针对微服务安全的深度防御机制。

在本章中,我们将讨论以下主题:

  • 为什么传统的认证系统不起作用?
  • 引入 OAuth 2.0
  • 引入 Azure 应用编程接口管理作为应用编程接口网关
  • 使用 Azure 应用编程接口管理安全性
  • 服务间通信安全方法
  • 集装箱安全和其他外围安全方面

技术要求

本章包含各种代码示例,以便解释它所描述的概念。代码保持简单,只是为了演示。大多数示例都涉及一个. NET Core 控制台应用,它是用 C#编写的。

Note that there will be a difference between the actual screen you'll see and the screenshots that are in this chapter. The look and feel of Visual Studio depends on the themes you select.

要运行和执行本章中的代码,您需要以下先决条件:

  • Visual Studio 2019 或更高版本
  • .NET Core 3.1 或更高版本

要安装和运行这些代码示例,您需要安装 Visual Studio 2019 或更高版本(首选 IDE)。为此,请遵循以下步骤:

  1. https://docs . Microsoft . com/en-us/Visual Studio/install/install-Visual Studio下载 Visual Studio 2019(社区免费)。
  2. 按照操作系统的安装说明进行操作。Visual Studio 有多个版本。我们使用的是视窗操作系统。

如果你没有.NET Core 3.1 安装完毕,可以下载设置,前往https://dotnet.microsoft.com/download/dotnet-core/3.0

The complete source code is available at: https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2006.

单片应用的安全性

为了理解微服务安全性,让我们回顾一下我们是如何保护的.NET 单片应用。这将帮助我们理解为什么微服务的授权机制需要不同。

用于保护应用安全的关键机制一直是 auth。认证验证用户的身份。授权管理用户可以访问或不能访问的内容,也称为权限。当数据在客户端和服务器之间传递时,加密是帮助您保护数据的机制。不过,我们不会过多讨论加密——只是为了确保通过网络传输的数据在任何地方都是加密的。这可以通过使用《HTTPS 议定书》来实现。

下图描述了中典型身份验证机制的流程.NET 单片:

在上图中,我们可以看到用户通常通过网络浏览器输入他或她的用户名和密码。然后,这个请求到达负责授权的 web 应用中的一个薄层。在. NET Core 应用的情况下,该层或组件连接到用户凭据存储,用户凭据存储通常是一个 SQL 服务器。身份验证层根据存储在凭据存储中的用户名和密码验证用户提供的凭据。

一旦验证了用户的会话凭据,就会在浏览器中创建一个会话 cookie。除非用户拥有有效的会话 cookie,否则他们无法访问该应用。通常,每个请求都会发送一个会话 cookie。在这些类型的单片应用中,模块可以自由地相互交互,因为它们在同一个进程中并且具有内存访问。这意味着信任在这些应用模块中是隐含的,因此它们在相互对话时不需要单独的请求验证和确认。

这一节的重点主要是研究单片应用中的安全性,我们通过查看图表了解了安全性的流程。在下一节中,我们将讨论微服务中的安全性,并了解如何保护分布式系统。

微服务的安全性

现在,让我们看看微服务的例子。本质上,微服务是分布式系统。从来没有一个应用实例;相反,有几个不同的应用相互协调,以产生所需的输出。

在微服务安全方面,当与 ASP.NET Core 合作时,我们可以使用不同的安全机制,如 OAuth2.0、JWT 和 Azure Active Directory(带有额外的授权实现)。在微服务的情况下,并不是每个服务都必须进行身份验证,有些服务不需要任何身份验证来返回响应。总的来说,现在安全是一个大问题。

在下一节中,我们将讨论为什么传统的.NET 身份验证机制不起作用,然后我们将讨论各种其他身份验证机制。

为什么传统的?NET 身份验证机制工作正常吗?

微服务安全的一种可能方法如下:我们模拟与单块中授权层相同的行为。这可以描述如下:

在这种方法中,我们分发了身份验证层,并将其提供给所有微服务。因为每个都是不同的应用,所以它需要自己的身份验证机制。这本质上意味着每个微服务的用户凭据存储也是不同的。这引发了很多问题,比如,我们如何在所有服务中保持身份验证同步?我们如何验证服务间通信,还是跳过它?我们对这些问题没有满意的答案,所以这种方法没有意义,只会增加复杂性。我们甚至不能确定这种方法在现实世界中是否行得通。

对于现代应用,我们还需要考虑一个因素。在微服务领域,我们需要支持原生移动应用和其他非标准外形设备,以及物联网应用。随着本地移动应用的大量增加,微服务架构也需要支持这些客户端和微服务之间的安全通信。这不同于传统的基于网络浏览器的用户界面。在移动平台上,网络浏览器不是任何本地移动应用的一部分。这意味着在大多数应用中,基于 cookie 或基于会话的身份验证是不可能的,除非您使用特殊的方法或框架。例如,您可以在手机上使用 cookies 和会话,尤其是使用渐进式网络应用 ( PWAs )。因此,微服务需要支持客户端应用之间的这种互操作性。这从来不是一个问题.NET 单片应用。

在传统身份验证的情况下,浏览器负责根据每个请求发送 cookie。但是我们没有将浏览器用于本地移动应用。事实上,我们也没有使用 ASPX 页面,也没有使用表单的身份验证模块。对于 iOS 客户端或安卓系统来说,这完全是两码事。此外,我们还试图限制对我们的应用编程接口的未经授权的访问。在前面的例子中,我们将保护客户端,无论是 MVC 应用还是电话应用,而不是微服务。此外,所有这些移动客户端设备都不是信任子系统的一部分。对于每个请求,我们不能相信移动用户确实是所有者;通信信道也不安全。这意味着来自他们的任何请求根本不可信。

但是除了这些问题,我们还有另一个更概念性的问题。为什么应用应该负责验证用户和授权?这不应该分开吗?

对此的另一个解决方案是使用 SAML 协议,但同样,这是基于 SOAP 和 XML 的,因此它并不真正适合微服务。SAML 的实现复杂度也很高。

因此,很明显,我们需要一个基于令牌的解决方案。微服务授权的解决方案以开放身份连接和开放身份验证 2.0 的形式出现。OpenID Connect 是认证的标准,而 OAuth 2.0 是授权的规范;然而,这种授权是自然委托的。

我们将在接下来的章节中详细讨论这一点。但是在此之前,让我们绕道看看 JSON 网络令牌,看看它们为什么在微服务安全方面如此重要。

JSON Web 令牌

JSON 网络代币 ( JWT )发音为 JOT 。这是一个定义良好的 JSON 模式或格式,用于描述数据交换过程中涉及的令牌。JSON 网络令牌在https://tools.ietf.org/html/rfc7519详细定义。JWTs 可以用来验证请求,而不需要使用 OpenID 和 OAuth2.0.NET Core 应用。

OpenID Connect is an authentication layer, on top of OAuth 2.0 (which is an authorization framework).

在 JWT 的实现中,我们使用基于 JWT 的安全令牌。这个令牌是一个 JSON 对象,它包含关于发送者和接收者的信息,以及发送者的身份。因此,令牌应该通过电线进行保护,这样它们就不会被篡改。为此,令牌被分配对称或非对称密钥。这意味着当接收者信任令牌的发行者时,也可以信任令牌内部的信息。

If you want to find more general information related to security, visit the Open Web Application Security Project (OWASP) at http://www.owasp.org and the Microsoft Security development life cycle at https://www.microsoft.com/en-us/sdl/.

这里有一个 JWT 的例子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

这是 JWT 的编码形式。如果我们看到相同的令牌的解码形式,它有三个组成部分:报头、有效载荷和签名。它们都用句点(.)隔开。前面的令牌可以如下解码:

Header: {"alg": "HS256", "type": "JWT"}
Payload: {"sub": "1234567890","name": "John Doe","admin": true}
Signature:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

.NET Core 内置了生成和消费 JWTs 的支持。您可以在任何位置安装 JWT 支持.NET Core 应用,通过使用包管理器控制台和以下命令:

Install-Package System.IdentityModel.Tokens.Jwt

Visit https://jwt.io/ if you want to view and decode JWTs very easily. Moreover, you can add it as part of the Chrome debugger as well, which is quite handy.

本节向您概述了 JWT,包括如何独立于 OpenID Connect 和 OAuth2.0 使用它。在下一节中,我们将学习如何使用 OAuth2.0

使用 OAuth 2.0

好吧,你可能不知道 OAuth 2.0 是什么,但你肯定在几个网站上用过。如今,许多网站允许你使用脸书、推特和谷歌账户的用户名和密码登录。转到您最喜欢的网站,例如https://stackoverflow.com/登录页面。例如,有一个登录按钮,表示您可以使用您的谷歌帐户登录。当你点击谷歌按钮时,它会带你进入谷歌的登录页面,以及我们之前提到的一些权限。在这里,你提供你的谷歌用户名和密码,然后你点击允许按钮,授予你最喜欢的网站的权限。然后,谷歌将你重定向到堆栈溢出,你以堆栈溢出中适当的权限登录。这仅仅是 OAuth 2.0 和 OpenID Connect 的最终用户体验。

OAuth 2.0 处理 web 上、本地移动应用中以及所有无头服务器应用中的授权(在我们的上下文中,这些只不过是微服务实例)。您可能想知道为什么我们首先讨论授权,而不是身份验证。原因是 OAuth 2.0 是一个委托授权框架。这意味着,为了完成授权流程,它依赖于身份验证机制。

现在,让我们来看看与此相关的一些术语。

OAuth 2.0 角色描述了授权过程中涉及的各方,如下所示:

  • 资源:正在得到保护以防止意外访问和使用的实体。在我们的例子中,这是一个微服务。
  • 资源所有者:顾名思义,资源所有者可以是拥有资源所有权的个人,也可以是实体。他们也被称为最终用户。
  • 客户端:客户端是用来指代各种客户端应用的术语。这是指任何试图访问受保护资源的应用。在微服务上下文中,涉及的应用是单页应用、web 用户界面客户端和本地移动应用,甚至是试图访问下游其他微服务的微服务。
  • 授权服务器:简单来说,这个服务器叫做授权服务器,因为它通过颁发有效的令牌来认证终端用户。我们也可以称之为服务器,它对资源所有者进行身份验证,并向客户端颁发令牌。该令牌由托管在授权服务器上的安全令牌服务生成。

您可能已经注意到,OAuth 区分了最终用户和最终用户使用的应用。这有点奇怪,但这很有意义,因为它基本上是说,我授权这个应用代表我执行这些操作

下图描述了在 OAuth 框架的一般授权流程中,这些角色如何相互作用:

在上图中的步骤 6 中,客户端将授权许可传递给授权服务器。这一步并不像看起来那么简单。授权有不同的类型。授权类型代表了在 OAuth 2.0 中获取访问令牌的四种不同的可能用例。如果您选择了错误的授权类型,您可能会危及安全性。让我们来看看它们:

  • 授权代码:这是服务器端网络应用使用的典型 OAuth 授权,也是你在 ASP.NET 应用中使用的授权。
  • 隐式:这对于单页应用非常有用,因为单页应用中的通信大部分是公共的,但也可以是私有的。身份验证是通过识别从服务器返回到浏览器的访问令牌来完成的,然后可以使用它来访问资源。
  • 资源所有者密码凭证:这需要用户直接在应用中输入自己的用户名和密码。这在您开发第一方应用时非常有用,以便使用您自己的服务器进行身份验证。
  • 客户端凭证:在您会遇到的大多数场景和情况下,访问受保护的资源都需要客户端凭证。除此之外,它们通常在客户端代表自己行动时使用(客户端也是资源所有者)。

本节的目的是强调分布式系统的安全性。微服务是分布式系统,所以我们研究了为什么不能采用经典的安全方法。然后,我们了解了 JWT 和 OAuth 的安全机制。下一节将重点介绍 OpenID Connect,它是 OAuth 2.0 协议之上的一层。

探索开放身份连接

OpenID Connect 1.0 是 OAuth 2.0 协议之上的一个简单的身份层。OpenID Connect 是关于身份验证的。它允许客户端根据授权服务器执行的身份验证来验证最终用户。它还用于以可互操作和类似 REST 的方式获取终端用户的基本配置文件信息。

OpenID Connect 允许所有类型的客户端——基于网络的、移动的和 JavaScript——请求和接收关于经过身份验证的会话和最终用户的信息。我们知道 OAuth 2.0 定义了访问令牌。好吧,OpenID Connect 定义了一个标准化的身份令牌(通常称为 ID 令牌)。身份令牌被发送到应用,以便应用可以验证用户是谁。它定义了一个端点来获取该用户的身份信息,例如他们的姓名或电子邮件地址。这是用户的信息端点。

它建立在 OAuth 2.0 之上,所以流程是相同的。它可以与授权码授权和隐式授权一起使用。这对于客户端凭证授权是不可能的,因为客户端凭证授权是用于服务器到服务器的通信。

这个过程中没有最终用户,因此也没有最终用户身份。同样,对于资源所有者的使用路径或流程来说,这也没有意义。那么这是如何工作的呢?我们将从安全令牌服务 ( STS )请求一个额外的 ID 令牌,而不是只请求一个访问令牌,后者实现了 OpenID Connect 规范。客户端接收一个标识令牌,通常还有一个访问令牌。要为经过身份验证的用户收集更多信息,客户端可以使用访问令牌向用户信息端点发送请求;然后,该用户信息端点将返回关于新用户的声明。OpenID 支持授权代码流和隐式流。它还增加了一些附加协议,即发现和动态注册。

身份验证和授权是任何安全应用都需要的重要元素。在本节中,我们了解了 OpenID Connect,它是 OAuth 框架之上的一个身份层。在下一节中,我们将讨论 Azure Active Directory,它是一个遵循 OpenID Connect 和 OAuth2.0 规范的提供程序。

了解 Azure 活动目录

OAuth 2.0 和 OpenID Connect 1.0 规范有多个提供者。 Azure 活动目录 ( AAD )就是其中之一。AAD 为组织提供企业级身份管理,用于云应用。AAD 集成将为您的用户提供简化的登录体验,它将帮助您的应用符合 it 政策。AAD 提供了高级安全功能,例如多因素身份验证,并且它可以随着应用的增长而很好地扩展。它用于所有微软 Azure 云产品,包括 Office 365,每天处理超过 10 亿次登录。

传统的一个更有趣的方面.NET 环境中,他们可以很好地将他们组织的 Windows Server 活动目录与 AAD 集成在一起。这里,我们使用了 OAuth/OIDC,因为我们采用了活动目录联合服务 ( ADFS )。但是在基于生产的应用中,我们有一个混合的情况,在内部 AD 中有一个不同的协议集(Kerberos、LDAP 等)。

Azure Active Directory

AAD

例如,假设组织的一名员工希望从特定位置访问特定资源。在这里,AAD 帮助验证该访问。这可以通过 AAD 同步工具或新的直通身份验证功能来完成,因此组织的信息技术合规性仍将得到管理。

Path-Through Authentication

PTA

https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-pta

Password Hash Synchronization

PHS

https://docs.microsoft.com/en-us/azure/active-directory/hybrid/whatis-phs

在这一节中,我们了解到 Azure Active Directory 是一个提供者,并且遵循 OpenID Connect 和 OAuth2.0 的规范。在下一节中,我们将借助一个代码示例来介绍 OpenID Connect、OAuth2.0 和 AAD。

使用 OpenID Connect、OAuth 2.0 和 Azure AD 的微服务身份验证示例

既然我们已经具备了所有的必备知识,我们就可以开始编码了。让我们构建一个ProductService应用。我们将保护FlixOne.BookStore.ProductService,它代表了我们的微服务之一。在解决方案中,FlixOne.BookStore微服务由FlixOne.BookStore.ProductService项目代表,FlixOne.BookStore.Web代表服务器端 web 应用。如果您打开本章提供的名为OpFlixOne.BookStore.sln的 Visual Studio 解决方案,将会更容易理解。此示例使用客户端凭据授予。

请注意,由于 Azure 门户和相应的 Azure 服务用户界面的不断变化的性质,建议您使用 Azure 资源管理器 ( ARM )应用编程接口,并自动执行您即将执行的一些注册任务。然而,出于学习的目的,并且主要是为了鼓励对 Azure 不熟悉或者第一次尝试 Azure 广告的开发人员,我们将遵循 Azure 门户用户界面。

以下是先决条件:

  • Visual Studio 2019 或更高版本
  • Azure 订阅(如果您没有这个,您可以使用免费试用帐户进行演示)
  • Azure AD 租户(单租户)—您还可以使用 Azure 帐户的默认目录,该目录应该不同于 Microsoft 组织的目录

本节旨在让您开始实施 AAD。我们创建了两个项目:服务和网络。在接下来的部分中,我们将扩展这个例子,从向 AAD 租户注册我们的应用开始。

向 Azure 广告租户注册我们的应用

我们需要将我们的应用集成到微软身份平台,这要求我们使用 Azure 门户中的应用注册体验来注册我们的应用。

现在,我们来看看如何注册FlixOne.BookStore.ProductService

在这一步中,我们将向 Azure AD 租户添加FlixOne.BookStore.ProductService。为此,请登录 Azure 管理门户。请遵循以下步骤:

  1. 登录蔚蓝门户(https://portal.azure.com)。
  2. 点击 Azure 活动目录,如下图所示:

  1. 前面的步骤将带您进入默认目录的概览屏幕。点击应用注册,如下图:

  1. 单击新建注册。这将打开注册应用屏幕,如下所示:

提供前面截图中显示的所有强制详细信息,然后单击注册应用屏幕底部的注册按钮。当我们提供登录网址时,请确保您为您的应用提供了该网址。在我们的例子中,FlixOne.BookStore.ProductService是一个微服务,所以我们不会有一个特殊的登录网址。这意味着我们必须提供默认的网址,或者仅仅是微服务的主机名。在这里,我们将从我们的机器上运行服务,所以本地主机网址就足够了。您可以找到登录网址,一旦右键单击FlixOne.BookStore.ProductService项目下的项目网址,然后导航到调试,如下图截图所示:

/

  1. 如果您使用微软 Azure 应用服务计划部署您的服务,您将获得一个类似于https://productservice-flixone.azurewebsites.net/的网址。如果您在 Azure 上部署服务,您可以随时更改登录网址。
  2. 单击注册按钮后,Azure 会将应用添加到您的 Azure 广告租户中。不过还有几个细节需要完成,才能完成ProductService的注册。导航至应用注册| FlixOne.BookStore.ProductService。您会注意到,现在已经提供了一些附加属性,例如应用标识 URI。
  3. 对于应用标识网址,输入https://[Your_Tenant_Name]/ProductService,用您的 Azure 广告租户的名称替换[Your_Tenant_Name]。单击“确定”完成注册过程。最终配置应该如下所示:

现在,让我们进入FlixOne.BookStore.Web的注册:

  1. 首先,我们注册FlixOne.BookStore.Web。这是必要的,因为我们将使用 OpenID Connect 来连接到这个基于浏览器的网络应用。因此,我们需要在最终用户——也就是我们——和FlixOne.BookStore.Web之间建立信任。
  2. 点按“应用注册”,然后点按“新注册”。这将打开注册应用屏幕,如下图所示。填写名称,然后单击注册:

  1. 类似于我们注册ProductServices时,输入一些额外的必填信息,如下:

  1. 注意注销网址设置:我们设置为https://localhost:44322/Account/EndSession
    这是因为,结束会话后,Azure AD 会将用户重定向到这个 URL。对于应用标识网址,输入https://[Your_AD_Tenant_Name]/TodoListWebApp,用您的 Azure 广告租户的名称替换[Your_AD_Tenant_Name]。单击“确定”完成注册过程。
  2. 现在,我们需要为 FlixOne 设置权限。书店。网络,这样它就可以调用我们的微服务:产品服务。导航至应用注册|动画。再次单击书店.网站|应用编程接口权限,然后单击添加权限。这将保存权限。

注册应用是必需的,这样我们的服务和网络应用就可以与微软身份服务集成。本节还向我们介绍了 AAD 的实现。在接下来的章节中,我们将详细介绍这个应用。

正在为 FlixOne 生成 AppKey。网上书店

注册的另一个重要步骤是添加client_secret,这是在 Azure AD 和FlixOne.BookStore.Web之间建立信任所必需的。这个client_secret只生成一次;它是在 web 应用中配置的。

要生成该密钥,请执行以下步骤:

  1. 导航至应用注册|动画。书店.网站|证书和机密|新的客户端机密,如下图所示:

  1. 然后,添加描述为AppKey,选择密钥何时到期(我们选择了 1 年),点击【添加】,如下图截图所示:

  1. 保存密钥后,Azure 将自动生成密钥的值,并显示在描述旁边。该键仅显示一次,因此您必须立即复制并保存它以备后用。在这种情况下,我们将把这个密钥保存在FlixOne.BookStore.Webappsettings.json文件中。

密钥将显示在 Azure 门户上,如下所示(密钥值将显示在右侧):

client_Secret

appsettings.json

https://azure.microsoft.com/en-us/services/key-vault/

Managed identities for Azure resources

MSI

https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview

这一部分帮助我们为假想的应用FlixOne.BookStore.Web生成一个 AppKey,通过引导我们完成生成一个 Key 的步骤。现在,我们需要使用项目的配置设置为我们的应用配置这个键。在下一节中,我们将使用 Visual Studio 配置我们的解决方案。

配置 Visual Studio 解决方案项目

一旦我们完成了应用的注册,我们需要确保我们配置了我们的项目,这样这些项目将按照我们的期望工作。首先我们来看看如何用ProductServices项目配置这个。

打开appsettings.json文件,然后为以下键添加一些值:

  1. 搜索Tenant键。用您的 AD 租户名称替换它的值,例如,contoso.onmicrosoft.com。这也将是任何应用的应用标识网址的一部分。
  2. Audience键值替换为https://[Your_AD_Tenant_Name]/FlixOneBookStoreProductService
    [Your_AD_Tenant_Name]替换为您的 Azure AD 租户的名称。

现在,让我们学习如何使用FlixOne.BookStore.Web配置这个,以便我们的 web 项目具有正确的值。

打开appsettings.json文件。然后用提供的值查找并替换以下键:

  1. WebResourceid键的值替换为https://[Your_Tenant_Name]/ProductService
  2. WebBaseAddress替换为https://localhost:44321/
  3. ClientId替换为 FlixOneBookStoreWeb 的应用 ID。您可以通过导航到应用注册|动画电子书商店网站来获取它。
  4. AppKey替换为client_secret,这是我们在注册 TodoListWebApp 过程的步骤 2 中生成的。如果您没有注意到这个键,那么您需要删除以前的键并生成一个新的键。
  5. Tenant替换为您的 AD 租户名称,例如contoso.onmicrosoft.com
  6. 当用户注销 TodoListWebApp 时,用您希望应用将您重定向到的网址替换RedirectUri。在我们的例子中,默认值是https://localhost:44322/,因为我们希望用户导航到应用的主页。

在本节中,我们学习了如何获取密钥以及如何处理项目。在下一节中,我们将了解为什么管理它至关重要。

在 IIS Express 上生成客户端证书

到目前为止,我们已经注册了我们的应用来利用微软服务器。现在,FlixOne.BookStore.ProductServiceFlixOne.BookStore.Web将通过安全通道进行对话。要建立安全通道,FlixOne.BookStore.Web需要信任客户端证书。这两种服务都托管在同一台机器上,并且都在 IIS Express 上运行。

要将您的计算机配置为信任 IIS Express SSL 证书,请执行以下步骤:

  1. 以管理员身份打开 PowerShell 命令窗口。
  2. 查询您的个人证书库,找到CN=localhost的证书指纹。下面的代码片段显示了这个的 PowerShell 脚本。在这里,我们通过在本地计算机的证书存储上存储指纹,向 IIS Express web 服务器添加一个证书:
PS C:windowssystem32> dir Cert:LocalMachine\My
Directory: Microsoft.PowerShell.SecurityCertificate::LocalMachineMy
Thumbprint Subject
---------- -------
C24798908DA71693C1053F42A462327543B38042 CN=localhost
  1. 接下来,将证书添加到受信任的根存储中。下面的代码将证书添加到本地计算机上的证书存储中:
PS C:windowssystem32> $cert = (get-item cert:LocalMachineMyC24798908DA71693C1053F42A462327543B38042)
PS C:windowssystem32> $store = (get-item cert:LocalmachineRoot)
PS C:windowssystem32> $store.Open("ReadWrite")
PS C:windowssystem32> $store.Add($cert)
PS C:windowssystem32> $store.Close()

前面的一组指令将向本地计算机的证书存储中添加一个客户端证书。

要使用 HTTPS,我们应该安装 SSL 证书。为了展示和测试这一点,我们在本地机器上生成并添加了一个证书。在下一节中,我们将运行演示应用。

运行两个应用

我们已经完成了所有那些繁琐的配置屏幕和关键替换。但是在你按下 F5 之前,先把ProductServiceFlixOne.BookStore.Web设置为启动项目。一旦你做到了这一点,你就可以安全地运行我们的应用,你会看到我们应用的登录页面。点击登录按钮,将被重定向至login.microsoftonline.com;这代表 Azure AD 登录。登录后,您将看到登录页面,如下所示:

当您登录到应用时,您将观察到网络流量和网址重定向。因此,研究身份令牌的详细交换,并获得一个访问令牌。如果您通过“产品列表”菜单浏览应用,您将能够访问“产品列表”屏幕,以及向“产品列表”添加项目。这是我们的ProductService微服务被调用的地方,也是它从FlixOne.BookStore.Web web 应用获得授权许可的地方。如果您浏览配置文件菜单,您将看到返回的标识令牌,以及您的名字、姓氏和电子邮件标识,这将显示 OpenID Connect 正在运行。

在这个例子中,我们使用 OAuth 和 OpenID Connect 来保护基于浏览器的用户界面、web 应用和微服务。如果我们在用户界面网络应用和微服务之间有一个应用编程接口网关,情况可能会不同。

要验证我们的功能,请打开ProductController,然后对[Authorize]属性进行注释,如下图所示:

在主屏幕中,单击产品列表。您将看到一条消息,提示您在访问产品列表之前应首先登录:

这表明我们不能访问我们的产品列表,直到我们登录或拥有特定的访问权限。

在这种情况下,我们需要在 web app 和 API 网关之间建立信任。我们还必须将标识令牌和访问令牌从网络应用传递到应用编程接口网关。这又将令牌传递给微服务;但是,在本章中讨论并实现它是不可行的。

为了利用微软 Azure 上的微软身份框架,通过使用 Azure 活动服务和 OpenID Connect,我们可以创建安全的应用。本节重点介绍并实现了 Azure 活动目录,使用了一个虚拟项目。接下来,我们将学习如何将其作为 API 网关进行管理。

将 Azure 应用编程接口管理作为应用编程接口网关进行管理

微服务实现中的另一个重要模式是前端的后端。这种模式是由山姆·纽曼引入并流行起来的。BFF 模式的实际实现是通过在各种类型的客户端和微服务之间引入一个 API 网关来完成的。

如下图所示:

蔚蓝色 API 管理(以下简称蔚蓝色 APIM 或刚好 APIM )正合适。它可以在中充当应用编程接口网关。基于. NET 的微服务实现。由于 Azure APIM 是云服务之一,它具有超强的可扩展性,可以很好地集成到 Azure 生态系统中。在本章中,我们将重点向您展示 Azure APIM 的功能。

蔚蓝 APIM 在逻辑上分为三个部分:

  • API 网关:API 网关基本上是客户端应用和服务之间的代理。它负责以下功能,这些功能主要由各种应用用来与微服务对话:
    • 接受应用编程接口调用,并将它们路由到后端
    • 验证应用编程接口密钥、JWTs 和证书
    • 支持通过 Azure AD 和 OAuth 2.0 访问令牌进行身份验证
    • 强制实施使用配额和费率限制
    • 动态转换您的应用编程接口,无需修改代码
    • 缓存设置它们的后端响应
    • 日志调用元数据进行分析
  • 发布者门户:这是一个管理界面,用来组织和发布一个 API 程序。它主要由微服务开发人员使用,以使微服务/应用编程接口对应用编程接口消费者或客户端应用可用。通过这一点,API 开发人员可以做到以下几点:
    • 定义或导入应用编程接口模式
    • 将原料药包装到产品中
    • 在应用接口上设置策略,如配额或转换
    • 从分析中获得见解
    • 管理用户
  • 开发人员门户:这是应用编程接口消费者的主要网络存在,他们可以在这里做以下事情:
    • 阅读应用编程接口文档
    • 通过交互式控制台尝试应用编程接口
    • 创建一个帐户并订阅它以获取 API 密钥
    • 访问分析以供自己使用

Azure APIM 提供了易于使用的用户界面和良好的文档。Azure API Management 还自带 REST API,因此 Azure APIM 门户的所有功能都可以通过 Azure REST API 端点以编程方式实现,该端点可用于 Azure APIM。

现在,让我们快速了解一下 Azure APIM 中的一些安全相关概念,以及它们如何在微服务中使用:

  • 产品:产品只是原料药的集合。它们还包含使用配额和使用条款。

  • 策略:策略是 API 管理的动态安全特性。它们允许发布者通过配置改变应用编程接口的行为。策略是根据应用编程接口的请求或响应按顺序执行的语句的集合。应用编程接口管理基本上是位于我们的微服务之间的代理,微服务托管在 Azure 和客户端应用中。由于它是一个中间层,它能够提供额外的服务。这些额外的服务是在一个名为策略的基于 XML 的声明性语法中定义的。蔚蓝 APIM 允许各种政策。事实上,您可以通过组合现有策略来构建自己的自定义策略。
    一些重要的限制政策如下:

    • 检查 HTTP 头:该策略检查 Azure APIM 接收的每个请求中是否存在特定的 HTTP 头或其值。
    • 按订阅限制呼叫速率:此策略根据特定服务被调用的次数,在每个订阅的基础上允许或拒绝对微服务的访问。
    • 限制主叫 IP:此策略指的是白盒 IP 地址,这样只有已知的 IP 才能访问服务。
    • 按套餐设置使用额度:此政策允许多次通话。它允许您在每次订阅的基础上强制实施可续订或终身的呼叫量和/或带宽配额。
    • 验证 JWT :此策略验证应用中用于身份验证的 JWT 参数。
  • 一些身份验证策略如下:

    • 使用基本进行身份验证:该策略帮助我们对传入的请求应用基本身份验证。
    • 使用客户端证书进行身份验证:该策略帮助我们使用客户端证书对 API 网关后面的服务进行身份验证。
  • 一些跨域策略如下:

    • 允许跨域通话:这个政策允许我们通过 Azure APIM 进行 CORS 请求。
    • CORS :这为端点或微服务增加了 CORS 支持,允许基于浏览器的网络应用进行跨域调用。
    • JSONP:JSONP 策略为一个端点或者整个微服务增加了 JSON 填充 ( JSONP )支持,允许 JavaScript web 应用跨域调用。
  • 一些转换策略如下:

    • 屏蔽内容中的 URL:此策略屏蔽响应中的 URL;它是通过蓝色 APIM 实现的。
    • 设置后端服务:该策略改变了传入请求的后端服务的行为。

本节重点介绍了作为我们应用的应用编程接口网关的 Azure 应用编程接口管理服务。我们还讨论了策略以及如何将它们应用于入站和出站请求。接下来,我们将讨论利率限制和配额政策。

费率限制和配额政策的示例

在上一节中,我们学习了什么是策略。现在,让我们看一个例子。以下是已应用于端点的配额策略之一:

<policies>
  <inbound>
    <!-- Change the quota to immediately see the effect-->
    <rate-limit calls="100" renewal-period="60">
    </rate-limit>
    <quota calls="200" renewal-period="604800">
    </quota>
    <base />
  </inbound>
  <outbound>
    <base/>
  </outbound>
</policies>

在本例中,我们限制来自单个用户的传入请求(inbound)。这意味着应用编程接口用户只能在60秒内进行100调用。如果他们试图在这段时间内打更多的电话,用户会收到一个错误的状态代码429,它基本上表示超过了速率限制。我们还为同一用户分配了一年内200通话的配额限制。这种节流行为是保护微服务免受不想要的请求甚至 DOS 攻击的好方法。

Azure APIM 还支持使用 OAuth 2.0 和 OpenID Connect 进行身份验证。在发布者门户中,您可以看到 OAuth 和 OpenID Connect 选项卡,这样您就可以配置提供者。

本节重点介绍了 OAuth 2.0、OpenID Connect、Azure API 管理服务及其策略。接下来,我们将讨论容器安全性。

了解集装箱安全

当我们使用容器时,容器安全性是一个非常重要的考虑因素。我们在应用中使用 Docker 容器。Docker 是行业应用容器化的一大部分。随着集装箱的广泛使用,显然我们需要在集装箱周围采取有效的安全措施。如果我们看一下容器的内部架构,它们非常接近主机操作系统内核。

Docker 在隔离方面坚持最小特权原则,减少了攻击面。尽管在这一领域取得了进步,但以下最佳实践将帮助您了解可以对容器采取的安全措施:

  • 确保用于微服务的所有容器映像都经过签名,并且源自受信任的注册表。
  • 强化主机环境、守护进程和映像。
  • 要访问设备,请遵循最小权限原则,不要提升访问权限。
  • 使用 Linux 中的控制组来监视资源,如内存、输入/输出和中央处理器。
  • 尽管容器存在的时间很短,但是记录所有的容器活动是明智的,对于后期分析来说理解这一点很重要。
  • 如果可能,集成容器加工工具,如 Aqua(http://www.aquasec.com)或 Twistlock(https://www.twistlock.com)。

当我们使用任何 web 应用时,安全性是我们最应该考虑的方面。本节讨论了在使用容器时,我们应该考虑的安全因素。除了容器安全之外,我们应该考虑其他安全实践,以确保我们的应用是安全的。在下一节中,我们将了解其他安全最佳实践。

其他安全最佳实践

微服务架构风格是新的,尽管围绕基础设施和编写安全代码的一些安全实践仍然适用。在本节中,我们将讨论其中的一些实践:

  • 库和框架的标准化:在开发过程中应该有一个引入新的库和框架或者工具的过程。这将在发现漏洞时减轻修补工作;它还将最小化由围绕开发的库或工具的特别实现引入的风险。
  • R 规则漏洞识别和缓解:使用行业标准漏洞扫描器,扫描源代码和二进制文件,应该是开发的常规部分。这些发现和观察结果应被视为功能缺陷。
  • 第三方审核和笔测试:外部审核和渗透测试练习非常有价值。进行这样的练习应该是一种常规做法。这在处理关键任务或敏感数据的应用中非常重要。
  • 日志记录和监控:日志记录是一种非常有用的检测和恢复攻击的技术。在微服务的情况下,具有聚合来自不同系统的日志的能力是必不可少的。一些工具,如河床 Azure、AppDynamics(Azure 的性能监控工具)和 Splunk Azure,在这个领域非常有用。
  • 防火墙:在网络边界有一个或多个防火墙,总是有好处的。应该正确配置防火墙规则。
  • 网络隔离:在单片的情况下,网络划分受到约束和限制。但是,对于微服务,我们需要在逻辑上创建不同的网段和子网。当涉及到保持和开发额外的安全措施时,基于微服务交互模式的分段可能非常有效。

本节描述了一些其他的安全最佳实践。除了容器安全之外,我们应该考虑所有的安全实践,以确保我们的应用是安全的。

摘要

微服务架构风格是按设计分布的,在保护有价值的关键业务系统时,它为我们提供了更好的选择。传统。基于. NET 的身份验证和授权技术是不够的,它们不能应用于微服务领域。我们也看到了为什么安全的基于令牌的方法,比如 OAuth 2.0 和 OpenID Connect 1.0,正在成为微服务授权和认证的具体标准。

Azure AD 可以很好的支持 OAuth 2.0 和 OpenID Connect 1.0。Azure API Management 还可以充当微服务实现中的 API 网关,它提供了俏皮的安全特性,比如策略。

Azure AD 和 Azure API Management 提供了相当多的强大功能,因此我们可以监视和记录收到的请求。这非常有用,不仅对于安全性,而且对于跟踪和故障排除场景。

在下一章中,我们将关注日志记录、监控以及围绕微服务故障排除的整体检测。

问题

  1. 什么是软件安全?
  2. 单片应用的安全挑战是什么?
  3. 什么是 OAuth,你会如何使用它?
  4. 什么是授权服务器,它是如何工作的?
  5. 什么是 Azure API 管理,为什么我们需要一个用于微服务的 API 网关?

进一步阅读

七、监控微服务

当一个系统出现问题时,利益相关者会想知道发生了什么,为什么会发生,你能给出的关于如何解决的任何提示或线索,以及如何防止同样的问题在未来再次发生。这是监控的主要用途之一;然而,监控还可以做得更多。

英寸 NET 单片,有多种监控解决方案可供选择。监控目标始终是集中的,监控当然很容易设置和配置。如果有什么东西坏了,我们知道该找什么,去哪里找,因为一个系统中只有有限数量的组件,它们的寿命相当长。

然而,微服务是分布式系统,从本质上来说,它们比单片更复杂,因此在微服务生产环境中,资源利用率以及运行状况和性能监控非常重要。我们可以使用这些诊断信息来检测和纠正问题,并发现潜在的问题,然后防止它们发生。监控微服务带来了不同的挑战。

在本章中,我们将讨论以下主题:

  • 监测的必要性
  • 微服务中的监控和日志挑战
  • 监测战略
  • 中微服务的可用工具和策略.NET 监控空间
  • Azure 诊断和应用洞察的使用
  • ELK 堆栈和 Splunk 的简要概述

监控到底是什么意思?监测没有正式的定义;但是,以下内容是合适的:

"Monitoring provides information around the behavior of an entire system, or different parts of a system in their operational environment. This information can be used for diagnosing and gaining insight into the different characteristics of a system."

技术要求

本章包含各种代码示例来解释它所描述的概念。代码保持简单,只是为了演示。

要运行和执行代码,您需要以下先决条件:

  • Visual Studio 2019
  • 。网络核心 3.1
  • 有效的 Azure 帐户

要安装和运行这些代码示例,您需要安装 Visual Studio 2019(首选的集成开发环境)。为此,请遵循以下步骤:

  1. https://docs . Microsoft . com/en-us/Visual Studio/install/install-Visual Studio下载 Visual Studio 2019(社区免费)。
  2. Visual Studio 有多个版本。按照操作系统的安装说明进行操作。我们使用的是视窗操作系统。
  3. 不要忘记安装 Azure SDK(从“工作负载”选项卡中选择 Azure 开发):

正在设置。网络核心 3.1

如果你没有.NET Core 3.1 安装完毕,可以从https://dotnet.microsoft.com/download/dotnet-core/3.1下载。

有效的 Azure 帐户

您将需要 Azure 门户的登录凭据。如果你没有 Azure 账号,可以在https://azure.microsoft.com/free/免费创建一个。

The complete source code is available at https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2007.

从仪器和遥测技术开始

监控解决方案依赖于仪器和遥测技术。因此,当我们谈论监控微服务时,我们也自然会讨论仪器和遥测数据。应用日志只不过是一种检测机制。

There are two things related to monitoring that we may need to consider, while implementing this in a production-based application:

Information overload: It is easy to go all-out and collect a lot of information, but you should make sure that the most important information is the most accessible information. One example is a Grafana-type (https://grafana.com/docs/features/panels/graph/) dashboard to verify things. It provides a value for operations, whereas detailed logging may help with forensics and advanced troubleshooting.

Alerting fatigue: Don't set up alerts for everything—this just means that people will start ignoring the alerts. This is why, sometimes, too much information would be overkill.

这些日志只能帮助我们看到手头任务的步骤,但我们只能通过完整的监控系统捕获应用中发生的每个资源和操作的细节。

在接下来的章节中,我们将更详细地了解仪器和遥测技术。

使用仪器

现在,让我们看看什么是仪器仪表。仪器是我们向应用添加诊断功能的方法之一。它可以正式定义如下:

"Most applications will include diagnostic features that generate custom monitoring and debugging information, especially when an error occurs. This is referred to as instrumentation and is usually implemented by adding event and error handling code to the application."  
                                                                                                                                                                                                                  – MSDN

在正常情况下,可能不需要来自信息事件的数据,从而降低存储成本和收集数据所需的事务。但是,当应用出现问题时,您必须更新应用配置,以便诊断和检测系统可以收集事件数据。该事件数据可能是信息性的、错误消息和/或警告消息,有助于修复系统故障。

遥感勘测

最基本的遥测技术是收集仪器和测井系统产生的信息的过程。通常,它使用支持大规模扩展和应用服务广泛分布的异步机制来执行。它可以定义如下:

"The process of gathering remote information that is collected by instrumentation is usually referred to as telemetry."
                                                                                                                                                                                        – MSDN

通常,高度复杂系统的信息是以这样一种方式存储的,即当需要对其进行分析时,这些信息很容易获得。使用这些信息可以了解系统的性能、变化检测或任何故障检测。

Azure 云中没有可以提供遥测和报告系统的内置支持/系统。然而,我们可以在 Azure 诊断和应用洞察的帮助下获得这样的系统。Azure 应用洞察允许我们收集遥测数据和监控机制。

遥测技术提供数据,以便我们能够分析手头的信息,并纠正系统中的故障或分析变化。

这部分是关于遥测技术的。我们还可以使用监控来检测系统中的变化。我们将在下一节讨论这个问题。

监测的必要性

微服务是复杂的分布式系统。微服务实施是任何现代信息技术业务的支柱。理解服务的内部,以及它们的交互和行为,将帮助您使整个业务更加灵活和敏捷。微服务的性能、可用性、规模和安全性会直接影响企业及其收入,因此监控微服务至关重要。它帮助我们观察和管理服务属性的质量。

让我们讨论需要它的场景。

健康监测

通过运行状况监控,我们以一定的频率监控系统及其各种组件的运行状况,通常是几秒钟。这确保了系统及其组件按预期运行。借助详尽的运行状况监控系统,我们可以监控整个系统的运行状况,包括中央处理器、内存利用率等。这可以是 pings 或广泛的健康监控端点的形式,这些端点发出服务的健康状态,以及来自特定时间点的一些有用的元数据。

健康监测的指标基于成功率或失败率的阈值。如果参数值超出配置的阈值,则会触发警报。很有可能会因为这个故障而触发一些维护系统健康的预防措施。例如,此操作可能是在故障状态下重新启动服务,或者调配服务器资源。

可用性监控

可用性监控与健康状态监控非常相似。然而,微妙的区别在于,在可用性监控中,重点是系统的可用性,而不是当时的运行状况快照。

系统的可用性取决于各种因素,例如应用、服务和服务依赖关系的整体性质和领域,以及基础架构或环境。可用性监控系统捕获与这些因素相关的低级数据点,并由系统表示它们,从而使业务级功能可用。可用性监控参数通常用于跟踪业务指标和服务级别协议 ( 服务级别协议)。

服务水平协议监控

具有服务级别协议的系统基本上保证了某些特性,例如性能和可用性。对于基于云的服务,这是一个非常常见的场景。本质上,服务级别协议监控就是监控系统的那些有保证的服务级别协议。服务水平协议监控是作为服务提供商和消费者之间的合同义务来实施的。

它通常基于可用性、响应时间和吞吐量来定义。SLA 监控所需的数据点可以来自性能端点监控或日志记录,以及监控参数的可用性。对于内部应用,许多组织跟踪因服务器停机而引发的事件数量。基于这些事件的根本原因分析 ( RCA )采取的措施降低了重复这些问题的风险,并且有助于满足服务级别协议。

出于内部目的,组织还可以跟踪导致服务失败的事件的数量和性质。了解如何快速解决这些问题,或者如何完全消除这些问题,有助于减少停机时间并满足服务级别协议。

性能监控

系统的性能通常由关键的性能指标来衡量。大型网络系统的一些关键性能指标如下:

  • 每小时服务的请求数
  • 每小时服务的并发用户数
  • 用户执行业务事务(例如下订单)所需的平均处理时间

此外,性能还通过系统级参数来衡量,例如:

  • 中央处理器利用率
  • 内存利用率
  • 输入输出速率
  • 排队的消息数

如果系统不满足这些关键性能指标中的任何一个,就会发出警报。

在分析性能问题时,通常会使用监控系统以前捕获的基准测试的历史数据来解决问题。

安全监控

监控系统可以检测异常的数据模式请求、异常的资源消耗模式以及对系统的攻击。特别是在拒绝服务的情况下,可以预先识别攻击或注入攻击,并向团队发出警报。安全监控还保留经过身份验证的用户的审计跟踪,并保留已签入和签出系统的用户的历史记录。它还可以方便地满足合规性要求。

安全性是分布式系统(包括微服务)的一个交叉问题,因此在系统中有多种方式生成这些数据。安全监控可以从不属于系统的各种工具中获取数据,但它们可能是托管系统的基础架构或环境的一部分。不同类型的日志和数据库条目可以作为数据源;然而,这确实取决于系统的性质。

审核敏感数据和关键业务交易

出于法律义务或合规原因,系统可能需要保留系统中用户活动的审计跟踪,并且可能需要记录他们的所有数据访问和修改。由于审计信息本质上是高度敏感的,因此可能只向系统中少数享有特权和信任的个人披露。审计跟踪可以是安全子系统的一部分,也可以单独记录。

Azure is certified for many regulatory specs. Sometimes, it still requires you to do additional work, and you should understand what you are trying to be compliant with. However, this becomes easier when Azure has something to build it on, than if you were to build your solution from scratch.

您可能需要按照法规或合规性规范的规定,以特定的格式传输和存储审计线索。

最终用户监控

在终端用户监控中,系统功能的使用情况和/或终端用户的总体系统使用情况被跟踪和记录。使用情况监控可以使用各种用户跟踪参数来完成,例如使用的功能、为指定用户完成关键事务所需的时间,甚至是强制的配额。强制配额是针对系统使用情况对最终用户施加的约束或限制。一般来说,各种现收现付服务使用强制配额,例如,免费试用,您可以上传大小高达 25 MB 的文件。此类监控的数据源通常是根据日志和跟踪用户行为收集的。

排除系统故障

系统的最终用户可能会遇到系统故障。这可能是系统故障,也可能是用户无法执行某项活动。使用系统日志监控这类问题;如果没有,最终用户将需要提供详细的信息报告。此外,有时,服务器崩溃转储或内存转储会非常有帮助。然而,在分布式系统的情况下,理解故障的确切根源会有点困难。

在许多监控场景中,仅使用一种监控技术是无效的。最好使用多种监控技术和工具进行诊断。特别是,监控分布式系统非常具有挑战性,它需要来自各种来源的数据。除了适当地分析情况和决定行动要点之外,我们还必须考虑整体的监测观点,而不是只从一种类型的系统角度来看。

既然我们对通用监控需要做些什么有了更好的了解,让我们重新审视一下微服务的视角。在下一节中,我们将讨论微服务架构风格带来的不同监控挑战。

了解监控挑战

微服务监控提出了不同的挑战。在某些情况下,一个服务可能依赖于另一个服务,或者客户端向一个服务发送请求,而响应来自另一个服务,这会使操作变得复杂。在这里,扩展微服务将是一项具有挑战性的任务。同样,流程实现,比如说 DevOps,将是一项具有挑战性的工作,同时实现一个巨大的企业微服务应用。我们将在本节中讨论这些挑战。

缩放问题

一种服务可能依赖于其他各种微服务提供的功能。这导致了复杂性,这在.NET 整体系统。检测所有这些依赖关系相当困难。伴随规模而来的另一个问题是变化率。随着持续部署和基于容器的微服务的发展,代码始终处于可部署状态。容器只能存活几分钟,甚至几秒钟。

It is worth noting that containers can indeed be short-lived, so this issue doesn't always apply in the case of virtual machines (VMs). Apart from the fact that it usually takes a couple of minutes just to spin up a VM, it is generally longer lived than containers.

虚拟机也是如此(但并不总是如此)。虚拟机的寿命约为几分钟到几小时。

在这种情况下,测量常规信号,如每分钟的 CPU 使用率和内存消耗使用率,是没有意义的。有时,容器实例可能一分钟都不存在;一分钟之内,容器实例可能已经被处理掉了。这是微服务监控的挑战之一。

DevOps 心态

传统上,服务或系统一旦部署,就归运营团队所有和管理。然而,DevOps 打破了开发人员和运营团队之间的孤岛。它伴随着许多实践,例如持续集成和持续交付,以及持续监控。随着这些新的实践,出现了新的工具集。

然而,DevOps 不仅仅是一套实践或工具;更重要的是,这是一种心态。改变人们的心态总是一个艰难而缓慢的过程。微服务监控需要这样的心态转变。

随着服务自治的出现,开发团队现在不得不拥有服务。这也意味着他们必须解决开发问题,并关注服务的所有操作参数和服务级别协议。仅仅通过使用最先进的监控工具,开发团队不会在一夜之间改变。运营团队也是如此。他们不会在一夜之间突然变成核心平台团队(或者你喜欢的任何花哨名字)。

为了使微服务对组织、开发人员和运营部门来说成功且有意义,团队需要相互帮助,了解各自的痛点,并朝同一个方向思考——也就是说,他们如何共同为业务带来价值。没有服务的规范,监控就不可能发生,这是开发团队可以提供帮助的地方。同样,没有运营团队的帮助,警报和运营指标的设置也不会发生。这是交付微服务监控解决方案的挑战之一。

数据流可视化

市场上有许多用于数据流可视化的工具。其中有 AppDynamics、New Relic 等等。这些工具能够处理数十甚至数百个微服务的可视化。然而,在更大的环境中,有成千上万的微服务,这些工具无法处理可视化。这是微服务监控的挑战之一。

测试监控工具

我们信任监控工具,并理解它们描述了我们的微服务实现的全貌。然而,为了确保他们保持这种理解,我们将不得不测试监控工具。这在整体实现中从来不是一个挑战;但是,当涉及到微服务时,出于监控目的,需要对微服务进行可视化。这意味着我们必须花时间生成假的/合成的交易,然后我们利用整个基础设施,而不仅仅是为客户服务。

因此,测试监控工具是一件昂贵的事情,并且它在微服务监控中提出了重大挑战。在下一节中,我们将讨论监控策略。

致力于监测战略

在这一节中,我们将了解使微服务可见的监控策略。通常实施以下(以及其他)策略来创建定义明确的整体监控解决方案,以便我们可以监控系统、修复各种故障等。

应用/系统监控

应用/系统监控也称为基于框架的策略。在这里,应用,或者在我们的例子中,微服务,本身在给定的执行上下文中生成监控信息。可以基于应用数据中的阈值或触发点动态配置应用,这可以生成跟踪语句。也可以有一个基于探针的框架(例如.NET CLR,它提供了收集更多信息的钩子)来生成监控数据。因此,可以将有效的检测点嵌入到应用中,以便于这种监控。除此之外,承载微服务的底层基础设施也可能引发关键事件。监控代理可以监听和记录这些事件,监控代理与应用位于同一台主机上。

真实用户监控

真实用户监控基于真实终端用户在系统中的事务流。当最终用户实时使用系统时,该策略可用于捕获与响应时间和延迟相关的参数,以及用户遇到的错误数量。

这对于特定的故障排除和问题解决非常有用。通过这种策略,系统的热点和服务交互瓶颈也可以被捕获。可以记录整个端到端的用户流或事务,以便我们可以在以后重放它。这样做的好处是,这类录制的播放可以用于故障排除以及各种测试目的。

语义监控和合成交易

语义监控策略侧重于业务事务;但是,它是通过使用合成事务来实现的。顾名思义,在语义监控中,我们试图模拟最终用户流。然而,这是以一种受控的方式并使用虚拟数据来完成的,因此我们可以区分流的输出和实际的最终用户流数据。这种策略通常用于服务依赖、运行状况检查和诊断系统中出现的问题。为了实现合成事务,我们需要在计划流程时小心谨慎。我们还需要足够小心,不要让系统不堪重负——例如,在系统中传播的整个交易中,为假冒产品目录创建假冒订单,并观察响应时间和输出。

压型

概要分析方法特别侧重于解决整个系统的性能瓶颈。这种方法不同于前面的方法。真实和语义监控侧重于业务事务,或者系统的功能方面,它收集相关的数据。概要分析是关于系统级或低级信息捕获的。其中一些参数是响应时间、内存和线程。

这种方法在应用代码或框架中使用探测技术,并收集数据。通过利用分析过程中捕获的数据点,相关的 DevOps 团队可以确定性能问题的原因。在生产环境中应避免使用探测进行分析。然而,它非常适合生成调用时间等等,而不会在运行时使系统过载。一般来说,概要分析的一个很好的例子是一个用 ASP.NET 迷你概要分析器,甚至是用窥镜分析的 ASP.NET MVC 应用。

端点监控

通过这种方法,我们公开了服务的一个或多个端点,以发出与服务本身以及基础设施参数相关的诊断信息。通常,不同的端点专注于提供不同的信息。例如,一个端点可以给出服务的健康状态,而另一个端点可以提供该服务执行中出现的 HTTP 500 错误信息。对于微服务来说,这是一种非常有用的技术,因为它从本质上改变了从推送模型到拉取模型的监控,并且减少了服务监控的开销。我们可以在一定的时间间隔内废弃/丢弃来自这些端点的数据,然后构建一个仪表板并为运营指标收集数据。

An important point about endpoint monitoring is to test where your users are. If most of the end users are in Asia, there is less value in testing from North America than testing a bit closer to home. This, of course, brings in the fact that endpoint monitoring can be both for general "everything works" checks, and also things such as latency and response times.

使用各种监控策略可以让我们监控系统,从而修复任何故障,分析各种信息数据,等等。当我们分析系统时,日志会增加更多的价值。在下一节中,我们将解释日志的概念。

了解日志记录

日志是一种由系统、其各种组件或基础设施层提供的工具。日志是收集与系统相关的特定或相关数据的一种方式。这些数据也被称为日志。日志可以是任何类型,也可以根据要求而定,例如信息日志、错误日志和警告日志。有时,日志可能是自定义日志。借助这些日志,我们可以分析系统执行的任何任务的故障、崩溃或进程。这种分析有助于我们解决系统中的问题。

在本节中,我们将了解日志记录的挑战,然后讨论一些策略来解决这些挑战。

日志挑战

首先,我们将尝试理解微服务中日志管理的问题。其中一些如下:

  • 为了记录与系统事件和参数以及基础设施状态相关的信息,我们需要保存日志文件。传统意义上.NET 单片,日志文件保存在应用部署的同一台机器上。在微服务的情况下,它们托管在虚拟机或容器上。然而,虚拟机和容器都是短暂的,这意味着它们不会保持状态。在这种情况下,如果我们用虚拟机或容器保存日志文件,我们将丢失它们。这是微服务中日志管理的挑战之一。
  • 在微服务架构中,有许多服务构成了一个事务。假设我们有一个下单交易,其中服务 A、服务 B 和服务 C 参与交易。比如说,如果服务 B 在事务期间失败了,我们如何在日志中理解和捕获这个失败?不仅如此,更重要的是,我们如何理解服务 B 的一个特定实例已经失败,并且它正在参与事务?这种情况给微服务带来了另一个挑战。

既然我们已经知道了这些挑战,让我们继续学习一些关于日志记录的策略。

日志策略

到目前为止,在本节中,我们已经讨论了日志记录、它的挑战以及为什么我们应该实现日志记录。同时进行多个调用是可能的,因此当我们实现日志记录时,我们应该以这样一种方式来实现它,即我们知道所记录事务的确切来源。我们将使用关联标识进行记录。

Logging is not only used in microservices specifically; it is also important for monolithic applications.

为了在微服务中实现日志记录,我们可以使用我们将在下面几节中讨论的日志记录策略。

集中式日志记录

集中日志记录和集中监控是有区别的。在集中式日志记录中,我们记录系统中发生的事件的所有详细信息—它们可能是错误或警告,或者只是为了提供信息—而在集中式监视中,我们监视关键参数,即特定信息。

通过日志,我们可以了解系统中或特定事务中实际发生了什么。我们会有具体交易的所有细节,比如为什么开始,是谁触发的,记录了什么样的数据或者资源等等。在复杂的分布式系统中,例如微服务,这确实是我们可以用来解决信息流或错误的整个难题的关键信息。我们还需要将超时、异常和错误视为需要记录的事件。

我们记录的关于特定事件的信息也应该是结构化的,并且这个结构应该在我们的系统中是一致的。在这里,我们会有以下内容:

  • 我们的结构化日志条目可以包含基于级别的信息,以说明该日志条目是用于信息、错误,还是作为日志条目事件记录的调试信息或统计信息。
  • 结构化日志条目还必须有日期和时间,以便我们知道事件发生的时间。我们还应该在结构化日志中包含主机名,这样我们就可以确切地知道日志条目来自哪里。
  • 我们还应该包括服务名称和服务实例,这样我们就可以确切地知道哪个微服务创建了日志条目。
  • 最后,我们还应该在结构化日志格式中包含一条消息,这是与事件相关的关键信息。

例如,对于错误,这可能是调用堆栈或关于异常的详细信息。关键是我们保持结构化日志格式的一致性。一致的格式将允许我们查询日志信息。然后,通过使用我们的集中式日志工具,我们基本上可以搜索特定的模式和问题。在微服务架构中,集中式日志记录的另一个关键方面是使分布式事务更具可追溯性。

在日志记录中使用关联标识

关联标识是分配给每个事务的唯一标识。因此,当一个事务分布在多个服务中时,我们可以通过使用日志信息跨不同的服务跟踪该事务。关联 ID 基本上是从服务传递到服务的。处理该特定事务的所有服务都将接收相关标识,并将其传递给下一个服务,以此类推,这样它们就可以将与该事务相关的任何事件记录到我们的集中日志中。这极大地帮助了我们,当我们必须可视化和理解这个事务在不同的微服务中发生了什么。

语义日志

【Windows 的事件跟踪 ( ETW )是一种结构化的日志机制,可以在日志条目中存储结构化的负载。该信息由事件侦听器生成,它可能包括关于事件的类型化元数据。这只是语义日志的一个例子。语义日志传递额外的数据以及日志条目,以便处理系统可以获得围绕事件构建的上下文。这就是为什么语义日志也被称为结构化日志或类型化日志。

例如,指示已下订单的事件可以生成一个日志条目,该日志条目包含作为整数值的项目数、作为十进制数的总值、作为长数值的客户标识符以及作为字符串值的交货城市。订单监控系统可以读取有效负载,并可以轻松提取单个值。ETW 是 Windows 附带的标准功能。

在 Azure 云中,可以从 ETW 获得日志数据源。语义日志应用块是最好的框架之一,使全面的日志记录更加容易。它允许您将事件写入您选择的目的地,如磁盘文件、数据库、电子邮件等。语义日志应用块也可以在 Azure 应用中使用。

本节重点介绍如何理解日志记录,以及日志记录和日志记录策略面临的挑战。我们还强调了语义日志的重要性。

当涉及到日志记录时,监控很重要。在下一节中,我们将讨论 Azure 云中的监控。

Azure 上的监控

Azure 中没有单一的现成解决方案或产品,也没有任何云提供商来应对微服务带来的监控挑战。有趣的是,没有太多的开源工具可以使用。基于. NET 的微服务。

我们正在利用微软 Azure 云和云服务来构建我们的微服务,因此寻找它附带的监控功能非常有用。如果您希望管理大约几百个微服务,您可以利用基于微软 Azure 解决方案的定制监控解决方案(主要是交织 PowerShell 脚本)。

我们将主要关注以下日志记录和监控解决方案:

  • 微软 Azure 诊断:这有助于通过资源和活动日志收集和分析资源。
  • 应用洞察:这有助于收集我们微服务的所有遥测数据,并且有助于分析数据。这是一种基于框架的监控方法。
  • 日志分析:日志分析分析和显示数据,并对收集的日志提供可扩展的查询能力。

让我们从不同的角度来看这些解决方案。这个视角将帮助我们可视化我们基于 Azure 的微服务监控解决方案。微服务由以下内容组成:

  • 基础设施层:虚拟机或应用容器(例如 Docker 容器)
  • 应用堆栈层:由操作系统、。和微服务应用代码

这些层组件中的每一个都可以按如下方式进行监控:

  • 虚拟机:使用 Azure 诊断日志
  • Docker 容器:使用容器日志和应用洞察,或第三方容器监控解决方案,如 cAdvisor、Prometheus 或 Sensu
  • Windows 操作系统:使用 Azure 诊断日志和活动日志
  • 微服务应用:使用应用洞察
  • 数据可视化和度量监控:使用日志分析或第三方解决方案,如 Splunk 或 ELK 堆栈

各种 Azure 服务的日志条目中都有一个活动标识。此活动标识是为每个请求分配的唯一 GUID,可以在日志分析期间用作关联标识。

让我们继续学习 Azure 诊断。

微软 Azure 诊断

Azure 诊断日志使我们能够为已部署的微服务收集诊断数据。我们还可以使用诊断扩展从各种来源收集数据。网络和工作者角色、Azure 虚拟机和 Azure 应用服务支持 Azure 诊断。其他 Azure 服务有自己独立的诊断工具。

启用 Azure 诊断日志和探索各种设置很容易,并且可以作为切换开关使用。

Azure 诊断程序可以从以下来源收集数据:

  • 性能计数器
  • 应用日志
  • Windows 事件日志
  • .NET 事件源
  • IIS 日志
  • 基于清单的 ETWs
  • 崩溃转储
  • 自定义错误日志
  • Azure 诊断基础架构日志

让我们继续,看看 Azure 存储。

使用 Azure 存储存储诊断数据

Azure 诊断日志不会永久存储。它们是滚动日志,也就是说,它们会被更新的日志覆盖。所以,如果我们想在任何分析工作中使用它们,我们必须存储它们。Azure 诊断日志可以存储在文件系统中,也可以通过文件传输协议传输;更好的是,它们可以存储在 Azure 存储容器中。

有不同的方法可以为指定的 Azure 资源(在我们的例子中,是 Azure 应用服务上托管的微服务)指定用于诊断数据的 Azure 存储容器。这些措施如下:

  • 命令行界面工具
  • 管理员
  • Azure 资源管理器
  • Visual Studio 2019
  • Azure 门户–我们可以从 Azure 门户直接创建 Azure 存储容器

在下一节中,我们将学习如何从 Azure 门户使用和创建 Azure 存储容器。

使用 Azure 门户

下面的截图描述了我们通过 Azure 门户调配的 Azure 存储容器:

定义 Azure 存储帐户

我们还可以使用 Azure 存储帐户存储诊断数据。为此,我们需要定义一个存储帐户,通过使用 Azure 门户(http://portal.azure.com/,我们还可以在ServiceConfiguration.cscfg文件中定义存储帐户。这很方便,因为在开发期间,您可以指定存储帐户。还可以在开发和生产过程中指定不同的存储帐户。

The Azure storage account can also be configured as one of the dynamic environment variables during the deployment process.

Azure 存储帐户中的信息可以作为配置设置添加,可以通过从配置设置中获取信息来使用。例如,我们使用的是 Visual Studio。以下代码显示了新微服务项目的默认连接字符串:

<ConfigurationSettings>
  <Setting name="Microsoft.WindowsAzure.Plugins.
  Diagnostics.ConnectionString" value="UseDevelopmentStorage=true" />
</span></ConfigurationSettings>

前面的连接字符串可能对您有所不同,或者可能需要更改以匹配您的 Azure 存储帐户的信息。

现在,让我们了解一下 Azure 存储是如何存储诊断数据的。所有日志条目都存储在 blob 或表存储容器中。可以在创建和关联 Azure 存储容器时指定要使用的存储。

诊断数据的 Azure 存储模式

对于存储在表中的数据,用于存储诊断数据的 Azure 表存储的结构如下:

  • WadLogsTable:该表通过使用跟踪侦听器存储代码执行期间编写的日志语句。
  • WADDiagnosticInfrastructureLogsTable:该表指定了诊断监视器和配置更改。
  • WADDirectoriesTable:这个表包含了正在被监控的文件夹,它包含了各种相关的信息——例如,IIS 日志,IIS 失败的请求日志,等等。

To get the location of this blob log file, you need to check the container field, the RelativePath field, or the AbsolutePath field, where the RelativePath field contains the name of the blob, and the AbsolutePath field contains both the location and the name.

  • WADPerformanceCountersTable:此表包含与配置的性能计数器相关的数据。
  • WADWindowsEventLogsTable:此表包含 Windows 事件跟踪日志条目。

对于 blob 存储容器,诊断存储模式如下:

  • wad-control-container:如果你还在用遗留代码或者以前的版本,比如 SDK 2.4 和以前的版本,那么这个就是给你的。它通过使用 XML 配置文件来控制诊断。
  • wad-iis-failedreqlogfiles:这包含来自 IIS 失败请求日志的信息。
  • wad-iis-logfiles:包含 IIS 日志的信息。
  • custom:有时候,我们需要一个定制的 Azure 存储容器。这些容器是为被监视的目录配置的。WADDirectoriesTable包含该自定义容器的名称。

这里需要注意的一个有趣的事实是,可以在这些容器表或 blobs 上看到的 WAD 后缀来自微软 Azure Diagnostics 以前的产品名称,即 Windows Azure Diagnostics。

A quick way to view the diagnostic data is with the help of Cloud Explorer. Cloud Explorer is available from Visual Studio (if you have installed the SDK).

本节描述了如何在创建和关联 Azure 存储容器时指定存储选择。我们学习了如何创建存储帐户,以及如何捕获数据。在下一节中,我们将讨论应用洞察。

应用洞察简介

Application Insights 是微软提供的一款应用性能管理 ( APM )产品。它是 Azure 监视器服务的一个功能集。这是一项用于监控性能的有用服务。基于. NET 的微服务。这有助于理解单个微服务的内部操作行为。它将调整服务性能并了解微服务的性能特征,而不是仅仅关注于检测和诊断问题。

这是基于框架的监测方法的一个例子。这意味着,在微服务的开发过程中,我们将把 Application Insights 包添加到我们微服务的 Visual Studio 解决方案中。这就是 Application Insights 如何为遥测数据测量您的微服务。这可能并不总是每个微服务的理想方法。然而,如果您没有仔细考虑监控您的微服务,它就派上了用场。这样,您的服务就可以开箱即用地进行监控。

借助应用洞察,您可以收集和分析以下类型的遥测数据:

  • HTTP 请求率、响应时间和成功率
  • 依赖(HTTP 和 SQL)调用率、响应时间和成功率
  • 来自服务器和客户端的异常跟踪
  • 诊断日志跟踪
  • 页面视图计数、用户和会话计数、浏览器加载时间和异常
  • AJAX 调用率、响应时间和成功率
  • 服务器性能计数器
  • 自定义客户端和服务器遥测
  • 按客户端位置、浏览器版本、操作系统版本、服务器实例、自定义维度等进行细分
  • 可用性测试

除了上述类型之外,还有相关的诊断和分析工具,可用于使用各种不同的可定制指标进行警报和监控。借助自己的查询语言和可定制的仪表盘,Application Insights 为微服务提供了一个很好的监控解决方案。

接下来,我们将在现有的 FlixOne 应用中实现 Application Insights。

监控我们的 FlixOne 应用

在前一节中,我们讨论了监控遥测技术,它为我们提供了可以分析的数据。它帮助我们监控解决方案的运行状况。在本节中,我们将向 FlixOne 应用中添加应用洞察。

For this code example, you'll need to have a valid Azure account. Refer to the previous Technical requirements section for a list of prerequisites.

要为我们的应用实现 Application Insights,我们需要一个有效的插装密钥。要获得这个密钥,我们需要设置应用洞察资源。按照以下步骤创建资源:

  1. 使用您的凭据登录 Azure 门户。
  2. 搜索应用洞察,然后从搜索结果列表中单击应用洞察:

  1. 在应用洞察屏幕中,单击创建应用洞察应用,如下图所示:

  1. 现在,提供所有必需的值,然后单击“查看+创建”,如下所示:

在这里,我们创建了一个名为 FlixOne 的新资源组,并为(美国)东部美国地区提供了一个实例名,即 FlixOneWeb。

  1. 在下一个屏幕上,查看您的输入,然后单击创建,如下图所示:

For automation purposes, you can also download a template of your Application Insights. We've created a template already (template.zip). It can be found in the Chapter07 folder of this book's code repository.

  1. 成功创建应用洞察实例后,您需要单击转到资源。之后,您应该会看到以下屏幕:

  1. 为了开始我们的代码示例,我们使用 Visual Studio 2019 创建了一个 ASP.NET Core web 应用。我们将跳过创建应用的步骤,因为我们在上一章中讨论了这些步骤。
  2. 从项目|添加应用洞察遥测将应用洞察软件开发工具包添加到您的项目,然后将遥测添加到 FlixOne。书店。网络项目:

  1. 现在,您将看到“入门”页面;单击开始:

  1. 通过提供正确的值并单击注册,向应用洞察注册您的应用:

  1. 您应该会看到以下进度屏幕:

  1. 以下是完成配置过程后的最终屏幕:

  1. 现在,从 Visual Studio 打开解决方案资源管理器。您会注意到一个新的包和一些连接的服务已经添加到项目中:

  1. 现在,运行应用,然后单击默认页面(请注意,ASP.NET Core 网络应用模板中的所有页面都是默认页面)。这是 Visual Studio 中的“诊断工具”窗口:

在这里,当我们与应用交互时,您将从网络应用中看到事件触发器。要从 Visual Studio 中查看遥测数据,请打开解决方案资源管理器,右键单击连接的服务下的应用洞察,然后单击搜索实时遥测,如下图所示:

从这里,您可以查看遥测数据并对其执行分析任务:

要查看 Azure 门户中的遥测数据,请单击“连接的服务”下的“从解决方案资源管理器打开应用洞察门户”(与我们在上一步中所做的相同)。这将为我们的FlixOneWeb打开 Azure 门户和应用洞察。您将看到过去一小时的图形数据,如下图所示:

需要注意的是,Visual Studio 向您的 web 应用添加了一些东西(例如包和连接的服务)。仪器键也被添加到appsettings.json文件中。如果打开此文件,您将在应用洞察中看到一个新条目:

您还将在Startup.cs文件中看到一个新条目,如下图所示:

It is recommended that you should store a production-based application instrumentation key in an environment variable.

您还可以在 Azure 门户上创建自定义仪表板。这是我们创建的:

从 Azure 门户(使用查询资源管理器),我们可以查询我们的洞察数据,以便获得用于分析目的的数据。

Query Explorer uses the Kusto query language to retrieve the log data. A Kusto query is a read-only request that's used to process data and return results. You can find out more by reading the official documentation at: https://docs.microsoft.com/en-us/azure/kusto/query/.

通过使用以下查询,我们可以检查我们的FlixOne.BookStore.Web应用上的请求:

requests
| limit 5

前面的查询提供了以下输出:

此外,我们可以通过使用各种图表来可视化数据结果。在这里,我们根据前面的数据创建了一个饼图:

我们可以做进一步的分析,并根据数据创建报告。为了使这个例子更简单,我们可以进行一些小的查询来获取数据。Application Insights 帮助我们收集遥测数据,并将其用于分析和报告。

本节重点介绍了应用洞察的特性。除了监控应用的解决方案,我们还看到了更多的监控解决方案,我们将在下一节中讨论。请写下检测密钥,因为我们将在应用中使用它。

其他微服务监控解决方案

现在,让我们看看一些流行的监控解决方案,我们可以使用它们来构建定制的微服务监控解决方案,以监控应用。显然,这些解决方案不是开箱即用的;然而,它们肯定是经过开源社区时间考验的,并且可以很容易地与。基于. NET 的环境。

在接下来的章节中,我们将详细讨论这些监控工具。

ELK 堆栈概述

ELK 堆栈(也称为弹性堆栈)是最流行的日志管理平台。我们已经知道,监视的基本工具之一是日志记录。对于微服务,会生成数量惊人的日志,这些日志有时甚至是人类无法理解的。ELK 堆栈也是微服务监控的一个很好的候选对象,因为它具有聚合、分析、可视化和监控的能力。ELK 堆栈是一个工具链,包括三个不同的工具,即 Elasticsearch、Logstash 和 Kibana。让我们一个接一个地看它们,以了解它们在 ELK 堆栈中的作用。

弹性搜索

Elasticsearch 是一个全文搜索引擎,基于 Apache Lucene 库。该项目是开源的,用 Java 开发。Elasticsearch 支持水平缩放、多租户和聚类方法。弹性搜索的基本要素是它的搜索索引。该索引存储在内部的 JSON 表单中。单个 Elasticsearch 服务器存储多个索引(每个索引代表一个数据库),单个查询可以搜索具有多个索引的数据。

Elasticsearch 确实可以提供接近实时的搜索,并且可以以非常低的延迟进行扩展。编程模型的搜索和结果通过弹性搜索应用编程接口公开,并可通过 HTTP 访问。

logstash(日志记录)

Logstash 在 ELK 堆栈中扮演日志聚合器的角色。它是一个日志聚合引擎,收集、分析、处理日志条目并将其保存在其持久存储中。由于其基于数据管道的架构模式,Logstash 非常广泛。它被部署为代理,并将输出发送到 Elasticsearch。

姆纳人

Kibana 是一个开源的数据可视化解决方案。它是为弹性搜索而设计的。您可以使用 Kibana 来搜索、查看和交互弹性搜索索引中存储的数据。

它是一个基于浏览器的 web 应用,允许您执行高级数据分析,并在各种图表、表格和地图中可视化您的数据。此外,它是一个零配置应用。因此,在安装之后,它既不需要任何编码,也不需要额外的基础设施。

本节概述了数据可视化解决方案 Kibana。我们可以使用网页分析通过基巴纳捕获的数据,这样我们就可以用各种图表可视化数据。除了这个用于监控和报告的工具,我们还需要一个日志管理解决方案。Splunk 是目前最有利的工具之一。在下一节中,我们将讨论 Splunk。

软体

Splunk 是最好的商业日志管理解决方案之一。它可以非常容易地处理万亿字节的日志数据。随着时间的推移,它增加了许多额外的功能,现在被认为是成熟的运营智能领先平台。Splunk 用于监控大量应用和环境。

它在实时监控任何基础架构和应用方面发挥着至关重要的作用,对于在问题、问题和攻击影响客户、服务和盈利能力之前识别它们至关重要。Splunk 的监控能力、具体模式、趋势和阈值等等,都可以建立为 Splunk 需要关注的事件。这是为了让特定的个人不必手动完成这项工作。

Splunk 在其平台中包含了警报功能。它可以实时触发警报通知,以便采取适当的措施,从而避免应用或基础架构停机。

触发警报和配置操作时,Splunk 可以执行以下操作:

  • 发送电子邮件
  • 执行脚本或触发运行手册
  • 创建组织支持或行动单

通常,Splunk 监控标记可能包括以下内容:

  • 应用日志
  • 活动目录对事件数据的更改
  • Windows 事件日志
  • Windows 性能日志
  • 基于 WMI 的数据
  • Windows 注册表信息
  • 来自特定文件和目录的数据
  • 性能监控数据
  • 从 API 和其他远程数据接口和消息队列获取数据的脚本输入

本节的目的是概述 Splunk,一个日志管理工具。对于监控解决方案,我们需要警报/通知。在下一节中,我们将讨论 Splunk 的警报功能。

发信号

Splunk 不仅是一个监控解决方案;它还具有报警功能。它可以配置为根据任何实时或历史搜索模式设置警报。这些警报查询可以定期自动运行,警报可以由这些实时或历史查询的结果触发。

您可以将 Splunk 警报基于各种阈值和基于趋势的情况,例如条件、关键服务器或应用错误以及资源利用率阈值。

报告

如果满足某些条件,Splunk 还可以报告已经触发和执行的警报。Splunk 的警报管理器可用于根据前面的警报数据创建报告。

报告和监控非常有用,尤其是当我们使用企业级应用时。如果我们希望生成各种报告来监控这些应用,我们在本节中讨论的报告和监控解决方案/工具非常有用。

本节还讨论了一些用于监控和报告的定制解决方案,这些解决方案使用了微软 Azure Cloud 提供的工具之外的工具。在这里,我们讨论了日志管理工具 Splunk 和 Elastic Stack ( ELK ),其中包含了不同的工具,例如 Elasticsearch、Logtash 和 Kibana。最后,我们学习了如何通过使用数据分析来监控和获取各种报告。

摘要

调试和监控微服务并不简单;这是一个具有挑战性的问题。我们故意在这里用这个词来挑战:这个没有银弹。没有一个你可以安装的工具可以像魔法一样工作。但是,借助 Azure 诊断和应用洞察,或者 ELK 堆栈或 Splunk,您可以提出解决方案,帮助您解决微服务监控挑战。

实现微服务监控策略是监控微服务实现的一种有用的方法。监控策略包括应用/系统监控、实时用户监控、合成事务、集中日志记录、语义日志块,以及在整个事务性 HTTP 请求中实现相关标识。我们看到了如何在各种工具的帮助下创建报告来监控应用。借助 ELK 堆栈,我们可以创建一个完整的报告和监控系统。

在下一章中,我们将了解如何扩展微服务,并了解扩展微服务解决方案的解决方案和策略。

问题

  1. 什么是监控?
  2. 监控的必要性是什么?
  3. 什么是健康监测?
  4. 监控的挑战是什么?
  5. 微软 Azure 有哪些主要的日志记录和监控解决方案?

进一步阅读

八、使用 Azure 扩展微服务

假设您是负责开发公司旗舰产品 TaxCloud 的开发和支持团队的一员。TaxCloud 帮助纳税人自行报税,然后在成功报税后向他们收取少量费用。假设您使用微服务开发了这个应用。现在,假设产品变得受欢迎并获得吸引力,突然,在纳税申报的最后一天,你会看到一群消费者想要使用你的产品并申报纳税。但是你的系统支付服务比较慢,几乎把系统拖垮了,所有的新客户都转移到你竞争对手的产品上。这对你的生意来说是一个失去的机会。

尽管这是一个虚构的场景,但它很可能发生在任何企业。在电子商务中,我们在现实生活中总是会经历这种事情,尤其是在特殊的场合,比如圣诞节和黑色星期五。总而言之,它们指向一个重要的特征——系统的可伸缩性。可扩展性是任何任务关键型系统最重要的非功能性需求之一。用几百个事务服务几个用户,并不等于用几百万个事务服务几百万个用户。我们还将讨论如何单独扩展微服务,在设计它们时要考虑什么,以及如何使用不同的模式避免级联故障。在本章中,我们将一般性地讨论可伸缩性。

到本章结束时,您将了解以下内容:

  • 获得可扩展性概述
  • 扩展基础设施
  • 了解微服务的可扩展性
  • 实施扩展基础架构
  • 扩展服务设计

作为开发人员,我们可以浏览各种扩展技术,然后是扩展服务设计,这在编写生产就绪系统时非常有帮助。

技术要求

本章不包含代码示例,因此本章没有技术先决条件。

获得可扩展性概述

设计决策会影响单个微服务的可扩展性。与其他应用功能一样,在设计和早期编码阶段做出的决策将在很大程度上影响服务的可伸缩性。

微服务的可伸缩性要求在服务及其支持基础设施之间采取平衡的方法。服务及其基础设施也需要协调扩展。

可伸缩性是系统最重要的非功能特性之一,因为它可以处理更多的负载。人们通常认为可伸缩性通常是大规模分布式系统的一个关注点。性能和可伸缩性是系统的两个不同特征。性能涉及系统的吞吐量,而可伸缩性涉及为大量用户或大量事务提供所需的吞吐量。

本节介绍了缩放的概述。在下一节中,我们将讨论扩展我们的基础架构。

扩展基础设施

微服务是现代应用,通常利用云。因此,在可扩展性方面,云提供了一定的优势。然而,这也是关于自动化和管理成本。因此,即使在云中,我们也需要了解如何调配基础架构,如虚拟机或容器,以成功地为我们基于微服务的应用提供服务,即使是在突发流量峰值的情况下。

现在,我们将访问基础架构的每个组件,看看我们如何扩展它。最初的纵向扩展和横向扩展方法更多地应用于硬件扩展。通过自动缩放功能,您将了解 Azure 虚拟管理器缩放集。最后,您将学习在 Docker Swarm 模式下使用容器进行缩放。

垂直扩展(向上扩展)

纵向扩展是一个术语,用于通过向同一台机器添加更多资源来实现可扩展性。它包括以更高的速度增加更多的内存或处理器,或者它可以简单地将应用迁移到更强大的机器上。

随着硬件的升级,您如何扩展机器是有限制的。更有可能的是,你只是在转移瓶颈,而不是解决提高可伸缩性的真正问题。如果你给机器增加更多的处理器,你可能会把瓶颈转移到内存上。处理能力不会线性提高系统的性能。在某一点上,即使您增加了更多的处理能力,系统的性能也会稳定下来。扩展的另一个方面是,由于只有一台机器服务于所有请求,它也成为单点故障。

总之,垂直缩放很容易,因为它不涉及代码更改;然而,这是一项相当昂贵的技术。堆栈溢出是基于. NET 的系统垂直扩展的罕见例子之一。

水平缩放(横向扩展)

如果不想垂直缩放,您可以始终水平缩放系统。通常,它也被称为横向扩展。谷歌确实让这种方法变得相当流行。谷歌搜索引擎已经没有便宜的硬件了。因此,尽管是一个分布式系统,横向扩展在早期帮助谷歌在短时间内扩展了搜索过程,同时仍然很便宜。大多数情况下,常见的任务被分配给工作机器,它们的输出被执行相同任务的几台机器收集。这种安排也会在失败中幸存下来。为了横向扩展,负载平衡技术非常有用。在这种布置中,负载平衡器通常被添加在所有节点集群的前面。因此,从消费者的角度来看,你击中的是哪台机器/盒子并不重要。这使得通过添加更多服务器来增加容量变得容易。将服务器添加到集群可以线性提高可扩展性。

当应用代码不依赖于运行它的服务器时,向外扩展是一种成功的策略。如果请求需要在特定的服务器上执行,也就是说,如果应用代码具有服务器关联性,将很难向外扩展。但是,在无状态代码的情况下,在任何服务器上执行该代码都更容易。因此,当无状态代码在水平扩展的机器或集群上运行时,可伸缩性得到了提高。

由于横向扩展的性质,它是整个行业中常用的方法。您可以看到许多以这种方式管理的大型可扩展系统的例子,例如谷歌、亚马逊和微软。我们建议您也横向扩展微服务。

Session stickiness can be configured, where you either want sessions going to a specific node or not. In some scenarios, it might also be an option to sync the sessions in the load balancer, if that is supported.

本节旨在说明扩展的基础设施;我们经历了水平和垂直缩放。在下一节中,我们将看到微服务的可伸缩性。

了解微服务的可扩展性

在本节中,我们将回顾可用于微服务的扩展策略。我们将研究可伸缩性的 Scale Cube 模型,我们将看到如何为微服务扩展基础设施层,以及如何在微服务设计中嵌入可伸缩性。

可伸缩性的尺度立方体模型

看待可伸缩性的一种方法是理解 Scale Cube。马丁·阿博特和迈克尔·费希尔解释了缩放,并将缩放立方体定义为查看和理解系统的可伸缩性。规模立方体也适用于微服务架构。

下图是比例立方体的可视化视图:

在这个可伸缩性的三维模型中,原点(0,0,0)代表最不可伸缩的系统。它假设系统是部署在单个服务器实例上的整体。如图所示,一个系统可以通过在三个维度上投入适当的努力来扩展。为了使系统朝着正确的可扩展方向发展,我们需要正确的权衡。这些权衡将帮助您获得系统的最高可扩展性。这将有助于您的系统迎合不断增长的客户需求,这是由比例立方体模型表示的。让我们看看这个模型的每一个轴,并讨论它们在微服务可伸缩性方面意味着什么。

x 轴的缩放

x 轴上扩展意味着在负载平衡器后面运行应用的多个实例。这是单片应用中非常常见的方法。这种方法的缺点之一是,应用的任何实例都可以利用该应用可用的所有数据。它也没有解决我们应用的复杂性。

微服务不应该共享一个全局状态或一种可以被所有服务访问的数据存储。这将造成瓶颈和单点故障。因此,仅在标度立方体的 x 轴上进行微服务标度不是正确的方法。

现在,我们来看看 z- 轴缩放。我们跳过 y- 轴缩放是有原因的。我们会回来的。

z 轴的缩放

z 轴的缩放基于分割,分割基于交易的客户或请求者。 z- 轴拆分可能会也可能不会解决指令、流程或代码的整体性。然而,它们经常解决执行这些指令、过程或代码所必需的数据的整体性。自然,在 z- 轴缩放中,有一个专用组件负责应用偏置因子。偏见因素可能是国家、请求来源、客户群或与请求者或请求相关的任何形式的订阅计划。请注意 z- 轴缩放有许多好处,例如改进了请求的隔离和缓存。然而,它也有以下缺点:

  • 它增加了应用的复杂性。
  • 它需要一个分区方案,这可能很棘手,尤其是当我们需要重新分区数据时。
  • 它没有解决开发和应用复杂性增加的问题。要解决这些问题,需要应用 y- 轴缩放。

由于 z 轴缩放的前述性质,它不适合在微服务的情况下使用。

y 轴的缩放

y 轴的缩放是指将一个应用分解成不同的组件。它还代表了职责的分离,由事务中某个组件所执行的数据或工作的角色或类型决定。为了划分责任,我们需要根据他们所执行的操作或角色来划分系统的组件。这些角色可能基于事务的大部分,也可能基于非常小的一部分。根据角色的大小,我们可以扩展这些组件。这种拆分方案被称为面向服务或面向资源的拆分。

这与我们在微服务中看到的非常相似。我们根据应用的角色或操作来划分整个应用,并根据其在系统中的角色来扩展单个微服务。这种相似不是偶然的;它是设计的产物。因此,我们可以相当容易地说, y- 轴缩放非常适合微服务。

理解 y- 轴扩展对于扩展基于微服务的架构系统非常重要。因此,实际上,我们是在说微服务可以通过按照它们的角色和动作来拆分它们来扩展。考虑一个订单管理系统,它被设计来满足某些初始客户需求。为此,将应用拆分为单独的服务很好,例如客户服务、订单服务和支付服务。但是,如果需求增加,您需要仔细检查现有系统。您可能会发现已经存在的服务的子组件,这些子组件可以很好地再次分离,因为它们在该服务以及整个应用中扮演着非常特定的角色。对于增加的需求和负载,这种重新设计可能会触发将订单服务重新拆分为报价服务、订单处理服务、订单履行服务等。现在,报价服务可能需要更多的计算能力,因此与其他服务相比,我们可能会推送更多的实例(其背后的相同副本)。

这是我们应该如何在 AFK Scale Cube 的三维模型(https://akfpartners.com/growth-blog/scale-cube)上缩放微服务的一个近乎真实的例子。你可以在一些属于行业的知名微服务架构中观察到这种三维可伸缩性和 y- 轴的服务伸缩性,比如亚马逊、网飞和 Spotify。

可扩展微服务的特征

在缩放立方体部分,我们主要关注于缩放整个系统或应用的特征。在本节中,我们将重点关注扩展单个微服务的特性。当微服务表现出以下主要特征时,可以说它是可扩展的和高性能的:

  • 已知增长曲线:例如,在订单管理系统的情况下,我们需要知道当前服务支持多少订单,并且我们需要知道它们与订单履行服务指标(以每秒请求数衡量)的比例如何。当前测量的指标称为基线数字

  • 经过充分研究的使用指标:流量模式通常会揭示客户需求,并且基于客户需求,可以计算出前面章节中提到的关于微服务的许多参数。因此,微服务是仪表化的,监控工具是微服务的必要伙伴。

  • 基础设施资源的有效利用:基于定性和定量的参数,可以进行资源利用的预判。这将有助于团队预测基础设施的成本并进行规划。

  • 使用自动化基础设施测量、监控和增加容量的能力:基于微服务资源消耗的运营和增长模式,规划未来的容量非常容易。如今,随着云的弹性,能够规划和自动化容量变得更加重要。本质上,基于云的架构是成本驱动的架构。

  • 获得可扩展性概述:资源需求包括每个微服务需要的具体资源(计算、内存、存储和 I/O)。识别这些对于更顺畅的运营和可扩展的服务至关重要。如果我们确定了资源瓶颈,就可以解决和消除它们。

  • 有相同比例的依赖缩放:这个不言而喻。但是,您不能只关注微服务,让它的依赖性成为瓶颈。微服务的可伸缩性取决于其最小的伸缩依赖性。

  • 容错高可用:在分布式系统中故障是不可避免的。如果遇到微服务实例故障,应该自动将其重新路由到健康的微服务实例。在这种情况下,仅仅将负载平衡器放在微服务集群前面是不够的。服务发现工具对于满足可扩展微服务的这一特性非常有帮助。

  • 具有可扩展的数据持久化机制:对于可扩展的微服务,单个数据存储的选择和设计应该是可扩展和容错的。在这种情况下,缓存和分离读写存储会有所帮助。

现在,当我们讨论微服务和可扩展性时,扩展的自然安排就出现了,如下所示:

  • 扩展基础设施:微服务在动态和软件定义的基础设施上运行良好。因此,扩展基础设施是扩展微服务的重要组成部分。
  • 围绕服务设计扩展:微服务设计包括一个基于 HTTP 的应用编程接口,以及一个存储服务本地状态的数据存储。

本节概述了缩放及其特性。我们讨论了扩展基础架构的概述。接下来,我们将更详细地讨论扩展基础架构。

实施扩展基础架构

在本节中,我们将访问微服务基础架构的所有层,我们将看到它们之间的相互关系,即每个基础架构层如何扩展。在我们的微服务实现中,有两个主要组件:

  • 虚拟计算机
  • 托管在虚拟机或物理机上的容器

下图显示了微服务基础架构的逻辑视图:

上图可视化了使用 Azure 公共云的微服务基础设施。

使用扩展集扩展虚拟机

在 Azure 云中,扩展虚拟机非常简单和容易。这是微服务闪耀的地方。使用扩展集,您可以根据规则集在短时间内自动提升相同虚拟机映像的实例。刻度集与 Azure 自动刻度集成在一起。

Azure 虚拟机的创建方式可以是,作为一个组,它们总是为请求提供服务,即使请求量增加。在特定情况下,如果不需要这些虚拟机来执行工作负载,也可以自动删除它们。这由虚拟机规模集负责。

秤台也能与 Azure 中的负载平衡器很好地集成。由于它们被表示为计算资源,因此可以与 Azure 的资源管理器一起使用。可以配置扩展集,以便按需创建或删除虚拟机。这有助于以宠物对牛的心态来管理虚拟机,我们在本章前面的部署中已经看到了这一点。

对于需要扩展计算资源的应用,扩展操作在故障域和更新域之间是隐式平衡的。

使用扩展集,您不需要关联独立资源的循环,如网卡、存储帐户和虚拟机。即使横向扩展,我们如何保证这些虚拟机的可用性?虚拟机规模集已经解决了所有这些问题和挑战。

扩展集允许您根据需求自动扩展和缩减应用。假设有一个 40%利用率的阈值。因此,一旦我们达到 40%的利用率,我们将开始经历性能下降。利用率达到 40%时,就会增加新的 web 服务器。如前几节所述,比例集允许您设置规则。规模集的输入是虚拟机。规模集上的规则规定,五分钟内 CPU 的平均利用率为 40%,因此 Azure 将向规模集中添加另一台虚拟机。这样做之后,它会再次校准规则。如果性能仍高于 40%,它会添加第三个虚拟机,直到达到可接受的阈值。一旦性能下降到 40%以下,它将根据流量不活动等情况开始删除这些虚拟机,以降低运营成本。

因此,通过实现一个扩展集,您可以为性能构建一个规则,并通过简单地自动添加和删除虚拟机来使您的应用更大以处理更大的负载。一旦这些规则确立,作为管理员的你将无事可做。

Azure Autoscale 测量性能并确定何时向上和向下扩展。它还集成了负载平衡器和网络地址转换。现在,它们与负载平衡器和网络地址转换集成的原因是,当我们添加这些额外的虚拟机时,我们将有一个负载平衡器和一个网络地址转换设备。随着请求不断传入,除了部署虚拟机之外,我们还必须添加一个允许流量重定向到新实例的规则。扩展集的伟大之处在于,它们不仅可以添加虚拟机,还可以与基础架构的所有其他组件协同工作,包括网络负载平衡器等。

在 Azure 门户中,一个扩展集可以被视为一个条目,即使它包含多个虚拟机。要查看扩展集中虚拟机的配置和规范细节,您必须使用 Azure 资源浏览器工具。这是一个基于网络的工具,可在https://resources.azure.com获得。在这里,您可以查看订阅中的所有对象。您可以在Microsoft.Compute部分查看比例集。

通过使用 Azure 模板库,构建一个比例集非常容易。一旦您创建了自己的 Azure 资源管理器 ( ARM )模板,您也可以基于比例集创建自定义模板。有关于如何利用 ARM 模板建立一个标度集的详细讨论和说明。您可以在这里快速开始使用这些模板:https://github.com/Azure/azure-quickstart-templates

An availability set is an older technology, and this feature has limited support. Microsoft recommends that you migrate to virtual machine scale sets, for faster and more reliable autoscale support.

本节概述了扩展虚拟机,这在微软 Azure Cloud 中非常简单和容易。本节讨论了扩展虚拟机。接下来,我们将讨论自动缩放。

自动缩放

借助监控解决方案,我们可以测量基础架构的性能参数。这通常是以性能服务级别协议(SLa)的形式。自动缩放使我们能够根据性能阈值增加或减少系统可用的资源。

自动缩放功能增加了额外的资源来应对增加的负载。它的工作原理也相反。如果负载降低,则自动缩放会减少可用于执行任务的资源数量。自动扩展无需预先配置资源就能完成所有工作,而且是以自动方式完成的。

自动缩放可以通过两种方式进行缩放——纵向(向现有资源类型添加更多资源)或横向(通过创建该类型资源的另一个实例来添加资源)。

自动缩放功能根据两种策略决定添加或删除资源。一种是基于资源的可用指标,或者基于满足某个系统阈值。另一种策略是基于时间的,例如,上午 9 点到下午 5 点(系统需要 30 台网络服务器,而不是 3 台网络服务器)。

蔚蓝监测仪器的每一个资源;收集并监控所有与指标相关的数据。根据收集的数据,自动缩放做出决策。

Azure Monitor 自动扩展仅适用于虚拟机扩展集、云服务和应用服务(例如,网络应用)。

使用 Docker Swarm 进行容器缩放

早些时候,在关于部署的一章中,我们研究了如何将微服务打包到 Docker 容器中。我们还详细讨论了为什么容器化在微服务领域很有用。在这一节中,我们将使用 Docker 来提升我们的技能,我们将看到使用 Docker Swarm 来扩展我们的微服务是多么容易。

从本质上讲,微服务是分布式系统,需要分布式和隔离的资源。Docker Swarm 提供容器编排集群功能,因此多个 Docker 引擎可以作为单个虚拟引擎工作。这类似于负载平衡器功能。此外,如果需要,它还会创建新的容器实例或删除容器。

您可以在 Docker Swarm 中使用任何可用的服务发现机制,例如 DNS、consul 或 Zookeeper 工具。

集群是 Docker 引擎或节点的集群,在这里您可以将微服务部署为服务。现在,不要将这些服务与微服务混淆。在 Docker 实现中,服务是一个不同的概念。在 Docker 中,服务是要在工作节点上执行的任务的定义。您可能想了解我们在最后一句中提到的节点。Docker Swarm 上下文中的节点用于参与集群的 Docker 引擎。一个完整的 Swarm 演示是可能的,ASP.NET Core 图像可在 ASP。GitHub(https://github.com/dotnet/dotnet-docker)上的 NET-Docker 项目。

To date, Azure Container Service is a good solution for scaling and orchestrating Linux or Windows containers using DC/OS, Docker Swarm, or Google Kubernetes. Recently, Microsoft announced that Azure Containers will no longer be available from January 31, 2020. Instead, Microsoft is investing in Azure Kubernetes Service (AKS). To learn more about AKS, visit: https://docs.microsoft.com/en-us/azure/aks/.

既然我们已经了解了如何扩展微服务基础设施,那么让我们在接下来的章节中重新讨论微服务设计的可扩展性方面。

扩展服务设计

在这些部分中,我们将了解在设计或实现微服务时需要注意的组件/问题。在基础架构扩展考虑服务设计的情况下,我们可以真正释放微服务架构的力量,并且我们可以获得大量的业务价值,从而使微服务成为真正的成功案例。那么,服务设计的组成部分是什么?让我们看看。

数据持久化模型设计

在传统应用中,我们一直依赖关系数据库来保存用户数据。关系数据库对我们来说并不陌生。它们出现在 70 年代,作为一种以结构化方式存储持久信息的方式,允许您进行查询和执行数据维护。

在当今的微服务世界中,现代应用需要在超大规模阶段进行扩展。在任何意义上,我们都不建议您放弃使用关系数据库。他们仍然有他们的有效用例。然而,当我们在单个数据库中混合读写操作时,在我们需要提高可伸缩性的地方就会出现复杂的情况。关系数据库强化关系并确保数据的一致性。关系数据库工作在众所周知的 ACID 模型上。因此,在关系数据库中,我们对读取和写入操作使用相同的数据模型。

在大多数情况下,读操作通常必须比写操作快。也可以使用不同的筛选条件进行读取操作,返回单行或结果集。在大多数写操作中,只涉及一行或一列,通常,与读操作相比,写操作花费的时间稍长。因此,我们既可以优化读取并提供服务,也可以在同一数据模型中优化写入并提供服务。

不如我们将基本数据模型分成两半:一个用于所有读操作,另一个用于所有写操作?如果我们这样做,事情会变得简单得多,并且很容易用不同的策略优化两个数据模型。这对我们的微服务的影响是,反过来,它们对于这两种操作都变得高度可扩展。

这种特殊的架构被称为公共查询责任隔离 ( CQRS )。自然的结果是,就我们的编程模型而言,CQRS 也得到了扩展。现在,我们的编程模型之间的数据库-对象关系变得更加简单和可伸缩。

随之而来的是扩展微服务实现的下一个基本要素:数据缓存。

缓存机制

缓存是增加应用吞吐量的最简单方法。原理很简单。一旦从数据存储器中读取了数据,它就尽可能地靠近处理服务器。在未来的请求中,数据直接从数据存储或缓存中提供。缓存的本质是最小化服务器必须做的工作量。HTTP 有一个内置的缓存机制,它嵌入在协议本身中。这就是它伸缩性如此之好的原因。

关于微服务,我们可以在三个级别进行缓存,即客户端、代理和服务器端。让我们看看他们每个人:

  • 首先,我们有客户端缓存。通过客户端缓存,客户端存储缓存的结果。因此,客户端负责缓存失效。

During cache invalidation, which is a process in a computer system, the cache entries are replaced or removed. This requires manual intervention (using code), so that it can be done explicitly.

通常,服务器使用缓存控制和到期头等机制,提供关于它可以保留数据多长时间以及何时可以请求新数据的指导。随着浏览器支持 HTML5 标准,有更多的机制可用,例如本地存储、应用缓存或 web SQL 数据库,客户端可以在其中存储更多的数据。

  • 接下来,我们进入代理端。许多反向代理解决方案,如 Squid、HAProxy 和 NGINX,也可以充当缓存服务器。
  • 现在,让我们详细讨论服务器端缓存。在服务器端缓存中,我们有以下两种类型:
    • 响应缓存:这是 web 应用 UI 的一种重要的缓存机制,说实话,也很简单,容易实现。为了响应缓存,与缓存相关的头被添加到微服务提供的响应中。这可以极大地提高微服务的性能。在 ASP.NET Core 中,您可以使用Microsoft.AspNetCore.ResponseCaching包实现响应缓存。
    • 持久化数据的分布式缓存:分布式缓存增强了微服务的吞吐量,因为缓存不需要对外部资源进行 I/O 访问。这有以下优点:
      • 微服务客户端得到完全相同的结果。
      • 分布式缓存由持久性存储备份,并作为不同的远程进程运行。因此,即使应用服务器重新启动或出现任何问题,也绝不会影响缓存。
      • 对源数据存储的请求较少。

您可以在集群模式下使用分布式提供程序,例如 CacheCow、Redis(对于我们的书来说是 Azure Cache for Redis )或 memcache 来扩展您的微服务实现。

在下一节中,我们将概述用于 Redis 的 CacheCow 和 Azure 缓存。

CacheCow

当您想在客户机和服务器上实现 HTTP 缓存时,CacheCow 就出现了。这是一个轻量级库,目前,ASP.NET 网络应用编程接口支持是可用的。CacheCow 是开源的,并带有麻省理工学院的许可,可在 GitHub(https://github.com/aliostad/CacheCow)上获得。

要开始使用 CacheCow,您需要为服务器和客户端做好准备。重要的步骤如下:

  • 在您的 ASP.NET 网络应用编程接口项目中安装Install-Package CacheCow.Server NuGet 包;这将是你的服务器。
  • 在您的客户端项目中安装Install-Package CacheCow.Client NuGet 包;客户端应用将是 WPF、Windows 窗体、控制台或任何其他网络应用。
  • 在服务器端创建缓存存储,需要一个数据库来存储缓存元数据(https://github . com/aliostad/CacheCow/wiki/入门#缓存存储)。

If you want to use memcache, you can refer to https://github.com/aliostad/CacheCow/wiki/Getting-started for more information.

蓝色高速缓存为 Redis

Redis 的 Azure Cache 是一个名为Re****dis(https://github.com/antirez/redis)的开源项目的包装器,这是一个内存中的数据库,它保存在磁盘上。有关 Redis 的 Azure 缓存的更多信息,请参见https://azure.microsoft.com/en-in/services/cache/。以下是我的总结:

"Azure Cache for Redis gives you access to a secure, dedicated Redis cache, managed by Microsoft and accessible from any application within Azure."

借助以下步骤,开始使用 Redis 的 Azure 缓存非常简单:

  1. 创建一个网络应用编程接口项目——参考我们在第 2 章重构整体中的代码示例。

  2. 实施 Redis 对于推荐点,使用https://github.com/StackExchange/StackExchange.Redis,然后安装Install-Package StackExchange.Redis NuGet 包。

  3. 更新CacheConnection(https://docs . Microsoft . com/en-us/azure/azure-cache-for-redis/cache-web-app-how to # update-the-MVC-application)的配置文件。

  4. 然后,在 Azure 上发布(https://docs . Microsoft . com/en-us/Azure/Azure-cache-for-redis/cache-web-app-how to # publish-run-in-Azure)。

You can also use this template to create Azure Cache for Redis: https://github.com/Azure/azure-quickstart-templates/tree/master/201-web-app-redis-cache-sql-database.

有关用于 Redis 的 Azure Cache 的完整详细信息,请参考此 URL:https://docs . Microsoft . com/en-us/Azure/Azure-Cache-for-Redis/

在本节中,我们讨论了 Redis 的 Azure Cache 以及在项目中实现它的步骤。接下来,我们将讨论可扩展系统中的容错。

冗余和容错

我们知道,系统处理故障和从故障中恢复的能力与可伸缩性提供的能力不同。然而,我们不能否认,就系统而言,它们是密切相关的能力。除非我们解决可用性和容错的问题,否则构建高度可扩展的系统将是一项挑战。一般来说,我们通过为系统的不同部分或组件提供冗余副本来实现可用性。因此,在接下来的部分,我们将触及两个这样的概念。

断路器

断路器是电子设备中的一种安全功能,在发生短路时,它可以切断电流并保护设备,或者防止对周围环境造成任何进一步的损害。这种思想可以应用于软件设计。当从属服务不可用或不处于健康状态时,断路器会阻止呼叫转到该从属服务,并在配置的时间段内将流量重定向到备用路径。

下图显示了典型的断路器模式:

如图所示,断路器作为一个状态机,有三种状态,即闭合断开半断开

让我们在接下来的章节中更多地了解它们。

关闭状态

这是电路的初始状态,描述了正常的控制流程。在这种状态下,有一个失败计数器。如果在该流程中出现OperationFailedException,故障计数器增加1。如果故障计数器持续增加,这意味着电路遇到更多异常,并且它达到了设置的故障阈值,则断路器转换到断开状态。但是如果调用成功,没有任何异常或失败,失败计数将被重置。

开放状态

在打开状态下,电路已经跳闸,超时计数器已经启动。如果达到超时时间,电路仍然继续出现故障,代码流将进入半开状态。这告诉我们,来自应用的请求会立即失败,并且会向应用返回一个异常。

半开状态

在半开状态下,状态机/断路器组件重置超时计数器,并再次尝试断开电路,重新启动到打开状态的状态改变。但是,在此之前,它会尝试执行常规操作,例如调用依赖项。如果成功,则断路器组件将状态更改为关闭,而不是打开状态。这是为了使操作的正常流程能够发生,并且电路再次闭合。

For .NET-based microservices, if you want to implement the circuit breaker and a couple of fault-tolerant patterns, there is a good library named Polly available, in the form of a NuGet package. It comes with extensive documentation and code samples, and it has a fluent interface. You can add Polly from http://www.thepollyproject.org/ or by just issuing the install--Package Polly command from the package manager console in Visual Studio.

服务发现

对于一个小的实现,如何确定微服务的地址?对任何人来说.NET 开发者,答案就是我们简单的把 IP 地址和服务端口放在配置文件里,我们就好了。然而,当您在运行时动态地处理成百上千个服务时,就会出现服务位置问题。

现在,如果你看得更深一点,你可以看到我们试图解决问题的两个部分:

  • 服务注册:这是在某种中央注册表内注册的过程,所有的服务级别元数据、主机列表、端口和秘密都存储在这个注册表中。
  • 服务发现:这是通过一个集中的注册表组件,在运行时建立有依赖关系的通信的过程。

任何服务注册和发现解决方案都需要具备以下特征,才能被视为微服务服务发现问题的解决方案:

  • 集中式注册表本身应该是高度可用的。
  • 一旦特定的微服务启动,它应该会自动接收请求。
  • 智能和动态负载平衡功能应该存在于解决方案中。
  • 该解决方案应该能够监控服务健康状态的能力及其所承受的负载。
  • 服务发现机制应该能够将流量从不健康的节点转移到其他节点或服务,而不会造成任何宕机或对其使用者造成影响。
  • 如果服务位置或元数据发生变化,服务发现解决方案应该能够在不影响现有流量或服务实例的情况下应用这些变化。

一些服务发现机制,如 Zookeeper(http://zookeeper.apache.org/)和 Consul,在开源社区中都是可用的。

这一部分帮助我们理解了扩展服务设计以及数据和电路如何围绕它们工作,从而提供了更好的缓存,降低了容错性。

摘要

在这一章中,我们讨论了追求微服务架构风格的关键优势,我们还研究了微服务可伸缩性的特点。我们看到了微服务如何通过系统的功能分解在 y 轴上扩展。我们了解到 Azure 云的高扩展能力,因此有助于利用 Azure 扩展集和容器编排解决方案,如 Docker Swarm、DC/操作系统和 Kubernetes。

然后,我们重点讨论了服务设计的扩展,并讨论了应该如何设计我们的数据模型。我们还看到了某些注意事项,例如在设计高可伸缩性的数据模型时,有一个分离的 CQRS 风格模型。我们还简单介绍了缓存,尤其是分布式缓存,以及它如何提高系统的吞吐量。在最后一节中,为了使我们的微服务具有高度可伸缩性,我们讨论了断路器模式和服务发现机制,它们对于微服务架构的可伸缩性至关重要。

在下一章中,我们将研究微服务的反应性和反应性微服务的特征。

问题

  1. 什么是缓存,缓存在微服务应用中的重要性是什么?
  2. 什么是服务发现,它如何在微服务应用中发挥重要作用?
  3. 你能在一个小程序中为 Redis 定义 Azure Cache 并描述它的实现吗?
  4. 什么是断路器?

进一步阅读

你刚刚读完这一章,但这不是我们学习曲线的终点。以下是一些参考资料,可以增强您对该主题的了解:

九、反应式微服务简介

我们现在已经对基于微服务的架构以及如何利用其力量有了清晰的了解。到目前为止,我们已经详细讨论了这个架构的各个方面,比如通信、部署和安全性。我们还研究了微服务在需要时如何协作。本章旨在将反应式编程与我们基于微服务的架构相结合。

反应式微服务将微服务的概念提升到了一个新的层次。随着微服务数量的增长,它们之间的通信需求也在增长。用不了多久,跟踪十几个其他服务的列表、编排它们之间的级联事务或者仅仅生成一组服务的通知的挑战就会出现。在本章的范围内,级联的概念比事务本身更重要。根据一些过滤标准,很可能只是需要通知一些外部系统,而不是事务。

挑战出现了,因为基于企业级微服务的系统总是远远超出少数微服务。这种情况的规模和复杂性无法在一章中完整描述。在这种情况下,跟踪一组微服务并与之通信的需求很快就会变成噩梦。

如果我们可以将向其他微服务传达事件的责任从单个微服务中剥离出来呢?这方面的另一个方面很可能是服务的自由,不受生态系统的跟踪。为此,你必须追踪他们的行踪。再加上身份验证,你很容易陷入你从未注册过的混乱。

解决方案在于设计变更,其中跟踪事件的微服务或将事件传达给其他人的责任从单个微服务中分离出来。让我们通过引入微服务中的反应式编程,将微服务的有效性提升到一个新的水平。

我们将在本章中讨论以下主题:

  • 了解反应性微服务
  • 使代码具有反应性
  • 理解事件通信
  • 管理数据
  • 尝试反应式微服务的编码

技术要求

本章包含各种代码示例来解释这些概念。代码保持简单,只是为了演示。

要运行和执行代码,先决条件如下:

  • Visual Studio 2019
  • .NET Core

安装 Visual Studio 2019

要运行这些代码示例,您需要安装 Visual Studio 2019 或更高版本(我们首选的 IDE)。为此,请遵循以下说明:

  1. 从下载链接下载 Visual Studio 2019(社区免费),安装说明中提到:https://docs . Microsoft . com/en-us/visualstudio/install/install-Visual-Studio
  2. 按照操作系统的安装说明进行操作。Visual Studio 安装有多个版本。我们使用的是视窗操作系统。

如果你没有.NET Core 3.1 安装完毕,可以从这里的链接下载:https://dotnet.microsoft.com/download/dotnet-core/3.1

The complete source code is available here: https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2009.

了解反应性微服务

在我们深入被动微服务之前,让我们看看被动这个词是什么意思。一个软件必须具备某些基本属性,才能被认为是反应性的。这些属性是响应性、弹性、自主性,最重要的是,是消息驱动的。我们将详细讨论这些属性,并研究它们如何使微服务更适合大多数企业需求。

响应性

不久前,在需求收集会议上讨论的业务发起人的关键需求之一是保证几秒钟的响应时间。例如,我记得当我们第一次看到那些定制 t 恤印花的电子商店时,你可以上传一个图像,然后把它渲染到选定的服装上。让我们快进几年——我可以自己担保——现在,如果任何网页加载时间超过几秒钟,我们将关闭浏览器窗口。

如今的用户期待近乎即时的响应。但是这是不可能的,除非您编写的代码遵循某些标准来提供预期的性能。总会有许多不同的组件合作和协调来解决我们的业务问题。因此,预计每个组件返回结果的时间今天已减少到毫秒。此外,在响应时间方面,系统必须表现出一致性和性能。如果您的服务在规定的时间内表现出可变的响应时间,那么这是您的系统即将出现问题的迹象。你迟早要处理这件行李。毫无疑问,在大多数情况下,你会设法解决它。

然而,挑战比表面上看得见的要大得多。任何这样的特征都需要探究设计中出现问题的可能性。它可能是对另一个服务的某种依赖,太多的功能在服务中同时执行,或者同步通信阻塞了工作流。

弹性

随着分布式计算的流行,在一个或多个组件出现故障的情况下,用户对这样的系统有什么期望?单一故障是否会导致灾难性的多米诺骨牌效应,从而导致整个系统的故障?或者,系统是否在预期的时间表内优雅地从这样的事件中反弹回来?在这种情况下,终端用户根本不应该受到影响,或者系统至少应该在一定程度上最小化影响,确保用户体验不受影响。

基于弹性微服务的应用将遵循服务间通信。在这样的弹性应用中,两个或多个服务可以继续相互通信,而不会影响系统,即使在任何其他服务中存在通信故障。这意味着应该有一个机制来处理故障、错误或服务失败,以确保弹性。

自治

一直以来,我们都在大力倡导微服务的正确隔离。我们在第二章的【理解接缝的概念】一节中触及了接缝识别的话题。在成功实现我们的微服务式架构的同时,我们获得了许多好处。我们可以有把握地说,隔离是这里的基本要求之一。然而,成功实施隔离的好处远不止于此。**

微服务需要自治,否则我们的工作将是不完整的。即使在实现了微服务架构之后,如果一个微服务故障导致其他服务延迟,或者发生了多米诺骨牌效应,这意味着我们在设计中错过了一些东西。然而,如果微服务隔离做得好,以及这个特定的微服务要执行的功能的正确分解,这将意味着设计的其余部分将自行到位,以处理任何类型的解决冲突、通信或协调。

执行这种编排所需的信息主要取决于服务本身的明确定义的行为。因此,定义良好的微服务的消费者不需要担心微服务失败或抛出异常。如果在规定的时间内没有反应,就再试一次。

消息驱动——反应式微服务的核心

消息驱动是反应式微服务的核心。作为行为的一部分,所有反应性微服务都定义了它们可能生成的任何事件。根据单个事件的设计,这些事件中可能有也可能没有额外的信息有效负载。无论生成的事件是否被执行,作为该事件生成器的微服务都不会被打扰。在这个特定服务的范围内,除了这个事件的生成之外,没有这个动作的行为定义。范围到此为止。整个系统的任何服务都将在它们的范围内运行,并且这些服务都不会被打扰,不管它是否是事件触发的。

这里的不同之处在于,所有这些正在生成的事件都可以通过侦听来异步捕获。没有其他服务在阻塞模式下等待这些服务中的任何一个。任何收听这些事件的人都被称为订阅者,而收听事件的行为被称为订阅。订阅这些事件的服务被称为观察者,生成的事件的源服务被称为可观察。这个图案被称为观察者设计图案

然而,在每个观察器上具体实现的练习与我们设计松散耦合的微服务的目标有些不一致。如果这是你所想的,那么你有正确的思维上限,我们在正确的轨道上。一会儿,当我们将流程映射为反应式微服务时,我们将看到如何在反应式微服务的世界中实现这一目的。

在我们继续映射我们的过程之前,重要的是我们简要地讨论一下模式,关于我们这里的主题。要对一条消息采取行动,你首先需要表明你想看那种类型的消息的意图。同时,消息的始发者必须有向感兴趣的观察者发布他们的消息的意图。因此,至少会有一个可观察到的现象被一个或多个观察者观察到。为了增加一些趣味,观察者可以发布多种类型的消息,观察者可以观察一个或多个他们想要操作的消息。

当观察者想要停止监听这些消息时,该模式不会限制他们取消订阅。所以,它看起来很漂亮,但是它容易实现吗?让我们继续前进,让我们的代码具有反应性。

使代码具有反应性

让我们检查一下我们的应用,看看它在反应式编程风格下会是什么样子。下图描述了本质上是反应性的、完全由事件驱动的应用流程:

在这个图中,服务用六边形描述,事件用方框表示。

图表中描述的流程描述了一个客户在搜索了他/她正在寻找的项目后下订单的场景。这个过程是这样的:

  1. 下订单事件升级为订单服务

  2. 针对此事件,我们的服务分析了订单项目和数量等参数,并将项目可用事件提升至产品服务

  3. 从这里开始,有两种可能的结果:要么请求的产品可用并具有所需的数量(继续到步骤 4 ),要么它不可用或没有所需的数量。

  4. 如果项目可用,产品服务会引发一个名为生成发票 (I 项目可用发票)到发票服务的事件。因为提高发票意味着我们确认订单,发票上的项目将不再有库存;我们需要注意这一点,并相应地更新库存。

  5. 为了解决这个问题,我们的发票服务进一步提出了一个名为的事件,将产品数量更新为产品服务,它处理了这个需求。为简单起见,我们不再赘述邮寄发票 ( 步骤 6 )事件由谁处理。

本节旨在检查我们现有的基于微服务的应用,然后我们收集信息,使其成为一个反应式微服务应用。为了做到这一点,我们已经在图表的帮助下完成了几个步骤。事件通信是基于微服务的应用最重要的部分之一,因为当一个服务需要来自另一个服务的输入时,服务之间的通信是必需的。让我们继续来看看事件通信。

理解事件通信

前面的讨论可能让您思考正在引发的事件将如何完美地映射各个微服务的调用;让我们更详细地讨论这个问题。将所有引发的事件都视为存储在事件存储中。存储的事件有一个关联的委托函数,调用该函数是为了迎合相应的事件。请考虑下图:

虽然我们显示商店只有两列事件功能(在图的顶部),但它存储了更多的信息,例如发布者和订阅者的详细信息。每个事件都包含触发相应服务所需的完整信息。因此,事件委托可能是要调用的服务,也可能是应用本身的一个函数。对这个架构来说无所谓。

换句话说,随着事件通信的适应,以及发布/订阅模型的实现,我们作为开发人员不会担心冗长的代码。你看,一旦一个事件被订阅和发布,它将被自动触发来提供一个成功操作的预期输出。这里有一件事应该很重要,那就是安全。必须有某种机制来处理安全通信,我们将在下一节中讨论。

安全

在实现反应式微服务时,有许多方法可以处理安全性。然而,鉴于我们这里的范围有限,我们将把我们的讨论仅限于一种类型。让我们继续在这里讨论消息级安全性,看看它是如何实现的。

消息级安全性

消息级安全性是保护您的单个请求消息的最基本的方法。执行初始身份验证后,根据实现方式,请求消息本身可能包含 OAuth 承载令牌或 JWTs。这样,每一个请求都得到验证,并且与用户相关的信息可以嵌入到这些令牌中。信息可以像用户名一样简单,还有一个指示令牌有效性的到期时间戳。毕竟,我们不希望令牌的使用超过特定的时间范围。

实现将是渐进的,我们应该添加一些逻辑,以便令牌应该在规定的时间框架内过期。借助System.IdentityModel.Tokens.Jwt名称空间,这很容易实现。除了时间到期之外,您还可以通过添加应用所需的更多信息来实现jwt

安全通信确保请求和/或响应是安全的,不会被篡改。消息级安全性专门处理经过身份验证的请求。让我们继续讨论可伸缩性如何受到影响。

可量测性

对于反应式微服务,还有一个方面需要考虑,那就是可伸缩性。在这个令牌中(上一节中讨论过),除了身份验证信息之外,我们还可以嵌入授权信息。请注意,将所有这些信息放在一个频繁传递的令牌中,可能很快就会成为一种开销。我们可以进行必要的更改,以确保关于授权的信息是一次性的活动,并且我们可以确保它随后根据需要与服务一起保存。

当我们决定将授权相关的信息保存在单个服务中时,在某种程度上,我们使它们具有弹性。将授权信息保存在各个服务中的任务不再需要每次都联系身份验证服务来获取与授权相关的数据。这意味着我们可以非常轻松地扩展我们的服务。

扩展应用的方法也取决于代码的实现(对于业务逻辑)。在本节中,我们了解到,如果令牌加载了大量信息(即使应用需要这些信息),令牌(可能是jwt,如前一节所述)可能会成为服务的过载。因此,我们找到了传递这些信息和扩展服务的方法。当通信安全时,它也应该具有弹性,这是我们接下来要讨论的。

通信弹性

如果包含所有用户身份验证数据和授权数据的身份验证服务突然变得不可用,会发生什么情况?这是否意味着整个微服务生态系统将会崩溃,因为所有的操作——或者其中很大一部分——都需要授权给尝试该操作的用户?这不适合微服务架构领域。让我们看看如何处理这件事。

一种方法是在每个需要的服务中复制用户授权数据。当授权数据已经在相应的服务中可用时,它将减少通过移动的 JWTs 传输的数据。这样做的结果是,如果我们的身份验证服务变得不可用,经过身份验证并访问过系统的用户将不会受到影响。由于需要验证的各个服务中已经有了所有授权数据,业务可以照常进行,没有任何障碍。

然而,这种方法也有其自身的代价。维护这些数据将成为一项挑战,因为所有服务都在不断更新这些数据。每个服务所需的复制本身就是一种练习。不过,也有办法摆脱这种特殊的挑战。

我们可以简单地将这些数据存储在一个中央存储中,并让服务从这个中央存储中验证/访问与授权相关的数据,而不是让这些数据在所有的微服务中可用。这将使我们能够建立超越身份验证服务的弹性。

服务之间的通信应该是安全的,弹性和代码应该以应用可以扩展的方式编写。安全通信确保请求来自经过身份验证的来源。服务不应该以这样一种方式过载(例如,在我们的例子中,当令牌被信息过载时),这将产生扩展应用的问题。数据管理也是应用的重要组成部分,这也是我们接下来要讨论的!

管理数据

跟踪正在下的单个订单很容易。然而,将这个数字乘以每小时发出和取消的百万订单;它可能很快成为反应式微服务领域的一个挑战。挑战在于如何跨多个服务执行事务。不仅很难跟踪这样的事务,而且还会带来其他挑战,例如持久化跨越数据库和消息代理的事务。例如,用户订购了一件商品,将其添加到购物车,然后结账付款。

在本活动中,我们的代码流程如下:

  1. 系统检查订购物品的可用性。
  2. 如果物品可用,系统会保留该物品。
  3. 结账时,系统会调整物品的库存。
  4. 最后,在付款时,将确认订单,系统将继续显示项目的交货状态进度。

在本例中,每个步骤都需要将数据保存在数据库、缓存或任何其他形式中。在实际场景中,如果持久性在任何一步都失败了,那么剩下的步骤就不应该执行,已经执行的步骤应该回滚。在这种情况下,我们谈论的是来自单个用户的单个项目。但是考虑一个场景,数以千计的请求正在执行这些步骤,如果某件事失败了,跟踪所有的事务会有多复杂。由于服务故障,撤销此类操作的任务可能会在某个地方中断事务,这可能更令人生畏。

在这种情况下,我们可以利用事件源模式。这是一个强有力的候选,尤其是因为我们不需要两阶段提交(通常称为 2PC )。我们不存储事务,而是保存实体的所有状态变化事件。换句话说,我们以实体的形式存储所有改变状态的事件,比如订单和产品。在正常情况下,当客户下订单时,我们会将订单作为一行保存到订单表中。然而,在这里,我们将持续整个事件序列,直到订单被接受或拒绝的最后阶段。

参考上图(在理解事件通信部分),我们分析了创建订单时生成的事件序列。看看这些事件将如何存储在事件源模式中,并注意一个事务将如何从那组事件中推断出来。首先,让我们考虑数据将如何存储。如下图所示,单个记录保存为行。交易后确认数据一致性:

如前图所示,产品服务可以订阅订单事件,并进行相应的自我更新。活动商店由所有活动组成,如下单商品可用确认订单,最后更新产品。这些事件按照命令存储。整个工艺流程如下:

  1. 订单服务在检查物品的可用性后下订单。
  2. 购物车服务将物品添加到购物车中,并从购物车中检出物品。
  3. 产品服务将更新特定产品的项目。这种方法有许多好处:
    • 由于事件是持久化的,识别事务的挑战与维护数据库完整性的任务是分开的。
    • 在任何给定的时间点,都有可能找到系统的确切状态。
    • 用这种方法迁移单块更容易。
    • 可以回到特定的一组事件,然后找出所有可能的问题。

下图从订单服务的角度描述了我们的订单订单明细表:

除了所有的好处,它也有一些缺点。最重要的是如何查询事件存储。要在给定的时间点重建给定业务实体的状态,需要一些复杂的查询。除此之外,还会涉及到一个学习曲线,掌握事件存储取代数据库的概念,然后推断实体的状态。在 CQRS 模式的帮助下,可以轻松处理查询复杂性。但是,这不在本章范围之内(有关 CQRS 的更多信息,请参考。值得注意的是,在被动微服务之后,事件源模式和 CQRS 是重要的模式。

数据管理是微服务应用的重要组成部分,尤其是当我们讨论电子商务应用时。本节旨在讨论数据管理:数据库、事务数据等的逻辑分离。让我们继续了解微服务生态系统。

尝试反应式微服务的编码

正如在最初的章节中所讨论的,当拥抱微服务时,我们需要为大的变化做好准备。到目前为止,我们在部署、安全性和测试方面的讨论已经让您考虑接受这个事实。与单片不同,微服务的采用需要您提前做好准备,这样您就可以开始与它一起构建基础设施,而不是在它完成之后。

在某种程度上,微服务在一个完整的生态系统中茁壮成长,在这个生态系统中,从部署到测试、安全和监控,一切都得到了解决。拥抱这种变化的回报是巨大的。做出所有这些改变肯定是有成本的。然而,与其拥有一个无法推向市场的产品,不如承担一些成本,然后在最初的几次推出后,设计和开发一些能够蓬勃发展且不会消亡的产品。

在向您概述了微服务生态系统之后,我们现在了解了正在经历部署、测试、安全和监控的系统/应用。接下来,我们将编写代码来实现反应式微服务,就像之前讨论的那样。

现在,让我们尝试总结所有内容,看看它在代码中的实际外观。我们将为此使用 Visual Studio 2019。第一步是创建一个反应式微服务,然后我们将继续,创建一个客户端来消费我们创建的服务。让我们在接下来的部分中尝试这些步骤。

创建项目

我们现在将继续创建我们的反应式微服务示例。为此,我们需要创建一个 ASP.NET web 应用类型的项目。只需遵循这些步骤,您应该能够看到您的第一个反应性微服务在运行:

  1. 启动 Visual Studio。
  2. 通过导航至文件|新建|项目,或按Ctrl+Shift+N创建新项目,如下图截图所示:

  1. 从创建新项目屏幕中,选择 ASP.NET Core 网络应用,然后单击下一步:

  1. 从“配置新项目”屏幕中,转到“项目名称”并在中添加一个项目。我将其命名为FlixOne.BookStore.ProductService,然后选择位置路径和解决方案名称。完成后,单击创建:

  1. 在创建新的 ASP.NET Core 网络应用屏幕上,确保。选择 NET Core 和 ASP.NET Core 3.1,然后选择 Web 应用(模型-视图-控制器)。完成后,单击创建:

You can enable Docker support for Windows, if you want to enable the container. Select Enable Docker Support, from the  Advanced section on the right.

  1. 在解决方案资源管理器中右键单击项目,然后单击“获取管理器”并添加系统。反应式。核心数字获取项目包:

You are also required to add a package for EF Core; to do so, refer to Chapter 2, Refactoring the Monolith.

  1. Product.cs模型添加到Models文件夹,代码如下:
namespace FlixOne.BookStore.ProductService.Models
{
  public class Product
  {
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Image { get; set; }
    public decimal Price { get; set; }
    public Guid CategoryId { get; set; }
    public virtual Category Category { get; set; }
  }
}
  1. Category.cs模型添加到Models文件夹,代码如下:
namespace FlixOne.BookStore.ProductService.Models
{
  public class Category
  {
    public Category() => Products = new List<Product>();
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public IEnumerable<Product> Products { get; set; }
  }
}
  1. contextpersistence文件夹添加到项目中。将ProductContext添加到context文件夹,将IProductRepository界面和ProductRepository类添加到persistence文件夹。

考虑下面的代码片段,展示我们的上下文和持久性类:

public class ProductContext : DbContext
  {
    public ProductContext(DbContextOptions<ProductContext> options)
    : base(options)
    { }
    public ProductContext()
    { }
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
  }
}

前面的代码声明了ProductContext,继承了DbContext,有DbSet ProductsCategories

对于持久性或存储库,以下是接口代码:

namespace FlixOne.BookStore.ProductService.Persistence
{
  public interface IProductRepository
  {
    IObservable<IEnumerable<Product>> GetAll();
    IObservable<IEnumerable<Product>> GetAll(IScheduler scheduler);
    IObservable<Unit> Remove(Guid productId);
    IObservable<Unit> Remove(Guid productId, IScheduler scheduler);
  }
}

在前面的代码中,我们创建了IProductRepository来获取和移除产品。

以下是实现IProductRepository接口的ProductRepository类的代码:


namespace FlixOne.BookStore.ProductService.Persistence
{
  public class ProductRepository : IProductRepository
  {
    private readonly ProductContext _context;
    public ProductRepository(ProductContext context)
    => _context = context;
    public IObservable<IEnumerable<Product>> 
    GetAll() => Observable.Return(GetProducts());
    public IObservable<IEnumerable<Product>>
    GetAll(IScheduler scheduler) => 
    Observable.Return(GetProducts(), scheduler);
    public IObservable<Unit> Remove(Guid productId) =>
    Remove(productId, null);
    public IObservable<Unit> Remove(Guid productId,
    IScheduler scheduler)
    {
      DeleteProduct(productId);
      return scheduler != null
      ? Observable.Return(new Unit(), scheduler)
      : Observable.Return(new Unit());
    }
    ...
}

我们创造了我们的模型。我们的下一步是添加与数据库交互的代码。这些模型帮助我们将数据从数据源投射到我们的模型中。

对于数据库交互,我们已经创建了一个上下文,即ProductContext,从DbContext派生出来。在前面的一个步骤中,我们创建了一个名为Context的文件夹。

实体框架核心上下文有助于查询数据库。此外,它还帮助我们整理对数据执行的所有更改,然后在数据库中一次性执行这些更改。我们将不详细讨论实体框架核心或这里的上下文,因为它们不属于本章的范围。

上下文从connectionStrings部分的appsettings.json文件中选择连接字符串—一个名为ProductConnectionString的键。

您可以给它起任何名字,如下面的代码片段所示:

 "ConnectionStrings": 
 {
   "ProductConnection": "Data Source=.;Initial
   Catalog=ProductsDB;Integrated  
   Security=True;MultipleActiveResultSets=True"
 }

您需要更新startup.cs文件,以确保您使用的是正确的数据库。我们已经在第 2 章重构整体中讨论了修改appsettings.jsonStatrup.cs文件。在更新Startup.cs类的同时,您需要在项目中添加Swashbuckle.AspNetCore NuGet 包来支持斯瓦格。

应用和数据库之间的通信

有了我们的上下文,并考虑到我们的应用和数据库之间的通信,让我们继续添加一个存储库来促进我们的数据模型和数据库之间的交互。请参考我们存储库的代码,如创建项目部分的步骤 10 中所述。

通过将来自GetAll的结果标记为IObservable,我们添加了我们正在寻找的反应功能。另外,请特别注意退货声明:

 return Observable.Return(GetProducts());

有了这个可观察的模型,我们就可以像处理其他更简单的集合一样轻松地处理异步事件流。

我们现在准备通过我们的控制器公开功能。右键单击文件夹控制器,单击添加新项目,然后选择 ASP.NET Core,然后选择应用编程接口控制器类。命名为ProductController。完成后,单击添加:

我们的控制器是这样的:

namespace FlixOne.BookStore.ProductService.Controllers
{
  [Route("api/[controller]")]
  public class ProductController : Controller
  {
    private readonly IProductRepository _productRepository;
    public ProductController() => _productRepository =
    new ProductRepository(new ProductContext());
    public ProductController(IProductRepository 
    productRepository) => _productRepository = 
    productRepository;
    [HttpGet]
    public async Task<IEnumerable<Product>> Get() =>
    await _productRepository.GetAll().SelectMany(p => p).ToArray();
  }
}

最终结构类似于解决方案资源管理器的以下屏幕截图:

要创建数据库,您可以参考第 2 章重构整体中的 EF 核心迁移部分,或者简单地调用我们新部署的服务的 Get API。当服务发现数据库不存在时,在这种情况下,实体框架核心代码优先的方法将确保数据库被创建。

我们现在可以继续将这项服务部署到我们的客户。随着我们的反应式微服务的部署,我们现在需要一个客户端来调用它。

本节有助于提供关于反应式微服务的想法。我们在这一部分创建了一个 RESTful API(这并不意味着我们已经完成了微服务)。为了简单起见,我们举了一个单一服务的例子。这些服务将由客户端直接使用或通过应用编程接口网关使用。

接下来,我们将讨论将使用这项服务的客户。

客户–编码

在 AutoRest 的帮助下,我们将创建一个网络客户端来使用我们新部署的反应式微服务。

AutoRest is a tool that helps us to generate client libraries, so that we can access RESTful web services. AutoRest fetches the API definition from the OpenAPI Specification (Swagger).

让我们为它创建一个控制台应用,并添加这些 NuGet 包:System.Reactive.CoreMicrosoft.AspNet.WebApi.ClientMicrosoft.Rest.ClientRuntimeNewtonsoft.Json:

  1. AutoRest 将在主项目中添加一个名为Models的文件夹,并创建模型产品和类别的副本,这是我们刚刚创建的服务。它将内置必要的反序列化支持。
  2. ProductOperations.csProductServiceClient.cs包含呼叫所需的主要管道。
  3. Program.cs文件的Main功能中,更改Main功能如下:
 static void Main(string[] args)
 {
   var client = new ProductServiceClient {BaseUri = 
   new Uri("http://localhost:22651/")};
   var products = client.Product.Get();
   Console.WriteLine($"Total count {products.Count}");
   foreach (var product in products)
   {
     Console.WriteLine($"ProductId:{product.Id},Name:
     {product.Name}");
   }
   Console.Write("Press any key to continue ....");
   Console.ReadLine();
 }  

此时,如果数据库没有被创建,那么它将按照实体框架的要求被创建。

我们需要知道这个从微服务返回的列表与常规列表有何不同。答案是,如果这是一个非反应性的场景,并且如果您要对列表进行任何更改,那么它将不会反映在服务器中。在被动微服务的情况下,对这样的列表所做的更改将被保存到服务器上,而不必经历手动跟踪和更新更改的过程。

You can use any other client to make the Web API call (for example, RestSharp or HttpClient).

你可能已经注意到,当遇到混乱的回调时,我们只需要做很少的工作或者根本不做任何工作。这有助于保持我们的代码干净和易于维护。在可观察的情况下,当值可用时,是生产者推动这些值。此外,这里还有一个客户端没有意识到的区别:您的实现是阻塞的还是非阻塞的。对客户来说,这一切似乎都是异步的。

你现在可以专注于重要的任务,而不是弄清楚接下来要打什么电话,或者哪些电话你完全错过了。

编写或创建服务并不能完成任务。应该使用这些服务,或者,如果创建这些服务的目的是作为业务逻辑,而不是向客户端请求任何东西,那么它们应该用于相互通信,这意味着这些服务相互调用。在大多数场景中,服务将由客户端应用使用,如我们示例中的产品服务。为了向您展示如何使代码变得简单,并演示主题,我们创建了一个控制台应用。

摘要

在这一章中,我们在基于微服务的架构中增加了反应式编程的方面。这种消息驱动的微服务相互通信的方法存在权衡。然而,与此同时,当我们进一步推进我们的微服务架构时,这种方法倾向于解决一些基本问题。事件源模式拯救了我们,让我们摆脱了 ACID 事务或 2PC 选项的限制。这个话题可以大大扩展。我们使用我们的示例应用来理解如何以一种被动的方式重构我们的初始微服务。

在下一章中,我们将准备好整个应用供我们探索,我们将把到目前为止在本书中讨论的所有内容放在一起。

问题

  1. 什么是反应式微服务?
  2. 什么是消息级安全性?
  3. 什么是自动测试?

进一步阅读

你刚刚读完这一章,但这不是我们学习曲线的终点。以下是一些参考资料,可以增强您对测试相关主题的了解:

十、设计模式和最佳实践

在任何编程语言中,总是建议实现模式和最佳实践。对于这样的实现,设计模式非常常用,因为它们有助于使代码可重用,允许代码片段与其他部分和组件很好地匹配。这些都是非常重要的技术,也是微服务生态系统中的游戏规则改变者。在本章中,我们将介绍一些高级设计模式和最佳实践,它们将帮助我们设计真实世界的微服务应用。

本章将涵盖以下主题:

  • 聚合器模式
  • 共享数据微服务模式
  • 反腐败层模式

除了聚合器模式、共享数据微服务模式和反腐败层模式之外,我们已经在本书前面的章节中介绍了设计模式的实现,我们将在这里用代码示例深入介绍这些模式。

技术要求

本章包含一些代码示例来解释我们将要看到的模式的概念。代码保持简单,仅用于演示目的。这些示例涉及一个用 C#编写的. NET Core 控制台应用。

以下是运行和执行代码的先决条件:

  • Visual Studio 2019
  • 正在设置.NET Core

安装 Visual Studio 2019

要安装和运行这些代码示例,您需要安装 Visual Studio 2019 或更高版本(首选 IDE)。为此,请遵循以下说明:

  1. 按照安装说明,从https://docs . Microsoft . com/en-us/Visual Studio/install/install-Visual Studio下载 Visual Studio 2019(社区版免费)。
  2. 按照操作系统的安装说明进行操作。Visual Studio 安装有多个版本。我们使用的是视窗操作系统。

如果你没有。安装了 NET Core 3.1,可以从https://www.microsoft.com/net/download/windows下载设置。

The complete source code is available at https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2010.

为了开始本章中设计模式的旅程,我们将首先了解聚合器模式,然后检查问题解决方案的上下文。

聚合器模式

在微服务中,我们倾向于将我们的业务特征分解成作为独立服务的小项目,这些服务托管在完全不同的服务器上。每个服务都有自己的信息(有时服务共享一个数据库),这些服务的传入数据包含这些信息。有时,有必要混合来自服务的数据的细节。该数据要求顾客和该工作/任务仅在服务级别完成。数据协作是系统的责任,而不是客户的责任。

为了处理这种情况,我们可以使用聚合器模式。顾名思义,它聚合或组合信息并返回最终响应。在聚合器模式的帮助下,我们将混合两个或更多服务的响应,应用任何业务逻辑(如果需要的话),然后返回一个组合响应。

我们可以在复合微服务的帮助下实现聚合器模式。当我们需要聚合来自多个微服务的数据,然后在应用各种业务规则后将响应返回给消费者时,复合服务发挥了重要作用。

Composite microservices may also be called UI composite services. These services take the output of two or more microservices, apply the business logic, and provide the combined result as a final response for the consumers. These consumers might be internet applications, mobile devices, or other consumers of the response.

在更常见的场景中,如果我们不需要对数据应用任何业务逻辑,那么我们可以简单地使用 API 网关来聚合数据并将其返回给消费者。

利弊

您应该在以下情况下使用聚合器模式:

  • 需要多个后端服务来处理或执行客户端请求。
  • 客户端的网络不稳定,例如,他们使用网络中延迟时间较长的移动设备发送请求。

以下是这种模式可能不适合的情况:

  • 当我们需要打一个电话以便减少多次通话时。当客户端调用单个服务但进行多个操作时,就会出现这种情况。在这种情况下,最好为被调用的服务实现批处理操作。
  • 当网络延迟非常低时。在这种情况下,不应该使用聚合器模式,因为它不会给操作带来任何好处。

应遵循的最佳实践

如果我们想要实现这种模式,我们应该使用以下最佳实践:

  • 当我们开始实现这种模式时,数据隔离是最佳实践之一。鉴于我们想象中的应用 FlixOne,这一点非常重要,我们已经在整本书中讨论过了(当我们将单片应用转换为微服务时)。但是,在复合微服务的帮助下,实现这种模式时可能已经注意到了这一点(因为每个微服务都有自己的数据库),但是可能仍然需要为每个服务隔离数据库。
  • 我们将在示例中采用的另一个最佳实践是将服务作为面向客户端的服务和内部服务来管理。我们将在报价服务(面向客户的服务)以及产品和供应商服务(内部服务)的帮助下实现这一点。

让我们看看这些最佳实践的实现在我们的 FlixOne 应用中会是什么样子。

问题及其解决方案——以 FlixOne 为例

在我们想象的应用中,我们有两种不同的服务:一种是产品服务,另一种是供应商服务。这两种服务都是细粒度的,并使用它们的数据库。为此,我们需要来自客户(消费者)的两种服务的数据。这方面的业务逻辑处理以下两个标准,在将响应发送回消费者之前,我们需要满足这两个标准:

  • 回复应包含供应商的完整信息。
  • 应该指定产品价格。

为了满足这些标准,我们需要创建一个复合微服务,它将聚合产品服务和供应商服务的响应。在应用业务逻辑后,这些聚合数据将返回给消费者:

上图是复合服务的图示。产品服务厂商服务使用自己的数据库,提供的服务依赖于这两个服务的信息和数据。最后,提供服务将这些数据/信息合并并发送回客户端。在这里,我们的提供服务充当聚合器。

既然我们知道了这个问题,我们将看看解决方案,并使用代码示例来实现它。

实施解决方案

在本节中,我们将创建一个复合微服务并实现聚合器模式。请重新访问技术要求部分,检查该应用的先决条件。按照以下步骤创建我们的服务:

  1. 打开 Visual Studio。

  2. 转到开始|创建新项目。您也可以在此屏幕中单击“无代码继续”链接;在这种情况下,您需要单击文件|新建项目来创建新项目。

  3. 选择 ASP.NET Core 网络应用,然后单击下一步。

  4. 输入项目名称,选择路径,然后单击创建。

  5. 在下一个屏幕上,选择一个应用编程接口,然后单击创建;确保您已经选择.NET Core 和 ASP.NET Core 3.1。

  6. 为了使示例项目易于演示,我添加了文件夹和文件,如下图所示:

前面的截图显示了我们的 FlixOne。书店。提供服务解决方案。这里面有几个文件夹,我们现在详细研究一下,分别是CommonControllerModelsPersistenceServices

公共文件夹

这个文件夹包含了我们所有的常用操作。在我们的应用中,我们添加了一个API.cs文件,其中包含为DealVendor微服务创建服务端点的方法。

以下代码用于收集服务端点:

public static class Deal
{
    public static string GetAllDeals(string baseUri) => $"{baseUri}";
    public static string GetDeal(string baseUri, string id) => $"{baseUri}/{id}";
}

在前面的代码中,我们创建了一个名为Deal的静态类。这个类有静态方法,即GetAllDealsGetDeal。这些方法将各种参数与baseUri连接起来,生成服务端点,并将其作为完整的baseUristring返回。

The code aims to demonstrate the Aggregator pattern (using composite microservices). It does not focus on covering complete CRUD operations, but instead, it focuses on explaining the pattern, using the Get operation.

以下代码属于Vendor类:

public static class Vendor
{
    public static string GetList(string baseUri) => $"{baseUri}";
    public static string GetVendor(string baseUri, string id) => $"{baseUri}/{id}";
}

前面的代码生成服务端点来获取Vendor列表,并基于Vendor ID获取供应商记录。

DealVendor类简单易懂。这些类只形成一串服务端点。为了使我们的代码简单,我们使用这个方法来形成服务端点的字符串,但是您可以通过将端点放在配置文件、环境或数据库中来实现这一点。在接下来的部分中,我们将看到如何使用这些方法。

控制器文件夹

这个文件夹包含我们的作为服务公开的应用编程接口控制器。下面的代码对此进行了解释:

[HttpGet("{dealId}/{vendorId}")]
[ProducesResponseType(typeof(Models.Offer), 200)]
public async Task<Models.Offer> GetOffer(string dealId, string vendorId)
{
    var res = await _repository.Get(dealId, vendorId);
    return res;
}

我们在OfferController中添加了一个GET资源,获取一个Offer对象的记录。Offer对象旨在聚合来自VendorDeal模型的数据。

下面的代码展示了如何聚合来自DealVendor模型的数据:

public Offer Get()
{
    Offer offer = new Offer();
    if (_deal != null && _deal.StartOn.HasValue)
    {
        offer.OfferCode = _deal.Id.ToString();
        offer.OfferDetails = $"Offer:{_deal.Name}, {_deal.Description}";
        offer.OfferBanner = _deal.Image;
        offer.Discount = _deal.OfferPrice;
        offer.OfferValidTill = _deal.EndOn;
    }
    else
    {
        offer.OfferDetails = "Deal is not available.";
    }
    if (_vendor != null)
    {
        offer.VendorCode = _vendor.Code;
        offer.VendroName = _vendor.Name;
        offer.AccessURL = _vendor.URL;
        offer.VendorLogo = _vendor.Logo;

        if (_vendor.Address != null)
        {
            var address = _vendor.Address;
            offer.VendorAddress = $"{address.AddressLine1} {address.AddressLine2}, {address.City}, {address.State}, {address.PIN}";
        }

    }

    return offer;
}

在前面的代码中,根据我们的业务需求,我们实现了逻辑并组合了数据,因此它将作为一个对象可用,具有以下属性:

public string OfferCode { get; set; }
public string OfferDetails { get; set; }
public string OfferBanner { get; set; }
public decimal Discount { get; set; }
public DateTime OfferValidTill { get; set; }
public string VendorCode { get; set; }
public string VendorName { get; set; }
public string AccessURL { get; set; }
public string VendorLogo { get; set; }
public string VendorAddress { get; set; }

下面的列表解释了上述属性:

  • OfferCode:包含唯一的标识,代表Deal型号的记录标识。
  • OfferDetails:包含Deal型号的名称和描述。
  • Offerbanner:包含交易图像的Base64字符串。
  • Discount:包含供应商在交易中提供的折扣。
  • OfferValidTill:包含Deal有效期的日期和时间。
  • VendorCode:包含提供交易的供应商的代码。
  • VendorName:包含提供交易的供应商的名称。
  • AccessURL:包含所提供交易的网址。这代表供应商为报价提供的网址
  • VendorLogo:包含Vendor标志的Base64串。

模型文件夹

这个文件夹包含模型类,可以帮助我们保存和转置模型对象数据。让我们看看下面的代码:

public class Address
{
    public Guid Id { get; set; }
    public string AddressLine1 { get; set; }
    public string AddressLine2 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
    public string PIN { get; set; }
    public Guid VendorId { get; set; }
}

前面的代码包含Address类及其属性来维护用户的地址(Vendor)。这些属性解释如下:

  • Id:包含唯一标识,代表Address型号的记录标识
  • AddressLine1:包含Vendor的地址详情
  • AddressLine2:包含Vendor的地址详情
  • City:包含Vendor的城市名称
  • State:包含Vendor的州名
  • Country:包含Vendor的国家名称
  • PIN:包含邮政索引号 ( PIN )
  • VendorId:包含唯一标识,代表Vendor型号的记录标识

让我们考虑以下代码:

public class Deal
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Image { get; set; }
    public decimal OfferPrice { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime? StartOn { get; set; }
    public DateTime? EndOn { get; set; }
}

前面的代码包含Deal类及其属性,以维护我们用户的可用交易。这些属性解释如下:

  • Id:包含唯一的 ID,代表Deal型号的记录 ID。
  • Name:包含交易名称。
  • Description:包含交易的描述。
  • Image:包含交易形象的Base64字符串。
  • OfferPrice:包含交易价格。
  • CreatedOn:包含交易创建的日期。
  • StartOn:包含交易开始的日期。这是一个可以为空的属性:如果是null,那就意味着交易还没有开始。
  • EndOn:包含交易达成的日期。这是一个可空属性:如果是null,那么交易永远不会过期。

让我们考虑以下代码:

public class Vendor
{
    public Guid Id { get; set; }
    public string Code { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string URL { get; set; }
    public string Logo { get; set; }
    public DateTime AddedOn { get; set; } = DateTime.UtcNow;
    public bool Active { get; set; }
    public Address Address { get; set; }
}

前面的代码包含Vendor类,其属性对应于Vendor细节。这些属性解释如下:

  • Id:包含唯一的 ID,代表Vendor型号的记录 ID。
  • Code:包含供应商代码;该代码对于每个Vendor都是唯一的。
  • Name:包含Vendor的名称。
  • Description:包含对Vendor的描述。
  • URL:包含Vendor网站的网址。
  • Logo:包含Vendor标志的Base64串。
  • AddedOn:系统中Vendor首次上船的日期。
  • Active:包含TrueFalse值。如果包含True,则表示Vendor处于活动状态;如果包含False,则Vendor未激活。
  • Address:包含代表Address型号的Vendor地址信息。

接下来,我们将讨论PersistenceServices如何帮助我们实现聚合器模式。

持久性文件夹

这个文件夹包含我们的存储库,它们提供 CRUD 操作并返回聚合的数据。让我们看看下面的代码:

public class OfferRepository : IOfferRepository
{
    private readonly IDealService _dealService;
    private readonly IVendorService _vendorService;
    public OfferRepository(IDealService dealService, IVendorService vendorService)
    {
        _dealService = dealService;
        _vendorService = vendorService;
    }
    public async Task<Models.Offer> Get(string dealId, string vendorId)
    {
        Deal deal = await _dealService.GetDeal(dealId);
        Vendor vendor = await _vendorService.GetBy(vendorId);
        var offer = new Models.Offer(deal, vendor);
        return offer.Get();
    }
    …
}

前面的代码给了我们从Deal微服务和Vendor微服务获取数据的能力,然后我们将组合数据作为Offer的对象返回。

以下代码注册了存储库和Appsettings,以便我们可以在需要时访问配置值:

//Configure AppSettings
services.AddOptions();
services.Configure<AppSettings>(Configuration);
//Register repository
services.AddTransient<IOfferRepository, OfferRepository>();

服务文件夹

该文件夹包含我们的服务,并提供聚合这些服务的响应的能力。以下代码来自DealService。这项服务为我们提供了可用的交易:

public class DealService : IDealService
{
    private HttpClient _httpClient;
    private readonly IOptions<AppSettings> _settings;
    private readonly string _baseURL;
    public DealService(HttpClient httpClient, IOptions<AppSettings> settings)
   {
        _httpClient = httpClient;
        _settings = settings;
        _baseURL = $"{settings.Value.DealUrl}/api/deal";
    }
    public async Task<List<Deal>> GetDeals()
    {
        var endPoint = API.Deal.GetAllDeals(_baseURL);
        var resString = await _httpClient.GetStringAsync(endPoint);
        var response = JsonConvert.DeserializeObject<List<Deal>>(resString);
        return response;
    }
    public async Task<Deal> GetDeal(string id)
    {
        var endPoint = API.Deal.GetDeal(_baseURL,id);
        var resString = await _httpClient.GetStringAsync(endPoint);
        var response = JsonConvert.DeserializeObject<Deal>(resString);
        return response;
    }
    …
}

在前面的代码中,GetDeal方法调用Deal微服务,并将响应返回给我们的存储库进行进一步处理。

为了使用微服务的远程 URL,我们在配置文件中添加了密钥,如下所示:

{
  "DealUrl": "http://localhost:52786",
  "VendorUrl": "http://localhost:52788",
  …
}

前面的代码有两个键,DealUrlVendorUrl,带有一个远程 URL。这些值可以根据您的设置进行更改。

我们用AppSettings.cs文件映射了这些键,如下所示:

public class AppSettings
{
    public string DealUrl { get; set; }
    public string VendorUrl { get; set; }
}

我们已经将该文件映射到我们的Startup.cs文件,因此我们可以访问如下配置值:

public DealService(HttpClient httpClient, IOptions<AppSettings> settings)
{
    _httpClient = httpClient;
    _settings = settings;

    _baseURL = $"{settings.Value.DealUrl}/api/deal";
}

以下代码来自VendorService:

public class VendorService : IVendorService
{
    private HttpClient _httpClient;
    private readonly IOptions<AppSettings> _settings;
    private readonly string _baseURL;
    public VendorService(HttpClient httpClient, IOptions<AppSettings> settings)
    {
        _httpClient = httpClient;
        _settings = settings;
        _baseURL = $"{settings.Value.VendorUrl}/api/vendor";
    }
    public async Task<List<Vendor>> GetAll()
    {
        var endPoint = API.Vendor.GetList(_baseURL);
        var resString = await _httpClient.GetStringAsync(endPoint);
        var response = JsonConvert.DeserializeObject<List<Vendor>>(resString);
        return response;
    }
    public async Task<Vendor> GetBy(string id)
    {
        var endPoint = API.Vendor.GetVendor(_baseURL,id);
        var resString = await _httpClient.GetStringAsync(endPoint);
        var response = JsonConvert.DeserializeObject<Vendor>(resString);
        return response;
    }
    …
}

前面代码中的GetBy方法调用Vendor微服务,获取响应,并将其返回到OfferRepository进行进一步处理。

最后,当客户端消费OfferService时,我们将从Offer服务获得聚合数据作为响应。创建该服务是为了演示聚合器模式,使用Composite微服务,但不使用应用编程接口网关。

本节旨在汇总两种不同服务(也称为复合服务)的响应。我们已经通过一个代码示例讨论了聚合器模式,并且我们在不使用 API 网关的情况下实现了代码。在下一节中,我们将讨论共享数据微服务模式,实现两个共享公共数据库的独立服务。

共享数据微服务模式

共享数据微服务模式可以被视为微服务上下文中的反模式。我们也可以说共享数据模式是最有争议的模式之一。

The great concept behind this pattern is that it uses the same physical structure for data storage. This pattern can be used when there is some doubt about the structure of the data, or when the communication layer between the microservices is not well defined.

一方面,它是反模式;但是,另一方面,却是最有利的格局。关于这种数据模式,您应该记住以下几点:

  • 作为反模式的共享数据模式:当我们谈论关于微服务的这种模式时,这种模式当然会被称为反模式。这种模式不适合从头开始开发的应用(也称为绿地应用)。这里有一个非常基本的概念,如果我们正在开发任何新的应用,那么我们应该考虑应用的设计和模式的最佳实践,也要考虑数据库设计。在这种情况下,如果我们试图实现共享数据模式,我们将不会得到任何好处;在这种情况下,它只是一个反模式。
  • 作为适当模式的共享数据模式:当我们在使用微服务的同时将任何遗留应用(也称为褐地应用)迁移到新的应用时,这个模式是使用的适当模式之一。

在这本书里,我们从一开始就使用了共享数据模式,我们开始转换整体应用。然后,我们在活动期间以下列方式调整了共享数据模式:

  • 打破单一应用
  • 在单一应用过渡期间重新考虑和开发新的微服务
  • 数据编排

模式的利弊

共享数据模式被认为是反模式和有利模式,这取决于实现它的开发人员。在大多数情况下,开发人员认为这是一个旧的概念,因此他们建议不再使用这种模式。但是,关于这种模式还有各种各样的其他想法,所以我们可以说这种模式既有优点也有缺点,列举如下:

  • 随着技术的发展,我们的应用也应该得到加强;这里有许多需要迁移或升级的遗留应用。对于需要升级以实现自动化、可伸缩性和弹性的传统应用,共享数据模式是最受欢迎的选择之一。
  • 共享数据模式的积极好处是,它有助于给开发团队时间从数据库中分离信息并评估数据的一致性。
  • 此外,当开发人员使用架构项目时,它有助于重置架构项目。
  • 关于共享数据模式的负面影响,最常见的影响是,通过使用这种模式,所有的微服务都存放在存储上。

应遵循的最佳实践

总的来说,没有存储的应用会少得多。数据库的主要目标是只保存或存储数据。作为开发人员,我们不应该在数据库中存储任何其他信息。但是,出于某种原因,我们可能会存储一些业务规则或任何应该成为应用代码一部分的信息。如果我们将业务规则存储在数据库中,那么我们将推动我们的应用变得依赖于我们的数据库,这将阻碍数据迁移和分发的过程。

一个常见的错误是,有时候,开发团队采用使用触发器的过程,使用他们自己的数据库资源,或者工作人员观察存储信息的变化。问题是这些触发器很难监控和调试,而且它们也是为存储创建业务规则的一种方式。

在这种模式的帮助下,我们可以使我们的遗留应用像绿地应用一样,或者,换句话说,在微服务的上下文中,共享数据模式是帮助重置架构项目的有利选择之一。共享数据模式是最有争议的模式之一,这也是事实,许多开发人员认为这是一种较旧的技术,现在不应该使用。作为一个反模式直接表明这不应该被使用。

关于这个主题有不同的观点,现在我们将从我们想象中的应用 FlixOne Store 来看它,它已经从一个单一的应用转变而来。这是展示共享数据模式的最佳实践和用法的最佳示例之一。

问题及其解决方案——以 FlixOne 为例

FlixOne 团队在提取订单细节时遇到了一个新问题。该团队面临这样一个场景,两个不同的服务要求他们将相似的信息保存到数据库中。目前,它正在复制信息,因为客户服务和订单服务都使用自己的数据库。当业务团队提出为客户增加钱包功能的想法时,团队发现了一个问题。根据这项功能,客户可以在他们的 FlixOne 帐户中拥有钱包点数。每当客户从 FlixOne 门户订购/购买任何产品时,其发票的计算应遵循以下规则:

  1. 在计算订单的网上支付之前,应该检查钱包余额。

  2. 如果钱包余额是信用余额(信用余额将大于零),那么首先使用钱包余额,然后计算网上支付。

  3. 一旦计算出网付,调整钱包余额。

  4. 钱包余额不能为负(小于零)。

为了解决这个问题,FlixOne 团队决定为这两种服务使用一个数据库。要详细了解它,请考虑下图:

在上图中,我们的两个服务订单服务客户服务共享同一个数据库。这些服务和数据库之间的每一次交互都通过事务管理器进行。这样,一个服务可以使用正确的数据,而另一个服务正在使用这些数据。例如,如果我们的订单服务消费了钱包金额(信用),那么我们的客户服务应该在使用该金额后返回正确的钱包金额(信用)。

现在,是时候在我们的 FlixOne 应用中实现这个解决方案了。

实施解决方案

为了实现上述问题的解决方案,我们将首先创建OrderService:

  1. 打开 Visual Studio。
  2. 转到开始|创建新项目。您也可以在此屏幕中单击“无代码继续”链接。在这种情况下,您需要单击文件|新建项目来创建新项目。
  3. 选择 ASP.NET Core 网络应用,然后单击下一步。
  4. 输入项目名称,选择路径,然后单击创建。
  5. 在下一个屏幕上,选择一个 ASP.NET Core MVC 模板,然后单击创建。确保您已经选择.NET Core 和 ASP.NET Core 3.1。

为了帮助我们遵循代码及其实现,我添加了文件和文件夹,现在解决方案如下:

前面的截图由几个默认文件组成。除了这些文件之外,我们还添加了更多的文件和文件夹,稍后我们将对此进行讨论。在我们进入更多细节之前,首先让我们确保我们已经添加了 NuGet 包Microsoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore。这些包将帮助我们使用实体框架核心的特性。另外,添加 NuGet Swashbuckle.AspNetCore包,这样我们就可以将文档添加到我们的 API 中。

为此,请转到工具|否获取包管理|包管理器控制台。在包管理器控制台中执行以下代码:

Install-Package Swashbuckle.AspNetCore

上面的代码为斯瓦格安装了软件包。同样,您应该安装所有其他软件包。

现在,让我们详细讨论所有的文件夹和文件。

扩展文件夹

这个文件夹包含将我们的Models转置到ViewModels的代码,反之亦然。该文件夹包含Transpose.cs文件,以帮助在OrderOrderItems模型与视图模型的OrderViewModel之间进行置换。

这些模型代表数据库表,如下图所示:

让我们讨论这些模型和视图模型,如下面的代码所示:

public static Models.Order ToModel(this Models.OrderViewModel vm)
{
    return new Models.Order
    {
        Id = vm.OrderId,
        CustomerId = vm.CustomerId,
        Date = vm.Date,
        StatusCode = vm.StatusCode,
        StatusDesc = vm.StatusDesc,
        Tax = vm.Tax,
        Total = vm.Total,
        NetPay = vm.NetPay,
        Items = vm.Items.ToModel()
    };
}

public static IEnumerable<Models.Order> ToModel(this IEnumerable<Models.OrderViewModel> vm) => vm.Select(ToModel);

前面的代码包含ToModel方法。这将我们的OrderViewModel转换为Order模型;一种方法转换成一条记录,而其他方法转换成一个列表:

public static Models.OrderViewModel ToViewModel(this Models.Order model)
{
    return new Models.OrderViewModel
    {
        CustomerId = model.CustomerId,
        Date = model.Date,
        OrderId = model.Id,
        StatusCode = model.StatusCode,
        StatusDesc = model.StatusDesc,
        Tax = model.Tax,
        Items = model.Items.ToViewModel()
    };
}
public static IEnumerable<Models.OrderViewModel> ToViewModel(this IEnumerable<Models.Order> model) => model.Select(ToViewModel);

前面的代码包含ToViewModel方法。这就把我们的Order转到了OrderViewModel;一种方法转换成一条记录,而其他方法转换成一个列表。

模型文件夹

这个文件夹包含模型类,可以帮助我们保存和转置模型对象数据。让我们看看下面的代码:

public abstract class BaseEntity
{
    [Key]
    public Guid Id { get; set; }
    public DateTime DateAdded { get; set; } = DateTime.UtcNow;
    public DateTime? DateModified { get; set; }
}

前面的代码包含一个抽象类BaseEntity。这将是我们所有模型的基类。它包含以下属性:

  • Id:包含一个唯一的 ID,代表继承这个类的所有模型的记录 ID。
  • DateAdded:包含添加记录的日期;其默认值为当前DateTime
  • DateModified:包含记录修改的日期;它也可以包含空值。

让我们考虑以下代码,对于Customer模型:

public class Customer : BaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime MemberSince { get; set; }
    public decimal Wallet { get; set; }
    public string FullName => LastName + " " + FirstName;
}

前面的代码包含Customer模型,它继承了BaseEntity类。Customer模型包含如下详细属性,包括BaseEntity的属性:

  • FirstName:包含客户的FirstName
  • LastName:包含客户的LastName
  • MemberSince:包含客户加入FlixOne门户时的DateTime
  • Wallet:包含贷方金额。
  • FullName:包含全名,串联LastNameFirstName

让我们考虑以下代码,对于Address模型:

public class Address : BaseEntity
{
    public Guid CustomerId { get; set; }
    public string AddressLine1 { get; set; }
    public string AddressLine2 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
    public string PIN { get; set; }
}

前面的代码包含Address模型,它继承了BaseEntity类。Address模型包含如下详细属性,包括BaseEntity的属性:

  • CustomerId:包含唯一的 ID,代表Customer型号的记录 ID。
  • AddressLine1:包含客户的地址。
  • AddressLine2:包含客户地址的第二行(如果适用)。
  • City:包含客户所在的城市。
  • State:包含客户的状态。
  • Country:包含客户所在国家。
  • PIN:包含客户地址的个人识别码。

让我们考虑以下代码,对于Order模型:

public class Order : BaseEntity
{
    public Order()
    {
        Items = new List<OrderItem>();
    }
    public Guid CustomerId { get; set; }
    public string StatusCode { get; set; }
    public string StatusDesc { get; set; }
    public DateTime Date { get; set; }
    public decimal Total { get; set; }
    public decimal Tax { get; set; }
    public decimal NetPay { get; set; }
    public IEnumerable<OrderItem> Items { get; set; }
}

前面的代码包含Order模型,它继承了BaseEntity类。Order模型包含如下详细属性,包括BaseEntity的属性:

  • CustomerId:包含唯一的 ID,代表Customer型号的记录 ID。
  • StatusCode:包含OrderStatusCode
  • StatusDesc:包含状态详细信息。
  • Date:包含Order日期。
  • Total:包含Order总数。
  • Tax:包含Tax的量,如果应用的话。
  • NetPay:包含OrderNetPay数量。
  • Items:包含OrderItems的列表。

让我们考虑以下代码,对于OrderItem模型:

public class OrderItem : BaseEntity
{
    public Guid OrderId { get; set; }
    public Guid ProductId { get; set; 
    public int Sequence { get; set; }
    public string Name { get; set; }
    public string ImagePath { get; set
    public decimal UnitePrice { get; s
    public decimal Discount { get; set
    public int Qty { get; set; }
    public decimal Total { get; set; }
}

前面的代码包含OrderItem模型,它继承了BaseEntity类。OrderItem模型包含属性,具体如下,包括BaseEntity的属性:

  • OrderId:包含唯一的 ID,代表Order型号的记录 ID。
  • ProductId:包含唯一的 ID,代表Product型号的记录 ID。
  • Sequence:包含订单项目的序列号。
  • Name:包含项目的Name
  • ImagePath:包含项目的图像路径。
  • UnitPrice:包含项目的UnitPrice
  • Discount:包含物品的Discount数量,如果有的话。
  • Qty:包含项目的Qty
  • Total:包含项目合计,计算为Qty * UnitPrice - Discount

除了讨论的模型,我们还有OrderViewModelOrderItemViewModel,讨论如下:

public class OrderViewModel
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public string StatusCode { get; set; }
    public string StatusDesc { get; set; }
    public DateTime Date { get; set; }
    public decimal Total { get { return Items.Sum(i => i.Total); } }
    public decimal Tax { get; set; }
    public decimal WalletBalance { get; set; }
    public decimal NetPay { get { return (Total + Tax) - WalletBalance; } }
    public IEnumerable<OrderItemViewModel> Items { get; set; }
}

OrderViewModel前面的代码包含以下属性:

  • OrderId:包含唯一的标识,代表Order的记录标识。
  • CustomerId:包含唯一的标识,代表Customer的记录标识。
  • StatusCode:订单的状态码。
  • StatusDesc:订单的状态信息。
  • Date:OrderDate
  • Total:持有OrderTotal
  • Tax:金额Tax,如果适用。
  • WalletBalance:持有信用额度,保留在客户的Wallet中。
  • NetPay:调整WalletBalanceOrder上的到期金额。
  • Items:列表OrderItems

让我们考虑以下代码,对于OrderItemViewModel:

public class OrderItemViewModel
{
    public Guid OrderItemId { get; set; }
    public Guid OrderId { get; set; }
    public Guid ProductId { get; set; }
    public int Sequence { get; set; }
    public string Name { get; set; }
    public string ImagePath { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Discount { get; set; }
    public int Qty { get; set; }
    public decimal Total { get { return (Qty * UnitPrice) - Discount; } }
}

前面的代码包含OrderItemViewModel,它具有以下属性:

  • OrderItemId:包含OrderItem的唯一标识。
  • OrderId:包含Order的唯一标识。
  • ProductId:包含Product的唯一标识。
  • Sequence:这是订购物品的序列号。
  • Name:订单项目名称。
  • ImagePath:图像路径。
  • UnitPrice:物品的单价。
  • Discount:商品的折扣金额,如果有的话。
  • Qty:物品的数量。
  • Total:项目的Total金额,计算为Qty * UnitPrice - Discount

持久性文件夹

这个文件夹包含我们的存储库,这些存储库提供了所需的 CRUD 操作以及适当的业务规则。

As per our requirement, we have implemented the business logic in our repository's classes, but, in most cases, we'd require separate classes that contain the business logic/business rules, and so on.

让我们考虑以下代码:

public IEnumerable<Models.Order> List() => _context.Orders.Include(o => o.Items).ToList();

public Models.Order Get(Guid id) => _context.Orders.Include(o => o.Items).FirstOrDefault(o => o.Id == id);

前面的代码包含两种方法,ListGet。两种方法都提取Order数据,其中List提取所有可用订单,Get基于数据库中的OrderId提取单个订单。

让我们考虑以下代码:

public void Add(Models.Order order)
{
    using (var transaction = _context.Database.BeginTransaction())
    {
        try
        {
            var customer = _context.Customers.Where(c => c.Id == order.CustomerId).FirstOrDefault();
            var walletBalance = customer.Wallet;
            if(walletBalance > 0)
            {
                if(walletBalance >= order.NetPay)
                {
                    order.NetPay = 0M; //Deduct total payment from Wallet Balance
                    customer.Wallet = walletBalance - order.NetPay; //Deduct amount from wallet and save the remaining amount
                }
                else
                {
                    order.NetPay = walletBalance - order.NetPay; //partially deduct amount from wallet
                    customer.Wallet = 0M; // empty the wallet
                }
                //Update customer to reflect new/updated Wallet balance
                _context.Customers.Update(customer);
            }
            _context.Orders.Add(order);
            _context.SaveChanges();

            transaction.Commit();
        }
        catch (Exception)
        {

            throw;
        }
    }
}

前面的代码包含Add方法,这个方法帮助我们将新的Order插入到数据库中。你会发现Add方法包含了WalletBalance调整的业务规则(为了代码解释和演示的目的,为了让代码更简单易懂,我将这些业务规则放在了Add方法中)。该方法遵循以下业务规则:

  • 验证WalletBalance是否有足够的信用额度:WalletBalance > 0
  • 调整信用金额后,计算到期金额:order.NetPay = walletBalance - order.NetPay;
  • 进行调整后,重置WalletBalance数量:customer.Wallet = 0M;

需要注意的最重要的一点是,前面代码中的Add方法完全在事务using (var transaction = _context.Database.BeginTransaction())中运行。在本方法的操作过程中,不会有机会获得CustomerService的错误数据(当本服务需要数据时)。下面代码中的AddOrderItem方法是维护事务的另一个例子:

public void AddOrderItem(Models.OrderItem item)
{
    using (var transaction = _context.Database.BeginTransaction())
    {
        try
        {
            var orderItemForProduct = _context.OrderItems.Where(o => o.ProductId == item.ProductId).SingleOrDefault(); 
            if (orderItemForProduct != null)
            {
                if (item.Discount < 0)
                {
                    //discount can't be -ve leave it
                    //if there is specific case then we can through an exception
                    //and notify the user
                }
                orderItemForProduct.Discount = item.Discount;
                if (item.Qty > 0)
                {
                    orderItemForProduct.Qty += item.Qty;
                }
                orderItemForProduct.DateModified = DateTime.UtcNow;
                _context.OrderItems.Update(orderItemForProduct);
            }
            else
            {
                var orderItem = _context.OrderItems.OrderBy(o => o.Sequence).LastOrDefault();
                item.Sequence = (orderItem != null) ? orderItem.Sequence + 1 : 1;
                _context.OrderItems.Add(item);
            }

            _context.SaveChanges();

            transaction.Commit();
        }
        catch (Exception)
        {

            throw;
        }

    }
}

前面的代码包含AddOrderItem方法,它帮助我们将新排序的项目插入到表中。通过实现事务,我们确保操作被成功提交,并且在事务期间出现任何故障时,完整的操作都将被回滚。

控制器文件夹

这个文件夹包含OrderController作为我们的 API 控制器。这个OrderController将作为服务公开。下面的代码对此进行了解释:

[Route("api/v1/[controller]")]
public class OrderController : Controller
{
    private readonly IOrderRepository _orderRepository;
    public OrderController(IOrderRepository orderRepository) => _orderRepository = orderRepository;
    [HttpGet]
    public IActionResult List() => new OkObjectResult(_orderRepository.List().ToViewModel());
    [HttpGet("{id}")]
    public IActionResult Get(Guid id) => new OkObjectResult(_orderRepository.Get(id).ToViewModel());
    [HttpPost]
    public void Add([FromBody]OrderViewModel OViewModel) => _orderRepository.Add(OViewModel.ToModel());
    [HttpPost("OrderItem")]
    public void AddItem([FromBody]OrderItemViewModel item) => _orderRepository.AddOrderItem(item.ToModel());
}

前面的代码包含所有必需的 API,详细如下:

  • List:这是一个GET资源,从数据库中获取所有可用的订单。
  • Get:这是一个GET资源,根据订单 ID 获取一个Order
  • Add:一个POST资源,插入一个新的Order
  • AddItem:一个POST资源,帮助添加新订购的物品。

注意前面所有的资源都是从ToModelToViewModel调用的,后续的方法都是从OrderRepository调用的。

OrderController公开 API,详见下表:

| HTTP 方法 | API 资源 | 描述 |
| GET | /api/v1/Order | 从数据库中获取所有可用的订单 |
| GET | /api/v1/Order/{id} | 基于OrderId获取订单记录 |
| POST | /api/v1/Order | 增加新的Order |
| POST | /api/v1/Order/OrderItem | 添加新的Order项 |

上表中列出的应用编程接口资源可用于所有使用OrderServices的客户端。

In the solution, our Contexts folder contains the auto-generated files, which can be created by executing the following two commands from the Package Manager console:
 - Add-Migration FlixOneSharedDB
 - Update-Database
Also, you need to register the repositories in your startup file. Tell the system where the connection string is, which needs to connect to SQLServer. You must add the following code:
services.AddTransient<IOrderRepository, OrderRepository>();
services.AddDbContext<VendorContext>(o =>
o.UseSqlServer(Configuration.GetConnectionString("OrderConnection")));

至此,我们就完了。共享数据库有助于解决我们的问题,并在两个独立的服务之间提供数据一致性。现在,我们的两个服务都将获得正确的数据。比如WalletBalance的金额为500Order的总金额为3071。现在,每当Order被处理时,就会显示NetPay金额为2571WalletBalance0。在共享数据库的帮助下,我们解决了数据一致性问题。我们还解决了数据重复的问题,并将服务转移到了不同的表中。

通过共享数据库的实现,我们在两个独立的服务之间创建了一致性。然而,有时我们需要维护两个不同的系统来实现这一点。因此,在下一节中,我们将讨论反腐败层模式。

反腐败层模式

在这种模式中,两个不同的子系统不共享相同的语义,但是它们可以通过一个层的实现(主要是借助 Facade 或 Adapter 模式)相互对话。该层的工作方式是,一个子系统发出的请求到达另一个子系统并与之对话。在本节中,我们将从 FlixOne 应用的角度讨论反腐败层。

维护对新系统和遗留系统的访问需要新系统遵守遗留系统的至少一些应用编程接口或其他语义。如果这些遗留应用在一致性上有问题,那么它们就会破坏原本可以是一个干净的现代应用的东西。这些问题也可能出现在您想要与外部/传统系统连接的现有系统中。为了理清这些问题,我们需要使用这种模式。

考虑下图,该图显示了反腐败层的图示视图:

在上图中,我们可以看到我们有两个不同的系统:一个是LEGACY SYSTEM(subsystem-B)另一个是我们不断增长的基于微服务的subsystem-A(S1S2 )。反腐败层作为子系统-A子系统-B 之间的适配器。

利弊

虽然我们选择实现反腐败层模式,但我们应该注意,根据您的要求和实现,以下几点可能是有利的,也可能是不利的。让我们从优点开始:

  • 反腐败层模式有助于维护旧系统和新系统。
  • 它有助于在两个不同的子系统之间进行适当的转换。

与前面的优点相反,这些是反腐败层模式的缺点:

  • 反腐败层模式可能会增加系统间调用的延迟。
  • 反腐败层模式提供了一个需要管理和维护的功能。
  • 我们需要确保事务受到保护,并且可以检查数据完整性。

应遵循的最佳实践

当我们既需要新系统又需要旧系统时,以及当旧系统要经过多个阶段迁移时,这种模式是最有用的。考虑以下最佳实践:

  • 我们的子系统有完全或部分不同的语义,这取决于它们应该相互通信的要求。
  • 如果新系统和旧系统之间没有显著的语义差异,这种模式可能不适合。我们将在下一节看到为什么我们可能需要这样的模式。

问题及其解决方案——反腐败层模式的必要性

当 FlixOne 团队将装运报告引入系统时,他们发现了一个新的挑战。该报告取决于订单系统和装运系统。这两个系统完全不同,我们需要将它们同步在一起。团队开始讨论各种解决方案,经过几次头脑风暴会议,他们最终决定让实现尽可能简单。他们建议在两个系统之间实施反腐败层模式,以便装运系统能够同步并正确翻译产品数据。

现在是时候在我们的 FlixOne 应用中实现该解决方案了。

实施解决方案

为了实现该解决方案,让我们首先讨论装运系统。项目的以下快照可视化了装运系统的结构:

System1有以下文件夹: BLDALServices。在接下来的章节中,我们将讨论这些文件夹。

BL 文件夹

这个文件夹代表了我们在一个单独项目FlixOne.BookStore.Shipping.BL中的业务实体或模型,这将在下面的章节中详细介绍。

模型文件夹

这个文件夹包含模型类,可以帮助我们保存和转置模型对象数据。我们来看看BaseEntity模型的以下代码:

public abstract class BaseEntity
{
    [Key]
    public Guid Id { get; set; }
    public DateTime DateAdded { get; set; } = DateTime.UtcNow;
    public DateTime? DateModified { get; set; }
}

前面的代码包含BaseEntity抽象类,并具有以下属性:

  • Id:包含一个唯一的 ID,代表继承这个类的所有模型的记录 ID。
  • DateAdded:包含添加记录的日期;其默认值为当前DateTime.
  • DateModified:包含记录修改的日期;它也可以包含空值。

让我们考虑以下代码,对于Customer模型:

public class Customer : BaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime MemberSince { get; set; }
    public string FullName => LastName + ", " + FirstName;
}

前面的代码包含Customer模型,它继承了BaseEntity抽象类。它具有以下属性(包括BaseEntity类的属性):

  • FirstName:客户的名字。
  • LastName:客户的姓氏。
  • MemberSince:客户加入 FlixOne 的日期。
  • FullName:客户的全名。

让我们考虑以下代码,对于Order模型:

public class Order : BaseEntity
{
    public Guid CustomerId { get; set; }
    public string StatusCode {get;set;} 
    public string StatusDesc { get; set; }
    public DateTime Date { get; set; }
    public decimal Total { get; set; }
    public decimal Discount { get; set; }
    public decimal Tax { get; set; }
}

前面的代码包含Order模型,它继承了BaseEntity抽象类。它具有以下属性(包括BaseEntity类的属性):

  • CustomerId:包含唯一的 ID,代表Customer型号的记录 ID。
  • StatusCode:显示状态的代码。
  • StatusDesc:状态描述。
  • Date:订单的日期。
  • Total:订单的总金额。
  • Discount:折扣金额。
  • Tax:税额。

让我们考虑以下代码,对于OrderItem模型:

public class OrderItem:BaseEntity
{
    public Guid OrderId { get; set; }
    public Guid ProductId { get; set; }
    public string Name { get; set; }
    public string ImagePath { get; set; }
    public decimal UnitPrice { get; set; }
    public int Qty { get; set; }
}

前面的代码包含OrderItem模型,它继承了BaseEntity抽象类。它具有以下属性(包括BaseEntity类的属性):

  • OrderId:包含唯一的 ID,代表Order型号的记录 ID。
  • ProductId:包含唯一的 ID,代表Product型号的记录 ID。
  • Name:正在订购的商品名称。
  • ImagePath:物品图像路径。
  • UnitPrice:物品的单价。
  • Qty:物品的数量。

让我们考虑以下代码,对于Shipping模型:

public class Shipping:BaseEntity
{
    public Guid OrderId { get; set; }
    public string InvoiceNumber { get; set; }
    public DateTime Date { get; set; }
    public string TrackingNumber { get; set; }
}

前面的代码包含Shipping模型,它继承了BaseEntity抽象类。它具有以下属性(包括BaseEntity类的属性):

  • OrderId:包含唯一的 ID,代表Order型号的记录 ID。
  • InvoiceNumber:发票号。
  • Date:订单发货的日期。
  • TrackingNumber:发货单据的跟踪号。

让我们考虑以下代码,对于Address模型:

public class Address:BaseEntity
{
    public string AddressLine1 { get; set; }
    public string AddressLine2 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
    public string PIN { get; set; }
    public Guid CustomerId { get; set; }
}

前面的代码包含Address模型,它继承了BaseEntity抽象类。它具有以下属性(包括BaseEntity类的属性):

  • AddressLine1:客户地址的第一行。
  • AddressLine2:客户地址的第二行。
  • City:客户所在的城市。
  • State:客户的状态。
  • Country:客户所在国家。
  • PIN:邮政索引号。
  • CustomerId:包含唯一的 ID,代表Customer型号的记录 ID。

DAL 文件夹

该文件夹包含单独项目FlixOne.BookStore.Shipping.DAL中的DataAccess操作,这将在以下章节中详细介绍。

内容文件夹

该文件夹包含ShipmentDbContext.cs文件。实体框架核心DbContext帮助查询和保存实体实例。考虑以下代码:

public class ShipmentDbContext : DbContext
{
    public ShipmentDbContext(DbContextOptions<ShipmentDbContext> options)
        : base(options) { }

    public ShipmentDbContext() { }
    public DbSet<Address> Addresses { get; set; }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderItem> OrderItems { get; set; }
    public DbSet<Models.Shipping> Shippings { get; set; }
}

前面的代码由ShipmentDbContext类组成,实现DbContextDbContext代表与数据库的会话,它使用各种DbSet实例进一步帮助您与数据库交互。在前面的代码中,我们有DbSet实例,包括AddressesCustomersOrdersOrderItemsShippings。这些DbSet实例表示它们的特定实体(也称为模型)。

存储库文件夹

这个文件夹包含我们提供 CRUD 操作并返回聚合数据的存储库。让我们看看下面的代码:

public class CustomerRepository : ICustomerRepository
{
    private readonly ShipmentDbContext _context;
    public CustomerRepository() => _context = Context();
    public CustomerRepository(ShipmentDbContext context) => _context = context;
    public IEnumerable<Customer> Get() => _context.Customers.Include(c => c.CustomerAddresses).ToList();
    public Customer Get(Guid id) => _context.Customers.Include(c => c.CustomerAddresses).Where(c => c.Id == id).FirstOrDefault();
    ...
}

前面的代码有两个Get方法;一个列出所有可用的装运记录,而另一个根据装运标识获取装运记录。

In the solution, our Migrations folder contains the autogenerated files, which can be created by executing the following two commands, from the Package Manager console:
 - Add-Migration FlixOneShipmentDB
 - Update-Database
Also, you need to register the repositories in your startup file. Tell the system where the connection string is, which needs to connect to SQLServer. You must add the following code:
services.AddTransient<IOrderRepository, OrderRepository>();
services.AddTransient<ICustomerRepository, CustomerRepository>();
services.AddTransient<ICustomerRepository, CustomerRepository>();
services.AddDbContext<ShipmentDbContext>(o =>
o.UseSqlServer(Configuration.GetConnectionString("ShipmentConnection")));

服务文件夹

该文件夹包括我们在单独项目FlixOne.BookStore.Shipping.API中的业务实体或模型,这将在以下章节中详细介绍。

控制器文件夹

这个文件夹包含我们的应用编程接口控制器。这将作为服务公开;下面的代码对此进行了解释:

[HttpGet]
public IEnumerable<ShippingViewModel> Get()
{
    var shippings = _repository.Get().ToViewModel();
    foreach (var shipping in shippings)
    {
        shipping.Order = _repository.AssociatedOrder(shipping.OrderId).ToViewModel();
        shipping.Order.ShippingAddress =  _repository.ShippingAddress(shipping.Order.CustomerId).ToViewModel();

    }
    return shippings;
}

前面的代码包含一个GET资源,它列出了数据库中所有可用的运输记录。

让我们考虑以下代码:

[HttpGet("{id}")]
public ShippingViewModel Get(string id)
{
    var shipping = _repository.Get(new Guid(id)).ToViewModel();
    shipping.Order = _repository.AssociatedOrder(shipping.OrderId).ToViewModel();
    shipping.Order.ShippingAddress = _repository.ShippingAddress(shipping.Order.CustomerId).ToViewModel();
    return shipping;
}

前面的代码包括一个GET资源,它基于ShippingId获取一个运输记录。

我们的ShippingController公开了 API,详见下表:

| HTTP 方法 | Api 资源 | 描述 |
| GET | /api/Shipping | 获取数据库中所有可用运输记录的列表。 |
| GET | /api/Shipping/{id} | 获取指定ShippingId的运输记录。 |

为了获得报告,我们有反腐败层,它提供了一种创建报告的方法,有助于确保产品和运输服务顺利工作,而不会相互影响。

在本节中,我们讨论并实现了维护两个不同子系统的代码示例。我们看到在真实场景中有一个更复杂的子系统,我们会以这样一种方式实现代码,以便两个系统都能按照预期的方式工作。

摘要

在这一章中,我们已经介绍了一些可以帮助我们构建健壮的基于微服务的系统的模式。在这本书里,我们已经介绍了许多例子,这些例子都是基于本章讨论的模式。我们通过观察微服务生态系统中模式的角色和实践,遵循了每个模式的描述。

我们从聚合器模式开始,解释复合微服务,然后使用我们想象中的 FlixOne 商店应用中的一个例子来实现这个模式。然后,我们采用了共享数据模式和反腐败层模式。我们在应用的上下文中讨论了这些模式的最佳实践,并基于它们实现了解决方案。

在下一章中,我们将创建一个基于微服务架构设计的应用。

进一步阅读

  • 微服务模式和最佳实践(T2)
  • 用 C#和动手设计模式.NET Core(T2)
  • 用 C# 8 和动手操作软件架构.NET Core 3(T2)

十一、构建微服务应用

在应用开发领域,开发人员遵循各种方法,包括设计模式、设计原则等,来深入研究他们的问题解决方案,并做出好的应用。根据业务需求,当业务方法发生变化时,或者当开发人员必须遵循实际的业务需求来将应用与业务及其客户同步时,需求就会发生变化。当涉及到制作满足业务需求的应用时,有很多挑战。

发展的世界是不断发展的技术的海洋。每一个新的一天都伴随着新的流行语,预示着新技术的到来。如今,微服务架构风格已经变得很出名,它帮助我们满足了几乎所有的需求。在我看来,我们可以这样说:

"Microservices is a culture where you follow various patterns: approaches to make sure that an application is easily available to developers and their end users. This is so that developers can work in parallel on different modules or on parts of the applications, and so the end user can use hassle-free applications."

到目前为止,在这本书里,我们已经讨论了各种例子,通过观察各种方法、模式等等。本章将指导您如何创建这样一个示例应用。在介绍完我们之前介绍的所有概念和功能之后,您将能够创建一个完整的微服务应用。

本章将涵盖以下主题:

  • 引入扼杀者模式
  • 重新审视应用
  • 了解应用的业务需求
  • 构建我们的应用

技术要求

本章包含各种代码示例来解释其中的概念。代码一直保持简单,只是为了演示。大多数示例都涉及一个. NET Core 控制台应用,它是用 C#编写的。

要运行和执行本章中的代码,您需要具备以下条件:

  • Visual Studio 2019 或更高版本
  • .NET Core 3.1 已经建立

安装 Visual Studio 2019

要运行本章中的代码示例,您需要安装 Visual Studio 2019 或更高版本(我们首选的 IDE)。为此,请遵循以下说明:

  1. 从以下链接下载 Visual Studio 2019(社区免费):https://docs . Microsoft . com/en-us/visualstudio/install/install-Visual Studio
  2. 遵循中提到的安装说明。
  3. 将提供多个版本的 Visual Studio。在本章中,我们将使用 Visual Studio for Windows。

Setting up .NET Core: If you don't have .NET Core 3.1 installed, you need to download it from https://dotnet.microsoft.com/download/dotnet-core/3.1.

设置 Azure

要使用本章中的代码示例,您需要一个有效的 Azure 帐户。如果你有一个有效的帐户,你可以忽略这个部分。否则,以下步骤将帮助您设置 Azure 帐户:

  1. https://signup.azure.com/注册一个免费的 Azure 账户。
  2. 按照该页上显示的说明进行操作。
  3. 你将需要一张有效的信用卡来验证你的支付方式,以防你想使用任何付费服务(你只需要使用这本书的现收现付方式)。如果你是一名学生或教育家,在 https://azure.microsoft.com/education/有一个替代的免费版本。

The complete source code is available here: https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Chapter%2011.

重新审视整体架构风格和 SOA

第 1 章微服务介绍中,我们了解到微服务架构消除了面向服务架构 ( SOA )的大部分缺点。它也比 SOA 服务更面向代码。在您继续了解架构之前,您需要了解导致其存在的两个重要架构:

  • 整体建筑风格
  • 前进速度

我们大多数人都会意识到这样一个场景,在企业应用开发的生命周期中,决定了一个合适的架构风格。然后,在不同的阶段,最初的模式被进一步改进和调整,做出了一些改变来应对各种挑战,例如部署复杂性、大代码库和可伸缩性问题。这正是单体架构风格如何演变成 SOA,进而导致微服务的方式。

We discussed SOA and the monolithic architecture in Chapter 1An Introduction to Microservices. You can skip this section if you understand this in full. However, if you want to learn more than what we have in this chapter, please refer back to the aforementioned chapter.

单体建筑风格是一种传统的建筑类型,在业界得到了广泛的应用。monolithic 一词并不新鲜,它是从 UNIX 世界借用来的:

上图显示,在单片架构中,我们可以有各种组件,包括:

  • 用户界面:处理所有的用户交互,同时用 HTML、JSON 或任何其他首选的数据交换格式(在 web 服务的情况下)进行响应。
  • 业务逻辑:这包括应用于输入的所有业务规则,这些输入是以用户输入、事件和数据库的形式接收的。
  • 数据库访问:这包含了访问数据库的完整功能,用于查询和持久化对象。一个被广泛接受的规则是,它是通过业务模块使用的,而不是直接通过面向用户的组件。

根据我们的业务需求,我们将面临多种情况,这导致我们需要应对许多挑战。在这方面,我们可能面临以下挑战:

  • 大代码库:这是一个代码行比注释多很多的场景。由于组件是互连的,我们将不得不处理重复的代码库。
  • 业务模块太多:这是针对同一系统内的模块。
  • 代码库复杂性:由于其他模块或服务需要修复,这导致代码被破坏的几率更高。
  • 复杂的代码部署:您可能会遇到需要整个系统部署的微小变化。
  • 影响整个系统的一个模块故障:这是关于相互依赖的模块。除此之外,我们将有直接相互依赖的模块。然而,我们也会有隐藏的依赖关系,在它崩溃之前我们并不知道它(例如,如果有人忘记在项目文档中记录它)。另一方面,我们可以有间接的依赖。例如,我们可能有两个在同一个 IIS 主机上运行的虚拟目录,其中一个覆盖了部分根配置,尽管从表面上看,它们并不相互依赖。
  • 扩展性:这是整个系统都需要的,不仅仅是其中的模块。
  • 模块间依赖:这是由于紧耦合。这将导致任何模块的操作发生重大变化(如果需要)。

以上图中提到的方式使用一个服务或多个服务的系统称为 SOA 系统。SOA 和 monolithic 的主要区别不是它产生一个或多个程序集,而是它突出了应用运行的过程。由于 SOA 中的服务是作为一个独立的过程运行的,因此相比之下,SOA 的伸缩性更好。在这种方法中,服务可以是 RESTful 或 ASMX web 服务。

In the microservice architecture, services are small, independent units with their own persistent stores.

使用微服务架构风格的应用,我们可以获得许多与它们的开发和业务实现相关的好处。让我们看几个:

  • 扩展的性价比:你不需要投入很多,就可以让整个应用可扩展。就购物车而言,我们可以简单地对产品搜索模块和订单处理模块进行负载平衡,同时省去不太常用的操作服务,如库存管理、订单取消和交货确认。
  • 明确代码边界:这个动作应该与组织的部门层级相匹配。在大型企业中,不同的部门赞助产品开发,这可能是一个巨大的优势。
  • 更容易的代码更改:这是以一种不依赖于其他模块的代码,只实现隔离功能的方式完成的。如果做得对,那么一个微服务中的变化影响另一个微服务的机会是最小的。
  • 轻松部署:由于整个应用更像是一组相互隔离的生态系统,如果需要,可以一次部署一个微服务。其中任何一个的失败都不会使整个系统瘫痪。
  • 技术适配:你可以一夜之间将单个微服务或者一大堆微服务移植到不同的技术上,而你的用户甚至不知道。希望您不要指望我们会告诉您需要维护这些服务合同。
  • 分布式系统:这里隐含的意思,但需要提醒一句。确保异步调用使用良好,并且同步调用不会阻塞整个信息流。很好地使用数据分区。我们稍后再谈这个,所以暂时不要担心。
  • 快速市场反应:世界是一个竞争的地方,这是一个确定的优势。如果您对新功能请求的响应速度较慢,或者您在系统中采用新技术的速度较慢,用户往往会很快失去兴趣。

This section exists to let you revisit what we have learned about so far; you can easily skip this section if you wish.

引入扼杀者模式

在本书中,我们从整体应用过渡到基于微服务的应用,开始了我们的学习之旅。这种转变遵循扼杀者模式,我们将在本节中讨论。

简而言之,我们可以将扼杀者模式定义为通过在引入新服务/应用的同时替换特定功能来帮助我们持续迁移遗留应用(在我们的例子中,是整体应用)的模式。使用这种模式,我们用新的系统替换了之前识别的遗留应用的特性。综上所述,扼杀者模式就是在将所有必需的特性/功能迁移到新系统之后,让旧系统退役。

什么时候应该使用扼杀者模式?当您需要迁移现有的应用时,应该使用这种模式,在将特性/功能添加到新的系统或架构之前,缓慢地识别它们。

在本章中,我们将按照以下主题构建示例应用:

  1. 讨论示例应用。
  2. 业务需求是什么?
  3. 为什么我们需要申请?
  4. 结语。

在接下来的章节中,我们将更详细地探讨这些步骤,并且我们将编写一个基于微服务架构风格的简单应用。

了解应用的业务需求

这一部分是关于创造想象中的应用和应用背后的业务。让我们访问我们想象中的应用,我们将其命名为 FlixOne Store。

FlixOne Store 是一家基于定制软件的点播商店提供商。这款定制软件的目的是通过克服营销团队过去面临的技术挑战来拓展业务(更多信息,请参考第 1 章微服务简介)。最终用户登陆了该应用,他/她成功地满足了我们在商店市场中的一个需求:购买 FlixOne Store 中列出的各种商品。

FlixOne Store 是一个软件平台,不仅满足单个用户的需求和需求,也满足需要在线商店来销售产品/商品的零售商的需求和需求。FlixOne 也是一个支持在线销售和购买的平台。这样,零售商可以通过额外努力推广他们的产品/商品来赚钱。FlixOne 是一个迎合全球市场的解决方案,在这个市场中,行业的双方——零售商和制造商——建立了一个买方-卖方关系模型:

上图展示了整个 FlixOne 软件的全貌。在这里,我们很容易理解 FlixOne Store 提供的应用背后的商业理念:一个同时面向卖家买家的平台。厂商零售商都可以注册为卖家,而买家则是打算购买这些物品的最终用户。

Business model: FlixOne charges a commission off each transaction and provides a way for the sellers to manage products. FlixOne allows us to maintain the inventory that's uploaded by the sellers, as well as the ability to schedule deliveries, monitor product types and levels, and so on.

下图显示了 FlixOne 商店软件中卖家的流程:

上图定义了使用 FlixOne 商店的卖家的流程工作流程,如下所示:

  1. 卖家参观了 FlixOne 商店。

  2. 卖家在 FlixOne 商店注册。

  3. 然后卖家登记他们的产品类型。

  4. 卖方创建库存。

  5. 清单由 FlixOne 管理员验证/批准。

  6. FlixOne 要求卖方批准库存。

  7. 卖方支付注册费。

  8. FlixOne 与卖家达成了合作协议。

下图显示了购买者在 FlixOne 商店软件中的流程:

以下是使用 FlixOne 商店的买家的流程工作流程,如下所示:

  1. 买家参观了 FlixOne 商店。
  2. 买方在 FlixOne 商店注册。
  3. 提供可用的服务。
  4. 买方检查他们想要购买的产品的可用库存。
  5. 买方下订单。
  6. 买方支付他们的物品。
  7. 出现了智能合约流程。
  8. 向买方提供最近交易的确认。
  9. 物品被运送到所需的位置。

FlixOne 商店系统的完整图片包含以下内容:

  • 管理面板:这是由 FlixOne 团队管理的后端。
  • 卖家面板:这是所有卖家的界面,让他们可以管理自己的产品和库存,包括销售等等。
  • 用户面板:这个界面是准备购买物品的终端用户的主界面。

以下是我们想象中的用户面板:

前面的截图显示了想象中的用户界面,在那里可以购买各种书籍。这个快照是实际应用的可视化,并且会有不同的输出。

在下一节中,我们将讨论技术堆栈和应用的流程。

重新审视应用

本节将探讨我们将要开发的内容。在本书中,我们使用了各种 API 来展示其中的代码示例。在本节中,我们将概述我们的示例应用,它是各种流程的组合,最终用户进入应用并处理购书请求。

下图概述了我们的应用:

上图是我们的示例应用的功能概述的图示,它显示了包含以下项目的流程:

  • 客户端应用:移动和网络应用是终端用户将要使用的客户端应用。我们的代码存储库随 web 应用一起提供。
  • 授权服务器:验证用户并生成 JWT 令牌,以便进一步处理。
  • RESTful 服务:这些是将帮助我们的应用的各种服务。这些服务有自己的数据库。文件存储将是一个 CDN 或单独的服务器,用于存储各种内容,包括文档。
  • 通知服务:这些是外部服务,用于生成一次性密码 ( OTP )、认证用户并通知他们已经生成的订单、他们已经预订的项目等。

除此之外,我们的图表还包含广告服务器分析服务流媒体服务,如果我们需要播客我们的一些视频,这些都是必需的。为此,我们需要流媒体服务。到目前为止,我们的代码没有实现这些服务。

在下一节中,我们将讨论最小可行产品 ( MVP ,这样我们就可以创建我们的 FlixOne 应用。

构建应用

在前几节中,我们讨论了 FlixOne 软件的全貌。这个软件很大,我们不打算在这里全部开发;相反,我们将定义 MVP 来展示软件的实力。这个软件可以扩展到任何级别。

By taking an MVP approach, the scope of a piece of work is limited to the smallest set of requirements, in order to produce a functioning deliverable. MVP is often combined with Agile software development by limiting requirements to a manageable amount that can be designed, developed, tested, and delivered. This approach lends itself well to smaller websites or application development, where a feature set can be progressed all the way to production in a single development cycle.

我们将开发一个具有以下功能的 FlixOne Store 应用:

  • 登录功能
  • 注册功能
  • 项目搜索功能
  • 订单功能
  • 通知功能

让我们看一下下面的图表,它定义了我们搜索功能的流程:

在上图中,服务由六边形描述,而事件由矩形框表示。如上图所示,该流程描述了客户在搜索了他/她正在寻找的商品后下订单的场景。事情是这样的:

  1. 下订单事件升级为订单服务

  2. 针对此事件,我们的服务分析了订单项目和数量等参数,并将可用项目事件提升为产品服务

  3. 从这里开始,有两种可能的结果:要么请求的产品可用并具有所需数量,要么是不可用或没有所需数量。

  4. 如果物品可用,产品服务会向发票服务发出一个名为生成发票的事件。由于开具发票意味着确认订单,发票上的项目将不再有库存;我们需要注意这一点,并相应地更新库存。

  5. 为此,我们的发票服务提出了一个名为的事件,将产品数量更新为产品服务,并满足了这一要求。为简单起见,我们不再赘述由谁来处理邮寄发票事件。

  6. 可能会出现商店没有产品的情况。根据我们的业务需要,我们必须将产品标记为一有货就通知我。这完全取决于最终用户;也就是说,如果最终用户选择了该选项,那么他/她将在产品可用时收到通知。对于售前产品,也会触发相同的操作,正如 FlixOne Store 的售前产品所表示的那样——已经添加了一个产品,但还不能购买。

这就完成了我们的 MVP,并展示了如何创建 FlixOne Store 应用。我们没有添加任何代码,因为我们已经在本书中讨论了我们需要的代码(服务)。您可以从本书的代码库中获得完整的应用代码。

摘要

本章的目的是创建一个示例应用。通过这样做,我们已经讨论了应用的业务需求。然后,通过定义我们的 MVP,我们理解了应用的流程,并了解了应用的工作模型。

到目前为止,在我们编写应用的旅程中,我们已经经历了不同的阶段。我们探索了是什么导致了微服务的出现,并研究了利用它们的各种优势。我们还讨论了各种集成技术和测试策略。

在下一章中,我们将重新审视和讨论我们应用的每一个方面,从整体开始,然后我们将过渡到审查微服务应用。

十二、微服务架构总结

在理解微服务及其发展的道路上,我们经历了不同的阶段。我们探讨了是什么导致了微服务的出现以及利用它们的各种优势。我们还讨论了各种集成技术和测试策略。

在这一章中,我们将重新审视和讨论我们应用的每一个方面,从整体到过渡到微服务应用,然后讨论监控和策略。

让我们回顾一下到目前为止我们讨论的所有内容,包括本章中的以下主题列表:

  • 在微服务之前了解架构
  • 整体转变
  • 云原生微服务概述
  • 监视
  • 监测战略
  • 反应性微服务

技术要求

本章不包含代码示例,因此没有技术先决条件。

在微服务之前了解架构

微服务从来不是从一开始就设计成现在的形式。相反,已经从其他形式的流行架构风格逐渐过渡到微服务。在微服务出现之前,我们有整体架构和面向服务的架构,它们统治着企业开发的世界。

在快速回顾微服务体系结构及其各种属性和优势之前,让我们回顾一下这两种体系结构。

整体建筑

单片架构已经存在了相当长的一段时间,它产生了具有单个.NET 程序集。如果一个整体生产多个组件也没问题,只要它服务于一个孤立的任务。单片架构由以下组件组成:

  • 用户界面
  • 业务逻辑
  • 数据库访问

自给自足的代价是所有组件都是相互关联和相互依赖的。任何模块的微小变化都会影响整个软件。由于所有组件都以这种方式紧密耦合,因此有必要对整个应用进行测试。此外,如此紧密耦合的另一个后果是必须部署整个应用,而不是只部署其中的一部分。让我们总结一下由于采用这种架构风格而面临的所有挑战:

  • 大型相互依赖代码
  • 代码复杂性
  • 可量测性
  • 系统部署
  • 采用新技术

我们的整体应用是一个具有大量相互依赖代码的单一应用。用户界面、业务逻辑和数据库访问层紧密耦合。这个应用有前面讨论过的挑战。当我们想要按照目标框架/语言编写标准化代码时,这种应用也创造了一个复杂的条件。接下来,我们将讨论与相关的挑战.NET 堆栈。

标准化的挑战.NET 堆栈

当涉及到单片架构时,采用技术并不容易。这带来了一定的挑战。其中一些挑战包括安全性、响应时间、吞吐率和技术采用。这并不是说这种建筑风格不会用解决方案反击。挑战在于,在单片架构中,代码的可重用性非常低或根本不存在,这使得技术采用变得困难。

整体应用存在安全和技术采用方面的挑战,但它们也在扩展方面受到影响,我们将在下一期看到这一点。

缩放比例

我们还讨论了如何扩展是一个可行的选择,但随着回报的减少和费用的增加。垂直和水平缩放各有利弊。纵向扩展似乎更容易开始:投资于信息技术基础设施,如内存升级和磁盘驱动器。然而,回归很快就停滞了。垂直扩展所需的停机时间的缺点在水平扩展中不存在。然而,超过一个点,水平回报的成本就变得太高了。

让我们继续讨论行业中另一个广泛使用的架构,那就是面向服务的架构。

面向服务的架构

行业中另一个广泛使用的架构是面向服务架构 ( SOA )。这种架构摆脱了单一架构,并参与解决了前面部分提到的一些挑战。首先,它基于服务的集合。提供服务是 SOA 的核心概念。

服务是向其他系统组件提供某些功能的一段代码、程序或软件。这段代码能够直接与数据库交互,或者通过其他服务间接与数据库交互。它是独立的,可以让桌面和移动应用轻松使用服务。

SOA 相对于单一架构提供的一些明确优势如下:

  • 可重复使用的
  • 无国籍的
  • 可攀登的
  • 基于合同
  • 升级能力

SOA 是应用最广泛的架构之一,因为它有很多好处;本节讨论 SOA 及其相对于单一架构的优势。接下来,我们将讨论微服务风格的架构。

微服务式架构

除了 SOA 的一些明确优势之外,微服务还提供了一些额外的差异化因素,使该架构成为明显的赢家。微服务的核心是被设计成完全独立于系统中的其他服务,并在其流程中运行。独立的属性要求在应用设计中有一定的原则和策略。它们提供的一些好处如下:

  • 清晰的代码边界:这导致更容易的代码更改。它们独立的模块提供了独立的功能,这导致一个微服务的变化对其他微服务的影响很小。
  • 轻松部署:如果需要,可以一次部署一个微服务。
  • 技术适配:前面的属性带来了这个非常抢手的好处。这允许我们在不同的模块中采用不同的技术。
  • 可承受的可扩展性:这让我们可以只扩展选择的组件/模块,而不是整个应用。
  • 分布式系统:这是含蓄的,但这里需要提醒一句。确保您的异步调用被很好地使用,并确保同步调用不会阻塞整个信息流。很好地使用数据分区。我们稍后再谈这个,所以现在不要担心。
  • 快速的市场响应:在竞争激烈的世界中,这是一个确定的优势,因为如果你对新功能请求的响应速度较慢,或者在你的系统中采用新技术的速度较慢,用户往往会很快失去兴趣。

在讨论微服务时,消息传递是重要的方面之一。本节向我们介绍了微服务的优势。接下来,我们将讨论微服务中的消息传递。

微服务中的消息传递

这是另一个需要讨论的重要领域。微服务中主要使用两种主要的消息传递类型:

  • 同步的
  • 异步的

在微服务中,通过消息传递进行通信非常重要。服务可能相互依赖,也可能不相互依赖。

微服务风格的架构比整体应用有很多优势。这一节旨在总结所有的好处和优势。这些优势在我们将整体服务转变为微服务时非常有用,这将在下一节中介绍。

理解整体转换是如何工作的

作为练习的一部分,我们决定将现有的单片应用 FlixOne 转换为微服务风格的架构。我们看到了如何根据以下参数识别整料中的分解候选物:

  • 代码复杂性
  • 技术采用
  • 所需资源
  • 人类依赖性

除了技术独立性之外,它在成本、安全性和可扩展性方面也有明显的优势。这也使应用更加符合我们的业务目标。

转换的整个过程需要您识别类似于微服务边界的接缝,沿着这些接缝您可以开始分离。你必须小心选择合适的参数。我们已经讨论了模块的相互依赖性、团队结构、数据库和技术是几个可能的候选者。处理主数据需要特别小心。它更多的是一种选择,即您是否希望通过单独的服务或配置来处理主数据。你将是你的剧本的最佳评判者。微服务拥有自己的数据库的基本要求是删除许多现有的外键关系。这就需要智能地选择事务处理策略,以保持数据的完整性。

让我们继续讨论集成技术。

集成技术

我们已经探索了微服务之间的同步和异步通信方式,并讨论了服务的协作风格。这些风格是基于请求/响应和事件的。虽然请求/响应在本质上似乎是同步的,但事实是,正是实现决定了集成风格的结果。另一方面,基于事件的风格是纯异步的。

当处理大量微服务时,我们必须利用集成模式,以促进微服务之间的复杂交互。在第 3 章服务之间的有效通信中,我们探索了 API 网关,以及事件驱动模式。

API 网关为您提供了大量服务,其中一些如下:

  • 路由应用编程接口调用
  • 验证应用编程接口密钥、JWT 和证书
  • 强制实施使用配额和费率限制
  • 在不修改代码的情况下动态转换应用编程接口
  • 设置缓存后端响应
  • 出于分析目的记录调用元数据

事件驱动模式通过一些服务发布它们的事件,以及一些服务订阅那些可用的事件来工作。订阅服务只是根据事件及其元数据独立于事件发布服务进行反应。发布者不知道订阅者将要执行的业务逻辑。

集成确保我们应用的每个部分都是集成的;本节总结了我们在前几章中讨论过的各种观点。任何应用的一个重要方面是平滑部署,接下来我们将讨论这一点。

部署

对于企业应用而言,整体部署具有挑战性的原因不止一个。拥有一个难以分解的中央数据库,只会增加整体挑战,以及上市时间。

对于微服务,情况非常不同。好处不仅仅是因为架构是微服务。相反,这是从最初阶段开始的规划。没有持续交付 ( 光盘)和持续集成 ( CI )你不能指望一个企业级的微服务被管理。从早期阶段开始,对 CI 和 CD 的需求就非常强烈,如果没有它们,生产阶段可能永远看不到曙光。

CFEngine、Chef、Puppet、Ansible 和 PowerShell DSC 等工具可以帮助您用代码来表示基础架构,它们可以让您轻松地使不同的环境(例如,UAT、预生产、生产等)完全相同。Azure 在这方面可能是一个盟友:所需的快速和重复供应很容易得到满足。

与最接近的竞争对手虚拟机相比,容器可以更有效地满足隔离要求。我们已经探索了 Docker 作为容器化的热门候选之一,并向您展示了如何部署它。

完成代码后,应用必须在相关环境中进行测试,这在部署时是可能的;CI/CD 在这里工作。在本节中,我们总结了应用的部署,接下来,我们将总结测试微服务。

测试微服务

我们都知道单元测试的重要性,以及为什么每个开发人员都应该编写单元测试。单元测试是验证有助于构建更大系统的最小功能的好方法。

然而,测试微服务并不是一件常规的事情,就像测试一个整体一样,因为一个微服务可能会与其他几个微服务交互。在这种情况下,我们应该利用对实际微服务的调用来确保整个工作流正常工作吗?答案是否定的,因为这将使开发一个微服务依赖于另一个部分。如果我们这样做,那么拥有基于微服务的架构的整个目的就失去了。为了解决这个问题,我们将使用模拟和存根方法。这种方法不仅使测试独立于其他微服务,而且使数据库测试更加容易,因为我们也可以模拟数据库交互。

用单元测试来测试一个小的、孤立的功能,或者通过模仿外部微服务的响应来测试一个组件,都有它的范围,并且在这个范围内运行良好。然而,如果你已经在问自己关于测试大环境的问题,那么你并不孤单。集成测试和合同测试是测试微服务的下一步。

在集成测试中,我们关心外部微服务,并且作为过程的一部分,我们与它们进行通信。为此,我们模拟外部服务。我们在合同测试中更进一步,我们独立测试每个服务调用,然后验证响应。值得花时间研究的一个重要概念是消费者驱动的合同。详见第四章测试微服务、T4 对此进行详细研究。

本节总结了各种测试,我们讨论了消费者驱动的契约。安全性是任何应用中最重要的部分,在下一节中,我们将对其进行总结。

安全

具有单点身份验证和授权的传统方法在单体架构中运行良好。但是,在微服务的情况下,您需要为每个服务都这样做。这不仅在实现服务方面,而且在保持服务同步方面都会带来挑战。

OAuth 2.0 授权框架,结合 OpenID Connect 1.0 规范,可以为我们解决这个问题。OAuth 2.0 描述了授权过程中涉及的所有角色,这些角色很好地满足了我们的需求。我们只需要确保选择了正确的资助类型;否则,安全性会受到损害。OpenID Connect 身份验证建立在 OAuth 2.0 协议之上。

Azure 活动目录 ( Azure AD )是 OAuth 2.0 和 OpenID Connect 规范的提供者之一。这里的理解是,Azure AD 可以很好地与应用进行扩展,并且它可以与任何组织的 Windows Server Active Directory 很好地集成。

正如我们已经讨论过的容器,理解容器非常接近主机操作系统的内核是非常重要和有趣的。保护他们是另一个不能被高估的方面。Docker 是我们考虑的工具,它使用最小特权原则提供必要的安全性。

Azure 活动目录(AAD)非常适合验证用户和验证应用,这样我们就不用担心验证请求了。但是为了完成验证,我们需要在使用 AAD 的同时采取一些额外的步骤。除了安全性,监控和日志记录也是应用的重要方面。让我们继续并总结监控我们的应用。

监控应用

铁板一块的世界有它自己的一些优势。与微服务相比,监控和日志记录是更容易的领域。企业系统可能分布的微服务数量之多令人难以置信。

正如第 1 章微服务简介中所讨论的,在微服务架构的先决条件一节中,组织应该为深刻的变革做好准备。监测框架是这方面的关键要求之一。

与单一架构不同,在基于微服务的架构中,从一开始就非常需要监控。监控有多种分类方式:

  • 健康:我们需要先发制人地知道什么时候服务故障即将发生。关键参数,如 CPU 和内存利用率,以及其他元数据,可能是我们服务即将失败的前兆,或者只是服务中需要修复的缺陷。试想一下,当数百名现场高管试图与潜在客户分担成本时,保险公司的费率引擎会过载并停止服务,甚至表现缓慢。如今,没人没人能等得起数据。
  • 可用性:可能会有服务不执行大量计算的情况,但是服务本身的最低可用性可能对整个系统至关重要。在这种情况下,当系统停机(称为停机)时,我们必须使用一些工具来检查系统是否恢复(启动)。一个常用的命令是ping,我们必须依靠它。它可能会等待几分钟,然后向系统管理员发送电子邮件。这种解决方案适用于单片,需要监控一两个服务。然而,有了微服务,就有了更多的元数据。
  • 性能:对于获得高流量的平台,例如银行和电子商务,仅仅可用性并不能提供所需的服务。考虑到在很短的时间内,从几分钟到几十秒钟,有多少人在他们的平台上汇合,性能不再是奢侈品。您需要知道系统是如何响应的,通过使用它的数据,例如正在服务的并发用户的数量,并将其与后台的健康参数进行比较。这可能会为电子商务平台提供在即将到来的假期之前决定是否需要升级的能力。为了更多的销售,你需要服务更多的人。
  • 安全性:在任何系统中,你只能规划到特定级别的恢复能力。不管一个系统设计得多好,总会有一些阈值,超过这个阈值,系统就会摇摇欲坠,从而导致多米诺骨牌效应。然而,有一个精心设计的安全系统可以很容易地避免拒绝服务和 SQL 注入攻击。在处理微服务时,这在系统与系统之间非常重要。因此,在设置微服务之间的信任级别时,要三思而后行。我看到人们使用的默认策略是用微服务保护端点。覆盖这一方面可以提高系统的安全性,值得花些时间。
  • 审计:医疗保健、金融和银行是在相关服务方面具有最严格合规标准的几个领域,在全球范围内也是如此。根据您所处理的法规遵从性的类型,您可能需要将数据作为记录保留一段特定的时间,以特定的格式保存数据以便与管理机构共享,甚至与管理机构提供的系统同步。税收制度可能是另一个例子。对于分布式体系结构,您不想冒丢失与单个事务相关的数据记录集的风险,因为这将导致法规遵从性失败。
  • 排除系统故障:我敢打赌,在未来很长一段时间内,这将是任何开始使用微服务的人的最爱。我记得早期,我曾尝试对涉及两个 Windows 服务的场景进行故障排除。我再也没有想过推荐类似的设计。但是时代变了,科技也变了。

当向其他客户提供服务时,监控变得更加重要。在当今竞争激烈的世界中,服务级别协议(SLA)将是任何交易的一部分,并且在成功和失败的情况下都有与之相关的成本。你有没有想过,不管发生什么,我们多么容易就认为微软 Azure 服务级别协议会成立?我已经习惯了,担心云资源可用性的客户的查询得到了 99.9%正常运行时间的回复,甚至客户都没有眨眼。请记住,并非 Azure 中的所有组件都具有相同的 SLA。然而,通过对 Azure 服务的适当设计,它将击败大多数内部部署。

因此,除非您有信心与您的客户就服务级别协议达成一致,否则在提供服务时,他们不能指望它承诺相同的服务级别协议。事实上,没有 SLA 可能意味着您的服务可能不够稳定,无法提供服务。接下来,我们将总结监控的各种挑战。

监测挑战

在建立成功的监控系统之前,可能有多个关键点需要解决。这些需要被识别,并且需要被分配一个解决方案。接下来将讨论其中的一些要点。

规模

如果你有一个成功运行的系统,几十个微服务完美协调地组织成功的事务,那么你就赢得了第一场战斗。恭喜你!但是,您必须插入必要的监控部分,如果您还没有这样做。理想情况下,这应该是第一步本身的一部分。

组件寿命

随着虚拟机和容器的使用,我们需要弄清楚哪些部分值得监控。当您查看通过监视这些组件生成的数据时,其中一些组件可能已经不存在了。因此,明智地选择要监控的信息变得极其重要。

信息可视化

有一些可用的工具,比如 AppDynamics 和 New Relic,可以让你可视化多达 100 个微服务的数据。然而,在现实应用中,这只是这个数字的一小部分。必须清楚这些信息的目的,我们需要一个设计良好的可视化。这是我们可以选择反向设计的一个领域。首先,考虑您想要的报告/可视化,然后看看如何对其进行监控。

我们在本节中总结了监控和日志记录。我们还讨论了监控系统的各种挑战。现在,让我们继续并总结监控系统的各种策略。

了解监控策略

从监控开始,您可以考虑不同的常用策略,作为您的问题的解决方案。一些常见的实施策略如下:

  • 应用/系统监控
  • 实时用户监控
  • 语义监控和合成交易
  • 压型
  • 端点监控

请记住,这些策略中的每一个都专注于解决特定的目的。虽然一个可以帮助分析事务传播,但另一个可能适用于测试目的。因此,在设计整个系统时,您需要选择这些策略的组合,因为仅仅使用单一策略无法满足您的需求。

监控策略确保并专注于服务于特定目的,如本节所述。出于不同的目的,我们需要更多的策略。所以,让我们继续下一步,讨论可伸缩性。

理解可伸缩性

我们已经详细讨论了可伸缩性的 scale-cube 模型,并且我们已经发现了每个轴上的伸缩意味着什么。请注意, x- 轴缩放是通过在多个实例和微服务用户之间使用负载平衡器来实现的。我们还看到了基于交易发起的 z 轴缩放存在一些缺陷。

总的来说,微服务领域的扩展可以分为两个独立的领域:

  • 基础设施
  • 服务设计

让我们在以下几节中研究它们。

基础设施扩展

虚拟机是微服务世界不可或缺的组成部分。作为微软 Azure 平台的一部分,可用的功能使您能够毫不费力地执行这个看似复杂的任务。

通过与 Azure 自动缩放集成的缩放集功能,我们可以轻松管理一组相同的虚拟机。

自动缩放允许您为各种受支持的参数定义阈值,例如 CPU 使用率。一旦超过阈值,缩放设置就会生效,具体取决于参数是放大还是缩小。

这意味着,如果扩展集预测它需要添加更多虚拟机来应对增加的负载,它将继续这样做,直到阈值恢复正常。同样,如果对被管理资源的需求下降,它将决定从扩展集中删除虚拟机。对我来说,这听起来像是网络团队的和平。围绕自动扩展的选项可以进一步探索,因为它能够处理复杂的扩展需求,例如在扩展或向外扩展时运行数百台虚拟机。

服务设计

在我们的微服务中,我们已经实现了每个微服务的数据隔离。然而,读写数据库的模式仍然是一样的。由于底层关系数据库实施 ACID 模型,这可能是一件代价高昂的事情。或者我们可以说,这种方法可以稍加修改,以不同的方式实现数据库读/写操作。

我们可以使用公共查询责任分离,也称为 CQRS,在我们的微服务中进行有效的设计更改。一旦完成了模型级分离,我们就可以使用不同的策略来优化读写数据模型。

基础架构和服务设计是扩展的两个部分,在本节中,我们将两者都进行了总结。下一节将帮助我们了解和了解反应式微服务。

反应式微服务综述

我们进展顺利,同时将我们的整体应用转变为微服务风格的架构。我们还简要地讨论了在我们的服务中引入反应特性的可能性。我们现在知道了反应式微服务的关键属性,即:

  • 响应性
  • 弹性
  • 自治
  • 消息驱动

在管理微服务之间的通信时,我们也看到了反应式微服务的好处,也就是减少了我们的工作量。

这种优势不仅转化为工作量的减少,还转化为专注于执行业务逻辑的核心工作的能力,而不是试图应对服务间通信的复杂性。因此,下一节将重点介绍一个绿地应用。

构建绿地应用

前面的章节已经讨论了整体应用(棕色地带),然后我们将一个整体过渡到一个新的微服务应用(绿色地带)。在这一部分,我们将从头开始创建一个 FlixOne 书店。

A brownfield application is already developed (like a monolith application), whereas a greenfield application is one created from scratch.

首先,我们将确定我们的微服务及其功能,我们还将确定服务间的交互。

我们的 FlixOne 书店将提供以下功能:

  • 在现有书籍中搜索
  • 根据类别过滤书籍
  • 向购物车添加书籍
  • 对购物车进行更改
  • 从购物车下订单
  • 用户认证

为了满足应用的业务需求,创建一个绿地应用需要大量的工作。在下一节中,我们将讨论创建应用的各个步骤。

确定我们的服务范围

为了理解这些功能将如何映射为不同的微服务,我们需要首先理解支持它需要什么,以及什么可以作为微服务组合在一起。我们将从微服务本身的角度来看看数据存储会是什么样子。

定义应用的范围非常重要,所以接下来,当我们显示我们的 FlixOne 书籍列表时,我们将看到范围是什么。

图书列表微服务

让我们试着把搜索书籍的第一个功能分解开来。为了让我们的用户在商店中浏览图书,我们需要维护我们提供的图书列表。在这里,我们的第一个候选人被雕刻成一个微服务。图书目录服务不仅负责搜索可用的图书,还负责维护存放所有图书相关信息的数据存储。微服务应该能够处理系统中可用书籍所需的各种更新。我们称之为图书目录微服务,它将有自己的图书数据存储。

图书搜索微服务

检查过滤书籍的下一个功能似乎属于图书目录微服务本身的范围。然而,说到这里,让我们通过质疑我们自己对业务领域的理解来证实这一点。我想到的问题与我们的用户将执行的所有搜索的影响有关,这会使服务瘫痪。那么,图书搜索功能应该是一种不同的服务吗?这里,答案在于微服务应该有自己的数据存储。通过让图书目录和图书目录搜索功能作为不同的服务,这将需要我们在两个不同的位置维护一个图书列表,这带来了额外的挑战,例如必须同步它们。解决方案很简单:我们需要单个微服务,如果需要,我们应该扩展和负载平衡图书目录微服务。

购物车微服务

下一个候选人因在线购物革命而出名,这场革命是由亚马逊这样的公司带来的,并由智能手机进一步推动:购物车微服务。在我们最终决定结账并付款之前,它应该让我们在购物车中添加或删除书籍。毫无疑问,这是否应该是一个单独的微服务。然而,这带来了一个有趣的问题,即它是否处理产品的数据存储;它需要这样做来接收一些基本的细节,比如可用性(有什么存货)。跨服务访问数据存储是不可能的,因为这是微服务最基本的先决条件之一。我们问题的答案是服务间的通信。微服务使用另一个微服务提供的服务是可以的。我们称之为我们的购物车微服务。

订单微服务

接下来是下订单的业务功能。当用户决定他们的购物车里有合适的书时,他们决定下单。此时,与订单相关的一些信息必须被确认/传达给其他各种微服务。例如,在确认订单之前,我们需要从图书目录中确认有足够的库存数量来完成订单。确认后,应该从图书目录中扣除正确数量的项目。成功确认订单后,购物车也必须清空。

我们的订单微服务听起来更普遍,它似乎与不在微服务之间共享数据的规则相矛盾,但事实并非如此,我们很快就会看到这一点。所有操作都将完成,同时保持清晰的边界,每个微服务管理自己的数据存储。

用户认证

我们的最后一个候选是用户认证微服务,它将验证登录我们书店的客户的用户凭证。这个微服务的唯一目的是确认提供的凭证是否正确,以限制未经授权的访问。这对于微服务来说似乎很简单;但是,我们必须记住这样一个事实,如果您决定更改您的身份验证机制,通过使该功能成为任何其他微服务的一部分,它将影响不止一个业务功能。这种变化的形式可能是基于 OAuth 2.0 授权框架和 OpenID Connect 1.0 身份验证,使用正在生成和验证的 JWT。

以下是微服务的最终候选列表:

  • 图书目录微服务
  • 购物车微服务
  • 订单微服务
  • 用户身份验证微服务

在下图中,我们可视化了四种服务,它们是目录(书店)、购物车(购物车商店)、订单(订单商店)和身份验证(用户商店):

上图可视化了用户如何通过应用编程接口网关与四个服务交互:图书目录、用户授权、订单和购物车。

同步还是异步

在我们开始简单介绍微服务之前,这里有一个要点需要考虑。我们的微服务将相互通信,并且有可能它们将依靠响应来进一步发展。这给我们带来了一个困境,我们经历了抛弃心爱的整块石头的所有痛苦,然后陷入了同样的情况,一个故障点可能导致系统的级联崩溃。

服务之间的通信非常重要;让我们继续,并在下面几节中用几个示例服务来讨论它们。

图书目录微服务

目录服务将被设计为从目录中列出、添加和删除项目。这项服务还将告诉我们特定产品(书籍)的库存。

这个微服务有六个主要功能,通过一个 HTTP API 组件公开。这个 HTTP API 组件负责处理这些函数的所有 HTTP 请求。

这些功能如下:

| API 资源描述 | API 资源描述 |
| GET /api/book | 获取可用书籍的列表 |
| GET /api/book/{category} | 获取某个类别的图书列表 |
| GET /api/book/{name} | 按名称获取图书列表 |
| GET /api/book/{isbn} | 根据国际标准书号获得一本书 |
| GET /api/bookquantity/{id} | 获取预定图书的可用库存 |
| PUT /api/bookquantity/{id}/ {changecount} | 增加或减少一本书的可用库存数量 |

下图可视化了目录服务的表:

在上图中,我们已经可视化了作者股票图书T7】和类别的表格。它还描述了表之间的关系。简而言之,图表代表了以下内容:一本书属于一个特定的类别,显示了它的库存,并有一个作者。

购物车微服务

该微服务将具有以下作为 HTTP 端点公开供使用的功能:

| API 资源描述 | API 资源描述 |
| POST / api / book / {customerid } | 将特定的书籍添加到客户的购物车中 |
| DELETE / api / book / {customerid } | 从顾客的购物车中取出书 |
| GET / api / book / {customerid} | 获取客户购物车中的图书列表 |
| PUT / api / empty | 移除购物车中当前包含的所有书籍 |

下图可视化了购物车服务的两个支持表:

上图可视化了两个表之间的关系,即购物车和购物车。该关系表示 Cart 和 CartItems 之间的一对多关系,其中 CartId 是 Cart 表的主键,CartItems 表的外键。

订单微服务

该微服务将具有以下作为 HTTP 端点公开供使用的功能:

| API 资源描述 | API 资源描述 |
| POST / api / order / {customerid } | 获取客户购物车中的所有书籍,并为它们创建订单 |
| DELETE / api / order / {customerid } | 从顾客的购物车中取出书 |
| GET / api / order / {orderid} | 获取所有书籍作为特定订单的一部分 |

下图描述了订单服务的两个表:

上图描述了两个表之间的关系,即 Order 和 OrderItems。该关系表示 Order 和 OrderItems 之间的一对多关系,其中 OrderId 是 Order 表中的主键和 OrderItems 表中的外键。这意味着一个订单可能有多个项目。

用户身份验证微服务

该微服务将具有以下作为 HTTP 端点公开供使用的功能:

| API 资源描述 | API 资源描述 |
| GET / api / verifyuser / {customerid, password} | 验证用户 |

下面的屏幕截图显示了身份验证服务的用户表:

您可以根据需要查看应用源代码并进行分析。

这整个部分向我们介绍了创建绿地应用的各个步骤,我们在其中讨论了目录和购物车服务。

接下来,让我们进入一个不同的主题:云原生微服务,以及它们如何在我们的微服务容器上下文中使用。

云原生微服务概述

术语云优先,或者云原生,指的是容器的环境,或者我们可以说是基于容器的环境。在第 3 章服务之间的有效通信中,我们讨论了 Azure Kubernetes 服务,在第 5 章使用 Docker 部署微服务中,我们讨论了 Docker。每当我们讨论容器时,我们都会提到术语云原生

The term cloud-first or cloud-native initiated a new approach to achieving complex, scalable systems.

Kubernetes 作为云原生平台,在现有网络拓扑和云提供商原语的基础上提供网络。例如,在我们的案例中,我们有微软 Azure 作为我们的云提供商。这类似于我们处理存储,存储是一个逻辑抽象层,是开发人员和资源管理员最近正在使用的本地存储层。这是部署发生的地方。无论使用何种基础架构,这都可能基于物理服务器或虚拟机和/或私有/公共云。

此外,当我们开发微服务应用时,云原生技术是有帮助的。这些应用是用容器化的服务构建的(这意味着服务封装在容器中)。

从我们的系统架构来看,我们可以说云原生微服务基于以下原则:

  • 自动化:对于任何一个软件系统,最好是我们把它做成自动化系统。因为有了自动化系统,我们最初需要投资,然后,我们使用该系统几乎没有或很少投资。自动化系统将有助于建立基础设施;CI/CD 对于自动化系统来说也是非常流行的做法。一些示例包括缩放系统和监控系统。
  • 智能状态:这是任何系统最难的部分,而架构它。主要是,为此,我们应该注意我们的数据状态将如何存储用户数据。
  • 托管服务 : Azure 提供了很多托管服务,所以不仅仅是基础设施提供商。
  • 一个不断进化的系统:任何系统的改进和细化都有助于将其发展成为最好的系统。这些系统总是在不断发展,并且在这些系统的整个生命周期中都在进行改进。同样,我们的云原生系统也应该发展。

在本节中,我们看到了云优先或云原生系统是如何不断发展的,因此,这对于其整体发展非常重要。

摘要

在本章中,我们回顾了从单一应用到基于微服务的应用的整个过程。我们重新讨论了与遗留应用相关的问题。(我们的单片应用现在是一个遗留应用,因为在本章之前,我们已经将单片应用转换为基于微服务的应用。)然后我们讨论了业务案例、服务之间的通信、服务的测试、服务的监控等等。我们还讨论了反应式微服务。最后,我们讨论了Order微服务和Shopping-Cart微服务。

我们希望这本书能够向您介绍微服务风格架构的基本概念,我们希望它能帮助您深入了解微服务的优秀方面,并提供概念的清晰示例。最终的应用可供您仔细查看,以便按照您自己的速度分析到目前为止所学的内容。我们祝你在运用本书所学的技能并将其应用到现实挑战中时好运。

十三、附录

本节将指导您如何实现自己的应用编程接口网关。我们在本书中讨论的例子在 Azure 应用编程接口管理中使用。如果您想在 Azure 应用编程接口管理之外设置自己的应用编程接口网关,请参考本附录。

You can find all the working examples related to the API Gateway in the Chapter 10 folder of this book's GitHub repository, which is located here: https://github.com/PacktPublishing/Hands-On-Microservices-with-CSharp-8-and-.NET-Core-3-Third-Edition/tree/master/Appendix.

为了坚持实现应用编程接口网关,我们将专注于实现应用编程接口网关,并总结本书中提供的代码示例。

应用编程接口网关模式

API Gateway 是唯一一个位于 UI(客户端)和服务中心级别的网关,这意味着 UI 可以使用它来与微服务协作。它为我们管理和扩展这些服务提供了更简单的方法。它为我们提供了另一个关于所需的各种客户端(应用)的粒度(应用及其范围)。

Simply put, granularity describes a framework that is separated into little sections. Huge frameworks can also be separated or divided into better, more appropriate sections.

应用编程接口网关在微服务中的作用是通过特定的安排和解释,在客户端和托管服务之间提供一个中间的、直接的连接。这样,客户端只关注对它的要求,而不关注服务器端。

The API Gateway exemplifies the framework's design and gives us a service that is custom-fitted to every client. It may serve different operations – for instance, validating incoming requests.

根据其优缺点调整应用编程接口网关

应用编程接口网关模式的实现对我们开发人员有一些好处。例如,应用编程接口网关允许我们对客户端(用户界面/最终用户)隐藏服务的实际端点,还允许我们在一个地方为多个客户端(用户界面)处理业务逻辑。API 网关有它自己的优点和缺点。

下面是实现应用编程接口网关模式的优点:

  • 客户端(用户界面)可以通过向服务发出最少的请求来获得完整的数据。
  • 我们可以处理多种格式的响应,比如 JSON、XML 等等。身份验证和日志记录也可以通过应用编程接口网关模式的实现来管理。
  • 我们可以减少客户端和应用之间的往返行程。
  • 它帮助我们在一个地方访问不同的 API,就像网关展示的那样。

应用编程接口网关提供了许多其他优点,但仍有一些缺点,例如:

  • 性能下降是有可能的,因为大多数响应的操作和转换都发生在应用编程接口网关本身。
  • 有可能出现单点故障。如果在应用编程接口网关方面出现任何问题,那么客户端将会遇到故障。
  • 它需要额外的关注和编码来管理路线。
  • 它增加了系统的复杂性。

实现应用编程接口网关时的首选实践

如果我们想要实现这种模式,我们应该遵循这些最佳实践:

  • 我们应该在应用编程接口网关的中心位置实现日志记录和监控。通过这样做,我们节省了大量的精力,这些精力是我们在实现、记录和监控单个服务时所投入的。可能存在您想要记录独立服务的场景。在这种情况下,您的代码实现应该非常干净,这样就不会在多个地方记录相同的数据。
  • 对于聚合服务,在应用编程接口网关中有一个组合逻辑来返回客户端特定的响应是很好的。但是,当聚合服务具有复杂的业务逻辑或者您有特定的需求(例如不使用应用编程接口网关)时,您可以在没有应用编程接口网关的情况下继续。例如,在第 10 章设计模式和最佳实践中,在聚合器模式部分,由于我们的需求,并且因为我们的代码包含不太复杂的业务逻辑,我们在不使用 API Gateway 的情况下实现了聚合服务。
  • 在实现安全性时,请确保以正确的方式进行。这意味着评估您是需要单个服务还是一组服务的安全性。您还可以创建安全策略。例如,假设您有一个策略,要求来自所有传入请求的登录和密码详细信息。您可以将此策略应用于服务或一组服务,在这些服务中,您希望使用服务的登录名和密码对服务进行身份验证。

这是我们将在代码示例中遵循的最佳实践的基本列表,我们将在后面讨论。

实现应用编程接口网关

关于我们的 FlixOne 应用,我们有不同的服务,所有这些服务都是细粒度的,并使用它们自己的数据库。消费这些服务客户端(任何网络或移动设备)有点复杂,因为这些服务托管在不同的服务器上。这使得很难从所有不同的客户端管理这么多不同的端点。如果在服务器端进行了任何更改,例如服务器发生了更改,这也需要客户端做出努力来更改代码。此外,如果承载这些 API 的任何服务器停机/离线,我们的最终用户(客户端)将会受到影响,这可能会对客户端应用产生影响。

以下是我们想要实现自己的应用编程接口网关的主要原因:

  • 所有客户端都应该有一个端点。目前,我们没有这样的设施。
  • 没有办法得到ProductServiceVendorService的组合输出。目前,这是通过显式调用单个服务来实现的。
  • 应该在中央一级进行记录和监测。目前,我们正在记录各个服务级别。
  • 在请求到达实际服务之前,安全性应该在中央级别完成。目前,所有的服务都有自己的安全实现。

从头开始创建一个应用编程接口网关非常耗时,并且需要大量的工作。正因为如此,对于 FlixOne 的应用,我们将采用 Ocelot。有几个理由去使用豹猫应用编程接口网关。现在我们来谈谈这些。

更喜欢豹猫

我们选择 Ocelot 作为我们的 API Gateway 实现的主要原因是,Ocelot 是开源的.NET 框架和.NET Core。它还支持。. NET Core 3.1(的当前版本.NET Core)。作为一个开源项目,我们有自由定制代码,并按照我们的要求使用它。

The source code for Ocelot can be found at https://github.com/ThreeMammals/Ocelot.

更重要的是,为了满足我们在内部使用应用编程接口网关的要求,我们需要各种各样的功能。其中一些必需的特性如下:

  • 它允许我们定义自己的路线。例如,如果我们有api/product/listproduct,这是我们的product服务的路线,那么通过使用这个功能,我们可以使它类似于/product/list的东西。在这种情况下,客户/消费者需要使用/product/list来获取产品列表。
  • 它为我们提供了安全性,这样我们就可以应用身份验证和授权。
  • 通过缓存功能,它减少了后续调用,并且可以根据我们的要求从可用的缓存中管理数据。
  • 我们可以设置重试策略。如果任何请求的服务关闭或没有响应,这将有所帮助。
  • 通过日志记录和监控,我们可以解决记录请求和响应数据的关键问题之一。

初始步骤

为了开始使用豹猫应用编程接口网关,我们需要设置我们的项目。这需要遵循以下步骤:

  1. 打开 Visual Studio。
  2. 转到开始|创建新项目。您也可以单击“继续”来继续操作,而无需此屏幕中的代码链接。在这种情况下,您需要单击文件|新建项目来创建新项目。
  3. 选择 ASP.NET Core 网络应用,然后单击下一步。
  4. 输入项目名称,选择路径,然后单击“创建”。
  5. 在下一个屏幕上,选择一个空模板并单击创建;确保您已经选择.NET Core 和 ASP.NET Core 3.1。
  6. 右键单击添加|新项目。
  7. 在下一个对话框中,选择 JSON 文件,命名为apiroute.json,点击【添加】。
  8. 接下来,我们添加对豹猫的支持。为此,请转到工具|否获取包管理|包管理器控制台。在包管理器控制台中执行以下代码:
Install-Package Ocelot
  1. 现在,打开 apiroute.json 文件。这个文件是一个配置文件,是 Ocelot 所需要的。添加以下 JSON 数据:
{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/product/productlist",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 44338
        }
      ],
      "UpstreamPathTemplate": "/product/list",
      "UpstreamHttpMethod": [ "GET" ]
    },
    ...
   ],
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:44340"
  }
}

在前面的代码示例中,有两个部分:ReRoutes(一个数组)和GlobalConfigurationReRoutes的主要功能是接收所有传入的请求,然后将其转发给相关服务(也称为下游服务)。然后,路由识别相关的服务(也称为上游服务),以便它可以传递请求。GlobalConfiguration顾名思义,是适用于所有路线的配置。它还允许我们覆盖ReRoutes设置。我们的配置文件具有以下属性:

  1. 在我们开始通过应用编程接口网关调用这些应用编程接口之前,我们必须对我们的代码进行一些更多的更改,以便豹猫能够顺利工作。打开Program.cs文件,用以下代码替换CreateHostBuilder(string[] args)方法:
public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hc, config) =>
            {
                config.AddJsonFile("apiroute.json");
            })
            .ConfigureServices(oc => 
            {
                oc.AddOcelot();
            })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                    .UseStartup<Startup>()
                    .Configure(app =>
                    {
                        app.UseOcelot().Wait();
                    });
                });

在前面的代码中,我们通过添加AddOcelot()来启用豹猫的依赖性。这包括当我们使用UserOcelot().Wait()处理所有请求时的中间件。这有助于我们的应用作为应用编程接口网关工作。现在我们已经准备好测试我们的 API Gateway 的基本实现,请按 F5 (假设产品和供应商服务正在运行)。

有了这些,我们已经实现了基本支持的豹猫应用编程接口网关。在下一节中,我们将向我们的应用编程接口网关添加更多功能,如下所示:

  • 聚合
  • 记录
  • 费率限制
  • 安全

聚合

我们需要应用编程接口网关聚合,以便从两个不同的服务中获得组合结果。下图可视化了聚合的工作原理:

上图是我们解决方案的示意图。应用编程接口网关是一个单点联系人,它接收来自用户界面的请求,并将其转发给供应商服务产品服务。此外,应用编程接口网关提供带有服务端点用户界面聚合响应

打开apiroute.json文件,使用以下代码更新 JSON 文档:

{
  "ReRoutes": [
    //Product Service Routes
      ...
      "UpstreamPathTemplate": "/product/single/byvendorid/{id}",
      "UpstreamHttpMethod": [ "GET" ],
      "Key": "product"
    },
    ...
    //Vendor Service Route
    {
      ...
      "UpstreamPathTemplate": "/vendor/{id}",
      "UpstreamHttpMethod": [ "GET" ],
      "Key": "vendor"
    }
  ],
  "Aggregates": [
    {
      "ReRouteKeys": [
        "product",
        "vendor"
      ],
      "UpstreamPathTemplate": "/productlist/{id}"
    }
  ],
 ...

在前面的代码中,我们为ProductServiceVendorService路线添加了Key属性。在ReRouteKeys中,我们指示我们的网关聚合由productvendor键表示的集合。

You can also run all the service projects by executing the runprojects.bat file, which is available under the Api Gateway pattern| Aggregation folder of the Chapter 10 folder in this book's GitHub repository.

为了使代码实现简单并符合其描述服务的范围,我们将使用 Postman 验证响应。为此,启动邮递员应用,输入/productlist/{id}资源,然后从邮递员界面单击发送。

记录

就我们的FlixOne应用而言,日志记录是应用编程接口网关的必备功能之一。我们将在应用编程接口网关级别实现日志记录。下图将帮助我们可视化应用编程接口网关日志记录:

上图是我们在 FlixOne 应用中实现的图示,其中我们将日志记录放在应用编程接口网关上。这样,我们就避免了向每个服务添加日志代码。

Log4Net is a logging library that allows us to log the preferred operations of the application. The log data can be available in various sources, such as Console, File, DB, and so on. Moreover, Log4Net is open source, which means you can take advantage of it to customize the source code and use it as per your requirements.

Package Console执行以下命令,打开FlixOne.BookStore.ApiGateway项目,为Log4Net增加支持:

Install-Package Microsoft.Extensions.Logging.Log4Net.AspNetCore

现在,我们需要指示我们的应用编程接口网关将输出记录到文件中。为此,添加一个新文件,将其命名为Log4Net.config,并用以下代码更新该文件:

<?xml version="1.0" encoding="utf-8" ?>
<log4net>
...
  <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
    <file value="FlixOneLog.log" />
     <appendToFile value="true" />
    <maximumFileSize value="10MB" />
    <maxSizeRollBackups value="3" />
...
  </appender>
  <root>
    <level value="ALL"/>
    <appender-ref ref="DebugAppender" />
    <appender-ref ref="RollingFile" />
  </root>
</log4net>

在前面的代码中,我们配置了Log4Net,以便使用DebugAppenderRollingFile记录输出。这些附加器被定义为以特定的格式记录消息,这在ConversionPattern中提到。您会发现在前面的代码中,DebugAppenderRollingFile的输出格式是不同的。我们还配置了RollingFile追加器,这样日志文件的大小不会从 10 MB 增加,文件的最大数量是3。这些文件将以FlixOneLog.log的名称存储。我们配置了我们的记录系统,以便通过将级别值设置为ALL来记录所有内容。该值可以根据您的环境进行更改;例如,您不会对在生产环境中记录DEBUG感兴趣。

以下是Log4Net的各个级别:

  • All:记录一切。
  • Debug:只记录调试操作。例如,如果记录器是Log4Net的实例,那么Logger.Debug("This is a Debug log!");将被记录。
  • Info:只记录信息操作。例如,如果记录器是Log4Net的实例,那么Logger.Info("This is an Info log!");将被记录。
  • Warn:只记录警告信息。例如,如果记录器是Log4Net的实例,那么Logger.Warn("This is a Warn log!");将被记录。
  • Error:主要记录异常或自定义错误信息。例如,如果记录器是Log4Net的实例,那么Logger.Debug("This is an Error log!");将被记录。

接下来,打开Startup.cs文件,使用以下代码更新Configure方法:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    loggerFactory.AddLog4Net();
    app.UseOcelot().Wait();
}

在前面的代码中,我们在app.UseOcelot().Wait()之前添加了loggerFactory.AddLog4Net();。通过这样做,我们说我们的应用编程接口网关将根据我们的配置记录每个请求/响应的输出,这在Log4Net.config文件中提供。

You can also run all the projects by executing the runprojects.bat batch file, which is available under the Api Gateway pattern | Logging folder of the Chapter 10 folder,  in this book's GitHub repository.

通过定义速率限制保护服务免受攻击

保护应用免受外部攻击是我们需要执行的最重要的任务之一。我们可以在应用编程接口网关的帮助下做到这一点,它将控制到达我们的应用编程接口网关的应用编程接口请求。攻击者可以在几分之一秒内发送大量请求——有时是数千个。这就是所谓的分布式拒绝服务 ( 分布式拒绝服务)攻击。在这里,攻击者以特定的机器或应用为目标,发送多个请求。为了处理这个问题,我们需要为请求设置速率限制。我们将在我们的FlixOne.BookStore应用中实现同样的功能。

请看下图,了解我们试图实现什么来解决上述问题:

上图清楚地显示,每当在应用编程接口网关收到请求时,应用编程接口网关内的速率限制配置会为客户端分析请求的配额,请求要么被转发到服务器端的相关服务,要么被返回到客户端,并带有指定配额超出的消息。

要在应用编程接口网关中实现速率限制,请打开FlixOne.BookStore.ApiGateway项目并限制我们的一项服务,以便它在3秒内发出请求。为此,请使用以下代码更新产品服务:

{
      "DownstreamPathTemplate": "/api/product/productlist",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 44338
        }
      ],
      "UpstreamPathTemplate": "/product/list",
      "UpstreamHttpMethod": [ "GET" ],
      //Setting up a limit for 1-request within 3second, retry in 5seconds
      "RateLimitOptions": {
        "ClientWhitelist": [],
        "EnableRateLimiting": true,
        "Period": "3s",
        "PeriodTimespan": 5,
        "Limit": 1
      }
    }

在前面的代码中,我们正在配置我们的Upstream服务的/product/list,这是一个GET HttpMethod,通过提供以下RateLimitOptions来限制3s内的1请求:

  • ClientWhitelist:由一组客户端 IP 组成,考虑白名单客户端。这意味着来自这些白名单客户端的传入请求将不属于白名单 IPs 的规则。
  • EnableRateLimiting:由true / false值组成。这表明我们是否指示我们的应用编程接口网关启用/禁用端点速率限制。在我们的案例中,我们启用了/product/list端点的速率限制。
  • Period:由{number}{unit}格式的时间段组成,其中number根据单位为 1n ,单位为s秒、m分钟、h小时、d天。在我们的例子中,我们为Period的值提供了3s,这意味着 3 秒。
  • PeriodTimespan:如果在规定的Period内超过限制,这实际上是一个等待期(以秒为单位)。在我们的例子中,我们必须等待 5 秒钟才能发出第二个请求。
  • Limit:表示我们在规定的Period内可以提出的最大请求数。在我们的例子中,我们只能在 3 秒内提出一个请求,我们需要等待 5 秒钟才能提出第二个请求。

You can also run all the projects by executing the runprojects.bat batch file, which is available under the Api Gateway pattern | Rate-Limit folder of the Chapter 10 folder in this book's GitHub repository.

运行应用编程接口网关项目,并在首选浏览器中按下/product/list端点。你会得到预期的结果。现在,想打多少次 F5 就打多少次。您将看到您无法发出请求,但可以看到限速消息,如下图所示:

前面的截图显示,我们试图在 3 秒内发出多个请求,这是不可能的。

When we set EnableRateLimiting to true, we are actually adding the X-Rate-Limit and Retry-After headers with the specified requests.

实施安全服务

安全性是目前最大的担忧之一。为了解决这个问题,我们将在我们的应用编程接口网关中实现安全性。我们将把我们所有的服务都标记为安全的,这样如果客户试图直接请求后端服务,他们就会得到一个未经授权的错误消息。

考虑下图,了解我们试图实现什么来解决我们的问题:

在上图中,我们用新的身份验证服务器身份验证配置组件添加到我们的应用编程接口网关中。现在,每当应用编程接口网关收到一个请求时,身份验证配置组件将检查该请求是否满足指定的身份验证机制。应用编程接口网关将相应地拒绝或转发该请求到后端服务。我们的授权服务器生成一个 JWT 令牌。它还处理用户注册操作。

If you are writing a production-ready application, do not create and validate tokens in the same place; you can secure and validate it by adding another layer.

现在,我们已经向我们的 API Gateway 添加了所需的特性,我们还需要注意一点,即公开每个客户端的服务端点。这意味着移动设备的客户端将具有与 web 应用的客户端不同的服务端点。现在我们已经实现了应用编程接口网关模式,让我们考虑一个场景,在这个场景中,我们的客户需要根据他们的目标设备提供特定的内容。我们可以借助前端 ( BFF )模式的后端来做到这一点。这就像创建一个客户端特定的应用编程接口网关。在下一节中,我们将学习如何为前端实现后端。

前端图案的后端

前端后端模式帮助我们为所有或特定的前端应用(用户界面)创建单独的后端服务,用户界面向其特定的后端服务发送请求。如果您想避免为单个后端进行多界面定制,这种模式非常有用。

下图描述了我们正在努力实现的目标:

上图告诉我们,我们有两个应用编程接口网关块。一个是移动-产品,为移动设备的客户端提供Product Service的端点,另一个是Web-厂商,为所有 Web 客户端提供Vendor Service的端点。

We have created two new projects for this: FlixOne.BookStore.Web.ApiGateway and FlixOne.BookStore.Mobile.ApiGateway.

考虑来自FlixOne.BookStore.Web.ApiGateway项目的apiroute.json文件的以下代码:

{
  "ReRoutes": [
    //Vendor Service Route
    {
      "DownstreamPathTemplate": "/api/Vendor/{id}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 44339
        }
      ],
      "UpstreamPathTemplate": "/vendor/{id}",
      "UpstreamHttpMethod": [ "GET" ],
      "Key": "vendor"
    }    

  ],
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:44340"
  }
}

前面的代码是不言自明的。这里,我们正在为应用编程接口网关配置端点,以获得来自Vendor Service的供应商响应。

考虑来自FlixOne.BookStore.Mobile.ApiGateway项目的apiroute.json文件的以下代码:

{
  "ReRoutes": [
    //Product Service Route

    {
      "DownstreamPathTemplate": "/api/product/{productid}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 44338
        }
      ],
      "UpstreamPathTemplate": "/mobile/product/{productid}",
      "UpstreamHttpMethod": [ "GET" ],
      "Key": "product"
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:44341"
  }
}

在前面的代码中,我们配置了移动应用编程接口网关,以获得来自Product Service的响应。

通过实现 BFF,我们为相关客户提供了独立的 API。通过这种方式,我们实现了以下目标:

  • 所有端点的单一联系人。这是在我们实现应用编程接口网关时实现的。
  • 现在,移动设备的客户端和 web 界面的客户端都有自己特定的 API。这些 API 为他们提供相关的内容。

现在,我们通过应用编程接口网关为客户提供了所需的服务。

十四、答案

第一章

什么是微服务?

微服务架构是一堆服务,其中每个服务都是独立部署的,并且应该实现单个业务功能。

可以定义 Azure 服务架构吗?

Azure Service Fabric 是一个帮助我们轻松打包、部署和管理可扩展且可靠的微服务的平台(容器也类似于 Docker 等等)。有时,由于复杂的基础设施问题等,很难专注于您作为开发人员的主要职责。在 Azure 服务结构的帮助下,开发人员无需担心基础设施问题。它提供了各种技术,并作为一个捆绑包提供,具有 Azure SQL 数据库、Cosmos DB、微软 Power BI、Azure 事件中心、Azure 物联网中心和许多其他核心服务的功能。

什么是数据库分片?

通常,数据库分片被简单地定义为大型数据库的无共享分区方案。这样,我们可以实现更高水平的高性能和可扩展性。分片这个词来自术语分片和传播,这意味着将数据库划分成块(分片),并将其传播到不同的服务器。

什么是 TDD,开发者为什么要采用这个?

使用 TDD,开发人员在编码之前编写测试,以便他们可以测试自己的代码。测试是另一段代码,可以验证功能是否按预期工作。如果发现任何功能不满足测试代码,则相应的单元测试失败。这个功能很容易修复;正如你已经知道的,这就是问题所在。为了实现这一点,我们可以利用框架,如微软测试或单元测试。

能详细说说依赖注入(DI)吗?

依赖注入 ( DI )是一种设计模式,它提供了一种技术,让你可以让一个类独立于它的依赖。这可以通过将对象从其创建中分离出来来实现。

第二章

重构单块应用时,我们应该考虑哪些因素?

就微服务而言,重构单个应用起着重要的作用。这不仅仅是像重构代码这样的事情,而是把它看作一个整体系统。这种重构进一步帮助将单块应用转变为微服务。在为 microservces 架构重构或准备我们的整体应用时,可能需要考虑许多因素。以下是重构单块时我们应该牢记的重要因素(这些因素基于我们想象中的应用和本章中的讨论):

  • 技术考虑:应该考虑分离特性、技术堆栈、团队和团队技能等因素。这些帮助我们做出关于重构组件的决定。
  • 商业考虑:如果我们决定重构整体,上市计划的考虑是最重要的因素。

c# 8.0 的默认接口方法有哪些?

默认接口方法是实现接口成员的接口中的方法。为了更好地理解这个特性,请看下面的代码:

public interface IProduct
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
  public string ProductDesc() => $"Book:{Name} has Price:{Price}";
}

让我们将IProduct接口实现到我们的Product类。考虑以下代码:

public class Product : IProduct
{
  public Product(int id, string name, decimal price)
  {
    Id = id;
    Name = name;
    Price = price;
  }
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
}

ProductDesc是我们界面的默认方法,总是会产生一个结果。在我们的例子中,它将返回一个字符串作为这个方法的结果。此外,该方法可用于所有实现IProduct接口的类。以类似的方式,我们在我们的Product类中实现了IProduct接口。

我们为什么要用斯瓦格?

斯瓦格基于开放应用编程接口规范(以前的斯瓦格规范)提供关于应用编程接口的文档。swag 还提供了测试 API 的工具。我们使用斯瓦格,以便向最终用户提供适当的文档。

第三章

什么是同步和异步通信?

同步通信是指客户端向远程服务(称为服务)请求特定功能,并等待直到得到响应。异步通信是客户端向远程服务(称为服务)请求特定的功能,而不等待,尽管它确实关心响应。

什么是整合模式?

集成模式是两个或多个服务从一个数据存储中读写数据。

什么是事件驱动模式,为什么它对微服务如此重要?

在事件驱动的模式中,我们以这样一种方式实现服务:每当一个服务更新其数据并且另一个服务(依赖服务)订阅该事件时,它就发布一个事件。每当从属服务接收到事件时,它都会更新其数据。这样,如果需要,我们的依赖服务可以获取和更新它们的数据。上图显示了服务如何订阅和发布事件的概述。这里,事件管理器可以是运行在服务上的程序,也可以是帮助您管理订阅者和发布者的所有事件的中介。它注册发布服务器的事件,并在特定事件发生/触发时通知订阅服务器。它还可以帮助您形成队列并等待事件。

什么是 CAP 定理?

CAP 定理又称布鲁尔定理,代表一致性可用性、(网络 ) 分区容差。根据这个定理,在分布式系统中,我们只能从这三个中选择两个:

  • 一致性(丙)
  • 可用性(一)
  • 分区容差

考虑我们有一个假想的系统,它是高度可用的(A)和高度一致的(C),但没有分区(CA)。当我们需要并执行分区(P)时,我们将系统分区到 n 个分区,或者说我们正在持续地对系统进行分区。在这种情况下,这是一个非常复杂的场景,数据可能无法到达/覆盖所有分区。这就是为什么我们要么使系统高度可用(AP),要么使系统高度一致(CP)。

第四章

写一篇关于单元测试的短文。

单元测试通常测试单个函数调用,以确保测试程序的最小部分。这些测试旨在验证特定功能,而不考虑其他组件。单元测试可以是任何规模的;单元测试没有明确的规模。一般来说,这些测试是在类级别编写的。

开发人员为什么要坚持测试驱动开发?

使用 TDD,开发人员在实际代码之前编写测试,这样他们就可以测试自己的代码。这个测试是另一段代码,可以验证功能是否按预期工作。如果发现任何功能不满足测试代码,则相应的单元测试失败。这个功能很容易修复,因为你知道这就是问题所在。为了实现这一点,我们可以利用框架,如微软测试或单元测试。

什么是存根和模拟对象?

存根对象不依赖于输入,这意味着响应或结果不会由于正确或不正确的输入而受到妨碍。最初,我们创建存根作为对象。模拟对象不是真实的,可能永远是假的。通过使用模拟对象,您可以测试可以调用的方法,并告诉我们单元测试是失败了还是通过了。换句话说,我们可以说模拟对象只是我们实际对象的复制品。

什么是测试金字塔?

测试金字塔是一种策略或方法,用来定义应该在微服务中测试什么。换句话说,我们可以说它帮助我们定义了微服务的测试范围。测试金字塔的概念是由 Mike Cohn 在 2009 年创建的。测试金字塔有多种口味;不同的作者描述了这一点,指出了他们是如何放置测试范围或确定测试范围的优先级的。

什么是消费者测试?

合同测试是一种方法,其中每个服务调用都独立地验证响应。如果有任何服务是依赖的,那么依赖关系就会被存根化。这样,服务在不与任何其他服务交互的情况下运行。消费者驱动的契约指的是一种模式,它指定并验证客户端/消费者和 API 所有者(应用)之间的所有交互。这里,消费者驱动意味着客户端/消费者以定义的格式指定它所要求的交互类型。另一方面,应用接口所有者(应用服务)必须同意这些合同,并确保它们不会违反这些合同。

如何在基于微服务的应用中使用消费者测试?

在微服务的情况下,实现消费者驱动的测试比. NET 单片应用更具挑战性。这是因为,在单片应用中,我们可以直接使用任何单元测试框架,比如 MS tests 或者 NUnit,但是我们不能在微服务架构中直接这样做。在微服务中,我们不仅需要模拟方法调用,还需要模拟服务本身,它们通过 HTTP 或 HTTPs 被调用。为了实现消费者驱动的测试,我们需要使用各种工具。一个著名的开源工具.NET 框架是 PactNet 另一个。网芯被称为 Pact.Net 芯。这些都是基于 Pact(https://docs.pact.io/)标准。

第五章

什么是 Docker 形象,为什么这么重要?

Docker 映像是一种模板,包含创建 Docker 容器的指令。只能看说明书;您不能向该模板添加自己的说明,因为这是一个只读模板。它由一个单独的文件系统、相关的库等组成。在这里,图像始终是只读的,并且可以运行完全相同的抽象、底层、主机差异。Docker 图像可以由一层叠加在另一层上组成。Docker 图像的这种可组合性可以比作分层蛋糕的类比。跨不同容器使用的 Docker 映像可以重用。这也有助于减少使用相同基础映像的应用的部署占用空间。

什么是 Docker 存储库?

Windows 注册表是一个数据库,用于存储 Microsoft Windows 操作系统内部或低级设置的信息。

什么是 Docker 容器?

当前正在运行的 Docker 映像的实例称为 Docker 容器。

什么是坞站枢纽?

这是一个公共注册表,用于存储图像。位于http://hub.docker.com

在 Docker 文件中可以用 JSON 代替 YAML 吗?如果是,如何做到?

是的,我们可以使用 JSON 文件来代替 YAML 文件。请注意,YAML 是 JSON 的超集。这样,当我们使用一个 JSON 文件时,它隐式地是一个有效的 YAML 文件。我们需要指定文件名来使用 JSON 文件;例如docker-compose -f jsoncomposefile.json up
这里,我们使用up告诉系统,我们可以启动或重启jsoncomposefile.json文件中定义的所有服务。

就集装箱解释这些词:FROMCOPYWORKDIREXPOSEENTRYPOINT

为了理解这些术语,让我们考虑下面的代码片段:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-nanoserver-1903
 AS base WORKDIR /app
 EXPOSE 80 EXPOSE 443
 FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime
 WORKDIR /app
 COPY ${source:-obj/Docker/publish} .
 ENTRYPOINT ["dotnet", "FlixOne.BookStore.ProductService.dll"]

FROM是某种消息,告诉 Docker 在现有图像上拉基础图像并调用microsoft/aspnetcore:3.0COPYWORKDIR将内容复制到被调用/app 容器内的新目录中,并将其设置到工作目录中以获得后续说明。要在容器的端口 80 上公开我们的产品服务,我们可以使用EXPOSEENTRYPOINT,它们指定了容器启动时要执行的命令。举个例子,我们有ProductService,我们的切入点是["dotnet", "FlixOne.BookStore.ProductService.dll"]

借助我们的Product Services,编写一个简单的 ASP.NET Core 网络应用,以表格形式显示AddDeleteUpdate产品。

参考我们想象中的 FlixOne 书店应用的代码示例。

第六章

什么是软件安全?

安全性是 web 应用最重要的交叉问题之一。不幸的是,如今知名网站的数据泄露似乎司空见惯。考虑到这一点,信息和应用安全对于 web 应用来说变得至关重要。出于同样的原因,安全应用不应再是事后的想法。安全是组织中每个人的责任。如果我们要定义安全性,那么我们可以说这是一种实现代码的方法,这样我们就可以保护我们的软件免受恶意攻击和黑客风险,从而我们可以提供一个具有不间断功能的安全应用。

单片应用的安全挑战是什么?

有关更多信息,请参考单片应用中的安全性部分。

创建一个演示应用,并详细说明 OAuth。

请参考我们在本章的使用 OAuth 2.0 一节中讨论的示例应用。

什么是授权服务器,它是如何工作的?

授权服务器验证凭证,并使用授权代码将用户重定向回客户端。

什么是 Azure API 管理,为什么微服务需要 API 网关?

Azure API 管理(【APIM】)服务具有易于使用的用户界面和良好的文档。Azure 应用编程接口管理还附带了一个 REST 应用编程接口,因此 Azure APIM 门户的所有功能都可以使用 Azure APIM 可用的 Azure REST 应用编程接口端点以编程方式实现。

第七章

什么是监控?

监控提供有关整个系统或系统不同部分在其运行环境中的行为的信息。这些信息可用于诊断和深入了解系统的不同特征。

监测的必要性是什么?

微服务是复杂的分布式系统。微服务实施是任何现代信息技术业务的支柱。了解服务的内部及其交互和行为将有助于您使整体业务更加灵活和敏捷。微服务的性能、可用性、规模和安全性会直接影响企业及其收入。因此,监控微服务至关重要。它帮助我们观察和管理服务属性的质量。

什么是健康监测?

通过健康监测,我们以一定的频率(通常为几秒钟)监测系统及其各种组件的健康状况。这确保了系统及其组件按预期运行。借助详尽的运行状况监控系统,我们可以监视整个系统的运行状况,包括中央处理器、内存利用率等。它可能以 pings 或广泛的健康监控端点的形式出现,这些端点会发出服务的健康状态以及一些有用的元数据。

监控面临哪些挑战?

微服务监控提出了不同的挑战。会有这样的场景:一个服务可能依赖于另一个服务,或者客户端向一个服务发送请求,而响应来自另一个服务,这会使操作变得复杂;因此,扩展微服务在这里将是一项具有挑战性的任务。同样,流程实现——比如说 DevOps——在实现一个巨大的企业微服务应用时将是一项具有挑战性的工作。

微软 Azure 主要的日志和监控解决方案有哪些?

谈到微服务带来的监控挑战,Azure 或任何云提供商都没有现成的解决方案或产品。微软 Azure 诊断、应用洞察和日志分析是 Azure 提供的日志记录和监控解决方案。

第八章

什么是缓存,缓存在微服务应用中的重要性是什么?

缓存是增加应用吞吐量的最简单方法。原理很简单。一旦从数据存储器中读取了数据,它就尽可能地靠近处理服务器。在未来的请求中,数据直接从数据存储或缓存中提供。缓存的本质是最小化服务器必须做的工作量。HTTP 在协议本身中嵌入了内置的缓存机制。这就是它伸缩性如此之好的原因。

什么是服务发现,它如何在微服务应用中发挥重要作用?

更多信息请参考服务发现部分。

通过实现一个小程序来定义 Azure Redis 缓存。

Azure Redis 让您可以访问安全、专用的 Redis 缓存,该缓存由微软管理,可从 Azure 中的任何应用访问。所需的实现步骤请参考 Azure Redis Cache 部分。

什么是断路器?

断路器是电子设备中的一种安全功能,在发生短路时,它可以切断电流并保护设备,或者防止对周围环境造成任何进一步的损害。这个确切的想法可以应用于软件设计。当从属服务不可用或不处于健康状态时,断路器会阻止呼叫转到该从属服务,并在配置的时间段内将流量重定向到备用路径。

第九章

什么是反应式微服务?

一个软件必须具备某些基本属性才能被认为是反应性的。这些属性是响应性、弹性、自主性,最重要的是,是消息驱动的。

什么是消息级安全?

如果您希望保护单个请求消息的安全,消息级安全是最基本的方法。执行初始身份验证后,根据实现方式,请求消息本身可能包含 OAuth 承载令牌或 JWTs。这样,每一个请求都得到验证,并且与用户相关的信息可以嵌入到这些令牌中。该信息可以像用户名以及指示令牌有效性的到期时间戳一样简单。毕竟,我们不希望令牌的使用超过特定的时间范围。

什么是自动测试?

AutoRest 是一个帮助我们生成客户端库的工具,这样我们就可以访问 RESTful web 服务。自动测试从开放应用编程接口规范中获取应用编程接口定义。

posted @ 2025-10-22 10:25  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报