原文:
zh.annas-archive.org/md5/e94fc480b4a73285f00b144d1072c83f译者:飞龙
前言
本书涵盖了软件架构中涉及的最常见的设计模式和框架。它通过提供实际的真实场景,讨论何时以及如何使用每个模式。本书还介绍了 DevOps、微服务、持续集成和云计算等技术流程,以便你能够为你的客户提供一流的软件解决方案。
本书将帮助你理解客户希望从你这里得到的产品。它将指导你解决开发过程中可能遇到的最大问题。它还涵盖了你在基于云的环境中管理应用时需要遵循的“做”和“不做”事项。你将了解不同的架构方法,如分层架构、面向服务的架构、微服务和云架构,并了解如何将它们应用于特定的业务需求。最后,你将使用 Azure 在远程环境或云中部署代码。
本书中的所有概念都将通过实际的应用案例进行解释,其中设计原则在创建安全且稳健的应用时起到关键作用。到本书结束时,你将能够开发和交付高度可扩展的企业级应用,以满足最终客户的业务需求。
值得注意的是,本书不仅将涵盖软件架构师在开发 C#和.NET Core 解决方案时应遵循的最佳实践,还将讨论我们需要掌握的所有环境,以便根据最新的趋势开发软件产品。
本书面向的对象
本书面向的是那些渴望成为架构师或希望使用.NET 堆栈构建企业应用的工程师和高级开发者。需要具备 C#和.NET 的经验。
本书涵盖的内容
第一章,理解软件架构的重要性,解释了软件架构的基础知识。本章将为你面对客户需求,并选择合适的工具、模式和框架提供正确的思维方式。
第二章,功能和非功能需求,指导你在应用开发的初期阶段,即收集用户需求并考虑应用必须满足的所有其他约束和目标。
第三章,使用 Azure DevOps 记录需求,描述了记录需求、错误和其他关于你应用的信息的技术。虽然大多数概念是通用的,但本章重点介绍了 Azure DevOps 的使用。
第四章,决定最佳云解决方案,为您提供了云中可用的工具和资源的广泛概述,特别是关于微软 Azure 的内容。在这里,您将学习如何搜索合适的工具和资源,以及如何配置它们以满足您的需求。
第五章,将微服务架构应用于您的企业应用程序,提供了微服务和 Docker 容器的广泛概述。在这里,您将了解基于微服务的架构如何利用云提供的所有机会,您将看到如何使用微服务来实现云中的灵活性、高吞吐量和可靠性。您将学习如何使用容器和 Docker 在您的架构中混合不同的技术,以及如何使您的软件平台独立。
第六章,在 C#中与数据交互 - Entity Framework Core,详细解释了您的应用程序如何借助对象关系映射(ORMs)和 Entity Framework Core 3.0 与各种存储引擎进行交互。
第七章,如何在云中选择您的数据存储,描述了云中(特别是在微软 Azure 中)可用的主要存储引擎。在这里,您将学习如何选择最佳的存储引擎以实现所需的读写并行性,以及如何配置它们。
第八章,使用 Azure Functions,描述了计算的无服务器模型以及如何在 Azure 云中使用它。在这里,您将学习如何在需要运行某些计算时分配云资源,从而只支付实际的计算时间。
第九章,设计模式和.NET Core 实现,描述了使用.NET Core 3.0 的常见软件模式。在这里,您将了解模式的重要性以及使用它们的最佳实践。
第十章,理解软件解决方案中的不同领域,描述了现代领域驱动设计软件生产方法论,如何使用它来面对需要多个知识领域的复杂应用程序,以及如何利用基于云和微服务的架构。
第十一章,在 C# 8 中实现代码复用,描述了在您的 C# .NET Core 应用程序中最大化代码复用的模式和最佳实践。
第十二章, 《使用 .NET Core 应用服务架构*》,介绍了服务架构,它使您能够将应用程序的功能作为端点暴露在互联网或私有网络上,以便用户可以通过各种类型的客户端与之交互。在这里,您将学习如何使用 ASP.NET Core 实现服务架构端点,以及如何使用现有的 OpenAPI 包来自动化文档。
第十三章, 《展示 ASP.NET Core MVC*》,详细介绍了 ASP.NET Core 框架。在这里,您将学习如何根据第十章《理解软件解决方案中的不同领域*》中描述的领域驱动设计原则来实现基于 Model-View-Controller(MVC)模式的 Web 应用程序。
第十四章, 《C# 8 编程的最佳实践*》,描述了在开发 .NET Core 应用程序时应该遵循的最佳实践。
第十五章, 《使用单元测试用例和 TDD 测试您的代码*》,描述了如何测试您的应用程序。在这里,您将学习如何使用 xUnit 测试 .NET Core 应用程序,并看到如何借助测试驱动设计轻松开发并维护满足您规格的代码。
第十六章, 《使用工具编写更好的代码*》,描述了评估软件质量的指标以及如何使用 Visual Studio 中包含的所有工具来测量它们。
第十七章, 《使用 Azure DevOps 部署您的应用程序*》,描述了如何自动化整个部署过程,从在源代码库中创建新版本,经过各种测试和审批步骤,到在真实生产环境中最终部署应用程序。在这里,您将学习如何使用 Azure Pipelines 自动化整个部署过程。
第十八章, 《理解 DevOps 原则*》,描述了 DevOps 软件开发和维护方法的基本原理。在这里,您将学习如何组织您应用程序的持续集成/持续交付周期。
第十九章, 《在 DevOps 中应用 CI 场景的挑战*》,补充了 DevOps 的连续集成场景描述。
第二十章,软件测试自动化,致力于自动验收测试——即验证整个应用程序版本是否符合约定规格的测试。在这里,您将学习如何使用自动化工具模拟用户操作,以及如何与 xUnit 一起使用这些工具编写验收测试。
要充分利用本书
不要忘记安装 Visual Studio Community 2019 或更高版本。
确保你理解 C# .NET 原则。
下载示例代码文件
您可以从 www.packt.com 的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packt.com 登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载”。
-
在搜索框中输入书名,并遵循屏幕上的说明。
文件下载后,请确保使用最新版本解压缩或提取文件夹,使用以下工具:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
书籍的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Hands-On-Software-Architecture-with-CSharp-8。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包可供选择,地址为 github.com/PacktPublishing/。查看它们!
代码实战
您可以在 bit.ly/2Old2IG 上查看代码实战视频。
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781789800937_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“它们在调用 sb.ToString() 获取最终结果时只复制一次。”
代码块设置如下:
[Fact]
public void Test1()
{
var myInstanceToTest = new ClassToTest();
Assert.Equal(5, myInstanceToTest.MethodToTest(1));
}
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“在解决方案资源管理器中,您可以通过右键单击来选择发布…”。
警告或重要注意事项如下所示。
提示和技巧如下所示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过 customercare@packtpub.com 发送邮件给我们。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一情况。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误表提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能向我们提供位置地址或网站名称。请通过 copyright@packt.com 发送邮件,并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买书籍的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问 packt.com.
第一部分:在现实世界应用中转换客户需求
本节包含本书的前三章。目的是确保你理解如何将用户需求转化为对项目成功至关重要的实际架构需求,这涉及到使用 C#和.NET Core 开发企业应用程序时涉及的各个架构方面和设计考虑。
在第一章《理解软件架构的重要性》中,我们将讨论软件架构以及与.NET Core 和 C#相关方面的意义。本章还将讨论分析软件需求以及为可扩展性和健壮性等原则进行设计的重要性。无论你决定在你的项目中采用何种软件开发周期,分析需求都将帮助你坚持项目的目标。没有这一点,你项目的成功将面临风险。本章将展示一些案例,其中对收集到的需求缺乏理解导致了项目失败。此外,在每个案例中,我们还将提供实际建议,以帮助保护你的软件免受所呈现场景的影响。
一旦你理解了收集系统需求的过程,在第二章《功能和非功能需求》中,我们将引导你思考需求对架构设计的影响。可扩展性、性能、多线程、互操作性以及其他主题将被讨论,包括它们的理论和实践。
为了完成第一部分,在第三章《使用 Azure DevOps 记录需求》中,我们将介绍 Azure DevOps,这是微软目前提供的工具,旨在实现遵循 DevOps 哲学原则的应用程序开发生命周期。它提供了各种有助于你记录和组织软件的优良特性,本章的目的就是概述这些特性。
本节包含以下章节:
-
第一章,《理解软件架构的重要性》
-
第二章,《功能和非功能需求》
-
第三章,《使用 Azure DevOps 记录需求》
第一章:理解软件架构的重要性
现在,软件架构是软件行业中最热门的话题之一,毫无疑问,其重要性在未来将会增长。我们构建的解决方案越复杂、越出色,就越需要伟大的软件架构来维护它们。这就是您决定阅读这本书的原因,也是我们决定编写这本书的原因。
当然,撰写关于这个重要话题并不容易,因为它提供了许多替代技术和解决方案。本书的主要目标不仅仅是构建一个详尽无遗且永不停止的技术和解决方案列表,而且还展示如何将各种技术家族联系起来,以及它们如何在实践中影响可维护和可持续解决方案的建设。
随着用户总是需要在他们的应用程序中添加更多新功能,对如何创建实际有效的企业解决方案的关注度不断提高。此外,由于市场变化迅速,需要频繁交付应用程序版本(版本),这增加了拥有复杂软件架构和开发技术的必要性。
本章将涵盖以下主题:
-
软件开发的历史和软件架构的定义
-
当前成功企业使用的软件流程
-
收集需求的过程
到本章结束时,您将能够确切地了解软件架构的使命是什么。您还将了解 Azure 是什么以及如何在平台上创建您的账户。此外,考虑到这是一个入门章节,您将获得软件流程、模型和其他技术的概述,这些技术将使您能够领导您的团队。
技术需求
本章将指导您如何在 Azure 中创建账户,因此不会提供代码。
软件架构是什么?
如果您今天在阅读这本书,您应该感谢那些决定将软件开发视为工程领域的计算机科学家。这发生在上个世纪,更具体地说,是在六十年代末,当时他们提出我们开发软件的方式与建造建筑的方式非常相似。这就是我们为什么有“软件架构”这个名字。就像建筑的设计一样,软件架构师的主要目标是确保软件应用程序得到良好的实现。但良好的实现需要设计一个优秀的解决方案。因此,在一个专业的开发项目中,您必须做以下事情:
-
定义解决方案的客户需求。
-
设计一个满足那些需求的优秀解决方案。
-
实施设计的解决方案。
-
与您的客户验证解决方案。
-
在工作环境中交付解决方案。
软件工程将这些活动定义为软件开发生命周期。所有理论上的软件开发过程模型(瀑布、螺旋、增量、敏捷等)都与这个周期有关。无论您使用哪种模型,如果在您的项目过程中不执行之前提到的基本任务,您将无法提供可接受的软件解决方案。
设计优秀解决方案的主要观点与本书的目的完全一致。您必须理解,优秀的现实世界解决方案会带来一些基本约束:
-
解决方案需要满足用户需求。
-
解决方案需要按时交付。
-
解决方案需要遵守项目预算。
-
解决方案需要提供高质量。
-
解决方案需要保证安全和有效的未来演变。
优秀的解决方案需要可持续,您必须理解,没有优秀的软件架构就没有可持续的软件。如今,优秀的软件架构既依赖于工具也依赖于环境,以完美地满足用户需求。为了解释这一点,本书将使用微软提供的一些优秀工具:
-
Azure:这是微软的云平台,在这里您将找到它提供的所有组件,用于构建高级软件架构解决方案。
-
Azure DevOps:这是一个应用程序生命周期管理环境,您可以使用最新的软件开发方法来构建解决方案,即 DevOps。
-
C#:这是世界上使用最广泛的编程语言之一。C#在不同的操作系统和环境中的小型设备上运行,直到巨大的服务器。
-
.NET Core:这是一个由微软和.NET 社区在 GitHub 上维护的开源开发平台。
-
ASP.NET Core:这是一个使用.NET Core 开发的开源多平台环境,用于构建 Web 应用程序,并托管在云中或在标准服务器(本地)上。
成为软件架构师意味着理解上述内容以及许多其他技术。本书将引导您踏上旅程,作为在团队中工作的软件架构师,您将使用列出的工具提供最佳解决方案。让我们从创建您的 Azure 账户开始这段旅程。
创建 Azure 账户
Microsoft Azure 是目前市场上最好的云解决方案之一。重要的是要知道,在 Azure 内部,我们将找到一系列组件,这些组件可以帮助我们在 21 世纪解决方案的架构中。
本小节将指导您创建 Azure 账户。如果您已经有了账户,您可以跳过这部分:
- 您可以使用此 URL 访问 Azure 门户:
azure.microsoft.com。在这里,您将找到一个网站,如下所示。您的母语翻译可能已自动设置:
-
一旦你访问了这个门户,就有可能注册。如果你以前从未这样做过,你可以免费注册,这样你就可以使用一些 Azure 功能而无需花费任何费用。
-
一旦你完成了表格,你就可以访问 Azure 面板。正如你在下面的屏幕截图中所看到的,面板显示了一个你可以定制的仪表板,以及一个左侧菜单,你可以在这里设置你将在解决方案中使用 Azure 组件。在这本书中,我们将多次回到这个屏幕截图来设置创建现代软件架构巨大机会的组件:
一旦你创建了 Azure 账户,你就准备好了解软件架构师如何利用 Azure 提供的所有机会来指导团队开发软件。然而,重要的是要记住,软件架构师需要了解的不仅仅是具体技术,因为如今,这个角色是由那些被期望定义软件交付方式的人扮演的。软件架构师不仅架构软件的基础,还决定整个软件开发和部署过程是如何进行的。
软件开发过程模型
作为一名软件架构师,了解目前大多数企业中使用的常见开发过程非常重要。软件开发过程定义了团队中的人们如何生产和交付软件。一般来说,这个过程与一种称为软件开发过程模型的软件工程理论相关联。自从软件开发被定义为工程过程以来,已经提出了许多软件开发的过程模型。让我们看看目前常见的那些。
审查传统的软件开发过程模型
在软件工程理论中介绍的一些模型已经被认为是传统的并且相当过时。本书并不旨在涵盖所有这些模型,但在这里,我们将简要解释一些仍在某些公司中使用的模型。
理解瀑布模型原则
在 2019 年的软件架构书中,这个话题可能看起来有些奇怪,但确实,你仍然可能会发现一些公司仍然将最传统的软件开发过程模型作为软件开发指南。这个过程按顺序执行所有基本任务。任何软件开发项目都包括以下步骤:
-
需求规范
-
软件设计
-
编程
-
测试和交付
让我们来看一下这种表示方法的图示:
通常,使用瀑布模型会导致软件功能版本交付延迟的问题,以及由于最终产品质量差而引起用户不满。
分析增量模型
增量开发是一种试图克服瀑布模型最大问题的方法:用户只能在项目结束时测试解决方案。这个模型的想法是尽早让用户与解决方案互动,以便他们可以提供有用的反馈,这将有助于软件开发过程中的工作。
然而,在这个模型中,有限的增量数量和项目的官僚主义可能会在开发人员和客户之间造成问题:
增量开发周期(https://en.wikipedia.org/wiki/Incremental_build_model)
增量模型被引入作为瀑布方法的替代方案,它减轻了与客户沟通不足相关的问题。对于大型项目来说,更少的增量仍然是一个问题。此外,在增量方法大规模使用的时候,主要在上世纪末,由于需要大量的文档,许多与项目官僚主义相关的问题被报道。这种情况导致了软件开发行业中一个非常重要的运动的兴起——敏捷。
理解敏捷软件开发过程模型
在本世纪初,软件开发被认为是工程领域中最混乱的活动之一。失败的项目数量极高,这一事实证明了需要一种不同的方法来处理软件开发项目所需的灵活性。因此,在 2001 年,敏捷宣言被介绍给世界,从那时起,各种敏捷过程模型被提出。其中一些模型至今仍然非常常见。
请查看此链接了解敏捷宣言:agilemanifesto.org/.
敏捷模型与传统模型之间最大的区别之一是开发人员与客户互动的方式。所有敏捷模型传达的信息是,你越快将软件交付给用户,就越好。这个想法有时会让软件开发人员感到困惑,他们理解为——让我们尝试编码,就这样吧! 然而,敏捷宣言中的一个重要观察结果,很多人在开始使用敏捷时并没有阅读到:
“也就是说,虽然右侧的项目具有价值,但我们更重视左侧的项目。”
– 敏捷宣言,2001 年
软件架构师始终需要记住这一点。敏捷流程并不意味着缺乏纪律。而且,当你使用敏捷流程时,你会明白没有纪律就无法开发出好的软件。另一方面,作为软件架构师,你需要理解“软”意味着灵活性。一个不处理灵活性的软件项目往往会随着时间的推移而失败。
进入 Scrum 模型
Scrum 是一种敏捷的软件开发项目管理模型。该模型源于精益原则,并且无疑是当今软件开发中广泛使用的方法之一。
请查看此链接以获取有关 Scrum 框架的更多信息:docplayer.net/78853722-Scrum-insights-for-practitioners.html。
Scrum 的基础是,你有一个需要在每个敏捷周期中讨论的灵活的用户需求列表,这被称为Sprint。Sprint 目标由由产品负责人、Scrum Master 和开发团队组成的 Scrum 团队确定。产品负责人负责确定那个 Sprint 中将交付的内容的优先级。在 Sprint 期间,这个人将帮助团队开发所需的功能。在 Scrum 流程中领导团队的人被称为 Scrum Master。所有的会议和流程都由这个人执行。
重要的是要注意,Scrum 流程不讨论软件应该如何实现以及将进行哪些活动。因此,你必须再次记住本章开头讨论的软件开发基础。这意味着 Scrum 需要与一个流程模型一起实施。DevOps 是一种可能帮助你结合 Scrum 使用软件开发流程模型的方法。我们将在本书的第十八章中讨论这一点,理解 DevOps 原则。
将方面聚集起来以设计高质量的软件
太棒了!你刚刚开始了一个软件开发项目。现在,是时候利用你所有的知识来交付最好的软件了。可能,你的下一个问题是——我该如何开始? 好吧,作为一个软件架构师,你将是回答这个问题的那个人。并且请确保你的答案将在你领导的每个软件项目中不断演变:
-
定义软件开发流程显然是首先要做的事情。这通常在项目规划过程中完成。
-
此外,另一件非常重要的事情是收集软件需求。无论你决定使用哪种软件开发流程,收集真实用户需求都是一项非常困难和持续的工作的一部分。当然,有一些技术可以帮助你完成这项工作。并且请确保收集需求将帮助你发现软件架构的重要方面。
这些两个活动被大多数软件开发专家视为在开发项目旅程结束时取得成功的关键。作为软件架构师,你需要确保它们发生,这样你在引导团队时就不会有问题。
理解需求收集过程
有不同的方式来表示需求。最传统的方法是在分析开始之前,你必须写出一个完美的规范。敏捷方法建议你一旦准备好开始一个开发周期,就需要编写故事。
记住:你并不是为用户编写需求,而是为你和你的团队编写。用户只需要工作完成!
事实上,无论你决定在项目中采用哪种方法,你都必须遵循一些步骤来收集需求。这就是我们所说的需求工程。
请查看以下关于需求工程过程的图片以获取更多信息:www.slideshare.net/MohammedRomi/ian-sommerville-software-engineering-9th-edition-ch-4。
在这个过程中,你需要确保解决方案是可行的。在某些情况下,可行性分析也是项目规划过程的一部分,在你开始需求收集之前,你已经有了一份完成的可行性报告。所以,让我们检查这个过程的其它部分,这将为你提供大量关于软件架构的重要信息。
练习收集用户需求
有很多方法可以检测用户在特定场景中确切需要什么。一般来说,这可以通过帮助你理解我们所说的用户需求的技巧来完成。这里有一份常见技巧的列表:
-
想象力的力量:如果你是在你提供解决方案的领域中的专家,你可以利用自己的想象力来发现新的用户需求。头脑风暴可以一起进行,以便一组专家可以定义用户需求。
-
问卷调查:这个工具对于检测常见且重要的需求很有用,例如用户数量和种类、系统高峰使用时间以及常用的操作系统(OS)和网页浏览器。
-
访谈:访谈用户有助于你作为架构师发现那些可能问卷和你的想象力无法涵盖的用户需求。
-
观察:没有比和他们一起度过一天更好的方式来理解用户的日常习惯了。
一旦你应用了这些技巧中的一个或多个,你将获得宝贵的信息,即用户的需求。在那个时刻,你将能够分析它们并检测用户和系统需求。
记住:你可以在任何真正需要收集需求的情况下使用这些技术,无论是整个系统还是单个故事。
分析需求
一旦你发现用户需求,就是开始分析需求的时候了。那时,你可以使用以下技术:
-
原型设计:原型设计对于澄清和具体化系统需求非常有用。今天,我们有许多工具可以帮助你模拟接口。一个非常好的开源工具是Pencil Project。你可以在
pencil.evolus.vn/找到更多关于它的信息。 -
用例:如果你需要详细的文档,统一建模语言(UML)用例模型是一个选择。该模型由详细规范和图表组成。Argo UML 是另一个可以帮助你的开源工具:
当你分析系统的需求时,你将能够确切地澄清用户的真正需求。当你不确定将要解决的真实问题时,这非常有帮助,而且比开始编写系统代码要好得多。现在是时候投资于在不久的将来拥有更好的代码了。
编写规范
分析完成后,将其注册为规范是很重要的。这份文档可以使用传统的需求或用户故事来编写,这些在敏捷项目中很常见。
需求规范代表了用户和团队之间的技术合同。这份文档需要遵循一些基本规则:
-
所有利益相关者都需要确切了解技术合同中写的内容,即使他们不是技术人员。
-
文档需要清晰。
-
你需要分类每个需求。
-
使用一个简单的功能来代表每个需求。
-
需要避免歧义和争议。
此外,一些信息可以帮助团队理解他们将要工作的项目的背景。这里有一些关于它的提示:
-
编写一个介绍章节,以全面了解解决方案。
-
创建一个术语表,以便更容易理解。
-
描述解决方案将覆盖的用户类型。
-
编写功能和非功能需求。
-
附加文档,帮助用户理解规则。
如果你决定编写用户故事,一个很好的建议是写简短的句子,代表每个用户在系统中的每个时刻,如下所示:
As <user>, I want <feature>, so that <reason>
这种方法将确切地解释为什么那个功能将被实现。除此之外,你将有一个很好的工具来分析更关键的故事,并优先考虑项目的成功。
审查规范
一旦你写好了规格说明,就是时候与利益相关者确认他们是否同意了。这可以通过审查会议进行,或者可以使用协作工具在线完成。
这时,你需要展示你所收集的所有原型、文档和信息。一旦每个人都同意规格说明,你就可以开始研究实施项目这一部分的最佳方式了。
将设计思维作为有用的工具使用
在你作为软件架构师的职业生涯中,你将发现许多项目,你的客户将带来一个已准备好开发的解决方案。一旦你考虑这是正确的解决方案,这将会相当复杂;大多数情况下,将会有架构和功能错误,这将在未来的解决方案中引起问题。有些情况下,问题更严重——当客户不知道问题的最佳解决方案时。设计思维可以帮助我们解决这个问题。
设计思维是一个允许你直接从用户收集数据的过程,专注于实现解决问题的最佳结果。在这个过程中,团队将有机会发现所有将与系统交互的角色。这将对解决方案产生美妙的影响,因为你可以通过关注用户体验来开发软件,这可以产生美妙的结果。
该过程基于以下步骤:
-
同理心:在这个步骤中,你必须执行实地研究来发现用户的关注点。这是你了解系统用户的地方。这个过程有助于你理解为什么以及为谁开发这款软件。
-
定义:一旦你了解了用户的关注点,就是时候定义他们的需求以解决它们了。
-
构思:需求将提供机会来头脑风暴一些可能的解决方案。
-
原型:这些解决方案可以作为原型来开发,以确认它们是否是好的解决方案。
-
测试:测试原型将帮助你理解与用户真实需求最相关的原型。
你必须理解的是,设计思维可以是一个发现真实需求的绝佳选择。作为一名软件架构师,你致力于帮助你的团队在正确的时间使用正确的工具。
理解可扩展性、健壮性、安全性和性能的原则
识别需求是一项任务,将帮助你理解你将要开发的软件。然而,作为一名软件架构师,你不必只关注该系统的功能需求。理解非功能需求非常重要,并且是软件架构师的一项原始活动。
我们将在第二章中进一步讨论这个问题,功能和非功能需求,但在此阶段,了解可扩展性、健壮性、安全性和性能的原则需要应用于需求收集过程是很好的。让我们看看每个概念:
-
可扩展性:作为软件开发者,全球化给你提供了一个机会,让你的解决方案在世界各地运行。这是很棒的,但作为软件架构师,你需要设计一个提供这种可能性的解决方案。可扩展性是指应用程序在必要时能够增加其处理能力,这是由于正在消耗的资源数量。
-
健壮性:无论你的应用程序有多可扩展,如果不能保证稳定且始终在线的解决方案,你将无法得到任何安宁。对于那种应用程序解决的问题,你任何时候都没有维护的机会,因此健壮性对于关键解决方案来说非常重要。在许多行业中,软件不能停止,当没有人可用时(如夜间、假日等),会有很多常规运行。设计一个健壮的解决方案将给你在软件运行良好的同时生活的自由。
-
安全:这是在需求阶段之后需要讨论的另一个非常重要的领域。每个人都担心安全问题,并且世界各地都在提出处理这一问题的法律。作为软件架构师,你必须明白,安全性需要通过设计来提供。这是应对当前安全社区讨论的所有需求的唯一方法。
-
性能:理解你将要开发的系统的过程可能会给你一个很好的想法,了解你需要付出多少努力才能从系统中获得期望的性能。这个话题需要与用户讨论,以确定你在开发阶段将面临的大部分瓶颈。
值得注意的是,所有这些概念都是世界需要的新一代解决方案的要求。当然,区分优秀软件和卓越软件的将是满足项目要求所做的工作量。
一些情况下,需求收集过程影响了系统结果
本章到目前为止讨论的所有信息,如果你想要按照良好的工程原则设计软件,都是有用的。这次讨论与传统或敏捷方法开发无关,而是专注于专业或业余地构建软件。
此外,了解一些由于缺乏你所阅读的活动而给软件项目带来麻烦的情况是有好处的。以下案例旨在描述出了什么问题以及前面的技术如何帮助开发团队解决问题。在大多数情况下,简单的行动可以保证团队和客户之间更好的沟通,这种简单的沟通流程可以将大问题转化为真正的解决方案。
情况 1 – 我的网站打开那个页面太慢了!
性能是你作为软件架构师在职业生涯中会遇到的最大问题之一。任何软件的这一方面之所以如此有问题,是因为我们没有无限的计算资源来解决问题。此外,计算成本仍然很高,尤其是如果你在谈论具有大量同时用户的软件。
你不能通过编写需求来解决性能问题。然而,如果你正确地编写它们,你不会陷入麻烦。这里的想法是,需求必须展示系统的期望性能。一个简单的句子,描述这一点,可以帮助整个参与项目的团队:
非功能性需求:性能 – 该软件的任何网页至少要在 2 秒内响应。
前一句话只是让每个人(用户、测试人员、开发人员、架构师、经理等等)确信任何网页都有一个目标要实现。这是一个好的开始,但还不够。有了这个,一个既适合开发又适合部署应用程序的良好环境非常重要。这正是.NET Core 能大量帮助你之处。特别是如果你在谈论 Web 应用程序,ASP.NET Core 被认为是当今提供解决方案最快的选项之一。
如果你谈论性能,作为软件架构师的你应该考虑以下章节中列出的技术。值得一提的是,ASP.NET Core 将帮助你轻松使用它们,同时还有一些由微软 Azure 提供的平台即服务(PaaS)解决方案。
理解缓存
缓存是一种避免消耗时间和通常给出相同结果的查询的绝佳技术。例如,如果你正在从数据库中检索可用的汽车型号,数据库中的汽车数量可能会增加,但它们不会改变。一旦你有一个不断访问汽车型号的应用程序,一个好的做法是将这些信息缓存起来。
重要的是要理解缓存存储在后台,并且该缓存由整个应用程序共享(内存缓存)。在这里需要注意的一个点是,当你正在开发一个可扩展的解决方案时,你可以配置一个分布式缓存来使用 Azure 平台解决它。实际上,ASP.NET Core 提供了这两者,因此你可以选择最适合你需求的一个。
应用异步编程
当您开发 ASP.NET Core 应用程序时,您需要记住这个应用程序需要设计为许多用户同时访问。异步编程让您可以简单地做到这一点,提供了 async 和 await 关键字。
这些关键字背后的基本概念是,async 允许任何方法在调用它的线程之外的不同线程中运行。另一方面,await 允许您在不阻塞调用它的线程的情况下同步异步方法的调用。这种易于开发的模式将使您的应用程序在没有性能瓶颈的情况下运行,并具有更好的响应性。本书将在第二章 功能和非功能需求 中更详细地介绍这一主题。
处理对象分配
避免性能不足的一个非常好的建议是了解垃圾收集器是如何工作的。垃圾收集器是当您完成使用后自动释放内存的引擎。由于垃圾收集器具有复杂性,因此这个主题有一些非常重要的方面。
一些类型的对象不会被垃圾收集器收集。这个列表包括任何与 I/O 交互的对象,例如文件和流。如果您不正确地使用 C# 语法来创建和销毁这类对象,您将会有内存泄漏,这会降低您的应用程序性能。
与 I/O 对象交互的错误方式:
System.IO.StreamWriter file = new System.IO.StreamWriter(@"C:\sample.txt");
file.WriteLine("Just writing a simple line");
与 I/O 对象交互的正确方式:
using (System.IO.StreamWriter file = new System.IO.StreamWriter(@"C:\sample.txt"))
{
file.WriteLine("Just writing a simple line");
}
尽管前面的做法对于 I/O 对象是强制性的,但强烈建议您在所有可处置对象上继续这样做。这将有助于垃圾收集器,并确保您的应用程序以正确的内存量运行。
您需要了解的另一个重要方面是垃圾收集器收集将干扰您应用程序性能的对象所花费的时间。因此,避免分配大对象。这可能会在垃圾收集器完成其任务时给您带来麻烦。
获取更好的数据库访问
最常见的性能阿基里斯之踵之一是数据库访问。这仍然是一个大问题,原因是在编写查询或 lambda 表达式从数据库获取信息时缺乏关注。本书将在第六章 在 C# 中与数据交互 – Entity Framework Core 中介绍 Entity Framework Core,但了解选择什么、从数据库中读取哪些正确数据信息以及过滤列和行对于希望提供性能的应用程序至关重要。
好的一点是,与缓存、异步编程和对象分配相关的最佳实践完全适用于数据库环境。这只是一个选择正确模式以获得高性能软件的问题。
情况 2 – 用户的需求没有得到适当的实现
随着技术在各个领域的广泛应用,提供用户所需的确切内容变得越来越困难。也许这句话听起来有些奇怪,但您必须理解,开发者通常学习开发软件,但他们很少学习如何满足特定领域的需求。当然,学习如何开发软件并不容易,但理解特定领域的需求更加困难。如今,软件开发为所有可能的行业提供软件。问题是,开发者,无论是软件架构师还是其他角色,如何才能足够地进化,以提供他们负责领域的软件?
收集软件需求无疑将帮助您完成这项艰巨的任务。此外,编写它们将使您理解并组织系统的架构。有几种方法可以最小化实现与用户真正需求不符的风险:
-
通过原型设计界面以更快地理解用户界面
-
设计数据流以检测系统与用户操作之间的差距
-
定期会议以了解当前需求并确保与增量交付保持一致
再次强调,作为软件架构师,您将需要定义软件的实现方式。大多数情况下,您可能不会亲自编写代码,但您将始终是负责这一点的责任人。因此,一些技术可以用来避免错误的实现:
-
与开发者一起审查需求,以确保他们理解他们需要开发的内容
-
代码审查以验证预定义的代码标准
-
会议以消除障碍
案例 3 – 系统的可用性不符合用户需求
可用性是软件项目成功的关键。软件的展示方式和解决问题的方法可以帮助用户决定是否想要使用它。作为软件架构师,您必须牢记,如今提供具有良好可用性的软件是强制性的。
本书无意涵盖可用性的基本概念。但了解谁将使用该软件是满足正确用户需求的一个好方法。正如本章前面所讨论的,设计思维在这方面可以帮助您很多。
理解用户将帮助您决定软件是否将在网页、手机上运行,甚至是在后台运行。这种理解对于软件架构师来说非常重要,因为如果正确地映射出谁将使用这些元素,系统的元素将得到更好的展示。
另一方面,如果您不关心这一点,您只需交付能够正常工作的软件即可。这可能在短期内是好的,但它不会完全满足使人们要求您设计软件的真实需求。您必须牢记这些选项,并理解好的软件是为了在许多平台和设备上运行而设计的。
您会很高兴地知道 C#是一个令人难以置信的跨平台选项。因此,您可以开发解决方案,在 Linux、Windows、Android 和 iOS 上运行您的应用程序。您可以在大屏幕、平板电脑、手机甚至无人机上运行您的应用程序!您可以将应用程序嵌入到用于自动化的板上或在 HoloLens 中用于混合现实。软件架构师必须思想开放,以便设计出用户真正需要的精确方案。
案例研究 – 检测用户需求
本书的研究案例将带您踏上为名为World Wild Travel Club(WWTravelClub)的旅行社创建软件架构的旅程。本案例研究的目的在于让您理解每一章中解释的理论,并在阅读本书的过程中,开发一个使用 Azure、Azure DevOps、C#、.NET Core、ASP.NET Core 和其他本书将介绍的技术构建的企业应用程序。
案例研究 – 介绍 World Wild Travel Club
World Wild Travel Club(WWTravelClub)是一家旅行社,旨在改变人们关于度假和其他全球旅行的决策方式。为此,他们正在开发一个在线服务,其中每个旅行体验的细节都将由为每个目的地特别挑选的专家俱乐部协助。
该平台的概念是您可以同时成为访问者和目的地专家。您在目的地作为专家参与的越多,您获得的积分就越高。这些积分可以用来兑换人们通过平台在线购买的机票。
客户为该平台带来了以下要求。重要的是要知道,通常情况下,客户不会带着已经准备好的需求来进行开发。这就是为什么需求收集过程如此重要的原因:
-
普通用户视图:
-
首页上的促销软件包
-
搜索软件包
-
每个软件包的详细信息:
-
购买软件包
-
购买包含专家俱乐部的软件包:
-
对你的经验进行评论
-
咨询专家
-
评估专家
-
-
-
注册为普通用户
-
-
目的地专家视图:
-
与普通用户视图相同的视图
-
回答询问您目的地专业知识的问题
-
管理您通过回答问题获得的积分:
- 用积分兑换机票
-
-
管理员视图:
-
管理软件包
-
管理普通用户
-
管理目的地专家
-
为了完成这个案例研究,重要的是要注意,WWTravelClub 计划为每个软件包提供超过 100 位目的地专家,并将在全球范围内提供大约 1,000 种不同的软件包。
案例研究 – 理解用户需求和系统需求
要总结 WWTravelClub 的用户需求,你可以阅读以下用户故事:
-
US_001: 作为普通用户,我想在主页上查看促销套餐,这样我可以轻松找到我的下一个假期。 -
US_002: 作为普通用户,我想搜索主页上找不到的套餐,以便我可以探索其他旅行机会。 -
US_003: 作为普通用户,我想查看套餐的详细信息,以便我可以决定购买哪个套餐。 -
US_004: 作为普通用户,我想注册自己,以便我可以开始购买套餐。 -
US_005: 作为注册用户,我想购买套餐,以便我可以处理付款。 -
US_006: 作为注册用户,我想购买包含专家俱乐部的套餐,以便我可以拥有专属的旅行体验。 -
US_007: 作为注册用户,我想寻求专家的帮助,以便我可以获得我旅行的最佳体验。 -
US_008: 作为注册用户,我想评论我的经历,以便我可以从我的旅行中提供反馈。 -
US_009: 作为注册用户,我想评价帮助我的专家,以便我可以与他人分享他们多么出色。 -
US_010: 作为注册用户,我想注册为目的地专家查看,以便我可以帮助那些来我城市旅行的人。 -
US_011: 作为专家用户,我想回答关于我的城市的问题,以便我可以获得未来可以兑换的积分。 -
US_012: 作为专家用户,我想用积分兑换机票,以便我可以环游世界。 -
US_013: 作为管理员用户,我想管理软件包,以便用户能够获得旅行的好机会。 -
US_014: 作为管理员用户,我想管理注册用户,以便 WWTravelClub 可以保证良好的服务质量。 -
US_015: 作为管理员用户,我想管理专家用户,以便所有关于我们目的地的问题都能得到解答。 -
US_016: 作为管理员用户,我想提供超过 1,000 个全球套餐,以便不同国家可以体验 WWTravelClub 的服务。 -
US_017: 作为管理员用户,我想让超过 1,000 个用户同时访问网站,这样我可以支持所有用户的需求。 -
US_018: 作为用户,我想用我的母语访问 WWTravelClub,这样我可以轻松理解提供的套餐。 -
US_019: 作为用户,我想在 Chrome、Firefox 和 Edge 网络浏览器中访问 WWTravelClub,这样我就可以使用我偏好的网络浏览器。 -
US_020: 作为用户,我想安全地购买套餐,以便只有 WWTravelClub 会拥有我的信用卡信息。
注意,当你开始编写故事时,可以包括与安全、环境、性能和可扩展性等非功能性要求相关的信息。
然而,当你编写用户故事时,可能需要省略一些系统需求,这些需求需要包含在软件规范中。这些需求可能与法律方面、硬件和软件先决条件有关,甚至可能涉及正确交付系统的注意事项。它们需要被映射和列出,就像用户故事一样。以下列出了 WWTravelClub 系统需求清单。请注意,需求是用将来时写的,因为系统尚不存在:
-
SR_001: 系统将使用 Microsoft Azure 组件来提供所需的可伸缩性。 -
SR_002: 系统将遵守通用数据保护条例(GDPR)的要求。 -
SR_003: 系统将在 Windows、Linux、iOS 和 Android 平台上运行。 -
SR_004: 本系统的任何网页至少在 2 秒内响应。
摘要
在本章中,你学习了软件架构师在软件开发团队中的作用。本章还涵盖了软件开发流程模型的基础和需求收集过程。你还有机会了解如何创建你的 Azure 账户,该账户将在本书的案例研究中使用,这在上一节中已经向你介绍。此外,你还学习了功能性和非功能性需求以及如何使用用户故事来创建它们。这些技术无疑将帮助你交付更好的软件项目。
在下一章中,你将有机会了解功能性和非功能性需求对于软件架构的重要性。
问题
-
软件架构师需要具备哪些专业知识?
-
Azure 如何帮助软件架构师?
-
软件架构师如何决定在项目中使用最佳的软件开发流程模型?
-
软件架构师如何为需求收集做出贡献?
-
软件架构师在需求规范中需要检查哪些类型的需求?
-
设计思维如何帮助软件架构师在需求收集过程中?
-
用户故事如何帮助软件架构师在编写需求的过程中?
-
开发高性能软件有哪些好的技术?
-
软件架构师如何检查用户需求是否正确实施?
进一步阅读
这里有一些书籍和链接,你可以考虑阅读以获取更多关于本章的信息:
第二章:功能性需求和非功能性需求
一旦您收集了系统需求,就需要考虑它们对架构设计的影响。可伸缩性、性能、多线程、互操作性以及其他主题都需要进行分析,以便我们能够满足用户需求。
本章将涵盖以下主题:
-
什么是可伸缩性以及它与 Azure 和.NET Core 如何交互?
-
在性能改进方面,以下是一些编写更好代码的好建议
-
创建一个安全且有用的多线程软件
-
软件可用性,即如何设计有效的用户界面
-
.NET Core 和互操作性
技术要求
本章提供的示例将需要 Visual Studio 2019 Community Edition 或 Visual Studio Code。
您可以在此处找到本章的示例代码:github.com/PacktPublishing/Hands-On-Software-Architecture-with-CSharp-8/tree/master/ch02。
可伸缩性如何与 Azure 和.NET Core 交互?
对可伸缩性进行简短搜索会返回如下定义:系统在需求增加时仍能良好工作的能力。一旦开发者阅读了这一点,许多人会错误地得出结论,可伸缩性仅仅意味着添加更多硬件以保持事物运行而不停止应用程序。
可伸缩性依赖于涉及硬件解决方案的技术。然而,作为一名软件架构师,您必须意识到,优秀的软件将保持可伸缩性在一个可持续的模式中,这意味着一个良好架构的软件可以节省很多钱。因此,这不仅仅是硬件的问题,也是整体软件设计的问题。
在第一章“理解软件架构的重要性”中,当我们讨论软件性能时,我们提出了一些克服不良性能问题的好建议。同样的建议也会帮助您处理可伸缩性。我们投入每个过程中的资源越少,应用程序可以处理的用户就越多。
值得注意的是,Azure 和.NET Core Web 应用可以被配置为处理可伸缩性。让我们在以下小节中查看。
在 Azure 中创建可伸缩的 Web 应用
在 Azure 中创建一个可伸缩的 Web 应用非常简单,为扩展做好准备。您之所以必须这样做,是为了能够在不同季节维护不同数量的用户。用户越多,您需要的硬件就越多。以下步骤将向您展示如何在 Azure 中创建一个可伸缩的 Web 应用程序:
- 一旦您登录到您的 Azure 账户,您就可以创建新的资源(Web 应用、数据库、虚拟机等),如下面的截图所示:
- 然后,您可以选择 Web 应用。本教程将带您进入以下屏幕:
所需的详细信息如下:
-
应用名称:如您所见,这是您的网络应用创建后所假设的 URL。该名称经过检查以确保其可用性。
-
订阅:这是将收取所有应用费用的账户。
-
资源组:这是您可以定义以组织策略和权限的资源集合。您可以指定新的资源组名称或将网络应用添加到在定义其他资源时指定的组中。
-
操作系统:这是将托管网络应用的操作系统的名称。对于 ASP.NET Core 项目,可以使用 Windows 和 Linux。
-
发布:此参数指示网络应用是直接交付还是将使用 Docker 技术发布内容。Docker 将在第五章,将微服务架构应用于您的企业应用中更详细地讨论。
-
应用服务计划/位置:这是您定义用于处理网络应用和服务器位置的硬件计划的地方。此选择定义了应用程序的可扩展性、性能和成本。
-
应用洞察:这是用于监控和故障排除网络应用的 Azure 实用工具集。
应用可以从两种概念上不同的方式扩展:
-
纵向(扩展)
-
横向(扩展)
两者都可在网络应用设置中找到,如下截图所示:
让我们检查两种扩展类型。
垂直扩展(扩展)
扩展意味着更改将支持您的应用程序的硬件类型。在 Azure 中,您有机会从免费共享硬件开始,并在几个点击中切换到隔离的机器。
选择此选项,您有机会选择更强大的硬件(具有更多 CPU、存储和 RAM 的机器)。以下截图显示了扩展网络应用的用户界面:
横向扩展(扩展)
扩展意味着将所有请求分配给具有相同容量的更多服务器,而不是使用更强大的机器。所有服务器的负载将由 Azure 基础设施自动平衡。以下截图显示了一个由两个简单规则定义的自动扩展策略,该策略由 CPU 使用情况触发:
对所有可用自动扩展规则的完整描述超出了本书的范围。然而,它们相当直观,进一步阅读部分包含指向完整文档的链接。
扩展功能仅在付费服务计划中可用。
使用.NET Core 创建可扩展的网络应用
在所有可用于实现 Web 应用的框架中,ASP.NET Core 确保了良好的性能,同时生产和维护成本较低。ASP.NET Core 的性能与 Node.js 的性能相当,但由于使用了 C#(这是一种强类型和高级纯对象语言)而不是 JavaScript,因此生产和维护成本较低。
以下步骤将指导您通过创建基于 ASP.NET Core 的 Web 应用。所有步骤都非常简单,但一些细节需要特别注意。
首先,在创建 Web 应用时,您可以在 .NET Core 框架和 .NET 框架之间进行选择。请注意,只有 .NET Core 可以在 Windows 和更便宜的 Linux 服务器上运行,而经典 .NET 只能在 Windows 服务器上运行。另一方面,使用经典 .NET,您将能够访问更大的代码库,包括 Microsoft 和第三方包的遗留库。
现在,Microsoft 推荐使用经典 .NET,以防所需的特性在 .NET Core 中不可用,或者当您在不支持 .NET Core 的环境中部署 Web 应用时。在其他任何情况下,您都应该优先选择 .NET Core 框架,因为它允许您做以下事情:
-
在 Windows、Linux 或 Docker 容器中运行您的 Web 应用
-
使用微服务设计您的解决方案
-
拥有高性能和可扩展的系统
容器和微服务将在第五章应用微服务架构到您的企业应用中介绍。在那里,您将更好地了解这些技术的优势。目前,只需说 .NET Core 和微服务是为了性能和可扩展性而设计的,这就是为什么您应该在所有新项目中优先选择 .NET Core。
以下步骤将向您展示如何在 Visual Studio 2019 中使用 .NET Core 3.0 创建 ASP.NET Core Web 应用:
- 一旦您选择了 ASP.NET Core Web 应用,您将被引导到一个屏幕,您将需要设置项目名称、位置和解决方案名称:
-
之后,您将能够选择要使用的 .NET Core 版本。在撰写本文时,.NET Core 3.0 仍处于 Preview 1 版本。
-
现在我们已经添加了基本详情,您可以将您的 Web 应用项目连接到您的 Azure 账户,并发布它。
-
在解决方案资源管理器中,如果您在任意位置右键单击,您将有一个“发布…”的选项:
- 在您选择“发布…”菜单项后,您将能够连接到您的 Azure 账户,然后选择您希望部署的 Web 应用:
- Visual Studio 与 Azure 完全集成。这使您有机会在开发环境中查看在 Azure Portal 中创建的所有资源:
- 一旦您确定了发布设置,即您的发布配置文件,当您点击“确定”时,Web 应用将自动发布。
对于发布.NET Core 预览版本,您必须在 Azure 门户的 Web 应用设置面板中添加一个扩展,如图所示:
关于将 ASP.NET Core 3.0 部署到 Azure App Service 的更多信息,请参阅此链接:docs.microsoft.com/en-us/aspnet/core/host-and-deploy/azure-apps/?view=aspnetcore-2.2#deploy-aspnet-core-preview-release-to-azure-app-service。
在这里,我们描述了部署 Web 应用的最简单方法。第十七章,使用 Azure DevOps 部署您的应用程序,将向您介绍 Azure DevOps 持续集成/持续交付(CI/CD)管道。这个管道是 Azure 工具集的进一步扩展,它自动化了将应用程序部署到生产环境所需的所有步骤,即构建、测试、预部署和部署到生产。
在用 C#编程时需要考虑的性能问题
现在,C#是全球最广泛使用的编程语言之一,因此关于 C#编程的好建议对于设计满足最常见非功能性要求的好架构是基本的。
以下几节提到了一些简单但有效的技巧——相关的代码示例可在本书的 GitHub 仓库中找到。
字符串连接
这是一个经典案例!使用+字符串运算符进行字符串的简单连接可能会导致严重的性能问题,因为每次连接两个字符串时,它们的内容都会被复制到一个新的字符串中。
因此,如果我们连接,比如说,平均长度为 100 的 10 个字符串,第一次操作的成本为 200,第二次操作的成本为200+100=300,第三次操作的成本为 300+100=400,以此类推。不难说服自己,总成本的增长类似于mn²*,其中n是字符串的数量,m是它们的平均长度。对于小的n(比如说,n < 10),n²并不太大,但当n达到 100-1,000 的数量级时,它就变得相当大了,实际上对于 10,000-100,000 的数量级是不可接受的。
让我们通过一些测试代码来看看这个问题,这些代码比较了简单的字符串连接与使用StringBuilder类执行相同操作的情况(代码可在本书的 GitHub 仓库中找到):
如果你创建一个StringBuilder类,例如var sb = new System.Text.StringBuilder(),然后使用sb.Append(currString)将每个字符串添加到其中,字符串不会被复制;相反,它们的指针被排队到一个列表中。它们只在调用sb.ToString()以获取最终结果时复制一次。因此,基于StringBuilder的连接成本简单地增长为mn*。
当然,你可能永远不会找到一个像前面那样的函数,它将 10 万个字符串连接起来。然而,你需要认识到类似的代码片段,其中一些 20-100 个字符串的连接,比如在一个同时处理多个请求的 Web 服务器中,可能会造成瓶颈,损害你的性能非功能需求。
异常
总是记住——异常处理需要太多时间!因此,try-catch的使用需要简洁且必要;否则,你将创建大的性能问题。
以下两个示例比较了使用try-catch和Int32.TryParse来检查字符串是否可以转换为整数的方法,如下所示:
private static string ParseIntWithTryParse()
{
string result = String.Empty;
int value;
if (Int32.TryParse(result, out value))
result = value.ToString();
else
result = "There is no int value";
return $"Final result: {result}";
}
private static string ParseIntWithException()
{
string result = String.Empty;
try
{
result = Convert.ToInt32(result).ToString();
}
catch (Exception)
{
result = "There is no int value";
}
return $"Final result: {result}";
}
第二个函数看起来并不危险,但它比第一个慢数千倍:
总结一下,异常必须用于处理打破正常控制流程的异常情况,例如,当操作由于某些意外原因必须被中止时,并且必须将控制返回到调用堆栈的几个级别。
多线程环境以获得更好的结果——应该做什么和不应该做什么
如果你想充分利用你正在构建的系统提供的所有硬件,你必须使用多线程。这样,当一个线程正在等待一个操作完成时,它可以离开 CPU 和其他资源,让其他线程使用,而不是浪费 CPU 时间。
另一方面,无论微软如何努力帮助解决这个问题,并行代码并不像吃蛋糕那么简单:它容易出错,难以测试和调试。当你开始考虑使用线程时,作为软件架构师,最重要的要记住的是:你的系统需要它们吗? 非功能性和一些功能性需求肯定会为你回答这个问题。
一旦你确定你需要一个多线程系统,你应该决定哪种技术更合适。这里有几个选项,如下所示:
-
创建一个
System.Threading.Thread的实例:这是在 C#中创建线程的经典方式。线程的生命周期将完全由你控制。当你确定你要做什么时,这是好的,但你需要担心实现中的每一个细节。生成的代码难以构思、调试/测试/维护。因此,为了保持开发成本可接受,这种方法应该仅限于几个基本性能批判性模块。 -
使用 System.Threading.Tasks.Parallel 和 System.Threading.Tasks.Task 类进行编程:在 .NET Framework 4.0 版本中,你可以使用并行类以更简单的方式启用线程。这很好,因为你不需要担心你创建的线程的生命周期,但它会给你更少的控制权来了解每个线程中发生的事情。
-
使用异步编程进行开发:这无疑是开发多线程应用程序最容易的方式,因为你不需要关心线程协调,死锁也不可能发生。当一个异步方法调用另一个异步方法时,它会进入休眠模式,以避免在调用任务返回之前浪费资源。这样,异步代码模仿了经典同步代码的行为,同时保持了通用并行编程的大部分性能优势。
总体行为是确定性的,并且不依赖于每个任务完成所需的时间,因此不可能出现不可重现的 bug,生成的代码易于测试/调试/维护。将一个方法定义为异步任务或不是,这是程序员唯一的选择;其他所有事情都由运行时自动处理。你唯一需要关心的是哪些方法应该具有异步行为。
在本书的后续部分,我们将提供一些异步编程的简单示例。有关异步编程及其相关模式的信息,请参阅微软文档中的 基于任务的异步模式 (docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap)。
无论你选择哪种选项,作为软件架构师,你都必须注意一些“应该做”和“不应该做”的事情。以下是一些注意事项:
-
使用并发集合 (
System.Collections.Concurrent):一旦开始一个多线程应用程序,就必须使用这些集合。原因是你的程序可能会从不同的线程管理相同的列表、字典等。使用并发集合是开发线程安全程序的唯一选择。 -
关注静态变量:不能说在多线程开发中禁止使用静态变量,但你应该注意它们。再次强调,多个线程处理相同的变量可能会引起很多麻烦。如果你用
[ThreadStatic]属性装饰静态变量,每个线程都会看到该变量的不同副本,从而解决了多个线程竞争同一值的问题。然而,ThreadStatic变量不能用于跨线程通信,因为一个线程写入的值不能被其他线程读取。 -
在多线程实现后测试系统性能:线程使你能够充分利用你的硬件,但在某些情况下,编写不良的线程可能会浪费 CPU 时间而什么也不做!类似的情况可能会导致几乎 100%的 CPU 使用率和不可接受的系统减速。在某些情况下,通过在某个线程的主循环中添加简单的
Thread.Sleep(1)调用可以减轻或解决问题,但你需要测试这一点。 -
不要认为多线程很简单:在某些语法实现中,多线程并不像看起来那么简单。在编写多线程应用程序时,你应该考虑诸如用户界面的同步、线程终止和协调等问题。在许多情况下,程序只是因为多线程实现不当而停止正常工作。
-
不要忘记计划你的系统应该拥有的线程数量:这对于 32 位程序来说非常重要。在 32 位环境中,你可以拥有的线程数量有一个限制。在设计你的系统时,你应该考虑这一点。
-
不要忘记结束你的线程:如果你没有为每个线程提供正确的终止程序,你可能会遇到内存和处理句柄泄漏的问题。
可用性 - 为什么插入数据需要花费太多时间
可扩展性、性能技巧和多线程是我们可以用来调整机器性能的主要工具。然而,你设计的系统的有效性取决于整个处理管道的整体性能,这包括人类和机器。
作为软件架构师,你无法提高人类的表现,但你可以通过设计一个有效的用户界面(UI),即确保与人类快速交互的用户界面来提高人机交互的性能,这反过来意味着以下内容:
-
UI 必须易于学习,以减少学习时间和目标用户学会快速操作之前的时间浪费。如果 UI 更改频繁,以及需要吸引尽可能多用户的公共网站,这个限制是基本的。
-
UI 不得在数据插入时造成任何类型的减速;数据插入速度必须仅限于用户的打字能力,而不是由系统延迟或可以避免的额外手势。
在设计易于学习的用户界面时,以下是一些简单的技巧:
-
每个输入屏幕必须清楚地说明其目的。
-
使用用户的语言,而不是开发者的语言。
-
避免复杂化。设计 UI 时考虑平均情况;更复杂的情况可以通过仅在需要时出现的额外输入来处理。将复杂的屏幕拆分为更多输入步骤。
-
使用过去的输入来理解用户意图,并通过消息和自动 UI 更改将用户引导到正确的路径;例如,级联下拉菜单。
-
错误消息不是系统给用户的坏笔记,但它们必须解释如何插入正确的输入。
快速用户界面源于对以下三个要求的有效解决方案:
-
输入字段必须按照通常填充的顺序放置,并且应该可以使用 Tab 或 Enter 键移动到下一个输入。此外,经常保持空白的字段应放置在表单底部。简单来说,填写表单时使用鼠标的操作应尽可能减少。这样,用户手势的数量保持在最低。在 Web 应用程序中,一旦决定了输入字段的最佳放置,就足够使用
tabindex属性来定义用户使用 Tab 键从输入字段移动到下一个输入字段的正确方式。 -
系统对用户输入的反应必须尽可能快。特别是,错误(或信息)消息必须在用户离开输入字段时立即出现。实现这一点的最简单方法是将大部分帮助和输入验证逻辑移动到客户端,这样系统反应就不需要通过通信线路和服务器进行。
-
有效的选择逻辑。选择现有项目应该尽可能简单;例如,在优惠中从数千种产品中选择一个,应该可以通过几个手势完成,而且无需记住确切的产品名称或其条形码。下一小节将分析我们可以使用的提高复杂度以实现快速选择的技术。
设计快速选择逻辑
当所有可能的选择的数量级在 1-50 之间时,通常的下拉菜单就足够了。例如,检查货币选择下拉菜单:
当数量级较高但不到几千时,显示所有以用户输入的字符开头的项目名称的自动完成通常是一个好选择:
类似解决方案可以以低计算成本实现,因为所有主要数据库都可以有效地选择以给定子串开头的字符串。
当名称相当复杂时,在搜索用户输入的字符时,它们应在每个项目字符串内部扩展。这种操作不能使用常规数据库有效地执行,需要专门的数据结构。
最后,当我们搜索由多个单词组成的描述时,需要更复杂的搜索模式。例如,产品描述就是这样。如果所选数据库支持全文搜索,系统可以有效地搜索用户在所有描述中输入的多个单词的出现。
然而,当描述由名称而不是常见单词组成时,用户可能很难记住目标描述中包含的几个确切名称。这种情况在多国公司名称中很常见。在这些情况下,我们需要找到用户输入的字符最佳匹配的算法。必须在每个描述的不同位置搜索用户输入的字符串的子串。一般来说,无法有效地使用基于索引的数据库实现类似算法,但需要将所有描述加载到内存中,并以某种方式与用户输入的字符串进行排名。
这类算法中最著名的算法可能是Levenshtein算法,该算法被大多数拼写检查器用于找到与用户输入的错误拼写最匹配的单词。此算法最小化描述与用户输入的字符串之间的 Levenshtein 距离,即从一个字符串转换到另一个字符串所需的字符删除和添加的最小数量。
Levenshtein 算法效果很好,但计算成本非常高。现在,我们给出一个更快的算法,该算法在搜索描述中的字符发生时效果良好。用户输入的字符不需要在描述中连续出现,但必须按相同的顺序出现。某些字符可能缺失。每个描述都根据缺失的字符以及用户输入的字符发生的相对位置给予一个惩罚。更具体地说,算法使用两个数字对每个描述进行排名:
-
用户输入的字符在描述中出现的次数:描述中包含的字符越多,其排名越高。
-
每个描述都根据用户在描述中输入的字符发生之间的总距离给予一个惩罚。
以下截图显示了单词爱尔兰与用户输入的字符串ilad的排名情况:
发生次数为四个,而字符发生之间的总距离为三个。
一旦所有描述都被评分,它们将根据发生次数进行排序。发生次数相同的描述将根据最低的惩罚进行排序。以下是一个实现上述算法的自动完成示例:
以下是对用户输入的字符串进行排名的 C#代码:
public class SmartDictionary<T>
{
...
private Func<T, string> keyAccessor;
protected class Rater
{
public T Item;
public double Penalty=0;
public int FoundChars=0;
}
...
protected Rater RateItem(string search, Rater x)
{
var toSearch = search.ToLower();
var destination = keyAccessor(x.Item).ToLower();
bool firstMatch = true;
for (var j = 0; j < toSearch.Length; j++)
{
if (destination == string.Empty) return x;
var currChar = toSearch[j];
var index = destination.IndexOf(currChar);
if (index == -1) continue;
x.FoundChars++;
if (firstMatch)
{
x.Penalty += index;
firstMatch = false;
}
else x.Penalty += index*1000;
if (index + 1 < destination.Length)
destination = destination.Substring(index + 1);
else
destination = string.Empty;
}
return x;
}
...
}
要排名的项目被插入到Rater实例中。然后,通过keyAccessor函数提取其字符串描述。之后,代码计算字符发生和总距离中的发生次数。
完整的类代码,以及一个测试控制台项目,都可以在本书的 GitHub 仓库中找到。
从大量项目中选择
在这里,“巨大”并不是指存储数据所需的空间量,而是指用户记住每个项目功能的困难程度。当必须从 10,000-100,000 个项目中选择一个时,通过在描述中搜索字符出现来找到它的希望就消失了。在这里,用户必须通过一系列类别来引导到正确的项目。
在这种情况下,执行单个选择需要多个用户手势。换句话说,每个选择都需要与多个输入字段进行交互。一旦确定选择不能通过单个输入字段完成,最简单的选项就是级联下拉菜单,即一系列下拉菜单,其选择列表取决于之前下拉菜单中选择的值。
例如,如果用户需要选择世界上任何地方的城镇,我们可能使用第一个下拉菜单来选择国家,一旦选择了国家,我们可能使用这个选择来填充第二个下拉菜单,其中包含所选国家中的所有城镇。一个简单的例子如下:
显然,当需要时,每个下拉菜单都可以用自动完成来替换,因为选项数量很多。
如果通过交叉多个不同的层次结构来做出正确的选择变得不高效,那么级联下拉菜单也会变得低效,我们需要一个过滤器表单,如下所示:
现在,让我们了解 .NET Core 的互操作性。
.NET Core 与 .NET Core 的互操作性奇妙世界
.NET Core 为 Windows 开发者带来了将他们的软件部署到各种平台的能力。作为软件架构师,您需要特别注意这一点。对于 C# 爱好者来说,Linux 和 macOS 已不再是问题——这比那更好——它们确实是向新客户交付的真正好机会。因此,我们需要确保性能和多平台支持,这是几个系统中的两个常见非功能性需求。
在 Windows 中使用 .NET Core 设计的控制台应用程序和 Web 应用程序几乎也与 Linux 和 macOS 完全兼容。这意味着您不需要在这些平台上重新构建应用程序即可运行它。此外,现在许多特定于平台的行为都支持多平台,例如,从 .NET Core 3.0 开始的 System.IO.Ports.SerialPort 类,它在 Linux 上运行。
微软提供了脚本帮助您在 Linux 和 macOS 上安装 .NET Core。您可以在 docs.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script 找到它们。一旦安装了 SDK,您只需像在 Windows 上一样调用 dotnet 即可。
然而,您必须意识到一些与 Linux 和 macOS 系统不完全兼容的功能。例如,在这些操作系统中没有与 Windows 注册表等效的东西,您必须自己开发替代方案。如果需要,加密的 JSON 配置文件是一个不错的选择。
另一个重要点是,Linux 区分大小写,而 Windows 不区分。当您处理文件时,请记住这一点。另一个重要的事情是,Linux 的路径分隔符与 Windows 的分隔符不同。您可以使用 Path.PathSeparator 属性以及所有其他 Path 类方法来确保您的代码实际上是跨平台的。
此外,您还可以通过使用 .NET Core 提供的运行时检查来将您的代码适配到底层操作系统,如下所示:
using System;
using System.Runtime.InteropServices;
namespace CheckOS
{
class Program
{
static void Main()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Console.WriteLine("Here you have Windows World!");
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
Console.WriteLine("Here you have Linux World!");
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
Console.WriteLine("Here you have macOS World!");
}
}
}
在 Linux 中创建服务
以下脚本可以用于在 Linux 中封装命令行 .NET Core 应用程序。这个想法是,这个服务就像一个 Windows 服务一样工作。考虑到大多数 Linux 安装仅提供命令行界面,并且在没有用户登录的情况下运行,这可能会非常有用:
- 第一步是创建一个将运行命令行应用程序的文件。应用程序的名称是
app.dll,它安装在appfolder中。该应用程序将被每 5,000 毫秒检查一次。此服务是在 CentOS 7 系统上创建的。使用 Linux 终端,您可以输入以下内容:
cat > sample.service <<EOF
[Unit]
Description=Your Linux Service
After=network.target
[Service]
ExecStart=/usr/bin/dotnet $(pwd)/appfolder/app.dll 5000
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
- 一旦文件创建完成,您必须将服务文件复制到系统位置。之后,您需要重新加载
systemd并启用服务,以便在重启时自动启动:
sudo cp sample.service /lib/systemd/system
sudo systemctl daemon-reload
sudo systemctl enable *sample*
- 完成!现在,您可以使用以下命令启动、停止和检查服务。您在命令行应用程序中需要提供的整个输入如下:
# Start the service
sudo systemctl start sample
# View service status
sudo systemctl status sample
# Stop the service
sudo systemctl stop sample
现在我们已经了解了一些概念,让我们学习如何在我们的用例中实现它们。
书籍用例 - 理解 .NET Core 项目的类型
本书用例的开发将基于各种类型的 .NET Core Visual Studio 项目。本节描述了所有这些项目。让我们在 Visual Studio 文件菜单中选择“新建项目”。在打开的窗口中,所有 .NET Core 项目都将位于左侧菜单的 .NET Core、.NET Standard 和 Cloud 项下:
大多数项目都在 .NET Core 下可用:
在这里,我们有一个控制台项目、一个类库项目以及各种类型的测试项目,每个项目都基于不同的测试框架:xUnit、nUnit 和 MSTest。选择各种测试框架只是个人偏好的问题,因为它们都提供了类似的功能。将测试添加到构成解决方案的每个软件组件中是一种常见做法,允许软件频繁修改而不会危及其可靠性。
测试将在第十五章,使用单元测试用例和 TDD 测试您的代码和第二十章,软件测试自动化中详细讨论。最后,我们已经有了一个在使用.NET Core 创建可扩展的 Web 应用程序子节中描述的 ASP.NET Core 应用程序。在那里,我们定义了一个 ASP.NET MVC 应用程序,但 Visual Studio 还包含基于 RESTful API 和最重要的单页应用程序框架(如 Angular、React、Vue.js 以及基于 WebAssembly 的新 Blazor 框架)的项目模板。其中一些在标准的 Visual Studio 安装中可用;其他则需要安装 SPA 包。
对于每种项目类型,我们可以选择我们想要使用的.NET Core 版本。在.NET Standard 菜单项下,我们只有一个类库项目。.NET Standard 类库基于.NET 标准而不是特定的.NET Core 版本,因此它们与多个.NET Core 版本兼容。例如,基于 2.0 标准的库与所有大于或等于 2.0 的.NET Core 版本以及所有大于 4.6 的.NET Framework 版本兼容。
这种兼容性优势是以牺牲更少可用功能为代价的。然而,不属于标准的特性可以作为对附加库包的引用添加。
最后,在云菜单下,我们还有更多项目类型,但与.NET Core 相关的新项目只有 Service Fabric 应用程序:
这使我们能够定义微服务。基于微服务的架构允许将应用程序拆分为几个独立的微服务。可以创建多个相同的微服务实例,并将它们分布到多台机器上以微调每个应用程序部分的表现。微服务将在第五章,将微服务架构应用于您的企业应用程序中描述。
摘要
描述系统行为的职能需求必须与非职能需求相结合,这些非职能需求限制了系统的性能、可伸缩性、互操作性和可用性。性能需求来自响应时间和系统负载需求。作为软件架构师,您应该确保以最低的成本构建高效的算法,充分利用可用的硬件资源,并通过多线程实现所需的性能。
可伸缩性是指系统适应增加负载的能力。系统可以通过提供更强大的硬件进行垂直扩展,或者通过复制和负载均衡相同的硬件进行水平扩展。通常来说,云(尤其是 Azure)可以帮助我们动态地实施策略,而无需停止您的应用程序。
在多个平台上运行的工具,如 .NET Core,可以确保互操作性,即你的软件能够在不同的目标机器和不同的操作系统(Windows、Linux、macOS、Android 等)上运行。
通过关注输入字段的顺序、项目选择逻辑的有效性以及系统学习的便捷性来确保可用性。
在下一章中,你将学习 Azure DevOps 工具如何帮助我们收集、定义和记录我们的需求。
问题
-
系统扩展的两种概念方式是什么?
-
你能否从 Visual Studio 自动部署你的 Web 应用到 Azure?
-
多线程有什么用途?
-
异步模式相较于其他多线程技术的主要优势是什么?
-
为什么输入字段的顺序如此重要?
-
为什么 .NET Core 的
Path类对于互操作性如此重要? -
.NET 标准类库相较于 .NET Core 类库有什么优势?
-
列出各种 .NET Core Visual Studio 项目的类型。
进一步阅读
以下是一些你可能考虑阅读的书籍和链接,以获取更多关于本章的信息:
-
www.packtpub.com/virtualization-and-cloud/hands-azure-developers -
docs.microsoft.com/en-us/azure/architecture/best-practices/auto-scaling -
docs.microsoft.com/en-us/dotnet/standard/parallel-processing-and-concurrency
第三章:使用 Azure DevOps 记录需求
Azure DevOps 是 Visual Studio Team Services 的进化版本,它提供了许多新功能,可以帮助开发者记录和组织他们的软件。本章的目的是介绍由微软提供的这个工具的概述。
本章将涵盖以下主题:
-
使用您的 Azure 账户创建 Azure DevOps 项目
-
理解 Azure DevOps 提供的功能
-
使用 Azure DevOps 组织和管理需求
-
在 Azure DevOps 中展示用例
技术需求
本章要求您创建一个新的免费 Azure 账户或使用现有的一个。第一章中理解软件架构的重要性的创建 Azure 账户部分解释了如何创建一个。
介绍 Azure DevOps
Azure DevOps 是一个微软 软件即服务 (SaaS) 平台,它使您能够向客户持续交付价值。通过在那里创建账户,您将能够轻松地规划项目、安全地存储代码、测试它、将解决方案发布到预发布环境,然后将其发布到实际的生产基础设施。
当然,Azure DevOps 是一个完整的框架,它提供的生态系统为
软件开发目前是可用的。所有涉及软件生产的步骤的自动化确保了现有解决方案的持续增强和改进,以便适应市场需求。
您可以从 Azure 门户开始此过程。如果您不知道如何创建 Azure 门户账户,请参阅第一章,理解软件架构的重要性。创建 Azure DevOps 账户的步骤相当简单:
- 选择“创建资源”然后选择“DevOps 项目”:
- 一旦开始创建项目的向导,您就可以从几个不同的平台中选择您想要交付的系统。这是 Azure DevOps 的一个巨大优势,因为您不仅限于使用微软的工具和产品,而且可以从市场上所有常见的平台、工具和产品中进行选择:
- 可用的选项将取决于第一步中选择的平台。在某些情况下,您可以从以下截图中的几个部署选项中进行选择,该截图在您选择 .NET 平台时出现:
-
一旦设置完成,您将能够根据您提供的信息使用项目门户来管理项目。值得一提的是,如果您没有 Azure DevOps 服务,此向导将为您创建一个。Azure DevOps 组织是您可以组织所有您的
Azure DevOps 项目。整个过程不到 20 分钟:
- 之后,您将能够开始规划您的项目。以下截图显示了 Azure DevOps 项目创建完成后出现的页面。在本书的剩余部分,我们将多次回到这个页面,介绍和描述各种确保更快、更有效部署的有用功能:
如前一个截图所示,创建 Azure DevOps 账户并开始开发一流的 DevOps 工具的过程非常简单。值得一提的是,如果您团队中有最多五名开发者,以及任何数量的利益相关者,您可以使用这款出色的工具而不需付费。
使用 Azure DevOps 组织您的工作
DevOps 是一种 持续集成/持续部署 (CI/CD) 方法论,即一套关于如何将持续改进应用于软件应用以及如何将其交付到生产环境中的最佳实践。Azure DevOps 是一款功能强大的工具,其应用范围涵盖了应用初始开发以及随后的 CI/CD 流程中的所有步骤。
Azure DevOps 包含收集需求和组织整个开发流程的工具。您可以通过点击 Azure DevOps 页面中的“看板”菜单来访问它们,将在下一两个小节中更详细地描述:
Azure DevOps 中其他所有功能将在以下小节中简要介绍。它们将在第十五章 使用单元测试用例和 TDD 测试您的代码到第二十章 软件测试自动化中详细讨论。
Azure DevOps 仓库
“仓库”菜单项为您提供访问 Git 仓库的权限,以便放置项目代码:
您可以通过非常简单的方式从 Visual Studio 内部连接到这个仓库:
-
启动 Visual Studio 并确保您使用用于定义您的 DevOps 项目(或用于将您添加为团队成员)的相同 Microsoft 账户登录到 Visual Studio。
-
如果您的 DevOps 项目仓库为空,请准备一个包含您应用程序中需要的 Visual Studio 项目(开发过程中可以添加更多项目)的 Visual Studio 解决方案。
-
选择“团队资源管理器”选项卡,然后点击连接按钮:
-
点击 Azure DevOps 的“连接…”链接,您将被引导连接到您的其中一个 Azure DevOps 项目。
-
点击“团队资源管理器主页”按钮。现在,您将看到执行 Git 操作以及与其他 Azure DevOps 区域交互的命令:
-
如果 Azure DevOps 仓库为空,请通过点击“更改”按钮提交您刚刚创建的解决方案,然后按照后续的指示操作。
-
点击“同步”按钮以同步您的本地仓库与远程 Azure DevOps 仓库。如果远程仓库为空且您刚刚创建了一个解决方案,此操作将使用此解决方案初始化远程 Azure DevOps 仓库;否则,此操作将在您的本地机器上下载远程仓库。
-
一旦所有团队成员都通过前面的步骤初始化了他们的本地机器仓库和 Azure DevOps 仓库,打开 Visual Studio 就足够了。您在本地仓库中创建的解决方案将出现在 Team Explorer 窗口的底部区域。
-
点击窗口以在您的本地机器上打开解决方案。然后,与远程仓库同步以确保您正在修改的代码是最新的。
Team Explorer 菜单允许您执行大多数 Git 命令,启动远程构建(构建按钮)以及与其他 Azure DevOps 区域交互(例如,查看工作项按钮)。
包源
“工件”菜单处理项目使用的软件包。在那里,您可以定义基本上所有类型的包的源,如 NuGet、Node.js 和 Python。一旦进入工件区域,您可以通过点击“+新建源”链接创建多个源,其中每个源可以处理多种类型的包:
如果您选择从公共源连接到包的选项,默认情况下,源会连接到 npmjs、nuget.org 和 pypi.org。然而,您可以通过右上角的菜单进入“源设置”部分的“上游源”标签页来删除/添加包源:
每个源的“连接到源”链接会显示一个窗口,该窗口为每种包类型解释如何执行以下操作:
-
将私有包上传到源。这样,每个团队都可以使用其私有包的代码库。
-
通过连接到源来使用 Visual Studio 中的其包。您应该将所有项目源添加到您的 Visual Studio 源中,以便也能使用源中上传的私有团队包;否则,您的本地构建将失败。
-
管理访问源的凭据:
测试计划
测试计划部分允许您定义您想要使用的测试计划和它们的设置。您可以通过测试计划菜单项访问它:
在这里,您可以定义、执行和跟踪由以下内容组成的测试计划:
-
手动验收测试
-
自动单元测试
-
负载测试
必须在 Visual Studio 解决方案中包含的测试项目中定义自动单元测试,并基于如 NUnit、xUnit 和 MSTest(Visual Studio 为它们都提供了项目模板)等框架。测试计划为您提供在 Azure 上执行这些测试的机会,并定义以下内容:
-
一系列配置设置
-
何时执行它们
-
如何跟踪它们以及在哪里在整体项目文档中报告结果
对于手动测试,您可以定义操作员的完整说明、执行它们的执行环境(例如,操作系统),以及在哪里在项目文档中报告结果。您还可以定义如何执行负载测试、如何衡量结果以及在哪里报告它们。
管道
管道是自动行动计划,它指定了从代码构建到软件部署到生产中的所有步骤。它们可以在管道区域中定义,该区域可通过“管道”菜单项访问:
在那里,您可以定义一个完整的任务管道,包括它们的触发事件,这包括代码构建、启动测试计划以及在测试通过后要执行的操作。
通常,在测试通过后,应用程序会自动部署到一个预发布区域,在那里可以进行 beta 测试。您还可以定义生产中自动部署的准则。这些准则包括但不限于以下内容:
-
应用程序进行 beta 测试的天数
-
在 beta 测试期间发现的错误数量以及/或最后代码更改中删除的错误数量
-
一位或多位经理/团队成员的手动批准
准则决策将取决于公司希望如何管理正在开发的产品。作为软件架构师,您必须理解,当涉及到将代码移至生产时,越安全越好。
在 Azure DevOps 中管理系统需求
Azure DevOps 允许您使用 工作项 来记录系统需求。工作项作为信息存储在您的项目中,可以分配给个人。它们被分类为各种类型,可能包含所需开发工作的度量、状态以及它们所属的开发阶段(迭代)。
实际上,DevOps 方法论作为一种敏捷方法论,由多个迭代组成,整个开发过程被组织为一组冲刺。可用的工作项取决于在创建 Azure DevOps 项目时选择的 工作项流程。以下小节包含了对最常见工作项类型的描述。
Epic 工作项
想象你正在开发一个由各种子系统组成的系统。可能你不会在一个迭代中完成整个系统。因此,我们需要一个跨越几个迭代的总括来封装每个子系统的所有功能。每个史诗工作项代表这些总括中的一个,它可以包含在各个开发迭代中要实现的好几个功能。
在史诗工作项中,你可以定义状态和验收标准,以及开始日期和目标日期。此外,你还可以提供优先级和努力估计。所有这些详细信息都有助于利益相关者跟踪开发过程。这作为一个项目的宏观视图是非常有用的。
史诗不是默认可用的。必须在项目的团队设置页面中启用它们,该页面可以通过点击项目页面左下角的“项目设置”链接并选择“团队设置”来访问:
功能工作项
你在史诗工作项中提供的所有信息也可以放置在功能工作项中。因此,这两种类型工作项之间的区别并不在于它们包含的信息类型,而在于它们的角色和焦点;你的团队将得出结论。史诗可能跨越几个迭代,并且在功能之上,也就是说,每个史诗工作项都与几个子功能工作项相关联,而每个功能工作项必须在单个迭代中实现,并且是单个史诗工作项的一部分。
值得注意的是,所有工作项都有团队讨论的部分。在那里,你可以通过输入@字符(就像在许多论坛/社交应用中一样)在讨论区域找到团队成员。在每个工作项内部,你可以链接和附加各种信息。你还可以在特定部分检查当前工作项的历史记录。
功能工作项是开始记录用户需求的地方。例如,你可以写一个名为访问控制的功能工作项来定义实现系统访问控制所需的所有功能。
产品待办事项/用户故事工作项
在选择工作项流程之后,你将知道这两种工作项中哪一种可用。它们之间有一些细微的差别,但它们的目的基本上是相同的。它们包含详细的需求,这些需求由它们连接的功能工作项描述。更具体地说,每个产品待办事项/用户故事工作项指定了单个功能的需求,该功能是其父功能工作项中描述的行为的一部分。例如,在系统访问控制的功能中,用户维护和登录界面应该是两个不同的用户故事/产品待办事项。这些需求将指导创建其他子工作项:
-
任务:它们是重要的工作项,描述了为了满足父产品待办事项/用户故事工作项中声明的需求而需要完成的任务。任务工作项可以包含时间估计,这有助于团队容量管理和整体调度。
-
测试用例:这些项目描述了如何测试需求中描述的功能。
你将为每个产品待办事项/用户故事工作项创建的任务和测试用例的数量将根据你使用的开发和测试场景而有所不同。
书籍用例 – 在 Azure DevOps 中展示用例
本节通过 wwtravelclub 的实际案例来阐明上一节中提出的概念。考虑到第一章中描述的场景,即“理解软件架构的重要性”,我们决定定义三个史诗级工作项,如下所示:
创建这些工作项非常简单:
-
在每个工作项内部,链接不同类型的工作项,如下面的截图所示。
-
知道工作项之间的连接在软件开发中非常有用是非常重要的。因此,作为一名软件架构师,你必须向你的团队提供这方面的知识,而且更重要的是,你必须激励他们建立这些连接:
- 一旦你创建了功能工作项,你将能够将其连接到几个详细说明其规格的产品待办事项工作项。以下截图显示了产品待办事项工作项的详细信息:
- 之后,可以为每个产品待办事项工作项创建任务和测试用例工作项。Azure DevOps 提供的用户界面非常有效,因为它允许你跟踪功能链及其之间的关系:
- 一旦你完成了产品待办事项和任务工作项的输入,你将能够与你的团队一起规划项目冲刺。计划视图允许你将产品待办事项工作项拖放到每个计划冲刺中:
这些工作项就是这样创建的。一旦你理解了这个机制,你将能够创建和规划任何软件项目。值得一提的是,工具本身并不能解决与团队管理相关的问题。然而,这个工具是激励团队更新项目状态的一个很好的方式,这样你可以清楚地了解项目是如何发展的。
摘要
本章介绍了如何为软件开发项目创建 Azure DevOps 账户,以及如何使用 Azure DevOps 开始管理您的项目。它还简要回顾了所有 Azure DevOps 功能,解释了如何通过 Azure DevOps 主菜单访问它们。本章更详细地描述了如何管理系统需求,以及如何使用各种类型的工作项组织工作,以及如何规划和组织旨在交付具有许多功能的 Epic 解决方案的迭代。
下一章将讨论软件架构的不同模型。我们还将了解在开发基础设施时,如何从复杂的云平台(如 Azure)提供的选项中选择基本提示和标准。
问题
-
Azure DevOps 仅适用于.NET Core 项目吗?
-
Azure DevOps 能在特定分支提交后触发自动构建吗?Azure DevOps 能自动触发生产环境的部署吗?
-
Azure DevOps 中可用的测试计划有哪些?
-
DevOps 项目可以使用私有 NuGet 包吗?
-
我们为什么使用工作项?
-
Epic 和功能工作项之间的区别是什么?
-
任务与产品待办事项/用户故事工作项之间存在什么样的关系?
进一步阅读
这里有一些书籍和链接,您可以考虑阅读,以收集更多关于本章信息:
-
www.packtpub.com/virtualization-and-cloud/hands-devops-azure-video -
www.packtpub.com/application-development/mastering-non-functional-requirements
第二部分:在基于云的环境中进行软件解决方案的架构设计
本节向您介绍了主要现代云平台中包含的工具。重点是 Microsoft Azure,它提供了最灵活和多样化的服务。
第四章,决定最佳云解决方案,是对云和 Azure 的一般介绍。在那里,您可以找到所有相关的云概念,以及 Azure 整体服务的描述,还有示例说明您如何配置云中的资源以满足您的需求。第五章,将微服务架构应用于企业应用,描述了微服务计算模型,这是在云中实现灵活性、高吞吐量和可靠性的最有效方法。在那里,您还可以了解容器和 Docker,这将使您能够在架构中混合不同的技术,并使您的软件平台独立。
第六章,在 C#中使用 Entity Framework Core 与数据交互,以及第七章,如何在云中选择您的数据存储,描述了 Azure 的主要存储服务以及如何使用它们。在那里,您可以学习以下内容:
-
如何为您的架构中的每个子系统选择最佳的存储解决方案
-
如何配置存储解决方案以实现所需的读写并行性
-
如何将这些内容整合到您的软件中
最后,在第八章,使用 Azure Functions中,您将了解所有主要云服务中包含的计算无服务器模型,并了解如何在 Azure 云中使用它。得益于无服务器,您可以在需要时运行计算,而不必预先分配云资源,并且只需为实际的计算时间付费。
本节包括以下章节:
-
第四章,决定最佳云解决方案
-
第五章,将微服务架构应用于企业应用
-
第六章,在 C#中使用 Entity Framework Core 与数据交互
-
第七章,如何在云中选择您的数据存储
-
第八章,使用 Azure Functions
第四章:决定最佳云解决方案
为了设计你的应用程序使其基于云,你必须了解不同的架构设计——从最简单的到最复杂的。本章讨论了不同的软件架构模型,并教你如何利用云在解决方案中提供的机会。本章还将讨论我们在开发基础设施时可以考虑的不同类型的云服务,理想的场景是什么,以及我们可以在哪里使用它们。
本章将涵盖以下主题:
-
基础设施即服务解决方案
-
平台即服务解决方案
-
软件即服务解决方案
-
无服务器解决方案
-
如何使用混合解决方案以及为什么它们如此有用
技术要求
对于本章的实践内容,你必须创建或使用一个 Azure 账户。我在第一章,理解软件架构的重要性,的创建 Azure 账户部分解释了账户创建过程。
不同的软件部署模型
云解决方案可以采用不同的模型进行部署。你选择如何部署应用程序取决于你合作的团队类型。在拥有基础设施工程师的公司,你可能会发现更多的人在使用基础设施即服务(IaaS)。另一方面,在 IT 不是核心业务的公司,你会看到许多软件即服务(SaaS)系统。开发者决定使用平台即服务(PaaS)选项,或者无服务器部署,因为在这样的场景下他们不需要提供基础设施,这是非常常见的。
作为一名软件架构师,你必须应对这个环境,并确保你在解决方案的初始开发阶段以及维护阶段都在优化成本和工作因素。此外,作为架构师,你必须了解你系统的需求,并努力将这些需求与一流的周边解决方案相连接,以加快交付速度,并使解决方案尽可能接近客户的需求。
基础设施即服务与 Azure 机会
IaaS 是许多不同云服务提供商提供的云服务的第一代。它的定义在许多地方都可以找到,但我们可以将其总结为“你的计算基础设施通过互联网提供”。就像我们在本地数据中心中有服务的虚拟化一样,IaaS 也会为你提供虚拟化组件,如云中的服务器、存储和防火墙。
在 Azure 中,提供了多种以 IaaS 模型提供的服务。其中大部分是付费的,当进行测试时您应该注意这一点。值得一提的是,本书并不旨在详细描述 Azure 提供的所有 IaaS 服务。然而,作为一名软件架构师,您只需了解您将找到以下这样的服务:
-
虚拟机:Windows Server、Linux、Oracle 和数据分析 - 机器学习
-
网络:虚拟网络、负载均衡器和 DNS 区域。
-
存储:文件、表、数据库和 Redis。
执行以下步骤以在 Azure 中创建任何服务:
- 您必须找到最适合您需求的服务,然后创建一个资源。以下截图显示了一个 Windows Server 虚拟机的配置过程:
- 按照 Azure 提供的向导设置您的虚拟机,然后使用远程桌面协议(RDP)连接到它。这种订阅的一个大好奇点是您在几分钟内可以拥有的硬件容量。以下截图展示了这一点:
如果您将本地交付硬件的速度与云速度进行比较,您将意识到在上市时间方面,没有比云更好的选择。例如,截图底部展示的 D64s_v3 机器,拥有 64 个 CPU、256 GB 的 RAM 和 512 GB 的临时存储,这可能是您在本地数据中心中找不到的东西。此外,在某些用例中,这台机器在整个月份中可能只会使用几个小时,因此在本地场景中购买它的理由将是不成立的。这就是为什么云计算如此神奇!
IaaS 中的安全责任
安全责任是了解 IaaS 平台时需要知道的另一件重要事情。许多人认为一旦决定上云,所有的安全都是由提供商完成的。然而,正如以下截图所示,这并不正确:
IaaS 将迫使您从操作系统到应用程序的每个层面都关注安全性。在某些情况下,这是不可避免的,但您必须理解这将增加您的系统成本。
如果您只想将现有的本地结构迁移到云端,IaaS 可以是一个不错的选择。这得益于 Azure 提供的工具以及所有其他服务,从而实现了可扩展性。然而,如果您计划从头开发应用程序,您也应该考虑 Azure 上可用的其他选项。
让我们在下一节检查其中一个最快的系统,那就是 PaaS。
PaaS – 为开发者提供无限机会
如果您正在学习或已经学习过软件架构,您可能会完美理解下一句话的含义:世界在软件开发方面需要高速!如果您同意这一点,您会喜欢 PaaS。
正如您在前面的屏幕截图中看到的,PaaS 允许您仅在接近您业务的角度考虑安全问题:您的数据和应用程序。作为一名开发者,这意味着您不必实施大量配置,以确保您的解决方案安全运行。
此外,安全性处理并不是 PaaS 的唯一优势。作为一名软件架构师,您可以将这些服务作为快速提供更丰富解决方案的机会。上市时间可以肯定地证明许多基于 PaaS 的应用程序的成本是合理的。
现在,Azure 中提供了许多作为 PaaS 提供的服务,而且,这本书的目的并不是列出所有这些服务。然而,其中一些确实需要提及。这个列表还在不断增长,这里的建议是:尽可能多地使用和测试服务!确保您会考虑到这一点,提供更好的设计解决方案。
另一方面,值得一提的是,使用 PaaS 解决方案,您将无法完全控制操作系统。实际上,在许多情况下,您甚至没有连接到它的方法。这大多数时候是好的,但在某些调试情况下,您可能会错过这个功能。好消息是,PaaS 组件每天都在发展,微软最大的担忧之一是使它们广为人知。
以下部分介绍了 Microsoft 为 .NET Core Web 应用程序提供的最常见 PaaS 组件,例如 Azure Web 应用程序和 Azure SQL Server。我们还描述了 Azure 认知服务,这是一个非常强大的 PaaS 平台,展示了在 PaaS 世界中开发是多么美妙。我们将在本书的剩余部分更深入地探讨其中的一些。
Web 应用程序
Web 应用程序是一个 PaaS 选项,您可以使用它来部署您的 Web 应用程序。您可以部署不同类型的应用程序,例如 .NET、.NET Core、Java、PHP、Node JS 和 Python。这种类型的示例在第一章中有所介绍,理解软件架构的重要性。
好处在于创建 Web 应用程序不需要任何结构,也不需要 IIS Web 服务器设置。在某些情况下,如果您使用 Linux 来托管您的 .NET Core 应用程序,您甚至根本就没有 IIS。
此外,Web 应用程序有一个计划选项,您无需为使用付费。当然,有一些限制,例如只能运行 32 位应用程序,并且无法启用可伸缩性,但这对原型设计来说可以是一个美好的场景。
Azure SQL Server
想象一下,如果你不需要为部署这个数据库支付大服务器的费用,就能利用 SQL Server 的全部功能来快速部署解决方案会是什么样子。这适用于 Azure SQL Server。使用 Azure SQL Server,你有机会使用 Microsoft SQL Server 来完成你最需要的事情——存储和数据处理。在这种情况下,Azure 负责备份数据库。
Azure SQL Server 甚至提供了自动管理性能的选项。这被称为自动调优。同样,使用 PaaS 组件,你将能够专注于对你业务真正重要的事情:非常快的上市时间。
创建 Azure SQL Server 数据库的步骤相当简单,就像我们之前检查的其他组件一样。然而,有两件事你需要注意:服务器的创建以及你将如何被收费。
当你在 Azure SQL Server 中搜索创建数据库时,你会找到这个向导来帮助你:
正如你所见,你必须创建(至少对于第一个数据库)一个 database.windows.net 服务器,你的数据库将托管在这里。这个服务器将提供你使用当前工具(如 Visual Studio 或 SQL Server Management Studio)访问 SQL Server 数据库所需的所有参数。值得一提的是,你有一系列关于安全性的功能,例如透明加密和 IP 防火墙。
一旦你决定了你的数据库服务器的名称,你将能够选择你的系统将被收取费用的定价层。特别是在 Azure SQL Server 数据库中,有几种不同的定价选项,如以下截图所示。你应该仔细研究每一个,因为根据你的场景,通过优化定价层你可能能节省一些钱:
关于 SQL 配置的更多信息,你可以查看这个链接:azure.microsoft.com/en-us/services/sql-database/.
一旦完成配置,你将能够以与你的 SQL Server 安装在本地时相同的方式连接到这个服务器数据库。你唯一需要关注的是 Azure SQL Server 防火墙的配置,但这设置起来相当简单,也是 PaaS 服务安全性的良好证明。
Azure 认知服务
人工智能(AI)是软件架构中最常讨论的话题之一。我们离一个真正伟大的世界只有一步之遥,在那里人工智能将无处不在。为了使这句话成为现实,作为一名软件架构师,你不能把人工智能看作是每次都需要从头开始重新发明的软件。
Azure 认知服务可以帮助您实现这一点。在这个 API 集合中,您将找到开发视觉、知识、语音、搜索和语言解决方案的各种方法。其中一些需要训练才能使事物发生,但这些服务也提供了相应的 API。
从这个场景中可以看出 PaaS 的好处。在本地或 IaaS 环境中准备应用程序时,您需要执行的工作数量是巨大的。在 PaaS 中,您根本无需担心这一点。您完全专注于作为软件架构师真正关心的事情:解决您的业务问题。
在您的 Azure 账户中设置 Azure 认知服务也非常简单:
- 首先,您需要像添加任何其他 Azure 组件一样添加认知服务,如下面的截图所示:
- 一旦完成这些,您将能够使用服务器提供的 API。您将在您创建的服务中找到两个重要功能:端点和访问密钥。它们将在您的代码中用于访问 API:
以下代码示例展示了如何使用认知服务来翻译句子。这个翻译服务背后的主要概念是您可以按照服务设置的键和区域发布您想要翻译的句子。以下代码使您能够向服务 API 发送请求:
private static async Task<string> PostAPI(string api, string key, string region,
string textToTranslate)
{
string result = String.Empty;
using (var client = new HttpClient())
{
using (var request = new HttpRequestMessage(HttpMethod.Post, api))
{
request.Headers.Add("Ocp-Apim-Subscription-Key", key);
request.Headers.Add("Ocp-Apim-Subscription-Region", region);
// five seconds for timeout
client.Timeout = new TimeSpan(0, 0, 5);
var body = new object[] { new { Text = textToTranslate } };
var requestBody = JsonConvert.SerializeObject(body);
request.Content = new StringContent(requestBody, Encoding.UTF8,
"application/json");
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
result = await response.Content.ReadAsStringAsync();
}
}
return result;
}
值得注意的是,前面的代码将允许您根据在参数中定义的键和区域将任何文本翻译成任何语言。以下是一个调用前面方法的程序示例:
/// <summary>
/// Check this content at: https://docs.microsoft.com/en-us/azure/cognitive
/// services/translator/reference/v3-0-reference
/// </summary>
static void Main()
{
var host = "https://api.cognitive.microsofttranslator.com";
var route = "/translate?api-version=3.0&to=es";
var subscriptionKey = "[YOUR KEY HERE]";
var region = "[YOUR REGION HERE]";
var translatedSentence = PostAPI(host + route, subscriptionKey, region,
"Hello World!").Result;
Console.WriteLine(translatedSentence);
}
这是一个如何轻松快速地使用此类服务来构建项目的完美示例。此外,这种开发方法非常好,因为您正在使用其他解决方案已经测试并使用过的代码片段。
SaaS – 只需登录即可开始!
软件即服务可能是使用基于云的服务最简单的方式。云服务提供商为他们的最终用户提供了许多解决公司常见问题的良好选项。
这种类型服务的良好例子是 Office 365。这些平台的关键点在于您无需担心应用程序的维护。在您的团队完全专注于开发应用程序的核心业务场景中,这一点尤其方便。例如,如果您的解决方案需要提供良好的报告,您可能可以使用包含在 Office 365 中的 Power BI 来设计它们。
另一个很好的 SaaS 平台例子是 Azure DevOps。作为软件架构师,在 Azure DevOps 或 Visual Studio Team Services (VSTS) 之前,您需要安装和配置 Team Foundation Server(甚至更老的类似工具)以使您的团队能够使用一个共同的仓库和应用生命周期管理工具。
我们过去花费大量时间要么在准备服务器以安装团队基础服务器(TFS),要么在升级和维护已安装的 TFS。由于 SaaS Azure DevOps 的简单性,这不再需要。
理解无服务器意味着什么
这个词本身就说明了问题:无服务器解决方案意味着没有服务器的解决方案。但在云架构中这怎么可能呢?这很简单。在这种解决方案中,您不需要担心与服务器相关的任何问题。
您现在可能正在想无服务器只是另一个选项——当然,这是真的,因为这个架构并没有提供一个完整的解决方案。但关键点在于,在无服务器解决方案中,您拥有一个非常快速、简单和敏捷的应用生命周期,因为所有无服务器代码都是无状态的,并且与系统其余部分松散耦合。一些作者将其称为函数即服务(FaaS)。
当然,服务器在某处运行。关键点在于您不需要担心这一点,甚至不需要担心可扩展性。这将使您能够完全专注于您的应用程序业务逻辑。再次强调,世界需要快速发展和良好的客户体验。您越关注客户需求,效果就越好!
在第八章“使用 Azure Functions”,您将探索微软在 Azure 中提供的最佳无服务器实现之一——Azure Functions。在那里,我们将关注您如何开发无服务器解决方案,并了解它们的优缺点。
为什么混合应用在很多情况下如此有用?
混合解决方案是那些部分不共享统一架构选择的解决方案;每个部分都做出不同的架构选择。在云中,混合一词主要指将云子系统与本地子系统混合的解决方案。然而,它也可以指将 Web 子系统与特定设备子系统混合。
由于 Azure 可以提供的服务数量以及可以实施的设计架构数量,混合应用可能是本章解决的主要问题的最佳答案,即如何利用云在您的项目中提供的机会。如今,许多当前的项目正从本地解决方案迁移到云架构,并且根据您将交付这些项目的位置,您仍会发现许多关于迁移到云的负面观念。其中大部分与成本、安全和服务的可用性有关。
您需要理解这些先入为主的观念中确实有一些是真实的,但并非人们所想的那样。当然,作为软件架构师,您不能忽视它们。尤其是在开发关键系统时,您必须决定是否所有内容都可以放在云端,或者是否最好将系统的一部分部署在边缘。
移动解决方案可以被视为混合应用程序的典型例子,因为它们将基于 Web 的架构与基于设备的架构相结合,以提供更好的用户体验。有许多场景可以将移动应用程序替换为响应式网站。然而,当涉及到界面质量和性能时,可能响应式网站并不能满足最终用户真正的需求。
在下一节中,我们将讨论本书用例的实际示例。
用例 - 混合应用程序
如果您回到第一章,理解软件架构的重要性,您将找到一个系统需求,描述了我们的 WWTravelClub 示例应用程序应该运行的系统环境:
SR_003:系统应在 Windows、Linux、iOS 和 Android 平台上运行。
初看之下,任何开发者都会回答说:Web 应用程序。然而,iOS 和 Android 平台也需要作为软件架构师引起您的注意。在这种情况下,就像在许多情况下一样,用户体验是项目成功的关键。决策不仅需要由开发速度驱动,而且还需要由提供卓越用户体验所获得的收益驱动。
在这个项目中,软件架构师必须做出的另一个决定与移动应用程序的技术有关,如果他们决定开发一个。同样,这将是混合应用程序和本地应用程序之间的选择,因为在这种情况下,可以使用像 Xamarin 这样的混合解决方案。因此,在移动应用程序方面,您也有选择继续用 C#编写代码的选项。
以下截图展示了我们对 WWTravelClub 架构的第一选择。选择依赖 Azure 组件的决定与成本和维护考虑有关。以下各项将在本书的后续章节中讨论,包括第六章,在 C#中与数据交互 - Entity Framework Core,第七章,如何在云中选择您的数据存储,以及第八章,与 Azure Functions 一起工作,以及选择的原因。目前,只需知道 WWTravelClub 是一个混合应用程序,在移动设备上运行 Xamarin Apps,在服务器端运行.NET Core Web 应用程序即可:
将会有一个 Azure SQL Server 数据库通过 Entity Framework Core 连接到 Web 应用程序,这将在第六章,“在 C# 中与数据交互 - Entity Framework Core”中进行讨论。稍后,在第七章 7,“如何在云中选择您的数据存储”,我们将出于性能和成本考虑添加 NoSQL 数据库。对于图片存储,选择了文件存储。最后,Xamarin 应用程序将通过 Azure Functions 从系统中获取信息。
书籍用例 - 对于这个用例来说,哪个云平台是最好的?
正如你在最后一节的屏幕截图中可以验证的那样,WWTravelClub 架构主要是使用 Azure 提供的 Platform as a Service 和无服务器组件设计的。所有开发工作都将在 Azure DevOps SaaS Microsoft 平台上进行。
在我们 WWTravelClub 的假设场景中,赞助商指出 WWTravelClub 团队中没有人在基础设施方面有专长。这就是为什么软件架构使用了 PaaS 服务。考虑到这个场景和所需的发展速度,这些组件肯定会表现良好。
当我们飞越这本书中讨论的章节和技术时,这个架构将不断变化和演变,而不会受到任何早期选择的限制。这是 Azure 和现代架构设计提供的一个绝佳机会。你可以轻松地根据解决方案的发展更改组件和结构。
摘要
在本章中,你学习了如何在你的解决方案中利用云提供的服务,以及你可以选择的多种选项。
本章介绍了在云结构中交付相同应用程序的不同方式。我们还注意到微软是如何迅速将这些选项提供给其客户的,因为你可以将这些选项在实际应用程序中体验,并选择最适合你需求的选项,因为没有一种在所有情况下都适用的“银弹”。作为软件架构师,你需要分析你的环境和你的团队,然后决定在你的解决方案中实施的最佳云架构。
下一章将专门介绍如何构建由称为微服务的可扩展软件模块组成的灵活架构。
问题
-
为什么你应该在你的解决方案中使用 IaaS?
-
为什么你应该在你的解决方案中使用 PaaS?
-
为什么你应该在你的解决方案中使用 SaaS?
-
为什么你应该在你的解决方案中使用无服务器?
-
使用 Azure SQL Server 数据库的优势是什么?
-
你如何使用 Azure 加速应用程序中的 AI?
-
混合架构如何帮助你设计更好的解决方案?
进一步阅读
你可以查看这些网络链接,以决定本章中哪些主题你应该深入研究:
-
www.packtpub.com/application-development/xamarin-cross-platform-application-development -
www.packtpub.com/virtualization-and-cloud/learning-azure-functions -
azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/ -
docs.microsoft.com/azure/sql-database/sql-database-automatic-tuning -
azure.microsoft.com/en-us/overview/what-is-serverless-computing/ -
www.packtpub.com/virtualization-and-cloud/professional-azure-sql-database-administration
第五章:将微服务架构应用于您的企业应用程序
本章致力于描述基于称为微服务的小模块的高度可扩展架构。微服务架构允许进行细粒度的扩展操作,其中每个单独的模块都可以按需扩展,而不会影响系统的其余部分。此外,它们通过允许每个系统子部分独立于其他部分进行演变和部署,从而允许更好的持续集成/持续部署(CI/CD)。
在本章中,我们将涵盖以下主题:
-
什么是微服务?
-
微服务何时有帮助?
-
.NET Core 如何处理微服务?
-
管理微服务需要哪些工具?
-
用例 - 记录微服务
到本章结束时,你将学会如何根据本章的使用案例在 .NET Core 中实现一个微服务。
技术要求
在本章中,你需要以下内容:
-
Visual Studio 2017 或 2019 的免费社区版或更高版本,并安装了所有数据库工具。
-
一个免费的 Azure 账户。在第一章的“创建 Azure 账户”部分,理解软件架构的重要性,解释了如何创建一个账户。
-
用于在 Visual Studio 中调试微服务的 Azure Service Fabric 本地模拟器。它是免费的,可以从
www.microsoft.com/web/handlers/webpi.ashx?command=getinstallerredirect&appid=MicrosoftAzure-ServiceFabric-CoreSDK下载。为了避免安装问题,请确保您的 Windows 版本是最新的。此外,模拟器使用 PowerShell 高权限级别命令,默认情况下,这些命令被 PowerShell 块。要启用它们,您需要在 Visual Studio 包管理器控制台或任何 PowerShell 控制台中执行以下命令。Visual Studio 或外部 PowerShell 控制台必须以 管理员 身份启动,以下命令才能成功:
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force -Scope CurrentUser
- 如果您想在 Visual Studio 中调试 Docker 容器化的微服务,请使用 Docker CE for Windows (
store.docker.com/editions/community/docker-ce-desktop-windows?tab=description)。
什么是微服务?
微服务架构允许每个构成解决方案的模块独立于其他模块进行扩展,以实现最大吞吐量并最小化成本。实际上,扩展整个系统而不是其当前的瓶颈不可避免地会导致资源的巨大浪费,因此对子系统扩展的精细控制对系统的整体成本有重大影响。
然而,微服务不仅仅是可扩展的组件——它们是可以在彼此独立开发、维护和部署的软件构建块。将开发和维护分散到可以独立开发、维护和部署的模块中,可以改善整个系统的 CI/CD 循环(CI/CD 概念在第三章的使用 Azure DevOps 组织工作部分中进行了更详细的解释,使用 Azure DevOps 记录需求)。
CI/CD 的改进归功于微服务的独立性,因为它使得以下成为可能:
-
在不同类型的硬件上扩展和分发微服务。
-
由于每个微服务都是独立于其他微服务部署的,因此不存在二进制兼容性或数据库结构兼容性约束。因此,没有必要对组成系统的不同微服务的版本进行对齐。这意味着它们中的每一个都可以根据需要独立演进,而不会受到其他微服务的限制。
-
将其开发分配给完全独立的较小团队,从而简化工作组织并减少处理大型团队时产生的所有不可避免的协调低效率。
-
使用更合适的技术和更合适的环境来实现每个微服务,因为每个微服务都是一个独立的部署单元。这意味着选择最适合您需求的工具和最小化开发努力/最大化性能的环境。
-
由于每个微服务可以使用不同的技术、编程语言、工具和操作系统来实现,企业可以通过将环境与开发者的技能相匹配来利用所有可用的人力资源。例如,未充分利用的 Java 开发者也可以参与 .NET 项目,如果他们使用 Java 实现具有相同所需行为的微服务。
-
历史子系统可以被嵌入到独立的微服务中,从而使得它们能够与较新的子系统进行协作。这样,公司可以缩短新系统版本的上市时间。此外,通过这种方式,历史系统可以缓慢地向更现代的系统演进,同时对成本和组织的影响是可接受的。
下一个子节将解释微服务概念是如何产生的。然后,我们将继续本介绍性章节,探讨基本微服务设计原则,并分析为什么微服务通常被设计为 Docker 容器。
微服务与模块概念的演进
为了更好地理解微服务的优势以及它们的设计技术,我们必须牢记软件模块化和软件模块的双重特性:
-
代码模块化指的是一种代码组织方式,它使得我们能够轻松地修改代码块,而不会影响到应用程序的其他部分。这通常通过面向对象设计来实现,其中模块可以通过类来识别。
-
部署模块化取决于你的部署单元及其属性。最简单的部署单元是可执行文件和库。因此,例如,动态链接库(DLL)肯定比静态库更模块化,因为它们在部署之前不需要与主可执行文件链接。
虽然代码模块化的基本概念已经达到稳定状态,但部署模块化的概念仍在不断发展,微服务目前是这一发展路径上的尖端技术。
作为对导致微服务的主要里程碑的简要回顾,我们可以这样说,首先,单体可执行文件被分解为静态库。后来,动态链接库取代了静态库。
当.NET(以及其他类似框架,如 Java)改进了可执行文件和库的模块化时,发生了巨大的变化。实际上,由于.NET 在库首次执行时编译为中间语言,因此它们可以在不同的硬件和不同的操作系统上部署。此外,它们克服了先前 DLL 的一些版本问题,因为任何可执行文件都附带一个与操作系统中安装的相同 DLL 版本不同的 DLL。
然而,.NET 无法接受使用不同版本的共同依赖项的两个引用 DLL——比如说,A 和 B。例如,假设有一个 A 的新版本,它包含许多我们希望使用的新功能,而这些功能反过来又依赖于 B 不支持的新版本的 C。在类似的情况下,我们应该放弃 A 的新版本,因为 C 与 B 的不兼容性。这种困难导致了两个重要的变化:
-
开发世界从 DLL 和/或单个文件转向了包管理系统,如 NuGet 和 npm,这些系统通过语义版本化自动检查版本兼容性。
-
面向服务的架构(SOA)。部署单元开始以 XML 的形式实现,然后是 RESTful Web 服务。这解决了版本兼容性问题,因为每个 Web 服务都在不同的进程中运行,并且可以使用每个库的最合适版本,而不会与其他 Web 服务造成不兼容的风险。此外,每个 Web 服务暴露的接口是平台无关的,也就是说,Web 服务可以使用任何框架与应用程序连接,并在任何操作系统上运行,因为 Web 服务协议基于普遍接受的标准。SOA 和协议将在第十二章中更详细地讨论,使用.NET Core 应用面向服务的架构。
微服务是 SOA 的演进,增加了更多特性和约束,这些特性和约束提高了服务的可扩展性和模块化,从而改善了整体的 CI/CD 周期。有时人们会说,微服务是做得好的 SOA。
微服务设计原则
总结一下,微服务架构是一种 SOA,它最大化了独立性和细粒度扩展。既然我们已经阐明了微服务独立性和细粒度扩展的所有优势,以及独立的本质,我们现在可以看看微服务设计原则。
让我们从独立约束产生的原则开始:
-
设计选择的独立性:每个微服务的架构设计不能依赖于其他微服务实现中做出的设计选择。这个原则使得每个微服务的 CI/CD 周期完全独立,并给我们提供了更多关于如何实现每个微服务的科技选择。这样,我们可以选择最佳的技术来实现每个微服务。
这个原则的另一个后果是,不同的微服务不能连接到同一个共享存储(数据库或文件系统),因为共享相同的存储也意味着共享所有决定了存储子系统结构(数据库表设计、数据库引擎等)的设计选择。因此,要么微服务有自己的数据存储,要么它根本不存储任何数据,并与负责处理存储的其他微服务进行通信。
在这里,拥有专用数据存储并不意味着物理数据库分布在微服务自身的进程边界内,而是微服务可以独占访问由外部数据库引擎处理的数据库或数据库表集。实际上,出于性能原因,数据库引擎必须在专用硬件上运行,并使用针对其存储功能进行优化的操作系统和硬件功能。通常,设计选择的独立性通过区分逻辑微服务和物理微服务来以更轻的形式解释。更具体地说,逻辑微服务通过使用相同数据存储但独立负载均衡的多个物理微服务来实现。也就是说,逻辑微服务被设计为一个逻辑统一体,然后拆分为更多的物理微服务以实现更好的负载均衡。
-
独立于部署环境:微服务可以在不同的硬件节点上扩展,并且不同的微服务可以托管在同一节点上。因此,微服务对操作系统提供的服务的依赖性越小,对其他已安装软件的依赖性越小,它可以在更多的硬件节点上部署。还可以进行更多的节点优化。
这是微服务通常被容器化并使用 Docker 的原因。容器将在本章的“容器和 Docker”小节中更详细地讨论,但基本上,容器化是一种技术,它允许每个微服务携带其依赖项,以便它可以在任何地方运行。
-
松散耦合:每个微服务必须与其他所有微服务松散耦合。这个原则有两个方面。一方面,这意味着根据面向对象编程原则,每个微服务暴露的接口不应过于具体,而应尽可能通用。然而,这也意味着微服务之间的通信必须最小化,以减少通信成本,因为微服务不共享相同的地址空间,并且在不同的硬件节点上运行。
-
无链式请求/响应:当一个请求到达微服务时,它必须不会导致对其他微服务的嵌套请求/响应的递归链,因为类似的链会导致不可接受的响应时间。如果所有微服务的私有数据模型在每次更改时都通过推送通知同步,则可以避免链式请求/响应。换句话说,一旦微服务处理的数据发生变化,这些更改就会发送到所有可能需要它们的微服务,以便它们可以处理请求。这样,每个微服务都可以在其私有数据存储中拥有所有它需要的数据,以处理所有传入的请求,无需请求其他微服务提供它所缺少的数据。
总之,每个微服务都必须包含它为服务传入请求和确保快速响应所需的所有数据。为了保持其数据模型最新并准备好接收传入请求,微服务必须在数据变化发生时立即通知其数据变化。这些数据变化应通过异步消息进行通信,因为同步嵌套消息会导致不可接受的性能,因为它们会阻塞调用树中所有涉及的线程,直到返回结果。
值得指出的是,我们提到的第一个约束实质上是领域驱动设计的边界上下文原则,我们将在第十章“理解软件解决方案中的不同领域”中详细讨论。在本章中,我们将看到,通常,完整的领域驱动设计方法对每个微服务的更新子系统是有用的。
反过来也成立,即根据边界上下文原则开发出的系统,用微服务架构实现会更好。事实上,一旦一个系统被分解为几个完全独立且松散耦合的部分,由于不同的流量和不同的资源需求,这些不同的部分很可能需要独立扩展。
前述约束是构建可重用 SOA 的一些最佳实践。关于这些最佳实践的更多细节将在第十二章“使用.NET Core 应用服务架构”中给出,但如今,大多数 SOA 最佳实践都由用于实现 Web 服务的工具和框架自动执行。
精细粒度扩展要求微服务足够小,以便隔离定义良好的功能,但这同时也需要一个复杂的基础设施,该基础设施负责自动实例化微服务、在节点上分配实例以及按需扩展它们。这类结构将在本章的“需要哪些工具来管理微服务?”部分进行讨论。
此外,通过异步通信进行通信的分布式微服务的精细粒度扩展需要每个微服务都具有弹性。实际上,指向特定微服务实例的通信可能会因为硬件故障或简单的理由(例如,在负载均衡操作期间目标实例被杀死或移动到另一个节点)而失败。
可以通过指数重试来克服暂时性故障。这就是在每个故障后,我们都会以指数级增加的延迟重试相同的操作,直到达到最大尝试次数。例如,首先,我们会在 10 毫秒后重试,如果这次重试操作导致失败,则会在 20 毫秒后进行新的尝试,然后是 40 毫秒,以此类推。
另一方面,长期故障通常会导致重试操作的爆炸性增长,这可能会以类似于拒绝服务攻击的方式耗尽所有系统资源。因此,通常,指数退避与断路器策略一起使用:在给定数量的失败之后,假设发生长期故障,并通过返回一个立即失败而不尝试通信操作来防止在给定时间内访问资源。
此外,某些子系统由于故障或请求峰值而导致的拥塞不应传播到其他系统部分,以防止整体系统拥塞。隔离舱隔离通过以下方式避免拥塞传播:
-
只允许最大数量的类似同时出站请求,比如说,10 个。这类似于对线程创建设置上限。
-
超过之前限制的请求将被排队。
-
如果达到最大队列长度,任何进一步的请求将导致抛出异常以终止它们。
重试策略可能会使得相同的消息被接收和处理多次,因为发送者没有收到消息已被接收的确认,或者简单地因为超时了操作,而接收者实际上已经接收了消息。解决这个问题的唯一可能方法是设计所有消息,使它们是幂等的,也就是说,以这种方式设计消息,即多次处理相同的消息具有与处理一次相同的效果。
例如,将数据库表字段更新为某个值是一个幂等操作,因为重复一次或两次具有完全相同的效果。然而,增加一个十进制字段不是幂等操作。微服务设计者应努力设计尽可能多的幂等消息的整体应用程序。剩余的非幂等消息必须以下列方式或使用其他类似技术转换为幂等消息:
-
附加一个时间和一些唯一标识符,以唯一标识每条消息。
-
将所有已接收的消息存储在一个字典中,该字典按前一点提到的消息附加的唯一标识符进行索引。
-
拒绝旧消息。
-
当接收到可能重复的消息时,验证它是否包含在字典中。如果是,那么它已经被处理,因此拒绝它。
-
由于拒绝旧消息,它们可以定期从字典中删除,以避免其指数增长。
我们将在本章末尾的示例中使用这项技术。
在下一小节中,我们将讨论基于 Docker 的微服务容器化。
容器和 Docker
我们已经讨论了拥有不依赖于运行环境的微服务的优势:更好的硬件使用率,能够混合旧软件与较新的模块,能够混合多个开发堆栈以使用每个模块实现的最佳堆栈,等等。通过在每个微服务上部署所有依赖项到私有虚拟机,可以轻松实现对托管环境的独立性。
然而,启动带有其私有操作系统副本的虚拟机需要很长时间,而微服务必须快速启动和停止以减少负载均衡和故障恢复成本。实际上,新的微服务可能被启动是为了替换有故障的微服务,或者是因为它们从一个硬件节点移动到另一个节点以执行负载均衡。此外,将整个操作系统的副本添加到每个微服务实例中将会造成过度的开销。
幸运的是,微服务可以依赖一种更轻量级的技术:容器。容器是一种轻量级的虚拟机。它们不虚拟化整个机器——它们只是虚拟化了位于操作系统内核之上的操作系统(OS)文件系统级别。它们使用宿主机的操作系统(内核、DLL 和驱动程序)并依赖于操作系统的原生功能来隔离进程和资源,以确保为运行的镜像提供一个隔离的环境。
因此,容器与特定的操作系统相关联,但它们不会遭受在每个容器实例中复制和启动整个操作系统的开销。
在每台主机机器上,容器由一个运行时处理,该运行时负责从 镜像 中创建它们并为每个容器创建一个隔离的环境。最著名的容器运行时是 Docker,它是容器化的事实标准。
镜像是文件,指定了每个容器中包含的内容以及哪些容器资源,例如通信端口,要暴露在容器外部。没有任何镜像需要明确指定其全部内容,但它们可以引用其他镜像。这样,通过在现有镜像之上添加新的软件和配置信息来构建镜像。
例如,如果您想将 .NET Core 应用程序作为 Docker 镜像部署,只需将您的软件和文件添加到您的 Docker 镜像中,然后引用一个已经存在的 .NET Core Docker 镜像即可。
为了便于图像引用,图像被分组到注册表中,这些注册表可以是公共的或私有的。它们类似于 NuGet 或 npm 注册表。Docker 提供了一个公共注册表(hub.docker.com/_/registry),在那里您可以找到您可能需要在自己的图像中引用的大多数公共图像。然而,每个公司都可以定义私有注册表。例如,Azure 提供了一个私有容器注册表服务:https://azure.microsoft.com/en-us/services/container-registry/。
在实例化每个容器之前,Docker 运行时必须解决所有递归引用。这个繁琐的工作不是每次创建新容器时都执行,因为 Docker 运行时有一个缓存,其中存储了与每个输入镜像相对应的完全组装的镜像,并且这些镜像已经被处理。
由于每个应用程序通常由多个模块组成,这些模块需要在不同的容器中运行,因此 Docker 还允许.yml文件,也称为组合文件,这些文件指定以下信息:
-
部署哪些镜像。
-
每个镜像暴露的内部资源必须映射到宿主机的物理资源。例如,Docker 镜像暴露的通信端口必须映射到物理机的端口。
我们将在本章的*.NET Core 如何处理微服务?*部分分析 Docker 镜像和.yml文件。
Docker 运行时在单台机器上处理镜像和容器,但通常,容器化的微服务是在由多台机器组成的集群上部署和负载均衡的。集群由称为编排器的软件组件处理。编排器将在本章的*需要哪些工具来管理微服务?*部分进行讨论。
现在我们已经了解了微服务是什么,它们可以解决哪些问题,以及它们的基本设计原则,我们准备分析在系统架构中何时以及如何使用它们。下一节将分析何时应该使用它们。
微服务何时有帮助?
这个问题的答案需要我们理解微服务在现代软件架构中扮演的角色。我们将在接下来的小节中探讨这一点。
分层架构和微服务
企业系统通常以逻辑独立的层组织。第一层是与用户交互的层,称为表示层,而最后一层负责存储/检索数据,称为数据层。请求从表示层发起,穿过所有层直到达到数据层,然后返回,反向穿越所有层直到达到表示层,该层负责将结果展示给用户/客户端。层之间不能跳过。
每一层从前一层数据,处理它,并将其传递到下一层。然后,它从下一层接收结果,并将其发送回前一层。此外,抛出的异常不能跨越层——每一层都必须负责拦截所有异常,或者以某种方式解决它们,或者将它们转换为其前一层的语言表达的其他异常。层架构确保了每一层的功能完全独立于其他层的功能。
例如,我们可以更改数据库引擎,而不会影响数据层之上的所有层。同样,我们可以完全更改用户界面,即表示层,而不会影响系统的其余部分。
此外,每一层实现不同类型的系统规范。数据层负责系统必须记住的内容,表示层负责系统-用户交互协议,而中间的所有层实现领域规则,这些规则指定数据必须如何处理(例如,如何计算雇员的工资单)。通常,数据和表示层之间仅由一个领域规则层隔开,称为业务层或应用层。
每一层都说着不同的语言:数据层说着所选存储引擎的语言,业务层说着领域专家的语言,而表示层说着用户的语言。因此,当数据和异常从一个层传递到另一个层时,它们必须被翻译成目标层的语言。
如何构建分层架构的详细示例将在第十章的用例 - 日志微服务部分给出,该部分专门介绍软件解决方案中的不同领域,其链接为 Chapter 10。
话虽如此,微服务如何适应分层架构?它们是否足够满足所有层的功能,或者只是某些层的功能?单个微服务能否跨越几个层?
最后一个问题最容易回答:是的!实际上,我们早已指出,微服务应该在其逻辑边界内存储它们所需的数据。因此,存在跨越业务和数据层的微服务。还有一些其他微服务负责封装共享数据,并保持在数据层内。因此,我们可能有业务层微服务、数据层微服务和跨越两个层的微服务。那么,表示层呢?
如果表示层在服务器端实现,它也可以适应微服务架构。单页应用和移动应用在客户端机器上运行表示层,因此它们要么直接连接到业务微服务层,要么更常见的是连接到一个API 网关,该网关公开接口并负责将请求路由到正确的微服务。
在微服务架构中,当表示层是一个网站时,它可以由一组微服务实现。然而,如果它需要重型 Web 服务器和/或重型框架,容器化它们可能不方便。这个决定还必须考虑容器化 Web 服务器时可能发生的性能损失,以及可能需要在 Web 服务器和系统其余部分之间设置硬件防火墙的需求。
ASP.NET Core 是一个轻量级框架,它在轻量级的 Kestrel 网络服务器上运行,因此它可以有效地容器化并用于内部应用程序的微服务。然而,公共高流量网站需要专门的硬件/软件组件,这阻止了它们与其他微服务一起部署。实际上,虽然 Kestrel 对于内部网站是一个可接受的解决方案,但公共网站需要一个更完整的网络服务器,如 IIS。在这种情况下,安全要求更加紧迫,需要专门的硬件/软件组件。
单体网站可以很容易地被拆分成负载均衡的较小子站,而不需要特定的微服务技术,但微服务架构可以将微服务的所有优势带入单个 HTML 页面的构建中。更具体地说,不同的微服务可能负责每个 HTML 页面的不同区域。不幸的是,在撰写本文时,使用现有的.NET 和.NET Core 技术实现这样的类似场景并不容易。
一个实现基于 ASP.NET Core 的微服务的概念证明,这些微服务协同构建每个 HTML 页面,可以在以下位置找到:github.com/Particular/Workshop/tree/master/demos/asp-net-core。这种方法的主要限制是微服务仅合作生成生成 HTML 页面所需的数据,而不是生成实际的 HTML 页面。相反,这由单体网关处理。实际上,在撰写本文时,如 ASP.NET Core MVC 之类的框架不提供任何用于 HTML 生成的分发功能。我们将在第十三章,展示 ASP.NET Core MVC中回到这个例子。
现在我们已经明确了系统哪些部分可以从采用微服务中受益,我们准备好陈述在决定如何采用它们时的规则。
在什么情况下值得考虑微服务架构?
微服务可以提高业务和数据层的实现,但它们的采用有一些成本:
-
将实例分配到节点并对其进行扩展在云费用或内部基础设施和许可证方面都有成本。
-
将一个独特的过程拆分成更小的通信过程会增加通信成本和硬件需求,尤其是在微服务被容器化的情况下。
-
为微服务设计和测试软件需要更多时间,并增加了人力资源成本。特别是,使微服务具有弹性并确保它们能够充分处理所有可能的故障,以及通过集成测试验证这些功能,可能会将开发时间增加一个数量级以上。
那么,微服务何时值得使用它们的成本?是否有必须作为微服务实现的函数?
对于第一个问题的粗略答案是:是的,当应用在流量和/或软件复杂度方面足够大时。实际上,随着应用在复杂性和流量方面的增长,我们建议我们承担与扩展相关的成本,因为这允许进行更多的扩展优化,并且在开发团队方面有更好的处理能力。我们为此付出的成本很快就会超过采用微服务的成本。
因此,如果细粒度扩展对我们应用有意义,并且我们能够估计细粒度扩展和开发带来的节省,我们就可以轻松计算出整体应用吞吐量限制,这使得采用微服务变得方便。
微服务成本也可以通过我们产品/服务的市场价值增加来证明。由于微服务架构允许我们使用针对其使用进行优化的技术来实现每个微服务,因此我们软件中增加的质量可能证明所有或部分微服务成本是合理的。
然而,扩展和技术优化并不是唯一需要考虑的参数。有时,我们被迫采用微服务架构,而无法进行详细的成本分析。
如果负责整个系统 CI/CD 的团队规模过大,这个大团队的组织和协调会带来困难和低效。在这种情况下,转向一个将整个 CI/CD 周期分解为独立部分,这些部分可以由较小的团队负责的架构是可取的。
此外,由于这些开发成本只有通过高请求量才能证明是合理的,我们可能处理着由不同团队开发的独立模块的高流量。因此,扩展优化和减少开发团队之间交互的需要使得采用微服务架构非常方便。
从这个结论中,我们可能得出,如果系统和开发团队增长得太大,有必要将开发团队分成更小的团队,每个团队负责一个高效的边界上下文子系统。在类似的情况下,微服务架构可能是唯一可行的选择。
另一种迫使采用微服务架构的情况是将基于不同技术的较新子部分与旧子系统集成,因为容器化微服务是唯一能够实现旧系统与新子部分之间有效交互以逐步用较新的子部分替换旧子部分的方式。同样,如果我们的团队由具有不同开发栈经验的开发者组成,基于容器化微服务的架构可能成为必需的。
在下一节中,我们将分析可用的构建块和工具,以便我们可以实现基于.NET Core 的微服务。
.NET Core 如何处理微服务?
.NET Core 被构想为一个轻量级且快速的跨平台框架,足以实现高效的微服务。特别是,ASP.NET Core 是实现 REST API 的理想工具,用于与微服务通信,因为它可以与轻量级网络服务器如 Kestrel 高效运行,并且自身也是轻量级和模块化的。
整个.NET Core 框架都是基于微服务作为战略部署平台而演化的,它提供了构建高效且轻量级 HTTP 通信的设施和包,以确保服务弹性并处理长时间运行的任务。以下小节将描述我们可以使用的一些不同工具或解决方案来实现基于.NET Core 的微服务架构。
.NET Core 通信设施
微服务需要两种类型的通信渠道:
-
接收外部请求的通信渠道,可以是直接或通过 API 网关。由于可用的网络服务标准和工具,HTTP 是外部通信的常用协议。.NET Core 的主要 HTTP 通信设施是 ASP.NET Core,因为它是一个轻量级的 HTTP 框架,这使得它非常适合在小型微服务中实现 Web API。我们将在第十二章中详细描述 ASP.NET Core 应用程序,使用.NET Core 应用服务导向架构,该章节专门介绍 HTTP 服务。.NET Core 还提供了一个高效且模块化的 HTTP 客户端解决方案,能够池化和重用重量级的连接对象。此外,
HttpClient类将在第十二章中更详细地描述,使用.NET Core 应用服务导向架构。 -
一种不同的通信通道类型,用于向其他微服务推送更新。实际上,我们已提到,由于复杂的阻塞调用树会提高请求延迟到不可接受的水平,因此微服务之间的内部通信不能由一个正在进行的请求触发。因此,在它们被使用之前不应立即请求更新,并且应在状态发生变化时推送。理想情况下,这种通信应该是异步的,以实现可接受的性能。实际上,同步调用会在等待结果时阻塞发送者,从而增加每个微服务的空闲时间。然而,如果通信足够快(低通信延迟和高带宽),仅将请求放入处理队列并返回成功通信确认而不是最终结果的同步通信是可以接受的。发布/订阅通信会更可取,因为在这种情况下,发送者和接收者不需要相互了解,从而增加了微服务的独立性。实际上,所有对某种类型的通信感兴趣的接收者只需注册以接收特定的 事件,而发送者只需发布这些事件。所有连接都是由一个负责排队事件并将它们分发给所有订阅者的服务来完成的。发布/订阅模式将在第九章设计模式和 .NET Core 实现中更详细地描述,以及其他有用的模式。
虽然 .NET Core 没有直接提供帮助异步通信的工具,也没有实现发布/订阅通信的客户端/服务器工具,但 Azure 提供了类似的服务,即 Azure Service Bus。Azure Service Bus 通过 Azure Service Bus 队列处理排队异步通信,并通过 Azure Service Bus 主题处理发布/订阅通信。
一旦你在 Azure 门户上配置了 Azure Service Bus,你就可以通过包含在 Microsoft.Azure.ServiceBus NuGet 包中的客户端连接到它,以发送消息/事件并通过接收消息/事件。
Azure Service Bus 有两种通信类型:基于队列和基于主题。在基于队列的通信中,每个发送者放入队列的消息将由第一个从队列中拉取它的接收者移除。另一方面,基于主题的通信是发布/订阅模式的实现。每个主题都有多个订阅,并且每个发送到主题的消息的不同副本都可以从每个主题订阅中拉取。
设计流程如下:
-
定义一个 Azure Service Bus 私有命名空间。
-
获取由 Azure 门户创建的根连接字符串,或定义具有较少权限的新连接字符串。
-
定义发送者将发送其消息的二进制格式的队列和/或主题。
-
对于每个主题,为所有必需的订阅定义名称。
-
在基于队列的通信情况下,发送者向队列发送消息,接收者从同一个队列中拉取消息。每条消息只被发送给一个接收者。也就是说,一旦接收者获得对队列的访问权限,它就会读取并移除一条或多条消息。
-
在基于主题的通信情况下,每个发送者向一个主题发送消息,而每个接收者从与该主题关联的私有订阅中拉取消息。
还有其他商业替代方案,如 NServiceBus、MassTransit、Brighter 和 ActiveMQ。还有一个免费的开源选项:RabbitMQ。RabbitMQ 可以安装在本地、虚拟机或 Docker 容器中。然后,你可以通过包含在RabbitMQ.Client NuGet 包中的客户端与之连接。
RabbitMQ 的功能与 Azure Service Bus 提供的功能类似,但你必须注意所有实现细节、执行操作的确认等,而 Azure Service Bus 则负责所有底层任务并提供一个更简单的接口。Azure Service Bus 和 RabbitMQ 将与基于发布/订阅的通信一起在第九章,“设计模式和.NET Core 实现”中描述。
如果微服务发布到将在下一节中描述的 Azure Service Fabric,我们可以使用内置的可靠二进制通信。由于通信原语自动使用重试策略,因此通信是弹性的。这种通信是同步的,但这不是一个大限制,因为 Azure Service Fabric 中的微服务具有内置队列;因此,一旦接收者收到消息,它们只需将其放入队列并立即返回,而不会阻塞发送者。
队列中的消息随后由一个单独的线程进行处理。这种内置通信的主要局限性在于它不是基于发布/订阅模式;发送者和接收者必须相互了解。当这不可接受时,你应该使用 Azure Service Bus。我们将在本章的“用例 - 日志微服务”部分学习如何使用 Service Fabric 的内置通信。
弹性任务执行
使用由.NET Foundation 维护的名为 Polly 的.NET Core 库,可以轻松实现弹性通信和一般性的弹性任务执行。Polly 可以通过 Polly NuGet 包获取。
在 Polly 中,你定义策略,然后在该策略的上下文中执行任务,如下所示:
var myPolicy = Policy
.Handle<HttpRequestException>()
.Or<OperationCanceledException>()
.Retry(3);
....
....
myPolicy.Execute(()=>{
//your code here
});
每个策略的第一部分指定必须处理的异常。然后,您指定在捕获到这些异常之一时执行的操作。在前面的代码中,如果报告失败是由 HttpRequestException 异常或 OperationCanceledException 异常引起的,则 Execute 方法将重试最多三次。
以下是一个指数重试策略的实现:
var erPolicy= Policy
...
//Exceptions to handle here
.WaitAndRetry(6,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2,
retryAttempt)));
WaitAndRetry 的第一个参数指定在失败的情况下最多执行六次重试。作为第二个参数传递的 lambda 函数指定在下次尝试之前等待的时间。在具体示例中,这个时间随着尝试次数的指数增长,以 2 的幂次(第一次重试为 2 秒,第二次重试为 4 秒,依此类推)。
以下是一个简单的断路器策略:
var cbPolicy=Policy
.Handle<SomeExceptionType>()
.CircuitBreaker(6, TimeSpan.FromMinutes(1));
在连续失败六次后,由于返回异常,任务将无法执行 1 分钟。
以下是 Bulkhead 隔离策略的实现(有关更多信息,请参阅 微服务设计原则 部分):
Policy
.Bulkhead(10, 15)
在 Execute 方法中允许最多 10 个并行执行。进一步的任务将被插入到执行队列中。这个队列的容量限制为 15 个任务。如果队列限制被超过,将抛出异常。
为了使 Bulkhead 策略正常工作,以及通常为了使每个策略正常工作,任务执行必须通过相同的策略实例触发;否则,Polly 无法计算特定任务的活跃执行次数。
可以将策略与 Wrap 方法结合使用:
var combinedPolicy = Policy
.Wrap(erPolicy, cbPolicy);
Polly 提供了更多选项,例如针对返回特定类型的任务的泛型方法、超时策略、任务结果缓存、定义自定义策略的能力等。官方 Polly 文档的链接在 进一步阅读 部分中。
使用泛型托管器
每个微服务可能需要运行几个独立的线程,每个线程对收到的请求执行不同的操作。这些线程需要几个资源,例如数据库连接、通信通道、执行复杂操作的专业模块等。此外,所有处理线程必须在微服务启动时得到适当的初始化,并在微服务由于负载均衡或错误而停止时优雅地停止。
所有这些需求促使 .NET Core 团队构思并实现了 托管服务 和 托管器。托管器为运行多个任务(称为 托管服务)提供了一个适当的环境,并为它们提供资源、通用设置和优雅的启动/停止。
网站托管的概念最初主要是为了实现 ASP.NET Core 网络框架,但从 .NET Core 2.1 开始,托管概念被扩展到所有 .NET 应用程序。与“托管”概念相关的所有功能都包含在 Microsoft.Extensions.Hosting NuGet 包中。
首先,你需要使用流畅接口配置主机,从 HostBuilder 实例开始。此配置的最终步骤是调用 Build 方法,该方法将所有提供的配置信息组装成实际的主机:
var myHost=new HostBuilder()
//Several chained calls
//defining Host configuration
.Build();
主机配置包括定义常用资源,定义文件默认文件夹,从多个来源(JSON 文件、环境变量以及传递给应用程序的任何参数)加载配置参数,并声明所有托管服务。
然后,可以启动主机,这将导致所有托管服务启动:
host.Start();
程序在上一条指令上保持阻塞,直到主机关闭。主机可以通过托管服务之一或通过调用 await host.StopAsync(timeout) 外部关闭。在这里,timeout 是一个时间跨度,定义了等待托管服务优雅停止的最大时间。在此时间之后,如果托管服务尚未终止,则所有托管服务都将被终止。
通常,微服务正在关闭的事实是通过在协调器启动微服务时传递 CancelationToken 来表示的。这发生在微服务托管在 Azure Service Fabric 中时。
在这种情况下,我们不必使用 host.Start(),而是可以使用 RunAsync 方法,并传递我们从协调器那里接收到的 CancelationToken:
await host.RunAsync(cancelationToken)
这种关闭方式在 cancelationToken 进入已取消状态时立即触发。默认情况下,主机关闭超时为 5 秒;也就是说,一旦请求关闭,它将等待 5 秒后才退出。这个时间可以在 ConfigureServices 方法中更改,该方法用于声明 托管服务 和其他资源:
var myHost = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.Configure<HostOptions>(option =>
{
option.ShutdownTimeout = System.TimeSpan.FromSeconds(10);
});
....
....
//further configuration
})
.Build();
然而,增加主机超时并不会增加协调器超时,所以如果主机等待时间过长,整个微服务将被协调器杀死。
托管服务是实现 IHostedService 接口的实现,其唯一的方法是 StartAsync(CancellationToken) 和 StopAsync(CancellationToken)。这两个方法都传递了一个 CancelationToken。在 StartAsync 方法中,CancelationToken 表示请求关闭。StartAsync 方法在执行启动主机所需的所有操作时,会定期检查这个 CancelationToken,如果它被触发,则主机启动过程将被终止。另一方面,在 StopAsync 方法中,CancelationToken 表示关闭超时已过期。
托管服务必须在用于定义主机选项的相同 ConfigureServices 方法中声明,如下所示:
services.AddHostedService<MyHostedService>();
ConfigureServices 中的大多数声明都需要添加以下命名空间:
using Microsoft.Extensions.DependencyInjection;
通常,IHostedService 接口不是直接实现的,而是可以继承自 BackgroundService 抽象类,该类公开了更容易实现的 ExecuteAsync(CancellationToken) 方法,我们可以在其中放置整个服务的逻辑。通过传递 CancellationToken 作为参数来表示关闭信号,这更容易处理。我们将在本章末尾的示例中查看 IHostedService 的实现。
为了允许托管服务关闭宿主,我们需要将其构造函数参数声明为 IApplicationLifetime 接口:
public class MyHostedService: BackgroundService
{
private applicationLifetime;
public MyHostedService(IApplicationLifetime applicationLifetime)
{
this.applicationLifetime=applicationLifetime;
}
protected Task ExecuteAsync(CancellationToken token)
{
...
applicationLifetime.StopApplication();
...
}
}
当托管服务被创建时,它将自动传递一个 IApplicationLifetime 的实现,其 StopApplication 方法将触发宿主关闭。这种实现是自动处理的,但我们可以声明自定义资源,其实例将被自动传递到所有声明它们为参数的宿主服务构造函数。定义这些资源有几种方式:
services.AddTransient<MyResource>();
services.AddTransient<IResourceInterface, MyResource>();
services.AddSingleton<MyResource>();
services.AddSingleton<IResourceInterface, MyResource>();
当我们使用 AddTransient 时,将创建一个不同的实例并将其传递给所有需要该类型实例的构造函数。另一方面,使用 AddSingleton 时,将创建一个唯一的实例并将其传递给所有需要声明类型的构造函数。具有两个泛型类型的重载允许你传递一个接口及其实现该接口的类型。这样,构造函数需要接口,并且与该接口的具体实现解耦。
如果资源构造函数包含参数,它们将以递归方式自动使用在 ConfigureServices 中声明的类型进行实例化。这种与资源交互的模式称为 依赖注入(DI),将在第九章 设计模式和 .NET Core 实现 中详细讨论。
HostBuilder 还有一个我们可以用来定义默认文件夹的方法:
.UseContentRoot("c:\\<deault path>")
它还提供了我们可以用来添加日志目标的方法:
.ConfigureLogging((hostContext, configLogging) =>
{
configLogging.AddConsole();
configLogging.AddDebug();
})
上述示例展示了基于控制台的日志源,但我们可以使用适当的提供程序将日志记录到 Azure 目标。进一步阅读 部分包含了一些可以与在 Azure Service Fabric 中部署的微服务一起工作的 Azure 日志提供程序的链接。一旦配置了日志,你就可以通过在它们的构造函数中添加 ILoggerFactory 参数来启用你的托管服务并记录自定义消息。
最后,HostBuilder 有我们可以用来从各种来源读取配置参数的方法:
.ConfigureHostConfiguration(configHost =>
{
configHost.AddJsonFile("settings.json", optional: true);
configHost.AddEnvironmentVariables(prefix: "PREFIX_");
configHost.AddCommandLine(args);
})
在第十三章 展示 ASP.NET Core MVC 中,我们将更详细地解释如何在应用程序内部使用参数,该章节专门介绍 ASP.NET Core。
Visual Studio 对 Docker 的支持
Visual Studio 提供了创建、调试和部署 Docker 镜像的支持。Docker 部署需要我们在开发机器上安装Windows Docker CE,这样我们才能运行 Docker 镜像。下载链接可以在本章开头的技术要求部分找到。在我们开始任何开发活动之前,我们必须确保它已安装并正在运行(当 Docker 运行时,你应该在窗口通知栏中看到 Docker 图标)。
Docker 支持将通过一个简单的 ASP.NET Core MVC 项目进行描述。让我们创建一个。为此,请按照以下步骤操作:
-
将项目命名为
MvcDockerTest。 -
为了简单起见,禁用身份验证。
-
当你创建项目时,你可以选择添加 Docker 支持,但请不要勾选 Docker 支持复选框。你可以在项目创建后测试如何将 Docker 支持添加到任何项目中。
一旦你的 ASP.NET Core MVC 应用程序已经搭建并运行,在解决方案资源管理器中右键单击其项目图标,选择容器编排器支持 | Docker Compose。这将不仅启用 Docker 镜像的创建,还会创建一个 Docker Compose 项目,这有助于你配置 Docker Compose 文件,以便它们可以同时运行和部署多个 Docker 镜像。实际上,如果你将另一个 MVC 项目添加到解决方案中并为它启用容器编排器支持,新的 Docker 镜像将被添加到同一个 Docker Compose 文件中。
启用 Docker Compose 而不是仅仅docker的优势在于,你可以通过编辑添加到解决方案中的 Docker Compose 文件来手动配置镜像在开发机器上的运行方式,以及 Docker 镜像端口如何映射到外部端口。
如果你的 Docker 运行时已正确安装并正在运行,你应该能够从 Visual Studio 运行 Docker 镜像。
让我们分析由 Visual Studio 创建的 Docker 文件。它是一系列创建镜像的步骤。每个步骤都通过From指令(它是对已存在镜像的引用)帮助丰富现有的镜像。以下是最初的步骤:
FROM microsoft/dotnet:x.x-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
第一步使用了由 Microsoft 在 Docker 公共仓库中发布的microsoft/dotnet:x.x-aspnetcore-runtime ASP.NET Core 运行时(其中x.x是你在项目中选择的 ASP.NET Core 版本)。
WORKDIR命令在将要创建的镜像中创建当前目录下的目录。如果目录尚不存在,它将在镜像中创建。两个EXPOSE命令声明了哪些镜像端口将暴露在镜像外部并映射到实际托管机器。映射的端口在部署阶段决定,可以是 Docker 命令的命令行参数,也可以是在 Docker Compose 文件中。在我们的例子中,有两个端口:一个用于 HTTP(80),另一个用于 HTTPS(443)。
这个中间图像被 Docker 缓存,因为它不依赖于我们在选定的 ASP.NET Core 运行时版本上编写的代码,所以不需要重新计算它。
第二步生成一个不同的镜像,它将不会用于部署。相反,它将用于创建特定于应用程序的文件,这些文件将被部署:
FROM microsoft/dotnet:x.x-sdk AS build
WORKDIR /src
COPY MvcDockerTest/MvcDockerTest.csproj MvcDockerTest/
RUN dotnet restore MvcDockerTest/MvcDockerTest.csproj
COPY . .
WORKDIR /src/MvcDockerTest
RUN dotnet build MvcDockerTest.csproj -c Release -o /app
FROM build AS publish
RUN dotnet publish MvcDockerTest.csproj -c Release -o /app
此步骤从 ASP.NET SDK 镜像开始,该镜像包含我们不需添加到部署中的部分;这些部分是处理项目代码所需的。在build镜像中创建新的src目录,并将其设置为当前镜像目录。然后,项目文件被复制到/src/MvcDockerTest。
RUN命令在镜像上执行操作系统命令。在这种情况下,它调用dotnet运行时,请求它恢复之前复制的项目文件中引用的 NuGet 包。
然后,COPY..命令将整个项目文件树复制到src镜像目录。最后,将项目目录设置为当前目录,并请求dotnet运行时以发布模式构建项目,并将所有输出文件复制到新的/app目录。最后,一个新的名为publish的镜像在输出文件上执行publish命令。
最后一步从我们在第一步中创建的镜像开始,该镜像包含 ASP.NET Core 运行时,并添加了之前步骤中发布的所有文件:
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "MvcDockerTest.dll"]
ENTRYPOINT命令指定执行镜像所需的操作系统命令。它接受一个字符串数组。在我们的情况下,它接受dotnet命令及其第一个命令行参数,即我们需要执行的 DLL。
如果我们右键单击我们的项目并单击发布,我们会看到几个选项:
-
将镜像发布到现有的或新的 Web 应用(由 Visual Studio 自动创建)
-
发布到多个 Docker 注册表中,包括一个私有的 Azure 容器注册表,如果它还不存在,可以从 Visual Studio 内部创建。
-
发布到 Azure 虚拟机
Docker Compose 支持允许您运行和发布多容器应用程序,并添加更多镜像,例如可在任何地方使用的容器化数据库。
以下 Docker Compose 文件将两个 ASP.NET Core 应用程序添加到同一个 Docker 镜像中:
version: '3.4'
services:
mvcdockertest:
image: ${DOCKER_REGISTRY-}mvcdockertest
build:
context: .
dockerfile: MvcDockerTest/Dockerfile
mvcdockertest1:
image: ${DOCKER_REGISTRY-}mvcdockertest1
build:
context: .
dockerfile: MvcDockerTest1/Dockerfile
上述代码引用现有的 Docker 文件。任何依赖于环境的信息都放置在docker-compose.override.yml文件中,当应用程序从 Visual Studio 启动时,该文件与docker-compose.yml文件合并:
version: '3.4'
services:
mvcdockertest:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
- ASPNETCORE_HTTPS_PORT=44355
ports:
- "3150:80"
- "44355:443"
volumes:
- ${APPDATA}/Asp.NET/Https:/root/.aspnet/https:ro
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
mvcdockertest1:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
- ASPNETCORE_HTTPS_PORT=44317
ports:
- "3172:80"
- "44317:443"
volumes:
- ${APPDATA}/Asp.NET/Https:/root/.aspnet/https:ro
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
对于每个镜像,文件定义了一些环境变量,这些变量将在应用程序启动时在镜像中定义,端口映射和一些主机文件。
主机上的文件直接映射到镜像中,所以如果镜像没有映射到包含这些文件的宿主机,镜像将无法正常运行。每个声明包含宿主机中的路径,路径在镜像中的映射方式,以及期望的访问权限。在我们的例子中,使用 volumes 映射了 Visual Studio 和 ASP.NET Core 所使用的自签名 https 证书以及用户密钥(加密设置)。
现在,假设我们想要添加一个容器化的 SQL Server 实例。我们需要像以下指令一样在 docker-compose.yml 和 docker-compose.override.yml 之间分割:
sql.data:
image: mssql-server-linux:latest
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
ports:
- "5433:1433"
在这里,前面的代码指定了 SQL Server 容器的属性,以及 SQL 服务器配置和安装参数。更具体地说,前面的代码包含以下信息:
-
sql.data是分配给容器的名称。 -
image指定了从哪里获取镜像。在我们的例子中,镜像包含在公共 Docker 仓库中。 -
environment指定了 SQL Server 需要的环境变量,即管理员密码和接受 SQL Server 许可证的协议。 -
如同往常,
ports指定了端口映射。
docker-compose.override.yml 用于在 Visual Studio 内运行镜像。如果你需要为生产环境或测试环境指定参数,可以添加额外的 docker-compose-xxx.override.yml 文件,例如 docker-compose-staging.override.yml 和 docker-compose-production.override.yml,然后在目标环境中手动启动它们,如下面的代码所示:
docker-compose -f docker-compose.yml -f docker-compose-staging.override.yml
然后,你可以使用以下代码销毁所有容器:
docker-compose -f docker-compose.yml -f docker-compose.test.staging.yml down
虽然 docker-compose 在处理节点集群方面能力有限,但它主要用于测试和开发环境。对于生产环境,需要更复杂的工具,我们将在 需要哪些工具来管理微服务? 这一部分中看到。
Azure 和 Visual Studio 对微服务编排的支持
Visual Studio 为基于 Service Fabric 平台的微服务应用程序提供了一个特定的项目模板,你可以定义各种微服务,配置它们,并将它们部署到 Azure Service Fabric,这是一个微服务编排器。Azure Service Fabric 将在下一节中更详细地描述。
在本节中,我们将描述你可以在 Service Fabric 应用程序中定义的微服务类型。最后一节将提供一个完整的代码示例。如果你想在开发机器上调试微服务,需要安装本章技术要求中列出的 Service Fabric 模拟器。
通过在 Visual Studio 项目类型下拉筛选器中选择 云 可以找到 Service Fabric 应用程序。一旦选择了项目,你可以从各种服务中进行选择:
所有.NET Core 项目都使用特定于 Azure Service Fabric 的微服务模型。访客可执行文件在现有 Windows 应用程序周围添加一个包装器,将其转换为可以在 Azure Service Fabric 中运行的微服务。容器应用程序允许在 Service Fabric 应用程序中添加任何 Docker 镜像。所有其他选项都构建了一个模板,允许您使用 Service Fabric 特定的模式编写微服务。
一旦您在先前的屏幕截图中选择任何选项并填写所有请求信息,Visual Studio 将创建两个项目:一个包含整体应用程序配置信息的应用程序项目,以及一个包含所选特定服务代码和服务特定配置的项目。如果您想向应用程序添加更多微服务,请右键单击应用程序项目并选择“添加”|“新 Service Fabric 服务”。
如果您在解决方案上右键单击并选择“添加”|“新项目”,将创建一个新的 Service Fabric 应用程序,而不是将新服务添加到已存在的应用程序中。
如果您选择访客可执行文件,您需要提供以下信息:
-
包含主要可执行文件及其所有必要文件的文件夹。如果您想在项目中创建此文件夹的副本或简单地链接到现有文件夹,则需要此文件夹。
-
主要可执行文件。
-
要传递给该可执行文件的命令行参数。
-
在 Azure 上使用哪个文件夹作为工作文件夹。您希望使用包含主要可执行文件(
CodeBase)的文件夹,Azure Service Fabric 将整个微服务打包到的文件夹(CodePackage),或者一个名为Work的新子文件夹。
如果您选择容器,您需要提供以下信息:
-
您私有 Azure 容器注册表中 Docker 镜像的完整名称。
-
将用于连接到 Azure 容器注册表的用户名。密码将在为该用户名自动创建的应用程序配置文件中的相同
RepositoryCredentialsXML 元素中手动指定。 -
您可以访问服务(主机端口)的端口以及主机端口必须映射到的容器内部端口(容器端口)。容器端口必须是 Docker 文件中公开的相同端口,并用于定义 Docker 镜像。
之后,您可能需要添加进一步的手动配置以确保您的 Docker 应用程序能够正常工作。进一步阅读部分包含指向官方文档的链接,您可以在那里找到更多详细信息。
有五种类型的.NET Core 原生 Service Fabric 服务。Actor 服务模式是一种由 Carl Hewitt 几年前构思的具有偏见的模式。我们在此不讨论它,但进一步阅读部分包含一些链接,提供了更多关于此模式的信息。
剩下的四种模式涉及 ASP.NET Core 作为主要交互协议的使用(或未使用),以及服务是否有内部状态。实际上,Service Fabric 允许微服务使用分布式队列和字典,这些队列和字典对所有声明它们的微服务的实例都是全局可访问的,无论它们运行在哪个硬件节点上(当需要时,它们会被序列化和分布到所有可用的实例中)。
有状态和无状态模板主要在配置方面有所不同。所有原生服务都是指定仅两个方法的类:
protected override IEnumerable<ServiceReplicaListener> CreateServiceReplicaListeners()
protected override async Task RunAsync(CancellationToken cancellationToken)
CreateServiceReplicaListeners方法指定了一个由微服务用来接收消息及其处理这些消息的代码的监听器列表。监听器可以使用任何协议,但它们必须指定相对套接字的实现。
RunAsync包含后台线程的代码,这些线程异步运行由接收到的消息触发的任务。在这里,你可以构建运行多个托管服务的宿主。
ASP.NET Core 模板遵循相同的模式;然而,它们使用基于 ASP.NET Core 的独特监听器和没有RunAsync实现,因为后台任务可以从 ASP.NET Core 内部启动。但是,你可以向由 Visual Studio 创建的CreateServiceReplicaListeners实现返回的监听器数组中添加更多的监听器,还可以添加自定义的RunAsync重写。
在“哪些工具需要用来管理微服务?”这一节中,将提供有关 Service Fabric 原生服务模式的更多详细信息,而在本章的“测试应用程序”部分将提供一个完整的代码示例,该部分专门用于本书的使用案例。
虽然本节介绍了我们可以用来为我们的微服务构建代码的工具,但下一节将描述我们可以用来定义和管理微服务部署的集群的工具。
哪些工具需要用来管理微服务?
在 CI/CD 周期中有效地处理微服务需要私有 Docker 镜像注册库和最先进的微服务编排器,该编排器能够执行以下操作:
-
在可用的硬件节点上分配和负载均衡微服务
-
监控服务的健康状态,并在硬件/软件故障发生时替换故障服务
-
记录和展示分析
-
允许设计者动态更改需求,例如分配给集群的硬件节点、服务实例数量等
以下小节描述了我们可以用来存储 Docker 镜像和编排微服务的 Azure 设施。
在 Azure 中定义你的私有 Docker 注册库
在 Azure 中定义你的私有 Docker 注册库很容易。只需在 Azure 搜索栏中输入“容器注册库”并选择容器注册库。在出现的页面上,点击添加按钮。
将出现以下形式:
您选择的名称用于组成整体注册表 URI:<name>.azurecr.io。像往常一样,您可以指定订阅、资源组和位置。SKU 下拉菜单允许您从具有不同性能、可用内存和其他一些辅助功能的各个级别中选择。
如果您启用管理员用户,将创建一个管理员用户,其用户名为 <name>,密码由门户自动创建;否则,用户将使用您的 Azure 门户凭据登录。一旦选择了管理员用户,其登录信息将在资源 访问密钥 菜单项下可用。
在 Docker 命令或 Visual Studio 发布表单中提及镜像名称时,必须在名称前加上注册表 URI:<name>.azurecr.io/<my imagename>。
如果使用 Visual Studio 创建镜像,则可以按照发布项目后出现的说明进行发布。否则,您必须使用 docker 命令将它们推送到您的注册表。
假设您在其他注册表中拥有该镜像。第一步是将镜像拉到您的本地计算机上:
docker pull other.registry.io/samples/myimage
如果前一个镜像有多个版本,由于没有指定版本,将拉取最新版本。可以按照以下方式指定镜像版本:
docker pull other.registry.io/samples/myimage:version1.0
使用以下命令,您应该在本地镜像列表中看到 myimage:
docker images
现在,通过输入以下命令并输入您的凭据来登录 Azure:
docker login myregistry.azurecr.io
然后,使用您想要分配的路径在 Azure 注册表中标记镜像:
docker tag myimage myregistry.azurecr.io/testpath/myimage
名称和目标标签都可以有版本 (:<版本名称>)。
最后,推送它:
docker push myregistry.azurecr.io/testpath/myimage
在这种情况下,您可以指定版本;否则,将推送最新版本。
通过执行以下命令,您应该可以在本地镜像列表中看到 myimage:
docker rmi myregistry.azurecr.io/testpath/myimage
Azure 服务 Fabric
Azure 服务 Fabric 是主要的 Microsoft 调度器,可以托管 Docker 容器、原生 .NET 应用程序以及称为 可靠服务 的分布式计算模型。我们已经在 Azure 和 Visual Studio 对微服务调度的支持 子部分中解释了如何创建和发布包含这三种类型服务的应用程序。在本节中,我们将解释如何在 Azure 门户中创建 Azure 服务 Fabric 集群,并提供有关 可靠服务 的更多详细信息。有关 可靠服务 的更多实际细节将在 用例 - 记录微服务 部分的示例中提供。
您可以通过在 Azure 搜索栏中输入 Service Fabric 并选择 Service Fabric Cluster 来进入 Azure 的 Service Fabric 部分。将出现一个多步骤向导。以下子部分描述了可用的步骤。
第 1 步:基本信息
以下截图显示了 Azure 服务 Fabric 的创建:
在这里,您可以选择要用于连接远程桌面到所有集群节点的操作系统、资源组、订阅、位置、用户名和密码。您必须选择一个集群名称,该名称将用于组成集群 URI,即 <cluster name>.<location>.cloudapp.azure.com,其中 location 是与您选择的数据中心位置相关联的名称。
第 2 步:集群配置
在第二步中,您可以配置节点数量及其功能:
您可以指定最多三种节点类型。不同节点类型的节点可以独立扩展,节点类型 1,称为主节点类型,是 Azure Service Fabric 运行时服务托管的地方。对于每种节点类型,您可以指定机器类型(耐久性层)、机器尺寸(CPU 和 RAM)以及初始节点数量。
您还可以指定所有将从集群外部可见的端口(自定义端点)。
集群不同节点上托管的服务可以通过任何端口进行通信,因为它们是同一本地网络的一部分。因此,自定义端点必须声明需要接受来自集群外部的流量的端口。在自定义端点中公开的端口是集群的公共接口,可以通过集群 URI 访问,即 <cluster name>.<location>.cloudapp.azure.com。它们的流量会自动重定向到所有由集群负载均衡器打开相同端口的微服务。
要理解启用反向代理选项,我们必须解释如何将通信发送到在它们的生命周期中物理地址发生变化的服务的多个实例。在集群内部,服务通过 URI(如 fabric://<application name>/<service name>)进行标识。也就是说,此名称允许我们访问 <service name> 的几个负载均衡实例之一。然而,这些 URI 不能直接由通信协议使用。相反,它们用于从 Service Fabric 命名服务获取所需资源的物理 URI,以及所有可用的端口和协议。
之后,我们将学习如何使用可靠服务执行此操作。然而,此模型不适用于未设计在 Azure Service Fabric 上运行的 Docker 化服务,因为它们不了解 Service Fabric 特定的命名服务和 API。
因此,Service Fabric 提供了两个额外的选项,我们可以使用它们来标准化 URL,而不是直接与其命名服务交互:
-
DNS:每个服务都可以指定其
hostname(也称为其DNS 名称)。DNS 服务负责将其转换为实际的服务 URL。例如,如果一个服务指定了order.processingDNS 名称,并且它在端口80上有一个 HTTP 端点以及/purchase路径,我们可以通过http://order.processing:80/purchase访问此端点。 -
反向代理:Service Fabric 的反向代理拦截所有被定向到集群地址的调用,并使用命名服务将它们发送到该应用程序内的正确应用程序和服务。由反向代理服务解析的地址具有以下结构:
<cluster name>.<location>.cloudapp.azure.com: <port>//<app name>/<service name>/<endpoint path>?PartitionKey=<value> & PartitionKind=value。在这里,分区键用于优化状态、完全可靠的服务,将在本小节的末尾进行解释。这意味着无状态服务缺少先前地址的查询字符串部分。因此,由反向代理解决的典型地址可能类似于myCluster.eastus.cloudapp.azure.com: 80//myapp/myservice/<endpoint path>?PartitionKey=A & PartitionKind=Named。如果从同一集群托管的服务调用前面的端点,我们可以指定localhost而不是完整的集群名称(即,来自同一集群,而不是来自同一节点):localhost: 80//myapp/myservice/<endpoint path>?PartitionKey=A & PartitionKind=Named。
默认情况下,DNS 服务已激活,但反向代理未激活。因此,我们必须通过在 Service Fabric 配置的第二步中勾选“启用反向代理”复选框来启用它。
第 3 步:安全配置
提交第二步后,我们将进入一个安全页面:
如果我们选择基本选项,向导将创建一个 X509 证书来保护我们与集群的通信。否则,我们可以从 Azure Key Vault 中选择一个现有的证书。如果您没有密钥保管库,向导将要求您创建一个,以便您可以存储新创建的证书。在证书选项中,找到证书用途选项并选择发布/部署。如果不这样做,您将收到一个错误消息,以及一些指示您如何修复问题的说明。
一旦证书准备就绪,请按照向导的说明将其下载到您的计算机上(双击安装),并在本地计算机上安装它。该证书将用于从您的计算机部署应用程序。具体来说,您需要将以下信息插入 Visual Studio Service Fabric 应用程序的云发布配置文件中(有关更多详细信息,请参阅本章的“用例 - 记录微服务”部分):
<ClusterConnectionParameters
ConnectionEndpoint="<cluster name>.<location
code>.cloudapp.azure.com:19000"
X509Credential="true"
ServerCertThumbprint="<server certificate thumbprint>"
FindType="FindByThumbprint"
FindValue="<client certificate thumbprint>"
StoreLocation="CurrentUser"
StoreName="My" />
由于客户端(Visual Studio)和服务器使用相同的证书进行身份验证,因此服务器和客户端的指纹相同。证书指纹可以从您的 Azure Key Vault 复制。值得一提的是,您还可以通过在步骤 3中选择自定义选项,添加与主服务器证书一起使用的客户端特定证书。
一旦您提交您的证书,您将看到您的配置摘要。提交您的批准将创建集群。请注意:集群可能在短时间内消耗您的 Azure 免费信用额度,因此当您在测试时,请保持集群开启。之后,您应该将其删除。
正如我们在Azure 和 Visual Studio 对微服务编排的支持子节中提到的,Azure Service Fabric 支持两种类型的可靠服务:无状态和有状态。无状态服务要么不存储永久数据,要么将其存储在外部支持中,例如 Redis 缓存或数据库(有关 Azure 提供的主要存储选项,请参阅第七章,如何在云中选择您的数据存储)。
与此相反,有状态服务使用 Service Fabric 特定的分布式字典和队列。每个分布式数据结构都可以从服务的所有相同副本中访问,但只有一个副本,称为主副本,被允许在其上写入,以避免对这些分布式资源的同步访问,这可能会导致瓶颈。所有其他副本,称为辅助副本,只能从这些分布式数据结构中读取。
您可以通过查看代码从 Azure Service Fabric 运行时接收到的上下文对象来检查副本是否为主副本,但通常您不需要这样做。实际上,当您声明服务端点时,您需要声明那些只读的端点。只读端点应该接收请求,以便它可以从共享数据结构中读取数据。因此,由于只有只读端点被激活在辅助副本上,如果您正确实现它们,则无需进行进一步检查,写入/更新操作应该自动在有状态辅助副本上被阻止。
在有状态服务中,辅助副本在读取操作上启用并行性,因此为了在写入/更新操作上获得并行性,有状态服务被分配不同的数据分区。更具体地说,对于每个有状态服务,Service Fabric 为每个分区创建一个主实例。然后,每个分区可能有几个辅助副本。
分布式数据结构在分区的每个主实例及其辅助副本之间共享。有状态服务可以存储的数据总量根据要存储的数据上生成的分区键的数量来分割。
通常,分区键是整数,属于分配给所有可用分区的给定区间。例如,可以通过在.NET 的GetHashCode()方法上对一个或多个字符串字段进行调用来生成分区键,从而得到整数,然后对这些整数进行处理以获得一个唯一的整数(例如,通过在整数位上执行异或操作)。然后,可以通过取整数除法的余数(例如,除以 1,000 的余数将是一个在 0-999 区间的整数)来将这个整数约束到为分区键选择的整数区间。
假设我们想要四个分区,这些分区将通过 0-999 区间的整数键进行选择。在这里,Service Fabric 将自动创建我们状态化服务的四个主实例,并将以下四个分区键子区间分配给它们:0-249,250-499,500-749,750-999。
在您的代码内部,您需要计算发送到状态化服务的数据的分区键。然后,Service Fabric 的运行时将为您选择正确的首选实例。本章末尾的“用例 - 日志微服务”部分提供了更多关于此以及如何在实践中使用可靠服务的实际细节。
Azure Kubernetes Service (AKS)
Kubernetes 是一个高级开源编排器,您可以在本地安装到您的私有机器集群上。在撰写本文时,它是应用最广泛的编排器,因此 Microsoft 将其作为 Azure Service Fabric 的替代品提供。此外,如果您更喜欢 Azure Service Fabric,您可能被迫使用Azure Kubernetes Service(AKS),因为一些高级解决方案(例如,一些大数据解决方案)是建立在 Kubernetes 之上的。本节提供了一个关于 AKS 的简要介绍,但更多详细信息可以在官方文档中找到,该文档在“进一步阅读”部分有引用。
要创建 AKS 集群,请在 Azure 搜索中输入AKS,选择 Kubernetes 服务,然后点击“添加”按钮。以下表单将出现:
如同往常,您需要指定订阅、资源组和位置。然后,您可以选择一个唯一的名称(Kubernetes 集群名称)、集群 URI 的前缀(DNS 名称前缀)以及您想要使用的 Kubernetes 版本。为了计算能力,您需要为每个节点选择一个机器模板(节点大小)和节点数量。如果您点击“下一步”,您可以提供安全信息,即一个服务主体,并指定您是否希望启用基于角色的访问控制。在 Azure 中,服务主体是与您可能使用的服务关联的账户,您可以使用它来定义资源访问策略。如果您没有这方面的经验,或者您没有现有的服务主体,可以让向导为您创建一个。
您还可以更改其他设置,但默认值效果良好。
一旦创建了 Kubernetes 集群,您就可以通过 kubectl 命令行工具与之交互。kubectl 集成到 Azure 控制台中,因此您只需激活您的集群凭据。在页面门户的顶部选择 Azure 控制台,然后输入以下命令:
az aks get-credentials --resource-group <resource group> --name <cluster name>
前面的命令下载了自动创建的凭据,以启用您与集群的交互,并配置 Kubernetes CLI 以使用它们。
然后,如果您编写 kubectl get nodes,您应该会得到一个可用的 Kubernetes 节点列表。
可以通过编写一个 .yaml 配置文件(例如 myClusterConfiguration.yaml)并将以下命令输入到集群中来将 Docker 镜像加载到集群中并进行配置:
kubectl apply -f myClusterConfiguration.yaml
您可以通过在 Azure 控制台中输入 nano 来创建和编辑此文件,以启动 nano 编辑器。一旦进入编辑器,您可以从本地文件粘贴内容,然后保存它。
前面的命令部署了应用程序并运行它。可以使用以下命令监控部署状态:
kubectl get service MyDeployment --watch
在这里,MyDeployment 是在 .yaml 文件中分配给部署的名称。
当集群不再需要时,您可以使用以下命令将其删除:
az aks delete --resource-group <resource group> --name <cluster name> --no-wait
您可以通过从资源 Azure 菜单中选择洞察来监控应用程序状态。在这里,您可以应用过滤器并选择您所需的信息。
.yaml 文件的结构与 JSON 文件相同,但它们的语法不同。您有对象和列表,但对象的属性不需要用 {} 包围,列表也不需要用 [] 包围。相反,通过简单地使用空格缩进来声明嵌套对象的内容。空格的数量可以自由选择,但一旦选择,就必须连贯地使用。
列表项可以通过在它们前面加上破折号(-)来与对象属性区分开来。.yaml 文件可以包含多个由包含 --- 字符串的行分隔的章节。通常,您定义一个 Deployment 来描述要部署哪些镜像以及它们必须有多少个副本。每个部署将一组要一起部署到同一节点上的镜像分组在一起,这意味着在任意节点上部署的每个副本都必须安装所有这些镜像。要一起部署的镜像集称为 pod。
例如,以下配置部署了一个包含单个镜像的 pod 的两个副本:
apiVersion: apps/v1
kind: Deployment
metadata:
name: MyDeployment
spec:
replicas: 2
selector:
matchLabels:
app: MyApplication
template:
...
...
初始头声明了要使用的 Kubernetes API 版本和我们将要定义的对象类型(一个部署),并为对象分配一个名称。部署名称可以在以后使用部署编辑命令来修改集群。
template:
metadata:
labels:
app: MyApplication
spec:
containers:
- name: MyContainerName
image: myregistry.azurecr.io/testpath/myimage
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
ports:
- containerPort: 80
- name: http
env:
- name: MyEnvironmetVariable
value: "MyEnvironmetVariable"
另一方面,模板下的 spec 属性列出了将组成每个 pod 副本的所有容器。
然后,每个容器都有一个名称,并指定用于实例化的 Docker 镜像。接着,它指定所需的平均计算资源及其最大限制。最后,它指定外部暴露的端口。这些端口不会转发到不同的端口,而是直接暴露。此端口设置覆盖了 Docker 文件中的 EXPOSE 设置。
最后,我们可以指定一些要在每个容器内部设置的环境变量。
由于同一服务在不同节点上有多个副本,并且分配服务到节点可能会动态变化,因此在 Pod 之间的内部通信以及内部到外部的通信中存在一个问题。这个问题通过定义提供所有 Pod 实例唯一入口点的服务来解决。可以在同一个.yaml文件中添加服务定义,并用---分隔:
---
apiVersion: v1
kind: Service
metadata:
name: MyApplication-service
spec:
ports:
- port: 8080
targetPort: 80
protocol: TCP
name: http
selector:
app: MyApplication
上述定义创建了一个在端口8080上暴露的服务,该服务将所有请求重定向到MyApplication副本的端口80。由该服务提供服务的 Pod 是通过selector属性选择的。服务 IP 在内部可见,但客户端 Pod 不需要知道服务 IP,因为可以通过名称访问服务,就像经典网络中的主机一样。因此,在这种情况下,MyApplication-service:8080就完成了这项工作。
如果我们需要一个公开可访问的 IP,我们需要在spec下的ports之前添加type: LoadBalancer。AKS 将为您选择一个公共 IP。我们可以通过使用kubectl get service MyDeployment --watch监视部署过程来获取所选的公共 IP,直到 IP 被选中。如果我们已经在与 AKS 相同的资源组中购买了 IP 地址,我们可以在服务spec下通过添加clusterIP: <your IP>来指定此 IP 地址。
如果我们在.yaml文件中创建命名空间,Pod 可以被组织到命名空间中:
apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
labels:
name: my-namespace
然后,您可以通过在其定义元数据中的名称后添加namespace: <your namespace>来在命名空间中定位对象(服务或部署)。同样,您可以通过添加--namespace=<your namespace>选项来在特定命名空间中定位kubectl命令。
下一个用例将提供更多关于定义 Service Fabric 应用程序的详细信息。有关 Kubernetes 集群的更多详细信息,可以在进一步阅读部分找到的参考资料中找到。
用例 - 日志微服务
在本节中,我们将探讨一个基于微服务的系统,该系统记录了我们 WWTravelClub 用例中与各种目的地相关的购买数据。特别是,我们将设计负责计算每个位置每日收入的微服务。在这里,我们假设这些微服务从同一 Azure Service Fabric 应用程序中托管的其他子系统接收数据。更具体地说,每个购买日志消息由位置名称、整体套餐成本以及购买日期和时间组成。
作为第一步,让我们确保我们在本章 技术要求 部分中提到的 Service Fabric 模拟器已经安装并正在您的开发机器上运行。现在,我们需要将其切换,以便它运行 5 个节点。
现在,我们可以按照我们在 Azure 和 Visual Studio 对微服务编排的支持 部分中解释的步骤来创建一个名为 PurchaseLogging 的 Service Fabric 项目。选择一个 .NET Core 有状态可靠服务,并将其命名为 LogStore。
由 Visual Studio 创建的解决方案由一个 PurchaseLogging 项目组成,它代表整个应用程序,以及一个 LogStore 项目,该项目将包含 PurchaseLogging 应用程序中包含的第一个微服务的实现。
在 PackageRoot 文件夹下,LogStore 服务和每个可靠服务都包含一个 ServiceManifest.xml 配置文件和一个 Settings.xml 文件夹(位于 Config 子文件夹下)。Settings.xml 文件夹包含一些可以从服务代码中读取的设置。初始文件包含 Service Fabric 运行时所需的预定义设置。让我们添加一个新的设置部分,如下面的代码所示:
<?xml version="1.0" encoding="utf-8" ?>
<Settings
>
<!-- This is used by the StateManager's replicator. -->
<Section Name="ReplicatorConfig">
<Parameter Name="ReplicatorEndpoint" Value="ReplicatorEndpoint" />
</Section>
<!-- This is used for securing StateManager's replication traffic. -->
<Section Name="ReplicatorSecurityConfig" />
<!-- Below the new Section to add -->
<Section Name="Timing">
<Parameter Name="MessageMaxDelaySeconds" Value="" />
</Section>
</Settings>
我们将使用 MessageMaxDelaySeconds 的值来配置系统组件并确保消息幂等性。设置值是空的,因为大多数设置都被包含在 PurchaseLogging 项目中的整体应用程序设置覆盖了。
ServiceManifest.xml 文件包含一些由 Visual Studio 自动处理的配置标签以及端点列表。由于它们由 Service Fabric 运行时使用,因此已预配置了两个端点。在这里,我们必须添加我们的微服务将监听的端点的配置细节。每个端点定义具有以下格式:
<Endpoint Name="<endpoint name>" PathSuffix="<the path of the endpoint URI>" Protocol="<a protcolo like Tcp, http, https, etc.>" Port="the exposed port" Type="<Internal or Input>"/>
如果 Type 是 Internal,则端口将在集群的本地网络内部打开;否则,端口也将对外部集群可用。在前一种情况下,我们必须在 Azure Service Fabric 集群的配置中声明该端口,否则集群负载均衡器/防火墙不会将消息转发到它。
公共端口可以直接从集群 URI (<cluster name>.<location code>.cloudapp.azure.com) 访达,因为接口每个集群的负载均衡器将接收到的输入流量转发到它们。
在这个例子中,我们不会定义端点,因为我们打算使用基于远程通信的通信,这已经定义好了,用于所有内部交互,但我们将向您展示如何使用它们。
PurchaseLogging 项目在 services 解决方案资源管理器节点下包含对 LogStore 项目的引用,并包含各种文件夹,其中包含各种 XML 配置文件。更具体地说,我们有以下文件夹:
ApplicationPackageRoot,其中包含名为ApplicationManifest.xml的整体应用程序清单。此文件包含一些初始参数定义,然后是进一步的配置。参数具有以下格式:
<Parameter Name="<parameter name>" DefaultValue="<parameter definition>" />
- 一旦定义,参数就可以替换文件其余部分中的任何值。参数值通过在方括号中包围参数名称来引用,如下面的代码所示:
<UniformInt64Partition PartitionCount="[LogStore_PartitionCount]" LowKey="0" HighKey="1000" />
一些参数定义了每个服务的副本和分区数量,并由 Visual Studio 自动创建:
<Parameter Name="LogStore_MinReplicaSetSize" DefaultValue="1" />
<Parameter Name="LogStore_PartitionCount" DefaultValue="2" />
<Parameter Name="LogStore_TargetReplicaSetSize" DefaultValue="1" />
让我们用前面代码中的值替换 Visual Studio 建议的初始值。我们将使用两个分区来向您展示分区是如何工作的,但您可以将此值增加以提高写/更新并行性。LogStore 服务的每个分区不需要多个副本,因为副本可以提高读取操作的性能,而这个服务不是设计来提供读取服务的。因此,您可以选择仅使用一个副本,或者最多两个副本,以使系统冗余并提高对失败的鲁棒性。
上述参数用于定义 LogStore 服务在整体应用程序中的角色。此定义由 Visual Studio 自动生成,在同一文件中,在 Visual Studio 创建的初始定义下方,只是将分区间隔更改为 0-1,000:
<Service Name=LogStore ServicePackageActivationMode="ExclusiveProcess">
<StatefulService ServiceTypeName="LogStoreType"
TargetReplicaSetSize=
"[LogStore_TargetReplicaSetSize]"
MinReplicaSetSize="[LogStore_MinReplicaSetSize]">
<UniformInt64Partition PartitionCount="
[LogStore_PartitionCount]"
LowKey="0" HighKey="1000" />
</StatefulService>
</Service>
-
ApplicationParameters包含对ApplicationManifest.xml中定义的参数的可能的覆盖,适用于各种部署环境:云(即实际的 Azure Service Fabric 集群)和具有一个或五个节点的本地仿真器。 -
PublishProfiles包含发布应用程序所需的设置,这些设置由ApplicationParameters文件夹处理的环境所处理。您只需自定义云发布配置文件,使用您在 Azure 集群配置过程中下载的实际 Azure Service Fabric URI 名称,以及您在 Azure 集群配置过程中下载的认证证书:
<ClusterConnectionParameters
ConnectionEndpoint="<cluster name>.<location
code>.cloudapp.azure.com:19000"
X509Credential="true"
ServerCertThumbprint="<server certificate thumbprint>"
FindType="FindByThumbprint"
FindValue="<client certificate thumbprint>"
StoreLocation="CurrentUser"
StoreName="My" />
为了完成应用程序,需要遵循的剩余步骤已被组织成几个子节。让我们首先看看如何确保消息幂等性。
确保消息幂等性
消息可能会因为故障或由负载均衡引起的小超时而丢失。在这里,我们将使用基于远程通信的预定义通信,在发生故障的情况下自动重试消息。然而,正如我们在 微服务设计原则 子节中解释的那样,这可能会导致相同的消息被接收两次。由于我们正在汇总采购订单的收入,我们必须防止多次汇总相同的采购订单。
为了做到这一点,我们将实现一个包含确保消息副本被丢弃所需工具的库。
让我们在解决方案中添加一个新的.NET Standard 2.0 库项目,命名为IdempotencyTools。现在,我们可以移除 Visual Studio 生成的初始类框架。这个库需要引用与LogStore相同的版本的Microsoft.ServiceFabric.Services NuGet 包,所以让我们验证版本号并将相同的 NuGet 包引用添加到IdempotencyTools项目中。
确保消息幂等的主体工具是IdempotentMessage类:
using System;
using System.Runtime.Serialization;
namespace IdempotencyTools
{
[DataContract]
public class IdempotentMessage<T>
{
[DataMember]
public T Value { get; protected set; }
[DataMember]
public DateTimeOffset Time { get; protected set; }
[DataMember]
public Guid Id { get; protected set; }
public IdempotentMessage(T originalMessage)
{
Value = originalMessage;
Time = DateTimeOffset.Now;
Id = Guid.NewGuid();
}
}
}
我们添加了DataContract和DataMember属性,因为它们是我们将要用于所有内部消息的远程通信序列化器所需的。基本上,退行类是一个包装器,它向传递给其构造函数的消息类实例添加一个Guid和时间标记。
IdempotencyFilter类使用分布式字典来跟踪它已经接收到的消息。为了避免这个字典无限增长,定期删除较旧的条目。太旧以至于在字典中找不到的消息将自动丢弃。
时间间隔条目被保存在字典中,并通过IdempotencyFilter静态工厂方法传递,该方法创建新的过滤器实例,以及创建分布式字典所需的字典名称和IReliableStateManager实例:
public class IdempotencyFilter
{
protected IReliableDictionary<Guid, DateTimeOffset> dictionary;
protected int maxDelaySeconds;
protected DateTimeOffset lastClear;
protected IReliableStateManager sm;
protected IdempotencyFilter() { }
public static async Task<IdempotencyFilter> NewIdempotencyFilter(
string name,
int maxDelaySeconds,
IReliableStateManager sm)
{
var result = new IdempotencyFilter();
result.dictionary = await
sm.GetOrAddAsync<IReliableDictionary<Guid, DateTimeOffset>>
(name);
result.maxDelaySeconds = maxDelaySeconds;
result.lastClear = DateTimeOffset.Now;
result.sm = sm;
return result;
}
...
...
字典包含每个消息时间标记,按消息Guid索引,通过调用IReliableStateManager实例的GetOrAddAsync方法创建,该方法带有字典类型和名称。lastClear包含所有旧消息移除的时间。
当收到新消息时,NewMessage方法检查是否必须丢弃该消息。如果必须丢弃该消息,则返回null;否则,将新消息添加到字典中,并返回不带IdempotentMessage包装器的新消息:
public async Task<T> NewMessage<T>(IdempotentMessage<T> message)
{
DateTimeOffset now = DateTimeOffset.Now;
if ((now - lastClear).TotalSeconds > 1.5 * maxDelaySeconds)
{
await Clear();
}
if ((now - message.Time).TotalSeconds > maxDelaySeconds)
return default(T);
using (ITransaction tx = this.sm.CreateTransaction())
{
...
...
}
}
作为第一步,该方法验证是否是清除字典的时候,以及消息是否过于陈旧。然后,它开始一个事务来访问字典。所有分布式字典操作都必须在事务中封装,如下面的代码所示:
using (ITransaction tx = this.sm.CreateTransaction())
{
var result = await dictionary.TryGetValueAsync(tx,
message.Id);
if (result.HasValue)
{
tx.Abort();
return default(T);
}
else
{
await dictionary.TryAddAsync(tx, message.Id, message.Time);
await tx.CommitAsync();
return message.Value;
}
}
如果在字典中找到消息Guid,则由于字典不需要更新,方法返回default(T),实际上为null,因为消息不得被处理。否则,将消息条目添加到字典中,并返回未包装的消息。
Clear方法的代码可以在与本书相关的 GitHub 仓库中找到。
交互库
有一些类型必须在所有微服务之间共享。如果内部通信是通过远程或 WCF 实现的,每个微服务都必须公开一个接口,包含其他微服务调用的所有方法。这些接口必须在所有微服务之间共享。此外,对于所有通信接口,实现消息的类也必须在所有微服务(或它们的一些子集)之间共享。因此,所有这些结构都声明在外部库中,这些库被微服务引用。
现在,让我们向我们的解决方案中添加一个新的.NET Standard 2.0 库项目,命名为Interactions。由于这个库必须使用IdempotentMessage泛型类,我们必须将其作为引用添加到IdempotencyTools项目中。我们还需要添加对包含在Microsoft.ServiceFabric.Services.Remoting NuGet 包中的远程通信库的引用,因为所有用于公开微服务远程方法的接口都必须继承自该包中定义的IService接口。
IService是一个空接口,它声明了继承接口的通信角色。Microsoft.ServiceFabric.Services.Remoting NuGet 包的版本必须与在其他项目中声明的Microsoft.ServiceFabric.Services包的版本相匹配。
以下代码显示了需要由LogStore类实现的接口的声明:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using IdempotencyTools;
using Microsoft.ServiceFabric.Services.Remoting;
namespace Interactions
{
public interface ILogStore: IService
{
Task<bool> LogPurchase(IdempotentMessage<PurchaseInfo>
idempotentMessage);
}
}
以下为PurchaseInfo消息类的代码,该代码在ILogStore接口中被引用:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
namespace Interactions
{
[DataContract]
public class PurchaseInfo
{
[DataMember]
public string Location { get; set; }
[DataMember]
public decimal Cost { get; set; }
[DataMember]
public DateTimeOffset Time { get; set; }
}
}
现在,我们已经准备好实现我们的主要LogStore微服务。
实现通信的接收端
要实现LogStore微服务,我们必须添加对Interaction库的引用,这将自动创建对远程库和对IdempotencyTools项目的引用。然后,LogStore类必须实现ILogStore接口:
internal sealed class LogStore : StatefulService, ILogStore
...
... private IReliableQueue<IdempotentMessage<PurchaseInfo>> LogQueue = null;
public async Task<bool>
LogPurchase(IdempotentMessage<PurchaseInfo> idempotentMessage)
{
if (LogQueue == null) return false;
using (ITransaction tx = this.StateManager.CreateTransaction())
{
await LogQueue.EnqueueAsync(tx, idempotentMessage);
await tx.CommitAsync();
return true;
}
}
一旦服务从远程运行时接收到LogPurchase调用,它将消息放入LogQueue以避免调用者保持阻塞,等待消息处理完成。这样,我们既实现了同步消息传递协议的可靠性(调用者知道消息已被接收),又实现了异步通信典型的异步消息处理的性能优势。
根据LoqQueue作为所有分布式集合的最佳实践,它在RunAsync方法中被创建,因此如果第一个调用在 Azure Service Fabric 运行时调用RunAsync之前到达,LogQueue可能为 null。在这种情况下,该方法返回false以表示服务尚未准备好。否则,将创建一个事务以入队新消息。
然而,如果我们不提供一个返回服务希望激活的所有听众的CreateServiceReplicaListeners()实现,我们的服务将不会收到任何通信。在远程通信的情况下,有一个预定义的方法执行整个任务,所以我们只需调用它:
protected override IEnumerable<ServiceReplicaListener>
CreateServiceReplicaListeners()
{
return this.CreateServiceRemotingReplicaListeners<LogStore>();
}
在这里,CreateServiceRemotingReplicaListeners是在远程通信库中定义的一个扩展方法。它为主副本和辅助副本(用于只读操作)创建监听器。在创建客户端时,我们可以指定其通信是否仅针对主副本,还是也针对辅助副本。
如果您想使用不同的听众,您必须创建一个IEnumerable的ServiceReplicaListener实例。对于每个听众,您必须使用三个参数调用ServiceReplicaListener构造函数:
-
一个函数,它接收可靠的服务上下文对象作为输入,并返回
ICommunicationListener接口的实现。 -
听众的名称。当服务拥有多个听众时,这个第二个参数变为必需。
-
一个布尔值,如果听众必须在辅助副本上激活则为真。
例如,如果我们想添加自定义和 HTTP 监听器,代码可能如下所示:
return new ServiceReplicaListener[]
{
new ServiceReplicaListener(context =>
new MyCustomHttpListener(context, "<endpoint name>"),
"CustomWriteUpdateListener", true),
new ServiceReplicaListener(serviceContext =>
new KestrelCommunicationListener(serviceContext, "<endpoint name>"
(url, listener) =>
{
...
})
"HttpReadOnlyListener",
true)
};
MyCustomHttpListener是ICommunicationListener的自定义实现,而KestrelCommunicationListener是基于 Kestrel 和 ASP.NET Core 的预定义 HTTP 监听器。以下是完全定义KestrelCommunicationListener监听器的代码:
new ServiceReplicaListener(serviceContext =>
new KestrelCommunicationListener(serviceContext, "<endpoint name>" (url, listener) =>
{
return new WebHostBuilder()
.UseKestrel()
.ConfigureServices(
services => services
.AddSingleton<StatefulServiceContext>(serviceContext)
.AddSingleton<IReliableStateManager>(this.StateManager))
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.UseServiceFabricIntegration(listener,
ServiceFabricIntegrationOptions.UseUniqueServiceUrl)
.UseUrls(url)
.Build();
})
"HttpReadOnlyListener",
true)
通常,ICommunicationListener实现在其构造函数中接受节点上下文和一个端点名称,并负责读取ServiceManifest.xml服务中定义的端点数据,以及创建满足其中包含的规范的监听端点。它们在其CommunicationListener.OpenAsync方法中这样做:
public async Task<string> OpenAsync(CancellationToken cancellationToken)
{
EndpointResourceDescription serviceEndpoint = serviceContext
.CodePackageActivationContext.GetEndpoint("ServiceEndpoint");
//create service URI that depend on current Ip
(FabricRuntime.GetNodeContext().IPAddressOrFQDN)
//partition id (serviceContext.PartitionId)
//and replica id (serviceContext.ReplicaOrInstanceId)
//open the listener
return <computedURISchema>;
}
<computedURISchema>是将 IP 地址替换为"+"的 URI。一旦通过OpenAsync返回,它将在服务 Fabric 命名服务中发布,并用于从已部署在其上的集群节点 IP 地址计算实际服务地址。
ICommunicationListener实现还必须有一个Close方法,它必须关闭打开的通信通道,以及一个Abort方法,它必须立即关闭通信通道(即不通知连接的客户端等)。
现在我们已经开启了通信,我们可以实现服务逻辑。
实现服务逻辑
服务逻辑由在 Service Fabric 运行时调用RunAsync时启动的独立线程的任务执行。当只需要实现一个任务时,创建一个IHost并将所有任务设计为IHostedService实现是良好的实践。实际上,IHostedService实现是独立的软件块,更容易进行单元测试。"IHost"和"IHostedService"在使用通用宿主小节中进行了详细讨论。
在本节中,我们将实现将每日收入计算逻辑实现为名为ComputeStatistics的IHostedservice,它使用一个分布式字典,其键是位置名称,值是名为RunningTotal的类的实例。这个类存储当前的运行总计数和正在计算的天:
namespace LogStore
{
public class RunningTotal
{
public DateTime Day { get; set; }
public decimal Count { get; set; }
public RunningTotal
Update(DateTimeOffset time, decimal value)
{
...
}
}
}
这个类有一个Update方法,当接收到新的购买消息时更新实例。首先,将传入的消息时间标准化为通用时间。然后,从这个时间中提取出天部分,并将其与运行总计的当前Day进行比较,如下面的代码所示:
public RunningTotal Update(DateTimeOffset time, decimal value)
{
var normalizedTime = time.ToUniversalTime();
var newDay = new DateTime(normalizedTime.Year,
normalizedTime.Month, normalizedTime.Day);
...
...
}
如果是新的日子,我们假设前一天的总计算已经完成,因此Update方法返回一个新的RunningTotal实例,并重置Day和Count以便它可以计算新的一天的总计数。否则,新值将添加到当前的Count中,并且方法返回null,表示这一天的总计数尚未准备好。这种实现可以在以下代码中看到:
public RunningTotal Update(DateTimeOffset time, decimal value)
{
...
...
var result = newDay > Day && Day != DateTime.MinValue ?
new RunningTotal
{
Day=Day,
Count=Count
}
: null;
if(newDay > Day) Day = newDay;
if (result != null) Count = value;
else Count += value;
return result;
}
IHostedService的ComputeStatistics实现需要一些参数才能正常工作,如下所示:
-
包含所有传入消息的队列
-
IReliableStateManager服务,以便它可以创建存储数据的分布式字典 -
ConfigurationPackage服务,以便它可以读取在Settings.xml服务文件中定义的设置,以及可能在应用程序清单中覆盖的设置
在创建ComputeStatistics实例时,必须通过IHost通过依赖注入将前面的参数传递给ComputeStatistics构造函数。我们将在下一小节回到IHost的定义。现在,让我们集中讨论ComputeStatistics构造函数及其字段:
namespace LogStore
{
public class ComputeStatistics : BackgroundService
{
IReliableQueue<IdempotentMessage<PurchaseInfo>> queue;
IReliableStateManager stateManager;
ConfigurationPackage configurationPackage;
public ComputeStatistics(
IReliableQueue<IdempotentMessage<PurchaseInfo>> queue,
IReliableStateManager stateManager,
ConfigurationPackage configurationPackage)
{
this.queue = queue;
this.stateManager = stateManager;
this.configurationPackage = configurationPackage;
}
所有构造函数参数都存储在私有字段中,以便在调用ExecuteAsync时使用:
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
bool queueEmpty = false;
var delayString=configurationPackage.Settings.Sections["Timing"]
.Parameters["MessageMaxDelaySeconds"].Value;
var delay = int.Parse(delayString);
var filter = await IdempotencyFilter.NewIdempotencyFilter(
"logMessages", delay, stateManager);
var store = await
stateManager.GetOrAddAsync<IReliableDictionary<string, RunningTotal>>("partialCount");
....
...
在进入循环之前,ComputeStatistics服务准备了一些结构和参数。它声明队列不为空,以便它可以开始出队消息。然后,它从服务设置中提取MessageMaxDelaySeconds并将其转换为整数。这个参数在Settings.xml文件中留空。现在,是时候在ApplicationManifest.xml中覆盖它并定义其实际值了:
<ServiceManifestImport>
<ServiceManifestRef ServiceManifestName="LogStorePkg" ServiceManifestVersion="1.0.0" />
<!--code to add start -->
<ConfigOverrides>
<ConfigOverride Name="Config">
<Settings>
<Section Name="Timing">
<Parameter Name="MessageMaxDelaySeconds" Value="[MessageMaxDelaySeconds]" />
</Section>
</Settings>
</ConfigOverride>
</ConfigOverrides>
<!--code to add end-->
</ServiceManifestImport>
ServiceManifestImport导入服务清单到应用程序中,并覆盖一些配置。每次其内容以及/或服务定义更改,并且应用程序在 Azure 中重新部署时,其版本号都必须更改,因为版本号的变化告诉服务 Fabric 运行时在集群中要更改什么。版本号也出现在其他配置设置中。它们必须在它们引用的实体更改时更改。
MessageMaxDelaySeconds被传递给幂等性过滤器实例,以及已接收消息字典的名称,以及IReliableStateManager服务实例。最后,用于存储运行总量的主要分布式字典被创建。
之后,服务进入其循环,并在stoppingToken被触发时结束,即当服务 Fabric 运行时指示服务即将停止时:
while (!stoppingToken.IsCancellationRequested)
{
while (!queueEmpty && !stoppingToken.IsCancellationRequested)
{
RunningTotal total = null;
using (ITransaction tx = stateManager.CreateTransaction())
{
...
...
...
}
}
await Task.Delay(100, stoppingToken);
queueEmpty = false;
}
}
内层循环会一直运行,直到队列不为空,然后退出并等待 100 毫秒,以验证是否有新消息入队:
await Task.Delay(100, stoppingToken);
queueEmpty = false;
以下是为内层循环编写的代码,该循环被包含在一个事务中:
RunningTotal total = null;
using (ITransaction tx = stateManager.CreateTransaction())
{
var result = await queue.TryDequeueAsync(tx);
if (!result.HasValue) queueEmpty = true;
else
{
var item = await filter.NewMessage<PurchaseInfo>(result.Value);
if(item != null)
{
var counter = await store.TryGetValueAsync(tx,
item.Location);
//counter update
...
}
...
...
}
}
在这里,服务正在尝试出队一个消息。如果队列为空,它将queueEmpty设置为true以退出循环;否则,它将消息通过幂等性过滤器。如果消息通过了这一步,它将使用它来更新消息中引用的位置的运行总数。然而,为了正确操作分布式字典,每次更新条目时都必须用新计数器替换旧计数器。因此,旧计数器被复制到一个新的RunningTotal对象中。如果我们调用Update方法,这个新对象可以更新为新数据:
//counter update
var newCounter = counter.HasValue ?
new RunningTotal
{
Count=counter.Value.Count,
Day= counter.Value.Day
}
: new RunningTotal();
total = newCounter.Update(item.Time, item.Cost);
if (counter.HasValue)
await store.TryUpdateAsync(tx, item.Location,
newCounter, counter.Value);
else
await store.TryAddAsync(tx, item.Location, newCounter);
然后,事务被提交,如下面的代码所示:
if(item != null)
{
...
...
}
await tx.CommitAsync();
if(total != null)
{
await SendTotal(total, item.Location);
}
当Update方法返回完整的计算结果时,即调用total != null方法时:
protected async Task SendTotal(RunningTotal total, string location)
{
//Empty, actual application would send data to a service
//that exposes daily statistics through a public Http endpoint
}
SendTotal方法将总数发送到公开通过 HTTP 端点暴露所有统计信息的服务。在阅读了第十二章 Applying Service-Oriented Architectures with .NET Core 之后,该章节专门介绍了 Web API,你可能希望实现一个类似的服务,该服务是一个无状态的 ASP.NET Core 微服务,连接到一个数据库。无状态的 ASP.NET Core 服务模板会自动为你创建一个基于 ASP.NET Core 的 HTTP 端点。
然而,由于此服务必须从SendTotal方法接收数据,它还需要基于远程的端点。因此,我们必须创建它们,就像我们为LogStore微服务所做的那样,并将基于远程的端点数组与包含 HTTP 端点的现有数组连接起来。
定义微服务的宿主
现在,我们已经准备好定义微服务的RunAsync方法:
protected override async Task RunAsync(CancellationToken cancellationToken)
{
// TODO: Replace the following sample code with your own logic
// or remove this RunAsync override if it's not needed in your service.
cancellationToken.ThrowIfCancellationRequested();
LogQueue = await
this.StateManager
.GetOrAddAsync<IReliableQueue
<IdempotentMessage<PurchaseInfo>>>("logQueue");
var configurationPackage = Context
.CodePackageActivationContext
.GetConfigurationPackageObject("Config");
...
...
在这里,该方法验证取消令牌是否被信号,如果是,则抛出异常以终止方法。然后,创建服务队列,并将服务设置保存在configurationPackage中。
之后,我们可以创建IHost服务,正如我们在使用通用宿主子节中解释的那样:
var host = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton(this.StateManager);
services.AddSingleton(this.LogQueue);
services.AddSingleton(configurationPackage);
services.AddHostedService<ComputeStatistics>();
})
.Build();
await host.RunAsync(cancellationToken);
ConfigureServices定义了所有可能由IHostedService实现需要的单例实例,因此它们被注入到所有引用其类型的实现构造函数中。然后,AddHostedService声明微服务的唯一IHostedService。一旦构建了IHost,我们就运行它,直到RunAsync取消令牌被信号。当取消令牌被信号时,关闭请求传递给所有IHostedService实现。
与服务通信
由于我们尚未实现整个购买逻辑,我们将实现一个无状态微服务,该服务向LogStore服务发送随机数据。在解决方案资源管理器中右键单击PurchaseLogging项目,并选择添加 | Service Fabric 服务。然后,选择.NET Core 无状态模板,并将新的微服务项目命名为FakeSource。
现在,让我们添加对Interaction项目的引用。在继续到服务代码之前,我们需要更新ApplicationManifest.xml中以及所有其他环境特定参数覆盖(云、一个本地集群节点、五个本地集群节点)中新建服务的副本计数。
<Parameter Name="FakeSource_InstanceCount" DefaultValue="2" />
这个模拟服务不需要监听器,其RunAsync方法很简单:
string[] locations = new string[] { "Florence", "London", "New York", "Paris" };
protected override async Task RunAsync(CancellationToken cancellationToken)
{
Random random = new Random();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
PurchaseInfo message = new PurchaseInfo
{
Time = DateTimeOffset.Now,
Location= locations[random.Next(0, locations.Length)],
Cost= 200m*random.Next(1, 4)
};
//Send message to counting microservices
...
...
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
在每个循环中,创建一个随机消息并发送到计数微服务。然后,线程休眠一秒钟并开始新的循环。发送创建的消息的代码如下:
//Send message to counting microservices
var partition = new ServicePartitionKey(Math.Abs(message.Location.GetHashCode()) % 1000);
var client = ServiceProxy.Create<ILogStore>(
new Uri("fabric:/PurchaseLogging/LogStore"), partition);
try
{
while (!await client.LogPurchase(new
IdempotentMessage<PurchaseInfo>(message)))
{
await Task.Delay(TimeSpan.FromMilliseconds(100),
cancellationToken);
}
}
catch
{
}
在这里,从位置字符串计算出一个 0-9,999 区间的键。这个整数传递给ServicePartitionKey构造函数。然后,创建一个服务代理,并将要调用的服务的 URI 和分区键传递。代理使用这些数据向命名服务请求给定分区值的物理 URI。
ServiceProxy.Create还接受一个第三个可选参数,指定代理发送的消息是否也可以路由到二级副本。默认情况下,消息仅路由到主实例。如果消息目标返回false,表示它尚未准备好(记住,当LogStore消息队列尚未创建时,LogPurchase返回false),则在 100 毫秒后尝试相同的传输。
向远程目标发送消息非常简单。然而,其他通信监听器要求发送者手动与命名服务交互以获取物理服务 URI。这可以通过以下代码完成:
ServicePartitionResolver resolver = ServicePartitionResolver.GetDefault();
ResolvedServicePartition partition =
await resolver.ResolveAsync(new Uri("fabric:/MyApp/MyService"),
new ServicePartitionKey(.....), cancellationToken);
//look for a primary service only endpoint
var finalURI= partition.Endpoints.First(p =>
p.Role == ServiceEndpointRole.StatefulPrimary).Addreess;
此外,在通用通信协议的情况下,我们必须手动使用库(如 Polly)处理失败和重试(有关更多信息,请参阅弹性任务执行子节)。
测试应用程序
为了测试应用程序实际上是否计算了累计购买总额,让我们在ComputeStatistics.cs文件中放置一个断点:
total = newCounter.Update(item.Time, item.Cost);
if (counter.HasValue)...//put breakpoint on this line
每当断点被触发时,查看newCounter的内容以验证所有位置的累计总和是如何变化的。
摘要
在本章中,我们描述了微服务是什么以及它们是如何从模块的概念演变而来的。然后,我们讨论了微服务的优势以及何时值得使用它们,以及它们设计的一般标准。我们还解释了 Docker 容器是什么,并分析了容器与微服务架构之间的紧密联系。
然后,我们通过描述.NET Core 中所有可用的工具来实施更实际的实现,这样我们就可以实现基于微服务的架构。我们还描述了微服务所需的基础设施以及 Azure 集群如何提供 Azure Kubernetes 服务和 Azure Service Fabric。
最后,我们通过实现 Service Fabric 应用程序将这些概念付诸实践。在这里,我们探讨了 Service Fabric 应用程序可以实现的多种方式。
下一章将重点介绍如何使用 ORM 和 Entity Framework Core 与各种类型的数据库交互,同时保持我们的代码与所选数据库引擎的独立性。
问题
-
模块概念的双重性质是什么?
-
微服务的优势仅仅是扩展优化吗?如果不是,请列出一些其他优势。
-
Polly 是什么?
-
ConfigureServices是什么? -
Visual Studio 提供了哪些 Docker 支持?
-
那么,哪种 Docker 应用程序方法更强大:基于
.yml文件的方法还是基于.yaml文件的方法? -
在定义 Azure Service Fabric 集群时必须声明哪些类型的端口?
-
为什么需要可靠状态服务的分区?
-
我们如何声明远程通信必须由辅助副本处理?对于其他类型的通信呢?
进一步阅读
以下链接是 Azure Service Bus 和 RabbitMQ 这两个事件总线技术的官方文档:
-
Azure Service Bus:
docs.microsoft.com/en-us/azure/service-bus-messaging/ -
RabbitMQ:
www.rabbitmq.com/getstarted.html
可靠通信/任务的工具 Polly 的文档可以在这里找到:github.com/App-vNext/Polly.
更多关于 Docker 的信息可以在 Docker 的官方网站上找到:docs.docker.com/.
Kubernetes 和.yaml文件的官方文档可以在这里找到:kubernetes.io/docs/home/.
Azure Kubernetes 的官方文档可以在这里找到:docs.microsoft.com/en-US/azure/aks/.
Azure Service Fabric 的官方文档可以在这里找到:docs.microsoft.com/en-US/azure/service-fabric/.
Azure Service Fabric 的可靠服务的官方文档可以在这里找到:docs.microsoft.com/en-us/azure/service-fabric/service-fabric-reliable-services-introduction.
更多关于 Actor 模型的信息可以在这里找到:www.researchgate.NET/publication/234816174_Actors_A_conceptual_foundation_for_concurrent_object-oriented_programming.
可以在 Azure Service Fabric 中实现的 Actor 模型的官方文档可以在这里找到:docs.microsoft.com/en-US/azure/service-fabric/service-fabric-reliable-actors-introduction.
微软还实现了一个独立于 Service Fabric 的高级 actor 模型,这被称为 Orleans 框架。更多关于 Orleans 的信息可以在以下链接中找到:
89



被折叠的 条评论
为什么被折叠?



