-NET-Core-2-0-微服务构建指南-全-

.NET Core 2.0 微服务构建指南(全)

原文:zh.annas-archive.org/md5/fcb09c483bdb21866eb6782158d1f8d5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

分布式系统总是难以取得完全的成功。最近,微服务受到了相当多的关注。Netflix 和 Spotify 的微服务实现已经成为行业中最成功的故事。另一方面,有些人认为微服务并不是什么新事物,或者它们只是 SOA 的重新品牌化。

无论如何,微服务架构确实具有关键优势——尤其是在推动复杂企业应用的敏捷改进和交付方面。

然而,关于如何在 Microsoft 生态系统中实现微服务,尤其是利用 Azure 和.NET Core 框架的优势,并没有明确的实际建议。本书试图填补这一空白。

它探讨了使用.NET Core 2.0 构建的微服务架构在规划、构建和运营方面的概念、挑战和优势。本书讨论了所有横切关注点以及微服务设计。它还通过实际“如何做”和最佳实践,突出了构建和运营微服务时需要考虑的一些重要方面,包括安全性、监控和可伸缩性。

本书涵盖的内容

第一章,微服务简介,使你熟悉微服务架构风格、历史以及它与前辈(单体架构和面向服务的架构 SOA)的不同之处。

第二章,实现微服务,讨论了可以用来在高级别识别和隔离微服务的不同因素,良好服务的特征以及如何实现微服务的垂直隔离。

第三章,集成技术和微服务,使你熟悉同步和异步通信、协作类型以及 API 网关。

第四章,微服务测试,解释了微服务测试与正常.NET 应用程序测试的不同之处。它使你熟悉测试金字塔。

第五章,部署微服务,涵盖了如何部署微服务和最佳实践。它还考虑了隔离因素,这是一个关键的成功因素,以及设置持续集成和持续交付以快速交付业务变更。

第六章,微服务安全,解释了如何使用 OAuth 使微服务安全,并带你了解容器安全和一般最佳实践。

第七章,监控微服务,涵盖了微服务的调试和监控,这不是一个简单的问题,而是一个相当具有挑战性的问题,因为在.NET 生态系统中没有设计用来专门针对微服务的单一工具。然而,Azure 监控和故障排除是最有希望的。

第八章,扩展微服务,探讨了可伸缩性,这是追求微服务架构风格的最关键优势之一。本章将根据微服务架构,通过设计和基础设施来解释可伸缩性。

第九章,反应式微服务简介,使您熟悉反应式微服务的概念。您将学习如何使用反应式扩展来构建反应式微服务。这将帮助您专注于主要任务,并让您摆脱跨服务通信的繁琐工作。

第十章,创建完整的微服务解决方案,将向您介绍您迄今为止所学的所有微服务概念。然后,我们将从头开始开发一个应用程序,同时将您所学的所有技能应用到实践中。

您需要这本书什么

本书中的所有支持代码示例都是在.NET Core 2.0 上使用 Visual Studio 2017 更新 3 和 SQL Server 2008R2 或更高版本在 Windows 平台上测试的。

这本书面向谁

这本书是为希望学习和理解微服务架构并将其应用于.NET Core 应用程序的.NET Core 开发者而编写的。它非常适合对微服务完全陌生或只是对这种架构方法有理论理解的开发者,他们希望通过获得实际视角来更好地管理应用程序复杂性。

术语

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称显示如下:“我们可以通过使用include指令来包含其他上下文。”

代码块设置如下:

 "ConnectionStrings": {
    "ProductConnection": "Data Source=.;Initial Catalog=ProductsDB;Integrated Security=True;MultipleActiveResultSets=True"
  }

任何命令行输入或输出都如下所示:

Install-Package System.IdentityModel.Tokens.Jwt

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“点击“下一步”按钮将您移动到下一屏幕。”

警告或重要提示会以这样的框中出现。

小贴士和技巧如下所示。

读者反馈

我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。要发送一般反馈,请简单地发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书的标题。如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

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

  1. 使用您的电子邮件地址和密码在我们的网站上登录或注册。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误”。

  4. 在“搜索”框中输入书的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击“代码下载”。

文件下载完成后,请确保您使用最新版本的软件解压缩或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Building-Microservices-with-.NET-Core-2.0-Second-Edition。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。去看看吧!

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/BuildingMicroserviceswith.NETCore2.0_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

侵权

在互联网上,版权材料的侵权问题是一个持续存在于所有媒体中的问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。请通过copyright@packtpub.com与我们联系,并提供疑似侵权材料的链接。我们感谢您在保护我们的作者和为您提供有价值内容的能力方面的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过questions@packtpub.com联系我们,我们将尽力解决问题。

第一章:微服务简介

本章的重点是让您熟悉微服务。我们将从简要介绍开始。然后,我们将定义它们的先辈:单体架构和面向服务的架构SOA)。在此之后,我们将看到微服务如何与 SOA 和单体架构相媲美。然后,我们将比较这些架构风格的优缺点。这将使我们能够确定这些风格的正确场景。我们将了解分层单体架构产生的问题。我们将讨论在单体世界中解决这些问题的方案。最后,我们将能够将单体应用程序分解为微服务架构。本章将涵盖以下主题:

  • 微服务的起源

  • 讨论微服务

  • 理解微服务架构

  • 微服务的优势

  • SOA 与微服务的比较

  • 理解单体架构风格的问题

  • 标准化.NET 堆栈的挑战

  • Azure 服务 Fabric 概述

微服务的起源

“微服务”这个术语首次在 2011 年中期的一个软件架构师研讨会上被使用。2012 年 3 月,詹姆斯·刘易斯(James Lewis)提出了一些关于微服务的想法。到 2013 年底,IT 行业的各个群体开始就微服务进行讨论,到 2014 年,它们已经足够流行,被视为大型企业的一个严肃的竞争者。

对于微服务,没有官方的介绍。对这个术语的理解纯粹基于过去的使用案例和讨论。我们将详细讨论这个问题,但在那之前,让我们看看维基百科(en.wikipedia.org/wiki/Microservices)对微服务的定义,它将其总结如下:

“微服务是 SOA 的一个专业化和实现方法,用于构建灵活的、可独立部署的软件系统。”

2014 年,詹姆斯·刘易斯(James Lewis)和马丁·福勒(Martin Fowler)共同提供了一些实际案例,用自己的话介绍了微服务(参考martinfowler.com/microservices/),并进一步详细说明如下:

“微服务架构风格是一种将单个应用程序作为一系列小型服务开发的方法,每个服务都在自己的进程中运行,并通过轻量级机制(通常是 HTTP 资源 API)进行通信。这些服务围绕业务能力构建,并且可以由完全自动化的部署机器独立部署。对这些服务的集中式管理最少,这些服务可能使用不同的编程语言,并使用不同的数据存储技术。”

你看到 Lewis 和 Fowler 在这里定义的所有属性非常重要。他们将单体架构定义为一种开发者可以利用的架构风格,以开发一个业务逻辑分布在多个小型服务中的应用程序,每个服务都有自己的持久化存储功能。同时,注意其属性——它可以独立部署,可以在自己的进程中运行,是一种轻量级通信机制,并且可以用不同的编程语言编写。

我们想要强调这个特定的定义,因为它整个概念的精髓。随着我们继续前进,当我们完成这本书的时候,它将会逐渐显现。

讨论微服务

我们已经讨论过几个关于微服务的定义;现在,让我们详细讨论微服务

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

将应用程序切割成多个服务既不是 SOA 也不是微服务。然而,结合 SOA 世界的服务设计和最佳实践,以及一些新兴实践,如隔离部署、语义版本化、提供轻量级服务和多语言编程中的服务发现,就是微服务。我们实现微服务以满足业务功能,并以更短的时间上市和更大的灵活性来实现它们。

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

  • 单体架构风格

  • SOA

我们大多数人都会意识到,在企业应用程序开发的生命周期中,在各个阶段,都会决定一个合适的架构风格。然后,随着各种挑战的出现,如部署复杂性、大型代码库和可伸缩性问题,初始模式会进一步改进和适应。这正是单体架构风格演变成 SOA,进而发展到微服务的过程。

单体架构

单体架构风格是一种传统的架构类型,在业界得到了广泛的应用。术语单体并不新鲜,它借用了 UNIX 世界的概念。在 UNIX 中,大多数命令都存在作为独立程序,其功能不依赖于任何其他程序。如图所示,我们可以有应用程序中的不同组件,例如:

  • 用户界面:它处理所有用户交互,同时以 HTML 或 JSON 或其他任何首选的数据交换格式(在 Web 服务的情况下)进行响应。

  • 业务逻辑:所有应用于以用户输入、事件和数据库形式接收的输入的业务规则都存在于这里。

  • 数据库访问:这包含了用于查询和持久化对象的数据库访问的完整功能。一个普遍接受的规则是,它通过业务模块使用,而不是直接通过面向用户的组件使用。

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

图片

由于软件是自包含的,其组件是相互关联和相互依赖的。即使在某个模块中进行的简单代码更改也可能破坏其他模块中的主要功能。这会导致我们需要测试整个应用程序的情况。由于业务严重依赖其企业应用程序框架,这段时间可能会非常关键。

所有组件紧密耦合提出了另一个挑战——无论何时执行或编译此类软件,所有组件都应可用,否则构建将失败;参考前面的图示,它代表了一个单体架构,是一个自包含的或单个 .NET 程序集项目。然而,单体架构可能也有多个程序集。这意味着尽管业务层(程序集、数据访问层程序集等)是分离的,但在运行时,它们将一起运行作为一个进程。

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

部署此类应用程序将变成另一个挑战。在部署过程中,我们必须确保每个组件都得到正确部署;否则,我们可能会在我们的生产环境中遇到许多问题。

如果我们使用之前讨论的单体架构风格开发应用程序,我们可能会面临以下挑战:

  • 大型代码库:这是一个代码行数远多于注释的情况。由于组件是相互关联的,我们将不得不忍受重复的代码库。

  • 业务模块太多:这是关于同一系统内的模块。

  • 代码库复杂性:这导致由于在其他模块或服务中进行的修复而出现代码破坏的可能性更高。

  • 复杂代码部署:你可能会遇到需要整个系统部署的微小更改。

  • 一个模块的故障影响整个系统:这是关于相互依赖的模块。

  • 可扩展性:这是整个系统所需,而不仅仅是其中的模块。

  • 模块间依赖:这是由于紧密耦合造成的。

  • 开发时间螺旋上升:这是由于代码复杂性和相互依赖性造成的。

  • 难以轻松适应新技术:在这种情况下,整个系统都需要升级。

如前所述,如果我们想减少企业应用程序的开发时间、部署的便捷性和可维护性,我们应该避免传统的或单体架构。

面向服务的架构

在上一节中,我们讨论了单体架构及其局限性。我们还讨论了为什么它不符合我们的企业应用程序需求。为了克服这些问题,我们应该采取模块化方法,这样我们可以将组件分开,使它们从自包含或单个.NET 组件中分离出来。

SOA 与单体架构的主要区别不在于一个或多个组件。由于 SOA 中的服务作为一个独立的过程运行,因此与单体架构相比,SOA 具有更好的可扩展性。

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

什么是服务?

在这个例子中,服务是 SOA 的一个基本概念。它可能是一段代码、程序或软件,为其他系统组件提供功能。这段代码可以直接与数据库交互,或者通过另一个服务间接交互。此外,它可以直接被客户端消费,客户端可能是一个网站、桌面应用程序、移动应用程序或任何其他设备应用程序。请参考以下图表:

图片

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

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

产品销售服务是一组与客户端应用程序交互并提供数据库直接访问或通过另一个服务(在这种情况下,服务-渠道合作伙伴)的服务。在服务-直销的情况下,如前图所示,它为网店、桌面应用程序和移动应用程序提供功能。此服务还与数据库进行交互,执行各种任务,即获取和持久化数据。

通常,服务通过某种通信渠道与其他系统交互,通常是 HTTP 协议。这些服务可能部署在相同的服务器上,也可能不在同一服务器上:

图片

在前述图像中,我们展示了一个 SOA 示例场景。这里有许多需要注意的细微之处,所以让我们开始吧。首先,我们的服务可以分布在不同的物理机器上。在这里,服务-直销托管在两个独立的机器上。可能的情况是,整个业务功能不是全部都驻留在服务器 1上,而是部分驻留在服务器 2上。同样,服务-合作伙伴销售似乎在服务器 3服务器 4上有相同的安排。然而,这并不妨碍服务-渠道合作伙伴作为一个完整的集合托管在两个服务器上:服务器 5服务器 6

使用前述图中提到的方式使用服务或多个服务的系统称为SOA。我们将在以下部分详细讨论 SOA。

让我们回顾一下单体架构。在这种情况下,我们没有使用它,因为它限制了代码的可重用性;它是一个自包含的组件,所有组件都是相互连接和相互依赖的。在部署方面,在这种情况下,在选择了 SOA(参见图表和后续讨论)之后,我们必须部署我们的完整项目。现在,由于使用了这种架构风格,我们有了代码可重用性和易于部署的好处。让我们在前述图表的基础上进行考察:

  • 可重用性:多个客户端可以消费服务。服务也可以同时被其他服务消费。例如,OrderService被 Web 和移动客户端消费。现在,OrderService也可以被报告仪表板 UI 使用。

  • 无状态:服务不会在客户端请求之间持久化任何状态,也就是说,服务不知道,也不关心后续请求是否来自已经/没有发出先前请求的客户端。

  • 基于契约:接口在实现和消费的两端都使技术无关。它还用于使底层功能代码更新免疫。

  • 可伸缩性:系统可以扩展;SOA 可以单独进行集群,并使用适当的负载均衡。

  • 升级:推出新功能或引入现有功能的新的版本非常容易。系统不会阻止你保留同一业务功能的多个版本。

理解微服务架构

微服务架构是一种开发包含一组较小服务单一应用程序的方法。这些服务相互独立,并在它们自己的进程中运行。这些服务的一个重要优势是它们可以独立开发和部署。换句话说,我们可以认为微服务是一种将我们的服务分割开来的方法,以便在设计、开发、部署和升级的背景下完全独立地处理。

在单体应用程序中,我们有一个包含用户界面、直接销售和库存的自包含组件。在微服务架构中,应用程序的服务部分变为以下描述:

图片

在这里,业务组件已经被分割成独立的服务。这些独立的服务现在是之前在自包含组件中存在的较小单元,在单体架构中。直接销售服务和库存服务相互独立,虚线表示它们存在于同一生态系统中,但并未被限制在单一范围内。请参考以下图表:

图片

从前面的图表中可以看出,我们的用户界面可以与任何服务进行交互。当 UI 调用服务时,无需干预任何服务。这两个服务相互独立,且不知道用户界面何时会调用另一个服务。两个服务都对其自身的操作负责,而不是对整个系统中的任何其他部分负责。尽管与微服务架构非常接近,但前面的可视化并不是完全完整的微服务架构可视化。

在微服务架构中,服务是小型、独立的单元,拥有自己的持久存储。

现在让我们引入这一最终变化,以便每个服务都将拥有自己的数据库,用于持久化必要的数据。请参考以下图表:

图片

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

微服务提供的解决方案具有许多好处,以下将进行讨论:

  • 更小的代码库:每个服务都很小,因此作为单元开发和应用部署更为容易

  • 独立环境的便利性:随着服务的分离,所有开发者都可以独立工作,独立部署,没有人会因任何模块依赖而感到困扰

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

微服务中的消息传递

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

对于微服务架构,没有固定的规则来选择各种框架或协议。然而,这里有一些值得考虑的点。首先,它应该足够简单,以便实现,而不会给您的系统增加任何复杂性。其次,它应该足够轻量级,考虑到微服务架构可能会大量依赖服务间消息。让我们继续前进,考虑我们同步和异步消息以及不同消息格式的选择。

同步消息

当系统期望从服务中获得及时响应,并且系统会等待直到收到服务的响应,这就是同步消息。剩下的就是微服务中最受欢迎的选择。它是简单且支持 HTTP 请求-响应,因此几乎没有寻找替代方案的空间。这也是为什么大多数微服务的实现都使用 HTTP(基于 API 的风格)的原因之一。

异步消息

当系统不立即期望从服务中获得及时响应,并且系统可以在不阻塞该调用的情况下继续处理,这就是异步消息。

让我们将这个消息概念融入到我们的应用程序中,看看它会如何改变和看起来:

图片

消息格式

在过去的几年里,使用 MVC 等工具让我对 JSON 格式产生了依赖。您也可以考虑 XML。这两种格式在 API 风格资源上都是可以接受的。如果您需要使用二进制消息格式,它们也容易获得。我们不推荐任何特定的格式;您可以选择任何选定的消息格式。

我们为什么应该使用微服务?

已经探索了大量的模式和架构,其中一些获得了流行;然而,还有一些正在失去互联网流量的竞争。由于每个解决方案都有其自身的优缺点,因此公司迅速响应基本需求(如可扩展性、高性能和易于部署)变得越来越重要。任何单一方面未能以成本效益的方式得到满足,都可能对大型企业产生负面影响,从而在盈利和非盈利企业之间造成巨大差异。

这就是我们在哪里看到微服务帮助企业系统架构师摆脱困境的地方。他们可以利用这种架构风格确保其设计不受先前提到的问题的影响。同时,考虑到所涉及的时间,以成本效益的方式实现这一目标也很重要。

微服务架构是如何工作的?

到目前为止,我们已经讨论了关于微服务架构的各个方面,现在我们可以描绘微服务架构是如何工作的;我们可以根据我们的设计方法使用任何组合,或者押注一个适合它的模式。以下是一些有利于微服务架构工作的要点:

  • 这是现代编程,我们期望遵循所有 SOLID 原则。它是面向对象编程OOP)。

  • 这是将功能暴露给其他或外部组件的最佳方式,以便任何其他编程语言都能使用该功能,而无需遵循任何特定的用户界面,即服务(Web 服务、API、REST 服务等)。

  • 整个系统按照一种非互联或相互依赖的协作方式工作。

  • 每个组件都对其自身的责任负责。换句话说,组件只负责一个功能。

  • 它通过分离概念来隔离代码,并且隔离的代码是可重用的。

微服务的优势

现在让我们快速了解微服务是如何在 SOA 和单体架构中取得飞跃的:

  • 成本效益高的扩展性:您不需要投入大量资金来使整个应用具有可扩展性。就购物车而言,我们可以简单地负载均衡产品搜索模块和订单处理模块,同时排除使用频率较低的运营服务,例如库存管理、订单取消和交货确认。

  • 清晰的代码边界:这一行动应与组织的部门层级相匹配。在大型企业中,不同的部门资助产品开发,这可以是一个巨大的优势。

  • 更容易的代码更改:代码是以一种方式编写的,它不依赖于其他模块的代码,并且只实现隔离的功能。如果做得正确,那么微服务中的更改影响另一个微服务的可能性非常小。

  • 简单部署:由于整个应用程序更像是一组相互隔离的生态系统,如果需要,可以一次部署一个微服务。任何一个微服务的失败都不会导致整个系统崩溃。

  • 技术适应性:你可以在一夜之间将单个微服务或一大堆微服务迁移到不同的技术,而用户甚至可能都不知道。是的,我们希望你不要期望我们告诉你你需要维护那些服务合同。

  • 分布式系统:这一点是隐含的,但在这里需要提醒一下。确保你的异步调用被正确使用,而同步调用不会真正阻塞整个信息流。合理使用数据分区。我们稍后会提到这一点,所以现在不用担心。

  • 快速市场响应:世界竞争激烈是一个明显的优势;否则,如果你对新的功能请求或系统内新技术的采用反应迟缓,用户可能会迅速失去兴趣。

SOA 与微服务

如果你没有完全理解微服务和 SOA,你会在两者之间感到困惑。从表面上看,微服务的特性和优势听起来几乎像是 SOA 的一个精简版本,许多专家建议实际上没有必要使用额外的术语,如微服务,SOA 可以满足微服务所列出的所有属性。然而,事实并非如此。它们之间有足够的差异,可以在技术上区分开来。

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

  • 事实是,在 SOA 中开发出的系统依赖于其相互交互的组件。所以无论你多么努力,它最终都会在消息队列中遇到瓶颈。

  • SOA 的另一个焦点是命令式单例编程。因此,我们失去了使用面向对象编程(OOP)使代码单元可重用的途径。

我们都知道,组织在基础设施上的投入越来越多。企业越大,正在开发的应用程序的所有权问题就越复杂。随着利益相关者的数量不断增加,满足他们不断变化的企业需求变得不可能。这正是微服务明显区别于其他地方的地方。尽管云开发不在我们讨论的当前范围内,但说云平台可以轻松扩展微服务架构的可扩展性、模块化和适应性,并不会对我们造成伤害。是时候改变一下了。

微服务架构的先决条件

理解微服务架构实施后的结果生态系统非常重要。微服务的影响不仅仅是预操作性的。对于任何选择微服务架构的组织来说,变化将是如此深刻,以至于如果他们没有做好充分准备来应对,优势很快就会变成劣势。

在同意采用微服务架构之后,明智的做法是确保以下先决条件已经到位:

  • 部署和 QA:需求将变得更加严格,开发需求周转速度更快。这要求你尽可能快地进行部署和测试。如果只是少数服务,那么这不会成为问题。然而,如果服务的数量在增加,这可能会很快对现有的基础设施和实践构成挑战。例如,你的 QA 和预发布环境可能不再足以测试开发团队返回的构建数量。

  • 开发和运维团队的协作平台:随着应用程序进入公共领域,很快就会再次上演古老的开发与 QA 之间的对决。这次的不同之处在于,业务将受到威胁。因此,你需要准备好在需要时以自动化的方式快速响应,以确定根本原因。

  • 监控框架:随着微服务数量的增加,你很快就需要一种方法来监控整个系统的运行状况和健康,以发现任何可能的瓶颈或问题。如果没有任何手段来监控已部署微服务的状态和结果业务功能,任何团队都无法采取主动部署方法。

理解单体架构风格的问题

在本节中,我们将讨论基于单体.NET 堆栈的应用程序所遇到的所有问题。在单体应用程序中,核心问题是这样的:单体扩展很困难。结果应用程序最终拥有一个非常大的代码库,并在可维护性、部署和修改方面提出了挑战。

标准化.NET 堆栈的挑战

在单体应用程序技术中,堆栈依赖阻止了从外部世界引入最新技术。当前的堆栈本身作为一个网络服务也面临一些挑战:

  • 安全性:无法通过网络服务识别用户(没有关于强认证方案的明确共识)。想象一下,一个银行应用程序发送未加密的数据,其中包含用户凭据,而没有加密。所有提供免费 Wi-Fi 的机场、咖啡馆和公共场所都很容易成为身份盗窃和其他网络犯罪的受害者。

  • 响应时间:尽管网络服务本身在整体架构中提供了一些灵活性,但由于服务本身的高处理时间,这种灵活性很快就会减少。因此,在这种场景下,网络服务本身并没有错。事实上,单体应用程序涉及大量的代码;复杂的逻辑使得网络服务的响应时间很高,因此,这是不可接受的。

  • 吞吐量:这处于较高水平,因此会阻碍后续操作。依赖于调用库存网络服务并需要搜索数百万条记录的结账操作并非坏事。然而,当相同的库存服务为整个门户的产品搜索/浏览提供数据时,可能会导致业务损失。在十次调用中,如果有一次服务调用失败,则意味着业务转化率降低 10%。

  • 经常性停机:由于网络服务是整个单体生态系统的一部分,它们在每次升级或应用程序故障时必然会出现停机且不可用。这意味着外部世界对应用程序网络服务的任何 B2B 依赖都会进一步复杂化决策过程,从而寻求停机时间。这绝对使得系统的较小升级看起来成本高昂;因此,它进一步增加了待处理的系统升级积压。

  • 技术采用:为了采用或升级技术堆栈,需要整个应用程序进行升级、测试和部署,因为模块是相互依赖的,整个项目的代码库都会受到影响。考虑使用需要合规性相关框架升级的组件的支付网关模块。开发团队别无选择,只能升级框架本身,并仔细检查整个代码库以预防性地识别任何代码冲突。当然,这仍然不能排除生产崩溃的可能性,但这很容易让即使是最好的架构师和管理者都感到焦虑,甚至失眠。

可用性:服务运行的时间百分比。

响应时间:服务响应所需的时间。

吞吐量:处理请求的速率。

容错性

单体应用程序具有高度模块间依赖性,因为它们紧密耦合。不同的模块以这种方式利用模块内的功能,以至于即使是单个模块的故障也会由于级联效应而使整个系统崩溃,这与多米诺骨牌倒下的情况非常相似。我们都知道,用户在产品搜索中得不到结果会比整个系统崩溃要轻微得多。

使用网络服务进行解耦在传统上是在架构层面尝试的。对于数据库级别的策略,长期以来一直依赖于 ACID。让我们进一步探讨这两个点:

  • 互联网服务:在当前的单体应用中,由于这个原因,客户体验受到了影响。即使客户试图下订单,像互联网服务的高响应时间或服务本身的完全失败这样的原因,都会导致无法成功下单。任何一次失败都是不可接受的,因为用户往往会记住他们的最后一次体验并假设可能的重现。这不仅会导致潜在销售的损失,还会导致未来商业前景的损失。互联网服务的故障可能导致依赖它们的系统出现级联故障。

  • ACID:ACID 是原子性、一致性、隔离性和持久性的缩写;它是数据库中的一个重要概念。它已经到位,但它是福是祸需要根据综合性能来判断。它负责处理数据库级别的故障,并且毫无疑问,它确实提供了一些防范数据库错误的能力。同时,每个 ACID 操作都会阻碍/延迟其他组件/模块的操作。它造成更多伤害而不是益处的点需要非常仔细地判断。

扩展

诸如不同通信手段的可用性、信息的便捷获取和开放的世界市场等因素导致企业迅速增长并多样化。随着业务的快速增长,满足不断增长的客户基础的需求也在不断增加。扩展是任何企业在尝试满足增加的用户基础时面临的最大挑战之一。

可扩展性不过是系统/程序更好地处理工作增长的能力。换句话说,可扩展性是系统/程序扩展的能力。

在开始下一节之前,让我们详细讨论和理解扩展,因为这将是我们从单体过渡到微服务时的一个重要组成部分。

系统的可扩展性是其处理不断增加/增加的工作负载的能力。我们可以通过两种主要策略或可扩展性类型来扩展我们的应用程序。

垂直扩展或向上扩展

在垂直扩展中,我们分析现有应用程序,以找到由于执行时间较长而使应用程序变慢的模块部分。使代码更高效可能是一种策略,这样就可以消耗更少的内存。这种减少内存消耗的练习可能针对特定的模块或整个应用程序。另一方面,由于这种策略涉及明显的挑战,我们可以在不更改应用程序的情况下,向现有的 IT 基础设施添加更多资源,例如升级 RAM 或添加更多磁盘驱动器。垂直扩展的这两条路径都有其有益程度的限制。在某个时间点之后,产生的利益将达到平台期。重要的是要记住,这种类型的扩展需要停机时间。

水平扩展或扩展到外部

在水平扩展中,我们深入分析那些由于高并发等因素而对整体性能影响较大的模块;因此,这将使我们的应用程序能够服务不断增长的用户基础,现在用户数量已达到百万级别。我们还实施了负载均衡来处理更多的工作量。向集群中添加更多服务器的选项不需要停机时间,这无疑是一个优势。每个案例都是不同的,因此是否值得投入额外的电力、许可证和冷却成本,以及到什么程度,将根据每个案例进行评估。

扩展将在第八章“扩展微服务”中详细讨论。

部署挑战

当前应用程序也存在部署挑战。它被设计为一个单体应用程序,任何对顺序模块的更改都要求重新部署整个应用程序。这是耗时且每次更改都必须重复整个周期。这意味着这可能会是一个频繁的周期。在这种情况下,扩展可能只是一个遥远的梦想。

如同讨论的关于扩展当前应用程序的部署挑战,需要我们部署整个组件,模块之间相互依赖,这是一个单一的.NET 组件应用程序。一次性部署整个应用程序也使得测试我们应用程序的整个功能成为强制性的。这种练习的影响将是巨大的:

  • 高风险部署:一次性部署整个解决方案或应用程序存在高风险,因为即使是对某个模块的单个更改,所有模块都将被部署。

  • 更高的测试时间:由于我们必须部署完整的应用程序,因此我们必须测试整个应用程序的功能。没有测试,我们不能上线。由于高度依赖性,更改可能会在其他模块中引起问题。

  • 计划外停机时间:完整的生产部署需要代码被完全测试,因此我们需要安排我们的生产部署。这是一个耗时的工作,导致高停机时间。尽管在这个时间段内,由于系统不可用,业务和客户都会受到影响,这可能导致业务收入损失。

  • 生产中的错误:任何项目经理的梦想都是无错误的部署。然而,这远非现实,每个团队都害怕这种可能性。单体应用程序与这种场景并无不同,生产错误的解决比说起来容易做起来难。如果一些之前的错误尚未解决,情况可能会变得更加复杂。

组织对齐

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

  • 同样的目标:在一个团队中,所有团队成员都有相同的目标,那就是在每天结束时及时且无错误地交付。然而,拥有庞大的代码库和当前的单一架构风格,对于团队成员来说并不会感到舒适。由于代码和相关的交付成果相互依赖,开发团队中也会出现同样的效果。在这里,每个人都只是在匆忙和努力地完成工作。互相帮助或尝试新事物的想法不会出现。简而言之,这个团队不是一个自我组织的团队。

罗伊·奥斯霍夫在他的书《团队领导者》中定义了团队的三个阶段:

生存阶段:没有时间学习。

学习阶段:学习解决自己的问题。

自我组织阶段:促进,实验。

  • 不同的视角:由于功能增强、错误修复或模块相互依赖等原因,开发团队花费太多时间来完成交付成果。QA 团队依赖于开发团队,而开发团队也有自己的问题。当开发人员开始处理错误、修复或功能增强时,QA 团队就会陷入困境。没有为 QA 提供单独的环境或构建来进行测试。这种延迟阻碍了整体交付,客户或最终用户将无法及时获得新功能或修复。

模块化

就我们的单体应用程序而言,我们可能有一个订单模块,模块订单的变化会影响模块库存等等。正是模块化的缺失导致了这种状况。

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

业务正在增长,其客户也在飞速增长。来自不同地区的新的或现有客户在应用的使用上有着不同的偏好。有些人喜欢访问网站,但其他人更喜欢使用移动应用。系统结构是这样的,我们无法在网站和移动应用之间共享组件。这使得为业务引入移动/设备应用成为一项挑战性任务。在这种情况下,公司会失去那些偏好移动应用的客户。

替换组件应用中的困难在于使用第三方库、外部系统(如支付网关)和外部订单跟踪系统。在目前风格的单体架构应用中替换旧组件是一项繁琐的工作。例如,如果我们考虑升级我们模块的库,该库正在消耗外部订单跟踪系统,那么整个变更将证明是非常困难的。此外,用另一个支付网关替换我们的支付网关也将是一项复杂的任务。

在任何上述场景中,每次我们升级组件时,都会升级应用程序中的所有内容,这要求对系统进行完全测试,并需要大量的停机时间。除此之外,升级可能会以生产中的错误形式出现,这将需要你重复整个开发、测试和部署的周期。

大型数据库

我们当前的应用程序拥有一个庞大的数据库,包含单一架构和大量的索引。这种结构在调整性能时提出了挑战:

  • 单一架构:数据库中的所有实体都被归类到一个名为 dbo 的单一架构下。这又因为与单一架构相关的各种表属于不同的模块而阻碍了业务;例如,客户和供应商表属于同一个架构,即 dbo

  • 数量众多的存储过程:目前,数据库中拥有大量的存储过程,这些过程也包含了相当一部分的业务逻辑。一些计算是在存储过程中进行的。因此,当需要优化或将其分解成更小的单元时,这些存储过程变得难以处理。

在计划部署时,团队必须仔细审查每一个数据库变更。这又是一项耗时的工作,很多时候甚至比构建和部署本身还要复杂。

微服务的先决条件

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

FlixOne 是一家遍布印度的电子商务玩家(销售书籍)。他们以非常快的速度增长,并在同时多元化他们的业务。他们基于.NET 框架构建了现有的系统,这是一个传统的三层架构。他们有一个庞大的数据库,是这个系统的核心,并且在其生态系统中还有外围应用。其中一个应用是为他们的销售和物流团队设计的,碰巧是一个 Android 应用。这些应用连接到他们的集中数据中心,并面临性能问题。FlixOne 有一个内部开发团队,并得到外部顾问的支持。参看以下图表:

应用功能概述

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

FlixOne 应用架构

上述图表仅描述了我们的购物车模块。该应用是用 C#、MVC5 和 Entity Framework 构建的,并且是一个单一项目应用。这张图片只是我们应用架构的示意图。这个应用是基于 Web 的,可以通过任何浏览器访问。最初,任何使用 HTTP 协议的请求都会落在使用 MVC5 和 JQuery 开发的用户界面上。对于购物车活动,UI 与购物车模块交互,该模块实际上是一个业务逻辑层,它进一步与数据库层(用 C#编写)通信;数据在数据库(SQL Server 2008R2)中持久化。

应用功能概述

在这里,我们将了解 FlixOne 书店应用的功能概述。这只是为了可视化我们的应用。以下是从购物车到简化的功能概述的应用。

应用架构概述

在当前的应用中,客户首先进入主页,在那里他们可以看到特色/突出显示的书籍。如果他们没有找到喜欢的书籍,他们可以选择搜索书籍项目。在得到期望的结果后,客户可以选择书籍项目并将它们添加到他们的购物车中。在最终结账之前,客户可以验证书籍项目。一旦客户决定结账,现有的购物车系统就会将他们重定向到外部支付网关,以支付购物车中所需支付的书籍项目的金额。

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

当前挑战的解决方案

业务正在迅速增长,因此我们决定在另外 20 个城市开设我们的电子商务网站;然而,我们仍在面对现有应用程序的挑战,并且难以适当地服务现有用户群。在这种情况下,在我们开始过渡之前,我们应该使我们的单体应用程序准备好向微服务过渡。

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

尽管开发人员能够分割代码并重用它,但这个提出的解决方案并不足以解决我们现有的应用程序问题。然而,业务逻辑的内部处理将保持不变,而不会改变它与 UI 或数据库交互的方式。新的代码将与 UI 和数据库层交互,而数据库仍然保持为同一个旧的单一数据库。由于我们的数据库保持未分割并且紧密耦合的层,必须更新和部署整个代码库的问题仍然存在。因此,这个解决方案不适合解决我们的问题。

处理部署问题

在上一节中,我们讨论了我们将面临的当前.NET 单体应用程序的部署挑战。在本节中,让我们看看我们如何通过在同一个.NET 堆栈内创建或适应一些实践来克服这些挑战。

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

制作更好的单体应用程序

我们理解我们现有单体应用程序的所有挑战。我们必须以更好的方式为新增长服务。随着我们广泛地增长,我们不能错过吸引新客户的机会。如果我们错过解决任何挑战,那么我们就会失去商业机会。让我们讨论一些解决这些问题的观点。

介绍依赖注入

我们的模块相互依赖,因此由于一个模块的变化,我们面临着诸如代码重用性和未解决的错误等问题。这些都是部署挑战。为了解决这些问题,让我们将应用程序分割成这样的方式,以便我们可以将模块划分为子模块。我们可以将我们的订单模块分割成这样的方式,使其能够实现接口,并且这可以从构造函数中启动。以下是一个小的代码片段,展示了我们如何在现有的单体应用程序中应用这一点。

这里是一个代码示例,展示了我们的Order类,其中我们使用了构造函数注入:

    namespace FlixOne.BookStore.Common
    {
      public class Order : IOrder
      {
        private readonly IOrderRepository _orderRepository;
        public Order(IOrderRepository orderRepository)
        {
          _orderRepository = orderRepository;
        }
        public OrderModel GetBy(Guid orderId)
        {
          return _orderRepository.Get(orderId);
        }
      }
    }

控制反转,或 IoC,不过是对象不创建它们依赖来完成工作的其他对象的一种方式。

在前面的代码片段中,我们以这种方式抽象了我们的Order模块,使其能够使用IOrder接口。之后,我们的Order类实现了IOrder接口,并且通过使用控制反转,我们创建了一个对象,因为这是通过控制反转自动解决的。

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

    namespace FlixOne.BookStore.Common
    {
      public interface IOrderRepository
      {
        OrderModel Get(Guid orderId);
      }
    }
    namespace FlixOne.BookStore.Common
    {
      public class OrderRepository : IOrderRepository
      {
        public OrderModel Get(Guid orderId)
        {
          //call data method here
          return new OrderModel
          {
            OrderId = Guid.NewGuid(),
            OrderDate = DateTime.Now,
            OrderStatus = "In Transit"
          };
        }
      }
    }

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

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

    namespace FlixOne.BookStore.Controllers
    {
      public class OrderController : Controller
      {
        private readonly IOrder _order;
        public OrderController(IOrder order)
        {
          _order = order;
        }
        // GET: Order
        public ActionResult Index()
        {
          return View();
        }
        // 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模式。这不会影响任何与从数据库中检索数据相关的功能。但它将以结构化或组织的方式排列我们的表,这样我们就能识别并关联每个表及其特定的模块。当我们处于将单体应用过渡到微服务阶段时,这项练习将非常有帮助。参见图示:

图片

在前面的图中,我们看到数据库模式是如何在逻辑上分离的。它并不是在物理上分离——我们的订单模式库存模式属于同一个数据库。因此,在这里我们在逻辑上而不是在物理上分离数据库模式。

我们还可以以我们的用户为例——并非所有用户都是管理员或属于特定的区域、地区或区域。但我们的用户表应该以这样的方式组织,即我们应该能够通过表名或它们的结构来识别用户。在这里,我们可以根据地区来组织我们的用户表。我们应该将用户表映射到区域表,这样它就不会影响或对现有的代码库造成任何变化。

  • 将业务逻辑从存储过程迁移到代码:在当前数据库中,我们有数千行存储过程,其中包含大量的业务逻辑。我们应该将业务逻辑移动到我们的代码库中。在我们的单体应用中,我们使用 Entity Framework;在这里,我们可以避免创建存储过程。我们可以将所有的业务逻辑整合到代码中。

数据库分片和分区

在数据库分片和分区之间,我们可以选择数据库分片,这样我们将将其分解成更小的数据库。这些较小的数据库将部署在单独的服务器上:

图片

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

上述图示概述了我们的数据库是如何被划分为更小的数据库的。请看以下图示:

图片

DevOps 文化

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

自动化

基础设施设置可能是一个非常耗时的工作;当基础设施为他们准备时,开发者将保持空闲。他们将在加入团队并做出贡献之前花费一些时间。基础设施设置的过程不应该阻止开发者变得高效,因为这会降低整体生产力。这应该是一个自动化的过程。使用 Chef 或 PowerShell,我们可以轻松创建我们的虚拟机,并在需要时快速增加开发者数量。这样,我们的开发者可以在加入团队的当天开始工作。

Chef 是一个 DevOps 工具,它提供了一个框架来自动化和管理你的基础设施。

PowerShell 可以用来创建我们的 Azure 机器并设置 TFS。

测试

我们打算将自动化测试作为解决我们先前问题的方案,这些问题是在部署期间测试时遇到的。在这个解决方案的部分,我们必须将我们的测试方法划分为以下几方面:

  • 采用 测试驱动开发(TDD)。使用 TDD,开发者需要测试自己的代码。测试不过是一段代码,可以验证功能是否按预期工作。如果发现任何功能不符合测试代码,相应的单元测试就会失败。由于你知道问题所在,因此可以轻松修复此功能。为了实现这一点,我们可以利用如 MS 测试或单元测试等框架。

  • 质量保证团队可以使用脚本来自动化他们的任务。他们可以通过使用 QTP 或 Selenium 框架来创建脚本。

版本控制

当前系统没有任何版本控制系统,因此在变更过程中发生问题时无法回滚。为了解决这个问题,我们需要引入版本控制机制。在我们的情况下,这应该是 TFS 或 Git。使用版本控制,我们现在可以在发现更改破坏了某些功能或引入了任何意外行为时回滚更改。我们现在有能力跟踪在此应用程序上工作的团队成员所做的更改,达到个人层面。然而,在我们的单体应用程序中,我们没有这种能力。

部署

在我们的应用程序中,部署是一个巨大的挑战。为了解决这个问题,我们引入了持续集成(CI)。在这个过程中,我们需要设置一个 CI 服务器。随着 CI 的引入,整个过程实现了自动化。一旦任何团队成员通过版本控制 TFS 或 Git(在我们的案例中)将代码提交,CI 过程就会启动。它确保新代码被构建,并且运行单元测试以及集成测试。在成功构建或失败的情况下,团队都会被通知结果。这使得团队能够快速响应问题。

接下来,我们转向持续部署。在这里,我们引入了各种环境,例如开发环境、预发布环境、QA 环境,等等。现在,一旦任何团队成员将代码提交,CI 就会启动。它调用单元/集成测试套件,构建系统,并将其推送到我们已设置的各种环境中。这样,开发团队提供适合 QA 的构建的周转时间就缩短到了最小。

在单体架构中识别分解候选者

我们现在已经清楚地识别出当前 FlixOne 应用程序架构及其产生的代码对开发团队所提出的各种问题。此外,我们也理解了开发团队无法承担哪些商业挑战以及原因。

并非团队能力不足——问题在于代码。让我们继续前进,看看针对我们需要迁移到微服务架构的 FlixOne 应用程序各个部分的最佳策略是什么。你应该知道,你有一个单体架构的候选者,它会在以下某个领域引起问题:

  • 焦点部署:尽管这在整个过程的最后阶段,但它确实需要更多的尊重,这是理所当然的。在这里,重要的是要理解这个因素从识别和设计的初始阶段开始就塑造和定义了整个开发策略。以下是一个例子:业务要求你解决两个同等重要的问题。其中一个问题可能需要你对更多相关模块进行测试,而另一个问题的解决方案可能允许你进行有限的测试。做出这样的选择是错误的。业务不应该有做出这种选择的权利。

  • 代码复杂性:这里的关键是拥有较小的团队。你应该能够为与单个功能相关的更改分配小型开发团队。小型团队由一到两名成员组成。如果超过这个规模,就需要项目经理。这意味着模块之间的相互依赖性比应有的要强。

  • 技术采用:你应该能够升级组件到新版本或不同技术,而不会破坏其他东西。如果你必须考虑依赖它的组件,你就有不止一个候选者。即使你必须担心这个组件所依赖的模块,你仍然有不止一个候选者。我记得我的一个客户有一个专门的团队来测试即将发布的技术是否适合他们的需求。后来我了解到,他们实际上会移植其中一个模块,并测量整个系统的性能影响、努力需求和周转时间。不过,我并不认同这一点。

  • 高资源消耗:在我看来,系统中的每一件事,从内存、CPU 时间和 I/O 需求,都应该被视为一个模块。如果任何一个模块花费的时间更多,并且/或者更频繁,它应该被单独指出。在任何涉及高于正常内存的操作中,处理时间会阻塞延迟,I/O 会使系统等待;在我们的情况下,这将是好的。

  • 人力资源依赖:如果跨模块移动团队成员似乎工作量太大,你就有更多的候选者。开发者很聪明,但如果他们在大系统中遇到困难,这不是他们的错。将系统分解成更小的单元,开发者将感到更加舒适和高效。

重要的微服务优势

我们已经完成了将候选者迁移到微服务的第一步。了解微服务提供的相应优势将是有价值的。

技术独立性

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

整个应用程序可以基于 SQL 服务器进行数据存储,而库存则可以基于 NoSQL。这种灵活性是无限的。

消除相互依赖

由于我们试图在每个微服务中实现隔离的功能,因此很容易添加新功能、修复错误或升级技术。这将对其他微服务没有影响。现在你有了垂直代码隔离,这使你能够在保持快速部署的同时完成所有这些操作。

这还没有结束。FlixOne 团队现在有能力在现有的支付网关旁边发布一个新的支付网关选项。这两个支付网关可以共存,直到团队和业务所有者都对报告感到满意。这就是这种架构巨大力量的体现。

与业务目标对齐

商业主不一定擅长理解为什么某个功能更难或耗时更长来实现。他们的责任是不断推动业务并使其增长。开发团队应该成为业务目标的支点,而不是障碍。

极其重要的是要理解,能够快速响应业务需求并适应市场趋势的能力不是微服务的副产品,而是其目标。

只有较小的团队能够实现这一点,这使得它更适合商业主。

成本效益

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

由于停机时间要么被消除要么减少到最小,用户体验得到了提升。

易于扩展

在垂直隔离已经实施,并且每个微服务向整个系统提供特定服务的情况下,扩展变得容易。不仅扩展候选者的识别更容易,而且成本更低。这是因为我们只扩展整个微服务生态系统的一部分。

这种练习可能对业务来说成本高昂;因此,优先考虑哪个微服务应该首先扩展现在可以成为业务团队的选择。这个决定不再需要是开发团队的选择。

安全性

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

数据管理

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

因为每个微服务都有自己的独立数据库,与数据库更改相关的决策可以很容易地委派给相应的团队。我们不必担心对整个系统的影响,因为不会有的。

同时,这种数据库的分离为团队实现自我组织提供了可能性。他们现在可以开始尝试了。

例如,团队现在可以考虑使用 Azure Table 存储或 Azure Redis Cache 来存储庞大的产品目录,而不是像目前那样使用数据库。团队现在不仅可以进行实验,他们的经验还可以很容易地根据其他团队的需求以对他们方便的时间表进行复制。

事实上,现在没有任何东西阻止 FlixOne 团队创新并同时使用多种技术,然后在现实世界中比较性能并做出最终决定。一旦每个微服务都有自己的数据库,FlixOne 将看起来是这样的:

图片

集成单体架构

每当选择从单体架构转向微服务风格的架构时,该活动的时序和成本轴将产生一些阻力。商业评估可能会反对迁移那些没有为过渡提供商业案例的单体应用程序的部分。

如果我们从一开始就开发应用程序,情况将会有所不同。然而,这也是我认为微服务的力量所在。对整个单体架构的正确评估可以安全地识别出以后要移植的单体部分。

然而,为了确保这些隔离的部分不会在未来对其他微服务造成问题,我们必须采取一种防范措施来降低风险。

对于单体应用程序的这些部分的目标是使它们以与其他微服务相同的方式进行通信。这样做涉及各种模式,并且你利用单体应用程序开发中使用的整个技术堆栈。

如果你使用事件驱动模式,请确保单体应用程序能够发布和消费事件,包括对源代码的详细修改以使这些操作成为可能。此过程也可以通过创建一个发布和消费事件的代理来完成。然后,事件代理可以将这些事件转换为单体应用程序,以将源代码中的更改保持在最低限度。最终,数据库将保持不变。

如果你计划使用 API 网关模式,请确保你的网关能够与单体应用程序通信。为了实现这一点,一个选项是修改应用程序的源代码以暴露 RESTful 服务,这些服务可以很容易地被网关消费。这也可以通过创建一个单独的微服务来实现,该微服务将暴露单体应用程序过程作为 REST 服务。创建一个单独的微服务可以避免对源代码进行大的更改。然而,它需要维护和部署一个新的组件。

Azure Service Fabric 概述

当我们在.NET Core 世界中谈论微服务时,Azure Service Fabric 是广泛使用的微服务名称。在本节中,我们将讨论 Fabric 服务。

这是一个帮助我们轻松打包、部署和管理可扩展且可靠的微服务的平台(容器也类似于 Docker 等)。有时,由于复杂的架构问题,作为开发人员很难专注于您的核心职责,借助 Azure 服务 fabric,开发者无需担心架构问题。

这捆绑了 Azure SQL 数据库、Cosmos DB、Microsoft Power BI、Azure 事件中心、Azure IoT Hub 以及许多其他核心服务的功能。

如官方文档所述 (docs.microsoft.com/en-us/azure/service-fabric/service-fabric-overview):

  • 服务 fabric——任何操作系统,任何云:您只需创建一个服务 fabric 集群,这个集群可以在 Azure(云)或本地运行,在 Linux 或 Windows 服务器上运行。此外,您还可以在其他公共云上创建集群。

  • 服务 fabric - 无状态和有状态微服务:是的,借助服务 fabric,您可以构建无状态和/或有状态的应用程序。

“如官方文档 (docs.microsoft.com/en-us/azure/service-fabric/) 中所述的微服务:

无状态微服务(如协议网关和 Web 代理)不维护请求及其响应之外的可变状态。Azure 云服务工作角色是一个无状态服务的例子。有状态微服务(如用户账户、数据库、设备、购物车和队列)在请求及其响应之外维护可变、权威的状态。

可用的服务 fabric 编程模型有多种,但本章范围之外。更多信息请参考:docs.microsoft.com/en-us/azure/service-fabric/service-fabric-choose-framework.

摘要

在本章中,我们详细讨论了微服务架构风格,其历史以及它与前辈(单体和 SOA)的不同之处。我们进一步定义了单体在处理大型系统时所面临的各项挑战。可扩展性和可重用性是 SOA 相对于单体提供的一些明确优势。我们还通过实现一个真实的单体应用程序来讨论了单体架构的局限性,包括扩展问题。微服务架构风格通过减少代码依赖性和隔离任何微服务工作的数据集大小来解决了所有这些问题。我们利用依赖注入和数据库重构实现了这一点。我们还进一步探讨了自动化、持续集成和部署。这些使得开发团队能够让业务赞助商首先选择响应哪些行业趋势。这导致了成本效益、更好的业务响应、及时的技术采用、有效的扩展和消除对人类的依赖。最后,我们讨论了 Azure 服务网格,并了解了服务网格及其不同的编程模型。

在下一章中,我们将继续将现有的应用程序过渡到微服务风格的架构,并检验我们的知识。

第二章:实现微服务

在上一章中,我们讨论了分层单体架构的问题。在本章中,我们将讨论如何从现有系统中重构它们,并为产品和订单构建独立的微服务。在本章中,我们将涵盖以下主题:

  • C# 7.0、Entity Framework Core、Visual Studio 2017 和 Microsoft SQLServer 简介

  • 微服务的大小

  • 什么使一个好的服务?

  • 领域驱动设计(DDD)及其对微服务的重要性

  • Seam 的概念

  • 微服务之间的通信

  • 重温 Flix One 案例研究

简介

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

C# 7.0

C# 是微软开发的一种编程语言。本书撰写时的当前版本是 C# 7.0。该语言于 2002 年出现。这是一种面向对象和面向组件的语言。当前版本具有各种新功能,如 ValueTuple、析构函数、模式匹配、switch 语句局部函数等。

我们不会深入探讨这些功能,因为它们超出了本书的范围。更多详细信息,请参考:docs.microsoft.com/en-us/dotnet/csharp/whats-new/

Entity Framework Core

Entity Framework CoreEF Core)是微软 Entity Framework 的跨平台版本,它是最受欢迎的 对象关系映射器ORM)之一。

ORM 是一种帮助您根据所需业务输出查询和操作数据的技术。有关更多详细信息,请参阅stackoverflow.com/questions/1279613/what-is-an-orm-and-where-can-i-learn-more-about-it

EF Core 支持多种数据库。数据库的完整列表可在以下链接找到:docs.microsoft.com/en-us/ef/core/providers/index。当前 EF Core 的版本是 2.0。为了熟悉 EF Core,我建议您详细阅读以下内容:docs.microsoft.com/en-us/ef/core/get-started/index

Visual Studio 2017

Visual Studio 是微软创建的最好的 集成开发环境IDE)之一。它使开发者能够使用著名的语言(例如,C#、VB.NET、F# 等)以各种方式工作。Visual Studio 2017 的当前发布版本是更新 3(VS15.3)。

集成开发环境(IDE)是一种软件应用程序,它为程序员提供了一个使用编程语言编写程序的平台。更多信息,请访问:en.wikipedia.org/wiki/Integrated_development_environment

微软还发布了适用于 macOS 的 Visual Studio,Visual Studio 的新版本拥有许多惊人的功能。更多信息请参阅www.visualstudio.com/vs/whatsnew/。本书中的所有示例都是使用 Visual Studio 2017 更新 3 编写的。您还可以下载免费的社区版:www.visualstudio.com/

Microsoft SQLServer

Microsoft SQLServerMSSQL)是一个关系型数据库管理系统软件应用。它主要用于作为数据库软件来存储和检索数据。这是建立在 SQL 之上的,即结构化查询语言:searchsqlserver.techtarget.com/definition/SQL

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

微服务的大小

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

如其名所示,微服务应该是微型的。一个问题随之而来:什么是微型?微服务全部关乎大小和粒度。为了更好地理解这一点,让我们考虑在第一章中讨论的应用,微服务简介

我们希望参与此项目的团队始终保持其代码的一致性。在发布完整项目时,保持一致性尤为重要。我们需要首先将我们的应用/特定部分分解成更小的功能/主服务段的片段。让我们讨论需要考虑的因素,以确保微服务的高级隔离:

  • 由于需求变更而产生的风险:一个微服务的需求变更应独立于其他微服务。在这种情况下,我们将我们的软件隔离/分割成更小的服务,以便如果某个服务有任何需求变更,它们将独立于另一个微服务。

  • 功能性变更:我们将隔离那些很少变更的功能性,以及那些可能频繁修改的依赖功能性。例如,在我们的应用中,客户模块通知功能很少变更。但与其相关的模块,如订单,在其生命周期中更有可能频繁发生业务变更。

  • 团队变更:我们还应考虑以这种方式隔离模块,使得一个团队能够独立于所有其他团队工作。如果使新开发者能够高效完成这些模块中的任务的过程不依赖于团队外的人,这意味着我们处于有利的位置。

  • 技术变更:技术使用需要在每个模块内垂直隔离。一个模块不应依赖于来自另一个模块的技术或组件。我们应该严格隔离使用不同技术或堆栈开发的模块,或者将它们迁移到公共平台作为最后的手段。

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

什么使良好的服务变得出色?

在微服务概念化之前,每当考虑到企业应用集成时,中间件看起来是最可行的选项。软件供应商提供了企业服务总线ESB),它是中间件的最佳选项之一。

除了考虑这些解决方案外,我们的主要优先级应倾向于架构特性。当微服务出现时,中间件不再是考虑的重点。相反,焦点转向了对业务问题的思考以及如何借助架构来解决这些问题。

为了使服务能够被开发者和用户轻松使用和维护,该服务必须具备以下特性(我们也可以将这些视为良好服务的特征):

  • 标准数据格式:良好的服务在与其他组件交换服务或系统时应遵循标准数据格式。在.NET 堆栈中最常用的数据格式是 XML 和 JSON。

  • 标准通信协议:良好的服务应遵守标准通信格式,如 SOAP 和 REST。

  • 松散耦合:良好服务的一个重要特征是它遵循松散耦合。当服务松散耦合时,我们不必担心变化。一个服务的变更不会影响其他服务。

DDD 及其对微服务的重要性

领域驱动设计DDD)是一种设计和复杂系统的方法论和过程。在这些部分中,我们将简要讨论 DDD 及其在微服务环境中的重要性。

领域模型设计

领域设计的首要目标是理解确切的领域问题,然后制定一个可以用任何语言或技术集编写的模型。例如,在我们的 Flix One 书店应用程序中,我们需要理解订单管理库存管理

这里是领域驱动模型的一些特征:

  • 领域模型应专注于特定的商业模式,而不是多个商业模式

  • 它应该是可重用的

  • 它应该设计成可以以松散耦合的方式调用,而不同于系统的其余部分

  • 它应该独立于持久化实现进行设计

  • 它应该从一个项目被拉取到另一个位置,因此它不应该基于任何基础设施框架

微服务的重要性

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

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

如果你觉得这个基准过程对你来说很难实现,你可以选择另一条路。遍历每个微服务中的所有接口和类。考虑到我们遵循的所有步骤和行业标准编码指南,任何新加入系统的人都应该能够理解其目的。

你还可以进行另一个简单的测试,以检查是否实现了服务的正确垂直隔离。你可以部署每一个,并让它们与仍然不可用的其他服务一起运行。如果你的服务上线并继续监听传入的请求,你可以给自己鼓掌。

你可以从隔离部署能力中获得许多好处。仅仅独立部署它们的能力,就允许它们中的主机进入自己的独立进程。它允许你利用云和其他混合托管模型的力量,这些模型是你能想到的。你也可以自由地为每一个选择不同的技术。

接缝的概念

微服务的核心能力在于能够在与其他系统隔离的情况下处理特定的功能。这转化为之前讨论的所有优势,例如减少模块依赖、代码重用性、更容易的代码维护和更好的部署。

在我看来,在实施微服务的过程中获得相同的属性应该在实施过程中保持。为什么将单体迁移到微服务的过程会痛苦,而不是像使用微服务本身那样有回报?只需记住,过渡不可能一蹴而就,需要细致的计划。许多有能力的解决方案架构师在展示他们高度有能力的团队时,他们的方法各不相同。答案不仅在于已经提到的点,还在于对业务本身的潜在风险。

这是非常容易实现的。然而,我们必须正确地识别我们的方法才能实现它。否则,整个将单体应用过渡到微服务的过程可能会变得非常糟糕。

模块间依赖

当尝试将单体应用过渡到微服务风格的架构时,这始终应该是起点。识别并挑选出那些对其他模块依赖最少且依赖程度最低的应用部分。

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

尽管已经识别出这些缝隙,但实现微服务风格开发的能力仍然还有一段距离。这是一个好的开始。

技术

这里需要采取双管齐下的方法。首先,你必须确定应用基础框架中正在利用的不同功能。这种区分可以基于对某些数据结构的重依赖、执行进程间通信或报告生成活动。这是比较容易的部分。

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

其中一些部分可能看起来并不是最终微服务风格架构应用的很有希望的候选者。但它们现在仍然需要处理。最终,它们将使你能够轻松地进行过渡。

团队结构

随着这个识别过程的每一次迭代执行,这个因素变得越来越重要。可能会有基于各种理由区分的团队,例如他们的技术技能集、地理位置或安全要求(员工与外包)。

如果功能的一部分需要特定的技能集,那么你可能正在寻找另一个可能的 Seam 候选。团队可以由不同程度的不同化因素组成。作为向微服务过渡的一部分,能够使他们独立工作的清晰区分可以进一步优化他们的生产力。

这也可以提供一种形式的利益,即保护公司的知识产权——将应用程序的特定部分外包给顾问并不少见。允许顾问或合作伙伴仅在一个特定模块上帮助你的能力使得这个过程更加简单和安全。

数据库

任何企业系统的核心和灵魂是其数据库。它是系统在任何给定日期的最大资产。它也是在这种练习中最脆弱的部分。难怪数据库架构师在你要求他们进行哪怕是最小的更改时,听起来会显得有些刻薄和侵扰。他们的领域由数据库表和存储过程定义。

他们领域的健康状况是通过参照完整性和执行各种事务所需的时间来评判的。我不再认为他们过度行事是有罪的。他们有理由这样做——他们的过去经验。是时候改变了。让我告诉你,这不会容易,因为一旦我们踏上这条路,我们就必须利用一种完全不同的方法来处理数据完整性。

你可能会认为最简单的方法是一次性将整个数据库分割开来,但这并非如此。这可能会让我们陷入我们一直试图避免的情况。让我们看看如何以更有效的方式来进行这项工作。

随着你继续前进,在模块依赖分析之后收集碎片,确定正在用于与数据库交互的数据库结构。在这里你需要执行两个步骤。首先,检查你是否可以将代码中的数据库结构隔离出来进行分解,并将其与新定义的垂直边界对齐。其次,确定分解底层数据库结构需要什么。

如果分解底层数据结构看起来很困难,请不要担心。如果它似乎涉及到你尚未开始迁移到微服务的其他模块,这是一个好兆头。不要让数据库更改定义你将选择并迁移到微服务架构的模块。相反,保持另一种方式。这确保了当数据库更改被采纳时,依赖于该更改的代码已经准备好吸收这些更改。

这确保了你在忙于修改依赖于这部分数据库的代码时,不会陷入数据完整性的战斗。尽管如此,这种数据库结构应该引起你的注意,以便于选择依赖于它们的模块。这将允许你一次性轻松完成所有相关模块的微服务迁移。参考以下图表:

图片

在这里,我们还没有破坏数据库。相反,我们只是将数据库访问部分在第一步中分离成层。

我们所做的是简单地将代码数据结构映射到数据库中,它们不再相互依赖。让我们看看当我们移除外键关系时这一步会如何运作。

如果我们能够将用于访问数据库的代码结构和数据库结构一起过渡,我们将节省时间。这种方法可能因系统而异,并可能受到我们个人偏见的影响。如果你的数据库结构变化似乎影响了尚未标记为过渡的模块,那么现在可以继续前进。

在这里需要理解的一个重要点是,当你分解这个数据库表或将其与另一个部分结构合并时,哪些变化是可以接受的?最重要的是不要回避打破那些外键关系。这听起来可能与我们传统维护数据完整性的方法有很大的不同。然而,移除外键关系是在将数据库重构以适应微服务架构时最根本的挑战。记住,微服务应该是独立于其他服务的。如果有与其他系统部分的外键关系,它会使它依赖于拥有该数据库部分的服务。参考以下图表:

图片

作为第二步的一部分,我们在数据库表中保留了外键字段,但移除了外键约束。因此,ORDER 表仍然持有关于 ProductID 的信息,但现在外键关系已经断裂。参考以下图表:

图片

这就是我们最终会看到的微服务风格架构。中央数据库将被移除,以支持每个服务拥有自己的数据库。因此,分离代码中的数据结构和移除外键关系是我们最终做出改变的准备。前一个图表中微服务之间的连接边界表示了服务间的通信。

执行这两个步骤后,你的代码现在可以准备好将 ORDERPRODUCT 分割成独立的服务,每个服务都有自己的数据库。

如果这里的所有讨论都让你对至今为止安全执行的所有那些事务感到困惑,那么你并不孤单。这个关于事务的挑战的结果绝不是小事,值得集中关注。我们稍后会详细讨论这个问题。在此之前,还有一个部分在数据库中变成了无人之地。它就是主数据或静态数据,就像有些人可能会称呼它那样。

主数据

处理主数据更多的是关于你的个人选择和系统特定的需求。如果你发现主数据在很长时间内都不会改变,并且只占据很少量的记录,那么使用配置文件甚至代码枚举会更合适。

这需要有人在变化发生时偶尔推出配置文件。然而,这仍然为未来留下了缺口。由于系统的其他部分将依赖于这个模块,它将负责这些更新。如果这个模块表现不正确,那么依赖于它的系统其他部分也可能受到负面影响。

另一个选择可能是将主数据完全封装在一个独立的服务中。通过服务提供主数据将提供服务的优势,即服务能够立即了解变化并理解其消费能力。

请求这个服务的过程可能和读取配置文件的过程没有太大区别。它可能更慢,但只需要在必要时执行这么多次。

此外,你还可以支持不同的主数据集。维护每年都不同的产品集相对容易。采用微服务架构风格,始终独立于任何未来的外部依赖是一个好主意。

事务

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

一个好的例子是用户订购特定产品。在订单被接受的时候,有足够的库存可供订购。然而,当订单被记录下来时,由于某种原因,产品服务无法记录订单。我们还不知道这是否是由于库存不足或其他系统内的通信故障。这里有两种可能的选择。让我们逐一讨论。

第一个选项是再次尝试,并在稍后时间执行事务的剩余部分。这要求我们以跟踪跨服务单个事务的方式编排整个事务。因此,任何导致对多个服务执行事务的事务都必须被跟踪。如果其中之一没有成功,它值得重试。这可能适用于长期操作。

然而,对于其他操作,这可能会造成真正的问题。如果操作不是长期存在的,你仍然决定重试,结果将导致锁定其他事务或使事务等待——这意味着无法完成它。

我们在这里可以考虑的另一个选项是取消跨越各种服务的整个事务集。这意味着整个事务集中的任何阶段的单个故障都会导致所有先前事务的回滚。

这是一需要最大谨慎的领域,这将是一个明智的投资。只有在任何微服务风格的架构应用中妥善规划事务时,才能保证稳定的输出。

微服务之间的通信

在前面的章节中,我们将我们的Order 模块分离成Order 服务,并讨论了如何分解ORDERPRODUCT表之间的外键关系。

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

我们将在第三章“集成技术和微服务”中详细讨论微服务之间的通信。

请参考以下图表:

图片

在前面的图表中,我们可以看到我们的 UI 通过API 网关Order 服务Product 服务进行交互。这两个服务在物理上是分离的,并且这些服务之间没有直接交互。以这种方式进行的通信也被称为基于API 网关模式的通信。

API 网关实际上是一个中间层,通过它 UI 可以与微服务进行交互。它还提供了一个更简单的接口,简化了消费这些服务的流程。它根据需要为不同的客户端提供不同级别的粒度(浏览器和桌面)。

我们可以说,它为移动客户端提供粗粒度 API,为桌面客户端提供细粒度 API,并且它可以在其内部使用高性能网络来提供一些严重的吞吐量。

维基百科对粒度的定义如下(en.wikipedia.org/wiki/Granularity):

粒度是指系统分解成小部分的程度,无论是系统本身、其描述还是观察。它是更大实体细分到何种程度。例如,将码分成英寸的粒度比将码分成英尺的粒度更细。”

粗粒度系统由比细粒度系统更少、更大的组件组成;一个系统的粗粒度描述关注大子组件,而细粒度描述关注由较大组件组成的较小组件。

API 网关对微服务的益处

毫无疑问,API 网关对微服务是有益的。使用它,你可以做以下事情:

  • 通过 API 网关调用服务

  • 减少客户端与应用程序之间的往返次数

  • 客户端能够在一个地方访问不同的 API,这些 API 由网关进行隔离

它以这种方式为客户端提供灵活性,使他们能够根据需要与不同的服务进行交互。这样,就无需完全或全部公开服务。API 网关是完整 API 管理的一个组件。在我们的解决方案中,我们将使用 Azure API 管理,并在第三章“集成技术和微服务”中进一步解释。

API 网关与 API 管理的比较

在上一节中,我们讨论了 API 网关如何隐藏实际 API 以防止其客户端直接访问,然后仅将这些客户端的调用重定向到实际 API。API 管理解决方案提供了一个完整的系统来管理其外部消费者的所有 API。所有 API 管理解决方案,例如 Azure API 管理(docs.microsoft.com/en-us/azure/api-management/),都提供了各种功能和能力,例如:

  • 设计

  • 开发

  • 安全性

  • 发布

  • 可伸缩性

  • 监控

  • 分析

  • 赚钱

回顾 Flix One 案例研究

在上一章中,我们查看了一个虚构公司 Flix One Inc.的例子,该公司在电子商务领域运营,并拥有自己的.NET 单体应用:Flix One 书店。我们已经讨论了以下内容:

  • 如何隔离代码

  • 如何隔离数据库

  • 如何对数据库进行反规范化

  • 如何开始过渡

  • 可用的重构方法

在下一节中,我们将开始编写/过渡.NET 单体应用到一个微服务应用。

前提条件

在将我们的单体应用过渡到微服务架构风格时,我们将使用以下工具和技术:

  • Visual Studio 2017 更新 3 或更高版本

  • C# 7.0

  • ASP.NET Core MVC/Web API

  • Entity Framework Core

  • SQL Server 2008R2 或更高版本

转向我们的产品服务

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

  • 技术栈:我们已经为我们的产品服务选择了这项技术;我们将使用 ASP.NET Core、C#、Entity framework (EF) 等技术。微服务可以使用不同的技术栈编写,并且可以被由不同技术创建的客户端消费。对于我们的产品服务,我们将选择 ASP.NET Core。

  • 数据库:我们在第一章,微服务简介中讨论单体应用程序及其数据库隔离时已经讨论过这个问题。在这里,我们将使用 SQL Server,数据库模式将是 Product 而不是 dbo

我们的产品数据库是隔离的。我们将在产品服务中使用这个数据库,如下截图所示:

我们为我们的产品服务创建了一个分离的产品数据库。我们没有迁移所有数据。在接下来的章节中,我们将讨论产品数据库迁移。迁移很重要,因为我们有大量的 FlixOne 书店客户现有记录。我们不能忽略这些记录,并且它们需要迁移到我们修改后的结构中。让我们开始吧。

迁移

在前一个章节中,我们将产品数据库分离,以确保它只被我们的产品服务使用。我们还选择了我们选择的技术栈来构建我们的微服务(产品服务)。在本节中,我们将讨论如何迁移我们现有的代码和数据库,以确保它们与我们的新架构风格完美匹配。

代码迁移

代码迁移不仅仅是将现有单体应用程序中的一层或多层代码提取出来,然后与我们的新创建的 Product 服务捆绑在一起。为了实现这一点,你需要实施到目前为止所学的所有内容。在现有的单体应用程序中,我们有一个单一的资源库,它是所有模块共有的,而对于微服务,我们将为每个模块单独创建资源库,并将它们彼此隔离:

在前面的图像中,Product service 有一个 Product repository,它进一步与其指定的数据存储(Product database)交互。现在我们将更详细地讨论微组件。它们不过是应用程序的独立部分(微服务),即常见的类和业务功能。值得注意的是,Product repository 本身就是微服务世界中的一个微组件。

在我们的最终产品服务中,我们将使用 ASP.NET Core 2.0 来完成,我们将使用模型和控制器来创建我们的 REST API。让我们简要地谈谈这两者:

  • 模型:这是一个代表产品服务中数据的对象。在我们的案例中,已识别的模型堆叠在产品和类别字段中。在我们的代码中,模型不过是一系列简单的 C# 类。当我们谈论 EF Core 时,它们通常被称为 Plain Old CLR ObjectsPOCOs)。POCOs 不过是没有任何数据访问功能的简单实体。

  • 控制器:这是一个简单的 C# 类,继承自 Microsoft.AspNetCore.Mvc 命名空间中的抽象控制器类。它处理 HTTP 请求,并负责创建要发送回的 HTTP 响应。在我们的 Product 服务中,我们有一个处理所有事务的产品控制器。

让我们遵循逐步方法来创建我们的产品服务。

创建我们的项目

如前几节已决定,我们将使用 Visual Studio 在 ASP.NET Core 2.0 或 C# 7.0 中创建我们的 ProductService。让我们看看完成此操作所需的步骤:

  1. 启动 Visual Studio。

  2. 通过导航到文件 | 新建 | 项目来创建一个新的项目。

  3. 从可用的模板选项中选择 ASP.NET Core Web Application。

  4. 将项目名称输入为 FlixOne.BookStore.ProductService,然后点击确定。

  5. 从模板屏幕,选择 Web Application (Model-View-Controller),并确保您已从选项中选择 .NET Core 和 ASP.NET Core 2.0,如以下截图所示:

属性设置

  1. 将其余选项保留为默认值并点击确定。

新解决方案应该看起来像以下截图:

截图

  1. 从解决方案资源管理器,右键单击(或按 Alt + Enter)项目,然后点击属性:

  2. 从属性窗口,点击构建并点击高级。语言版本应该是 C#7.0,如以下截图所示:

截图

添加模型

在我们的单体应用程序中,我们还没有任何模型类。因此,让我们继续添加一个所需的新模型。

要添加新模型,添加一个新文件夹并将其命名为 Models。在解决方案资源管理器中,右键单击项目,然后从添加 | 新文件夹中点击选项:

添加模型

将所有模型类放入名为 Models 的文件夹中并没有硬性规定。实际上,我们可以在应用程序项目的任何地方放置我们的模型类。我们遵循这种做法,因为它使得文件夹名称变得自解释。同时,它也容易识别这个文件夹是用于模型类的。

要添加新的产品类别类(这些类将代表我们的 POCOs),请按照以下步骤操作:

  1. 右键单击 Models 文件夹并选择选项。

  2. 添加新项|类。我们将它们命名为 ProductCategory

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

关于属性名与表列名匹配没有限制。这只是一种常规做法。

以下代码片段描述了我们的 Product.cs 模型类将看起来像什么:

 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; }
   }
 }

以下代码片段显示了我们的 Category.cs 模型类将看起来像什么:

 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; }
   }
 }

添加仓库

在我们的单体应用程序中,整个项目中有一个通用的仓库。在 ProductService 中,通过遵循到目前为止学到的所有原则,我们将创建微组件,这意味着包含数据层的独立仓库。

仓库不过是一个简单的 C# 类,它包含从数据库检索数据的逻辑并将其映射到模型。

添加仓库就像遵循以下步骤一样简单:

  1. 创建一个新的文件夹并将其命名为 Persistence

  2. 添加 IProductRepository 接口和一个实现 IProductRepository 接口的 ProductRepository 类。

  3. 再次,我们命名文件夹为 Persistence,以遵循易于识别的一般原则。

以下代码片段提供了 IProductRepository 接口的一个概述:

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);
  }
}

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

 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 bool Remove(Guid id)
     {
       throw new NotImplementedException();
     }
     public void Update(Product Product)
     {
       throw new NotImplementedException();
     }
   }
 }

注册仓库

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

  1. 打开 Startup.cs

  2. 将仓库添加到 ConfigureServices 方法中。它应该看起来像这样:

public void ConfigureServices(IServiceCollection services)
{
  // Add framework services.
  services.AddMvc();
  services.AddSingleton<IProductRepository, 
  ProductRepository>();
}

添加产品控制器

最后,我们已经到达了可以继续添加我们的控制器类的阶段。这个控制器实际上将负责对传入的 HTTP 请求做出相应的 HTTP 响应。如果你想知道该怎么做,你可以查看 HomeController 类,因为它是由 ASP.NET core 模板提供的默认类。

右键点击 controllers 文件夹,选择 Add | New Item 选项,然后选择 Web API Controller Class。将其命名为 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(IProductRepositoryProductRepository)
     {
       _ProductRepository = ProductRepository;
     }
   }
 }

ProductService API

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

  • 添加一个新的 Product 模块

  • 更新现有的 Product 模块

  • 删除现有的 Product 模块

  • 获取 Product 模块

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

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

添加 EF core 支持

在继续之前,我们需要添加 EF 以便我们的服务可以与实际的产品数据库交互。到目前为止,我们没有向我们的存储库添加任何可以与数据库交互的方法。

要添加 EF core 支持,我们需要添加 EF 的核心 sqlserver 包(我们添加 sqlserver 包是因为我们正在使用 SQL Server 作为我们的数据库服务器)。打开 NuGet 包管理器(工具 | NuGet 包管理器 | 管理 NuGet 包)。

打开 NuGet 包管理器并搜索 Microsoft.EntityFrameworkCore.SqlServer

EF Core DbContext

在前面的部分中,我们为 SQL Server 支持添加了 EF Core 2.0 包;现在我们需要创建一个上下文,以便我们的模型可以与我们的产品数据库交互。我们有产品和分类模型,参考以下列表:

  1. 添加一个新的文件夹并将其命名为 Contexts—添加新文件夹不是强制性的。

  2. context 文件夹中,添加一个新的 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 文件,并在 ConfigureServices 方法下添加 SQL Server db 提供器以支持我们的 EF Core。一旦添加了提供器的 ConfigureServices 方法,我们的 Startup.cs 文件将看起来像这样:
 public void ConfigureServices(IServiceCollection services)
 {
   // Add framework services.
   services.AddMvc();
   services.AddSingleton<IProductRepository, ProductRepository>();
   services.AddDbContext<ProductContext>(o =>o.UseSqlServer
   (Configuration.GetConnectionString("ProductsConnection" )));
 }
  1. 打开appsettings.json文件并添加所需的数据库连接字符串。在我们的提供者中,我们已经将连接密钥设置为ProductsConnection。因此,现在添加以下行以使用相同的密钥设置连接字符串(将数据源更改为您的数据源):
 {
   "ConnectionStrings": 
   {
     "ProductConnection":
     "Data Source=.SQLEXPRESS;Initial Catalog=ProductsDB;
     IntegratedSecurity=True;MultipleActiveResultSets=True"
   }
 }

EF Core 迁移

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

  1. 前往工具 | NuGet 包管理器 | 包管理器控制台。

  2. 从包管理器控制台运行以下命令:

Install--Package Microsoft.EntityFrameworkCore.Tools --pre
Install--Package Microsoft.EntityFrameworkCore.Design 
  1. 要启动迁移,运行以下命令:
 Add-Migration ProductDB

重要的一点是,这应该只在进行一次(当我们还没有通过此命令创建数据库时)。

  1. 现在,每当您的模型有任何更改时,只需执行以下命令:
Update-Database

数据库迁移

在这个阶段,我们已经完成了ProductDatabase的创建。现在,是时候迁移我们现有的数据库了。有许许多多的方法可以做到这一点。我们目前拥有一个庞大的数据库的单体应用程序,其中包含大量的记录。仅使用数据库 SQL 脚本是无法迁移它们的。

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

重新审视仓储和控制器

我们现在准备好通过我们新创建的仓储来促进模型和数据库之间的交互。在适当修改ProductRepository后,它将如下所示:

 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();
     //Rest of the code has been deleted
   }
 }

介绍 ViewModel

models文件夹中添加一个新的类,命名为ProductViewModel。我们这样做是因为,在我们的单体应用程序中,每次我们搜索产品时,它应该显示在其产品类别中。为了支持这一点,我们需要将必要的字段纳入我们的视图模型。我们的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; }
   }
 }

重新审视产品控制器

最后,我们准备好为ProductService创建 REST 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));
    }

     //Rest of code has been removed
   }
 }

我们已经完成了创建 Web API 所需的所有任务。现在,我们需要调整一些设置,以便客户端可以获取有关我们的 Web API 的信息。因此,在接下来的章节中,我们将向 Web API 文档中添加 Swagger。

添加 Swagger 支持

我们在我们的 API 文档中使用 Swagger。在这里,我们不会深入 Swagger 的细节(更多信息,请参阅docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger)。

Swagger 是一个开源且著名的库,为 Web API 提供文档。有关更多信息,请参阅官方链接,swagger.io/

使用 Swagger 添加文档非常简单。按照以下步骤操作:

  1. 打开 NuGet 包管理器。

  2. 搜索Swashbuckle.AspNetCore包。

  3. 选择包然后安装包:

  1. 它将安装以下内容:

    • Swashbuckle.AspNetCore

    • Swashbuckle.AspNetCore.Swagger

    • Swashbuckle.AspNetCore.SwaggerGen

    • Swashbuckle.AspNetCore.SwaggerUI

这在下面的屏幕截图中显示:

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

  1. F5运行应用程序;您将获得一个默认页面。

  2. 通过在 URL 中添加 swagger 来打开 Swagger 文档。因此,URL 将是http://localhost:43552/swagger/

上一张图像显示了产品 API 资源,您可以在 Swagger 文档页面中尝试这些 API。

最后,我们已经完成了我们的单体.NET 应用程序向微服务的过渡,并讨论了ProductService的逐步过渡。对于此应用程序还有更多步骤要来:

  • 微服务如何通信:这将在第三章,集成技术和微服务中讨论。

  • 如何测试微服务:这将在第四章,测试微服务中讨论。

  • 部署微服务:这将在第五章,部署微服务中讨论。

  • 如何确保我们的微服务安全,以及监控微服务:这将在第六章,确保微服务安全第七章,监控微服务中讨论。

  • 微服务如何扩展:这将在第八章,扩展微服务中讨论。

摘要

在本章中,我们讨论了可以用于在高级别识别和隔离微服务的不同因素。我们还讨论了良好服务的各种特征。在谈论领域驱动设计(DDD)时,我们学习了它在微服务环境中的重要性。

此外,我们详细分析了如何通过各种参数正确实现微服务的垂直隔离。我们试图借鉴我们对单体应用带来的挑战及其在微服务中的解决方案的先前理解,并了解到我们可以利用模块间的依赖性、技术利用率和团队结构来识别接口,并有序地将单体架构过渡到微服务架构。

显然,数据库在这个过程中可能构成一个明显的挑战。然而,我们确定了如何使用简单策略和可能的实施方法来执行此过程。然后我们确定,通过减少/移除外键,事务可以以完全不同的方式处理。

从单体过渡到边界上下文,我们进一步将我们的知识应用于将 FlixOne 应用程序过渡到微服务架构。

第三章:集成技术和微服务

在上一章中,我们使用.NET 单体应用程序开发了微服务。这些服务彼此独立,位于不同的服务器上。那么,有什么更好的方法来实现服务之间的交互/通信呢?在这一章中,我们将讨论各种模式和技巧,帮助我们促进这种通信。我们将涵盖以下主题:

  • 服务之间的通信

  • 协作风格

  • 集成模式

  • API 网关

  • 事件驱动模式

  • Azure 服务总线

服务之间的通信

在.NET 单体应用程序的情况下,如果需要访问第三方组件或外部服务,我们使用 HTTP 客户端或另一个客户端框架来访问资源。在第二章《实现微服务》中,我们以独立工作的方式开发了产品服务。但情况并非如此;我们强制要求一些服务相互交互。

因此,这是一个挑战——让服务相互通信。产品服务订单服务都托管在不同的服务器上。这两个服务器彼此独立,基于REST,并通过各自的端点相互通信(当服务与另一个服务交互时,我们称之为服务间通信)。

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

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

图片

在前面的图中(图示视图,不完整),你可以看到我们的不同微服务正在相互通信。我们所有的服务都是 RESTful 的。它们基于 ASP.NET Core Web API。在接下来的部分,我们将详细讨论服务是如何被调用的。这被称为同步方法,客户端必须等待来自服务的响应。在这种情况下,客户端必须等待直到收到完整的响应。

  • 异步:在这种情况下,

协作风格

在前一节中,我们讨论了两种不同的服务间通信模式。这些模式不过是协作风格,具体如下:

  • 请求/响应:在这种情况下,客户端发送一个请求并等待来自服务器的响应。这是一个同步通信的实现。但请求/响应并不只是同步通信的一种实现;我们也可以用它来进行异步通信。

让我们通过一个例子来理解这个概念。在第二章 实现微服务中,我们开发了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方法执行。在这里,我们以sync的方式调用async

为了使我们的代码更简洁,我们在第二章中编写的实现微服务相关代码的基础上添加了扩展方法:

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

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。让我们看看用户/客户如何下订单的例子;以下是功能性的图示概述:

图片

前面的图示显示了购买书籍的过程有几个主要功能:

    • 通过搜索功能,客户可以找到特定的书籍。

    • 在获取搜索书籍的结果后,客户可以查看书籍的详细信息。

    • 一旦用户进入结账环节,我们的系统将确保显示(可供购买的书本)显示正确的数量。例如,可供购买的数量是 10 本微服务与.NET,而顾客购买了一本书。在这种情况下,可供购买的数量现在应该显示为九本。

    • 系统将为购买的书籍生成发票并发送给客户,发送到他们注册的电子邮件。

从概念上讲,这似乎很简单;然而,当我们谈论实现微服务时,我们是在谈论那些分别托管并拥有自己的 REST API、数据库等的服务。这听起来更复杂了。涉及到的方面很多,例如,一个服务如何在从一个或多个服务成功响应后调用或调用另一个服务。这就是事件驱动架构出现的地方:

图片

在前面的图中,我们可以看到当 订单服务 执行时,会触发 发票服务产品服务。这些服务进一步调用内部异步方法以完成其功能。

我们正在使用 Azure API 管理作为我们的 API 网关。在接下来的部分中,我们将详细讨论这一点。

集成模式

到目前为止,我们已经讨论了服务间通信,并通过使用同步和异步通信实现了 ProductService 的实际实现。我们还使用不同的协作风格实现了微服务。我们的 FlixOne 书店(按照微服务架构风格开发)需要更多的交互,因此需要更多的模式。在本节中,我们将讨论为我们的应用程序所需的集成模式的实现。

FlixOne 书店(按照微服务架构风格开发)的完整应用可在 第十章 创建完整的微服务解决方案 中找到。

API 网关

协作风格 部分,我们讨论了我们可以用来促进微服务之间交互的两种风格。我们的应用程序被分割成各种微服务:

  • 产品服务

  • 订单服务

  • 发票服务

  • 客户服务

在我们的 FlixOne 书店(用户界面)中,我们需要显示以下详细信息:

  • 书名、作者姓名、价格、折扣等

  • 可用性

  • 书评

  • 书籍评分

  • 发布者排名等

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

API 网关不过是 Backend For FrontendBFF)的实现。Sam Newman 介绍了这个模式。它充当客户端应用程序和服务之间的代理。在我们的例子中,我们使用 Azure API 管理作为我们的 API 网关

它负责以下功能:

  • 接受 API 调用并将它们路由到您的后端

  • 验证 API 密钥、JWT 令牌和证书

  • 通过 Azure AD 和 OAuth 2.0 访问令牌支持身份验证

  • 强制执行使用配额和速率限制

  • 无需代码修改即可动态转换您的 API

  • 在设置的地方缓存后端响应

  • 为分析目的记录调用元数据

参考以下链接了解有关设置 API Azure 门户和与 REST API 一起工作的更多过程:Azure API 管理 (social.technet.microsoft.com/wiki/contents/articles/31923.azure-create-and-deploy-asp-net-webapi-to-azure-and-manage-using-azure-api-management.aspx)。

图片

在前面的图中,我们看到了不同的客户端,例如移动和桌面应用程序以及 Web 应用程序,它们正在使用微服务。在这里,Azure API 管理正在充当 API 网关。

我们的客户端不知道我们的服务实际位于哪个服务器。API 网关为他们提供了其自己服务器的地址,并且内部使用有效的 Ocp-Apim-Subscription-Key 对客户端的请求进行身份验证。

我们的 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 API 管理发送请求。以下是该功能的代码片段:

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 这是 API 管理为客户端选择的特定产品分配的密钥。
Resource 这是我们的 API 资源,它是在 Azure API 管理中配置的。它将不同于我们的实际 REST API 资源。
Response 这指的是对特定请求的响应,在我们的案例中是默认的 JSON 格式。

由于我们使用 Azure API 管理作为 API 网关,我们将享受到某些好处:

  • 我们可以从单个平台管理我们的各种 API,例如,ProductServiceOrderService 以及其他服务可以轻松地被许多客户端管理和调用

  • 由于我们使用 API 管理,它不仅为我们提供了一个代理服务器,还提供了创建和维护我们 API 文档的便利性

  • 它提供了一个内置的设施来定义各种配额、输出格式和格式转换的策略,例如 XML 到 JSON 或相反

因此,借助 API 网关,我们可以访问一些出色的功能。

事件驱动模式

微服务架构具有每个服务一个数据库的模式,这意味着每个依赖或独立的服务都有自己的独立数据库:

  • 依赖服务:我们的应用程序需要一些外部服务(第三方服务或组件等)和/或内部服务(这些是我们自己的服务)才能按预期工作或运行。例如,结账服务需要客户服务;此外,结账服务需要外部(第三方)服务来验证客户的身份(例如,在印度客户的情况下,需要 Aadhaar 卡 ID)。在这里,我们的结账服务是一个依赖服务,因为它需要两个服务(一个内部服务和外部服务)才能按预期工作。如果依赖的服务中的任何或所有服务工作不正常,依赖服务将无法工作(服务无法工作的原因有很多,包括网络故障、未处理的异常等)。

  • 独立服务:在我们的应用程序中,我们有一些服务不需要其他服务正常工作。不需要其他服务即可正常工作的服务被称为独立服务;这些服务可以自行托管。我们的客户服务不需要其他服务即可正常工作,但其他服务可能需要或不需要此服务。

主要挑战是维护业务事务以确保这些服务之间的数据一致性。例如,何时以及如何客户服务知道结账服务已经工作;现在它需要客户服务的功能。一个应用程序中可能有几个服务(服务可能是自行托管的)。在我们的案例中,当结账服务被触发而客户服务未被调用时,我们的应用程序将如何识别客户的详细信息?

ASP.NET WebHooks 也可以用于提供事件通知;有关更多信息,请参阅 WebHooks 文档。

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

MSDN 上的文档(msdn.microsoft.com/en-us/library/windows/desktop/ms681205(v=vs.85).aspx)说明了以下内容:

分布式事务是更新两个或更多网络化计算机系统上数据的交易。分布式事务将事务的好处扩展到必须更新分布式数据的应用程序。实现健壮的分布式应用程序很困难,因为这些应用程序容易受到多个故障的影响,包括客户端、服务器以及客户端和服务器之间的网络连接故障。在没有分布式事务的情况下,应用程序程序本身必须检测并从这些故障中恢复。

以下图描述了我们在应用程序中实际实现的事件驱动模式,其中 PRODUCT-SERVICE 订阅事件,Event-Manager 管理所有事件:

在事件驱动模式中,我们以这种方式实现服务,即每当服务更新其数据时,它都会发布一个事件,另一个服务(依赖服务)订阅此事件。现在,每当依赖服务接收到事件时,它会更新其数据。这样,我们的依赖服务可以在需要时获取和更新其数据。以下图概述了服务如何订阅和发布事件。在图中,Event-Manager 可以是运行在服务上的程序或帮助您管理所有订阅者和发布者事件的调解者。

它注册了 Publisher 的事件,并在特定事件发生/触发时通知 Subscriber。它还帮助您形成队列并等待事件。在我们的实现中,我们将使用 Azure Service Bus 队列来完成这项活动。

让我们考虑一个例子。在我们的应用程序中,我们的服务将这样发布和接收事件:

  • CUSTOMER-SERVICE 对用户执行一些检查,例如登录检查、客户详情检查等;在这些必要的检查完成后,服务会发布一个名为 CustomerVerified 的事件。

  • CHECKOUT-SERVICE 接收此事件,并在执行必要的操作后,发布一个名为 ReadyToCheckout 的事件。

  • ORDER-SERVICE 接收此事件并更新数量。

  • 一旦完成结账,CHECKOUT-SERVICE 就会发布一个事件。无论从外部服务接收到的结果是什么,无论是 CheckedoutSuccess 还是 CheckedoutFailed,它都会被 CHECKOUT-SERVICE 使用。

  • InventoryService 接收到这些事件时,它会更新数据以确保确切的项目被添加或删除。

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

事件溯源

此模式帮助我们确保服务将在状态更改时发布事件。在此模式中,我们将业务实体(产品、客户等)视为一系列状态更改事件。Event Store 持久化事件,这些事件可用于订阅或作为其他服务。此模式通过避免同步数据模型和业务域的要求来简化我们的任务。它提高了性能、可扩展性和响应速度。

  • 这只是简单地定义了一种方法,说明我们如何通过一系列事件来处理我们数据上的各种操作;这些事件记录在存储中。

  • 事件代表对数据所做的更改集合,例如,InvoiceCreated

以下图描述了事件在 ORDERSERVICE 上的工作方式:

  • 命令从用户界面发出,以订购书籍

  • ORDERSERVICE(从事件存储)查询并使用CreateOrder事件填充结果

  • 然后,命令处理器引发一个事件来订购书籍

  • 我们的服务执行相关操作

  • 最后,系统将事件附加到事件存储

最终一致性

最终一致性不过是数据一致性方法的一种实现。这表明实现,因此系统将是一个具有高可用性的可扩展系统。

MSDN 上的文档(msdn.microsoft.com/en-us/library/dn589800.aspx)说明了以下内容:

“最终一致性不太可能被明确指定为分布式系统的显式要求。相反,它通常是实现必须展示可扩展性和高可用性的系统的一个结果,这排除了提供强一致性的大多数常见策略。”

根据这些分布式数据存储,它们受到 CAP 定理的约束。CAP 定理也被称为布赖尔定理。一致性可用性(网络)分区容错CAP)。根据这个定理,在分布式系统中,我们只能从这三个中选择两个:

  • 一致性

  • 可用性

  • 分区容错

补偿性事务

补偿性事务提供了一种方法,可以回滚或撤销一系列步骤中执行的所有任务。假设一个或多个服务已经实施了一系列操作,其中之一或多个失败了。那么你的下一步是什么?你会撤销所有步骤还是提交一个半完成的特性?

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

竞争消费者

竞争消费者

它可以通过将消息系统传递给另一个服务(消费者服务)来实现,并且可以异步处理,如下所示:

该场景可以通过使用 Azure Service Bus 队列来实现。

Azure Service Bus

在事件驱动模式中,我们讨论了服务发布和订阅事件。我们使用事件管理器来管理所有事件。在本节中,我们将看到 Azure Service Bus 如何管理事件并提供与微服务一起工作的便利。

Azure 服务总线是一个信息传递服务。它用于使两个或更多组件/服务之间的通信更加容易。在我们的案例中,每当服务需要交换信息时,它们将通过此服务进行通信。Azure 服务总线在这里发挥着重要作用。Azure 服务总线提供两种主要的服务类型:

  • 中介通信:此服务也可以称为雇佣服务。它的工作方式类似于现实世界中的邮政服务。每当一个人想要发送消息/信息时,他/她可以向另一个人发送一封信。这样,人们可以通过信件、包裹、礼物等形式发送各种类型的消息。这种消息服务确保即使在发送者和接收者不在同一时间在线的情况下,消息也能被传递。这是一个具有队列、主题、订阅等组件的消息平台。

  • 非中介通信:这类似于打电话。在这种情况下,呼叫者(发送者)给一个人(接收者)打电话,而不需要任何确认来表明他/她是否会接听电话。这样,发送者发送信息,而接收者是否接收通信并将信息传回发送者完全取决于接收者。

看一下以下图表:

Microsoft Azure 的文档(docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-fundamentals-hybrid-solutions)说明:

“服务总线是一个多租户云服务,这意味着服务被多个用户共享。每个用户,例如应用程序开发者,都会创建一个命名空间,然后在那个命名空间内定义她需要的通信机制。”

上述图表是 Azure 服务总线的一个图形视图,它描绘了四种不同的通信机制。每个人在连接应用程序方面都有自己的喜好:

  • 队列:这些允许单向通信,并充当经纪人。

  • 主题:这些提供单向通信,一个主题可以有多个订阅。

  • 中继:这些提供双向通信。它们不存储消息(如队列和主题所做的那样)。中继将消息传递到目标应用程序。

Azure 队列

Azure 队列实际上就是使用 Azure 表的云存储账户。它们提供了一种在应用程序之间排队消息的方法。在接下来的章节中,我们将实现消息队列,这是 Azure 服务总线的一部分。

实现 Azure 服务总线队列

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

  • 一个服务总线命名空间

  • 一个服务总线消息队列

  • 一个用于发送消息的控制台应用程序

  • 一个用于接收消息的控制台应用程序

前提条件

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

  • Visual Studio 2017 更新 3 或更高版本

  • 一个有效的 Azure 订阅

如果您没有 Azure 订阅,可以通过在此处登录免费获取:azure.microsoft.com/en-us/free/

如果您拥有上述所有内容,您可以从以下步骤开始:

  1. 登录到 Azure 门户(portal.azure.com/)。

  2. 在左侧导航栏中,点击“服务总线”。如果不可用,可以通过点击“更多服务”来找到它。

  3. 点击“添加”:

图片

  1. 在“创建命名空间”对话框中,输入一个命名空间,例如,flixone。选择下一个定价层:基本标准高级

  2. 选择您的订阅。

  3. 选择一个现有资源或创建一个新的资源。

  4. 选择您想要托管命名空间的位置。

  5. 打开一个新创建的命名空间(我们刚刚创建了flixone)。

  6. 现在点击“共享访问策略”。

  7. 点击“根管理共享访问密钥”。请参考以下截图:

图片

  1. flixone命名空间的“主对话框”中点击“队列”。

  2. 从“根管理共享访问密钥”窗口中,注意主密钥连接字符串以供进一步使用。请参考以下截图:

图片

  1. 点击“名称”以添加一个队列(例如,flixonequeue),然后点击“创建”(我们使用默认的 REST 值)。请参考以下截图:

图片

上一张图片是创建队列对话框。在创建队列对话框中,我们可以创建一个队列,例如,在上面的图片中,我们正在创建一个名为 floxxonequeue 的队列。可以通过访问队列对话框来验证队列。

现在我们已准备好创建我们的消息发送者和接收者应用程序。

向队列发送消息

在本节中,我们将创建一个控制台应用程序,该程序实际上会向队列发送消息。要创建此应用程序,请按照以下步骤操作:

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

图片

  1. 通过在项目上右键单击添加 NuGet 包 Microsoft Azure Service Bus。

  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();
 }

在前面的代码中,我们通过提供ConnectionStringQueueName来创建我们的队列客户端,这些我们在 Azure 门户中已经设置。它调用接受包含需要发送的消息数量的参数的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 的概述,我们可以看到活动消息计数等。

图片

添加配置设置

在上一个示例中,我们为 ConnectionStringQueueName 两个都使用了常量值。如果我们需要更改这些设置,我们必须修改代码。但为什么我们要为了这个小小的更改而修改代码呢?为了克服这种情况,我们有配置设置。您可以在 docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration 中了解更多关于配置的信息。在本节中,我们将使用 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 Service Bus 的 NuGet 包(如前一个应用程序中添加的)。

  3. 编写从 Azure Bus 服务队列接收消息的代码,因此您的 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. 控制台窗口将显示消息及其 ID。现在,前往 Azure 门户并验证消息。它应该是零。参考以下截图:

图片

上述示例演示了如何使用 Azure Bus 服务为我们微服务发送/接收消息。

摘要

服务的间通信可以通过同步或异步通信实现,这些是协作的风格。微服务应该拥有异步 API。API 网关是一个代理服务器,它提供了一种允许各种客户端与 API 交互的方式。API 管理,作为一个 API 网关,提供了许多功能来管理/托管各种 RESTful API。存在各种模式帮助我们与微服务进行通信。通过使用 Azure 总线服务,我们可以轻松地管理和玩转服务间的通信,使用 Azure 总线服务的消息队列;服务可以通过这种方式轻松地相互发送或接收消息。最终一致性讨论的是具有高可扩展性的可伸缩系统,并且这一点通过 CAP 定理得到了证明。

在下一章中,我们将讨论各种测试策略来测试应用程序,并基于微服务架构风格进行构建。

第四章:测试微服务

质量保证或测试是评估系统、程序或应用程序不同方面的一种很好的方式。有时,系统需要测试来识别错误代码,在其他情况下,我们可能需要它来评估我们系统的业务合规性。测试可能因系统而异,并且可能根据应用程序的架构风格而有很大差异。一切取决于我们如何制定我们的测试策略或计划。例如,测试单体.NET 应用程序与测试 SOA 或微服务不同。在本章中,我们将涵盖以下主题:

  • 如何测试微服务

  • 应对挑战

  • 测试策略

  • 测试金字塔

  • 微服务测试类型

如何测试微服务

测试微服务可能是一项具有挑战性的工作,因为它与传统架构风格构建的应用程序测试方式不同。测试.NET 单体应用程序比测试提供实现独立性和短交付周期的微服务要容易一些。

让我们在我们的.NET 单体应用程序的背景下理解这一点,在那里我们没有利用持续集成和部署。当测试与持续集成和部署结合时,情况变得更加复杂。对于微服务,我们需要了解每个服务的测试以及这些测试如何彼此不同。此外,请注意,自动化测试并不意味着我们完全不会进行任何手动测试。

这里有一些使微服务测试变得复杂和具有挑战性的任务:

  • 微服务可能包含多个服务,这些服务可以一起或单独为一个企业系统工作,因此它们可能很复杂。

  • 微服务旨在针对多个客户端;因此,它们涉及更复杂的使用案例。

  • 微服务架构风格中的每个组件/服务都是隔离和独立的,因此测试它们会稍微复杂一些,因为它们需要分别和作为一个完整的系统进行测试。

  • 可能会有独立的小组在单独的组件/服务上工作,这些组件/服务可能需要相互交互。因此,测试不仅应该涵盖内部服务,还应该涵盖外部服务。这使得测试微服务的任务更加具有挑战性和复杂性。

  • 微服务中的每个组件/服务都旨在独立工作,但它们可能需要访问公共/共享数据,其中每个服务负责修改自己的数据库。因此,测试微服务将变得更加复杂,因为服务需要通过 API 调用访问其他服务的数据,这进一步增加了对其他服务的依赖。这种类型的测试将需要使用模拟测试来处理。

应对挑战

在上一节中,我们讨论了测试微服务是一项复杂和具有挑战性的工作。在本节中,我们将讨论一些要点,说明进行各种测试如何帮助我们克服这些挑战:

  • 单元测试框架,如 Microsoft 单元测试框架,提供了一种测试独立组件单个操作的功能。为了确保所有测试都通过,并且新的功能或更改不会破坏任何东西(如果任何功能出现故障,相关的单元测试将失败),这些测试可以在每次代码编译时运行。

  • 为了确保响应与客户端或消费者的期望一致,可以使用消费者驱动的合同测试。

  • 服务使用来自外部方或其他服务的数据,并且可以通过设置负责处理数据的服务的端点来对这些服务进行测试。然后我们可以使用模拟框架或库,如moq,在集成过程中模拟这些端点。

测试策略(测试方法)

如第一章“微服务简介”的“先决条件”部分所述,部署和 QA 要求可能会变得更加严格。有效处理这种情况的唯一方法是通过预先规划。我一直倾向于在早期需求收集和设计阶段就包括 QA 团队。在微服务的情况下,架构组和 QA 组之间的紧密合作变得必要。不仅 QA 团队的输入会有所帮助,他们还能够制定出一种有效测试微服务的策略。

测试策略仅仅是描述测试完整方法的地图或概述计划。

不同的系统需要不同的测试方法。对于使用较新方法而非早期开发系统开发的系统,无法实施纯测试方法。测试策略应该对每个人都很清晰,以便创建的测试可以帮助团队的非技术成员(如利益相关者)了解系统是如何工作的。这些测试可以是自动化的,简单地测试业务流程,或者它们可以是手动测试,由在用户验收测试系统上工作的用户简单地执行。

测试策略或方法具有以下技术:

  • 积极的:这是一种早期方法,试图在从初始测试设计创建构建之前修复缺陷

  • 反应的:在这种方法中,一旦编码完成,就开始测试

测试金字塔

测试金字塔是一种策略或方法,用于定义在微服务中应该测试的内容。换句话说,我们可以这样说,它帮助我们定义微服务的测试范围。测试金字塔的概念是由迈克·科恩(Mike Cohn)在 2009 年提出的。www.mountaingoatsoftware.com/blog/the-forgotten-layer-of-the-test-automation-pyramid。测试金字塔有多种变体;不同的作者通过指出他们如何放置或优先考虑他们的测试范围来描述这一点。以下图像展示了迈克·科恩定义的相同概念:

图片

测试金字塔展示了如何构建一个精心设计的测试策略。当我们仔细观察时,我们可以很容易地看到我们应该如何遵循微服务的测试方法(请注意,测试金字塔并不特定于微服务)。让我们从这个金字塔的底部开始。我们可以看到测试范围仅限于使用单元测试。一旦我们移动到顶部,我们的测试范围就扩展到一个更广泛的范围,我们可以在这里执行完整的系统测试。

让我们详细讨论这些层(从下到上的方法):

  • 单元测试:这些测试基于微服务架构风格测试应用程序的小功能。

  • 服务测试:这些测试测试一个独立的服务或与另一个/外部服务通信的服务。

  • 系统测试:这些测试有助于测试整个系统,并涉及用户界面的一些方面。这些是端到端测试。

在这个概念中有一个有趣的观点,那就是顶层测试,即系统测试,编写和维护速度慢且成本高。另一方面,底层测试,即单元测试,相对较快且成本较低。

在接下来的章节中,我们将详细讨论这些测试。

微服务测试类型

在上一节中,我们讨论了测试方法或测试策略。这些策略决定了我们将如何进行系统测试。在本节中,我们将讨论各种类型的微服务测试。

单元测试

单元测试通常是测试单个函数调用,以确保程序的最小部分被测试。因此,这些测试旨在验证特定的功能,而不考虑其他组件:

  • 当组件被分解成小而独立的部件,这些部件应该独立测试时,测试将变得更加复杂。在这里,测试策略变得很有用,并确保系统将执行最佳的质量保证。当它与测试驱动开发TDD)方法结合使用时,它增加了更多的力量。我们将通过一个例子来讨论这一点,这个例子是单元测试,它是测试实践的一个子部分。

你可以通过 Katas 在github.com/garora/TDD-Katas学习并练习 TDD。

  • 单元测试可以是任何大小;单元测试没有固定的大小。通常,这些测试是在类级别编写的。

  • 较小的单元测试适合测试复杂系统的每个可能的功能。

组件(服务)测试

组件或服务测试是一种方法,我们绕过 UI 并直接测试 API(在我们的案例中,是 ASP.NET Core Web API)。使用这种测试,我们确认单个服务没有代码错误,并且功能上运行良好。

测试一个服务并不意味着它是独立的服务。这个服务可能正在与外部服务交互。在这种情况下,我们不应调用实际的服务,而应使用模拟和存根方法。这样做的原因是我们的座右铭:测试代码并确保它是无错误的。在我们的案例中,我们将使用moq框架来模拟我们的服务。

对于组件或服务测试,有几个值得注意的事项:

  • 由于我们需要验证服务的功能,这类测试应该是小而快的。

  • 通过模拟的帮助,我们不需要处理实际的数据库;因此,测试执行时间较短或略高。

  • 这些测试的范围比单元测试更广

集成测试

在单元测试中,我们测试单个代码单元。在组件或服务测试中,我们根据外部或第三方组件测试模拟服务。但在微服务中的集成测试可能有点挑战性,因为在这种测试中,我们测试的是协同工作的组件。这里应该调用与外部服务集成的服务调用。在这个测试策略中,我们确保系统正确协同工作,并且服务的表现符合预期。在我们的案例中,我们有各种微服务,其中一些依赖于外部服务。

例如,StockService 依赖于 OrderService,方式是在客户成功订购特定商品后,立即从库存中减少特定数量的商品。在这种情况下,当我们测试 StockService 时,我们应该模拟 OrderService。我们的座右铭应该是测试 StockService 而不是与 OrderService 通信。我们不直接测试任何服务的数据库。

合同测试

合同测试是一种方法,其中每个服务调用独立验证响应。如果任何服务有依赖,则依赖项被存根化。这样,服务在没有与其他服务交互的情况下运行。这是一种集成测试,允许我们检查外部服务的合同。在这里,我们来到了一个称为消费者驱动合同的概念(我们将在下一节中详细讨论)。

例如,CustomerService 允许新客户在 FlixOne Store 注册。我们不会在我们的数据库中存储新客户的数据。我们在这一步之前验证客户数据,以检查是否有黑名单或欺诈用户列表等情况。如果有人更改这个外部服务的契约,我们的测试仍然会通过,因为这种更改不会影响我们的测试,因为我们已经模拟了该外部服务的契约。

面向消费者的契约

在微服务中,我们有几个独立的服务或需要相互通信的服务。除此之外,从用户的角度来看(在这里,用户是一个开发者,他正在消费所提到的 API),他们了解服务以及它是否有或没有几个客户端/消费者/用户。这些客户端可能有相同或不同的需求。

面向消费者的契约指的是一种模式,它指定并验证客户端/消费者与 API 所有者(应用程序)之间所有的交互。因此,在这里,面向消费者意味着客户端/消费者指定它希望与定义的格式进行何种交互。另一方面,API 所有者(应用程序服务)必须同意这些契约并确保它们不会违反这些契约:

图片

这些是契约:

  • 提供者契约:这仅仅是 API 所有者(应用程序)提供的服务的一个完整描述。Swagger 的文档可以用于我们的 REST API(Web API)。

  • 消费者契约:这是消费者/客户端将如何利用提供者契约的描述。

  • 面向消费者的契约:这是 API 所有者满足消费者/客户端契约的描述。

如何实现面向消费者的测试

在微服务的情况下,实现一个面向消费者的测试比实现一个.NET 单体应用要更具挑战性。这是因为,在单体应用中,我们可以直接使用任何单元测试框架,例如 MS 测试或 NUnit,但在微服务架构中我们无法直接这样做。在微服务中,我们需要模拟不仅方法调用,还包括通过 HTTP 或 HTTPS 调用的服务本身。

要实现面向消费者的测试,有一些工具可以帮助我们。一个著名的.NET 框架的开源工具是 PactNet (github.com/SEEK-Jobs/pact-net),另一个.NET Core 的工具是 Pact.Net Core (github.com/garora/pact-net-core)。这些是基于 Pact (docs.pact.io/) 标准的。我们将在本章末尾看到面向消费者的契约测试的实际应用。

Pact-net-core 如何帮助我们实现目标

在消费者驱动的测试中,我们的目标是确保我们能够测试所有服务、内部组件以及依赖于或与其它/外部服务通信的服务。

Pact-net-core 是以一种保证合约能够得到满足的方式编写的。以下是一些关于它是如何帮助我们实现目标的要点:

  • 执行速度非常快

  • 它有助于识别故障原因

  • 最重要的是,Pact 不需要单独的环境来管理自动化测试集成

使用 Pact 有两个步骤:

  • 定义预期:在第一步中,消费者团队必须定义合约。在前面的图像中,Pact 帮助记录消费者合约,该合约将在回放时进行验证:

图片

  • 验证预期:作为下一步的一部分,合约被提供给提供者团队,然后提供者服务被实现以满足相同的需求。在以下图像中,我们展示了在提供者端回放合约以履行定义的合约:

图片

我们已经经历了消费者驱动的合约;它们通过开源工具 Pact-net 的帮助,缓解了微服务架构的挑战。

性能测试

这是一种非功能性测试,其主要宗旨不是验证代码或测试代码的健康状况。这是为了确保系统根据各种度量标准,如可伸缩性、可靠性等,表现良好。

以下是一些不同的性能测试技术或类型:

  • 压力测试:这是一个测试系统在特定负载的各种情况下的行为的过程。它还包括关键交易、数据库负载、应用服务器等。

  • 压力测试:这是一种方法,系统在回归测试下运行,并找到系统的上限容量。它还确定当当前负载超过预期最大负载时,系统的行为。

  • 沉浸测试:这也被称为耐久测试。在这个测试中,主要目的是监控内存利用率、内存泄漏或影响系统性能的各种因素。

  • 峰值测试:这是一种确保系统能够承受工作负载的方法。确定性能的最佳任务之一是通过突然增加用户负载。

端到端(UI/功能)测试

端到端、UI 或功能测试是针对整个系统进行的测试,包括整个服务和数据库。这些测试增加了测试范围。这是测试的最高级别,包括前端集成,并测试系统作为最终用户会使用的方式。这种测试类似于最终用户在系统上的操作方式。

社交型与隔离型单元测试

社交单元测试是包含具体协作者和跨越边界的测试。它们不是孤立的测试。孤立的测试是确保类的方法被测试的测试。社交测试并不新鲜。这个术语由马丁·福勒详细解释为单元测试(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 2017 更新 3 或更高版本

  • .NET Core 2.0

  • C# 7.0

  • ASP.NET Core 2.0

  • xUnit 和 MS 测试

  • moq 框架

准备测试项目

我们将测试我们的微服务应用程序:FlixOne 书店。通过代码示例的帮助,我们将了解如何执行单元测试、存根化和模拟。

我们在第二章,实现微服务中创建了 FlixOne 书店应用程序。

在我们开始编写测试之前,我们应该在我们的现有应用程序中设置一个测试项目。我们可以通过以下简单步骤进行测试项目设置:

  1. 在使用 Visual Studio 的解决方案资源管理器中,右键单击解决方案,然后单击新建项目—参考以下截图:

  1. 从“添加新项目”模板中选择.NET Core 和 xUnit 测试项目(.NET Core),并提供一个有意义的名称,例如FlixOne.BookStore.ProductService.UnitTests

图片

  1. 通过在解决方案资源管理器中右键单击项目名称,转到项目属性。从属性页面打开“构建”选项卡,然后单击“高级”并选择 C# 7.0 作为语言版本:

图片

我们的项目结构应该看起来像这样:

图片

单元测试

ProductService中,让我们通过测试确保我们的服务能够无故障地返回产品数据。在这里,我们将使用假对象来完成,按照以下步骤操作:

  1. FlixOne.BookStore.ProductService.UnitTests项目下添加一个新的文件夹,并将其命名为Fake

  2. Fake文件夹下添加ProductData.cs类,并添加以下代码:

public class ProductData
{
  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;
  }
  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. FlixOne.BookStore.ProductService.UnitTests项目下添加Services文件夹。

  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());
    }
}

在前面的代码示例中,这是一个单元测试示例,我们正在模拟我们的存储库并测试我们的 WebAPI 控制器的输出。这个测试基于AAA技术;如果在设置期间遇到模拟数据,则测试将通过。

集成测试

ProductService中,让我们确保我们的服务能够无故障地返回产品数据。在继续之前,我们必须添加一个新的项目和随后的测试类,按照以下步骤操作:

  1. 右键单击解决方案,然后添加项目。

  2. 从“添加新项目”窗口中选择 XUnit 测试项目(.NET Core)并提供一个有意义的名称,例如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方法设置为 void。这样我们就可以在我们的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?}");
    });
    // Enable middleware to serve generated Swagger 
    as a JSON endpoint.app.UseSwagger();
    // Enable middleware to serve swagger-ui (HTML, JS, 
    CSS, etc.), specifying the Swagger JSON endpoint.
    app.UseSwaggerUI(c =>
    {
      c.SwaggerEndpoint("/swagger/v1/swagger.json", 
      "Product API V1");
    });
}
  1. 添加一个新的文件夹,命名为Services

  2. 添加TestStartup.cs类。

  3. 打开 NuGet 管理器。搜索并添加Microsoft.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);
  }
}

在前面的代码示例中,我们正在检查一个简单的测试。我们通过使用 HttpClient 设置客户端来验证服务的响应。如果响应为空,测试将失败。

消费者驱动的合同测试

在上一节,合同测试中,我们详细讨论了相关内容。在本节中,我们将了解如何借助 pact-net-core 实现消费者驱动的合同测试。

我们将使用现有的 FlixOne.BookStore.ProductService 项目,它包含我们所有的 API。我们的 FlixOne.BookStore.ProductService 项目包含提供者测试,允许你创建提供者场景,以及我们的客户端项目,它实际消费服务,发起调用并测试合同。

要开始,你应该安装 NuGet 包。使用包控制台执行 Install-Package PactNet.Windows

根据 Pact 规范(已在之前的章节 合同测试 中讨论过),客户端将创建一个名为 消费者合同 的合同(一个 .json 文件)。我们编写了以下代码来生成我们的合同:

public class ConsumerProductApi : IDisposable
{
  public ConsumerProductApi()
  {
    PactBuilder = new PactBuilder(new PactConfig
    {
      SpecificationVersion = Constant.SpecificationVersion,
      LogDir = Helper.SpecifyDirectory(Constant.LogDir),
      PactDir = Helper.SpecifyDirectory(Constant.PactDir)
    })
    .ServiceConsumer(Constant.ConsumerName)
    .HasPactWith(Constant.ProviderName);
    MockProviderService = PactBuilder.MockService
    (Constant.Port, Constant.EnableSsl);
  }
  public IPactBuilder PactBuilder { get; }
  public IMockProviderService MockProviderService { get; }
  public string ServiceBaseUri => $"http://localhost:{Constant.Port}";
  public void Dispose()
  {
    PactBuilder.Build();
  }
}

在前面的代码中,我们正在构建一个合同。除此之外,我们还模拟了客户端测试。请看以下代码片段:

[Fact]
public void WhenApiIsUp_ReturnsTrue()
{
  //Arrange
  _mockProviderService.UponReceiving("a request to
  check the api status")
  .With(new ProviderServiceRequest
  {
    Method = HttpVerb.Get,
    Headers = new Dictionary<string, object> { { "Accept",
    "application/json" } },
    Path = "/echo/status"
  })
  .WillRespondWith(new ProviderServiceResponse
  {
    Status = 200,
    Headers = new Dictionary<string, object> { {
    "Content-Type", "application/json; charset=utf-8" } },
    Body = new
    {
      up = true,
      upSince = DateTime.UtcNow,
      version = "2.0.0",
      message = "I'm up and running from last 19 hours."
    }
  });
  var consumer = new ProductApiClient(_serviceBaseUri);
  //Act
  var result = consumer.ApiStatus().Up;
  //Assert
  Assert.True(result);
  _mockProviderService.VerifyInteractions();
}

我们的代码将创建如下所示的消费者合同:

{
  "consumer": 
  {
    "name": "Product API Consumer"
  },
  "provider": 
  {
    "name": "Product API"
  },
  "interactions": 
  [
    {
      "description": "a request to check the api status",
      "request": 
      {
        "method": "get",
        "path": "/echo/status",
        "headers": 
        {
          "Accept": "application/json"
        }
      },
      "response": 
      {
        "status": 200,
        "headers": 
        {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": 
        {
          "up": true,
          "upSince": "2017-11-06T00:52:01.3164539Z",
          "version": "2.0.0",
          "message": "I'm up and running from last 19 hours."
        } 
      }
    }
  ],
  "metadata": 
  {
    "pactSpecification": 
    {
      "version": "2.0.0"
    }
  }
}

一旦创建了消费者驱动的合同,它应该遵守提供者,因此我们需要相应地编写 API(我们正在使用现有的产品 API)。以下是一个提供者的代码片段:

//Arrange
const string serviceUri = "http://localhost:13607";
var config = new PactVerifierConfig
{
  Outputters = new List<IOutput>
  {
    new CustomOutput(_output)
  }
};
//code omitted

我们创建了一个 Web API 和一个测试来验证消费者驱动的合同,并最终从客户端的角度进行测试。

摘要

测试微服务与基于传统架构风格构建的应用程序略有不同。在 .NET 单体应用程序中,与微服务相比,测试要容易一些,并且它提供了实现独立性和短交付周期。微服务在测试过程中面临挑战。借助测试金字塔概念,我们可以制定我们的测试流程。根据测试金字塔,我们可以轻松地看到单元测试提供了测试类中一个小函数的便利,并且耗时较少。另一方面,测试金字塔的顶层涉及一个较大的范围,包括系统或端到端测试,这些测试耗时且成本高昂。消费者驱动的合同是测试微服务的一种非常有用的方式。Pact-net 是为此目的而设计的开源工具。最后,我们了解了实际的测试实现。

在下一章中,我们将了解如何部署微服务应用程序。我们将详细讨论持续集成和持续部署。

第五章:微服务部署

单体和微服务架构风格都伴随着不同的部署挑战。在.NET 单体应用的情况下,部署通常是一种 Xcopy 部署的变体。微服务部署则带来了一组不同的挑战。持续集成和持续部署是交付微服务应用时的关键实践。此外,承诺提供更大隔离边界的容器技术和工具链技术对于微服务部署和扩展至关重要。

在本章中,我们将讨论微服务部署的基本原理以及 CI/CD 工具和容器等新兴实践对微服务部署的影响。我们还将演示如何在 Docker 容器中部署一个简单的.NET Core 服务。

到本章结束时,你将了解以下主题:

  • 部署术语

  • 成功微服务部署的因素有哪些?

  • 什么是持续集成和持续部署?

  • 微服务部署的隔离要求

  • 容器化技术及其在微服务部署中的需求

  • Docker 快速入门

  • 如何使用 Visual Studio 将应用程序打包为 Docker 容器

在进一步讨论之前,我们首先应该了解为什么我们要讨论微服务的部署。部署周期是一个具有特定流程的过程。

单体应用部署的挑战

单体应用是所有数据库和业务逻辑都绑定在一起并打包为一个单一系统的应用。由于通常单体应用以单个包的形式部署,因此部署相对简单,但以下原因使得部署变得痛苦:

  • 部署和发布作为一个单一概念:部署构建工件和实际上向最终用户提供功能之间没有区别。更常见的是,发布与其环境相关联。这增加了部署新功能的风险。

  • 全有或全无部署:全有或全无部署增加了应用停机时间和失败的风险。在回滚的情况下,团队未能交付预期的新的功能或热修复,或者必须发布服务包以提供正确类型的功能。

热修复,也称为快速修复,是一个单一或累积的包(通常称为补丁)。它包含对生产中发现的问题/错误进行的修复,这些错误必须在下一个主要版本发布之前得到解决。

  • 中央数据库作为单一故障点:在单体应用中,一个庞大且集中的数据库是单一故障点。这个数据库通常相当大且难以分解。这导致平均恢复时间MTTR)和平均故障间隔时间MTBF)的增加。

  • 部署和发布是重大事件:由于应用程序中的微小变化,整个应用程序可能会被部署。这对开发者和运维团队来说需要巨大的时间和精力投入。不用说,涉及到的各个团队之间的协作是成功发布的关键。当许多分布在全球的团队在进行开发和发布工作时,这变得更加困难。这类部署/发布需要大量的指导和手动步骤。这影响了最终用户,他们必须面对应用程序的停机。如果你熟悉这类部署,那么你也会熟悉所谓的“战室”中的马拉松会议和会议桥上的缺陷分类无休止的会议。

  • 市场投放时间:在这种情况下,对系统进行任何更改都变得更加困难。在这种环境中,执行任何业务变更都需要时间。这使得对市场力量的反应变得困难——企业也可能失去其市场份额。使用微服务架构,我们正在解决这些挑战之一。这种架构为服务部署提供了更大的灵活性和隔离性。它已被证明可以提供更快的周转时间和急需的业务敏捷性。

理解部署术语

微服务部署术语仅包括从代码更改开始直到发布的步骤。在本节中,我们将按以下方式讨论所有这些部署术语的步骤:

  • 构建:在构建阶段,服务源代码在没有错误的情况下编译,并且所有相应的单元测试都通过。这一阶段产生构建工件。

  • 持续集成CI):每次开发者提交任何更改时,CI 都会强制整个应用程序重新构建——应用程序代码被编译,并对其运行一系列全面的自动化测试。这种做法源于大型团队中频繁集成代码的问题。基本思想是保持软件更改的增量或更改尽可能小。这提供了软件处于可工作状态的信心。即使开发者的提交破坏了系统,也可以通过这种方式轻松修复。

  • 部署:硬件配置和安装基础操作系统以及正确的.NET 框架版本是部署的先决条件。接下来是将其构建工件在生产环境中通过各个阶段进行推广。这两部分的组合被称为部署阶段。在大多数单体应用程序中,部署阶段和发布阶段之间没有区别。

  • 持续部署CD):在持续部署中,每个成功的构建都会被部署到生产环境中。从技术团队的角度来看,CD 更为重要。在持续部署的框架下,还有其他一些实践,例如自动单元测试、标记、构建号的版本控制和变更的可追溯性。通过持续交付,技术团队确保通过各个低级环境推送到生产环境中的变更能够按预期工作。通常,这些变更很小,部署速度很快。

  • 持续交付:持续交付与 CD 不同。CD 来自技术团队的角度,而持续交付更侧重于尽可能早地将部署的代码提供给客户。为了确保客户获得无缺陷的正确产品,在持续交付中,每个构建都必须通过所有的质量保证检查。一旦产品通过满意的质量验证,何时发布就是业务利益相关者的决定。

  • 构建和部署管道:构建和部署管道是自动化实现持续交付的一部分。它是一系列步骤的流程,代码通过源代码库提交。在部署管道的另一端,生成发布所需的工件。构建和部署管道可能包含的步骤如下:

    1. 单元测试

    2. 集成测试

    3. 代码覆盖率与静态分析

    4. 回归测试

    5. 部署到预发布环境

    6. 压力/负载测试

    7. 部署到发布仓库

  • 发布:提供给最终用户的企业功能被称为功能的发布。为了发布功能或服务,应事先部署相关的构建工件。通常,功能开关管理功能的发布。如果功能标志(也称为功能切换)在生产环境中未开启,则称为指定功能的暗色发布。

成功部署微服务的先决条件

任何架构风格都伴随着一系列要遵循的关联模式和惯例。微服务架构风格也不例外。采用以下实践,微服务实现成功的可能性更大:

  • 自给自足的团队:作为 SOA 和微服务架构的先驱,亚马逊遵循两披萨团队模式。这意味着一个微服务团队通常不会超过 7-10 名成员。这些团队成员将拥有所有必要的技能和角色;例如,开发、运维和业务分析师。这样的服务团队负责微服务的开发、运维和管理。

  • 持续集成和持续部署:CI 和 CD 是实现微服务的先决条件。能够频繁集成工作的较小、自给自足的团队是微服务成功的前提。这种架构并不像单体架构那样简单。然而,自动化和定期推送代码升级的能力使团队能够处理复杂性。Team Foundation Online Services(TFS)、TeamCity 和 Jenkins 等工具在这个领域非常受欢迎。

  • 基础设施即代码:将硬件和基础设施组件,如网络,用代码表示的想法是新的。这有助于使部署环境,如集成、测试和生产,看起来完全相同。这意味着开发人员和测试工程师将能够在较低的环境中轻松地重现生产缺陷。使用 CFEngine、Chef、Puppet、Ansible 和 PowerShell DSC 等工具,您可以编写整个基础设施的代码。随着这种范式转变,您还可以将基础设施置于版本控制系统之下,并以部署工件的形式进行分发。

  • 云计算的应用:云计算是采用微服务的强大催化剂。尽管如此,它对于微服务部署并不是强制性的。云计算具有近乎无限的规模、弹性和快速供应能力。云是微服务的天然盟友。因此,对 Azure 云的知识和经验将帮助您采用微服务。

微服务部署的隔离要求

2012 年,Heroku 平台联合创始人亚当·威金斯提出了 12 项基本原则。这些原则讨论了从想法到部署定义新的现代 Web 应用程序。这套原则现在被称为12 因子应用。这些原则为新架构风格铺平了道路,这些风格演变成了微服务架构。12 因子应用的原则之一如下:

“将应用程序作为一个或多个无状态进程执行”

因此,服务将基本上是无状态的(除了数据库,它充当状态存储)。无共享原则也应用于整个模式和惯例的范围内。这不过是组件隔离以实现规模和敏捷性的手段。

在微服务世界中,这一隔离原则被应用于以下方式:

  • 服务团队:将围绕服务建立自给自足的团队。实际上,这些团队将能够做出所有必要的决策,以开发和支持他们负责的微服务。

  • 源代码隔离:每个微服务的源代码库将是独立的。它不会共享任何源代码、文件等。在微服务世界中,跨服务复制少量代码是可以接受的。

  • 构建阶段隔离:每个微服务的构建和部署管道应该保持隔离。构建和部署管道甚至可以并行运行,独立部署服务。因此,CI-CD 工具应该扩展以支持不同服务和管道,速度要快得多。

  • 发布阶段隔离:每个微服务都应该与其他服务隔离发布。也可能存在同一服务不同版本在生产环境中的情况。

  • 部署阶段隔离:这是隔离最重要的部分。传统的单体部署使用裸金属服务器。随着虚拟化的进步,虚拟服务器已经取代了裸金属服务器。

通常,单体应用程序的标准发布流程如下:

图片

考虑到这些隔离级别,微服务的构建和部署管道可能看起来是这样的:

图片

需要新的部署范式

通过添加新的物理机或裸金属服务器,可以实现应用程序的最高隔离级别,因此有一个拥有自己操作系统的服务器来管理所有系统资源。这在传统应用程序中是常规操作,但对于现代应用程序来说并不实用。现代应用程序是庞大的系统。这些系统的例子包括亚马逊、Netflix 和耐克,甚至是传统的金融机构,如 ING。这些系统托管在成千上万的服务器上。这类现代应用程序需要超强的可扩展性来服务数百万用户。对于微服务架构,仅仅为了在上面运行一个小服务而设置新的服务器是没有意义的。

随着新的 CPU 架构突破,出现的一个选项是虚拟机。虚拟机通过虚拟化技术抽象出操作系统的所有硬件交互。虚拟化技术使我们能够在单个物理机上运行多个机器或服务器。需要注意的是,所有虚拟机都从物理主机资源中获取其独立系统资源的一部分。

这仍然是一个良好的隔离环境来运行应用程序。虚拟化带来了为整个应用程序提升服务器的合理性。在这样做的同时,它保持了组件的相对隔离;这有助于我们利用数据中心中的备用计算机资源。它提高了我们数据中心的效率,同时满足了应用程序的合理隔离需求。

然而,仅仅虚拟化本身并不能满足微服务的一些需求。根据 12 要素原则,亚当也谈到了这一点:

“十二要素应用程序的过程是可丢弃的,这意味着它们可以随时启动或停止。这促进了快速弹性扩展、代码或配置更改的快速部署,以及生产部署的健壮性。”

这个原则对于微服务架构风格非常重要。因此,在使用微服务时,我们必须确保服务能够更快地启动。在这种情况下,让我们假设每个虚拟机运行一个服务。如果我们想启动这个服务,首先需要启动虚拟机;然而,虚拟机的启动时间很长。还有一点是,对于这类应用程序,我们谈论的是大量的集群部署。因此,服务肯定会在集群中分布。

这也意味着虚拟机可能需要在集群中的某个节点上启动。这又是虚拟机启动时间的问题。这并没有带来我们期望的微服务效率。

现在,唯一剩下的选择是使用操作系统进程模型,这具有更快的启动时间。进程编程模型早已为人所知,但即使是进程也有成本。它们没有很好地隔离,并且共享系统资源以及操作系统的内核。

对于微服务,我们需要更好的隔离部署模型和新的部署范式。答案是:容器技术的创新。一个值得考虑的因素是,容器技术很好地位于虚拟化和操作系统进程模型之间。

容器

容器技术在 Linux 世界中并不新鲜。容器基于 Linux 的 LXC 技术。在本节中,让我们看看容器在微服务场景中的重要性。

容器是什么?

容器是一个包含完整文件系统的软件组件。它包含运行代码、运行时、系统工具和系统库所需的一切——任何可以安装在服务器上的东西。这保证了软件将始终以相同的方式运行,无论其环境如何。容器与同一主机上的其他容器共享宿主操作系统和内核。容器周围的技术并不新鲜。它已经很长时间是 Linux 生态系统的一部分。由于最近围绕基于微服务的讨论,容器技术再次受到关注。此外,它是谷歌、亚马逊和 Netflix 运行的技术。

容器相对于虚拟机的适用性

让我们了解容器和虚拟机之间的区别——在表面层面上,两者都是实现隔离和虚拟化的工具。

从以下图表中可以清楚地看出虚拟机和容器之间的架构差异:

通过查看虚拟机的内部结构,我们可以看到有一个宿主操作系统以及内核,在其之上是虚拟化层。托管的应用程序必须引入它们自己的操作系统和环境。然而,在容器中,容器化技术层作为一个单独的层,被不同应用程序共享。这消除了对客户操作系统的需求。因此,容器中的应用程序具有更小的占用空间和强大的隔离级别。另一个鼓励你使用容器进行微服务部署的方面是,与在虚拟机上部署相同的应用程序相比,我们可以在同一台物理机器上打包更多的应用程序。这有助于我们实现更大的规模经济优势,并提供了虚拟机优势的比较。

关于容器,还有一点需要注意,那就是它们也可以在虚拟机上运行。因此,拥有一个物理服务器并在其上运行虚拟机是可以的。这个虚拟机作为多个容器的宿主。

运营团队思维模式的转变

微软的比尔·贝克提出了宠物和牛的类比,并将其应用于数据中心的服务器。好吧,说实话,我们关心我们的宠物。我们爱它们,对它们表示关爱,我们还给它们起名字。我们考虑它们的卫生;如果它们生病了,我们会带它们去看兽医。我们会这样关心我们的牛吗?当然不会;这是因为我们对牛并不那么关心。

这个类比在服务器和容器方面也是适用的。在 DevOps 之前的日子,服务器管理员关心服务器。他们过去会给那些服务器机器起名字,并且有专门的维护停机时间等等。随着 DevOps 实践,如基础设施即代码和容器化,容器可以被当作牛来对待。作为运维团队,我们不需要关心它们,因为容器意味着短暂的寿命。它们可以在集群中快速启动,也可以快速拆除。当你处理容器时,始终记住这个类比。在日常工作运营中,预期容器的启动和拆除是正常做法。

这个类比改变了我们对微服务部署及其如何支持容器化的看法。

容器是新的二进制文件

这是你作为.NET 开发者将面临的新现实:与微服务一起工作。容器是新的二进制文件。使用 Visual Studio,我们编译.NET 程序,编译后,Visual Studio 生成.NET 程序集,即 DLL 或 EXE 文件。我们收集编译器生成的这些相关的 DLL 和 EXE 文件,并将它们部署到服务器上。

"容器是新的部署二进制文件"

  • 史蒂夫·拉斯克,微软高级项目经理

简而言之,我们的部署单元以前是以程序集的形式存在的。现在不再是了!好吧,我们仍然有 .NET 程序生成 EXEs 和 DLLs,但在微服务世界中,我们的部署单元已经改变。现在它是一个容器。我们仍然会将程序编译成程序集。这些程序集将被推送到容器中,并准备好进行部署。

当我们在本章下一节查看代码遍历时,你会理解这个观点。作为 .NET 开发者,我们有能力(也许可以说必要性)来部署容器。此外,容器部署的另一个优点是它消除了不同操作系统甚至不同语言和运行时之间的障碍。

它在你的机器上工作吗?让我们把你的机器也运走吧!

通常,我们经常从开发者那里听到这样的话:“嗯,在我的机器上它工作得很好!”这通常发生在生产环境中无法复制的缺陷出现时。由于容器是不可变和可组合的,消除开发和生产环境之间的配置阻抗是完全可能的。

介绍 Docker

Docker(www.docker.com)是推动应用程序容器化普及的主要力量。Docker 对于容器来说,就像 Google 对于搜索引擎一样。有时,人们甚至将容器和 Docker 视为同义词。微软与 Docker 合作,并积极为 Docker 平台和开源工具做出贡献。这使得 Docker 对于我们这些 .NET 开发者来说非常重要。

Docker 是一个非常重要的主题,对于任何严肃的 .NET 开发者来说都足够重要。然而,由于时间和范围的限制,我们在这里只会对 Docker 生态系统进行简要介绍。我们强烈建议你阅读 Packt 出版公司提供的 Docker 书籍。

如果你想在不需要在机器上安装 Docker 的情况下安全地尝试和学习 Docker,你可以通过 KataCoda.com 来实现。

现在,让我们关注 Docker 平台的一些术语和工具。这将是我们的下一节所必需的:

  • Docker 镜像:Docker 的 镜像 是一个只读模板,包含创建 Docker 容器的指令。Docker 镜像由一个独立的文件系统、相关库等组成。在这里,镜像始终是只读的,并且可以运行完全相同的抽象、底层、主机差异。Docker 镜像可以由一个层叠加在另一个层之上。Docker 镜像的可组合性可以与分层蛋糕的类比相比较。用于不同容器的 Docker 镜像可以被重用。这也帮助减少了使用相同基础镜像的应用程序的部署足迹。

  • Docker 仓库:Docker 仓库是一个镜像库。仓库可以是公开的,也可以是私有的。此外,它可以是与 Docker 守护进程或 Docker 客户端在同一服务器上,或者完全在不同的服务器上。

  • Docker hub:这是一个公共仓库,它存储镜像。它位于 hub.docker.com

  • Dockerfile:Dockerfile 是一个包含构建 Docker 镜像指令的构建或脚本文件。Dockerfile 中可以记录多个步骤,从获取基础镜像开始。

  • Docker 容器:Docker 容器是 Docker 镜像的可运行实例。

  • Docker compose:Docker compose 允许您在单个文件中定义应用程序的组件——它们的容器、配置、链接和卷。然后,一个命令将设置一切并启动您的应用程序。它是您应用程序的架构/依赖关系图。

  • Docker swarm:Swarm 是 Docker 服务,通过它容器节点协同工作。它运行一个副本任务的定义实例,该任务本身是一个 Docker 镜像。

让我们来看看 Docker 生态系统中的各个组件;让我们尝试理解 Docker 工作流程在软件开发生命周期中合理性的其中一种方式。

Docker 微服务部署概述

为了支持此工作流程,我们需要一个 CI 工具和一个配置管理工具。为了说明目的,我们选择了 Visual Studio Team Services (VSTS) 构建服务作为 CI 和 VSTS 发布管理以实现持续交付。对于任何其他工具或部署模式,工作流程都将保持不变。以下是一种 Docker 微服务部署的变体:

  1. 代码已提交到 VSTS 仓库。如果这是项目的第一次提交,它将与项目的 Dockerfile 一起完成。

  2. 前面的检查触发 VSTS 从源代码构建服务并运行单元/集成测试。

  3. 如果测试成功,VSTS 将构建一个 Docker 镜像,并将其推送到 Docker 仓库。VSTS 发布服务将镜像部署到 Azure 容器服务。

  4. 如果 QA 测试也通过,则使用 VSTS 将容器提升到部署并启动它在生产中。

以下图表详细描述了步骤:

图片

注意,通常的 .NET CI-CD 工具,如 TeamCity 和 Octopus Deploy(功能处于 alpha 阶段),具有生成 Docker 容器作为构建工件并将其部署到生产的特性。

使用 Docker 的微服务部署示例

现在我们已经拥有了迈向编码并亲自查看事物如何工作的所有必需品。我们在这里选择了产品目录服务示例,以作为 Docker 容器部署。在运行相关的源代码之后,您应该能够在 Docker 容器中成功运行产品目录服务。

在您的机器上设置 Docker

本教程不需要任何现有的 Docker 知识,并且应该花费大约 20 或 30 分钟来完成。

前置条件

您需要执行以下操作:

  1. 安装 Microsoft Visual Studio 2017 Update 3 (www.visualstudio.com/downloads/download-visual-studio-vs)

  2. 安装 .NET Core 2.0 (www.microsoft.com/net/download/core)

  3. 安装 Docker For Windows 以在本地运行 Docker 容器 (www.docker.com/products/docker#/windows)

我们使用 Docker Community Edition for Windows 来演示示例。

  1. 安装完成后,您的系统需要重启以完成安装。

  2. 重启后,如果您的系统尚未启用 Hyper-V 功能,Docker for Windows 将提示您启用该功能。点击“确定”以在您的系统上启用 Hyper-V 功能(需要重启系统)。参考以下截图:

图片

  1. Docker for Windows 安装完成后,在系统托盘中的 Docker 图标上右键单击,然后点击“设置”并选择“共享驱动器”:

图片

创建 ASP.NET Core Web 应用程序

以下是开始使用的简单步骤:

  1. 通过导航到“文件”|“新建项目”|“.NET Core”|选择 ASP.NET Core Web 应用程序来创建一个新项目,参考以下截图:

图片

  1. 在“新建 ASP.NET Core Web 应用程序”窗口中,选择 .NET Core 和 ASP.NET Core 2.0。

  2. 从可用的模板中选择 Web 应用程序(模型-视图-控制器)。

  3. 检查“启用 Docker 支持”。

  4. 由于我们是在 Windows 上进行演示,请选择操作系统为 Windows(如果您没有按照上一节所述安装 Docker,那么您需要安装 Docker for Windows)。

  5. 点击“确定”继续,参考以下截图:

图片

上述步骤将创建具有 Docker 支持的 FlixOne.BookStore.ProductService 项目。以下是我们项目结构的截图:

图片

以下文件被添加到项目中:

  • Dockerfile:ASP.NET Core 应用程序的 Dockerfile 基于 microsoft/aspnetcore 镜像 (hub.docker.com/r/microsoft/aspnetcore/)。此镜像包含预编译的 ASP.NET Core NuGet 包,这提高了启动性能。当构建 ASP.NET Core 应用程序时,Dockerfile 的 FROM 指令(命令)指向 Docker Hub 上的最新 microsoft/dotnet 镜像 (hub.docker.com/r/microsoft/dotnet/))。以下是由模板提供的默认代码片段:
FROM microsoft/aspnetcore:2.0
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "FlixOne.BookStore.ProductService.dll"]

上述代码基本上是一组指令,这些指令如下:

FROM 告诉 Docker 从现有镜像中拉取基础镜像,调用 microsoft/aspnetcore:2.0。这个镜像已经包含了在 Linux 上运行 ASP.NET Core 所需的所有依赖项,因此我们不需要设置它。

COPYWORKDIR 将当前目录的内容复制到被调用/app 容器内部的新目录中,并将其设置为后续指令的工作目录。

EXPOSE 告诉 Docker 在容器的 80 端口上公开产品目录服务。

ENTRYPOINT 指定了容器启动时执行的命令。在这种情况下,是 .NET。

  • Docker-compose.yml:这是用于定义要使用 Docker-compose build/run 构建和运行的镜像集合的基本 Compose 文件。

  • Docker-compose.dev.debug.yml:这是一个额外的 Compose 文件,用于在配置设置为调试时进行迭代更改。Visual Studio 将调用 -f docker-compose.yml-f docker-compose.dev.debug.yml 来合并它们。此 Compose 文件由 Visual Studio 开发工具使用。

  • Docker-compose.dev.release.yml:这是一个额外的 Compose 文件,用于调试您的发布定义。它将在隔离模式下加载调试器,因此不会更改生产镜像的内容。

docker-compose.yml 文件包含在项目运行时创建的镜像名称。

我们现在拥有运行/启动 Docker 容器中我们的服务所需的一切。在继续之前,请参阅第二章,实现微服务,并添加完整的代码(即控制器、存储库等),以便项目结构看起来像以下截图:

截图

现在你只需按 F5 键并在容器中启动你的服务。这是将服务放入容器中最简单、最直接的方法。一旦你的微服务被容器化,你可以使用 Visual Studio 团队服务和 Azure 容器服务将容器部署到 Azure 云中(docs.microsoft.com/en-us/azure/container-service/dcos-swarm/container-service-deployment)。

摘要

微服务部署对我们来说是一次激动人心的旅程。为了成功交付微服务,应遵循部署的最佳实践。在我们讨论使用自动化工具进行部署之前,我们需要关注实现微服务的隔离要求。通过成功的微服务部署实践,我们可以快速交付业务变更。从自给自足的团队到持续交付的不同隔离要求,为微服务提供了基本承诺的规模和敏捷性。容器化到目前为止是我们拥有的最重要的创新技术之一,我们必须利用它来进行微服务部署。将 Azure 云与 Docker 结合使用将帮助我们实现我们期望的微服务的规模和隔离。有了 Docker,我们可以轻松实现更高的应用程序密度,这意味着我们云基础设施成本的降低。我们还看到了如何使用 Visual Studio 和 Windows 的 Docker 工具轻松启动这些部署。

在我们接下来的章节中,我们将探讨微服务安全。我们将讨论用于身份验证的 Azure 活动目录,如何利用 OAuth 2.0,以及如何使用 Azure API 管理来保护 API 网关。

第六章:微服务安全

安全性是网络应用最重要的横切关注点之一。不幸的是,知名网站的数据泄露似乎已成为家常便饭。考虑到这一点,信息和应用安全对网络应用变得至关重要。同样,安全应用不应再是事后考虑的事情。在组织中,安全是每个人的责任。

与微服务相比,单体应用具有更小的攻击面。然而,微服务本质上是由分布式系统组成的。原则上,微服务是相互隔离的;因此,实施良好的微服务比单体应用更安全。单体与微服务相比有不同的攻击向量。微服务架构风格迫使我们从安全的角度进行不同的思考。然而,我要提前告诉你,微服务安全是一个复杂且难以理解和实施的领域。

在我们深入探讨微服务安全之前,让我们了解我们对此的方法。我们将更多地关注认证和授权(在章节中统称为认证)的工作方式以及.NET 生态系统中的可用选项。

我们将探索 Azure API 管理及其作为.NET 微服务环境 API 网关的适用性;我们还将看到 Azure API 管理如何通过其安全功能帮助我们保护微服务。然后,我们将简要介绍为微服务安全提供深度防御机制的不同外围方面。我们还将讨论以下主题:

  • 为什么表单认证和较老的技术不足够?

  • 认证和可用的选项,包括 OpenID 和 Azure Active Directory

  • 介绍 OAuth 2.0

  • 介绍 Azure API 管理作为 API 网关

  • 使用 Azure API 管理进行安全

  • 互服务通信安全方法

  • 容器安全和其他外围安全方面

单体应用中的安全性

要理解微服务安全,让我们回顾一下我们过去是如何保护.NET 单体应用的。这将帮助我们更好地理解为什么微服务的认证机制需要不同。

保护应用的关键机制一直是认证。认证验证用户的身份。授权管理用户可以或不可以访问的内容,也称为权限。加密,嗯,这是帮助你在客户端和服务器之间传输数据时保护数据的机制。不过,我们不会过多地讨论加密,只需确保所有通过网络传输的数据都进行了加密。这可以通过使用 HTTPS 协议来实现。

以下图表描述了.NET 单体中典型认证机制的流程:

图片

在前面的图中,我们可以看到用户通常通过 Web 浏览器输入他们的用户名和密码。然后,这个请求击中了 Web 应用中的一个薄层,该层负责认证。这个层或组件连接到用户凭据存储,在.NET 应用的情况下通常是 SQL 服务器。认证层验证用户提供的凭据与凭据存储中存储的用户名和密码是否匹配。

一旦验证了会话的用户凭据,浏览器中就会创建一个会话 cookie。除非用户有一个有效的会话 cookie,否则他无法访问应用。通常,会话 cookie 会随每个请求一起发送。在这些类型的单体应用中,模块可以自由地相互交互,因为它们在同一个进程中,并且有内存访问权限。这意味着在这些应用模块之间信任是隐含的,因此它们在相互通信时不需要单独验证和验证请求。

微服务中的安全性

现在让我们看看微服务的案例。从本质上讲,微服务是分布式系统。没有单个应用实例;相反,有多个不同的应用,它们和谐地相互协调以产生所需的输出。

为什么传统的 .NET 认证机制不起作用?

微服务安全性的可能方法之一可能是:我们模仿单体应用中认证层的相同行为。这可以描述如下:

在这种方法中,我们将认证层进行了分布式部署,并将其提供给所有微服务。由于每个都是一个不同的应用,它将需要一个自己的认证机制。这本质上意味着每个微服务的用户凭据存储也是不同的。这引发了许多问题,例如我们如何保持所有服务之间的认证同步?我们如何验证服务间的通信,或者我们是否跳过它?我们对这些问题没有满意的答案。因此,这种方法没有意义,只是增加了复杂性。采用这种方法,我们甚至不能确定它是否能在现实世界中工作。

对于现代应用,我们还需要考虑一个额外的因素。在微服务世界中,我们需要支持原生移动应用和其他非标准形态设备,以及物联网应用。随着原生移动应用的显著普及,微服务架构也需要支持客户端和微服务之间的安全通信。这与传统的基于 Web 浏览器的用户界面不同。在移动平台上,Web 浏览器不是任何原生移动应用的一部分。这意味着基于 cookie 或基于会话的认证是不可能的。因此,微服务需要支持这种客户端应用之间的互操作性。这对于.NET 单体应用从未是问题。

在传统认证的情况下,浏览器负责在每次请求时发送 cookie。但我们在原生移动应用中并没有使用浏览器。实际上,我们既没有使用 ASPX 页面,也没有使用表单的认证模块。对于 iOS 客户端或 Android,情况完全不同。更重要的是,我们还在尝试限制对 API 的未授权访问。在前面的例子中,我们会保护客户端,无论是 MVC 应用还是 Windows Phone 应用,而不是微服务。此外,所有这些移动客户端设备都不是信任子系统的组成部分。对于每个请求,我们无法信任移动用户确实是所有者;通信通道也没有得到保护。因此,来自他们的任何请求都完全不可信。

但除了这些问题之外,我们还有一个更概念性的问题。为什么应用程序应该负责认证用户和授权?这不是应该分开的吗?

解决这个问题的另一个方案是使用 SAML 协议,但同样,这基于 SOAP 和 XML,所以并不是微服务的最佳选择。SAML 实现的复杂性也很高。

因此,从前面的讨论中可以明显看出,我们需要一个基于令牌的解决方案。微服务认证的解决方案以 OpenID Connect 和 OAuth 2.0 的形式出现。OpenID Connect 是认证的标准,OAuth 2.0 是授权的规范。然而,这种授权本质上是委托的。

我们将在后续章节中详细探讨这一点。但在那之前,让我们暂时偏离一下,看看 JSON Web Tokens,并了解为什么它们在微服务安全方面具有重要意义。

JSON Web Tokens

JSON Web Tokens (JWT) 读作 JOT。它是一个定义良好的 JSON 架构或格式,用于描述数据交换过程中涉及的令牌。JWTs 在 RFC 7519 中进行了描述。

JWTs 与 OpenID Connect 或 OAuth 2.0 无关。这意味着它们可以独立使用,不受 OAuth 2.0 或 OpenID Connect 的影响。OpenID Connect 强制在过程中交换的所有令牌都使用 JWT。在 OAuth 2.0 中,JWT 的使用不是强制性的,而更像是一种实现格式。此外,.NET 框架内置了对 JWT 的支持。

基于 JWT 的安全令牌的目的是生成一个包含发行者、接收者信息以及发送者身份描述的数据结构。因此,令牌应该在传输过程中得到保护,以防止被篡改。为此,令牌使用对称或非对称密钥进行签名。这意味着当接收者信任令牌的发行者时,它也可以信任其中的信息。

下面是一个 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 4.5.1 及以后的版本内置了对生成和消费 JWT 的支持。您可以使用包管理控制台使用以下命令在任何.NET 应用程序中安装 JWT 支持:

Install-Package System.IdentityModel.Tokens.Jwt

访问jwt.io/,您可以在那里非常容易地查看和解码 JWT。此外,您还可以将其添加到 Chrome 调试器中,这非常方便。

什么是 OAuth 2.0?

好的,你可能不知道 OAuth 2.0 是什么,但你肯定在多个网站上使用过它。如今,许多网站允许你使用 Facebook、Twitter 或 Google 账户的用户名和密码登录。例如,访问你喜欢的网站,比如www.stackoverflow.com的登录页面。那里有一个登录按钮,上面写着你可以使用 Google 账户登录,例如。当你点击 Google 按钮时,它会带你到 Google 的登录页面,并显示一些提到的权限。在这里,你提供你的 Google 用户名和密码,然后点击允许按钮,以授予你喜欢的网站权限。然后,Google 将你重定向到 Stack Overflow,你将使用适当的权限在 Stack Overflow 上登录。这仅仅是 OAuth 2.0 和 OpenID Connect 的最终用户体验。

OAuth 2.0 可以最好地描述为一系列规范转变为授权框架。RFC 6749将 OAuth 定义为以下内容:

“OAuth 2.0 授权框架允许第三方应用程序代表资源所有者通过在资源所有者和 HTTP 服务之间协调一个批准交互,或者允许第三方应用程序代表自己获取对 HTTP 服务的有限访问。”

OAuth 2.0 处理网络、原生移动应用程序以及所有无头服务器应用程序(在我们的语境中,这些不过是微服务实例)。你可能想知道为什么我们先讨论授权而不是认证。原因是 OAuth 2.0 是一个委托授权框架。这意味着,为了完成授权流程,它依赖于一个认证机制。

现在我们来看一些与之相关的术语。

OAuth 2.0 角色描述了授权过程中的相关方:

  • 资源:这是指从未授权访问和使用的实体。在我们的案例中,这不过是一个微服务。

  • 资源所有者:资源所有者是指拥有指定资源的个人或实体。当一个人拥有资源时,他就是最终用户。

  • 客户端:客户端是指所有类型的客户端应用程序。这指的是任何试图访问受保护资源的应用程序。在微服务语境中,涉及的应用程序包括单页应用程序、Web 用户界面客户端、原生移动应用程序,甚至是一个试图访问下游另一个微服务的微服务。

  • 授权服务器:这是托管安全令牌服务并在成功验证资源所有者并从资源所有者或代表他们获得权限后向客户端颁发令牌的服务器。

你可能已经注意到 OAuth 区分了最终用户和最终用户使用的应用程序。这有点奇怪,但完全合理,因为它也被普遍视为“我正在授权这个应用程序代表我执行这些操作”。

下图描述了这些角色如何在 OAuth 框架中授权的一般流程中相互交互:

步骤 6,如前图所示,客户端将授权授予传递给授权服务器。这一步看起来并不简单。授权授予有多种类型。授权类型代表 OAuth 2.0 中获取访问令牌的四种不同可能的使用场景。如果你选择了错误的授权类型,可能会危及安全:

  • 授权代码:这是服务器端 Web 应用程序典型使用的 OAuth 授权,你会在你的 ASP.NET 应用程序中使用它。

  • 隐式:通过服务器进行身份验证会返回一个访问令牌到浏览器,然后可以用来访问资源。这对于通信不能是私密的单页应用程序很有用。

  • 资源所有者密码凭证:这要求用户直接在应用程序中输入他们的用户名和密码。当你开发一个用于与自己的服务器进行身份验证的第一方应用程序时,这很有用。例如,一个移动应用程序可能会使用资源所有者授权来与你的服务器进行身份验证。

  • 客户端凭证:这通常用于客户端代表自己(客户端也是资源所有者)或基于与授权服务器预先安排的授权请求访问受保护资源时使用。

什么是 OpenID Connect?

OpenID Connect 1.0 是 OAuth 2.0 协议之上的简单身份层。OpenID Connect 全部关于身份验证。它允许客户端根据授权服务器执行的身份验证来验证最终用户。它还以可互操作和类似 REST 的方式获取关于最终用户的基本配置文件信息。

因此,OpenID Connect 允许所有类型的客户端——基于 Web、移动和 JavaScript 的客户端——请求并接收有关经过身份验证的会话和最终用户的信息。我们知道 OAuth 2.0 定义了访问令牌。那么,OpenID Connect 定义了一个标准化的身份令牌(通常称为 ID 令牌)。身份令牌被发送到应用程序,以便应用程序可以验证用户身份。它定义了一个端点,用于获取该用户的信息,例如他们的姓名或电子邮件地址。这就是用户信息端点。

它建立在 OAuth 2.0 之上,因此流程是相同的。它可以与授权码授权和隐式授权一起使用。由于客户端凭据授权用于服务器到服务器的通信,因此无法使用客户端凭据授权。

在此过程中没有涉及最终用户,因此也没有最终用户身份。同样,对于使用或流程的资源所有者路径来说,这也没有意义。那么它是如何工作的呢?好吧,我们不仅请求访问令牌,还会从实现 OpenID Connect 规范的 安全令牌服务STS)请求一个额外的 ID 令牌。客户端收到一个 ID 令牌,通常还会收到一个访问令牌。为了获取有关已认证用户的更多信息,客户端可以随后使用访问令牌向用户信息端点发送请求;然后该端点将返回关于新用户的声明。

OpenID 支持授权码流和隐式流。它还增加了一些额外的协议,包括发现和动态注册。

Azure Active Directory

OAuth 2.0 和 OpenID Connect 1.0 规范有多个提供者。Azure Active DirectoryAzure AD)就是其中之一。Azure AD 为组织提供企业级云应用程序的身份管理。Azure AD 集成将为您的用户提供流畅的登录体验,并帮助您的应用程序符合 IT 政策。Azure AD 提供高级安全功能,如多因素身份验证,并且随着应用程序的增长而具有良好的可扩展性。它被用于所有 Microsoft Azure 云产品中,包括 Office 365,并且每天处理超过十亿次登录。

传统 .NET 环境的一个有趣方面是,它们可以将组织的 Windows Server Active Directory 与 Azure AD 集成得非常好。这可以通过 Azure AD 同步工具或新的传递身份验证功能来实现。因此,组织的 IT 合规性仍然会被管理。

使用 OpenID Connect、OAuth 2.0 和 Azure AD 的微服务身份验证示例

现在我们已经具备了所有先决知识,可以开始编码了。让我们尝试构建一个 ToDoList 应用程序。我们将要保护 TodoListService,它代表我们的一个微服务。在解决方案中,ToDoList 微服务由 TodoListService 项目表示,而 ToDoListWebApp 代表服务器端 Web 应用程序。如果您打开本章提供的名为 OpenIdOAuthAzureAD.sln 的 Visual Studio 解决方案,这将更容易理解。此示例使用客户端凭据授权。

注意,由于 Azure 门户和相应的 Azure 服务 UI 的不断变化,建议您使用 Azure 服务管理 API 并自动化一些即将进行的注册任务。然而,出于学习和鼓励新接触 Azure 或可能首次尝试 Azure AD 的开发者的目的,我们将遵循 Azure 门户用户界面。

这里是先决条件:

  • Visual Studio 2017 更新 3

  • 一个 Azure 订阅(如果您没有这个,您可以使用此演示的免费试用账户)

  • Azure AD 租户(单租户):您也可以使用您 Azure 账户的默认目录,这应该与 Microsoft 组织的不同。

使用 Azure AD 租户注册 TodoListService 和 TodoListWebApp

现在我们来看看如何注册 TodoListService

在此步骤中,我们将使用 Azure AD 租户添加 TodoListService。为了实现这一点,请登录到 Azure 管理门户,然后执行以下操作:

  1. 点击“应用注册”。点击添加按钮。它将打开创建窗格,如图所示:

图片

  1. 提供前面截图显示的所有必填详细信息,并在创建窗格的底部点击创建按钮。当我们提供登录 URL 时,请确保您为您的应用程序提供它。在我们的例子中,TodoListService 是一个微服务,因此我们不会有特殊的登录 URL。因此,我们必须提供默认 URL 或只是我们的微服务的主机名。我们将从我们的机器上运行该服务,因此 localhost URL 就足够了。您可以在TodoListService项目下的项目 URL 上右键单击,然后转到 Web,如图所示:

图片

在 Azure 门户中的登录 URL 应该有尾随的 /;否则,即使您正确执行了所有步骤,也可能遇到错误。

  1. 如果您使用 Microsoft Azure App Service 计划部署您的服务,您将获得一个类似于 https://todolistservice-xyz.azurewebsites.net/ 的 URL。如果您在 Azure 上部署服务,您可以稍后更改登录 URL。

  2. 一旦点击创建按钮,Azure 将将应用程序添加到您的 Azure AD 租户中。然而,为了完成 TodoListService 的注册,还需要填写一些更多细节。因此,导航到“应用注册”|“TodoListService”|“属性”。您会注意到还有一些额外的属性,例如 App ID URL,现在已经提供了。

  3. 对于 App ID URL,输入 https://[Your_Tenant_Name]/TodoListService,将 [Your_Tenant_Name] 替换为您的 Azure AD 租户名称。点击确定完成注册。最终的配置应如下所示:

图片

现在我们继续注册 TodoListWebApp:

  1. 首先,我们注册 TodoListWebApp。这是必要的,因为我们打算使用 OpenID Connect 连接到这个基于浏览器的 Web 应用程序。因此,我们需要在最终用户(即我们)和 TodoListWebApp 之间建立信任。

  2. 点击“应用注册”。点击“添加”按钮。它将打开创建面板,如以下屏幕截图所示。填写登录 URL 为 https://localhost:44322/

  3. 同样,在 TodoListService 注册过程中,一旦创建 Web 应用程序,我们就能查看大部分附加属性。所以,最终的属性配置将看起来像这样:

  1. 这里需要注意的一个设置是注销 URL:我们将其设置为 https://localhost:44322/Account/EndSession

    这是因为在结束会话后,Azure AD 将将用户重定向到这个 URL。对于 App ID URL,输入 https://[Your_AD_Tenant_Name]/TodoListWebApp,将 [Your_AD_Tenant_Name] 替换为您的 Azure AD 租户名称。点击“确定”以完成注册。

  2. 现在我们需要在 TodoListWebApp 中设置权限,以便它可以调用我们的微服务:TodoListService。因此,再次导航到“应用注册”|“TodoListWebApp”|“必需权限”并点击“添加”。现在点击“1 选择一个 API”。此导航在以下屏幕截图中显示。您需要键入 ToDoListService 以使其在 API 面板中显示:

  1. 现在,您将能够查看“启用访问”面板,在该面板下,您需要在“委托权限”下勾选“访问 TodoListService 权限”,并在“添加 API 访问”面板下勾选“完成”。这将保存权限。

为 TodoListWebApp 生成 AppKey

注册过程中的另一个重要步骤是添加 client_secret,这是在 Azure AD 和 TodoListWebApp 之间建立信任所必需的。这个 client_secret 只生成一次,并在 Web 应用程序中进行配置。要生成这个密钥,请导航到“应用注册”|“TodoListWebApp”|“密钥”。然后,将描述添加为AppKey并点击保存。一旦密钥保存,Azure 将自动生成密钥值,并将其显示在描述旁边。这个密钥只会显示一次,所以您必须立即复制并保存以供以后使用。在这种情况下,我们将把这个密钥保存在 TodoListWebApp 的 web.config 文件中。

存储的密钥将在 Azure 门户中如下显示:

对于生产级应用程序,将 client_Secret 和所有此类关键密钥值保留在 web.config 中是一个坏主意。良好的做法是将它们加密并与应用程序隔离。为此目的,在生产级应用程序中,您可以使用 Azure Key Vault (azure.microsoft.com/en-us/services/key-vault/) 来保护所有密钥。密钥库的另一个优点是您可以按环境管理密钥,例如 dev-test-staging 和生产。

配置 Visual Studio 解决方案项目

首先,我们来看看如何使用 TodoListService 项目进行配置。

打开 web.config 文件并替换以下密钥:

  1. 搜索 ida:Tenant 密钥。将其值替换为您的 AD 租户名称,例如,contoso.onmicrosoft.com。这也将成为应用程序 APP ID URL 的一部分。

  2. 替换 ida:Audience 密钥。将其值替换为 https://[您的 AD 租户名称]/TodoListService

    [您的 AD 租户名称] 替换为您的 Azure AD 租户名称。

现在我们来看看如何使用 TodoListWebApp 项目进行配置。

打开 web.config 文件并找到并替换以下密钥,使用提供的值:

  1. 将 todo:TodoListResourceid 替换为 https://[您的租户名称]/TodoListService

  2. 将 todo:TodoListBaseAddress 替换为 https://localhost:44321/

  3. 将 ida:ClientId 替换为 ToDoListWebApp 的应用程序 ID。您可以通过导航到 App Registration | TodoListWebApp 获取它。

  4. 将 ida:AppKey 替换为在注册 TodoListWebApp 的过程中步骤 2 中生成的 client_secret。如果您错过了记录此密钥,您需要删除之前的密钥并生成一个新的密钥。

  5. 将 ida:Tenant 替换为您的 AD 租户名称,例如,contoso.onmicrosoft.com

  6. 将 ida:RedirectUri 替换为当用户从 TodoListWebApp 登出时应用程序要重定向到的 URL。在我们的例子中,默认是 https://localhost:44322/,因为我们希望用户导航到应用程序的主页。

在 IIS Express 上生成客户端证书

现在 TodoListServiceTodoListWebApp 将通过安全通道进行通信。为了建立安全通道,ToDoListWebApp 需要信任客户端证书。这两个服务都托管在同一台机器上,并在 IIS Express 上运行。

要配置计算机信任 IIS Express SSL 证书,以管理员身份打开 PowerShell 命令窗口。查询您的个人证书存储以找到 CN=localhost 证书的指纹:

PS C:windowssystem32> dir Cert:LocalMachineMy
Directory: Microsoft.PowerShell.SecurityCertificate::LocalMachineMy
Thumbprint Subject
---------- -------
C24798908DA71693C1053F42A462327543B38042 CN=localhost

接下来,将证书添加到受信任的根存储中:

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()

上一组指令将客户端证书添加到本地计算机的证书存储中。

运行两个应用程序

我们已经完成了所有那些繁琐的配置屏幕和密钥替换。兴奋吗?但在你按下 F5 之前,将 ToDoListServiceToDoListWebApp 设置为启动项目。一旦完成,我们就可以安全地运行我们的应用程序,并欢迎来到我们应用程序的登录页面。如果你点击登录按钮,你将被重定向到 login.microsoftonline.com;这代表 Azure AD 登录。一旦你能够登录,你将看到以下登录页面:

当你登录应用程序以研究 ID 令牌的详细交换并获取访问令牌时,你可以观察到网络流量和 URL 重定向。如果你通过 ToDoList 菜单探索应用程序,你将能够访问 ToDoList 屏幕以及向 ToDoList 添加项目。这就是我们的 TodoListService 微服务被调用的地方,以及从 TodoWebApp 网络应用程序获取授权权限的地方。如果你探索配置文件菜单,你会看到 ID 令牌与你的名字、姓氏和电子邮件 ID 一起返回,这显示了 OpenID Connect 的实际应用。

如果你想要详细探索代码,TodoListService 项目的 TodoListController.csStartup.Auth.csTodoListController.cs 包含了一些有趣的代码片段以及解释性注释。

在这个例子中,我们使用了 OAuth 和 OpenID Connect 来保护基于浏览器的用户界面、网络应用程序和微服务。如果我们有一个 API 网关位于用户界面网络应用程序和微服务之间,事情可能会有所不同。在这种情况下,我们需要在 Web 应用程序和 API 网关之间建立信任。此外,我们必须将 ID 令牌和访问令牌从 Web 应用程序传递到 API 网关。然后,它将这些令牌传递到微服务。然而,在本章的范围内讨论和实现这一点是不切实际的。

Azure API 管理作为一个 API 网关

微服务实现中的另一个重要模式是 前后端分离BFF)。这个模式是由 Sam Newman 引入并推广的。BFF 模式的实际实现是通过在各类客户端和微服务之间引入 API 网关来完成的。

这在以下图中表示:

Azure API Management(以下简称 Azure APIMAPIM)正是合适的选择,它可以在基于 .NET 的微服务实现中充当 API 网关。由于 Azure APIM 是云服务之一,它具有超强的可扩展性,并且可以很好地集成到 Azure 生态系统中。在本章中,我们将重点关注 Azure APIM 的以下功能。

Azure APIM 在逻辑上分为三个部分:

  • API 网关:API 网关仅仅是客户端应用程序和服务之间的代理。它负责以下功能;这些主要用于各种应用程序与微服务通信:

    • 接受 API 调用并将它们路由到您的后端

    • 验证 API 密钥、JWT 和证书

    • 支持通过 Azure AD 和 OAuth 2.0 访问令牌进行身份验证

    • 强制执行使用配额和速率限制

    • 无需代码修改即可即时转换 API

    • 缓存后端响应的设置位置

    • 为分析目的记录调用元数据

  • 发布者门户:这是组织并发布 API 程序的行政接口。它主要用于微服务开发者使微服务/API 可供 API 消费者或客户端应用程序使用。通过这种方式,API 开发者可以:

    • 定义或导入 API 模式

    • 将 API 打包成产品

    • 在 API 上设置策略,如配额或转换

    • 从分析中获得洞察

    • 管理用户

  • 开发者门户:这作为 API 消费者的主要网络存在,他们可以执行以下操作:

    • 阅读 API 文档

    • 通过交互式控制台尝试 API

    • 创建账户并订阅以获取 API 密钥

    • 分析它们自己的使用情况

Azure APIM 附带易于遵循的用户界面和良好的文档。Azure API 管理还附带其 REST API,因此您在 Azure APIM 门户中看到的所有功能都可以通过 Azure REST API 端点以编程方式实现。

现在,让我们快速了解一下 Azure APIM 中的一些安全相关概念以及它们如何在微服务中使用:

  • 产品:产品仅仅是 API 的集合。它们还包含使用配额和使用条款。

  • 策略:策略是 API 管理的动态安全功能。它们允许发布者通过配置更改 API 的行为。策略是在 API 请求或响应上顺序执行的语句集合。API 管理本质上是一个代理,位于我们托管在 Azure 中的微服务和客户端应用程序之间。由于它是一个中间层,因此能够提供额外的服务。这些额外服务是在称为策略的声明性 XML 语法中定义的。Azure APIM 允许各种策略。实际上,您可以通过组合现有的策略来创建自己的自定义策略。以下是一些重要的策略:

  • 访问限制策略:

    • 检查 HTTP 头:此策略检查是否每个接收到的 Azure APIM 请求中存在特定的 HTTP 头或其值。

    • 通过订阅限制调用速率:此策略根据每个订阅中特定服务被调用的次数,允许或拒绝对微服务的访问。

    • 限制调用者 IP:此策略指的是 IP 地址的白盒化,因此只有已知的 IP 可以访问服务。

    • 按订阅设置使用配额:此策略允许一定数量的调用。它允许您在每个订阅的基础上强制执行可续订或终身调用量以及/或带宽配额。

    • 验证 JWT:此策略验证用于应用程序认证的 JWT 令牌参数。

  • 认证策略:

    • 使用基本认证进行认证:此策略有助于在入站请求上应用基本认证。

    • 使用客户端证书进行认证:此策略有助于使用客户端证书对 API 网关后面的服务进行认证。

  • 跨域策略:

    • 允许跨域调用:此策略使我们能够通过 Azure APIM 进行 CORS 请求。

    • CORS:这为端点或微服务添加了 CORS 支持,允许基于浏览器的 Web 应用程序进行跨域调用。

    • JSONP:JSONP 策略为端点或整个微服务添加了JSON 填充JSONP)支持,以允许 Java Script Web 应用程序进行跨域调用。

  • 转换策略:

    • 在内容中遮蔽 URL:此策略通过 Azure APIM 遮蔽响应中的 URL。

    • 设置后端服务:此策略改变了入站请求的后端服务行为。

策略的另一个优点是它们可以应用于入站和出站请求。

速率限制和配额策略示例

在上一节中,我们看到了策略的含义。现在让我们看看一个例子。以下是一个应用于端点的配额策略之一:

<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>

在这个例子中,我们正在限制来自单个用户的入站请求(入站)。因此,API 用户只能在 60 秒内进行100次调用。如果他们在该时间段内尝试进行更多调用,用户将收到状态码为429的错误,这基本上表示速率限制已超过。此外,我们为同一用户分配了每年200次的配额限制。这种节流行为是保护微服务免受不受欢迎的请求甚至 DOS 攻击的好方法。

Azure APIM 还支持使用 OAuth 2.0 和 OpenID Connect 进行认证。在发布者门户中,您可以轻松地看到 OAuth 和 OpenID Connect 选项卡以配置提供者。

容器安全

Docker 是工业应用容器化的一个重要部分。随着容器的大规模使用,很明显,我们需要在容器周围采取有效的安全措施。如果我们看一下容器的内部架构,它们与宿主操作系统的内核非常接近。

Docker 应用了最小权限原则来提供隔离并减少攻击面。尽管取得了进展,以下要点将帮助您了解可以为容器采取的安全措施:

  • 确保所有用于微服务的容器镜像都已签名并来自受信任的注册表

  • 增强宿主环境、守护进程和镜像的安全性

  • 遵循最小权限原则,不要提升对访问设备的访问权限

  • 使用 Linux 中的控制组来监控资源,如内存、I/O 和 CPU。

  • 尽管容器生命周期非常短暂,但记录所有容器活动是建议的,并且对于后续分析非常重要。

  • 如果可能的话,将容器扫描过程与工具集成,例如 Aqua (www.aquasec.com) 或 Twistlock (www.twistlock.com)。

其他安全最佳实践

微服务架构风格是新的,尽管围绕基础设施和编写安全代码的一些安全实践仍然适用。在本节中,让我们讨论一些这些实践:

  • 库和框架的标准化:在开发过程中引入新的库、框架或工具应该有一个流程。这将简化补丁的修复,如果发现任何漏洞;它也将最小化由开发中临时实施的库或工具带来的风险。

  • 定期漏洞识别和缓解:使用行业标准的安全扫描器扫描源代码和二进制文件应该是开发过程中的一个常规部分。发现和观察结果应该像功能缺陷一样得到处理。

  • 第三方审计和渗透测试:外部审计和渗透测试练习非常有价值。应该定期进行此类练习。这对于处理关键任务或敏感数据的应用程序来说至关重要。

  • 日志和监控:日志是一种非常有用的技术,可以用于检测和从攻击中恢复。在微服务的情况下,能够聚合来自不同系统的日志是必不可少的。Riverbed、AppDynamics 和 Splunk 等工具在这个领域非常有用。

  • 防火墙:在网络边界拥有一个或多个防火墙总是有益的。防火墙规则应该配置得当。

  • 网络隔离:在单体架构的情况下,网络分区是受限制和有限的。然而,在微服务架构中,我们需要逻辑上创建不同的网络段和子网。基于微服务交互模式的分区可以非常有效地保持和发展额外的安全措施。

摘要

微服务架构风格,由于其设计上的分布式特性,为我们提供了更好的保护宝贵业务关键系统的选项。传统的基于 .NET 的身份验证和授权技术不足以应用在微服务世界中。我们也看到了为什么基于安全令牌的方法,如 OAuth 2.0 和 OpenID Connect 1.0,正在成为微服务授权和认证的实体标准。如果您想了解更多与安全相关的通用信息,请访问开放网络应用安全项目OWASP)的网站 www.owasp.org 和微软安全开发生命周期 www.microsoft.com/en-us/sdl/。Azure AD 可以很好地支持 OAuth 2.0 和 OpenID Connect 1.0。Azure API 管理还可以在微服务的实现中充当 API 网关,并提供诸如策略等便捷的安全功能。

Azure AD 和 Azure API 管理提供了许多强大的功能来监控和记录接收到的请求。这对安全和追踪及故障排除场景都将非常有用。我们将在下一章中看到关于微服务故障排除的日志记录、监控和整体仪表化。

第七章:微服务监控

当系统出现问题时,利益相关者将想知道发生了什么,为什么会发生,你可以提供的任何提示或线索来修复它,以及如何防止未来再次发生相同的问题。这是监控的主要用途之一。然而,监控还能做更多。

在.NET 单体应用中,有多个监控解决方案可供选择。监控目标是始终集中的,监控设置和配置当然也很容易。如果出现问题,我们知道该寻找什么以及在哪里寻找,因为只有有限数量的组件参与系统,并且它们有相当长的生命周期。

然而,微服务是分布式系统,并且从本质上讲比单体应用更复杂。因此,在微服务生产环境中,资源利用、健康和性能监控是相当关键的。我们可以使用这些诊断信息来检测和纠正问题,同时也可以发现潜在的问题并防止其发生。监控微服务面临着不同的挑战。在本章中,我们将主要讨论以下主题:

  • 监控的需求

  • 微服务中的监控和日志记录挑战

  • 监控策略

  • .NET 监控空间中可用的工具和策略

  • 使用 Azure 诊断和应用程序洞察

  • ELK 堆栈和 Splunk 的简要概述

监控究竟意味着什么?没有正式的监控定义;然而,以下定义是合适的:

“监控提供了关于整个系统或系统不同部分在运行环境中的行为信息。这些信息可用于诊断并深入了解系统的不同特性。”

仪表化和遥测

监控解决方案依赖于仪表化和遥测。因此,当我们谈论监控微服务时,自然也会讨论仪表化和遥测数据。日志不过是一种仪表化机制。

仪表化

现在让我们看看仪表化的含义。仪表化是你可以添加诊断功能到应用程序的一种方式。它可以正式定义为以下内容:

“大多数应用程序将包括诊断功能,生成定制的监控和调试信息,尤其是在发生错误时。这被称为仪表化,通常通过向应用程序添加事件和错误处理代码来实现。”

-MSDN

在正常条件下,信息事件的数据可能不需要,从而减少了存储成本和收集这些数据所需的交易成本。然而,当应用程序出现问题时,您必须更新应用程序配置,以便诊断和仪器系统可以收集信息事件数据以及错误和警告消息,以帮助隔离和修复故障。如果问题仅间歇性出现,可能需要运行应用程序在这种扩展报告模式下一段时间。

遥测

遥测在其最基本的形式中,是收集由仪器和日志系统生成信息的流程。通常,它使用支持大规模扩展和应用程序服务的广泛分布的异步机制来执行。它可以定义为如下:

“收集由仪器收集的远程信息的流程通常被称为遥测。”

-MSDN

在大型且复杂的应用程序中,信息通常被捕获在数据管道中,并以便于在不同粒度级别分析和显示的形式存储。这些信息用于发现趋势,深入了解使用和性能,并检测和隔离故障。

Azure 没有内置的系统直接提供此类遥测和报告系统。然而,所有 Azure 服务公开的功能、Azure 诊断和应用程序洞察的组合允许您创建跨越简单监控机制到综合仪表板的遥测机制。您所需的遥测机制的复杂性通常取决于应用程序的大小。这基于几个因素,例如角色的数量或虚拟机实例的数量,它使用的辅助服务的数量,应用程序在不同数据中心之间的分布以及其他相关因素。

监控需求

微服务是复杂的分布式系统。微服务实现是任何现代 IT 企业的支柱。了解服务的内部结构以及它们的交互和行为将帮助您使整体业务更加灵活和敏捷。微服务的性能、可用性、可扩展性和安全性可以直接影响业务及其收入。因此,监控微服务至关重要。它帮助我们观察和管理服务属性的质量。让我们讨论需要它的场景。

健康监控

通过健康监控,我们以一定频率(通常是几秒)监控系统的健康状态及其各种组件。这确保了系统和其组件按预期行为。借助详尽的健康监控系统,我们可以监控整体系统健康,包括 CPU、内存利用率等。这可能以 ping 或广泛的健康监控端点形式出现,这些端点在那一刻会发出服务的健康状态以及一些有用的元数据。

对于健康监控,我们可以使用请求失败和成功的比率;我们还可以利用诸如合成用户监控等技术。我们将在本章稍后看到合成用户监控。

健康监控的指标基于成功或失败率的阈值值。如果参数值超出配置的阈值,则会触发警报。很可能由于这种失败而触发了维护系统健康的预防措施。这种措施可能是重启失败状态下的服务,或者分配一些服务器资源。

可用性监控

可用性监控与刚刚讨论的健康状态监控相当相似。然而,细微的区别在于,在可用性监控中,重点是系统的可用性,而不是该时间点的健康快照。

系统的可用性取决于各种因素,例如应用程序的整体性质和领域、服务以及服务依赖性,以及基础设施或环境。可用性监控系统捕获与这些因素相关的低级数据点,并将它们表示出来,以便使业务级功能可用。很多时候,可用性监控参数被用来跟踪业务指标和服务级别协议SLA)。

性能监控

系统的性能通常通过关键性能指标来衡量。任何大型基于 Web 的系统的一些关键性能指标如下:

  • 每小时处理的请求数量

  • 每小时服务的并发用户数

  • 用户执行业务交易(例如,下订单)所需的平均处理时间

此外,性能还通过系统级参数来衡量,例如:

  • CPU 利用率

  • 内存利用率

  • I/O 比率

  • 队列中的消息数量

如果系统未满足任何这些关键性能指标,则会触发警报。

通常,在分析性能问题时,监控系统捕获的先前基准的历史数据被用来进行故障排除。

安全监控

监控系统可以检测异常的数据模式请求、异常的资源消耗模式,并检测对系统的攻击。具体来说,在 DoS 攻击或注入攻击的情况下,可以在事先识别并通知团队。安全监控还记录了已认证用户的审计轨迹,并保存了用户进出系统的历史记录。这对于满足合规要求也很有用。

安全性是分布式系统(包括微服务)的跨切关注点,因此系统中有多种生成这些数据的方式。安全监控可以从系统之外的多种工具中获取数据,这些工具可能是系统所在的基础设施或环境的一部分。不同类型的日志和数据库条目可以作为数据源。然而,这实际上取决于系统的性质。

SLA 监控

具有 SLA 的系统基本上保证某些特性,如性能和可用性。对于基于云的服务,这是一个相当常见的场景。本质上,SLA 监控就是监控系统保证的 SLA。SLA 监控作为服务提供商和消费者之间的合同义务得到执行。

它通常基于可用性、响应时间和吞吐量来定义。用于 SLA 监控所需的数据点可以来自性能端点监控或日志记录以及监控参数的可用性。对于内部应用,许多组织跟踪因服务器宕机而引发的事件数量。对这些事件的根本原因分析(RCA)行动减轻了重复这些问题的风险,并有助于满足 SLA。

对于内部目的,一个组织还可能跟踪导致服务失败的事件的数量和性质。学习如何快速解决这些问题或完全消除这些问题有助于减少停机时间并满足 SLA。

审计敏感数据和关键业务交易

对于任何法律义务或合规原因,系统可能需要记录系统内用户活动的审计轨迹,并记录所有数据访问和修改。由于审计信息性质高度敏感,可能仅向系统中的少数特权且值得信赖的个人披露。审计轨迹可以是安全子系统的组成部分或单独记录。您可能需要按照规定或合规规范以特定格式传输和存储审计轨迹。

最终用户监控

在终端用户监控中,跟踪和记录系统功能和/或终端用户对整体系统的使用情况。使用情况监控可以使用各种用户跟踪参数来完成,例如使用的功能、完成指定用户关键交易所需的时间,甚至强制配额。强制配额是对终端用户在系统使用方面的约束或限制。通常,各种按使用付费的服务使用强制配额;例如,免费试用,您可以上传的文件大小最多为 25 MB。此类监控的数据源通常以日志和跟踪用户行为的形式收集。

故障排除系统故障

系统的终端用户可能会遇到系统故障。这可能表现为系统故障或用户无法执行某些活动的情况。这类问题可以通过系统日志进行监控;如果没有,终端用户将需要提供详细的信息报告。此外,有时服务器崩溃转储或内存转储可以非常有帮助。然而,在分布式系统中,理解故障的确切根本原因将会有点困难。

在许多监控场景中,仅使用一种监控技术是无效的。使用多种监控技术和工具进行诊断会更好。特别是,监控分布式系统相当具有挑战性,需要来自各种来源的数据。除了正确分析情况并决定行动点外,我们还必须考虑监控的整体视角,而不仅仅是关注一种系统视角。

现在我们对一般用途监控需要做什么有了更好的了解,让我们重新审视微服务视角。因此,我们将讨论微服务架构风格所呈现的不同监控挑战。

监控挑战

微服务监控面临着不同的挑战。可能会出现一种情况,其中一个服务可能依赖于另一个服务,或者客户端向一个服务发送请求,响应来自另一个服务,这会使操作变得复杂;因此,扩展微服务在这里将是一项具有挑战性的任务。同样,在实施大型企业级微服务应用时,流程实现,比如 DevOps,也会是一项具有挑战性的工作。因此,让我们在本节中讨论这些挑战。

规模

一个服务可能依赖于由各种其他微服务提供的功能。这导致了复杂性,这在.NET 单体系统中并不常见。对所有的这些依赖进行监控相当困难。随着规模的增加,另一个问题是变化率。随着持续部署和基于容器的微服务的进步,代码始终处于可部署状态。容器仅存活几分钟,如果不是几秒钟。虚拟机也是如此。虚拟机的生命周期大约是几分钟到几个小时。

在这种情况下,测量每分钟的常规信号,如 CPU 使用率和内存消耗,是没有意义的。有时,容器实例甚至可能在一分钟内就不再存活。在一分钟内,容器实例可能已经被销毁。这是微服务监控的一个挑战。

DevOps 心态

传统上,服务或系统一旦部署,就由运维团队拥有和照顾。然而,DevOps 打破了开发者和运维团队之间的隔阂。它带来了许多实践,如持续集成、持续交付以及持续监控。随着这些新实践的诞生,也带来了新的工具集。

然而,DevOps 不仅仅是一套实践或工具;更重要的是,它是一种心态。改变人们的心态始终是一个困难和缓慢的过程。微服务监控也需要类似的思维转变。

随着服务自主性的出现,开发团队现在必须拥有服务。这也意味着他们必须解决开发问题,并关注所有服务的运营参数和 SLA。开发团队不会仅仅通过使用最先进的监控工具就一夜之间转型。这对运维团队也是如此。它不会一夜之间成为核心平台团队(或你喜欢的任何花哨的名字)。

为了使微服务对组织、开发者和运维团队来说成功且有意义,团队需要互相帮助理解自己的痛点,并朝着相同的方向思考,即他们如何共同为业务创造价值。没有服务的监控,监控是无法进行的,这是开发者团队可以提供帮助的地方。同样,没有运维团队的帮助,警报和操作指标的设置以及运行手册的编写也不会发生。这是提供微服务监控解决方案中的一个挑战。

数据流可视化

市场上存在许多用于数据流可视化的工具。其中一些包括 AppDynamics、New Relic 等等。这些工具能够处理多达 100 个甚至更多的微服务的可视化。然而,在拥有数千个微服务的大型环境中,这些工具无法处理可视化。这是微服务监控中的一个挑战。

监控工具的测试

我们信任监控工具,因为我们相信它们能够描绘出我们微服务实现的整体真实情况。然而,为了确保它们始终符合这一理解,我们必须测试监控工具。在单体应用实现中,这从来不是个挑战。然而,当涉及到微服务时,为了监控目的,需要可视化微服务。这意味着生成虚假/合成的交易和时间,并利用整个基础设施,而不是服务于客户。因此,监控工具的测试是一项成本高昂的事务,并且在微服务监控中提出了重大挑战。

监控策略

在本节中,我们将探讨使微服务可观察的监控策略。通常,为了创建一个定义明确且全面的监控解决方案,会实施以下策略或更多策略。

应用程序/系统监控

这种策略也被称为基于框架的策略。在这里,应用程序,或者在我们的案例中是微服务,本身在执行上下文中生成监控信息。应用程序可以根据应用程序数据中的阈值或触发点进行动态配置,从而生成跟踪语句。也可以有一个基于探针的框架(如.NET CLR,它提供了获取更多信息钩子的功能)来生成监控数据。因此,有效的仪器点本身可以嵌入到应用程序中,以促进这种类型的监控。此外,微服务托管的基础设施也可以触发关键事件。这些事件可以被同一主机上的监控代理监听和记录。

真实用户监控

这种策略基于真实最终用户的系统交易流程。当最终用户实时使用系统时,可以使用它捕获与响应时间和延迟相关的参数,以及用户遇到的错误数量。

这对于特定的故障排除和问题解决非常有用。使用这种策略,可以捕获系统在服务交互中的热点和瓶颈。有可能记录整个端到端用户流程或交易,以便稍后回放。这种记录的好处是,这些记录的播放可以用于问题的故障排除,以及各种测试目的。

语义监控和合成交易

语义监控策略侧重于业务交易;然而,它是通过使用合成交易来实现的。在语义监控中,正如其名称所暗示的,我们试图模拟最终用户流程。然而,这是以一种受控的方式,并使用模拟数据来完成的,这样你就可以区分流程输出与实际最终用户流程数据。这种策略通常用于服务依赖性、健康检查和系统发生问题的诊断。要实现合成交易,我们需要在规划流程时小心谨慎;同时,我们也需要足够小心,以免对系统造成压力。以下是一个例子:为假的产品目录创建假订单,并观察整个交易在系统中的传播过程中的响应时间和输出。

分析

这种方法特别关注解决系统中的性能瓶颈。这种方法与先前的不同。真实和语义监控关注业务交易或系统的功能方面,并收集相关的数据。相反,分析主要关注系统级或低级信息捕获。其中一些参数包括响应时间、内存或线程。这种方法在应用程序代码或框架中使用探针技术来收集数据。利用分析期间捕获的数据点,相关的 DevOps 团队可以确定性能问题的原因。在生产环境中应避免使用探针进行分析。然而,在生成调用时间等数据时,不会在运行时过载系统,这是完全可行的。一般来说,分析的一个好例子是使用 ASP.NET MiniProfiler 对 ASP.NET MVC 应用程序进行分析,或者甚至使用 Glimpse。

端点监控

使用这种方法,我们暴露一个或多个服务的端点,以发出与该服务本身以及基础设施参数相关的诊断信息。通常,不同的端点专注于提供不同的信息。例如,一个端点可以提供服务的健康状态,而另一个可以提供在该服务执行过程中发生的 HTTP 500 错误信息。这对于微服务来说是一个非常有用的技术,因为它本质上将监控从推模型转变为拉模型,并减少了服务监控的开销。我们可以在一定的时间间隔内从这些端点收集数据,构建仪表板并收集操作指标数据。

日志记录

日志记录是由系统、其各种组件或基础设施层提供的一种类型的仪器。在本节中,我们将首先探讨日志记录的挑战,然后讨论解决这些挑战的策略。

日志记录挑战

我们将首先尝试了解微服务中日志管理的问题:

  • 要记录与系统事件、参数以及基础设施状态相关的信息,我们需要持久化日志文件。在传统的.NET 单体中,日志文件保存在应用程序部署的同一台机器上。在微服务的情况下,它们托管在虚拟机或容器上。但虚拟机和容器都是短暂的,这意味着它们不持久化状态。在这种情况下,如果我们使用虚拟机或容器持久化日志文件,我们将丢失它们。这是微服务日志管理中的一个挑战。

  • 在微服务架构中,有许多服务构成了一个事务。让我们假设我们有一个订单放置事务,其中服务 A、服务 B 和服务 C 参与该事务。如果,比如说,服务 B 在事务期间失败,我们将如何理解和捕获这个失败在日志中?不仅如此,更重要的是,我们将如何理解特定实例的服务 B 已经失败,并且它参与了所述事务?这种情况为微服务带来了另一个挑战。

日志记录策略

到目前为止,在本节中,我们已经讨论了日志、其挑战以及为什么我们应该实施日志记录。同时进行多次调用是可能的,因此当我们实施日志记录时,我们应该以这种方式实施,以便我们知道已记录事务的确切来源。我们将采用关联 ID 进行日志记录。

日志记录与微服务特定无关;它对单体应用也很重要。

要在微服务中实施日志记录,我们可以使用以下章节中讨论的关键日志记录策略。

集中式日志

集中式日志和集中式监控之间存在差异。在集中式日志中,我们记录系统中发生的事件的所有详细信息——它们可能是错误或警告,也可能是为了信息目的——而在集中式监控中,我们监控关键参数,即特定信息。

通过日志,我们可以了解系统中或特定事务实际上发生了什么。我们将拥有关于特定事务的所有详细信息,例如为什么它开始,谁触发了它,它记录了哪些数据或资源,等等。在一个复杂的分布式系统,如微服务中,这实际上是解决整个信息流或错误谜团的关键信息。我们还需要将超时、异常和错误视为我们需要记录的事件。

我们记录的特定事件的信息也应结构化,并且这种结构应在我们系统中保持一致。例如,我们的结构化日志条目可能包含基于级别的信息,以说明日志条目是信息、错误,还是调试信息或已记录为日志事件事件的统计数据。结构化日志条目还必须包含日期和时间,以便我们知道事件发生的时间。我们还应在结构化日志中包含主机名,以便我们知道日志条目确实切来自哪里。我们还应包含服务名称和服务实例,以便我们知道确切是哪个微服务创建了日志条目。

最后,我们还应该在结构化日志格式中包含一条消息,这是与事件相关联的关键信息。例如,对于错误,这可能包括调用堆栈或异常的详细信息。关键是要保持我们的结构化日志格式一致。一致的格式将允许我们查询日志信息。然后,我们可以使用我们的集中日志工具基本搜索特定的模式和问题。在微服务架构中,集中日志的另一个关键方面是使分布式事务更具可追踪性。

在日志中使用关联 ID

关联 ID 是分配给每个事务的唯一 ID。因此,当事务分布在多个服务之间时,我们可以通过日志信息跟踪该事务在不同服务之间的流动。关联 ID 基本上是在服务之间传递。所有处理该特定事务的服务都会接收到关联 ID 并将其传递给下一个服务,依此类推,这样它们就可以将任何与该事务相关的事件记录到我们的集中日志中。这在我们需要可视化和理解跨不同微服务的事务发生情况时非常有帮助。

语义日志

Windows 事件跟踪ETW)是一种结构化日志机制,您可以在日志条目中存储结构化有效负载。这些信息由事件监听器生成,可能包括有关事件的类型化元数据。这仅仅是一个语义日志的例子。语义日志在日志条目中传递额外的数据,以便处理系统可以获取围绕事件的上下文结构。因此,语义日志也被称为结构化日志或类型化日志。

更多信息,请参阅docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-

例如,一个表示订单已下的事件可以生成一个包含项目数量作为整数值、总金额作为小数值、客户标识符作为长整数值以及配送城市作为字符串值的日志条目。一个订单监控系统可以读取有效负载并轻松提取个别值。ETW 是 Windows 的标准、内置功能。

在 Azure 云中,您可以从 ETW 获取日志数据源。微软模式与实践团队开发的语义日志记录应用程序块是一个框架示例,它使全面的日志记录更容易。当您将事件写入自定义事件源时,语义日志记录应用程序块会检测到这一点,并允许您将事件写入其他日志目标,例如磁盘文件、数据库、电子邮件消息等。您可以在使用 .NET 编写并运行在 Azure 网站上的 Azure 应用程序中使用语义日志记录应用程序块。

Azure 云中的监控

在 Azure 或任何云提供商中,都没有针对微服务带来的监控挑战的现成解决方案或产品。有趣的是,可用的开源工具并不多,它们可以与基于 .NET 的微服务一起工作。

我们正在利用微软 Azure 云和云服务来构建我们的微服务,因此查找它所提供的监控能力是有用的。如果您需要管理大约两百个微服务,您可以使用基于微软 Azure 的自定义监控解决方案(主要是 PowerShell 脚本交织而成)。

我们将主要关注以下日志和监控解决方案:

  • 微软 Azure 诊断:这有助于通过资源和活动日志收集和分析资源。

  • Application Insights:这有助于收集我们微服务的所有遥测数据并进行分析。这是一种基于框架的监控方法。

  • 日志分析:日志分析用于分析和显示数据,并提供了对收集日志的可扩展查询能力。

让我们从不同的角度来审视这些解决方案。这种视角将帮助我们可视化我们的基于 Azure 的微服务监控解决方案。一个微服务由以下部分组成:

  • 基础设施层:一个虚拟机或应用程序容器(例如,Docker 容器)

  • 应用程序堆栈层:包括操作系统、.NET CLR 和微服务应用程序代码

每个这些层组件都可以按照以下方式进行监控:

  • 虚拟机:使用 Azure 诊断日志

  • Docker 容器:使用容器日志和 Application Insights 或第三方容器监控解决方案,例如 cAdvisor、Prometheus 或 Sensu

  • Windows 操作系统:使用 Azure 诊断日志和活动日志

  • 微服务应用程序:使用 Application Insights

  • 数据可视化和指标监控:使用日志分析或第三方解决方案,例如 Splunk 或 ELK 堆栈

各种 Azure 服务在其日志条目中包含一个活动 ID。这个活动 ID 是为每个请求分配的唯一 GUID,可以在日志分析期间用作关联 ID。

Microsoft Azure 诊断

Azure 诊断日志使我们能够收集已部署微服务的诊断数据。我们还可以使用诊断扩展从各种来源收集数据。Azure 诊断支持 Web 和 Worker 角色、Azure 虚拟机以及所有 Azure 应用服务。其他 Azure 服务有自己的独立诊断。

启用 Azure 诊断日志并在 Azure 应用服务中探索各种设置非常简单,就像以下截图所示,它是一个切换开关:

图片

Azure 诊断可以从以下来源收集数据:

  • 性能计数器

  • 应用程序日志

  • Windows 事件日志

  • .NET 事件源

  • IIS 日志

  • 基于清单的 ETW

  • 崩溃转储

  • 自定义错误日志

  • Azure 诊断基础设施日志

使用 Azure 存储存储诊断数据

Azure 诊断日志不是永久存储的。它们是滚动日志,即它们会被新的日志覆盖。因此,如果我们想将它们用于任何分析工作,我们必须将它们存储起来。Azure 诊断日志可以存储在文件系统中,或通过 FTP 传输;更好的是,它们可以存储在 Azure 存储容器中。

对于指定的 Azure 资源(在我们的案例中,是托管在 Azure 应用服务上的微服务),指定用于诊断数据的 Azure 存储容器有不同方式。具体如下:

  • CLI 工具

  • PowerShell

  • Azure 资源管理器

  • Visual Studio 2017 与 Azure SDK 2.9 或更高版本

  • Azure 门户

使用 Azure 门户

以下截图展示了通过 Azure 门户配置的 Azure 存储容器:

图片

指定存储账户

另一种指定用于存储特定应用程序诊断数据的存储账户的方法是在ServiceConfiguration.cscfg文件中指定存储账户。这也非常方便,因为在开发期间,您就可以指定存储账户。在开发和生产阶段,也可以指定完全不同的存储账户。在部署过程中,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-failed 请求日志和自定义目录。blob 日志文件的位置在容器字段中指定,blob 的名称在 RelativePath 字段中。AbsolutePath 字段指示文件的位置和名称,就像它在 Azure 虚拟机上存在的那样。

  • WADPerformanceCountersTable:此表包含与配置的性能计数器相关的数据。

  • WADWindowsEventLogsTable:此表包含 Windows 的事件跟踪日志条目。

对于 blob 存储容器,诊断存储模式如下:

  • wad-control-container:这仅适用于 SDK 2.4 及以前版本。它包含控制 Azure 诊断的 XML 配置文件。

  • wad-iis-failedreqlogfiles:这包含来自 IIS-failed 请求日志的信息。

  • wad-iis-logfiles:这包含有关 IIS 日志的信息。

  • custom:这是一个基于由诊断监控器监控的配置目录的定制容器。此 blob 容器的名称将在 WADDirectoriesTable 中指定。

这里有一个值得注意的有趣事实:在这些容器表或 blob 上可以看到的 WAD 后缀,来源于 Microsoft Azure Diagnostics 的先前产品名称,即 Windows Azure Diagnostics。

您可以使用 Visual Studio 中的Cloud Explorer来探索存储的 Azure 诊断数据。

应用洞察的介绍

Application Insights 是微软提供的一种应用性能管理(APM)服务。它是监控基于.NET 的微服务性能的有用服务。它有助于理解单个微服务的内部和操作行为。它不仅关注于检测和诊断问题,还会调整服务性能并了解微服务的性能特征。它是基于框架的监控方法的一个例子。这意味着在微服务的开发过程中,我们将 Application Insights 包添加到我们的微服务的 Visual Studio 解决方案中。这就是 Application Insights 如何为微服务提供遥测数据。这可能并不是每个微服务的理想方法;然而,如果你没有对微服务的监控进行过深思熟虑,它将非常有用。这样,监控将随服务一起提供。

借助 Application Insights,你可以收集和分析以下类型的遥测数据:

  • HTTP 请求率、响应时间和成功率

  • 依赖(HTTP 和 SQL)调用率、响应时间和成功率

  • 来自服务器和客户端的异常跟踪

  • 诊断日志跟踪

  • 页面浏览量、用户和会话数、浏览器加载时间和异常

  • AJAX 调用率、响应时间和成功率

  • 服务器性能计数器

  • 自定义客户端和服务器遥测

  • 通过客户端位置、浏览器版本、操作系统版本、服务器实例、自定义维度等进行的分段

  • 可用性测试

除了前面提到的类型,还有相关的诊断和分析工具可用于通过各种不同的可自定义指标进行警报和监控。凭借其自己的查询语言和可自定义仪表板,Application Insights 为.NET 微服务提供了一个良好的监控解决方案。

其他微服务监控解决方案

现在让我们看看一些流行的监控解决方案,这些解决方案可以用来构建定制的微服务监控解决方案。显然,这些解决方案并非即插即用;然而,它们无疑经过了开源社区的充分验证,并且可以轻松集成到.NET 环境中。

ELK 堆栈的简要概述

正如我们所见,监控的基本工具之一是日志记录。对于微服务,将产生惊人的日志数量,有时甚至对人类来说都不易理解。ELK 堆栈(也称为弹性堆栈)是最受欢迎的日志管理平台。由于其聚合、分析、可视化和监控的能力,它也是微服务监控的良好候选者。ELK 堆栈是一个包含三个不同工具的工具链,即 Elasticsearch、Logstash 和 Kibana。让我们逐一了解它们在 ELK 堆栈中的作用。

Elasticsearch

Elasticsearch 是基于 Apache Lucene 库的全文搜索引擎。该项目是开源的,用 Java 开发。Elasticsearch 支持水平扩展、多租户和集群方法。Elasticsearch 的基本元素是其搜索索引。该索引以 JSON 形式内部存储。单个 Elasticsearch 服务器存储多个索引(每个索引代表一个数据库),单个查询可以搜索多个索引中的数据。

Elasticsearch 可以提供近乎实时的搜索,并且可以以非常低的延迟进行扩展。搜索和结果编程模型通过 Elasticsearch API 公开,并通过 HTTP 提供。

Logstash

Logstash 在 ELK 堆栈中扮演日志聚合器的角色。它是一个日志聚合引擎,它收集、解析、处理并持久化其持久存储中的日志条目。由于基于数据管道的架构模式,Logstash 非常广泛。它作为代理部署,并将输出发送到 Elasticsearch。

Kibana

Kibana 是一个开源的数据可视化解决方案。它旨在与 Elasticsearch 协同工作。您使用 Kibana 来搜索、查看和交互存储在 Elasticsearch 索引中的数据。

这是一个基于浏览器的网络应用程序,允许您执行高级数据分析,并以各种图表、表格和地图的形式可视化您的数据。此外,它是一个零配置应用程序。因此,安装后既不需要任何编码也不需要额外的基础设施。

Splunk

Splunk 是最佳的商用日志管理解决方案之一。它可以轻松处理数以 TB 计的日志数据。随着时间的推移,它增加了许多附加功能,现在已成为运营智能的全面领先平台。Splunk 用于监控众多应用程序和环境。

它在实时监控任何基础设施和应用中发挥着至关重要的作用,并且在识别问题、问题和攻击在影响客户、服务和盈利能力之前是必不可少的。Splunk 的监控能力、特定模式、趋势和阈值等可以设置为 Splunk 要关注的事件。这样,特定的个人就不必手动执行这些操作。

Splunk 在其平台中包含警报功能。它可以实时触发警报通知,以便采取适当的行动,避免应用程序或基础设施停机。

根据配置的警报和操作触发器,Splunk 可以:

  • 发送电子邮件

  • 执行脚本或触发运行手册

  • 创建组织支持或操作票据

通常,Splunk 监控标记可能包括以下内容:

  • 应用程序日志

  • 活动目录更改事件数据

  • Windows 事件日志

  • Windows 性能日志

  • 基于 WMI 的数据

  • Windows 注册表信息

  • 来自特定文件和目录的数据

  • 性能监控数据

  • 脚本输入以从 API 和其他远程数据接口以及消息队列获取数据

警报

与任何监控解决方案一样,Splunk 也有警报功能。它可以配置为根据任何实时或历史搜索模式设置警报。这些警报查询可以定期和自动运行,并且可以通过这些实时或历史查询的结果触发警报。

您可以将 Splunk 警报基于广泛的阈值和趋势情况,例如条件、关键服务器或应用程序错误,或资源利用率的阈值量。

报告

Splunk 可以报告已触发和执行的警报,以及它们是否满足某些条件。Splunk 的警报管理器可以用于根据前面的警报数据创建报告。

摘要

微服务的调试和监控并不简单;这是一个具有挑战性的问题。我们特意使用了“具有挑战性”这个词:对此没有一劳永逸的解决方案。没有单一的工具可以安装并像魔法一样工作。然而,使用 Azure 诊断和 Application Insights,或者使用 ELK 堆栈或 Splunk,您可以提出有助于解决微服务监控挑战的解决方案。实施微服务监控策略,如应用程序/系统监控、真实用户监控、合成事务、集中式日志记录、语义日志块以及在事务性 HTTP 请求中实施关联 ID,是监控微服务实现的有用方法。

在下一章中,我们将看到如何扩展微服务,以及扩展微服务解决方案的解决方案和策略。

第八章:微服务扩展

想象你是一个开发和支持团队的一员,该团队负责开发公司的旗舰产品——TaxCloud。TaxCloud 帮助纳税人自行申报税收,并在成功申报税收后收取一小笔费用。假设你使用微服务开发了此应用程序。现在,假设产品变得流行并获得关注,突然在申报税收的最后一天,你迎来了大量消费者想要使用你的产品并申报税收。然而,你系统的支付服务速度很慢,这几乎使系统崩溃,所有新客户都转向了你的竞争对手的产品。这对你的业务来说是一个失去的机会。

尽管这是一个虚构的场景,但它可能发生在任何业务中。在电子商务中,我们在现实生活中一直经历过这些事情,尤其是在圣诞节和黑色星期五这样的特殊场合。总的来说,它们指向一个主要的重要特征——系统的扩展性。扩展性是任何关键任务系统最重要的非功能性需求之一。为几百个用户提供数百笔交易与为几百万用户提供数百万笔交易是不同的。在本章中,我们将讨论扩展性的一般概念。我们还将讨论如何单独扩展微服务,设计它们时需要考虑什么,以及如何使用不同的模式避免级联故障。到本章结束时,你将了解以下内容:

  • 水平扩展

  • 垂直扩展

  • 扩展性立方模型

  • 使用 Azure 规模集和 Docker Swarm 进行基础设施扩展

  • 通过数据模型缓存和响应缓存扩展服务设计

  • 断路器模式

  • 服务发现

扩展性概述

设计决策影响单个微服务的扩展性。与其他应用程序功能一样,在设计阶段和早期编码阶段做出的决策在很大程度上影响了服务的扩展性。

微服务扩展需要平衡服务及其支持基础设施的方法。服务和其基础设施也需要和谐地扩展。

扩展性是系统最重要的非功能性特性之一,因为它可以处理更多的负载。通常认为,扩展性通常是大规模分布式系统关注的问题。性能和扩展性是系统的两种不同特性。性能涉及系统的吞吐量,而扩展性涉及为更多用户或更多交易提供服务所需的吞吐量。

扩展基础设施

微服务是现代应用程序,通常利用云服务。因此,当谈到可扩展性时,云提供了一定的优势。然而,这也关乎自动化和管理成本。因此,即使在云中,我们也需要了解如何配置基础设施,例如虚拟机或容器,以便在突发流量激增的情况下成功服务于我们的基于微服务应用程序。

现在我们将访问我们基础设施的每个组件,看看我们如何扩展它。最初的向上扩展和向外扩展方法更多地应用于硬件扩展。有了自动扩展功能,你将了解 Azure 虚拟管理器规模集。最后,你将学习在 Docker Swarm 模式下使用容器进行扩展。

垂直扩展(向上扩展)

扩展是一个术语,指的是通过向同一台机器添加更多资源来实现可扩展性。这包括添加更多内存或更高速度的处理器的操作,或者简单地将应用程序迁移到更强大的 macOS 上。

随着硬件的升级,你能够扩展机器的程度是有限的。更有可能的是,你只是在转移瓶颈,而不是解决提高可扩展性的真正问题。如果你向机器添加更多处理器,你可能会将瓶颈转移到内存上。处理能力并不线性地提高你的系统性能。在某个点上,即使你添加更多的处理能力,系统的性能也会稳定下来。向上扩展的另一个方面是,由于只有一个机器在处理所有请求,它也成为了一个单点故障。

总结来说,垂直扩展很容易,因为它不涉及代码更改;然而,这是一种相当昂贵的技巧。Stack Overflow 是那些罕见的基于.NET 的系统之一,它进行了垂直扩展。

水平扩展(向外扩展)

如果你不想垂直扩展,你总是可以水平扩展你的系统。通常,这也被称为向外扩展。谷歌确实使这种方法变得非常流行。谷歌搜索引擎正耗尽廉价的硬件盒子。因此,尽管它是一个分布式系统,但向外扩展帮助谷歌在其早期快速扩展其搜索过程,同时成本较低。大多数时候,常见任务分配给工作机器,它们的输出由执行相同任务的多台机器收集。这种安排也能通过故障生存。要向外扩展,负载均衡技术是有用的。在这种安排中,通常在所有节点集群的前面添加一个负载均衡器。因此,从消费者角度来看,你击中哪台机器/盒子并不重要。这使得通过添加更多服务器来增加容量变得容易。向集群添加服务器可以线性地提高可扩展性。

当应用程序代码不依赖于其运行的服务器时,扩展是成功的策略。如果请求需要在特定的服务器上执行,即如果应用程序代码具有服务器亲和性,那么扩展将会很困难。然而,在无状态代码的情况下,更容易在任何服务器上执行该代码。因此,当在水平扩展的机器或集群上运行无状态代码时,可扩展性得到了提高。

由于水平扩展的性质,这在整个行业中是一种常用的方法。你可以看到许多大型可扩展系统就是这样管理的,例如,谷歌、亚马逊和微软。我们建议你以水平方式扩展微服务。

微服务可扩展性

在本节中,我们将回顾可用于微服务的扩展策略。我们将查看可扩展性的规模立方体模型,如何为微服务扩展基础设施层,以及在微服务设计中嵌入可扩展性。

可扩展性的规模立方体模型

了解可扩展性的一种方法是通过理解规模立方体。在《可扩展性的艺术:现代企业的可扩展网络架构、流程和组织》一书中,马丁·L·艾博特和迈克尔·T·费舍尔将规模立方体定义为观察和理解系统可扩展性的方法。规模立方体也适用于微服务架构:

图片

在这个可扩展性的三维模型中,原点(0,0,0)代表最不可扩展的系统。它假设系统是在单个服务器实例上部署的单体。如图所示,系统可以通过在三个维度上投入适量的努力来进行扩展。为了将系统推向正确的可扩展方向,我们需要做出正确的权衡。这些权衡将帮助您获得系统最高的可扩展性。这将帮助您的系统满足不断增长的客户需求。这由规模立方体模型表示。让我们来看看这个模型的每一个轴,并讨论它们在微服务可扩展性方面的含义。

x 轴的扩展

x轴上进行扩展意味着在负载均衡器后面运行应用程序的多个实例。这是一种在单体应用程序中非常常见的做法。这种方法的缺点之一是,应用程序的任何实例都可以利用为该应用程序提供的所有数据。它也未能解决应用程序的复杂性。

微服务不应该共享全局状态或一种可以被所有服务访问的数据存储。这将会造成瓶颈和单点故障。因此,仅仅在规模立方体的x轴上扩展微服务并不是正确的做法。

现在让我们来看看z轴的扩展。我们跳过y轴扩展是有原因的。

z 轴的扩展

z 轴的缩放是基于一个分割,这个分割基于事务的客户或请求者。虽然 z 轴分割可能或可能不会解决指令、过程或代码的单一性质,但它们经常解决执行这些指令、过程或代码所需数据的单一性质。自然地,在 z 轴缩放中,有一个专门负责应用偏差因素的组件。偏差因素可能是一个国家、请求来源、客户细分或与请求者或请求相关的任何形式的订阅计划。请注意,z 轴缩放有许多好处,例如提高了请求的隔离和缓存;然而,它也遭受以下缺点:

  • 它增加了应用复杂性。

  • 它需要一个分区方案,这在需要重新分区数据时尤其棘手。

  • 它没有解决日益增长的开发和应用复杂性问题。为了解决这些问题,我们需要应用 y 轴缩放。

由于 z 轴缩放的前置性质,它不适合在微服务中使用。

y 轴的缩放

y 轴的缩放是基于将应用程序分解为不同组件的功能分解。Scale Cube 的 y 轴表示通过角色或数据类型、或某个组件在事务中执行的工作来分离责任。为了分割责任,我们需要根据系统组件执行的动作或角色来分割系统组件。这些角色可能基于事务的大块或非常小的一部分。根据角色的规模,我们可以对这些组件进行缩放。这种分割方案被称为 服务或资源导向分割

这非常类似于我们在微服务中看到的情况。我们根据其角色或动作来分割整个应用程序,并根据其在系统中的角色来缩放单个微服务。这种相似性并非偶然;它是设计的结果。因此,我们可以相当肯定地说,y 轴缩放非常适合微服务。

理解Y轴的扩展对于基于微服务架构的系统的扩展非常重要。因此,实际上我们是在说,可以通过根据它们的角色和动作来分割微服务来实现扩展。考虑一个旨在满足一定初始客户需求的订单管理系统;为此,将应用程序拆分为客户服务、订单服务和支付服务等服务将工作得很好。然而,如果需求增加,您可能需要仔细审查现有系统。您可能会发现已经存在的服务的子组件,由于它们在服务以及整个应用程序中扮演着非常特定的角色,因此可以再次分离。这种针对增加的需求/负载而对设计进行回顾可能会触发将订单服务重新拆分为报价服务、订单处理服务、订单履行服务等。现在,报价服务可能需要更多的计算能力,因此与其他服务相比,我们可能会推送更多的实例(其后的相同副本)。

这是在 AFK 规模立方体的三维模型上对微服务进行扩展的近乎真实世界的例子。您可以在一些属于行业的知名微服务架构中观察到这种三维可扩展性和Y轴扩展的服务,例如亚马逊、Netflix 和 Spotify。

可扩展微服务的特性

在“规模立方体”部分,我们主要关注整个系统或应用程序特性的扩展。在本节中,我们将关注单个微服务特性的扩展。一个微服务被认为具有可扩展性和高性能,当它展现出以下主要特性时:

  • 已知的增长曲线:例如,在订单管理系统的情况下,我们需要知道当前服务支持多少订单,以及它们与订单履行服务指标(以每秒请求数衡量)的比例。目前测量的指标被称为基线数据

  • 研究良好的使用指标:流量模式通常揭示客户需求,基于客户需求,可以计算前几节中提到的关于微服务的许多参数。因此,微服务被装备了,监控工具是微服务的必要伴侣。

  • 有效利用基础设施资源:基于定性和定量参数,可以预测资源利用率。这将帮助团队预测基础设施成本并为其制定计划。

  • 能够使用自动化基础设施进行测量、监控和增加容量:基于微服务资源消耗的操作和增长模式,很容易为未来的容量进行规划。如今,随着云弹性,能够规划和自动化容量变得更加重要。本质上,基于云的架构是成本驱动型架构。

  • 已知的瓶颈:资源需求包括每个微服务需要的特定资源(计算、内存、存储和 I/O)。确定这些对于更顺畅的操作和可扩展的服务是至关重要的。如果我们确定了资源瓶颈,它们可以被处理并消除。

  • 具有相同比例的依赖性扩展:这一点不言而喻。然而,你不能只关注一个微服务,而将其依赖项作为瓶颈。一个微服务的可扩展性与其最少的扩展依赖性一样。

  • 具有容错性和高可用性:在分布式系统中,故障是不可避免的。如果你遇到一个微服务实例故障,它应该自动重定向到一个健康的微服务实例。仅仅在微服务集群前放置负载均衡器在这种情况下是不够的。服务发现工具对于满足可扩展微服务的这一特性非常有帮助。

  • 具有可扩展的数据持久机制:对于可扩展的微服务,单个数据存储的选择和设计应该是可扩展和容错的。在这种情况下,缓存和分离读取和写入存储将有所帮助。

现在,当我们讨论微服务和可扩展性时,自然出现的扩展顺序是以下内容:

  • 基础设施的扩展性:微服务在动态和软件定义的基础设施上运行良好。因此,扩展基础设施是扩展微服务的一个基本组成部分。

  • 围绕服务设计进行扩展:微服务设计包括基于 HTTP 的 API 以及一个数据存储,其中存储了服务的本地状态。

扩展基础设施

在本节中,我们将访问微服务基础设施的所有层,并观察它们之间的关系,即每个单独的基础设施层如何进行扩展。在我们的微服务实现中,有两个主要组件。一个是虚拟机,另一个是托管在虚拟机或物理机上的容器。以下图表显示了微服务基础设施的逻辑视图:

图片

使用规模集扩展虚拟机

在 Azure 云中扩展虚拟机非常简单和容易。这正是微服务大放异彩的地方。使用规模集,你可以在短时间内增加相同虚拟机镜像的实例,并且可以根据规则集自动进行。规模集与 Azure 自动扩展集成。

Azure 虚拟机可以以这种方式创建,使得作为一个组,它们即使在请求量增加的情况下也能始终提供服务。在特定情况下,如果这些虚拟机不需要执行工作负载,它们也可以自动删除。这是由虚拟机规模集来处理的。

规模集也与 Azure 中的负载均衡器很好地集成。由于它们被视为计算资源,因此可以与 Azure 的资源管理器一起使用。规模集可以配置为按需创建或删除虚拟机。这有助于以“宠物与牛群”的心态管理虚拟机,这是我们之前在部署章节中看到的。

对于需要动态扩展和缩减计算资源的应用程序,扩展操作在故障域和更新域之间隐式平衡。

使用规模集,您不需要关联独立资源(如 NIC、存储帐户和虚拟机)的循环。即使在扩展时,我们如何确保这些虚拟管理器的可用性?所有这些关注和挑战都已经通过虚拟机规模集得到了解决。

规模集允许您根据需求自动扩展和缩减应用程序。假设有一个 40%的利用率阈值。所以,也许一旦我们达到 40%的利用率,我们就会开始体验到性能下降。在 40%的利用率时,会添加新的 Web 服务器。规模集允许您设置规则,如前几节所述。规模集的输入是一个虚拟机。规模集上的规则表示,在 40%的平均 CPU 使用率下,五分钟内,Azure 将为规模集添加另一个虚拟机。完成此操作后,再次校准规则。如果性能仍然高于 40%,则添加第三个虚拟机,直到达到可接受的阈值。一旦性能低于 40%,它将开始根据流量不活跃等因素删除这些虚拟机,以降低运营成本。

因此,通过实施规模集,您可以构建性能规则,并通过简单地自动添加和删除虚拟机来使应用程序变大以处理更大的负载。一旦这些规则建立,作为管理员,您将无事可做。

Azure 自动扩展会衡量性能并确定何时进行扩展和缩减。它还与负载均衡器和 NAT 集成。现在,它们与负载均衡器和 NAT 集成的理由是因为随着我们添加这些额外的虚拟机,我们将在前面有一个负载均衡器和 NAT 设备。随着请求的不断涌入,除了部署虚拟机外,我们还需要添加一条规则,允许流量被重定向到新实例。规模集的好处在于,它们不仅添加虚拟机,还与基础设施的所有其他组件协同工作,包括网络负载均衡器等。

在 Azure Portal 中,规模集可以被视为单个条目,尽管它包含多个虚拟机。要查看规模集中虚拟机的配置和详细规格,你必须使用 Azure Resource Explorer 工具。这是一个基于网络的工具,可在 resources.azure.com 上找到。在这里,你可以查看你订阅中的所有对象。你可以在 Microsoft.Compute 部分中查看规模集。

使用 Azure 模板存储库构建规模集非常简单。一旦你创建了自定义的 Azure 资源管理器 (ARM) 模板,你还可以根据规模集创建自定义模板。由于范围和空间限制,我们省略了关于如何构建规模集的详细讨论和说明。你可以通过利用在 github.com/gbowerman/azure-myriad 提供的 ARM 模板来遵循这些说明。

可用性集是一项较旧的技术,并且此功能支持有限。Microsoft 建议你迁移到虚拟机规模集以获得更快和更可靠的自动扩展支持。

自动扩展

通过监控解决方案,我们可以衡量基础设施的性能参数。这通常以性能 SLA 的形式出现。自动扩展使我们能够根据性能阈值增加或减少系统可用的资源。

自动扩展功能添加额外的资源以应对增加的负载。它也可以反向工作。如果负载减少,那么自动扩展会减少可用于执行任务的资源数量。自动扩展无需预先配置资源,并以自动化的方式进行。

自动扩展可以以两种方式扩展——垂直(向现有资源类型添加更多资源)或水平(通过创建该类型资源的另一个实例来添加资源)。

自动扩展功能基于两种策略来决定添加或移除资源。一种是基于资源的可用指标或达到某些系统阈值值。另一种策略是基于时间,例如,在印度标准时间上午 9 点到下午 5 点之间,系统需要 30 个 Web 服务器,而不是三个。

Azure 监控每个资源;所有与指标相关的数据都被收集和监控。基于收集的数据,自动扩展做出决策。

Azure Monitor 自动扩展仅适用于虚拟机规模集、云服务和应用程序服务(例如,Web 应用程序)。

使用 Docker Swarm 进行容器扩展

在前面的部署章节中,我们探讨了如何将微服务打包到 Docker 容器中。我们还详细讨论了为什么容器化在微服务世界中是有用的。在本节中,我们将通过 Docker 提升我们的技能,并了解我们如何轻松地使用 Docker Swarm 扩展我们的微服务。

本质上,微服务是分布式系统,需要分布式和隔离的资源。Docker Swarm 提供了容器编排集群功能,使得多个 Docker 引擎可以作为一个单一的虚拟引擎工作。这类似于负载均衡器的功能;此外,如果需要,它还可以创建容器的新实例或删除容器。

你可以使用任何可用的服务发现机制,如 DNS、consul 或 zookeeper 工具,与 Docker Swarm 一起使用。

一群(swarm)是由 Docker 引擎或节点组成的集群,你可以在其中部署你的微服务作为服务。现在,请不要将这些服务与微服务混淆。在 Docker 实现中,服务是一个不同的概念。一个服务是定义在工作节点上要执行的任务。你可能想了解我们上一句话中提到的节点。在 Docker Swarm 的上下文中,节点用于参与集群的 Docker 引擎。一个完整的 swarm 演示是可能的,并且 ASP.NET Core 镜像可以在 GitHub 上的 ASP.NET-Docker 项目中找到(github.com/aspnet/aspnet-docker)。

Azure 容器服务最近已经可用。它是一个使用 DC/OS、Docker Swarm 或 Google Kubernetes 扩展和编排 Linux 或 Windows 容器的良好解决方案。

现在我们已经了解了如何扩展微服务基础设施,接下来让我们在以下章节中回顾微服务设计的可扩展性方面。

服务设计扩展

在这些章节中,我们将探讨在设计或实现微服务时需要关注的组件/问题。随着基础设施扩展负责服务设计,我们才能真正释放微服务架构的潜力,并在将微服务打造成真正的成功故事方面获得大量的商业价值。那么,服务设计中有哪些组件?让我们来看看。

数据持久化模型设计

在传统应用中,我们一直依赖关系型数据库来持久化用户数据。关系型数据库对我们来说并不陌生。它们在 70 年代出现,作为一种以结构化方式存储持久信息的方法,允许你进行查询和执行数据维护。

在当今的微服务世界中,现代应用程序需要在超大规模阶段进行扩展。我们在这里并不建议你完全放弃使用关系型数据库。它们仍然有其有效的用例。然而,当我们在一个数据库中混合读写操作时,会出现复杂性,需要增加可扩展性。关系型数据库强制执行关系并确保数据的一致性。关系型数据库基于众所周知的 ACID 模型。因此,在关系型数据库中,我们使用相同的数据模型进行读写操作。

然而,读操作和写操作的需求相当不同。在大多数情况下,读操作通常需要比写操作更快。读操作还可以使用不同的过滤标准进行,返回单行或结果集。在大多数写操作中,只涉及单行或列,并且通常与读操作相比,写操作需要更长的时间。因此,我们可以在同一数据模型中优化并服务读操作,或者优化并服务写操作。

我们是否可以将基本数据模型分为两半:一半用于所有读操作,另一半用于所有写操作?现在事情变得简单多了,并且可以很容易地使用不同的策略优化这两个数据模型。这对我们的微服务的影响是,它们反过来,对这两种操作都变得高度可扩展。

这种特定的架构被称为通用查询责任分离CQRS)。作为一个自然的结果,CQRS 也在我们的编程模型方面得到了扩展。现在,我们的编程模型中的数据库-对象关系变得简单得多,并且更可扩展。

这带来了扩展微服务实现的基本要素的下一个基本元素:数据的缓存。

缓存机制

缓存是提高应用程序吞吐量的最简单方法。其原理非常简单。一旦数据从数据存储中读取,就尽可能地靠近处理服务器保存。在未来的请求中,数据直接从数据存储或缓存中提供。缓存的本质是尽量减少服务器需要完成的工作量。HTTP 协议本身内置了缓存机制。这也是它能够如此高效扩展的原因。

关于微服务,我们可以在三个级别上进行缓存,即客户端、代理和服务器端。让我们看看每一个。

首先,我们有客户端缓存。在客户端缓存中,客户端存储缓存的结果。因此,客户端负责执行缓存失效。通常,服务器通过使用诸如缓存控制和过期头等机制提供指导,关于数据可以保持多长时间以及何时可以请求新鲜数据。随着浏览器支持 HTML5 标准,有更多的机制可用,例如本地存储、应用程序缓存或 Web SQL 数据库,客户端可以在其中存储更多数据。

接下来,我们转向代理端。许多反向代理解决方案,如 Squid、HAProxy 和 NGINX,也可以作为缓存服务器。

现在,让我们详细讨论服务器端缓存。在服务器端缓存中,我们有以下两种类型:

  • 响应缓存:这对于 Web 应用程序 UI 来说是一种重要的缓存机制,而且说实话,它简单易实现。对于缓存,相关头信息会被添加到微服务提供的响应中。这可以显著提高你的微服务性能。在 ASP.NET Core 中,你可以使用Microsoft.AspNetCore.ResponseCaching包实现响应缓存。

  • 分布式缓存用于持久化数据:由于缓存不需要对任何外部资源进行 I/O 操作,因此分布式缓存可以提高微服务的吞吐量。这具有以下优势:

    • 微服务客户端将获得完全相同的结果。

    • 分布式缓存由持久化存储支持,并作为不同的远程进程运行。因此,即使应用服务器重启或出现任何问题,也不会影响缓存。

    • 源数据存储的请求次数更少。

你可以使用分布式提供程序,如 CacheCow、Redis(用于我们的书Azure Redis Cache)或 Memcache,以集群模式扩展你的微服务实现。

在下一节中,我们将概述 CacheCow 和 Azure Redis Cache。

CacheCow

当你想要在客户端和服务器上实现 HTTP 缓存时,CacheCow 就派上用场了。这是一个轻量级的库,目前支持 ASP.NET Web API。CacheCow 是开源的,并附带 MIT 许可证,可在 GitHub 上找到(github.com/aliostad/CacheCow)。

要开始使用 CacheCow,你需要为服务器和客户端做好准备。重要步骤包括:

  • 在你的 ASP.NET Web API 项目中安装Install-Package CacheCow.Server NuGet 包;这将是你服务器。

  • 在你的客户端项目中安装Install-Package CacheCow.Client NuGet 包;客户端应用程序将是 WPF、Windows Form、控制台或任何其他 Web 应用程序。

  • 创建一个缓存存储。你需要在服务器端创建一个缓存存储,该存储需要一个数据库来存储缓存元数据(github.com/aliostad/CacheCow/wiki/Getting-started#cache-store)。

如果你想使用 memcache,请参考github.com/aliostad/CacheCow/wiki/Getting-started获取更多信息。

Azure Redis Cache

Azure Redis Cache 建立在开源项目Redis(github.com/antirez/redis)之上,这是一个内存数据库,并持久化在磁盘上。根据微软(azure.microsoft.com/en-in/services/cache/):

“Azure Redis Cache 基于流行的开源 Redis 缓存。它为你提供了一个安全、专用的 Redis 缓存,由微软管理,并且可以从 Azure 中的任何应用程序访问。”

在这些步骤的帮助下,使用 Azure Redis Cache 入门非常简单:

  1. 创建一个 Web API 项目——请参阅我们第二章中的代码示例,实现微服务

  2. 实现 Redis——作为一个参考点使用github.com/StackExchange/StackExchange.Redis并安装Install-Package StackExchange.Redis NuGet 包。

  3. 更新你的CacheConnection配置文件(docs.microsoft.com/en-us/azure/redis-cache/cache-web-app-howto#configure-the-application-to-use-redis-cache)。

  4. 然后在 Azure 上发布(docs.microsoft.com/en-us/azure/redis-cache/cache-web-app-howto#publish-the-application-to-azure)。

您也可以使用此模板创建 Azure Redis Cache:

github.com/Azure/azure-quickstart-templates/tree/master/201-web-app-redis-cache-sql-database 关于 Azure Redis Cache 的完整详细信息,请参阅此 URL:

docs.microsoft.com/en-us/azure/redis-cache/

冗余和容错性

我们理解,一个系统处理故障和从故障中恢复的能力并不等同于提供的可扩展性。然而,我们无法否认,它们在系统层面上是密切相关的能力。除非我们解决可用性和容错性的问题,否则构建高度可扩展的系统将具有挑战性。在一般意义上,我们通过向系统的不同部分/组件提供冗余副本来实现可用性。因此,在接下来的章节中,我们将涉及两个这样的概念。

断路器

断路器是电子设备中的一个安全特性,在发生短路的情况下,它会切断电流流动并保护设备,或者防止对周围环境造成进一步损害。这个确切的想法可以应用于软件设计。当一个依赖的服务不可用或不在健康状态时,断路器会阻止调用该依赖服务,并将流量重定向到配置时间段内的备用路径。

在他的著名书籍《发布它!设计和部署生产就绪软件》中,Michael T. Nygard 详细介绍了断路器。以下图中显示了典型的断路器模式:

如图中所示,断路器作为一个具有三个状态的有限状态机。

关闭状态

这是电路的初始状态,描述了正常的控制流程。在这个状态下,有一个失败计数器。如果在这个流程中发生 OperationFailedException,失败计数器增加 1。如果失败计数器持续增加,意味着电路遇到更多的异常,达到设定的失败阈值,断路器将转换到开启状态。但如果调用成功且没有任何异常或失败,失败计数器将被重置。

开启状态

在开启状态,电路已经跳闸,并开始计时。如果达到超时并且电路仍然失败,代码流程将进入半开状态。

半开状态

在半开状态,状态机/断路器组件重置超时计数器,并再次尝试打开电路,重新启动状态变化到开启状态。然而,在这样做之前,它试图执行常规操作,比如对依赖项的调用;如果成功,那么断路器组件不会改变状态到开启状态,而是将状态改为关闭。这样,操作的正常流程就可以发生,电路再次关闭。

对于基于.NET 的微服务,如果您想实现断路器和一些容错模式,有一个名为 Polly 的优秀库,它以 NuGet 包的形式提供。它附带详细的文档和代码示例,并且还有一个流畅的接口。您可以从 www.thepollyproject.org/ 或通过在 Visual Studio 的包管理器控制台中执行 install--Package Polly 命令来添加 Polly

服务发现

对于小型实现,您如何确定微服务的地址?对于任何.NET 开发者来说,答案是我们在配置文件中简单地放置服务的 IP 地址和端口,我们就完成了。然而,当您处理数百或数千个动态配置的服务时,您就有了一个服务位置问题。

现在如果我们再深入一点,我们正在尝试解决问题的两个部分:

  • 服务注册:这是在某种集中式注册中注册的过程,其中存储了所有服务级别的元数据、主机列表、端口和密钥。

  • 服务发现:通过集中式注册组件在运行时与依赖项建立通信是服务发现。

任何服务注册和发现解决方案都需要以下特性,才能使其成为微服务服务发现问题的解决方案:

  • 集中式注册本身应该具有高可用性

  • 一旦特定的微服务启动,它应该自动接收请求

  • 解决方案中应存在智能和动态的负载均衡能力

  • 解决方案应该能够监控服务的健康状态和它所承受的负载

  • 服务发现机制应该能够将流量从不良节点重定向到其他节点或服务,而无需停机或对其消费者产生影响。

  • 如果服务位置或元数据发生变化,服务发现解决方案应该能够在不影响现有流量或服务实例的情况下应用这些更改。

一些服务发现机制在开源社区中可用。如下所示:

  • Zookeeper:Zookeeper(zookeeper.apache.org/)是一个集中式服务,用于维护配置信息和命名,提供分布式同步,并提供组服务。它用 Java 编写,强一致性(CP),并使用 Zab(www.stanford.edu/class/cs347/reading/zab.pdf)协议在集群中协调更改。

  • Consul:Consul 使得服务能够通过 DNS 或 HTTP 接口简单地注册自己并发现其他服务。它还注册外部服务,如 SaaS 提供商。它还充当一个以键值形式存在的集中式配置存储。它还具有故障检测属性。它基于对等八卦协议。

  • Etcd:Etcd 是一个高度可用的键值存储,用于共享配置和服务发现。它受到 Zookeeper 和 Doozer 的启发。它用 Go 编写,使用 Raft(ramcloud.stanford.edu/wiki/download/attachments/11370504/raft.pdf)进行共识,并具有基于 HTTP-plus JSON 的 API。

摘要

可扩展性是追求微服务架构风格的关键优势之一。我们探讨了微服务可扩展性的特点。我们讨论了可扩展性立方体模型以及微服务如何在y轴上通过系统的功能分解进行扩展。然后,我们通过扩展基础设施来处理扩展问题。在基础设施部分,我们研究了 Azure Cloud 强大的扩展能力,利用 Azure 扩展集和容器编排解决方案,如 Docker Swarm、DC/OS 和 Kubernetes。

在本章的后期阶段,我们专注于通过服务设计进行扩展,并讨论了我们的数据模型应该如何设计。我们还讨论了某些考虑因素,例如在设计高可扩展性的数据模型时采用分割 CQRS 风格模型。我们还简要提到了缓存,特别是分布式缓存,以及它是如何提高系统吞吐量的。在最后一节中,为了使我们的微服务具有高度可扩展性,我们讨论了断路器模式和发现机制,这对于微服务架构的可扩展性至关重要。

在下一章中,我们将探讨微服务的反应性及其特点。

第九章:反应式微服务的介绍

我们现在已经清楚地理解了基于微服务的架构以及如何利用其力量。到目前为止,我们已经详细讨论了该架构的各个方面,例如通信、部署和安全。我们还研究了微服务在需要时如何协作。现在,通过在它们中引入反应式编程方面,让我们将微服务的有效性提升到下一个层次。我们将涵盖以下主题:

  • 理解反应式微服务

  • 流程映射

  • 反应式微服务的通信

  • 处理安全

  • 管理数据

  • 微服务生态系统

理解反应式微服务

在我们深入反应式微服务之前,让我们看看“反应式”这个词的含义。为了被认为是反应式的,一个软件组件必须具备某些基本属性。这些属性包括响应性、弹性、可伸缩性,最重要的是,以消息驱动。我们将详细讨论这些属性,并探讨它们如何使微服务成为满足大多数企业需求更强的候选者。

响应性

不久前,业务赞助商在需求收集会议中讨论的一个关键要求是保证几秒钟的响应时间。例如,一个 T 恤定制打印网店,你可以上传图片,然后将其渲染到所选的服装上。向前推进几年——我可以为此作证——如果任何网页加载时间超过几秒钟,我们将关闭浏览器窗口。

用户今天期望几乎瞬间的响应。但除非你编写的代码遵循某些标准以提供预期的性能,否则这是不可能的。总会有许多不同的组件协同合作和协调以解决某些业务问题。因此,每个组件预期返回结果的时间已经减少到现在的毫秒级。此外,当涉及到响应时间时,系统必须表现出一致性和性能。如果你有一个在定义时间段内表现出可变响应时间的服务,那么这是你系统中即将出现问题的迹象。你迟早必须处理这个负担。毫无疑问,在大多数情况下,你将设法解决这个问题。

然而,挑战比表面上的更大。任何这样的特性都需要检查设计中可能存在的问题。这可能是对另一个服务的某种依赖,服务中同时执行太多功能,或者同步通信在某些时候阻塞了工作流程。

弹性

在分布式计算的喧嚣声中,当系统的一个或多个组件出现故障时,用户期望从这样的系统中得到什么?单个故障会导致灾难性的多米诺骨牌效应,导致整个系统失败吗?或者系统会优雅地、在预期的时限内从这种事件中恢复过来?在这种情况下,最终用户不应受到影响,或者系统至少应将影响降至最低,确保用户体验不受影响。

反应式微服务将微服务的概念提升到了新的水平。随着微服务数量的增加,它们之间的通信需求也在增加。不久,跟踪一打其他服务的列表、在它们之间编排级联事务,或者只是向一组服务生成通知的任务,将变得具有挑战性。在本章的范围内,级联的概念比事务本身更重要。它可能不仅仅是通知外部系统的需求,而是基于某些过滤标准。

挑战在于企业级基于微服务的系统通常会远远超出几个微服务。这种规模和复杂性的全面图景在这里的章节中无法完全展现。在这种情况下,跟踪一组微服务并与它们通信的需求可能会迅速变得令人头疼。

如果我们能从单个微服务中移除向其他微服务传达事件的职责呢?这个问题的另一个方面可能是生态系统中的服务从跟踪中获得的自由。为了做到这一点,你需要跟踪它们的位置。只需添加认证,你就能轻易陷入一个你从未签约的混乱之中。

解决方案在于设计上的改变,即将跟踪事件或向他人传达事件的职责从单个微服务中移除。

在从单体应用过渡到微服务风格的架构时,我们了解到它们是隔离的。通过接口识别,我们将模块隔离成独立的服务集合,这些服务拥有自己的数据,并且不允许其他微服务/进程直接访问它们。我们通过满足单一业务功能并关注其数据和封装的业务功能等方面实现了自治。异步性是我们为微服务实现的另一个特性,以便能够进行非阻塞调用。

自主

从一开始,我们就强烈倡导正确地隔离微服务。在第二章“实现微服务”中,我们简要提到了一个概念——接口识别。在成功实施微服务风格的架构时,我们从中获得了许多好处。我们可以安全地断言,隔离是这里的基本要求之一。然而,成功实施隔离的好处远不止于此。

对于微服务来说,实现自主性非常重要,否则我们的工作将是不完整的。即使实现了微服务架构,如果某个微服务的故障导致其他服务延迟或产生多米诺效应,这意味着我们在设计中遗漏了某些内容。然而,如果微服务隔离做得正确,再加上对特定微服务要执行的功能的正确分解,那么其余的设计就会自行到位,以处理任何类型的解决冲突、通信或协调。

执行此类编排所需的信息主要取决于服务本身的良好定义的行为。因此,对定义良好的微服务的消费者来说,不需要担心微服务失败或抛出异常。如果在规定的时间内没有响应,只需再次尝试即可。

消息驱动:反应式微服务的核心

成为消息驱动是反应式微服务的核心。所有反应式微服务都将它们可能生成的事件定义为它们行为的一部分。这些事件可能包含或不包含额外的信息负载,这取决于单个事件的设计。生成此事件的微服务不会关心生成的事件是否被处理。在这个特定服务的范围内,没有关于生成此事件之外的行为定义。范围到此为止。现在,从整个系统的角度来看,其他微服务将根据它们各自的范围来处理这些信息。

这里的区别在于,所有这些生成的事件都可以通过监听它们来异步捕获。没有其他服务在等待阻塞模式下的任何这些服务。任何监听这些事件的人被称为订阅者,监听事件的行为被称为订阅。订阅这些事件的称为观察者,生成这些事件的源服务称为可观察者。这种模式被称为观察者设计模式

然而,在每个观察者上具体实现的练习与我们的设计松耦合微服务的座右铭有些不一致。如果你这样想,那么你戴上了正确的思考帽,我们正走在正确的轨道上。在不久的将来,当我们将我们的流程映射为响应式微服务时,我们将看到如何在响应式微服务的世界中实现这一目标。

在我们继续映射我们的流程之前,简要讨论一下与我们的主题相关的模式是很重要的。为了对一条消息采取行动,你首先需要表明你想要观察该类型消息的意图。同时,消息的发起者必须有意向向感兴趣的观察者发布此类消息。因此,至少有一个可观察对象将被一个或多个观察者观察。为了增加一些趣味性,可观察对象可以发布多种类型的信息,而观察者可以观察他们打算采取行动的一个或多个信息。

这种模式并不限制观察者在想要停止监听这些消息时取消订阅。所以,听起来很美,但它的实现是否同样容易?让我们继续前进,亲自看看。

让我们编写响应式代码

让我们检查我们的应用程序,看看它将以响应式编程风格看起来如何。以下图显示了具有响应性且完全由事件驱动的应用程序的流程。在这个图中,服务以六边形表示,事件以方形框表示。以下是整个流程的详细说明:

图片

图中描述的流程描述了客户在搜索到他们想要的商品后下订单的场景。下订单事件被触发到订单服务。对此事件的响应,我们的服务分析诸如订单项目和数量等参数,并将项目可用事件触发到产品服务。从现在开始,有两种可能的结局:要么请求的产品可用且有足够的数量,要么不可用或没有足够的数量。如果项目可用,产品服务会向发票服务触发一个名为生成发票的事件。由于开出发票意味着确认订单,发票上的项目将不再有库存;我们需要注意这一点并相应地更新库存。为了处理这个问题,我们的发票服务进一步向产品服务触发一个名为更新产品数量的事件,并处理这一需求。为了简化,我们不会深入探讨谁将处理邮寄发票事件的细节。

事件通信

前面的讨论可能让你在思考被引发的事件如何完美地映射到相应的微服务的调用;让我们进一步详细讨论这个问题。想象一下,所有被引发的事件都存储在一个事件存储中。存储的事件有一个关联的委托函数,用于处理相应的事件。尽管显示存储只有两列,但它存储了更多信息,例如发布者、订阅者等的详细信息。每个事件都包含触发相应服务所需的全部信息。因此,事件委托可能是一个要调用的服务或应用程序内的一个函数。对于这个架构来说,这并不重要:

图片

安全性

在实现反应式微服务时,有无数种处理安全性的方法。然而,鉴于我们有限的范围,我们将只讨论一种类型。让我们继续讨论消息级安全,看看它是如何实现的。

消息级安全

消息级安全是保护你单个请求消息的最基本方法。在初始认证执行后,根据实现方式,请求消息本身可能包含 OAuth 承载令牌或 JWT。这样,每个请求都会被认证,并且与用户相关的信息可以嵌入到这些令牌中。这些信息可能很简单,比如一个用户名,以及一个表示令牌有效期的过期时间戳。毕竟,我们不想允许令牌在某个时间框架之外被使用。

然而,这里重要的是要注意,你可以自由地以这种方式实现它,以便可以嵌入和利用更多信息用于不同的用途。

可扩展性

这里还有一个方面你需要考虑。在这个令牌中,我们还可以嵌入除了认证信息之外的授权信息。请注意,所有这些信息都包含在频繁传递的令牌中,可能会很快成为负担。我们可以进行必要的更改,确保与授权相关的信息是一次性活动,并且随后根据需要与服务持久化。

当我们决定与各个服务持久化授权相关信息时,我们以这种方式使它们变得灵活。使用各个服务持久化授权信息的任务消除了每次都需要从认证服务获取授权相关数据的需要。这意味着我们可以非常容易地扩展我们的服务。

通信弹性

如果包含所有用户认证数据和授权数据的认证服务变得不可用,会发生什么?这是否意味着整个微服务生态系统都会崩溃,因为所有动作——或者大部分动作——都需要对尝试执行动作的用户进行授权?这不符合微服务架构的领域。让我们看看我们如何处理这种情况。

一种方法是在每个需要它的服务中复制用户授权数据。当授权数据已经与相应服务可用时,将减少通过 JWT(JSON Web Tokens)传输的数据量。这将实现的是,如果我们的 Auth 服务变得不可用,已经认证并访问系统的用户将不会受到影响。由于所有授权数据已经存在于需要验证的各个服务中,业务可以继续进行,没有任何阻碍。

然而,这种方法也有其自身的代价。由于数据需要随时更新以适应所有服务,维护这些数据将变得具有挑战性。为每个服务所需的复制本身就是一个挑战。尽管如此,也有解决这个特定挑战的方法。

而不是在所有微服务中提供这些数据,我们可以在一个中心存储中简单地存储它,并让服务从这个中心存储验证/访问授权相关数据。这将使我们能够构建超出认证服务的弹性。

管理数据

跟踪单个订单的下单过程是容易的。然而,将这个数字乘以每小时数百万个订单的下单和取消,在反应式微服务领域可能会迅速成为一个挑战。挑战在于如何在多个服务之间执行事务。不仅跟踪此类事务很困难,而且它还带来了其他挑战,例如持久化跨越数据库和消息代理的事务。如果在中间某个服务失败导致事务中断的情况下,这种操作的撤销任务可能会更加艰巨。

在这种情况下,我们可以利用事件溯源模式。这是一个强有力的候选方案,尤其是因为我们不寻求通常称为 2PC(两阶段提交)的两阶段提交。我们不是存储事务,而是持久化我们实体的所有状态改变事件。换句话说,我们以实体的形式存储所有改变它们状态的事件,例如订单和产品。当客户端下单时,在正常情况下,我们会将订单持久化到订单表作为一行。然而,在这里我们将持久化整个事件序列,直到订单被接受或拒绝的最终阶段。

参考前图,我们分析了创建订单时生成的事件序列。看看这些事件将如何在这个模式中存储,以及如何从这些事件集中推导出事务。首先,让我们看看数据将如何存储。如以下图所示,单个记录作为行保存。数据一致性在事务后得到确认:

图片

如前图所示,产品服务可以订阅订单事件并相应地更新自己。从这个方法中可以衍生出许多好处,例如:

  • 由于事件正在持久化,识别事务的挑战与维护数据库完整性的任务分离了

  • 有可能在任何给定时间点找到系统的确切状态

  • 使用这种方法更容易迁移单体应用

  • 有可能回到过去,识别任何可能的问题

以下图像展示了我们的订单订单详情表(s)在订单服务视图下的情况:

图片

除了所有的好处之外,还有一些缺点。其中最重要的一点是如何查询事件存储。要重建给定业务实体在特定时间点的状态,需要一些复杂的查询。除此之外,还需要一个学习曲线来掌握事件存储替代数据库并推导实体状态的概念。查询复杂性可以通过 CQRS 模式轻松处理。然而,这超出了本章的范围。值得注意的是,事件源模式和 CQRS 在响应式微服务之后值得有单独的章节。

微服务生态系统

如前几章所讨论的,当我们采用微服务时,我们需要为即将到来的大变化做好准备。到目前为止,我们关于部署、安全和测试的讨论可能会让你现在开始考虑接受这个事实。与单体应用不同,采用微服务需要你事先做好准备,以便你开始构建与之相关的基础设施,而不是在之后。从某种意义上说,微服务在一个完整生态系统中蓬勃发展,从部署到测试、安全性和监控,一切都已安排妥当。接受这种变化的回报是巨大的。当然,要做出所有这些改变肯定会有成本。然而,与其有一个无法上市的产品,不如承担一些成本,设计和开发出能够茁壮成长并在最初几轮发布后不会消亡的东西。

编码响应式微服务

现在,让我们尝试总结一下,看看它在代码中实际上看起来如何。我们将使用 Visual Studio 2017 来完成此操作。第一步是创建一个反应式微服务,然后我们将继续创建一个客户端来消费我们创建的服务。

创建项目

现在,我们将继续创建我们的反应式微服务示例。为了做到这一点,我们需要创建一个 ASP.NET Web 应用程序类型的项目。只需按照以下步骤操作,你应该能够看到你的第一个反应式微服务在行动:

  1. 启动 Visual Studio。

  2. 通过导航到文件 | 新建 | 项目创建一个新的项目。

  3. 从已安装的模板中,选择.NET Core | ASP.NET Core Web 应用程序。

  4. 将其命名为FlixOne.BookStore.ProductService并点击确定:

图片

  1. 在“新建 ASP.NET Core Web 应用程序”窗口中,选择.NET Core 和 ASP.NET Core 2.0,然后选择 Web 应用程序(模型-视图-控制器),然后单击确定:

图片

如果你启用了容器,你可以为 Windows 启用 Docker 支持。

  1. 确保已选择 C#7.1;为此,在解决方案资源管理器中右键单击项目,然后单击属性。在项目属性窗口中,单击生成选项卡,然后滚动到高级。单击高级,然后选择 C# 7.1:

图片

  1. 打开 NuGet 管理器,并将 System.Reactive.Core NuGet 包添加到项目中。确保在屏幕上选择包含预发布版本:

图片

你还必须添加一个 EF core 包;要这样做,请参阅第二章“实现微服务”中的EF Core 迁移部分。

  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文件夹中。

考虑以下代码片段,展示了我们的上下文和持久化类:

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; }
  }
}
//Persistence or repositories, following is the interface
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);
  }
}
//ProductRepository class that implements the IProductRepository interface
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());
    }
    private IEnumerable<Product> GetProducts()
    {
      var products = (from p in _context.Products.
      Include(p => p.Category)
      orderby p.Name
      select p).ToList();
      return products;
    }
    private Product GetBy(Guid id) => GetProducts().
    FirstOrDefault(x => x.Id == id);
    private void DeleteProduct(Guid productId)
    {
      var product = GetBy(productId);
      _context.Entry(product).State = EntityState.Deleted;
      _context.SaveChanges();
    }
  }
}

我们已经创建了我们的模型。我们的下一步是添加与数据库交互的代码。这些模型帮助我们将数据源中的数据投影到我们的模型中。

对于数据库交互,我们已创建了一个上下文,即ProductContext,它从DbContext派生而来。在之前的某个步骤中,我们创建了一个名为Context的文件夹。

Entity Framework Core 上下文有助于查询数据库。它还帮助我们汇总我们在数据上执行的所有更改,并一次性在数据库上执行它们。在这里,我们不会详细介绍 Entity Framework Core 或上下文,因为它们不属于本章的范围。

上下文从appsettings.json文件中的connectionStrings部分选择连接字符串——键名为ProductConnectionString

您需要更新startup.cs文件以确保您使用的是正确的数据库。我们已经在第二章,实现微服务中讨论了修改appsettings.jsonStartup.cs文件。在更新Startup.cs类时,您需要为项目添加Swashbuckle.AspNetCore NuGet 包以支持 Swagger。

您可以将其命名为以下代码片段中显示的任何名称:

 "ConnectionStrings": 
 {
   "ProductConnection": "Data Source=.;Initial
   Catalog=ProductsDB;Integrated  
   Security=True;MultipleActiveResultSets=True"
 }

应用程序与数据库之间的通信

在我们设置好上下文,并处理应用程序与数据库之间的通信后,让我们继续添加一个仓库以促进我们的数据模型与数据库之间的交互。请参考我们在创建项目部分的第 10 步中讨论的仓库代码。

GetAll的结果标记为IObservable添加了我们寻找的响应式功能。同时,请注意返回语句。

使用这个可观察模型,我们可以像处理其他更简单的集合一样轻松地处理异步事件的流:

 return Observable.Return(GetProducts());

我们现在准备好通过我们的控制器公开功能。在控制器文件夹上右键单击,点击添加新项,然后选择 ASP.NET Core,Web API 控制器类。将其命名为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();
  }
}

最终的结构看起来与以下解决方案资源管理器截图相似:

要创建数据库,您可以参考第二章,实现微服务中的EF Core 迁移部分,或者简单地调用我们新部署的服务中的 Get API。当服务发现数据库不存在时,在这种情况下,Entity Framework Core 代码优先的方法将确保数据库被创建。

我们现在可以继续部署这项服务到我们的客户那里。随着我们的反应式微服务部署完成,我们现在需要一个客户来调用它。

客户端 – 编码实现

我们将使用 AutoRest 的帮助创建一个用于消费我们新部署的反应式微服务的 Web 客户端。让我们为它创建一个控制台应用程序,并添加以下 NuGet 包:Reactive.CoreWebApi.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();
 }  

在这一点上,如果数据库尚未创建,那么它将根据 Entity Framework 的要求自动创建。

我们需要了解从我们的微服务返回的这个列表与常规列表有何不同。答案是,如果这是一个非响应式场景,并且你对列表进行了任何更改,这些更改将不会反映在服务器上。在响应式微服务的情况下,对这样一个列表所做的更改将自动持久化到服务器,而无需手动跟踪和更新更改的过程。

你可以使用任何其他客户端来执行 Web API 调用(例如,RestSharp 或 HttpClient)。

你可能已经注意到,在处理混乱的回调时,我们几乎不需要做任何工作,或者根本不需要做。这有助于保持我们的代码干净且易于维护。在使用可观察者时,当值可用时,是生产者推动这些值。此外,还有一个客户不意识到的区别:你的实现是阻塞的还是非阻塞的。对于客户来说,这一切似乎都是异步的。

现在,你可以专注于重要任务,而不是试图弄清楚下一步要调用什么或者完全错过了哪些调用。

摘要

在本章中,我们将响应式编程的方面添加到了我们的基于微服务的架构中。这种微服务之间通信的消息驱动方法有其权衡。然而,同时,这种方法在进一步推进我们的微服务架构时,往往能解决一些基本问题。事件溯源模式出现并帮助我们克服了 ACID 事务或两阶段提交选项的限制。这个主题需要一本书来专门讨论,将其限制在一个章节中是不公正的。我们使用我们的示例应用程序来了解如何以响应式的方式重构我们的初始微服务。

在下一章中,我们将有一个完整的应用程序供我们探索,并将我们在本书中迄今为止讨论的所有内容整合在一起。

第十章:创建完整的微服务解决方案

在我们理解微服务和它们演变的旅程中,我们经历了各种阶段。我们探讨了导致微服务出现的原因以及利用它们的各种优势。我们还讨论了各种集成技术和测试策略。让我们回顾一下到目前为止我们所讨论的内容:

  • 测试微服务

  • 安全性

  • 监控

  • 扩展性

  • 反应式微服务

微服务之前的架构

微服务并非从一开始就被设计成现在的形式。相反,它们是从其他流行的架构风格逐渐过渡到微服务的。在微服务之前,我们有单体架构和统治企业开发世界的面向服务的架构。

在快速回顾微服务和它们的各种属性和优势之前,让我们深入探讨这两者。

单体架构

单体架构已经存在很长时间了,它导致了具有单个.NET 组件的自包含软件。它包括以下组件:

  • 用户界面

  • 业务逻辑

  • 数据库访问

为了实现自包含而付出的代价是所有组件都是相互关联和相互依赖的。任何模块的微小变化都有可能影响整个软件。由于所有组件以这种方式紧密耦合,因此测试整个应用程序是必要的。此外,由于如此紧密耦合,整个应用程序必须重新部署。让我们总结一下由于采用这种架构风格而面临的全部挑战:

  • 大量相互依赖的代码

  • 代码复杂性

  • 可扩展性

  • 系统部署

  • 新技术的采用

标准化.NET 堆栈的挑战

当涉及到单体架构时,技术的采用并不容易。它带来了一定的挑战。安全性、响应时间、吞吐率和技术的采用就是其中的一些。并不是这种架构风格没有反击的解决方案。挑战在于,在单体架构中,代码的可重用性真的很低或者不存在,这使得技术的采用变得困难。

扩展性

我们还讨论了扩展性是一个可行的选择,但回报递减且成本增加。垂直扩展和水平扩展都有各自的优缺点。垂直扩展似乎更容易开始:投资于 IT 基础设施,如内存升级和磁盘驱动器。然而,回报很快就会达到顶峰。垂直扩展所需的停机时间的不利因素在水平扩展中不存在。然而,超过某个点,水平扩展的成本变得过高。

面向服务的架构

行业中广泛使用的另一种架构是面向服务的架构(SOA)。这种架构是从单体架构的转变,并涉及解决前述部分中提到的一些挑战。首先,它基于服务集合。提供服务是 SOA 的核心概念。

服务是一段代码、程序或软件,它为其他系统组件提供某些功能。这段代码能够直接与数据库交互,或者通过其他服务间接交互。它在一定程度上是自包含的,使得服务可以轻松被桌面和移动应用程序消费。

SOA 相对于单体架构提供的某些明确优势包括:

  • 可重用性

  • 无状态

  • 可扩展性

  • 基于合同

  • 升级能力

微服务风格架构

除了 SOA 的一些明确优势外,微服务还提供了一些额外的差异化因素,使它们成为明显的赢家。在核心上,微服务被定义为完全独立于系统中的其他服务,并在它们自己的进程中运行。独立的属性要求在应用程序设计中有一定的纪律和策略。它们提供的一些好处包括:

  • 清晰的代码边界:这导致了代码更改更加容易。其独立的模块提供了隔离的功能,导致一个微服务的更改对其他微服务的影响很小。

  • 易于部署:如果需要,可以一次部署一个微服务。

  • 技术适应性:上述属性导致了这一备受追求的益处。这使得我们能够在不同的模块中采用不同的技术。

  • 经济可扩展性:这允许我们仅扩展选定的组件/模块,而不是整个应用程序。

  • 分布式系统:这是隐含的,但在这里需要提醒一句。确保你的异步调用得到良好使用,而同步调用不会阻塞整个信息流。合理使用数据分区。我们稍后会提到这一点,所以现在不用担心。

  • 快速市场响应:在竞争激烈的世界中,这是一个明显的优势,因为用户如果对新功能请求或系统内采用新技术反应迟缓,往往会迅速失去兴趣。

微服务中的消息传递

这是需要讨论的另一个重要领域。在微服务中主要使用两种主要类型的消息传递:

  • 同步

  • 异步

单体过渡

作为我们练习的一部分,我们决定将现有的单体应用程序 FlixOne 过渡到微服务风格的架构。我们看到了如何根据以下参数在单体中识别分解候选者:

  • 代码复杂性

  • 技术采用

  • 资源需求

  • 人力资源依赖

除了技术独立性之外,它还提供了在成本、安全和可扩展性方面的明确优势,这也使应用程序更符合业务目标。

转变整个过程的步骤需要你识别出像微服务边界一样的缝隙,沿着这些缝隙你可以开始分离。你必须小心选择正确的参数来挑选缝隙。我们已经讨论了模块依赖性、团队结构、数据库和技术是一些可能的候选者。处理主数据需要特别注意。这更多的是一个选择,即你想要通过独立的服务还是通过配置来处理主数据。你将是判断你场景的最佳裁判。微服务拥有自己的数据库的基本要求是它消除了许多现有的外键关系。这将带来选择你的事务处理策略的智能需求,以保持数据完整性。

集成技术

我们已经探讨了微服务之间的同步和异步通信方式,并讨论了服务的协作风格。这些风格包括请求/响应和基于事件的。尽管请求/响应在本质上看起来是同步的,但事实是,实现方式决定了集成风格的最终结果。另一方面,基于事件的风格则是完全异步的。

当处理大量微服务时,我们利用集成模式来促进微服务之间复杂交互是很重要的。我们探讨了 API 网关以及事件驱动模式。

API 网关为你提供了一系列服务,以下是一些:

  • 路由 API 调用

  • 验证 API 密钥、JWT 令牌和证书

  • 强制使用配额和速率限制

  • 在不修改代码的情况下动态转换 API

  • 设置缓存后端响应

  • 为分析目的记录调用元数据

事件驱动模式通过一些服务发布它们的事件,而另一些服务则订阅这些可用的事件来实现。订阅服务简单地根据事件及其元数据独立于事件发布服务做出反应。发布者不知道订阅者将执行的业务逻辑。

部署

对于企业应用来说,单体部署可能由于多个原因而具有挑战性。拥有一个难以分解的中心数据库,不仅增加了整体挑战,还增加了上市时间。

对于微服务来说,情况非常不同。好处并不仅仅来自于架构是微服务的事实。相反,它是从最初阶段就开始的计划。你不能期望一个企业规模的微服务在没有持续交付CD)和持续集成CI)的情况下得到管理。CI 和 CD 的需求如此之强,以至于没有它们,生产阶段可能永远看不到光明。

CFEngine、Chef、Puppet、Ansible 和 PowerShell DSC 等工具可以帮助你用代码表示基础设施,并让你轻松地使不同的环境完全相同。Azure 在这里可以成为盟友:快速和重复的配置需求可以轻松满足。

与其最接近的竞争对手虚拟机相比,容器可以更有效地满足隔离需求。我们已经探讨了 Docker 作为容器化的热门候选之一,并看到了如何部署它。

测试微服务

我们都知道单元测试的重要性以及为什么每个开发者都应该编写它们。单元测试是验证对构建更大系统有贡献的最小功能的好方法。

然而,测试微服务并不像测试单体那样是常规事务,因为一个微服务可能会与多个其他微服务交互。在这种情况下,我们应该利用对实际微服务的调用来确保整个工作流程运行良好吗?答案是否定的,因为这会使微服务的开发依赖于另一个部分。如果我们这样做,那么拥有基于微服务的架构的全部目的就丧失了。为了解决这个问题,我们将使用模拟和存根方法。这种方法不仅使测试独立于其他微服务,而且使测试数据库变得更加容易,因为我们还可以模拟数据库交互。

使用单元测试测试一个小型的隔离功能或通过模拟外部微服务的响应来测试一个组件,都有其适用范围,并且在该范围内工作良好。然而,如果你已经在思考如何测试更大的上下文,那么你并不孤单。集成测试和合约测试是测试微服务的下一步。

在集成测试中,我们关注外部微服务,并在测试过程中与之通信。为此,我们模拟外部服务。在合约测试中,我们更进一步,独立测试每一个服务调用,然后验证响应。一个值得花时间研究的重要概念是消费者驱动的合约。请参考第四章,测试策略,以详细了解这一内容。

安全性

在单体架构中,拥有单一认证和授权点的传统方法运作得很好。然而,在微服务的情况下,你需要为每个服务都这样做。这不仅会带来实施上的挑战,还会带来保持同步的挑战。

OAuth 2.0 授权框架和 OpenID Connect 1.0 规范结合在一起可以解决我们的问题。OAuth 2.0 详细描述了授权过程中涉及的所有角色,很好地满足了我们的需求。我们只需确保选择了正确的授权类型;否则,安全将受到威胁。OpenID Connect 身份验证建立在 OAuth 2.0 协议之上。

Azure Active DirectoryAzure AD)是 OAuth 2.0 和 OpenID Connect 规范的提供者之一。在这里理解到,Azure AD 与应用程序的扩展性非常好,并且与任何组织的 Windows Server Active Directory 很好地集成。

如我们已讨论的容器,了解容器非常接近宿主操作系统的内核是很重要且有趣的。确保它们的安全是另一个不容忽视的方面。我们考虑的工具是 Docker,它通过最小权限原则提供了必要的安全保障。

监控

单体世界有其自身的优势。监控和日志记录是那些与微服务相比更容易处理的领域之一。企业系统可能分布的微服务数量可能令人难以置信。

如第一章中“微服务架构的先决条件”部分所讨论的,《微服务简介》,组织应该为这种深刻的变化做好准备。监控框架是这一需求的关键之一。

与单体架构不同,在基于微服务的架构中,监控从一开始就非常必要。监控可以归类的理由有很多:

  • 健康性:我们需要预先知道何时服务故障即将发生。关键参数,如 CPU 和内存利用率,以及其他元数据,可能是即将发生的故障的先兆,或者是需要修复的服务中的缺陷。想象一下,当几百名现场执行董事试图与潜在客户分享成本时,保险公司的费率引擎过载并停止服务,或者运行缓慢。如今没有人喜欢等待。

  • 可用性:可能存在一种情况,服务可能不需要进行广泛的计算,但服务本身的可用性对整个系统可能至关重要。在这种情况下,我记得依赖于向等待几分钟的监听器发送 ping,然后向系统管理员发送电子邮件。这对于只有一到两个服务需要监控的单一服务系统是有效的。然而,在微服务中,涉及更多的元数据。

  • 性能:对于像银行和电子商务这样的高流量平台,仅仅保证可用性并不能提供所需的服务。考虑到在非常短的时间内(从几分钟到甚至几十秒)汇聚到平台的人数,性能不再是奢侈品。你需要通过数据来了解系统的响应情况,例如正在服务的并发用户数,并将其与后台的健康参数进行比较。这可能使电子商务平台能够在即将到来的假日季节之前决定是否需要升级。为了获得更多销售,你需要服务更多的人。

  • 安全性:在任何系统中,你只能计划到一定程度的弹性。无论系统设计得多好,总会有系统无法承受的阈值,这可能导致连锁反应。然而,拥有一个精心设计的网络安全系统可以轻松避免拒绝服务(DoS)和 SQL 注入攻击。当处理微服务时,这一点在系统之间非常重要。因此,在设置微服务之间的信任级别时,要提前思考和仔细考虑。我看到的默认策略是使用微服务来保护端点。然而,覆盖这一方面可以提高系统的安全性,值得花些时间。

  • 审计:医疗保健、金融和银行是几个对相关服务有最严格合规标准的领域。而且,这在全世界都是一样的。根据你处理的合规类型,你可能需要保留数据一段时间作为记录,以特定格式保留数据以与监管机构共享,甚至与当局提供的系统同步。税收系统也可能是另一个例子。在分布式架构中,你不希望因为丢失与单个交易相关的数据记录集而面临合规失败的风险。

  • 故障排除系统故障:我相信,对于刚开始接触微服务的人来说,这将是长期以来的一个热门话题。我记得最初我尝试排除涉及两个 Windows 服务的场景。我从未想过再次推荐类似的设计。但时代已经改变,技术也是如此。

当向其他客户提供服务时,监控变得更加重要。在当今竞争激烈的世界里,SLA 将是任何交易的一部分,并且与其相关联的成本,无论是成功还是失败。你是否曾经想过,我们是如何轻易地假设微软 Azure 的 SLA 无论如何都会成立的?我已经习惯了它,以至于当客户担心云资源可用性时,我甚至没有眨一下眼,就给出了 99.9%正常运行时间的直接回复。

所以除非你能够自信地与客户达成服务级别的协议(SLA),否则他们不能指望你承诺未来提供相同的 SLA。事实上,没有 SLA 可能意味着你的服务可能不够稳定,无法提供 SLA。

监控挑战

在你成功建立监控系统之前,可能需要解决多个关键点。这些需要被识别并分配解决方案。以下是一些关键点的讨论。

规模

如果你已经拥有一个运行良好的系统,其中几十个微服务成功协调完美和谐的交易,那么你已经赢得了第一场战斗。恭喜!然而,如果你还没有这样做,你必须接入必要的监控部分。理想情况下,这应该是第一步本身的一部分。

组件生命周期

使用虚拟机和容器时,我们需要弄清楚哪些部分值得监控。在你查看监控产生的数据时,其中一些组件可能已经不存在了。因此,明智地选择要监控的信息变得极其重要。

信息可视化

有一些工具,如 AppDynamics 和 New Relic,可以让你可视化多达 100 个微服务的数据。然而,在实际应用中,这仅仅是数量的一小部分。必须明确这些信息的目的,并围绕它设计良好的可视化。这是我们可以选择逆向设计的一个领域。首先,考虑你想要的报告/可视化,然后看看它是如何被监控的。

监控策略

首先,在开始监控时,你可以将不同的常用策略视为解决你问题的方案。一些常用的策略包括:

  • 应用程序/系统监控

  • 真实用户监控

  • 语义监控和合成事务

  • 性能分析

  • 端点监控

只需记住,这些策略中的每一个都是针对特定目的的。虽然其中一个可能在分析事务传播时有所帮助,但另一个可能适合测试目的。因此,在设计整个系统时,选择这些策略的组合非常重要,因为仅仅使用单一策略无法满足需求。

可扩展性

我们已经详细讨论了可扩展性的立方模型,并发现了每个轴向上扩展的含义。请注意,x 轴扩展是通过在多个实例之间使用负载均衡器以及微服务的用户来实现的。我们还看到了基于事务起源的 z 轴扩展存在一些缺点。

在微服务世界中,扩展可以大致分为两个不同的方面:

  • 基础设施

  • 服务设计

基础设施扩展

虚拟机是微服务世界不可或缺的组成部分。作为微软 Azure 平台的一部分,可用的功能使您能够轻松地完成这项看似复杂的任务。

通过与 Azure 自动扩展集成的扩展集功能,我们可以轻松地管理一组相同的虚拟机。

自动扩展允许您为各种支持的参数定义阈值,例如 CPU 使用率。一旦阈值被突破,根据参数是扩展内还是扩展外,扩展集将启动。

这意味着如果扩展集预测需要添加更多虚拟机来应对增加的负载,它将继续这样做,直到阈值恢复正常。同样,如果受管理的资源需求下降,它将决定从扩展集中移除虚拟机。对我来说,这听起来像是网络团队的和平。可以进一步探索自动扩展的选项,因为它能够处理复杂的扩展需求,在扩展内或扩展外时运行数百个虚拟机。

服务设计

在我们的微服务中,我们已经实现了每个微服务的数据隔离。然而,读取和写入数据库的模型仍然是相同的。由于底层的关系数据库强制执行 ACID 模型,这可能会是一笔昂贵的交易。或者说,我们可以稍微修改这种方法,以不同的方式实现数据库的读写操作。

我们可以使用常见的查询责任分离,也称为 CQRS,在我们的微服务中进行有效的设计变更。一旦完成模型级别的分离,我们将能够自由地使用不同的策略来优化读取和写入数据模型。

响应式微服务

在将我们的单体应用程序过渡到微服务风格的架构过程中,我们已经取得了良好的进展。我们还简要地提到了向我们的服务引入响应式特性的可能性。我们现在知道响应式微服务的关键属性是什么:

  • 响应性

  • 弹性

  • 自主

  • 以消息驱动

我们也看到了响应式微服务的益处,在管理微服务之间的通信时,我们所需的工作量减少。这种益处不仅体现在工作量减少,还在于能够专注于执行业务逻辑的核心工作,而不是试图处理服务间通信的复杂性。

绿色田野应用

现在让我们从头开始创建 FlixOne 书店。首先,我们将确定我们的微服务和它们的职能,并识别服务间的交互。

我们的 FlixOne 书店将提供以下一系列功能:

  • 搜索可用的书籍

  • 根据类别过滤书籍

  • 将书籍添加到购物车

  • 修改购物车

  • 从购物车下订单

  • 用户身份验证

服务范围

为了理解这些功能如何映射为不同的微服务,我们首先需要了解支持它们需要什么,什么可以组合成一个微服务。我们将看到数据存储是如何从微服务的视角开始的。

书籍列表微服务

让我们尝试分解通过书籍搜索的第一个功能。为了使用户能够浏览书店中的书籍,我们需要维护一个提供的书籍列表。在这里,我们的第一个候选者被划为一个微服务。书籍目录服务不仅负责搜索可用的书籍,还要维护一个数据存储,该存储将包含所有与书籍相关的信息。该微服务应该能够处理系统中可用的书籍所需的各项更新。我们将称之为书籍目录微服务,并且它将拥有自己的书籍数据存储。

书籍搜索微服务

检查过滤书籍的下一个功能似乎属于书籍目录微服务本身的范畴。然而,话虽如此,让我们通过质疑我们对业务领域的理解来确认这一点。我心中浮现的问题是关于用户执行的所有搜索对服务的影响。因此,书籍搜索功能应该是一个不同的服务吗?在这里,答案在于微服务应该有自己的数据存储。如果将书籍目录和书籍目录搜索功能作为不同的服务,将需要我们在两个不同的位置维护书籍列表,并面临额外的挑战,例如需要同步它们。解决方案很简单:拥有一个单一的微服务,如果需要,可以扩展并负载均衡书籍目录微服务。

购物车微服务

下一个候选者是因亚马逊等在线购物革命而闻名,并由智能手机进一步推动的购物车微服务。它应该允许我们在最终决定结账并支付之前添加或删除书籍。毫无疑问,这应该是一个独立的微服务。然而,这引发了一个有趣的问题,即它是否处理产品的数据存储;为了获取一些基本细节,例如库存可用性,它必须这样做。跨服务访问数据存储是不可能的,因为这是最基本的前提之一。我们问题的答案是服务间通信。一个微服务使用另一个微服务提供的服务是可以接受的。我们将称之为我们的购物车微服务。

订单微服务

下一个要考虑的业务功能是下单。当用户认为他们的购物车中正好有足够的书籍时,他们决定下单。在那个时刻,一些与订单相关的信息必须被确认/传达给其他微服务。例如,在确认订单之前,我们需要从图书目录中确认是否有足够的库存来满足订单。确认之后,应该从图书目录中减少相应数量的项目。在订单成功确认后,购物车也应该被清空。

尽管我们的订单微服务听起来更具有普遍性,并且与微服务之间数据不共享的规则相矛盾,但事实并非如此,正如我们很快就会看到的。所有操作都将保持清晰的边界,每个微服务管理自己的数据存储。

用户身份验证

我们最后的候选者是用户身份验证微服务,它将验证登录到我们的书店的客户提供的用户凭据。这个微服务的唯一目的是确认提供的凭据是否正确,以限制未经授权的访问。对于一个微服务来说,这似乎很简单;然而,我们必须记住这样一个事实,如果决定更改身份验证机制,将这个功能作为任何其他微服务的一部分将对多个业务功能产生影响。这种变化可能以使用基于 OAuth 2.0 授权框架和 OpenID Connect 1.0 身份验证生成的 JWT 令牌并进行验证的形式出现。

以下是为微服务准备的最终候选列表:

  • 图书目录微服务

  • 购物车微服务

  • 订单微服务

  • 用户身份验证微服务

在以下图像中,我们可以可视化四个服务:目录、购物车、订单和身份验证:

图片

同步与异步

在我们开始简要介绍微服务之前,这里有一个重要的观点需要考虑。我们的微服务将相互通信,并且它们可能会依赖于响应来进一步操作。这给我们带来了困境,因为我们经历了从心爱的单体架构中解脱出来并陷入同样可能因故障点而引发系统级联崩溃的境地。

书籍目录微服务

此微服务通过 HTTP API 组件公开了六个主要功能。处理这些功能的所有 HTTP 请求是此 HTTP API 组件的责任。

这些功能包括:

API 资源描述 API 资源描述
GET /api/book 获取可用书籍列表
GET /api/book{category} 获取特定类别的书籍列表
GET /api/book{name} 根据名称获取书籍列表
GET /api/book{isbn} 根据 ISBN 号获取书籍
GET /api/bookquantity{id} 获取目标书籍的可用库存
PUT /api/bookquantity{id, changecount} 增加或减少特定书籍的可用库存数量

以下图像可视化了目录服务的表:

购物车微服务

此微服务将以下功能作为 HTTP 端点公开供消费:

API 资源描述 API 资源描述
POST /api/book {customerid } 将特定书籍添加到客户的购物车中
DELETE /api/book {customerid } 从客户的购物车中删除书籍
GET /api/book{customerid} 获取客户购物车中的书籍列表
PUT /api/empty 删除购物车中当前包含的所有书籍。

以下图像可视化了购物车服务的所有支持表:

订单微服务

此微服务将以下功能作为 HTTP 端点公开供消费:

API 资源描述 API 资源描述
POST /api/order {customerid } 获取客户购物车中的所有书籍并为同一订单创建订单
DELETE /api/order {customerid } 从客户的购物车中删除书籍
GET /api/order{orderid} 获取特定订单中的所有书籍

以下图像展示了订单服务的所有表:

用户身份验证微服务

此微服务将以下功能作为 HTTP 端点公开供消费:

API 资源描述 API 资源描述
GET /api/verifyuser{customerid, password} 验证用户

以下截图显示了认证服务的用户表:

图片

您可以查看应用程序源代码并根据需要进行分析。

摘要

我们希望这本书能够向您介绍微服务风格架构的基本概念,并帮助您通过清晰的概念示例深入理解微服务的细微之处。最终的应用程序可供您仔细查看,并根据自己的节奏分析到目前为止所学到的内容。我们祝愿您在使用本书中学到的技能并将其应用于现实世界挑战时一切顺利。

posted @ 2025-10-23 15:07  绝不原创的飞龙  阅读(30)  评论(0)    收藏  举报