ASP-NET-Core2-高性能-全-

ASP.NET Core2 高性能(全)

原文:ASP.NET Core 2 High Performance

协议:CC BY-NC-SA 4.0

零、前言

微软发布了其开源和跨平台网络应用框架的第二个主要版本——ASP.NET Core。这是在.NET Core,也是开放的,现在也是版本 2。ASP.NET Core 主要用于 C#编程语言,但也可以使用 F#和 VB.NET。你不再局限于在 ASP.NET 使用 Windows,现在你可以在 Mac 上开发并部署到 Linux。新平台还提供了更高的性能。

版本 2 就是版本 1 应该有的样子,因为第一个稳定版本还没有真正做好生产准备。在发布候选阶段很晚才做出重大更改。谢天谢地,有了新的工具,事情已经稳定下来,你现在可以认真使用 ASP.NET Core 了。

在当今世界,一个 web 应用只在开发人员的工作站上运行良好,而在生产中却无法提供高性能,这是不可接受的。web 应用现在大规模部署的方式已经改变,开发实践必须适应以利用这一点。通过阅读这本书,你将了解制作高性能网络应用的现代方式,以及如何使用 ASP.NET Core 来做到这一点。

这是一本高水平的书,提供了适用于任何编程栈的 web 应用开发的性能提示。但是,它特别关注 C#和 ASP.NET Core。读者应该已经知道如何建立一个网络应用,虽然不一定在.NET Core。

这本书从一般的角度(HTTP、HTTPS、HTTP/2、TCP/IP、数据库访问、压缩、输入/输出、资产优化、缓存、消息队列和其他关注点)以及从 C#、asset 核心和.NET Core 视角。这包括深入研究最新框架的细节和演示提高性能的软件设计模式。

常见的性能缺陷将被突出显示,这些缺陷通常会在开发人员工作站上被忽略,同时还会有策略来早期检测和解决这些问题。通过提前了解和解决挑战,就可以避免实时部署带来的任何令人不快的意外。

将引入许多性能改进,以及它们带来的权衡。我们将采取科学和基于证据的方法,关注大问题,避免影响不大的更改,从而在过早优化和低效代码之间取得平衡。

我们假设您理解性能对于 web 应用的重要性,但是我们将回顾为什么它是至关重要的。然而,你可能没有任何具体的或可操作的建议,或者对野外发生的性能问题没有太多经验。

通过阅读这本书,您将了解当 web 应用大规模部署到分布式基础架构时会出现什么问题,并且您将知道如何避免或减轻这些问题。您将获得如何编写高性能应用的经验,而不必费力地了解问题,可能是在深夜。

你会看到 ASP.NET Core 有什么新东西,为什么要从头开始重建,这对性能意味着什么。你会明白…的未来.NET Core,以及现在如何在 Windows、macOS 和 Linux 上开发和部署。您将欣赏 ASP.NET Core 中新功能的性能,包括对 Razor 视图引擎的更新,并且您将了解跨平台工具,如 Visual Studio Code。

这本书涵盖了什么

第一章ASP.NET Core 2 有什么新变化,总结了 ASP.NET Core 1.0 和 ASP.NET Core 2.0 的重大变化。我们还将探究该项目的历史,以展示为什么它是一个如此移动的目标。我们将看看 C# 6.0 和 C# 7.0 中的一些新功能,看看它们如何让您的生活变得更轻松。我们也会报道.NET 标准 2.0 以及这如何提高库的可移植性。

第二章为什么性能是一个特性,讨论了这本书的基本前提,说明了为什么需要关心软件的性能。响应性应用至关重要,仅仅让功能发挥作用是不够的;他们也需要快。想想你最后一次听到有人抱怨某个应用或网站,很可能他们对性能不满意。性能差不仅让用户不开心,还会影响你的底线。有很好的数据表明,快速的表现可以提高参与度和转化率,这就是为什么它会得到搜索引擎的回报。

第 3 章设置您的环境,展示了如何在您选择的操作系统上开始使用最新的工具。如果你想用. NET 开发,就不再需要在 Windows 上使用 Visual Studio 了。我们将介绍 VS 2017 和的新集成工具。网芯和 ASP.NET 芯。我们还将介绍用于 Mac 的 Visual Studio(以前称为 Xamarin Studio)和多平台 VS 代码编辑器(使用 TypeScript 和 web 技术构建)。我们将展示如何使用 Docker 容器使跨平台开发变得容易和一致。我们还将看一下 dotnet 命令行工具。

第 4 章测量性能瓶颈表明,解决性能问题的唯一方法是仔细测量您的应用。如果不知道问题在哪里,你解决问题的机会就非常渺茫,你甚至不知道你是改善了事情还是让事情变得更糟。我们将重点介绍几种手动监控性能的方法,以及一些可用于测量统计数据的有用工具。您将看到如何深入了解软件的数据库、应用、HTTP 和网络级别,这样您就知道内部发生了什么。我们还将向您展示如何构建您自己的基本计时代码,并介绍对结果采取科学方法的重要性。

第 5 章修复常见性能问题,查看一些最常见的性能错误。我们将展示如何解决一系列不同应用领域的简单问题,例如,如何通过调整图像大小或编码来优化媒体,选择 N+1 个问题,以及异步后台操作。一旦您知道了瓶颈所在,我们还将讨论使用硬件来提高性能。这种方法可以为你赢得一些时间,让你以合理的速度妥善解决问题。

第 6 章解决网络性能,深入到支撑所有网络应用的网络层。我们将展示远程资源如何降低您的应用速度,并演示您可以如何测量和解决这些问题。我们将研究互联网协议,包括 TCP/IP、HTTP、HTTP/2 和网络套接字,以及加密入门知识,以及所有这些如何改变性能。我们将讨论文本和图像资产的压缩,包括一些奇异的图像格式。最后,我们将在浏览器、服务器、代理和内容交付网络(CDN)级别介绍缓存,展示一些基础知识。

第 7 章优化输入/输出性能,重点关注输入/输出以及这会如何对性能产生负面影响。我们将研究磁盘、数据库和远程应用编程接口,其中许多都使用网络,尤其是在虚拟化的情况下。我们将通过聚合和采样来处理您的请求并优化数据库使用,旨在减少所需的数据和时间。由于网络在云环境中无处不在,我们将花费大量时间进行网络诊断,包括 ping、路由跟踪和在域名系统中查找记录。您将了解延迟是如何受物理距离或地理位置影响的,以及这会如何给您的应用带来问题。我们还将演示如何使用. NET 构建您自己的网络信息收集工具

第 8 章理解代码执行和异步操作,跳到错综复杂的 C#代码,看看它的执行如何改变性能。我们将看看组成 ASP.NET Core 和的各种开源项目.NET Core,包括红隼,一个高性能的网络服务器。我们将研究选择正确数据结构的重要性,并查看不同选项的各种示例,例如列表和字典。我们还将研究散列和序列化,并执行一些简单的基准测试。您将学习一些可以通过并行化来加快处理速度的技术,例如单指令多数据(SIMD)或使用任务并行库(TPL)和并行 LINQ (PLINQ)的并行扩展编程。您还将看到一些由于性能损失而最好避免的做法,例如反射和正则表达式。

第 9 章学习缓存和消息队列,最初看缓存,大家普遍认为很难。您将看到在浏览器、网络服务器、代理和 cdn 中,从 HTTP 的角度来看缓存是如何工作的。您将了解强制更改的缓存破坏(或破坏),以及在现代浏览器中使用新的 JavaScript 服务人员来更好地控制缓存。

此外,我们将检查基础架构中应用和数据库级别的缓存。我们将看到内存缓存(如 Redis)的好处,以及这些缓存如何降低数据库负载、降低延迟并提高应用的性能。

我们将研究消息队列作为构建分布式可靠系统的一种方式。我们将用一个类比来解释异步消息传递系统是如何工作的,我们将展示一些常见的消息队列风格,包括单播和发布/订阅。

我们还将展示消息队列如何通过广播缓存失效数据对内部缓存层有用。您将从. NET 中了解消息代理,如 RabbitMQ,以及与它们交互的各种库。

第 10 章性能增强工具的缺点集中在我们已经讨论过的技术的负面影响上,因为没有什么是免费的。我们将讨论降低复杂性、使用框架和设计分布式体系结构的各种方法的优点。我们还将介绍项目文化,并了解高性能不仅与代码有关,还与人有关。

我们将研究解决分布式调试问题的可能解决方案,并查看一些用于集中管理应用日志的可用技术。我们将简要介绍统计数据,以帮助理解您的性能指标,我们还将涉及管理缓存。

第 11 章监控性能回归,再次关注测量性能,但在本例中,是从自动化和持续集成(CI)的角度。我们将重申监控的重要性,并展示如何将其构建到您的开发工作流中,以使其变得常规且几乎透明。您将看到几乎任何类型的测试都可以自动化,从简单的单元测试到集成测试,甚至是复杂的浏览器用户界面(UI)测试。

我们将展示如何通过使用蓝绿色部署和功能切换等技术来使您的测试更加真实和有用。您将发现如何对一个网页的两个版本执行 A/B 测试,其中有一些非常基本的功能切换,以及一些有趣的硬件选项,以让人们参与测试结果。我们还将介绍 DevOps 实践和云托管,这两者都使 CI 更容易实现并很好地补充它。

第 12 章前路,简单总结了本书的教训,然后看一些你可能想多读的进阶话题。我们也将尝试预测未来.NET Core 平台,并给你一些进一步的想法。

这本书你需要什么

您将需要一个开发环境来遵循本书中的代码示例——Visual Studio 2017、Visual Studio Mac 或 VS Code。您也可以使用自己选择的文本编辑器和 dotnet 命令行工具。如果您使用的是 Visual Studio,那么您仍然应该安装最新的.NET Core 软件开发工具包来启用工具。

对于某些章节,您还需要 SQL Server 2016,尽管您可以使用 2017。但是,您也可以使用 Azure 并针对云数据库运行。

我们将介绍其他工具,但我们将在使用时介绍这些工具。详细的软件/硬件列表与代码文件一起提供。

这本书是给谁的

这本书面向那些希望提高软件性能并发现在云中托管时需要考虑的问题的 web 应用开发人员。它对 ASP.NET 和 C#开发人员来说非常有用,但是熟悉其他开源平台的开发人员也会发现其中的许多信息。

您应该有一些使用 web 应用开发框架的经验,并且应该考虑部署在实际生产环境中表现良好的应用。这些可以是虚拟机,也可以由云服务提供商(如 AWS 或 Azure)托管。

约定

在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“update命令与upgrade命令不同,但它们经常一起使用。”

代码块设置如下:

#import packages into the project 
from bs4 import BeautifulSoup 
from urllib.request import urlopen 
import pandas as pd

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

sudo apt-get install docker-ce

新名词重要词语以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中看到的单词,会出现在如下文本中:“在解决方案资源管理器中右键单击您的 web 应用项目,然后选择“管理”“获取包”...打开图形包管理器窗口。

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

读者反馈

我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。要给我们发送一般反馈,只需发送电子邮件 feedback@packtpub.com ,并在您的邮件主题中提及书名。如果您对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于www.packtpub.com/authors的作者指南。

客户支持

现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。

下载示例代码

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

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。
  2. 将鼠标指针悬停在顶部的“支持”选项卡上。
  3. 点击代码下载和勘误表。
  4. 在搜索框中输入图书的名称。
  5. 选择要下载代码文件的书籍。
  6. 从您购买这本书的下拉菜单中选择。
  7. 点击代码下载。

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

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

这本书的代码包也托管在 GitHub 上https://GitHub . com/PacktPublishing/ASPdotNET-高性能。我们还有来自丰富的图书和视频目录的其他代码包,可在https://github.com/PacktPublishing/获得。看看他们!

正误表

尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现一个错误,也许是文本或代码中的错误,如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问http://www.packtpub.com/submit-errata,选择您的书籍,点击勘误表提交表格链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。要查看之前提交的勘误表,请前往https://www.packtpub.com/books/content/support并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。

海盗行为

在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。请通过 copyright@packtpub.com 联系我们,获取疑似盗版资料的链接。我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以在 questions@packtpub.com 联系我们,我们将尽最大努力解决问题。

一、ASP.NET Core 2 有什么新内容?

在 ASP.NET Core 框架的第 2 版中,有很多东西已经改变了。它的一些支持技术也有很多改进。现在是一个尝试它的好时机,因为它的代码已经稳定下来了,变化的速度也稍微稳定下来了。

最初的候选版本和 ASP.NET Core 版本 1 之间有显著的差异,版本 1 和版本 2 之间有进一步的变更。其中一些变化一直存在争议,尤其是与工具相关的变化;但是,的范围.NET Core 已经大规模增长,这是一件好事。

版本 1 和版本 2 之间引人注目的区别之一是从基于新的 JavaScript 对象符号 ( JSON )的项目格式更改为基于可扩展标记语言 ( XML )的csproj格式。但是,与原始版本中使用的格式相比,它是一个简化和精简的版本.NET 框架。

不同部门之间已经朝着标准化的方向发展.NET 框架,以及。因此,NET Core 2 有一个更大的 API 表面。接口规范,称为.NET 标准 2,涵盖了.NET Core.NET 框架和 Xamarin。还努力将可扩展应用标记语言 ( XAML )标准化为 XAML 标准,该标准将在通用视窗平台 ( UWP )和 Xamarin 上运行。表单应用。

C#和.NET 可以在各种各样的平台上使用,也可以在大量不同的用例中使用,从服务器端的 web 应用到移动应用甚至游戏(使用 Unity 3D 等游戏引擎)。在这本书里,我们将集中讨论网络应用编程,特别是让网络应用运行良好的一般方法。这意味着我们还将讨论使用 JavaScript 编写客户端 web 浏览器脚本以及所涉及的性能问题。

这本书不仅仅是关于 C#和 ASP.NET 的。它采用整体的方法来表现,旨在教育你一系列相关的话题。我们没有足够的空间深入研究每一件事,所以这里的想法是帮助你发现一些有用的工具、技术和技巧。

在本章中,我们将介绍两者的版本 1 和版本 2 之间的变化。网芯和 ASP.NET 芯。我们还将研究 C#语言的一些新特性。也有许多有用的增加和过多的性能改进。

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

  • 最新消息。网络核心 2.0
  • ASP.NET Core 2.0 有什么新功能
  • 性能提升
  • 。网络标准 2.0
  • 新的 C# 6.0 特性
  • 新的 C# 7.0 特性
  • JavaScript 注意事项

核心 2 有什么新功能

Core 系列主要有两种产品。首先是.NET Core,这是一个提供基本库的低级框架。它可以用来编写控制台应用,也是更高级别的应用框架的基础。

第二个是 ASP.NET Core,这是一个构建运行在服务器上的网络应用和服务客户端(通常是网络浏览器)的框架。这原本是的唯一工作量.NET Core,直到它的范围扩大到可以处理更多样的场景。

我们将分别介绍这些框架在较新版本中的差异。的变化.NET Core 也将适用于 ASP.NET Core,除非您在.NET 框架,第 4 版。

最新消息.NET Core 2

的主要焦点.NET Core 2 是范围的巨大增加。包含的 API 数量是原来的两倍多,并且它支持.NET 标准 2(将在本章后面介绍)。也可以参考。无需重新编译的. NET Framework 程序集。只要程序集只使用已经在中实现的 API,这应该就可以了.NET Core。

这意味着将有更多的 NuGet 包可以使用.NET Core。在以前的版本中,找到您最喜欢的库是否得到支持一直是一个挑战。作者建立了一个列出包兼容性的存储库来帮助解决这个问题。你可以在【https://anclafs.com/】https://github.com/jpsingleton/ANCLAFS找到ASP.NET Core 库和框架支持 ( ANCLAFS )列表。如果您想进行更改,请发送拉取请求。希望在未来,所有的包都支持 Core,这个列表将不再需要。

现在有了支持.NET Core,以及更多的 Linux 发行版。您还可以使用 Visual Studio 2017(仅企业版)执行实时单元测试,这与旧的 NCrunch 扩展非常相似。我们将在第 3 章设置您的环境中更多地讨论工具,我们也将涵盖集装箱化。

性能提升

一些更有趣的变化.NET Core 2.0 是对原始版本的性能改进.NET 框架。对许多框架数据结构的实现进行了调整。一些快速改进或减少内存的类和方法包括:

  • List<T>
  • Queue<T>
  • SortedSet<T>
  • ConcurrentQueue<T>
  • Lazy<T>
  • Enumerable.Concat()
  • Enumerable.OrderBy()
  • Enumerable.ToList()
  • Enumerable.ToArray()
  • DeflateStream
  • SHA256
  • BigInteger
  • BinaryFormatter
  • Regex
  • WebUtility.UrlDecode()
  • Encoding.UTF8.GetBytes()
  • Enum.Parse()
  • DateTime.ToString()
  • String.IndexOf()
  • String.StartsWith()
  • FileStream
  • Socket
  • NetworkStream
  • SslStream
  • ThreadPool
  • SpinLock

我们这里不讨论具体的基准测试,因为基准测试很难,您看到的改进显然取决于您的使用情况。需要指出的是,已经做了大量工作来提高的性能.NET Core。这些变化中有许多来自社区,这显示了开源开发的好处之一。这些进步中的一些可能会回到常规的未来版本.NET 框架。

对的实时编译器进行了改进.NET Core 2。仅举一个例子,finally块现在几乎和根本不使用异常处理一样高效,这在没有抛出异常的正常情况下是有益的。你现在没有借口不自由使用tryusing块,例如通过checked算法来避免整数溢出。

ASP.NET Core 2 有什么新内容

ASP.NET Core 2 利用了的所有改进.NET Core 2,如果你选择运行它的话。它也将继续运行.NET Framework 4.7,但最好在其上运行.NET Core 如果可以的话。随着范围和支持的增加.NET Core 2,这个问题应该比以前少了。

.NET Core 2 包含了一个新的元包,所以你只需要引用一个 NuGet 项就可以得到所有的东西。但是,它仍然是由单独的包组成的,如果你想挑选的话。他们还没有回到拥有一个大型集会的糟糕的旧时代。一个新的包调整特性确保如果您不使用包,那么它的二进制文件将不会包含在您的部署中,即使您使用元包来引用它。

设置 web 主机配置还有一个合理的默认值。您不再需要单独添加日志记录、红隼和 IIS。日志记录也变得更简单了,因为它是内置的,所以你从一开始就没有借口不使用它。

一个新特性是支持无控制器的剃刀页面。这正是它听起来的样子,它允许你只用一个 Razor 模板来写页面。它类似于网页产品,不要与网页表单混淆。有人说网络表单正在卷土重来;如果发生了这种情况,那么很有希望的是,抽象会被更多地考虑,它不会携带太多的状态。

有一种新的身份验证模型可以更好地利用依赖注入。ASP.NET Core 身份允许您使用 OpenID 和 OAuth 2,并为您的 API 获取访问令牌。您可能还想研究提供许多类似功能的 Identity Server 4 项目。

一个很好的节省时间的方法是,您不再需要在表单中发出具有属性的防伪标记(以防止跨站点请求伪造)来在 post 方法上验证它们。这一切都是自动为您完成的,这应该可以防止您忘记这样做而留下安全漏洞。

性能提升

ASP.NET Core 区的绩效有了额外的提高,但与中的改进无关.NET Core,这也有帮助。通过发送已经通过及时编译过程的二进制文件,缩短了启动时间。

虽然输出缓存在 ASP.NET Core 2 中不是一个新特性,但是现在已经可以使用了。在 1.0 中,只包括响应缓存,它只是设置正确的 HTTP 头。在 1.1 中,增加了内存缓存,现在,您可以使用本地内存或保存在 SQL Server 或 Redis 中的分布式缓存。

标准

标准很重要;这就是为什么我们有这么多。的最新版本.NET 标准是第 2 版.NET Core 2 实现了这一点。思考的好方法.NET 标准是一个类要实现的接口。接口将定义一个抽象的应用编程接口,但是这个应用编程接口的具体实现将留给从它继承的类。另一种思考方式是类似于 HTML5 标准,它被不同的网络浏览器所支持。

版本 2.NET 标准是通过查看.NET 框架和 Mono。这个标准是由.NET Core 2,这就是它比版本 1 包含更多 API 的原因。的 4.6.1 版.NET 框架也实现了.NET 标准 2,并且有工作支持最新版本的.NET 框架、UWP 和 Xamarin(包括 Xamarin。表单)。

还有新的 XAML 标准,旨在寻找 Xamarin 之间的共同点。表格和 UWP。希望未来包括Windows Presentation Foundation(WPF)。由于这是一本关于 web 应用的书,我们将不讨论 XAML 和本地用户界面。

如果您创建使用这些标准的库和包,那么它们将在支持它们的所有平台上工作。作为一个简单使用库的开发人员,您不需要担心这些标准。这只是意味着您更有可能在您正在使用的平台上使用您想要的包。

新的 C#特性

不仅仅是框架和库被开发了。底层语言也增加了一些不错的新特性。这里我们将重点讨论 C#语言,因为它是公共语言运行时 ( CLR )最流行的语言。其他选项包括 Visual Basic 和函数式编程语言 F#。

C#是一种很好的工作语言,尤其是与 JavaScript 这样的语言相比。尽管 JavaScript 很棒有很多原因(比如它的无处不在和可用框架的数量),但是语言的优雅和设计并不是其中之一。我们将在本书的后面介绍 JavaScript。

这些新特性中的许多只是语法糖,这意味着它们没有添加任何新功能。它们只是提供了一种更简洁、更易于阅读的方式来编写做同样事情的代码。

C# 6

虽然 C#的最新版本是 7,但是 C# 6 中有一些非常方便的特性经常没有被充分利用。此外,7 中的一些新增加的内容是对 6 中增加的功能的改进,没有任何上下文就没有多大意义。我们将在这里快速介绍 C# 6 的一些特性,以防您不知道它们有多有用。

字符串插值

字符串插值是熟悉的字符串格式方法的一个更优雅和更容易操作的版本。您现在可以直接将参数嵌入到字符串中,而不是单独提供要嵌入到字符串占位符中的参数。这样可读性更强,更不容易出错。

让我们用一个例子来证明这一点。考虑以下在字符串中嵌入异常的代码:

catch (Exception e)

{

    Console.WriteLine("Oh dear, oh dear! {0}", e);

}

这将在字符串中由零标记的位置嵌入第一个(仅在本例中)对象。这看起来可能很简单,但是如果您有很多对象,并且想在开始时添加另一个对象,它会很快变得复杂。然后,您必须正确地对所有占位符重新编号。

相反,您现在可以在字符串前面加上一个美元字符,并将对象直接嵌入其中。这在下面的代码中显示,其行为与前面的示例相同:

catch (Exception e)

{

    Console.WriteLine($"Oh dear, oh dear! {e}");

}

异常上的ToString()方法输出所有需要的信息,包括名称、消息、堆栈跟踪和任何内部异常。不需要手动解构;如果你错过了,你甚至会错过一些东西。

您也可以使用与以前相同的格式字符串。考虑以下以自定义方式格式化日期的代码:

Console.WriteLine($"Starting at: {DateTimeOffset.UtcNow:yyyy/MM/dd HH:mm:ss}");

当构建这个特性时,语法略有不同。因此,要警惕任何可能不正确的旧博文或文档。

空条件

空条件运算符是简化空检查的一种方式。现在可以对 null 进行内联检查,而不是使用 if 语句或三元运算符。这使得它更容易在更多的地方使用,并有望帮助您避免可怕的空引用异常。

您可以避免执行手动空值检查,如以下代码所示:

int? length = (null == bytes) ? null : (int?)bytes.Length;

现在可以通过添加问号将其简化为以下语句:

int? length = bytes?.Length;

异常过滤器

您可以使用when关键字更容易地过滤异常。您不再需要捕捉您感兴趣的每一种异常,然后在catch块中手动过滤它。这是 VB 和 F#中已经存在的一个特性,所以 C#终于赶上来了,这很好。

这种方法有一些小的好处。例如,如果您的过滤器不匹配,那么异常仍然会被同一try语句中的其他 catch 块捕获。你也不需要记得重新抛出异常来避免被吞噬。这有助于调试,因为 Visual Studio 将不再像您使用throw时那样崩溃。

例如,您可以检查异常中是否有消息,并以不同的方式处理它,如下所示:

catch (Exception e) when (e?.Message?.Length > 0)

在开发该功能时,使用了不同的关键字(if)。所以要小心网上的任何旧信息。

需要记住的一点是,依赖特定的异常消息是脆弱的。如果您的应用是本地化的,那么消息可能与您期望的语言不同。在异常过滤之外也是如此。

异步可用性

另一个小改进是可以在catchfinally块内使用await关键字。当这个非常有用的特性被添加到 C# 5 中时,这在最初是不允许的。关于这一点没有更多的话要说了。实现是复杂的,但是你不需要担心这个,除非你对内部感兴趣。从开发人员的角度来看,它只是工作,就像这个简单的例子:

catch (Exception e) when (e?.Message?.Length > 0)

{

    await Task.Delay(200);

}

这个特性在 C# 7 中得到了改进,请继续阅读。你会看到asyncawait在这本书里用了很多。异步编程是提高性能的好方法,而不仅仅是从 C#代码中提高性能。

表情体

表达式主体允许您使用 lambda arrow 运算符(=>)将表达式分配给方法或 getter 属性,您可能从流利的 LINQ 语法中熟悉这一点。您不再需要提供完整的语句或方法签名和正文。这个特性在 C# 7 中也得到了改进,所以请看下一节中的例子。

例如,getter 属性可以这样实现:

public static string Text => $"Today: {DateTime.Now:o}";

方法可以用类似的方式编写,例如下面的示例:

private byte[] GetBytes(string text) => Encoding.UTF8.GetBytes(text);

C# 7

C#语言的最新版本是 7,在可读性和易用性方面还有更多改进。我们将在这里介绍一些更有趣的变化。

文字

在代码中指定文字值时,还有一些小的附加功能和可读性增强。您可以指定二进制文字,这意味着您不必再使用不同的基来表示它们。您也可以将下划线放在文字中的任何位置,以便于阅读数字。下划线被忽略,但允许您将数字分成常规分组。这特别适合新的二进制文字,因为它可能非常冗长,列出了所有的 0 和 1。

以下面的例子为例,它使用新的0b前缀来指定一个二进制文字,该文字将被呈现为字符串中的一个整数:

Console.WriteLine($"Binary solo! {0b0000001_00000011_000000111_00001111}");

您也可以用其他基数来实现这一点,比如这个整数,它被格式化为使用千位分隔符:

Console.WriteLine($"Over {9_000:#,0}!"); // Prints "Over 9,000!"

元组

C# 7 的一大新特性是支持元组。元组是一组值,现在可以直接从方法调用中返回它们。您不再局限于返回单个值。以前,您可以用一些次优的方法来解决这个限制,包括创建一个自定义的复杂对象来返回,也许用一个普通的旧 C#对象 ( POCO )或数据传输对象 ( DTO ),这是一回事。您也可以使用refout关键字传入一个引用,虽然语法有所改进,但仍然不是很好。

C# 6 中有System.Tuple,但并不理想。这是一个框架特性,而不是语言特性,项目只是编号,没有命名。使用 C# 7 元组,您可以命名对象,它们是匿名类型的绝佳选择,尤其是在 LINQ 查询表达式 lambda 函数中。例如,如果您只想处理可用数据的一个子集,也许在用操作系统过滤数据库表时,比如实体框架,那么您可以为此使用元组。

下面的示例从方法返回一个元组。您可能需要添加System.ValueTuple NuGet 包来实现这一点:

private static (int one, string two, DateTime three) GetTuple()

{

    return (one: 1, two: "too", three: DateTime.UtcNow);

}

您还可以在字符串插值中使用元组,所有值都将被呈现,如下所示:

Console.WriteLine($"Tuple = {GetTuple()}");

输出变量

如果您想将参数传递给一个方法进行修改,那么您总是需要首先声明它们。这不再是必需的,您可以简单地在传入变量时声明它们。还可以使用下划线声明要丢弃的变量。如果您不想使用返回值,例如,在本机框架数据类型的一些 try parse 方法中,这尤其有用。

这里,我们解析一个日期而不首先声明dt变量:

DateTime.TryParse("2017-08-09", out var dt);

在这个例子中,我们测试一个整数,但是我们不关心它是什么:

var isInt = int.TryParse("w00t", out _);

参考

现在,您可以通过引用方法返回值并使用它们。这有点像在 C 语言中使用指针,但更安全。例如,您只能返回传递给方法的引用,而不能修改引用以指向内存中的不同位置。这是一个非常专业的特性,但是在某些特殊情况下,它可以显著提高性能。

考虑以下方法:

private static ref string GetFirstRef(ref string[] texts)

{

    if (texts?.Length > 0)

    {

        return ref texts[0];

    }

    throw new ArgumentOutOfRangeException();

}

您可以像这样调用这个方法,第二个控制台输出行将会以不同的方式出现(one而不是1):

var strings = new string[] { "1", "2" };

ref var first = ref GetFirstRef(ref strings);

Console.WriteLine($"{strings?[0]}"); // 1

first = "one";

Console.WriteLine($"{strings?[0]}"); // one

模式

另一个重要的补充是你现在可以在 C# 7 中使用is关键字匹配模式。这简化了空值和类型匹配的测试。它还可以让您轻松使用强制转换值。这是使用完全多态性的更简单的替代方法(在这种情况下,派生类可以被视为基类并重写方法)。但是,如果你控制了代码库,并且能够适当地使用多态性,那么你仍然应该这样做,并且遵循良好的面向对象编程 ( OOP )原则。

在以下示例中,模式匹配用于分析未知对象的类型和值:

private static int PatternMatch(object obj)

{

    if (obj is null)

    {

        return 0;

    }

    if (obj is int i)

    {

        return i++;

    }

    if (obj is DateTime d ||

       (obj is string str && DateTime.TryParse(str, out d)))

    {

        return d.DayOfYear;

    }

    return -1;

}

switch语句的情况下也可以使用模式匹配,可以打开非原语类型,比如自定义对象。

更多表情体

表达式主体是从 C# 6 中的产品扩展而来的,现在您可以在更多的地方使用它们,例如,作为对象构造函数和属性设置器。在这里,我们扩展了前面的示例,将我们之前刚刚读到的属性的值设置包括在内:

private static string text;

public static string Text

{

    get => text ?? $"Today: {DateTime.Now:r}";

    set => text = value;

}

更多异步改进

async方法可以返回的内容有一些小的改进,虽然很小,但在某些情况下可以提供很大的性能提升。您不再需要返回一个任务,如果这个值已经存在的话,这个任务将是有益的。这样可以减少使用async方法和创建任务对象的开销。

Java Script 语言

你不能写一本关于网络应用的书而不涉及 JavaScript。到处都是。

如果你写了一个对每个请求都进行全页面加载的 web 应用,而它不是一个简单的内容站点,那么它会感觉很慢。然而,用户期望响应速度。

如果你是一个后端开发人员,那么你可能会认为你不必担心这个。但是,如果您正在构建一个应用编程接口,那么您可能想让它易于使用 JavaScript,并且您需要确保您的 JSON 被正确且快速地序列化。

即使你正在浏览器中运行的 JavaScript(或 TypeScript)中构建一个单页应用 ( SPA ),服务器仍然可以发挥关键作用。您可以使用 SPA 服务在服务器上运行 Angular 或 React 并生成初始输出。这可以提高性能,因为浏览器有东西要立即呈现。例如,有一个名为 React.NET 的项目将 React 与 ASP.NET 整合在一起,它支持 ASP.NET Core。

如果你一直在努力跟上.NET 世界,那么 JavaScript 就在另一个层面上了。似乎每周都有新的东西,这可能会导致框架疲劳和选择悖论。可供选择的东西太多了,你不知道该选什么。

我们将在本书的后面介绍一些更现代的实践,并展示它们可以带来的改进性能。我们将关注服务人员,并展示如何使用他们将工作移动到浏览器的背景中,使其感觉对用户更有响应。

摘要

在这一介绍性章节中,您看到了对中发生的变化的简要但高级的总结.NET Core 2 和 ASP.NET Core 2,与以前的版本相比。你也知道.NET 标准 2 及其用途。

我们展示了 C# 6 和 C# 7 中可用的一些新特性的例子。这些对于让你用更少的资源写更多的东西,让你的代码更易读、更容易维护是非常有用的。

最后,我们谈到了 JavaScript,因为它无处不在,这毕竟是一本关于网络应用的书。此外,这是一本关于一般 web 应用性能改进的书,无论使用什么语言或框架,许多课程都是适用的。

在下一章中,您将看到为什么性能很重要,并了解新的.NET Core 堆栈适合在一起。我们还将看到可用的工具,并通过图表了解硬件性能。

二、为什么性能是一个特性

这是成为 C#开发人员的激动人心的时刻。微软正处于其历史上最大的变化之一,它正在拥抱开源软件。ASP.NET 和.NET 框架已经从头开始重建,所以它们是组件化的、跨平台的和完全开源的。最近的许多改进来自社区。

ASP.NET Core 2 和.NET Core 2 包含其他流行的开源项目,包括 Linux。ASP.NET模型视图控制器 ( MVC ) web 应用框架是 ASP.NET Core 的一部分,大量借鉴 Ruby on Rails ,微软热衷于推广工具,如 Node.jsGrunt大口Yeoman 。还内置了对反应、还原和角度单页应用 ( SPAs )的支持。你可以在 TypeScript 中写这些,这是微软开发的静态类型的 JavaScript 版本。

通过阅读这本书,你将学会如何使用这些新的.NET Core 技术。您将能够使您的 web 应用响应输入并根据需求进行扩展。

我们将关注. NET 的最新核心版本。然而,这些技术中的许多也适用于以前的版本,并且它们对于一般的网络应用开发(在任何语言或框架中)都是有用的。

理解所有这些新框架和库是如何结合在一起的可能有点令人困惑。我们将展示使用最新技术时可用的各种选项,引导您走上高速成功之路,并避免性能陷阱。

读完这本书后,你将理解当 web 应用被大规模部署(到分布式基础设施)时会发生什么问题,并知道如何避免或减轻这些问题。您将获得如何编写高性能应用的经验,而不用费力地学习问题。

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

  • 性能是一个特征
  • 常见的性能问题类别
  • 基本硬件知识
  • 微软工具和替代品
  • 新的.NET 命名和兼容性

性能是一个特征

您可能以前听说过将性能视为一流功能的做法。传统上,性能(以及安全性、可用性和正常运行时间等)仅被视为非功能性需求 ( NFR )并且通常有一些需要满足的任意组合指标。你可能以前听过表演这个词。这是表现良好的质量,通常在没有量化的需求中获得,提供的价值很小。在与客户或用户沟通时,最好避免这种公司行话。

使用过时的瀑布开发方法,这些 nfr 不可避免地被留到最后,并从超预算和后期项目中删除,以便完成功能需求。这导致了一个不可靠、慢且经常不安全的不合格产品(因为可靠性和安全性也经常被忽视)。想想有多少次你对那些在响应你的输入方面落后的软件感到沮丧。也许,你使用的是自动售票机或自助结账机,它们对无法使用的情况毫无反应。

还有更好的办法。通过将性能视为一个特性,并在您的敏捷开发过程的每个阶段考虑它,您可以让用户和客户喜欢您的产品。当软件的响应速度超过用户的感知能力时,使用它是一件令人愉快的事情,因为它不会减慢用户的速度。当出现明显的滞后时,用户需要调整自己的行为来等待机器,而不是按照自己的节奏工作。

如今,计算机拥有令人难以置信的处理能力,它们现在拥有的资源比几年前还要多。那么,为什么计算机速度如此之快,计算速度却比人类快得多,而我们仍然拥有响应速度明显较慢的软件呢?答案是写得不好的软件,不考虑性能。为什么会这样?原因是性能差的迹象通常在开发中是看不到的,只有在部署时才会出现。但是,如果您知道要寻找什么,那么您可以在将软件发布到生产环境之前避免这些问题。

这本书将向你展示如何编写软件,这是一种使用的乐趣,永远不会让用户等待或不知情。你将学会如何制造用户会喜欢的产品,而不是让他们沮丧的产品。

常见的性能问题类别

让我们看看一些常见的性能问题,看看它们是否重要。我们还将了解为什么我们在开发过程中经常错过这些问题。我们将研究编程语言的选择、延迟、带宽、计算以及何时应该考虑性能。

语言考虑

人们通常关注使用的编程语言的速度。然而,这往往没有抓住重点。这是一个非常简单的观点,掩盖了技术选择的细微差别。用任何语言写慢软件都很容易。

有了今天大量可用的处理速度,相对慢的解释语言往往足够快,开发速度的提高是值得的。即使读完这本书后你决定使用 C#和. NET,理解其中的论点和权衡也很重要

写最快的软件的方法就是脚踏实地,用汇编语言(甚至是机器码)写。这是复杂的开发、调试和测试,需要专家知识。如今,我们很少这样做,除了非常小众的应用(如虚拟现实游戏、科学数据处理,有时还有嵌入式设备),通常只针对软件的一小部分。

更高层次的抽象是用 Go、C 或 C++等语言编写并编译代码以在机器上运行。这在游戏和其他对性能敏感的应用中仍然很流行,但是您通常必须管理自己的内存(这可能会导致内存泄漏或安全问题,例如缓冲区溢出)。那个.NET Native 项目,目前正在开发中,承诺这种提前编译有很多性能优势,没有缺点,类似于 Go。

上一级是编译成硬件无关的中间语言 ( IL )或字节码并在虚拟机 ( VM 上运行的软件。这方面的例子有 JavaScalaClojure(Java VM 上的字节码),当然还有 C#(公共语言运行库上的 IL)。内存管理通常会得到处理,通常会有一个垃圾收集器 ( GC )来整理未使用的引用(Go and the.NET 原生 CoreRT 也有一个 GC)。这些应用可以在多个平台上运行,并且更加安全。然而,就执行速度而言,您仍然可以获得接近本机的性能,尽管启动速度会受到影响。

上面这些是解释语言,如 Ruby、Python 和 JavaScript。这些语言通常不会被编译,而是由解释器一行一行地运行。它们通常比编译语言运行得慢,但这通常不是问题。一个更严重的问题是在使用动态类型时捕捉 bug。在遇到错误之前,您将看不到它,而当使用静态类型语言时,许多错误会在编译时被捕获。

最好避免一般性建议。您可能会听到反对在 Rails 上使用 Ruby 的论点,引用了 Twitter 出于性能原因不得不迁移到 Java 的例子。这对你的应用来说可能不是问题,事实上,推特的流行是一个很好的问题。运行 Rails 时更大的问题可能是内存占用量大,这使得在云实例上运行成本高。

这一节只是让你尝一尝,主要的教训是,正常情况下语言并不重要。让程序变慢的通常不是语言,而是糟糕的设计选择。C#在速度和灵活性之间提供了一个很好的平衡,这使得它适合广泛的应用,尤其是服务器端 web 应用。

性能问题的类型

有许多类型的性能问题,其中大多数与所使用的编程语言无关。其中很多是由代码在计算机上的运行方式导致的,我们将在本章的后面讨论这一点的影响。

我们将在这里简要介绍常见的性能问题,并将在本书的后续章节中更详细地介绍它们。您可能会遇到的问题通常分为几个简单的类别,包括:

  • 延迟:

    • 内存延迟
    • 网络延迟
    • 磁盘和输入/输出延迟
    • 闲聊/握手
  • 带宽:

    • 过多的有效载荷
    • 未优化的数据
    • 压缩
  • 计算:

    • 处理太多数据
    • 计算不必要的结果
    • 强力算法
  • 响应性:

    • 可以脱机完成的同步操作
    • 缓存和处理陈旧数据

为平台编写软件时,通常会受到两种资源的限制。这些是计算处理速度和访问远程(处理器)资源。如今,处理速度很少成为限制因素,这可以用其他资源来交换,例如,压缩一些数据以减少网络传输时间。访问远程资源,如主内存、磁盘和网络,会有各种时间成本。重要的是要理解速度不是一个单一的值,它有多个参数。这些参数中最重要的是带宽,最关键的是延迟

延迟是操作开始前的时间延迟,而带宽是操作开始后数据传输的速率。发帖硬盘带宽很高,但是延迟也很高。这将使来回发送大量文本文件变得非常慢,但也许,发送大量 3D 视频是一个不错的选择(取决于魏斯曼评分)。手机数据连接可能更适合文本文件。虽然这是一个人为的例子,但同样的问题通常适用于计算堆栈的每一层,它们在时间差上有相似的数量级。问题是,差异太快,我们无法察觉,我们需要使用工具和科学来观察它们。

解决性能问题的秘诀是对技术有更深入的了解,并知道在较低的级别会发生什么。您应该理解框架在网络级别上对您的指令做了什么。对这些命令如何在底层硬件上运行以及它们如何受到部署到的基础架构的影响有一个基本的了解也很重要。

当表现重要时

性能并不总是在每种情况下都很重要。学习什么时候表现重要,什么时候不重要,这是一项需要掌握的重要技能。一般的经验法则是,如果用户必须等待某件事情发生,那么它应该表现良好。如果这是可以异步执行的操作,那么约束就没有那么严格了,除非某个操作非常慢,以至于花费的时间超过了它的时间窗口,例如,在旧的金融服务大型机上的一夜批处理作业。

从 web 应用的角度来看,一个很好的例子是呈现用户视图,而不是发送电子邮件。在返回结果之前,接受表单提交并发送一封电子邮件(或者更糟,许多电子邮件)是一种常见但天真的做法。然而,与数据库更新不同,电子邮件不是需要立即发生的事情。有许多我们无法控制的阶段会延迟电子邮件到达用户。因此,在返回表单结果之前,无需发送电子邮件。在返回表单提交结果后,您可以在后台异步执行此操作。

这里需要记住的重要一点是,重要的是对性能的感知,而不是绝对的性能。与其加快速度,不如不做一些要求很高的工作(或者至少推迟到以后)。

这可能是反直觉的,尤其是考虑到单个计算操作可能太快而无法察觉。然而,乘法因子是成比例的。一次操作可能相对较快,但数百万次操作可能会累积到明显的延迟。优化这些将会由于放大而产生相应的效果。改进以紧密循环或为每个用户运行的代码比修复一天只运行一次的例程要好。

有时候慢一点更好

在某些情况下,进程被设计得很慢,这对于它们的运行和安全性至关重要。这方面的一个很好的例子是密码散列键拉伸,这可能会成为剖析中的一个热门。一个安全的密码散列函数应该很慢,这样密码就不容易被恢复,尽管这是一个糟糕的做法。

我们不应该使用通用的散列函数,如 MD5SHA1SHA256 来散列密码,因为它们太快了。一些为这个任务设计的更好的算法是 PBKDF2bcrypt 甚至 Argon2 用于新项目。也要始终记住每个密码使用一个唯一的盐。我们在这里不再赘述,但是您可以清楚地看到,加快密码散列是不好的,确定在哪里应用优化非常重要。

为什么错过了问题

性能问题在开发中没有被注意到的一个主要原因是一些问题在开发系统中是不可察觉的。在延迟增加之前,问题可能不会出现。这可能是因为大量数据被加载到系统中,检索特定记录需要更长的时间。这也可能是因为系统的每一部分都部署在单独的服务器上,从而增加了网络延迟。当访问资源的用户数量增加时,延迟也会增加。

例如,我们可以快速地将一行插入到一个空数据库中,或者从一个小表中检索一条记录,尤其是当数据库与 web 服务器运行在同一台物理机器上时。当 web 服务器位于一台虚拟机上,而大型数据库服务器位于另一台虚拟机上时,执行此操作所需的时间会急剧增加。

对于一个单独的数据库操作来说,这不是问题,在这两种情况下,对于用户来说,这看起来都一样快。但是,如果软件编写得很差,并且每个请求执行数百甚至数千个数据库操作,那么这很快就会变得很慢。

将此扩展到 web 服务器处理的所有用户(以及所有 web 服务器),这可能是一个真正的问题。开发人员可能没有注意到这个问题的存在,如果他们没有寻找它,因为软件在他们的工作站上表现良好。在软件发布之前,工具可以帮助识别这些问题。

测量

这本书最重要的收获是衡量的重要性。你需要衡量问题,否则你无法解决它们。你甚至不知道你什么时候修好的。度量是在性能问题变得明显之前解决它们的关键。缓慢的操作可以在早期发现,然后修复。

然而,并不是所有的操作都需要优化。保持洞察力很重要,但你应该了解瓶颈在哪里,以及当它们被放大时会如何表现。我们将在后面的章节中讨论测量和分析。

提前计划的好处

当您从一开始就考虑性能时,修复问题会更便宜、更快。软件开发中的大多数问题都是如此。越早抓到虫子越好。发现一个 bug 最糟糕的时候是在它被部署之后,然后被你的用户报告。

与功能性错误相比,性能问题有些不同,因为它们通常只是大规模地暴露出来,除非您去寻找它们,否则在实际部署之前您不会注意到它们。您可以编写集成和负载测试来检查您的具体量化目标的性能,我们将在本书的后面介绍。

了解硬件

请记住,计算机科学中有一台计算机。了解您的代码在什么上运行以及这种情况的影响是很重要的;这不是魔法。

存储访问速度

计算机速度如此之快,以至于很难理解哪个操作快,哪个操作慢。一切都是瞬间发生的。事实上,任何发生在不到几百毫秒内的事情,人类都是察觉不到的。然而,某些事情比其他事情要快得多,只有当数百万个操作并行执行时,才会出现大规模的性能问题。

应用可以访问各种不同的资源,这些资源的选择如下:

  • 中央处理器缓存和寄存器:
    • L1 缓存
    • L2 缓存
    • L3 缓存
  • 随机存取存储
  • 永久存储:
    • 本地固态硬盘 ( 固态硬盘)
    • 本地硬盘 ( 硬盘)
  • 网络资源:
    • 局域网 ( 局域网)
    • 区域网络
    • 全球互联网络

虚拟机 ( 虚拟机)和云基础设施服务可能会简化部署,但可能会增加更多性能复杂性。安装在机器上的本地磁盘实际上可能是共享网络磁盘,并且响应速度比连接到同一机器的真实物理磁盘慢得多。您可能还必须与其他用户争夺资源。

为了了解各种存储形式之间的速度差异,请考虑下图。这显示了从一系列存储介质中检索少量数据所需的时间:

这个图表是为这本书制作的,它使用了在线发现的平均延迟数据。它有对数刻度,这意味着差异非常大。图的顶部代表一秒或十亿纳秒。跨越大西洋来回发送一个数据包大约需要 150 毫秒(ms)或 1.5 亿纳秒(ns),这主要受到光速的限制。这仍然比你想象的要快得多,它会瞬间出现。事实上,将一个像素推到屏幕上通常比将一个数据包传送到另一个大陆需要更长的时间。

下一个最大的条是物理硬盘将机械臂移动到位开始读取数据所需的时间(10 ms)。机械装置很慢。

下一个小节是从本地固态硬盘随机读取一小块数据需要多长时间,大约是 150 微秒。这些是基于闪存技术,它们通常以与硬盘相同的方式连接。

下一个值是通过千兆局域网发送 1 KB (1 千字节或 8 千位)的小数据报所花费的时间,这不到 10 微秒。这通常是数据中心中服务器的连接方式。注意网络本身是多么的快速。真正重要的是你在另一端连接到什么。在另一台机器上对内存中的值进行网络查找比访问本地驱动器要快得多(因为这是一个日志图,所以不能只是堆叠条形图)。

这就把我们带到了主存或 RAM。这很快(查找大约需要 100 ns),这是您的大部分程序运行的地方。但是,这并不直接连接到中央处理器,并且比片上缓存慢。内存可以很大,通常足够容纳所有工作数据集。但是,它没有磁盘大,也不是永久的。停电时它就消失了。

中央处理器本身将包含当前正在处理的数据的小型缓存,可以在不到 10 纳秒的时间内做出响应。现代中央处理器可能有多达三个甚至四个高速缓存,其大小和延迟不断增加。最快的(响应时间不到 1 ns)是 1 级(l 1)缓存,但通常也是最小的。如果您可以将您的工作数据放入缓存中这几兆或几千字节的数据中,那么您可以非常快速地处理它。

缩放方法改变

多年来,计算机的速度和处理能力以指数级的速度增长。密集集成电路中晶体管数量大约每两年翻一番的观察被称为摩尔定律,以英特尔的戈登·摩尔命名。可悲的是,这个时代没有“摩尔”(抱歉)。尽管晶体管密度仍在增加,但单核处理器的速度已经趋于平稳,如今处理能力的提高来自于向多核、多 CPU 和多机器(虚拟和物理)的扩展。多线程编程不再是舶来品;这是必要的。否则,您不能希望超出单个内核的容量。现代 CPU 通常至少有四个内核(即使在移动设备上也是如此)。再加上超线程等技术,你至少有八个逻辑处理器可以玩。幼稚的编程将无法充分利用这些。

传统上,性能和冗余是通过改进硬件来提供的。一切都在一台服务器或大型机上运行,解决方案是使用更快的硬件并复制所有组件以提高可靠性。这就是所谓的垂直缩放,它已经到了生命的尽头。以这种方式扩展是非常昂贵的,超出一定的规模是不可能的。未来是分布式和横向扩展,使用商用硬件和云计算资源。这要求我们以不同于以前的方式编写软件。传统软件无法利用这种扩展,因为它可以轻松使用升级后的计算机处理器的额外功能和速度。

在考虑性能时,有许多必须做出的权衡,有时感觉它更像是一门黑色艺术,而不是科学。然而,采取科学的方法和衡量结果是至关重要的。您通常必须平衡内存使用与处理能力、带宽与存储、延迟与吞吐量。

一个例子是决定是应该在服务器上压缩数据(包括使用什么算法和设置)还是通过网络直接发送数据。这将取决于许多因素,包括网络的容量和两端的设备。

工具和成本

微软产品的授权历来是一个复杂的雷区。你甚至可以在上面参加正式考试并获得资格。微软最近向开源实践的转变非常令人鼓舞,因为开源的最大好处不是免费的金钱成本,而是你不必考虑许可成本。你也可以修复问题,有了许可的许可(比如 MIT ,你就不用太担心了。现在和将来解决许可问题的时间成本和认知负荷可能会使所涉及的资金数额相形见绌(尤其是对小公司或初创公司而言)。

工具

尽管有新的.NET 框架是开源的,但许多工具不是。Visual Studio 和 SQL Server 的某些版本可能非常昂贵。随着订阅的新许可实践,如果您停止支付,您将失去访问权限,并且您需要登录才能进行开发。以前,您可以在订阅过期后继续使用从微软开发者网络(【MSDN】)或 BizSpark 获得许可的现有版本,而无需登录。

考虑到这一点,我们将尝试坚持使用 Visual Studio 的免费(社区)版本和 SQL Server 的快速版本,除非有一个功能是本课必不可少的,当它出现时,我们将突出显示它。我们还将使用尽可能多的免费开源库、框架和工具。

对于增强 ASP.NET 生态系统的许多工具和软件来说,有许多替代选项,而且您不仅仅需要使用默认的微软产品。这就是众所周知的替代方案.NET(ALT.NET)运动,它包含来自开源世界的实践。

查看一些替代工具

版本控制方面,Git 是 Team Foundation 版本控制 ( TFVC )的非常受欢迎的替代品。Git 被集成到许多工具(包括 Visual Studio)和服务中,例如 GitHub 或 GitLab。Mercurial (Hg)也是一种选择,尽管 Git 获得了最多的开发者心智份额。Visual Studio Team Services(VSTS)和Team Foundation Server(TFS)都允许您使用 Git(包括 GitHub 和 Bitbucket 集成)或旧版 TFVC。

PostgreSQL 是一个非常棒的开源关系数据库,它与许多对象关系映射器 ( O/RMs )一起工作,包括实体框架 ( EF )和 NHibernate 。现在,它也可以在 Azure 和 MySQL 上使用。Dapper 是一个很好的工具,它提供了一个高性能,替代 EF 和其他臃肿的操作系统。也有很多可用的 NoSQL 选项,例如 Redis 和 MongoDB。

其他代码编辑器和集成开发环境 ( IDEs )都是可用的,比如微软的 Visual Studio Code,它也在苹果 OS X/macOS 上工作。ASP.NET Core 2 在 Linux 上运行(在 Mono 和 CoreCLR 上)。所以,你不需要 Windows(虽然 Nano Server 可能值得研究)。

RabbitMQ 是一个出色的开源消息队列服务器,它是用 Erlang 编写的(WhatsApp 也使用它)。这远远好于 Windows 自带的微软消息队列 ( MSMQ )。托管服务很容易获得,例如 CloudAMQP。

我是一个很长时间的 Mac 用户(从 PowerPC 时代开始),在此之前我已经运行了很长时间的 Linux 服务器。看到 OS X 变得流行,并观察到 Linux 在安卓智能手机和廉价电脑(如树莓皮)上的崛起,这是积极的。您可以在树莓 Pi 2 或 3 上运行 Windows 10,但这不是一个完整的操作系统,仅用于运行物联网 ( 物联网)设备。专业使用 Windows 很久了,用 Mac 和 Linux 开发部署,看看这会带来什么性能影响,这是一个很有意思的机会。

虽然不是开源的(或者总是免费的),但值得一提的是 JetBrains 的产品。TeamCity 是一个非常好的构建和持续集成 ( CI )服务器,有一个免费层。ReSharper 是 Visual Studio 的一个很棒的插件,它会让你成为一个更好的开发人员。还有他们新的 C# IDE,名为 Rider,它基于 ReSharper 和 IntelliJ(为 Android Studio 和他们的其他 IDE 提供动力的平台)。

有一款叫章鱼部署的产品,对于的连续部署极为有用.NET 和.NET Core 应用,它有一个自由层。TFS 还提供 CI 构建/光盘版本,还有云解决方案,如 AppVeyor 和 VSTS。

关于云服务,亚马逊网络服务 ( AWS )显然是 Azure 的替代品。即使 AWS 窗口支持还有待改进,但现在内核可用并在 Linux 上运行,这就不是什么问题了。还有许多其他可用的主机,如果您不需要云的动态扩展,专用服务器对于稳定的负载来说通常会更便宜。

这在很大程度上超出了本书的范围,但是研究其中一些工具是明智的。关键是,如何从大量可用的组件中构建一个系统,总是有选择的,尤其是使用较新版本的 ASP.NET。

新的。网

新 ASP.NET 和。它所依赖的. NET Framework 在 Core 版本 1 中被重写为开源和跨平台的。这些包裹也被分开了,尽管出于令人钦佩的目的,但还是造成了混乱。ASP.NET Core 2 现在被包括在.NET Core 2 安装包连同实体框架核心。这意味着您在部署时不再需要将 ASP.NET Core 框架与您的应用一起交付。这个项目被称为.NET Native 已经延期(除了 UWP 之外),有望在明年内到达。

所有这些不同的名字都可能令人困惑,但给事物命名却很难。菲尔·卡尔顿名言的幽默变体是这样的:

"There are only two hard things in Computer Science: cache invalidation, naming things, and off-by-one errors."

我们已经在这里讨论了命名,我们将在本书的后面讨论缓存。

理解所有这些版本是如何结合在一起的可能有点令人困惑。最好用下面这样的图表来解释这一点,它显示了各层是如何相互作用的:

ASP.NET Core 2 可以对抗现有的.NET 框架 4.7 或新的.NET Core 2 框架。同样地,.NET Core 可以在 Windows、OS X / macOS 和 Linux 上运行,但是旧的.NET 只在 Windows 上运行。

还有 Mono 框架,为了清楚起见,省略了它。这是以前的一个项目.NET 在多个平台上运行。Mono 被微软收购,它是开源的(和其他 Xamarin 产品一起)。在.NET 标准。

核心没有现有的特征填充.NET 框架,尽管在版本 2 中差距已经大大缩小。如果你写图形桌面应用,也许使用Windows Presentation Foundation(WPF),那么你应该坚持.NET 4。没有计划将这些特定于 Windows 的 API 移植到 Core,因为这样就不会跨平台。

由于这本书主要是关于网络应用开发的,我们将使用所有软件的最新核心版本。我们将研究各种操作系统和体系结构的性能影响。如果您的部署目标是计算机,例如使用 ARM 架构处理器的树莓 Pi,这一点尤其重要。它的内存也很有限,在使用包含垃圾收集的托管运行时(如. NET)时,这一点很重要

摘要

让我们总结一下这一章和下一章的内容。我们引入了将性能视为一个特性的概念,并介绍了为什么这很重要。我们还简单介绍了一些常见的性能问题,以及为什么我们在软件开发过程中经常忽略它们。我们将在本书后面更详细地介绍这些内容。

我们展示了不同类型存储硬件之间的性能差异。我们强调了了解您的代码在什么上运行的重要性,最重要的是,当您的用户看到它时,它将在什么上运行。我们讨论了缩放系统的过程是如何从过去改变的,缩放现在是如何水平执行而不是垂直执行的,以及如何在创建代码和系统时利用这一点。我们向您展示了您可以使用的工具以及其中一些工具的许可含义。我们还解释了.NET 以及这些最新的框架如何与稳定的框架相适应。我们谈到了为什么测量是至关重要的。

在下一章中,我们将向您展示如何在使用 Windows、Mac 或 Linux 时开始使用 ASP.NET Core 2。我们还将演示如何使用 Docker 容器来构建和运行您的应用。

三、设置您的环境

的主要好处之一.NET Core 是跨平台的,这意味着它运行在各种各样的操作系统上。您不再需要依赖 Windows 操作系统来托管甚至开发您的.NET 应用。虽然这在技术上以前是可能的,但现在比以往任何时候都更容易,并且得到了微软的积极鼓励。他们甚至在 Azure 上提供 Linux 服务器和预制的 Docker 映像,您可以在其上构建和托管代码。

在本章中,我们将向您展示如何在您选择的操作系统上开始使用最新的工具。我们将介绍在 Windows、macOS(以前的 OS X)和 Linux 上设置开发环境的过程。每个系统都有一个首选的解决方案,但是有跨平台的工具可以用在任何一个系统上。

我们还将讨论使用容器开发和部署应用的现代 DevOps 方式。特别是,我们将介绍如何使用 Docker。容器是打包应用及其依赖项的一种很好的方式,这样您就可以更一致地部署,并且减少令人不快的意外。通过将您的应用作为标准单元发货,您可以更少地担心配置实时生产服务器或设置新的开发人员工作站。

为了帮助您入门,我们将在本章中介绍以下主题:

  • Windows 操作系统
  • 苹果
  • Linux 操作系统
  • Visual Studio 2017
  • 用于 Mac 的 Visual Studio
  • Visual Studio 代码
  • 。网络核心软件开发工具包
  • 命令行工具
  • 集装箱化
  • 码头工人

请随意跳到与您相关的分步指南。或者,如果你对另一面的样子感兴趣,就把它们都读一遍。虽然我们不会涉及与每个操作系统相关的 Visual Studio 代码、命令行和 Docker,但它们适用于所有平台。如果我们看完这本书里所有可能的排列,我们会失去焦点,这将是一个很长的重复阅读。查看我在 https://unop.uk/的博客,了解更多不同的教程。由于 Linux 缺乏官方.NET IDE 和这些命令行工具更适合 Unix 理念(许多简单的工具可以很好地完成一件事),我们将在 Linux 部分主要介绍它们。

We won't be covering JetBrains Rider here, but if you're familiar with their other products (such as ReSharper, or Android Studio, which is based on the same IntelliJ platform), then you may want to give it a try. The main application is written in Java, and as such, it runs on most popular operating systems.

设置有两个主要步骤。首先,安装一些工具来帮助您更容易地处理代码。您可以使用简单的文本编辑器,但这不是一个很好的体验。其次,安装.NET Core 软件开发工具包 ( SDK )现在包含 ASP.NET Core。这将连接到工具中以提供模板,并且也可以从命令行使用。稍后,我们将使用它来构建和运行我们的应用。我们开始吧!

Windows 操作系统

Windows 传统上是开发和托管的主要平台.NET 应用,但这种情况正在改变。但是,它仍然是功能最丰富的开发环境,因为完整版的 Visual Studio 集成开发环境 ( IDE )有很多功能。

Visual Studio 2017

Visual Studio 2017 是撰写本文时 VS 的最新版本。它取代了 VS 2015,不幸的是,它放弃了对某些版本 Windows 的支持。例如,它仍然支持 Windows 7,但安装程序将拒绝在 Windows 8 上运行。这是因为 8.1 被认为是 8 的服务包,微软强烈建议您升级到 8.1。然而,微软会更强烈地鼓励你升级到 Windows 10,有人会说太强烈了:

Windows 10 的长期服务分部 ( LTSB )同样被列为不支持 Visual Studio。然而,安装程序实际上不会拒绝运行,一切都将正常工作。

Windows 10 LTSB is an Enterprise version of Windows 10 that doesn't include many of the annoyances of the regular version (such as forced upgrades, Cortana, and live tiles). You miss out on the Edge browser, but you can, of course, just install Firefox and Chrome, like you would anyway.

在 VS 2017 的较新版本中.NET Framework 可以单独发布到 IDE 中,您可以安装附加的(例如.NET Core 2.0),以便在集成开发环境中使用它们。VS 2017 还支持在容器中调试,这样您就可以在 Linux 虚拟机中运行您的代码,同时仍然可以从窗口单步执行它。

安装 VS

VS 2017 附带了一个新的安装程序,允许您管理并排安装的多个版本的 VS 实例。它负责安装和更新 IDE 实例。按照以下步骤开始:

  1. 打开你选择的公正的第三方浏览器,进入https://www.microsoft.com/net/core。您也可以使用https://dot.net/的简短网址,并按照链接操作:

  1. 除非您拥有专业版或企业版的许可证,否则请下载 VS 2017 社区版。运行下载的文件以启动安装程序;它可能需要自我更新:

  1. 然后,您可以选择想要使用的工作负载。选择一切可能很有诱惑力,但请留意右下角的安装尺寸。选择越多,需要的磁盘空间越多,安装时间越长:

  1. 选择.NET Core、ASP.NET 和.NET 框架,然后安装。下载和安装这些工作负载时,您可以执行其他操作:

  1. 完成后,系统会要求您选择开发设置并选择颜色主题:

正在安装.NET Core 2

如果 VS 安装程序不包括的版本.NET Core,然后可以单独安装。从核心网站下载安装程序并执行。SDK 与主 VS 安装解耦的原因是为了能够以更快的方式迭代它。您将获得 VS 附带的稳定版本,但它可能有点过时:

The .NET Core command-line tools include telemetry turned on by default. This means they report analytics information back to Microsoft, but you can disable this by setting an environment variable. Visual Studio and VS Code also send telemetry by default, but this can be disabled via the UI (or editing the JSON configuration in VS Code).

创建您的第一个应用

创建一个新项目,并选择 ASP.NET Core 网络应用模板。你可以运行这个.NET Core 或 on.NET 框架(但它不会在其他平台上运行)。如果您希望为分布式版本控制创建一个新的 Git 存储库,请选中添加到源代码管理框:

在下一个屏幕上,您可以选择一个网络应用模板。这包括用于 Angular 和 React 的网络应用、应用接口和 SPA 服务。这些模板与命令行工具使用的模板相同,您可以使用任何一个接口来创建它们。在本章的后面,我们将看到如何使用命令行,因为它在每个平台上几乎都是相同的。

选择标准的网络应用模板:

单击“更改身份验证”按钮以更改用户登录您的站点的方式。选择个人用户帐户,但将数据保存在本地。或者,您可以连接到云服务,如下图所示,但我们在此不做介绍:

一旦您创建了应用,VS 将在后台恢复 NuGet 包。完成后,按 F5 在调试器中构建并运行您的应用。您的默认浏览器应该打开,其中包含 ASP.NET Core 模板站点,VS 将显示性能跟踪图:

恭喜你!您刚刚创建了一个 ASP.NET Core 2 网络应用。

苹果个人计算机

苹果电脑一直都很受欢迎,并且有与之相匹配的价格标签。它们特别受开发者和网络开发者的欢迎。也许这是因为它们是唯一可以运行您可能想要测试的所有东西的机器。你可以在一个虚拟机上运行 Windows 和 Linux,但是苹果的 macOS(以前叫做 OS X,发音为 OS ten)不会在任何非苹果的硬件上运行(没有一些说服力)。

这也可能是由于 iPhone 和 iPad 的迅速崛起,以及随之而来的生态系统。如果你想为 iOS 和安卓开发应用,那么唯一合理的选择是使用苹果电脑——尽管进步网络应用正在侵占传统上由本地应用占据的大部分领土。

用于 Mac 的 Visual Studio

VS Mac 是 Xamarin Studio 的重新命名,但它也进行了扩展,并从常规 VS 中获取了很多功能和代码,它仍然最适合制作 Xamarin 跨平台移动和桌面应用,但它也支持。网络核心和 ASP.NET 网络应用。

首先,下载你需要的文件。这包括构建应用的软件,如苹果 VS 或 VS 代码。VS Mac 是一个包含安装程序的 DMG 磁盘映像,而 VS Code 是一个 ZIP 文件,您只需解压并运行即可。您还需要.NET Core SDK,它作为需要安装的 PKG 文件出现。您可能还想要一本方便的指南:

安装 VS Mac

最新版本的 macOS (High Sierra)支持 VS for Mac,但它也适用于旧版本,例如 OS X El Capitan (10.11)和 macOS Sierra (10.12)。

按照以下步骤为苹果电脑安装 Visual Studio:

  1. 打开您下载的 DMG 映像,它将作为驱动器挂载:

  1. 在 Finder 中打开该卷,双击它运行安装程序:

  1. 在安装程序下载并安装您需要的所有内容时,您可以一步一步地完成安装程序并做些其他事情:

在此过程结束时,您将安装 Visual Studio for Mac。在此过程中,您有足够的时间下载和解压缩 Visual Studio 代码。您可能希望将 VS 代码移动到您的Applications文件夹,而不是从Downloads运行它。然后,两个 VS 版本可以坐在一起做朋友:

正在安装.NET Core 2

运行.NET Core SDK 包启动安装程序,然后按照向导操作:

您现在应该能够打开一个终端并键入dotnet来使用命令行工具。例如,键入dotnet --version查看哪个版本的。你正在使用的。

You can use the default shell that comes with the macOS Terminal, or you can use an alternative, such as the Zsh shell. You can manage this with the Oh My Zsh project, which also supports Linux.

创建您的第一个应用

以下步骤将向您展示如何使用 VS 为 Mac 创建 ASP.NET Core 2 网络应用:

  1. 在您的苹果电脑上打开 Visual Studio 应用,创建一个新的解决方案,并在下面选择 ASP.NET Core 网络应用模板.NET Core 类别:

  1. 在下一个屏幕上,选择.NET Core 2.0 作为目标框架:

  1. 最后,添加项目名称并完成向导:

你现在有了一个全新的 ASP.NET Core 2 网络应用。您可以单击“播放”按钮来构建和运行它:

您也可以通过打开包含此解决方案的文件夹,在 Visual Studio 代码中处理它:

Visual Studio 代码

Visual Studio 代码在 Windows、Mac 和 Linux 上运行。VS Code 更像是一个文本编辑器,而不是一个 IDE,但它是一个非常有能力的编辑器,它模糊了编辑器和完整 IDE 之间的界限。它支持智能感知代码完成、构建代码和调试应用。

它是使用网络技术(特别是 TypeScript)构建的,运行在独立的基于网络浏览器的应用托管环境中。然而,它比类似的电子桌面应用,如 Atom 或 Slack 要快得多。在 Azure 网络界面中使用相同的引擎来编辑代码。

要处理庞大的数据文件,最好坚持使用原生文本编辑器,例如 Notepad++(在 Windows 上)或 Sublime Text。

Linux 操作系统

Linux 是桌面市场之外非常受欢迎的操作系统。安卓(谷歌收购的移动操作系统)就是基于此,在全球大部分智能手机上运行。有时它是主要的桌面选项,如果使用具有专门硬件的计算机,如树莓皮。Linux 在嵌入式设备上非常受欢迎,部分原因是它重量轻,对资源的要求通常比典型的 Windows 或 macOS 安装低得多。

Linux 也是 web 服务器最流行的操作系统,这也是我们这里最感兴趣的。可以说,Linux 是这个星球上最受欢迎的操作系统,尽管许多用户不会直接与之交互。

Strictly speaking, Linux only refers to the kernel of the OS. The equivalent part of Windows would be the NT kernel. Many different distributions are built using the Linux kernel at their core, and they bundle it up with many other (often GNU) applications. You can even roll your own stripped-down version, perhaps using something like Buildroot.

流行发行版的一些例子是 Ubuntu 和 CentOS(社区企业操作系统,红帽企业 Linux 的免费版本)。Linux 发行版可以根据它们使用的包管理类型进行广泛分类。这在试图找到要安装的软件的正确版本时非常有用,尽管您通常可以通过发行版的包管理器直接获得流行的应用。

If you've not used a package manager before, then you will be amazed at how much easier it is than manually installing software (or even using an App Store) as you'd normally do on Windows or Mac.

许多发行版都是基于 Debian 的,包括 Mint、Ubuntu 和 Raspbian(覆盆子 Pi 的官方操作系统)。这些使用基于 Debian 的包格式和 APT 包管理器。最新版本的 Debian 叫做拉伸和.NET Core 支持它。它也用于官方.NET Docker 图像。

Debian versions are named after Toy Story characters; the previous version was called Jessie.

其他发行版使用红帽企业 Linux ( RHEL ) RPM 包系统和百胜包管理器。其中包括 CentOS 和 YellowDog,仅举几个例子。还有 SUSE Linux 企业服务器 ( SLES ),这是另一个不同的发行版,但是.NET Core 也支持它。

还有很多其他的发行版,这里就不多提了。Linux 不像 Windows 或 Mac 那样是一个操作系统。我们甚至还没有涉及到使用不同内核的基于 BSD 的系统。其中包括 FreeBSD、OpenBSD 和 NetBSD。即使是苹果操作系统也是建立在 BSD 之上的,有其 NeXTSTEP 传统(运行欧洲核子研究中心第一个网络服务器的操作系统)。

这里的大部分说明也将在 Windows 和 Mac 的命令行上工作。工装是一样的;甚至 VS 中的图形工具也使用与命令行dotnet实用程序相同的模板。

开始。基于 Linux 的. NET 内核

我们将把这些东西稍微混合一下,展示一下它的多功能性和多平台性.NET Core 是。在下一节中,我们将演示如何在运行在 Azure 上的 Ubuntu Server 虚拟机上开发. NET Core 应用,只使用命令行,不使用窗口管理器。

这将非常类似于你如何在任何基于 Debian 的发行版上设置东西,包括 Ubuntu、Mint 和 Raspbian(针对树莓 Pi)。树莓 Pi 使用定制 ARM 处理器而不是传统的英特尔 x86 CPU 这一事实并不重要。当 Mono 最初移植到 Pi 时,专业的基于硬件的浮点实现存在问题,但这不再是一个问题.NET Core。

我们将使用命令行文本编辑器,在这里进入 Emacs 和 vi 的争论是愚蠢的,所以使用你喜欢的任何东西。如果您使用平板电脑(如带有蓝牙键盘的 iPad)通过 SSH 或 Mosh 连接到云服务器,这种设置可能会成为一个很好的工作流程。但是,如果您在本地工作,并且希望在您选择的 X11/Wayland 桌面环境中有一个图形编辑器,那么您可以运行 VS Code 或 JetBrains Rider。毫无疑问,你也会经常使用终端窗口。

正在安装.NET Core 2

在本指南中,我们使用的是 Ubuntu 服务器 16.04 LTS,但是这些说明也适用于其他版本。使用高级打包工具 ( APT ,我们可以安装.NET Core 非常容易,但首先我们需要添加一个自定义提要.NET Core 不在默认存储库中。

首先我们需要添加.NET Core 包源到 APT 使用的列表。包源在 Azure 上,所以我们把这个 URL(加上一些其他信息)写到apt-get使用的源文件夹中的一个文件中。为此,我们使用以下命令将echo的标准输出重定向到它:

sudo sh -c 'echo "deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet-release/ xenial main" > /etc/apt/sources.list.d/dotnetdev.list' 

所有包都用公钥加密技术签名,以验证它们的完整性(阅读后面的章节了解更多信息)。我们为导入公钥。使用apt-key命令从 Ubuntu 密钥服务器下载. NET Core 包:

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 417A0893

我们需要更新我们的源代码,以便使用这个简单的命令获取最新的更改:

sudo apt-get update

The update command is different to the upgrade command, but they are often used together. While update simply refreshes the information that APT holds about packages, upgrade allows you to very easily update all of the software on the machine (that hasn't been manually installed) to the latest versions. This can be quite a time-saver.

现在我们可以安装了.NET Core,使用以下命令:

sudo apt-get install dotnet-sdk-2.0.0

如果 SDK 包有一天成为默认的源代码列表,那么这就是你需要做的。

If you are trying out a preview, then the package will have the preview tag suffixed (for example, -preview2-006497 ).

在一墙的文本卷轴之后,假设你没有得到任何错误,并且你看到了遥测通知,你准备好了。键入dotnet查看使用信息,dotnet --version查看您正在运行的当前版本。

创建您的第一个应用

运行以下命令从模板创建新的控制台应用:

dotnet new console -o DotNetCoreTwoHelloWorld

然后切换到刚刚创建的目录,并使用以下两个命令运行它:

cd DotNetCoreTwoHelloWorld 

dotnet run

您的控制台应该类似于下面的截图(如果您在 Windows 上使用 PuTTY):

With .NET Core 1.0, you needed to call dotnet restore before building or running your application with the dotnet command. In .NET Core 2.0, this is called on your behalf for dotnet commands that require it, and you no longer need to restore NuGet packages manually this way.

假设基本控制台应用工作正常,让我们创建一个 web 应用。运行以下命令向上移动一个级别到父文件夹并列出模板:

cd .. 

dotnet new

如果没有任何附加参数,dotnet new命令将列出所有可用的模板。这些显示在下面的截图中:

让我们制作一个标准的 MVC 网络应用,就像我们在前面的例子中所做的那样。接下来的三个命令将根据模板创建一个新的 web 应用,切换到创建的文件夹,并运行该应用:

dotnet new mvc -o AspNetCoreTwoMvc 

cd AspNetCoreTwoMvc 

dotnet run &

我们在后台执行应用(用&),这样我们就可以在终端还在运行的时候使用它。您可以使用本地计算机上的浏览器连接到您的服务器,但这需要将 web 服务器绑定到外部 IP 地址,并打开服务器和云防火墙上的端口。

让我们通过使用以下命令下载主页来简单地测试我们的 web 服务器是否正常工作。如果您没有安装wget,那么您可以使用curl来代替:

wget localhost:5000 

这个过程显示在下面的截图中:

我们可以使用命令行文本编辑器查看主页的来源,如下所示:

nano index.html

现在,您将看到默认主页的 HTML 源,如下所示:

要整理,就要结束还在运行的dotnet流程。您可以使用kill命令以启动时显示的标识结束该过程。如果找不到 ID,那么可以使用ps x列出正在运行的进程。

码头工人集装箱化

Docker 是容器技术的实现,它是虚拟机的轻量级版本。它最初是建立在 Linux 容器 ( LXC )上的,但是现在它支持很多其他技术,包括 Windows。对于同一台服务器,您可以运行比传统虚拟机更多的容器,因为它们并不总是在每个映像中都包含完整的操作系统,而且它们共享许多资源。例如,如果您总是使用官方的微软图像,那么 Docker 将在您的.NET Core 应用,同时仍然保持它们的隔离和沙盒。

Containers can significantly reduce costs, both in hosting hardware and time saved not configuring and manually security patching the OS yourself. The .NET team saved about $100,000 a year in hosting costs by moving to Docker, although they probably don't pay the market rate on Azure.

集装箱的好处类似于现实世界中海运集装箱的好处。通过标准化运输单位,不用担心内容,成本可以大大降低。当您将如何移动容器的问题与容器内部的问题分开时,您就可以共享一个公共的基础架构,并且可以更容易地向外扩展。它还允许更大范围的自动化。

使用 Docker,您可以在容器中构建应用并部署它。容器负责运行应用,并携带其全部配置。您部署到的服务器不需要了解您的应用的任何信息;它只需要知道如何运行容器。这大大减少了与运行应用相关的配置问题。所有主要的云托管提供商都为以这种方式打包的托管应用提供容器服务。

One of the best reasons for using containers is that they allow you to develop as life like an environment as possible. This means that there should be fewer surprises and difficulties when the time comes to deploy your code to the production infrastructure.

我们都努力让代码库在新的实时服务器或新的开发人员工作站上正常工作。这些琐事会成为过去。另一个好处是,您可以确信所有的环境都将具有一致的配置,这将有助于解决与将环境链上的版本提升到生产相关的难题。

Docker 运营着一个 Docker Hub,它托管着随时可以使用的映像。微软保留了.NET 图像发布在那里,他们用补丁使它们保持最新。

根据您的互联网连接速度,您可以将这些图片拉下来,以便非常轻松快速地开始。有。适用于 Linux 和 Windows 纳米服务器的 NET Core 和 ASP.NET Core 映像。Linux 映像基于流行的 Debian 发行版。甚至还有.NET Framework 3.5 和 ASP.NET 4 映像,所以它不仅仅局限于最新的跨平台核心产品。

如果您有支持 Hyper-V 的 Windows 版本(例如,Windows 10 Pro、企业版或教育版),Docker 可以在 Hyper-V 上运行。它需要 64 位版本,内部版本号为 10,586 或更高,不幸的是,这排除了 LTSB 版本。要为 Mac 安装 Docker,您将需要一台 2010 年或更高版本制造的机器,运行 El Capitan 10.11 或更高版本。

然而,有一个旧版本的 Docker,叫做 Docker 工具箱,它运行在 VirtualBox(一个跨平台的 VM 主机)上。许多操作系统都支持这一点,包括稍微成熟和稳定的操作系统。

You can start a container from VS when using Visual Studio Tools for Docker, but Docker also allows you to try out .NET without installing anything. If you already have Docker installed, then simply download and run one of the .NET images.

作为.NET Core 是跨平台的,使用 Docker 最简单的方法是在 Linux 虚拟机中。它可以托管在您的苹果电脑或视窗系统上,或者像我们这里的例子一样,托管在云中。

让我们使用命令行从 Docker 网站安装 Docker 社区版,并尝试一下。首先,我们需要向 APT 添加 Docker GPG 密钥。在安装任何东西之前,手动验证这个的指纹是个好主意。

这个过程与我们安装的方式非常相似.NET,但是顺序和语法略有不同。首先,我们需要添加 Docker GPG 公钥,方法是使用curl下载它,并将其传送到apt-key,使用以下命令:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 

然后,我们显示用以下命令添加的密钥的指纹。您应该检查这是否与 Docker 网站上的匹配:

sudo apt-key fingerprint 0EBFCD88

接下来,我们将包源添加到本地列表中。此命令被拆分为多行,以便于阅读:

sudo add-apt-repository \

 "deb [arch=amd64] https://download.docker.com/linux/ubuntu \

 $(lsb_release -cs) \

 stable"

然后,我们用以下命令更新我们的新源:

sudo apt-get update

现在我们可以用这个简单的命令安装 Docker:

sudo apt-get install docker-ce

假设这里没有错误,下一步是测试 Docker 是否正在使用以下冒烟测试容器:

sudo docker run hello-world

您应该得到如下输出:

让我们测试一下。Docker 中的. NET Core,使用以下命令:

sudo docker run microsoft/dotnet-samples

你应该看看。网络机器人作为 ASCII 艺术:

接下来,让我们升级到的最新版本.NET Core 并尝试一个网络应用。

与 Docker 一起使用 ASP.NET Core 2

现在我们已经检查了 Docker 正在工作并且可以运行.NET Core,是时候做一些更复杂的事情了。我们将以交互方式登录一个容器,并在其中创建一个 ASP.NET Core 2 应用。

运行以下命令:

sudo docker run -it --rm microsoft/aspnetcore-build:2

下载映像后,您应该会得到一个稍微不同的命令提示符。让我们更加雄心勃勃,使用 React 和 Redux JavaScript 库创建一个单页应用:

dotnet new reactredux -o AspNetCoreTwoDocker

接下来我们需要从npm恢复 JS 包。注意我们没有安装npm(或者.NET Core);它包含在容器图像中:

cd AspNetCoreTwoDocker 

npm install

让我们再次在后台运行该应用,并下载主页。请注意,红隼网络服务器现在运行在80的默认 HTTP 端口上,因此我们不需要指定替代的非标准端口号:

dotnet run & 

wget localhost

这显示在下面的截图中:

Docker 图像不包含文本编辑器,但是我们仍然可以使用cat命令查看文件的内容。我们不会得到任何语法高亮显示,但我们可以清楚地看到这是一个 React 应用,因为我们的服务器将初始状态渲染到页面中(作为divreact-appid):

cat index.html

使用cat在终端显示的 React app HTML 页面来源如下截图所示:

您可以使用kill再次整理dotnet流程,并使用exit命令离开 Docker 容器返回到您的主机虚拟机。

摘要

在本章中,您看到了如何开始使用三种最流行的操作系统.NET Core 2 支持。您了解了每个平台上可用工具的选择以及使用 ASP.NET Core 2 的不同方式。

您还发现了容器的好处,并意识到如何使用它们。我们鼓励您从本章中选取示例,将它们混合起来,并将其扩展到您需要的环境中。如今,平台和工具之间有很多交叉,所以这些经验教训中有很多是可以转移的。

我们希望你会受到启发,尝试一些新的东西,看看有什么替代方案。只需启动一个新的虚拟机(可能在云中)并摆脱黑客攻击!

在下一章,我们将进入这本书的主要焦点:性能。我们将在您刚刚学习的工具的基础上,向您展示如何测量您的软件,看看它是否慢。您将看到如何识别代码中哪些部分需要改进,如果它确实表现不佳的话。

四、衡量性能瓶颈

测量是构建高性能系统最关键的方面。你不能改变你没有衡量的东西,因为你不知道你的改变产生了什么影响,如果有的话。如果不度量应用,您将无法知道它的性能是否良好。

如果你只是在你的软件感觉慢的时候路过,那么你已经离开它太晚了。你只是被动地解决问题,而不是主动地回避问题。您必须通过衡量来获得良好的性能,即使这种感觉对用户来说很重要。

有些书把测量、分析和剖析留到最后。然而,这是首先应该考虑的事情。修复错误的问题并优化没有性能问题的区域是很常见的。

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

  • 结构化查询语言数据库分析
  • Web 应用分析
  • HTTP 监控
  • 网络监视
  • 科学方法和可重复性

本章将向您展示如何检查性能问题及其发生的位置。我们将描述可以为您提供这些信息的工具,并演示如何有效和正确地使用它们。我们还将向您展示如何始终如一地重复您的实验,以便您在发现问题后能够判断是否已经解决了问题。

在本书的最后,我们将再次讨论测量,但是在这里,我们将关注连续的自动化监控以避免回归。本章将更侧重于手动测试,以识别开发和调试过程中的潜在性能问题。

工具

当你试图发现问题所在时,好的调试工具是必不可少的。您可以编写自己的粗略计时代码,我们将向您展示如何开始计时。然而,专门构建的工具比简单地记录调试信息要好得多。VS 2017 包括一些非常有用的应用洞察工具,使有用的信息变得容易看到。

本章中讨论的许多工具帮助您检查代码外部的区域。我们也将讨论代码的概要分析,但是很难通过这种方式识别问题,除非工作是纯计算性的。变慢经常发生是因为你的应用在它的直接堆栈之外发起的动作,这些动作很难通过简单地单步执行代码来调试。VS 2017 可以向你展示你的应用会采取哪些外部动作,例如,触发 HTTP API 调用。

一行一行地遍历程序会大大降低执行速度,以至于很难识别哪些行快,哪些行慢。但是,VS 确实显示了自上一个调试步骤以来所花费的时间,这有助于解决这个问题。然而,用于修复功能性错误的相同方法不能总是应用于修复性能问题。

结构化查询语言

首先,我们将讨论与 SQL 相关的问题。所以,如果你没有使用关系数据库,你可以跳过这一部分;例如,如果您改用 NoSQL 商店或文档数据库。关系数据库是一种非常成熟的技术,使用起来非常灵活。然而,为了有效地使用它们,对 SQL 语法和数据库如何工作有一个基本的了解是很重要的。甚至 Azure Cosmos DB(以前称为 DocumentDB)也有一个可选的 SQL API。

当使用一个 O/RM 工具,比如实体框架 ( EF )时,忽略 SQL,停留在 C#世界可能很有诱惑力;但是,有能力的开发人员应该能够编写高性能的 SQL 查询。忽视数据库引擎如何工作的现实往往会导致性能问题。使用操作系统工具编写代码很容易,该工具对数据库过于健谈,并且为一个操作发出太多的查询。表中没有正确的索引也会导致性能不佳。

在开发过程中,您可能不会注意到这些错误,除非您使用工具来识别发生的低效事件。在这里,我们将向您展示一些这样做的方法。

事件探查器

SQL Server 事件探查器是一个工具,它允许您检查在您的 SQL Server 数据库上正在执行哪些命令。如果您已经安装了管理工具,那么您的机器上应该已经安装了。您可以这样访问它:

  1. 如果您使用的是微软的 SQL Server,那么可以通过 SQL Server 管理工作室 ( SSMS )的工具菜单访问 SQL Server Profiler:

  1. 加载 SQL Server 事件探查器并连接到您正在使用的数据库。命名您的跟踪并选择优化配置文件模板:

  1. 单击运行,您应该会看到跟踪已经开始。现在,您已经准备好对数据库运行代码,以查看它实际执行的查询:

执行简单的查询

作为测试,您可以使用 Management Studio 执行一个简单的SELECT查询,配置文件窗口应该会充满条目。在噪音中找到您刚刚执行的查询。如果以注释开头,这就更容易了,如下图所示:

持续时间列显示查询花费的时间,单位为毫秒 ( 毫秒)。在本例中,从包含 100 多万个条目的表中选择前 1000 行需要 246 毫秒。对于用户来说,这看起来非常快,几乎是瞬间的。修改查询以返回所有行会使其速度慢很多,如下图所示:

该查询现在需要超过 13 秒(13,455 毫秒)的时间来完成,这是非常慢的。这是一个极端的例子,但是请求比需要更多的数据并在应用代码中过滤或排序是一个相当常见的错误。数据库通常更适合此任务,并且通常是选择所需数据的正确位置。

我们将在接下来的章节中介绍具体的 SQL 错误和补救措施。但是,第一步是知道如何检测应用和数据库之间的通信情况。如果您无法检测到您的应用正在进行低效的查询,那么将很难提高性能。

MiniProfiler

MiniProfiler 是调试数据查询的优秀开源工具。它支持 SQL 数据库和一些 NoSQL 数据库,比如 MongoDB 和 RavenDB。它来自 Stack Exchange,也就是管理 Stack Overflow 问答网站的人。它在你的网页中嵌入一个小部件,向你显示他们获取数据需要多长时间。它还会向您显示 SQL 查询,并警告您常见的错误。虽然 MiniProfiler 是从.NET,它也可用于 Ruby、Go 和 Node.js

与 SQL Server Profiler 相比,MiniProfiler 的最大优势在于它始终存在。您不需要显式地查找 SQL 问题,因此它可以更早地突出问题。在生产环境中运行它甚至是一个好主意,尽管只有登录的管理员才能看到。这样,每次你在网站上工作,你就会看到数据访问是如何进行的。不过,在部署它之前,一定要测试它对性能的影响。

MiniProfiler 现在支持 ASP.NET Core!您可以在http://miniprofiler.com/dotnet/AspDotNetCore阅读如何设置的简单说明。

应用分析

通常,您会想要一份应用中工作执行位置和大部分时间花费在哪里的明细。您可能希望了解计算资源是如何分配的,以及它们是如何变化的。有各种各样的工具可用于此,我们将在这里介绍其中的一些。

应用分析允许您缩小应用中性能不佳的区域。你可以从更高的层次开始,一旦你找到了广阔的领域,你就可以深入挖掘并找到具体的问题。将一个大问题分成几个小块是解决它的好方法,也是使它更容易管理的好方法。

一瞥

惊鸿一瞥是你的网络应用的一个极好的开源插件。像 MiniProfiler 一样,它在您的网页上添加了一个小部件,这样您就可以在浏览和处理网站时看到问题。它提供了类似于浏览器开发工具的信息,但也深入服务器端应用内部,向您展示了哪些操作花费了最多的时间。

惊鸿一瞥在http://getglimpse.com/有售,你可以用 NuGet 为你正在使用的 web 框架和 O/RM 安装它。对于 ASP.NET Core,我们将需要使用惊鸿一瞥版本 2。在撰写本文时,这仍然是一个测试版预发行版。

惊鸿一瞥与应用洞察集成(通过一个 NuGet 包),您可以在惊鸿一瞥工具栏中显示应用洞察收集的信息。现在 Node.js 也有一个新版本的 scope,可以在http://node.getglimpse.com/找到。

使用惊鸿一瞥

安装惊鸿一瞥真的很简单。只有三个步骤:

  1. 用 NuGet 安装。
  2. Startup.cs添加线条。
  3. 构建并运行该应用。

让我们看看这些步骤。

安装包装

在解决方案资源管理器中右键单击您的 web 应用项目,然后选择管理获取包...打开图形包管理器窗口。搜索惊鸿一瞥,选择它,然后点击安装按钮。如果您想安装测试版的惊鸿一瞥 2,那么请确保选中包括预发行复选框:

添加代码

您需要将三段代码添加到您的Startup.cs文件中。在文件顶部的using指令中,添加以下内容:

using Glimpse;

ConfigureServices功能中,添加以下内容:

services.AddGlimpse(); 

Configure功能中,添加以下内容:

app.UseGlimpse(); 

运行您的网络应用

通过按下 F5 来构建和运行您的网络应用。如果在运行时遇到重复类型错误,只需清理解决方案并进行完全重建。您可能需要将迁移应用于数据库,但这只是浏览器中的一个按钮点击。但是,如果将它添加到带有数据的现有项目中,则应该更加小心。

当您运行应用时,您会在浏览器窗口的底部发现一瞥栏,如下图所示。酒吧是一个长条状,但在这个截图中被分解了,以更清楚地显示它:

The first page load may take much longer than subsequent ones, so you should refresh your page for more representative data. However, we won't worry about this for the purposes of this demo.

将鼠标悬停在“浏览”工具栏中的每个部分上,以展开它了解更多信息。您可以通过单击标题来最小化和恢复每个部分(HTTP、HOST 或 AJAX)。

如果您使用默认的 web 应用模板(并保留所有单个用户帐户的身份验证默认值),那么您可以注册一个新用户,使应用执行一些数据库操作。然后,您将在数据库查询字段中看到一些值,如下图所示:

点击惊鸿一瞥标志,查看所有页面请求的历史记录。选择一个来查看详细信息,并通过单击它们来展开 SQL 查询,如下图所示:

一瞥向您展示了应用的每一层花费了多少时间,以及正在运行哪些 SQL 查询。在这种情况下,英孚核心会生成 SQL。

惊鸿一瞥对于追踪性能问题所在非常有用。您可以很容易地看到页面管道的每个部分需要多长时间,并识别速度较慢的部分。

集成驱动电子设备

使用内置于 Visual Studio ( VS )中的分析工具可以非常有助于了解代码的 CPU 和内存使用情况。您还可以看到某些操作所花费的时间。当您将应用部署到服务器集群时,这些应用洞察工具可以集成到 Azure 中;但是,您仍然可以在本地使用它们,而无需将任何内容发送到云中。您不需要向程序中添加任何代码,因为框架是自动检测的。

VS 2017 提供的应用洞察可以执行类似于 MiniProfiler 和 scope 的操作。它不仅可以告诉你你的服务器是如何运行的,还可以通过添加到 web 应用页面的一些嵌入式 JavaScript 向你展示客户端的性能。我们将在第 10 章性能增强工具的缺点中详细介绍应用洞察;这只是一个介绍。

运行应用时,在 VS 中打开诊断工具窗口,如下图所示:

您可以看到 CPU 和内存配置文件,包括用于释放内存的自动垃圾收集事件(内存使用减少之前的标记)。您将能够看到断点事件,如果您有 VS 的企业版,那么您也将能够看到智能跟踪事件。

IntelliTrace is only available in the Enterprise edition of Visual Studio. However, you can still use the performance and diagnostic tools in the free Community edition. You can also access them on Azure via a web interface, but this is optional.

如果您有智能跟踪,那么您可以在 VS 选项中找到它,如下图所示。但是,如果没有此高级功能,诊断工具仍然有用:

当你放入一个断点,你的代码命中它,然后 VS 会告诉你离上一个事件有多长时间了。这显示在事件列表中,并覆盖在断点附近。

VS 分析工具的一些替代工具包括 Redgate ANTS 和 Telerik JustTrace。JetBrains 也有 dotTrace 和 dotMemory。然而,所有这些工具都可能相当昂贵,我们在这里不涉及它们。

监控 HTTP

在处理 web 应用时,通常会使用 HTTP 作为应用协议。知道浏览器和服务器之间发生了什么请求是非常有用的。

浏览器

大多数现代浏览器都有优秀的开发工具,其中包括一个网络选项卡,用于监控对网络服务器的请求和响应。您通常可以通过按下 F12 来访问开发工具。这些都是方便查看网络流量,你仍然可以看到加密的通信,而不用乱动证书。

Chrome 和 Firefox 中的开发工具都非常棒。我们将重点关注网络和时序组件,但我们强烈建议您学习所有功能。如果你知道如何充分利用它们,那么网络开发就容易多了。

例如,Chrome dev 工具允许您在使用设备工具栏(用于模拟移动设备)时截图整个页面,即使页面并不完全适合屏幕。您还可以使用开发工具捕获图像的连续画面,显示页面如何逐渐加载。

Chrome 中的网络开发工具非常有用。它们提供了请求时间的良好可视化,您可以限制连接,以查看它在各种不同的互联网连接上的行为。这对移动设备尤其重要。

从 Chrome 开发工具中选择的网络请求显示在下面的截图中。如果在标题栏上单击鼠标右键,则可以使用其他列:

您可以使用一个简单的复选框来禁用缓存,这样浏览器将始终从您的 web 服务器加载新的资产。您也可以点击某个资源了解更多信息,定时选项卡对于提供连接组件的细分非常有用,例如到第一个字节的时间 ( TTFB )。下面的截图显示了本地 web 服务器的一些基本计时细节:

在本地网络服务器上,这种细分不会包含额外的信息,但是在远程服务器上,它会显示其他信息,例如域名系统 ( 域名系统)主机名查找和 SSL/TLS 握手。这些附加组件显示在下一个屏幕截图中:

火狐浏览器

火狐有类似于 Chrome 的开发工具,但是增加了一些功能。例如,您可以编辑请求并将其重新发送到 web 服务器。“网络”选项卡显示的信息与 Chrome 相同,如下图所示:

详细视图也非常相似,包括计时选项卡。该选项卡显示在以下屏幕截图中:

游手好闲的人

有时候,浏览器工具是不够的。比方说,您正在调试一个本机应用、代码中的 HTTP 客户端或移动浏览器。Fiddler 是一个免费的调试代理,可以捕获客户端和服务器之间的所有 HTTP 流量。只要稍加努力,它也能拦截 HTTPS 的交通。提琴手可以在http://www.telerik.com/fiddler免费下载。

由于这本书的重点是网络开发,我们就不再赘述了。浏览器开发工具适用于目前的大部分工作。他们现在完成了 Fiddler 在获得相同功能(网络请求捕获、节流和计时)之前所扮演的大部分角色。如果您需要的话,Fiddler 仍然在那里,如果您的 web 服务器调用外部 HTTP API,它会很方便,尽管这也经常可以直接在 VS 内部调试。

网络

有时,您需要在比 HTTP 或 SQL 更低的级别进行调试。这就是网络监视器或数据包嗅探器的作用。也许,您想调试一个发送到 SQL Server 数据库的表格数据流 ( TDS )消息,或者一个发送到 SSL 终端负载平衡器的 TLS 握手。或者,也许你想分析一个 SOAP web 服务信封或简单邮件传输协议 ( SMTP )电子邮件连接,看看为什么它不能正常工作。

微软消息分析器

微软消息分析器取代微软网络监视器 ( Netmon )是一个在 Windows 系统上捕获网络流量的工具.NETmon 要求您注销,然后在安装后再次登录,而 Wireshark 不要求。您可以在线阅读更多关于这两个微软工具的信息,但是为了清晰和简洁,我们将重点介绍用于低级网络监控的 Wireshark。

Wireshark

Wireshark(以前称为 Ethereal)是一个开源的跨平台数据包捕获和网络分析应用。它可能是同类工具中最受欢迎的,有许多用途。您可以安装 Wireshark,而无需重启或注销,这使得调试难以重现的实时问题变得非常棒。你可以从https://www.wireshark.org/下载 Wireshark,它运行在 Windows、Mac 和 Linux 上。

Wireshark 对本地开发不是特别有用,因为它只捕获网络上的流量,而不是本地主机。如果您针对本地网络应用运行它,您唯一可能看到的是 VS 报告分析您在集成开发环境中所做的事情回到微软。

You can turn off the Customer Experience Improvement Program (CEIP ) in VS by clicking on the button next to the quick launch box and selecting Settings... . By default, it is on (it's now opt-out, not opt-in as in the previous products).

点击 Wireshark 左上角的鳍图标按钮开始追踪。然后,执行一些网络操作,例如从测试服务器加载网页。一旦你捕捉到一些数据包,点击停止按钮;然后,您可以随意检查收集的数据。

Ask your IT administrator before running Wireshark. It can often pick up sensitive information off of the LAN, which may be against your IT acceptable use policy.

下面的截图显示了 Wireshark 捕获的部分结果:

使用 Wireshark 时可能会有很大的噪音。你会看到低级别的网络数据包,比如地址解析协议 ( ARP )流量,你可以过滤掉或者忽略掉。您可能还会看到来自网络电话和其他联网设备的数据,或者是打算提供给网络上其他计算机的数据。

从顶部窗格中选择您感兴趣的数据包。中间窗格将显示数据包的内容。您通常可以忽略网络堆栈的较低层,例如以太网和 TCP/IP(显示在列表的顶部)。

直接进入最后列出的应用层。如果这是 Wireshark 能够识别的协议,那么它将把它分解成由它组成的字段。

底部窗格显示原始数据包的十六进制转储。这通常不如中间窗格中的解析数据有用。

如果您使用 TLS/SSL(这是您真正应该使用的),那么您将看不到 HTTP 流量的内容。您需要一份服务器私钥的副本来查看 TLS 连接内部,TLS 连接包装并加密 HTTP 应用数据。您将只能看到通过域名系统查找和顶级域名系统证书连接到的域,而不能看到完整的网址或任何有效负载数据。

使用 Wireshark 是一个巨大的话题,有很多很好的资源可以用来了解它,无论是在线的还是离线的。我们在此不再赘述,因为通常没有必要深入到如此低的网络审查级别。然而,这是一个很好的工具,放在你的后口袋里。

滚自己的

有时,您可能想要编写自己的性能度量代码。确保您已经用尽了所有其他选项,并首先研究了可用的工具。

Perhaps, you want to record the execution time of some process to write to your logs. Maybe you want to send this to a system such as Logstash and then visualize the changes over time with Kibana . Both are great open source products from Elastic, and they store data in the Elasticsearch search server. You can read more about both of these tools at https://www.elastic.co/ .

您可以通过存储任务开始前的当前时间并将其与任务完成后的当前时间进行比较来轻松记录任务的时间长度,但是这种方法有许多限制。测量的行为会在一定程度上影响结果,并且时钟在短时间内不准确。如果您需要在进程或多次运行之间共享状态,这对于非常慢的事件可能很有用,但是,为了进行基准测试,您通常应该使用Stopwatch类。

It is usually best to store timestamps in Coordinated Universal Time (UTC ), which is otherwise known as Greenwich Mean Time (GMT ). You will avoid many issues with time zones and daylight saving if you use DateTimeOffset.UtcNow (or at least DateTime.UtcNow ). Name variables and columns to indicate this, for example, TimestampUtc . Use TimeSpan for lengths of time, but if you must use primitives (such as integers), then include the units in the variable or column name, for example, DurationInMilliseconds or TimeoutInSeconds . This will help you avoid confusion when another developer (or your future self) comes to use them. However, to benchmark quick operations, you should use Stopwatch . This class is in the System.Diagnostics namespace.

如果你试图测量一个单一的快速事件,那么你不会得到准确的结果。解决这个问题的一个方法是重复这个任务很多次,然后取平均值。这对于基准测试很有用,但通常不适用于实际应用。然而,一旦你确定了什么对测试最有效,你就可以把它应用到你自己的程序中。

让我们用一个小实验来说明用 PBKDF2 算法(在System.Security.Cryptography命名空间中)对密码进行散列需要多长时间。在这种情况下,测试中的操作并不重要,因为我们只是对时序代码感兴趣。天真的方法可能看起来像下面的代码:

var s = Stopwatch.StartNew(); 
pbkdf2.GetBytes(2048); 
s.Stop(); 
Console.WriteLine($"Test duration = {s.ElapsedMilliseconds} ms"); 

由于测量过程中的缺陷,该代码每次运行时都会输出不同的值。更好的方法是多次重复测试并对输出进行平均,如下面的代码所示:

var s = Stopwatch.StartNew(); 
for (var ii = 0; ii < tests; ii++) 
{ 
    pbkdf2.GetBytes(2048); 
} 
s.Stop(); 
var mean = s.ElapsedMilliseconds / tests; 
Console.WriteLine($"{tests} runs mean duration = {mean} ms"); 

这段代码仍然非常原始,但每次都会输出非常相似的结果。tests值越高,越准确,但测试时间越长。

在启动计时器并进入循环之前,您可以通过预热测试中的函数,然后运行垃圾收集来改进基准测试。您还可以将输出存储在成员变量中,以防止编译器进行巧妙的优化。

然而,在这一点上,最好只使用一个完整的基准测试工具,比如 BenchmarkDotNet。我们这里没有这样做的原因只是为了让您更容易自己运行这些示例。

BenchmarkDotNet 是一个非常有用的工具,它现在已经加入了.NET 基金会。我们将在第 8 章理解代码执行和异步操作中再次介绍。你可以在https://github.com/dotnet/BenchmarkDotNet了解更多。

In these examples, we are using C# 6 string interpolation, but you can use the traditional overloads to Console.WriteLine if you prefer.

让我们编写一个快速的示例应用,通过多次运行这两个不同的版本来演示不同之处。我们将把这两个测试提取到方法中,并分别调用它们几次:

var pbkdf2 = new Rfc2898DeriveBytes("password", 64, 256); 
SingleTest(pbkdf2); 
SingleTest(pbkdf2); 
SingleTest(pbkdf2); 

Console.WriteLine(); 
var tests = 1000; 
AvgTest(pbkdf2, tests); 
AvgTest(pbkdf2, tests); 
AvgTest(pbkdf2, tests); 

Console.WriteLine(); 
Console.WriteLine("Press any key..."); 
Console.ReadKey(true);

输出看起来像下面的截图。如果你想自己运行它,你可以在本书附带的代码中找到完整的应用列表。

你可以看到三个单独的测试可以给出截然不同的结果,但是平均的测试是相同的。

你的结果会有所不同。这是由于计算机系统固有的可变性。对时间敏感的嵌入式系统通常使用实时操作系统。正常的软件系统通常运行在分时操作系统上,在分时操作系统中,您的指令很容易被中断,虚拟机使问题变得更糟。

您将获得不同的结果,这取决于您是在调试模式还是发布模式下构建,以及您是否在调试模式下运行。不调试释放模式( Ctrl + F5 )最快。

下面的截图显示了运行调试的同一个基准测试演示应用。因为dotnet可执行文件显示在命令提示符的标题栏中,所以您可以分辨出来。如果没有调试就运行了,那么就会显示cmd.exe(在 Windows 上),就像前面的截图一样。

Unit testing is very valuable, and you may even practice test-driven development (TDD ), but you should be careful about including performance tests as unit tests. Unit tests must be quick to be valuable, and tests that accurately measure the time taken for operations are often slow. You should set a timeout on your unit tests to make sure that you don't write slow ones with external dependencies. You can still test performance, but you should do it at the integration testing stage along with tests that hit an API, DB, or disk.

科学

我们通过在第 2 章为什么性能是一个特性中展示一些硬件访问速度来讨论计算机科学中的计算机。现在,是科学的时候了。

如果你想获得持续可靠的结果,采取科学的方法是很重要的。有一个方法或测试计划,每次都以同样的方式执行,只改变你想要衡量的东西。自动化对此有很大帮助。

始终用您的数据来衡量系统上的用例也很重要。对别人有效的东西对你来说可能并不太好。

我们将在本书后面更多地谈论科学和统计。取一个简单的平均值可能会误导人,但用它作为一个温和的介绍也没关系。阅读第 10 章性能增强工具的缺点、了解更多关于中位数和百分位数等概念的信息。

可重复性

结果需要是可重复的。如果你每次测试都得到截然不同的结果,那就不能依赖它们。您应该重复测试,并取平均结果来标准化被测应用或硬件中的任何可变性。

清楚地记录测量单位也很重要。当你比较一个新值和一个历史值时,你需要知道这一点。美国宇航局因单位混乱而失去了一个著名的火星探测器。

只改变一件事

测试时,您的目标是衡量单个变更的影响。如果你一次改变一件以上的事情,那么你不能确定哪一件有所不同。

目的是将除了你感兴趣的变化之外的任何其他变化的影响降到最低。这意味着保持外部因素尽可能静态,并进行多次测试,取平均结果。

摘要

让我们总结一下本章和下一章中涉及的测量内容。我们讲述了度量在解决性能问题中的重要性。没有度量,就不能指望写出高性能的软件;你将在黑暗中编码。

我们重点介绍了一些可以用来衡量性能的工具。我们向您展示了如何使用这些选择,以及如何编写自己的选择。我们还讨论了采用科学方法进行测量的价值。确保您的结果是可重复的,并且您记录了正确的测量单位,这是重要的关注点。

在下一章中,我们将学习如何修复常见的性能问题。你将获得加速低挂水果的技能,并让自己在同事面前看起来像一个表演向导。不再是“它在测试中起作用;现在是运营问题”。

五、修复常见性能问题

一旦您识别并定位了性能问题,本章将进入优化的核心。它涵盖了各种领域中最常见的性能问题,并解释了人们经常犯的一些错误的简单解决方案。当使用这些技术时,通过快速加速客户和同事的软件,你会看起来像一个向导。

本章涵盖的主题包括:

  • 网络延迟
  • 选择 N+1 个问题
  • 虚拟机上的磁盘输入/输出问题
  • web 应用中的异步操作
  • 在一个 web 请求中执行太多操作
  • 静态站点生成器
  • 实用的硬件解决方案
  • 缩小过大的图像

本章中的大部分问题集中在当您给常见操作增加延迟或吞吐量从开发中降低时会发生什么。当一切都在一台物理机器上,只有最少的数据时,在测试中运行良好的东西,现在不再像在另一个大陆上有一个应用编程接口,在与网络服务器不同的机器上有一个完整的数据库,并且它的虚拟磁盘完全在网络上的其他地方那样快了。

您将学习如何识别和修复在单台机器上运行时并不总是显而易见的问题。您将看到如何识别您的操作系统或框架何时表现不佳,以及何时对数据库过于健谈,如果使用不当,这种情况很容易发生。

我们将了解如何确保工作在最合适的地方进行,我们还将了解使用正确的分辨率和格式保持图像小的一些方法。这些技术将确保您的应用是高效的,并且不会不必要地通过网络发送数据。

我们还将讨论如何通过改进底层硬件来减少在坏软件中放大问题的因素,从而用另一种方法减轻性能问题。如果软件应用已经部署到生产中并正在使用,这可能是一个很好的临时措施。如果您已经有实时性能问题,那么这可以为您赢得一些时间来设计一个适当的解决方案。

潜伏

如前几章所述,延迟是指操作完成前发生的延迟,有时也称为滞后。您可能无法控制软件应用运行的基础架构的延迟,但是您可以用这样一种方式编写您的应用,它可以以优雅的方式应对这种延迟。

我们将在这里讨论的两种主要延迟类型是网络延迟磁盘延迟。顾名思义,它们分别是通过网络执行操作的延迟和从持久存储介质读取或写入持久存储介质的延迟。您通常会同时处理这两者,例如,对远程虚拟机上的服务器的数据库 ( 数据库)查询将需要以下操作:

  • 从网络服务器到数据库服务器的网络操作
  • 从数据库服务器到存储区域网络(存储区域网络)上的远程磁盘的网络操作
  • 在物理驱动器上查找数据的磁盘操作

Although Solid State Drives (SSDs ) have much lower latency than spinning platter disks, they are still relatively slow. When we talk about disk I/O here, we refer to both types of drive.

您可以清楚地看到,如果您发出太多的数据库操作,典型生产基础架构中存在的延迟会使问题复杂化。您可以通过最小化数据库操作的数量来解决这个问题,这样它们就不会被放大太多。

让我们用一个例子来说明。假设您希望从数据库中返回 200 条记录,并且往返延迟为 50 毫秒 ( 毫秒)。如果您一次检索所有记录,那么总时间将是 50 毫秒加上传输记录的时间。但是,如果您首先检索记录标识符列表,然后单独检索所有记录标识符,总时间将至少为 201 * 50 毫秒= 10.05 秒

不幸的是,这是一个非常常见的错误。在延迟主导吞吐量的系统中,将请求保持在最小是很重要的。

异步操作

最新的。有显著延迟的. NET Framework APIs 将有异步 ( 异步)方法。例如.NET HTTP 客户端(取代 web 客户端)、SMTP 客户端和实体框架 ( EF )都有通用方法的异步版本。事实上,异步版本通常是本机实现,而非异步方法只是它的阻塞包装器。这些方法非常有益,你应该使用它们。然而,当应用于 web 应用编程时,它们可能没有您想象的效果。

We will cover async operations and asynchronous architecture later in this book. We'll also go into Message Queuing (MQ ) and worker services. This chapter is just a quick introduction, and we will simply show you some tools to go after the low-hanging fruit on web applications.

异步应用编程接口在调用方法完成之前将控制权返回给它。这也可以等待,以便在完成时,从进行异步调用的地方继续执行。对于本机桌面或移动应用,等待异步方法通常会将控制权返回给用户界面 ( 用户界面)线程,这意味着软件会保持对用户输入的响应。该应用可以处理用户交互,而不是阻止您的方法。传统上,您可能会使用后台工作人员来完成这些任务。

您永远不应该在 UI 线程上执行昂贵的工作。因此,这种技术确实提高了本机应用的性能。但是,对于服务器端 web 应用,不存在这种 UI 阻塞问题,因为浏览器就是 UI。因此,这种技术不会单独提高单个用户的性能。

浏览器中运行的 JavaScript 代码是另一回事,这会阻塞用户界面。在本书的后面,我们将看到如何以非阻塞的方式(使用异步服务工作者)在后台运行它。

在 web 应用中等待异步 API 方法仍然是一个很好的实践,但是它只允许软件更好地扩展和处理更多的并发用户。通常,在异步操作也完成之前,web 请求无法完成。因此,尽管线程被交还给线程池,并且您可以将它用于其他请求,但是单个 web 请求不会更快完成。

简单的异步工具

由于这本书讨论的是 web 应用编程,所以在本章中我们不会对本机应用用户界面做更多的详细介绍。相反,我们将展示一些简单的工具和技术来帮助 web 应用中的异步任务。

我们将要介绍的工具提供了一些简单的解决方案,只适用于非常小的应用。他们可能不总是可靠的,但有时他们也足够好。如果你想要一个更健壮的解决方案,那么你应该阅读后面关于异步编程和分布式架构的章节。

后台排队

当您有不需要立即执行的操作时,后台排队是一种有用的技术,例如,将统计数据记录到数据库、发送电子邮件或处理支付交易。如果您在单个 web 请求中执行了太多的工作,那么后台排队可能会提供一个方便的解决方案,尤其是如果您不要求操作总是成功的话。

如果你使用 ASP.NET 4.7(或者 4.5.2 以后的任何版本),那么你可以使用HostingEnvironment.QueueBackgroundWorkItem在后台运行一个方法。这比简单地设置一个任务运行更好,因为如果 ASP.NET 关闭,它将发出取消请求,并等待一段宽限期后再取消该项目。但是,这仍然不能保证完成,因为应用可能在任何时候由于意外的重新启动或硬件故障而死亡。如果任务需要完成,那么应该是交易并在完成时做成功记录。然后可以在失败时重试。如果你真的不在乎一个后台工作项目的成功与否,那么它在一次性事件中是可以排队的。

不幸的是,HostingEnvironment.QueueBackgroundWorkItem不是 ASP.NET Core 的一部分。因此,如果您想使用这个,那么您将不得不简单地对作业进行排队。我们稍后将向您展示如何做到这一点,但是如果您使用完整版本的 ASP.NET,那么您可以通过以下方式在后台发送电子邮件:

var client = new SmtpClient(); 
HostingEnvironment.QueueBackgroundWorkItem(ct => 
    client.SendMailAsync(message)); 

假设您已经有了邮件,这将创建一个 SMTP 客户端,并在后台发送电子邮件,而不会阻止进一步的执行。这不使用ct(取消令牌)变量。请记住,电子邮件不能保证发送。因此,如果你需要明确调度它,那么考虑使用另一种方法。

如果您使用 ASP.NET Core,则此功能不可用。但是,您可以手动创建类似于Task.Run的东西,如下例所示。然而,这可能不是处理任何重要事情的最佳方法:

Task.Run(() => asyncMethod(cancellationToken)); 

如果您可以取消您的任务,那么您可以从注入的IApplicationLifetime接口实例中获取ApplicationStopping令牌,作为您的取消令牌传入。这将让您的任务知道应用何时停止,您也可以在优雅地清理时用它阻止关闭。

您应该谨慎使用这种技术,所以我们在这里不会给你一个完整的例子。尽管如此,如果您愿意,您现在应该有足够的指针来深入挖掘和理解 ASP.NET Core 应用的生命周期。

杭火

Hangfire 是一个运行简单后台作业的优秀库,它现在支持.NET Core。你可以在https://www.hangfire.io/阅读更多关于航火的信息。

您需要持久存储,例如 SQL Server,才能使用 Hangfire。这是必需的,以便它可以确保完成任务。如果你的任务很快,那么这个开销会超过收益。您可以使用消息队列或内存存储 Redis 来减少延迟,但这些都是高级主题。

如果您只是想要一个一次性事件,那么使用 Hangfire 就像下面的代码一样简单:

var id = BackgroundJob.Enqueue(() => Console.WriteLine("Hangfire is awesome!"));

您可能希望探索许多更高级的用例,包括延迟和重复的作业。您甚至可以将作业设置为继续其他作业,因此不需要编写自己的批处理处理代码。

选择 N+1 个问题

你可能以前听说过选择 N+1 题。这是与数据库低效查询相关的一类性能问题的名称。病理情况是,您查询一个表中的项目列表,然后查询另一个表以获取每个项目的详细信息,一次一个。这就是这个名字的由来。您可以执行 N 查询(一个查询每个项目的详细信息)和一个查询来获得开始的列表,而不是所需的单个查询。也许更好的名字是选择 1+N延迟部分末尾的示例(本章前面)说明了选择 N+1 的问题。

希望您不会手动编写这样性能不佳的查询,但是如果使用不当,操作系统很容易输出非常低效的 SQL。您也可以使用某种业务对象抽象框架,其中每个对象从数据库中缓慢地加载自己。如果您想将许多这样的对象放在一个列表中,或者从一个大集合中计算一些仪表板指标,这可能会成为性能噩梦。

We will go into detail about SQL and O/RM optimization in Chapter 7 , Optimizing I/O Performance . This chapter will simply offer some quick fixes to common problems.

如果您有一个运行缓慢的应用,在检索数据时有性能问题,那么选择 N+1 可能是问题所在。如前一章所述,运行一个 SQL 事件探查器工具来发现是否是这种情况。如果您看到许多针对您的数据的 SQL 查询,而不是只有一个,那么您可以进入解决方案阶段。例如,如果您的屏幕在页面加载时充满了查询,那么您知道您有问题。

在下面的例子中,我们将使用微 O/RM Dapper(由堆栈溢出团队制作)来更好地说明发生了什么。但是,在使用大型惰性加载库或 O/RM(如 EF 或 NHibernate)时,您更有可能遇到这些问题。

Entity Framework Core does not support lazy loading yet, so you are unlikely to encounter select N+1 problems when using it. The previous full versions of EF do support this and it may be added to EF Core in the future.

考虑一个简单的博客网站。在主页上,我们想要一个帖子的列表,以及每个帖子的评论数量。我们的博文模型可能如下所示:

namespace SelectNPlusOne.Models 
{ 
    public class BlogPost 
    { 
        public int BlogPostId { get; set; } 
        public string Title { get; set; } 
        public string Content { get; set; } 
        public int CommentCount { get; set; } 
    } 
} 

我们还有一个评论模型,可能是这样的:

namespace SelectNPlusOne.Models 
{ 
    public class BlogPostComment 
    { 
        public int BlogPostCommentId { get; set; } 
        public string CommenterName { get; set; } 
        public string Content { get; set; } 
    } 
} 

As this is an example, we kept things simple and only used a single set of models. In a real application, you will typically have separate view models and data access layer models . The controller will map between these, perhaps assisted by a library such as AutoMapper (http://automapper.org/ ).

我们将它呈现为 HTML 的视图可能如下所示:

@model IEnumerable<SelectNPlusOne.Models.BlogPost> 
<table class="table"> 
    <tr> 
        <th>Title</th> 
        <th># Comments</th> 
    </tr> 
    @foreach (var post in Model) 
    { 
        <tr> 
            <td>@post.Title</td> 
            <td>@post.CommentCount</td> 
        </tr> 
    } 
</table> 

我们希望从数据库中填充这些模型和视图。我们有两张桌子,看起来像这样:

这两个表之间的关系如下所示:

在我们的控制器中,我们可以编写代码,如下所示,来查询数据库,从数据库结果中填充模型,并返回视图来呈现它:

using (var connection = new SqlConnection(connectionString)) 
{ 
    await connection.OpenAsync(); 
    var blogPosts = await connection.QueryAsync<BlogPost>(@" 
        SELECT * FROM BlogPost"); 
    foreach (var post in blogPosts) 
    { 
        var comments = await 
            connection.QueryAsync<BlogPostComment>(@" 
            SELECT * FROM BlogPostComment  
            WHERE BlogPostId = @BlogPostId", 
            new { BlogPostId = post.BlogPostId }); 
        post.CommentCount = comments.Count(); 
    } 
    return View(blogPosts); 
} 

我们测试了这个,它有效!我们对自己很满意。它在我们的本地测试数据库上很快完成,该数据库包含一些行。我们到处使用async方法,这一定是为什么这么快。我们甚至只得到每个有问题的博客的评论,而不是每次所有的评论。我们还使用了参数化查询来避免 SQL 注入,一切看起来都很好。运送它!

As this is an example, we kept it simple for clarity. In a real application, you will want to use techniques such as Dependency Injection (such as the DI built into ASP.NET Core) to make it more flexible.

不幸的是,当数据开始增加时(随着帖子和评论的增加),博客开始变得更慢,页面加载时间更长。我们的读者厌倦了等待,放弃了。观众人数随着收入下降。

让我们分析一下数据库,看看问题出在哪里。我们在有问题的数据库上运行 SQL Server Profiler 筛选,并查看正在执行的 SQL。

以下屏幕截图显示了 SQL Server 事件探查器中的筛选器对话框:

我们捕获的跟踪显示正在执行许多查询,对于我们需要的数据来说太多了。问题是我们的代码效率不是很高,因为它使用多个简单的查询,而不是一个稍微复杂的查询。

我们的代码首先获取博客文章列表,然后获取每篇文章的评论,一次一篇。我们也带回了比我们需要的多得多的数据。Async不会加快单个请求的速度,因为我们仍然需要所有数据才能呈现页面。

The bad coding is obvious in this example because Dapper has the SQL right in your code. However, if you use another O/RM, then you wouldn't typically see the SQL in Visual Studio (or your editor of choice). This is an additional benefit of using Dapper because you see the SQL where it's used, so there are no surprises. However, the main benefit of Dapper is that it's fast, very fast, and much faster than EF. It's a great choice for performance and you can read more about it at https://github.com/StackExchange/Dapper .

我们只需要每个帖子的评论数,我们可以在一个查询中获得我们需要的一切(也是唯一需要的)。让我们修改之前的代码,使用稍微复杂一点的 SQL 查询,而不是两个简单的查询,其中一个查询在foreach循环中:

using (var connection = new SqlConnection(connectionString)) 
{ 
    await connection.OpenAsync(); 
    var blogPosts = await connection.QueryAsync<BlogPost>(@" 
        SELECT  
            bp.BlogPostId, 
            bp.Title, 
            COUNT(bpc.BlogPostCommentId) 'CommentCount' 
        FROM BlogPost bp 
        LEFT JOIN BlogPostComment bpc 
            ON bpc.BlogPostId = bp.BlogPostId 
        GROUP BY bp.BlogPostId, bp.Title"); 
    return View(blogPosts); 
} 

An SQL query inside a loop is an obvious code smell that indicates things may not be as well thought-out as they can be.

这种更高效的代码只对数据库执行一次查询,并获得我们需要的所有信息。我们将注释表连接到数据库中的帖子,然后通过分组进行聚合。我们只请求我们需要的列,并将注释计数添加到我们的选择中。

让我们分析一下新代码,看看我们是否解决了这个问题。下面的截图显示,我们现在只执行了一个查询,而不是之前执行的数千个查询:

查询的数量已经大大减少。因此,页面加载要快得多。但是,页面还是很大,因为上面列出了所有的博文,而且很多。这会降低渲染速度,并增加将页面传递到浏览器的时间。

高效分页

在实际应用中,您希望实现分页,以便当表中有大量数据时,您的列表不会太长。在一页上列出成千上万的项目是个坏主意。

您可能希望使用 LINQ 命令来实现这一点,因为它们非常方便。但是,你需要小心。如果您的 O/RM 不知道 LINQ,或者如果您不小心过早地转换到了错误的类型,那么当执行此过滤的最佳位置实际上在数据库中时,过滤可能会在应用内部发生。您的代码可能正在检索所有数据,并在您没有意识到的情况下丢弃了大部分数据。

也许您想修改动作方法return语句,使其看起来像下面这样:

return View(blogPosts.OrderByDescending(bp => bp.CommentCount) 
                     .Skip(pageSize * (pageNumber - 1)) 
                     .Take(pageSize)); 

这是可行的,将大大加快您的应用。但是,它可能没有您认为的效果。应用更快,因为视图呈现更快,因为生成的页面更小。这也减少了向浏览器发送页面和浏览器呈现 HTML 的时间。

然而,应用仍然从数据库中获取所有的博客文章,并将它们加载到内存中。随着数据量的增长,这可能会成为一个问题。如果您想使用像这样的 LINQ 方法,那么您需要检查它们是否一直被处理到数据库。阅读操作系统或框架的文档并仔细检查使用分析器生成的 SQL 是一个非常好的主意。

让我们看看 SQL 应该是什么样子。例如,如果您从前面的查询开始使用 SQL Server,那么您可以按照如下方式对其进行修改,只获取前十个评论最多的帖子:

SELECT TOP 10

    bp.BlogPostId, 
    bp.Title, 
    COUNT(bpc.BlogPostCommentId) 'CommentCount' 
FROM BlogPost bp 
LEFT JOIN BlogPostComment bpc 
    ON bpc.BlogPostId = bp.BlogPostId 
GROUP BY bp.BlogPostId, bp.Title 
ORDER BY COUNT(bpc.BlogPostCommentId) DESC 

我们按评论数量降序排列。但是,如果您愿意,您可以按降序排列身份证,以获得大致的反向时间顺序。从这个有序集合中,我们只选择(或获取)前十条记录。

如果要跳过记录进行分页,SELECT TOP子句不够好。在 SQL Server 2012 及更高版本中,您可以改用以下内容:

SELECT 
    bp.BlogPostId, 
    bp.Title, 
    COUNT(bpc.BlogPostCommentId) 'CommentCount' 
FROM BlogPost bp 
LEFT JOIN BlogPostComment bpc 
    ON bpc.BlogPostId = bp.BlogPostId 
GROUP BY bp.BlogPostId, bp.Title 
ORDER BY COUNT(bpc.BlogPostCommentId) DESC 
OFFSET 0 ROWS

FETCH NEXT 10 ROWS ONLY 

您可以调整OFFSET的值,以获得正确的页码条目。FETCH NEXT值将改变页面大小(一页的条目数)。您可以通过参数化查询传递这些值,如下所示:

using (var connection = new SqlConnection(connectionString)) 
{ 
    await connection.OpenAsync(); 
    var blogPosts = await connection.QueryAsync<BlogPost>(@" 
        SELECT 
            bp.BlogPostId, 
            bp.Title, 
            COUNT(bpc.BlogPostCommentId) 'CommentCount' 
        FROM BlogPost bp 
        LEFT JOIN BlogPostComment bpc 
            ON bpc.BlogPostId = bp.BlogPostId 
        GROUP BY bp.BlogPostId, bp.Title 
        ORDER BY COUNT(bpc.BlogPostCommentId) DESC 
        OFFSET @OffsetRows ROWS 
        FETCH NEXT @LimitRows ROWS ONLY", new 
        { 
            OffsetRows = pageSize * (pageNumber - 1), 
            LimitRows  = pageSize 
        }
    ); 
    return View(blogPosts); 
} 

如果将操作方法签名更新为以下内容,则可以将页面大小和页码作为 URL 参数传入:

public async Task<IActionResult> Index(int pageNumber = 1, 
                                       int pageSize   = 10) 

这里,我们为这两个参数提供了默认值,因此它们是可选的。当没有提供参数时,显示十个结果的第一页。我们需要将页面大小乘以零索引页码来计算正确的偏移量。第一页应该为零,这样就不会跳过任何记录。

It would be a very good idea to apply some validation to the paging parameters. Don't allow users to set them to anything outside of a reasonable range. This is left as an exercise to the reader.

如果我们在探查器中查看数据库服务器上正在执行的查询,那么我们可以看到现在正在运行什么 SQL。我们还可以看到花费的时间,并将其与之前的结果进行比较:

截图中的查询获取第三页的数据,页面大小设置为50条目。因此,它使用了100的偏移量(跳过 50 的前两页)并获取了接下来的 50 行。该网址的查询字符串如下所示:

/?pagenumber=3&pagesize=50 

我们可以看到查询的持续时间已经从之前的 24 毫秒减少到现在的 14 毫秒。

Note how the SQL executes differently when parameters are passed into the query. This is much safer than concatenating user-supplied values directly into an SQL command (so, don't ever do this concatenation). If you build an SQL query with the user input, then you leave your app open to SQL injection attacks. Your entire database could be downloaded via the web using basic point-and-click tools. Your DB could also be altered or deleted and held for ransom, but the attackers won't actually have the backup they claim. Another way your DB could be stolen is if you carelessly put unencrypted backups in a web-accessible location.

如果不使用任何参数,则使用默认值,并且主页仅显示十个条目,根据数据库中的数据,如下图所示:

默认情况下,主页只显示前 10 个评论最多的帖子,但您可以轻松添加带有超链接的页面导航。只需将pagenumberpagesize查询字符串参数添加到网址中。

您可以使用之前在主页或错误分页路径上显示的示例 URL 查询字符串,例如/Home/BadPaging/?pagenumber=3&pagesize=50

导航栏中的链接加载了我们刚刚浏览过的示例。最好的和主页一样,是默认的。前 10 名和糟糕的分页应该是不言自明的。加载 Bad 需要很长时间,尤其是如果您使用项目中包含的数据库创建脚本。您可以使用浏览器开发工具来计时。

对于以前版本的 SQL Server(2012 年之前),有使用ROW_NUMBER()或嵌套SELECT语句的分页变通方法,它们颠倒了排序顺序。如果你使用另一个数据库,比如 PostgreSQL、MySQL 或者 SQLite,那么你可以很容易地用LIMIT子句实现分页。SQL Server 2017 现在有了串联聚合器,所以这是不正确的。

One of the touted benefits of big O/RMs is the layer of abstraction that they offer. This allows you to change the database that you use. However, in practice, it is rare to change something as core as the database. As you can see from the simple paging example, the syntax varies between databases for anything other than simple standard SQL. To get the best performance, you really need to understand the features and custom syntax of the database that you use.

静态站点生成器

数据库是执行任何数据过滤和排序工作的逻辑场所。在应用中这样做是一种浪费。然而,对于一个不经常更新的简单博客来说,数据库首先可能是不必要的。甚至可能成为瓶颈,拖慢整个博客。这是博客引擎的典型问题,比如 WordPress 。更好的方法可能是使用静态站点生成器。

静态站点生成器预先呈现所有页面,并保存生成的 HTML。这可以很容易地由一个简单的网络服务器提供服务,并且可以很好地扩展。当进行了更改并且页面需要更新时,就会重新生成站点并部署新版本。这种方法不包括动态功能,例如评论,但是第三方服务可以提供这些附加功能。

一个流行的静态站点生成器是杰基尔,用 Ruby 写的。 GitHub 提供免费静态网站托管服务 GitHub Pages ,支持 Jekyll,你可以在https://pages.github.com/了解更多。另一个静态站点生成器(写在 Go 中)是 Hugo ,可以在http://gohugo.io/了解。这些工具基本上是极端缓存的一种形式。我们将在下一节以及本书的后面部分讨论缓存。

经常值得后退一步,看看你试图解决的问题是否是一个问题。您可以通过删除数据库来提高数据库性能。

实用的硬件解决方案

对于性能不佳的应用,最好的方法通常是修复软件。然而,务实一点,试着从更大的角度看问题是好的。根据应用的大小和规模,投入更好的硬件可能会更便宜,至少作为一种短期措施。

硬件比开发人员的时间便宜得多,而且总是变得更好。安装一些新硬件可以起到快速修复的作用,并为您赢得一些时间。然后,作为持续开发的一部分,您可以解决软件中任何性能问题的根本原因。您可以在时间表中增加一点时间来重构,并在处理代码时改进代码库的一个区域。

一旦您发现性能问题的原因是延迟,您有两种可能的方法:

  • 减少延迟敏感操作的数量
  • 使用速度更快的计算机或通过将计算机靠得更近来减少延迟

随着云计算的兴起,你可能不需要购买或安装新的硬件。您可以为性能更高的实例类支付更多费用,也可以在云提供商的基础架构内移动东西以减少延迟。

桌面示例

借用本机桌面应用的一个例子,在公司桌面上有性能不佳的业务线 ( LoB )应用是很常见的。桌面可能会很旧,动力不足。由于连接可能是通过到地区办公室的远程链接,因此与中央数据库的联网可能会很慢。

对于一个写得很糟糕的应用,如果它与数据库之间过于频繁,那么从性能上来说,在一个靠近数据库的服务器上(或者在与数据库相同的服务器上)运行该应用会更好。或许,应用工作区和数据库服务器可以位于数据中心的同一个服务器机架中,并通过千兆(或万兆)以太网连接。

然后,用户可以使用远程桌面连接或思杰会话与应用交互。这将减少数据库的延迟,并加快速度,即使考虑到远程用户界面的滞后。这有效地将台式电脑变成了瘦客户机,类似于旧主机的使用方式。

例如,您可以构建一个具有 RAID 固态硬盘和大量内存的高性能服务器,成本远低于开发人员修复大型应用所需的时间。如果你在同一台机器上同时运行应用和数据库,即使写得不好的软件也能运行得很好,尤其是当你在没有虚拟机的裸机上运行时。这种策略会为你赢得时间来妥善解决问题。

这些远程应用和虚拟化技术通常作为工具出售,以帮助部署和维护。然而,潜在的性能优势也值得考虑。

由于 web 应用的兴起,厚客户端桌面应用现在不太常见了。随着网络速度和处理能力的相对进步,架构似乎在服务器上的计算和客户端上的工作之间摇摆不定。

Web 应用

同样的重新定位方法通常对 web 应用来说效果不太好,但这取决于所使用的体系结构。好消息是,对于 web 应用,您通常控制基础架构。本机应用硬件通常不是这样的。

如果使用三层架构,那么可以将应用服务器移近数据库服务器。这是否有效取决于网络服务器和应用服务器之间的聊天程度。如果他们发出太多的网络应用编程接口请求,那么这不会很好地工作。

一个两层架构(网络服务器直接与数据库对话)对于典型的网络应用来说更常见。有些解决方案使用集群数据库或只读镜像将数据放在靠近网络服务器的地方,但这些会增加复杂性和成本。

能够带来显著不同的是代理服务器。一个流行的开源代理服务器是清漆,你也可以使用 NGINX 网络服务器作为代理。代理服务器缓存 web 服务器的输出,这样就不必为每个用户重新生成一个页面。这对于共享页面很有用,但是缓存很困难;通常,您不应该缓存个性化页面。您不希望不小心将某人经过身份验证的私人信息提供给另一个用户。

像清漆这样的代理也可以将网站的不同部分路由到不同的服务器。如果您的站点中有一小部分由于数据库抖动而表现不佳,那么您可以从数据库机器上(或非常靠近数据库机器,例如在同一虚拟机主机上)的网络服务器托管该部分,并将对它的请求路由到那里。网站的其余部分可以保留在现有的 web 服务器场上。

这不是一个长期的解决方案,但是它允许您分离出程序中性能不佳的部分,这样它就不会影响系统的其他部分。一旦它被解耦或隔离,您就可以自由地修复它。您甚至可以将所需的数据分离到单独的数据库中,并与后台进程同步。

还有内容交付网络(CDN),比如 Cloudflare亚马逊 CloudFrontAzure CDN ,都是不错的静态资产缓存。CDNs 将您站点的一部分缓存在靠近用户的数据中心,减少了延迟。Cloudflare 甚至可以免费将 HTTPS 添加到您的站点,包括自动颁发证书。

但是,当信任代理和解密您的流量的服务时,您需要小心。Cloudflare 发生了一个尴尬的安全事件,被称为 Cloudbleed(一个关于 OpenSSL 中 Heartbleed 漏洞的游戏),他们在 web 响应中插入了未初始化的内存。这包括他们客户用户的私人信息,然后被搜索引擎缓存。

即使您只是将 CDN 用于静态库,您也需要相信脚本不会改变。您可以使用子资源完整性 ( SRI )将脚本绑定到特定的哈希来帮助确保这一点。但是,IE 或 Edge 目前不支持这种方式。有关最新的浏览器支持信息,请参见https://caniuse.com/#feat=subresource-integrity。如果哈希验证失败或者 CDN 关闭,您还需要本地回退,就像 Cloudflare 过去所做的那样。

当 SRI 验证失败或 CDN 不可用时,ASP.NET Core 允许您非常容易地向脚本库添加回退。请看本章示例 web 应用中的_Layout.cshtml。您将看到,对于非开发环境,jQuery 和 Bootstrap 脚本标签具有asp-fallback-integrity属性。integrity属性包含 Base64 编码形式的脚本的阿沙-384 哈希(SHA-2 家族的成员)。

You can read more about the CDN offerings of Cloudflare (including HTTP/2 server push and WebSockets on the free plan) at https://www.cloudflare.com/ .

我们将在第 9 章学习缓存和消息队列中更详细地介绍缓存,因此在此不再赘述。缓存是一个具有挑战性的主题,需要很好地理解它,这样您才能有效地使用它。

超大图像

当我们讨论静态资产时,我们应该简单地提到图像优化。我们将在下一章更详细地介绍这一点,但是值得在这里强调一些常见的问题。由于您对基础架构和用户之间的网络状况几乎无法控制,因此除了高延迟之外,低吞吐量可能也是一个问题。

Web 应用大量使用图像,尤其是在登录页或主页上,它们可能形成全屏背景。令人遗憾的是,很常见的情况是,直接从相机中看到一张原始照片。来自照相机的图像通常有几兆字节大小,对于网页来说太大了。

您可以使用一个工具来测试网页上是否有问题,例如谷歌的页面速度洞察。访问https://developers.google.com/speed/pagespeed/insights/,输入网址,点击 ANALYZE 查看结果。谷歌使用这些信息作为他们搜索引擎排名的一部分,所以你最好接受它的建议。慢速网站在搜索结果中排名较低。

您也可以使用浏览器开发工具来查看图像的大小。页面加载后,按下 F12 并查看网络选项卡,查看传输了多少数据以及花费了多长时间。在本地计算机或测试服务器上,您经常会错过这些性能问题,因为映像会很快加载。第一次加载后,它还将存储在浏览器的缓存中,因此请确保进行完全硬重新加载,并清空或禁用缓存。在 Chrome 中(当开发工具打开时),您可以右键单击或长按重载按钮来获得这些额外的选项。使用内置的节流工具来查看用户将如何体验页面加载也是一个好主意。

最基本的图像优化问题通常分为两类:

  • 对于显示区域来说过大的图像
  • 对主题使用错误压缩格式的图像

图像分辨率

最常见的问题是图像的分辨率对于显示它的区域来说太高了。这将强制浏览器调整图像大小以适合屏幕或区域。如果文件的大小不必要的大,那么通过互联网连接传输将花费更长的时间。浏览器将丢弃大部分信息。在将图像添加到网站之前,您应该提前调整图像的大小。

有许多图像处理工具可用于调整图片大小。如果你运行 Windows,那么Paint.NET(https://www.getpaint.net/)是一个优秀的免费软件。这比 Windows 附带的 Paint 程序好得多(尽管,如果您没有其他选择,这也可以)。

对于其他平台,GIMP(https://www.gimp.org/)很不错。如果您更喜欢使用命令行,那么您可能会喜欢 ImageMagick(http://imagemagick.org/script/index.php),它可以通过编程或批处理方式执行大量图像操作任务。还有云托管的影像管理服务,比如Cloudinary(http://cloudinary.com/)。

您应该将图像缩小到实际大小,以便它们显示在用户的屏幕上。处理响应图像时可能会出现复杂情况,这些图像会随着用户屏幕或浏览器窗口的大小而变化。此外,请记住高 DPI 或视网膜显示器,每个逻辑像素可能有多个物理像素。您的图像可能必须更大才能看起来不模糊,但上限仍然可能低于原始大小。很少需要比显示分辨率大一倍以上的图像。我们将在本书后面更详细地讨论响应图像,但值得记住它们。

下图显示了“绘制”中的“调整大小”对话框.NET:

调整图像大小时,保持图像的宽高比不变通常很重要。这意味着按比例改变水平和垂直尺寸。

例如,将“高度”从 600 像素减少到 300 像素,将“宽度”从 800 像素减少到 400 像素(这意味着两个尺寸都减少了 50%),会使图像看起来相同,只是更小。大多数图像处理软件都会帮助这个过程。保持纵横比不变将避免图像看起来拉伸。如果图像需要适合不同的形状,那么它们应该被裁剪。

图像形式

下一个最常见的问题是将图像以错误的文件格式用于内容。不要在网上使用未经压缩的原始图像,如位图 ( BMP )。

对于照片等自然图像,请使用 JPEG 文件格式。JPEG 是一种有损编解码器,这意味着在使用时会丢失一些信息。对于有很多渐变的图片非常好,比如自然场景或者人物的图片。如果图像中有任何文本,JPEG 看起来会很差,因为字母的边缘会有压缩伪像。大多数中低端相机都将图像保存为 JPEG 格式,因此使用它不会丢失任何东西。但是,您应该调整图像的大小,使其变小,如前所述。

对于图或图标等人工图像,使用 PNG 。PNG 是一种无损编解码器,这意味着不会丢弃任何信息。这最适用于带有大块纯色的图像,例如在绘画软件中绘制的图表或截图。PNG 还支持透明度,因此您可以拥有不显示为矩形或半透明的图像。你也可以拥有比gif质量更好的动画 png,但是我们不会在本章中详细讨论它们。

如前所述,您可以使用与调整图像大小相同的工具来更改图像的格式,只需在保存图像时更改文件格式即可。像往常一样,您应该测试什么在您的特定用例中最有效。您可以通过以不同格式(和不同分辨率)保存相同的图像,然后观察磁盘上文件的大小来执行实验。

下图显示了 Paint.NET 可用的图像选项。更多格式可供选择,我们将在第 6 章寻址网络性能中详细介绍:

即使你只在 JPEG 和 PNG 之间选择,你仍然可以做出显著的不同。以下屏幕截图以两种分辨率和两种格式显示了同一图像的文件大小:

以下测试图像是实验中使用的图像。由于边缘较硬,它看起来最好是 PNG,但渐变背景使其更难压缩:

The test images used here are available for download with this book, so you can try the experiment for yourself.

在网络环境中,使用 CSS 作为渐变的透明背景可能最适合这种特定的图像。但是,像这样简单的形状可以更好地表示为可缩放矢量图形 ( SVG )或者用 HTML5 画布。

摘要

在本章中,您学习了一些常见的性能问题以及如何修复它们。我们介绍了异步操作、选择 N+1 问题、实用的硬件选择和过大的图像。

在下一章中,我们将扩展图像优化,并将其扩展到不同类型资源的其他压缩形式。我们将看看使用开源工具在 ASP.NET Core 捆绑和缩小静态资产的过程。

此外,我们将介绍网络主题,如 TCP/IP、HTTP、网络套接字和加密。我们还将介绍缓存,包括对 cdn 的另一种观察。

六、解决网络性能问题

本章基于前一章中讨论过的问题的子集,但更详细。它处理延迟或滞后,这源于用户和应用之间的网络级别。这主要适用于用户通过网络浏览器与应用交互的网络应用。您将学习如何优化您的应用,以满足未知且不受您控制的带宽和延迟。你将把你的有效载荷压缩到尽可能小,然后你将尽快把它们传递给用户。您将了解可用于实现快速响应应用的工具和技术。您还将看到所涉及的权衡,并能够计算这些方法是否应该应用于您的软件。

我们将在本章中讨论的主题包括:

  • 传输控制协议
  • HTTP 和 HTTP/2
  • HTTPS (TLS/SSL)
  • 网络套接字和推送通知
  • 压缩
  • 捆绑和缩小
  • 缓存和 cdn

本章讨论如何加快应用用户的体验。本章中的技巧适用于静态网站或客户端 web 应用,也适用于动态 web 应用。

这些主题适用于任何 web 应用框架。然而,我们将重点关注如何与 ASP.NET,特别是 ASP.NET Core 实施这些措施。我们还将看到 Core 在某些方面与完整版的 ASP.NET 有何不同。

互联网协议

了解您的 HTML 和其他资产是如何从网络服务器传递到用户浏览器的非常重要。其中大部分是抽象出来的,对 web 开发来说是透明的,但是为了获得高性能,至少有一个基本的理解是一个好主意。

传输控制协议

传输控制协议/互联网协议 ( TCP / IP )是支撑互联网的一对通信协议的名称。 IP 是两者的低级协议,处理将数据包路由到正确的目的地。IP 可以运行在许多不同的低级协议(如以太网)之上,这也是 IP 地址的来源。

TCP 是 IP 之上的一层,它关注的是数据包的可靠传递和流量控制。TCP 是端口的来源,例如 HTTP 的端口 80 和 HTTPS 的端口 443。还有用户数据报协议 ( UDP ),可以代替 TCP 使用,但是提供的功能比较少。

HTTP 运行在 TCP 之上,这通常是您在 web 应用中要处理的。您可能偶尔需要直接使用简单邮件传输协议 ( SMTP )发送电子邮件,域名系统 ( DNS )将主机名解析为 IP 地址,或文件传输协议 ( FTP )上传和下载文件。

这些协议的基本未加密版本直接在 TCP 上运行,但安全加密版本(HTTPS、SMTPS 和 FTPS)之间有一层。这一层叫做传输层安全 ( TLS ),这是安全套接字层 ( SSL )的现代继承者。SSL 不安全,不推荐使用,不应该再使用。然而,SSL 这个术语仍然是描述顶级域名系统的常用术语,令人困惑。所有浏览器都要求使用 TLS 加密来支持 HTTP/2。

在构建 web 应用时,您可能不会经常考虑较低级别的协议。事实上,你可能甚至不需要考虑 HTTP/HTTPS 那么多。但是,应用下面的协议栈可能会对性能产生重大影响。

下图显示了协议通常是如何堆叠的:

慢启动

为了拥塞控制,TCP 实现了一种称为慢启动的算法。这意味着从浏览器到 web 服务器的连接最初很慢,随着时间的推移会逐渐增加,以发现可用的带宽。您可以更改此设置,以便加速更积极,连接更快。如果您增加初始拥塞窗口,那么性能可以提高,尤其是在带宽好但延迟高的连接上,例如移动 4G 或其他大陆的服务器。

像往常一样,您应该使用 Wireshark 测试您的用例,正如之前在第 4 章测量性能瓶颈中所描述的。改变这个窗口有不利的一面,应该慎重考虑。虽然这可能会加快网站的速度,但它会导致网络设备中的缓冲区被填满,如果没有端到端使用服务质量 ( 服务质量),这可能会给网络电话应用和游戏带来延迟问题。

您可以使用修补程序(KB2472264)或更高版本在 Windows Server 2008 R2 上更改此值。您也可以在 Linux 上轻松调整这一点,当然,ASP.NET Core 使您能够运行。除了 Windows 之外,还可以在 Linux(和苹果操作系统/ OS X)上使用。

我们不会在这里提供详细的说明,因为这应该是一个谨慎考虑的决定,你不应该盲目地应用建议。您可以很容易地在网上找到您在网络服务器上使用的操作系统的说明。

TCP 慢启动只是一个例子,说明为什么你不能忽视互联网技术的较低水平,网络应用站在互联网技术的肩膀上。让我们将堆栈向上移动一点到应用层。

超文本传送协议

作为一个想要提供高性能的网络应用开发者,理解超文本传输协议 ( HTTP )是很重要的。你应该知道你使用的是什么版本的 HTTP,它是如何工作的,以及这如何影响事情,比如请求流水线和加密。

HTTP/1.1 可能是您今天最熟悉的版本,因为它已经使用了一段时间。HTTP/2 变得越来越流行,它改变了做很多事情的最佳方式。

头球

HTTP 使用头来提供关于请求的元数据以及消息正文中的主要有效载荷,就像电子邮件一样。当您查看源代码时,您不会看到这些标题,但是您可以使用浏览器开发工具来观察它们。您可以将头用于许多事情,例如缓存控制和身份验证。Cookies 也作为标头发送和接收。

浏览器一次只能打开有限数量的到单个主机的 HTTP/1.1 连接。如果您需要大量请求来检索一个页面的所有资产,那么它们将被排队,这增加了完全加载它所花费的总时间。当与前面提到的 TCP 慢启动相结合时,这种影响会被放大,从而降低性能。这不是 HTTP/2 的问题,我们将很快讨论这个问题。您可以通过允许浏览器重用连接来减少此问题的影响。您可以通过确保您的网络服务器不发送带有 HTTP 响应的Connection: close头来做到这一点。

HTTP 方法

HTTP 使用多种方法(或动词)。最常见的是GETPOST,但还有很多。通常,我们使用GET请求从服务器检索数据,我们使用POST提交数据并进行更改。GET不应该用来改变数据。

其他有用的动词有HEADOPTIONSHEAD可以检查GET请求的报头,而无需下载主体的开销。这对于检查缓存头以查看资源是否已更改非常有用。OPTIONS通常用于跨来源资源共享 ( CORS )执行预检以验证域。

其他常用的动词有PUTDELETEPATCH。我们主要将这些用于表示状态转移(REST)API,因为它们可以模拟对资源或文件的操作。然而,并不是所有的软件(比如一些代理服务器)都理解它们,所以有时候,我们使用POST来模拟它们。你甚至可能会遇到OPTIONS被代理和网络服务器封锁的问题。

状态代码

HTTP 使用数字响应代码来指示状态。你可能对200 (OK)404 (Not Found)很熟悉,但是还有很多。例如,451 表示内容已被政府强制的审查过滤器阻止。

The 451 status code is in reference to the book Fahrenheit 451 (whose title is the purported temperature at which paper burns). You can read the official document (RFC 7725) at http://tools.ietf.org/html/rfc7725 . If this code is not used, then it can be tricky to discover if and why a site is unavailable. For example, you can find out whether the UK government is blocking your site at https://www.blocked.org.uk/ , but this is just a volunteer effort run by the Open Rights Group, the British version of the Electronic Frontier Foundation (EFF ).

我们通常使用 3xx 代码进行重定向(也许是到 HTTPS)。有各种形式的重定向,具有不同的性能特征(和其他效果)。您可以使用 302 来临时重定向页面,但是浏览器必须每次都请求原始页面,以查看重定向是否已经结束。这对搜索引擎优化 ( SEO )也有不好的影响,这里就不讨论这些了。

更好的方法是使用 301 来指示永久重定向。但是,您需要小心,因为这是无法撤销的,并且客户端不会再次查看原始网址。如果使用重定向将用户从 HTTP 升级到 HTTPS,那么还应该考虑使用 HTTP 严格传输安全 ( HSTS )头。还是那句话,小心行事。

加密

HTTP 加密非常重要。它不仅保护传输中的数据以防止窃听,而且还提供身份验证。这确保了用户实际连接到他们认为自己正在连接的站点,并且页面没有被篡改。否则,肆无忌惮的互联网连接提供商可以在你的网站上注入或替换广告,或者他们可以阻止互联网访问,直到你选择退出家长过滤器。或者还有更糟糕的事情,比如窃取你用户的数据,这些通常是法律要求你保护的。

今天真的没有充分的理由不在任何地方使用加密。开销很小,尽管我们仍然会考虑它们,并向您展示如何避免潜在的问题。反对使用 HTTPS 的理由通常是很久以前计算昂贵时的宿醉。

现代计算硬件非常强大,通常对常见的加密任务有特殊的加速。有许多研究表明,加密的处理开销可以忽略不计。但是,首次建立安全连接时可能会有一点延迟。为了理解这一点,举例说明 TLS 如何工作的简单模型是有用的。

安全通信有两个部分:初始密钥交换和正在进行的通道加密。会话密码,例如高级加密标准 ( AES ),可以非常快速,并且可以接近线速运行。然而,这些密码是对称的,双方都需要知道密钥。这个密钥需要安全地分发,以便只有通信双方拥有它。这叫做密钥交换,它使用非对称加密。这通常也需要第三方为服务器担保,所以我们有一个证书系统。这个初始设置是缓慢的部分,尽管我们稍后会向您展示缺乏 AES 加速的设备的替代方案。

密钥交换

如前所述,密钥交换是在双方之间安全共享加密密钥而不被截获的过程。有各种各样的方法可以做到这一点,这些方法大多依赖于非对称加密。与对称加密(我们用这个密钥交换)不同,这只能用一个密钥在一个方向上执行。换句话说,用于加密的密钥不能用于解密,需要不同的密钥。一旦我们共享了一个密钥,大多数数据就不是这样了。我们这样做的原因是对称加密比非对称加密更快。因此,非对称加密并不用于所有情况,只需要加密另一个密钥。

除了交换密钥之外,浏览器(或其他 HTTPS 客户端)应该检查证书,以确保服务器属于它声称的域。默认情况下,一些编程客户端无法做到这一点,因此这值得一试。也可以实现证书钉住(即使在带有 HTTP 公钥钉住的浏览器中)来提高安全性,但这已经超出了本书的范围。

我们将举例说明密钥交换的两种变体(以简化形式类推),向您展示该过程是如何工作的。如果你愿意,你可以查阅技术细节。

传统上,RSA 是用于顶级域名系统的最常见的密钥交换机制。直到最近,我们还在大多数 HTTPS 连接上使用它。

RSA 以其创造者的名字代表 Rivest-Shamir-Adleman ,它可能是最受欢迎的公钥密码形式。英国探听机构政府通信总部 ( GCHQ )大概在同一时间构思了公钥密码系统,但由于它在 1997 年才公开,因此不可能证明这一点。这项发明归功于惠特菲尔德·迪菲和马丁·赫尔曼,他们最近因此获得了图灵奖。我们将很快进一步讨论他们的工作。

The Turing Award is the Nobel Prize of computing. It's named after Alan Turing, the legendary computing pioneer who helped the allies win WWII while working for the nascent GCHQ, but who was later betrayed by the British government.

RSA 使用大素数来生成公钥和私钥对。公钥可用于加密只能用私钥解密的信息。除此之外,私钥还可以用来签署信息(通常是它的一个散列,可以用公钥来验证。即使使用另一种算法作为密钥交换机制(这是在初始 TLS 握手期间协商的),RSA 也经常用于签署 TLS 证书。

This hashing and signing of certificates is where you may have heard of SHA-1 certificates being deprecated by browsers. SHA-1 is no longer considered secure for hashing and, like MD5 before it, should not be used. Certificate chains must now use at least an SHA-2 hashing algorithm (such as SHA-256 ) to sign.

帮助解释 RSA 如何工作的一个类比是,考虑发送锁而不是发送密钥。你可以给某人挂一把开着的挂锁,保留钥匙。然后,他们可以用你的锁把装有密码的盒子锁在公文包里,并把它送回给你。现在,只有你能打开盒子得到代码。即使是给你发代码的人,一旦他们锁定了盒子,也不能打开它。一旦你们都知道了代码,那么就可以很容易地用这个案例交换文档了。

实际上,这更复杂。你不能确定有人没有拦截你的锁,然后用自己的锁拿到钥匙,复制后再发给你。通常,我们用公钥基础设施 ( PKI )来解决这个问题。受信任的第三方将签署您的证书,并验证它确实是您的公钥,并且您拥有锁。如果证书颁发机构 ( CA )没有以这种方式副署证书,浏览器通常会显示警告。

Diffie-Hellman(D-H)密钥交换是获得共享密钥的另一种方法。在 RSA 发明之前不久,它只是最近才在网络上流行起来。这部分是由于椭圆曲线变型的计算成本降低。然而,另一个原因是短命版本提供了一种被称为完美前向保密 ( PFS )的品质。与 RSA 不同,对称加密的会话密钥从不需要传输。双方都可以计算共享密钥,而不需要通过网络发送(甚至在加密状态下)或永久存储。这意味着,如果密钥被恢复,被窃听的加密交换将来无法解密。使用 RSA 密钥交换,如果您以后获得私钥,您可以以纯文本形式恢复记录的通信。PFS 是对抗大规模监控的一种有用的对策,在大规模监控中,所有的通信都被捕获在一个拉网式网络中并永久存储。

D-H 更适合用混色类比来解释,颜料代表数字。双方选择共享的颜色,每一方选择自己的秘密颜色。两者都将共享颜色与它们的秘密颜色混合,并将结果发送给另一个。然后,每一方将他们收到的颜色与他们的秘密颜色再次混合。双方现在都有相同的颜色,而不必将这种颜色发送到任何可以观察到的地方。

因为这不是一本关于安全性的书,所以我们不会再详细讨论加密算法。如果你感兴趣,这里有大量我们无法涵盖的信息。加密是一个很大的主题,但安全性是一个更广泛的问题。

简要说明 TLS 密钥交换如何为各种方法工作的要点是为了表明它是复杂的。许多消息需要来回发送才能建立安全连接,无论连接有多快,延迟都会减慢每条消息的速度。所有这些都发生在 TLS 握手过程中,在握手过程中,客户端(通常是网络浏览器)和服务器协商共同的功能,并就应该使用什么密码达成一致。还有服务器名称指示 ( SNI )要考虑,类似于 HTTP 主机头,允许多个站点使用同一个 IP 地址。一些年长的客户不支持 SNI。

我们可以使用 Wireshark 观察 TLS 握手。我们不会详细讨论,但是您可以看到客户端和服务器之间至少交换了四条消息。分别是客户端您好服务器您好(包括证书和服务器密钥交换)客户端密钥交换密码协议。如果我们不进行优化配置,浏览器可能会发送更多消息,例如请求中间证书。它(即浏览器)还可以检查吊销列表,以查看证书是否被吊销。

下面的截图显示了使用 Wireshark 捕获的 TLS 握手:

所有这些网络操作都发生得很快。然而,如果连接具有高延迟,那么这些额外的消息会对性能产生放大的影响。计算延迟通常比网络延迟小得多,因此我们可以忽略这些延迟,除非您使用非常旧的硬件。幸运的是,您可以做一些简单的事情来帮助加快速度,让您在享受高性能的同时保持安全。

延迟诊断

TLS 内置了各种机制,您可以使用它们来加快速度。然而,如果你做得不对,也有一些事情会减慢它的速度。一些评估您的顶级域名系统配置的免费在线工具可以从 https://www.ssllabs.com/的 Qualys SSL 实验室获得。https://www.ssllabs.com/ssltest/的服务器测试非常有用。你输入一个网址,他们会给你一个分数以及许多其他信息。

举个例子,如果我们分析一下https://www.packtpub.com/站点,我们可以看到在考试当天它获得了 B 级。这是因为它支持弱的迪菲-赫尔曼参数和过时且不安全的 RC4 密码。然而,这并不总是像删除旧密码那么简单。您可以有一个非常安全的站点,但是您可能会排除一些客户,他们使用不支持最新标准的旧客户端。当然,这将根据您的客户群的性质而有所不同,您应该衡量您的流量并仔细考虑您的选择。

以下截图显示了 SSL 实验室为https://www.packtpub.com/提供的部分报告。

如果我们看一下配置比较好的站点(https://emoncms.org/),可以看到它获得了 A 级。你可以用 HSTS 头球得分。此外,这些头避免了重定向的开销。如果您将域名提交给供应商,您还可以将您的网站嵌入浏览器附带的预加载列表中。

以下截图显示了 SSL 实验室为https://emoncms.org/提供的部分报告:

现代浏览器选择的选项通常是带有 RSA SHA-256 签名和 AES 会话密码的椭圆曲线 Diffie-Hellman 临时密钥交换 ( ECDHE )。临时密钥提供 PFS,因为它们只保存在会话的内存中。通过查看浏览器,您可以看到协商了什么连接。

在 Firefox 中,您可以通过单击网址栏中的锁定图标,然后单击“更多信息”按钮来实现这一点,如下图所示:

在“技术细节”部分,您将看到所使用的密码套件。Firefox 中的下图显示了 ECDHE 密钥交换和 RSA 证书签名:

您也可以通过单击查看证书按钮来查看证书详细信息。该域名通常作为主题字段中的通用名称 ( CN )包含在内。替代域也可以包含在证书主题替代名称扩展中:

在 Chrome 中,您可以在开发人员工具的“安全性”选项卡中查看 TLS 连接信息。例如,下面的截图显示了https://huxley.unop.uk/的安全细节:

以下截图显示了https://emoncms.org/的相同窗口:

You may need to refresh the page to see TLS information if the site was already loaded when you opened the developer tools. You can access the same tab by clicking on the padlock in the Chrome URL bar and then clicking on the Details link.

您可以通过单击打开完整证书详细信息按钮来查看证书(在本机操作系统证书存储中)。安卓 Chrome 的等效屏幕上有一个相同功能的链接,虽然减少了证书信息。

性能调整

我们已经讨论了顶级域名系统最重要的性能调整,因为它与顶级域名系统无关。您应该确保您的 HTTP 连接是可重用的,因为如果不是这样,那么您将招致额外的 TLS 协商损失以及 TCP 开销。缓存也非常重要,我们稍后会详细讨论这一点。

TLS and HTTP both support compression, but these have security implications. Therefore, consider them carefully. They can leak information, and a determined adversary can use an analysis of them to recover encrypted data. TLS compression is deprecated, and it will be removed in TLS 1.3. Therefore, do not use it. We will discuss HTTP compression later on in this chapter.

关于 TLS 的具体建议,您可以做一些事情来提高性能。主要技术是保证你使用会话恢复。这不同于重用 HTTP 连接,这意味着客户端可以重用现有的 TLS 连接,而不必经历整个密钥交换。

You can implement sessions with IDs on the server or with encrypted tickets (in a similar manner to ASP.NET cookies that are encrypted with the machine key). There was a bug in the Microsoft client implementation around ticket encryption key rotation, but the KB3109853 patch fixed it. Make sure that you install this update, especially if you see exceptions thrown when connecting to secure endpoints from your .NET code.

重要的是不要过度使用东西,越大并不总是越好,尤其是在关键尺寸方面。这是性能和安全性之间的权衡,这将取决于您的具体情况。一般来说,当使用 128 位密钥时,最好不要使用 256 位 AES 密钥。

一个 2048 位的 RSA 密钥足够大;低的不安全,大的太慢。可以使用椭圆曲线数字签名算法 ( ECDSA )代替 RSA 进行签名,因为这样会快很多。但是,支持有限,因此您需要并行部署 RSA。

如果您使用 ECDSA,那么 256 位密钥就足够了。对于 ECDHE,256 位也可以,对于没有椭圆曲线的较慢版本(DHE),2048 位就足够了。如果您使用 ECDSA,那么您将看到这个列表,而不是连接详细信息中的 RSA 签名。比如在访问https://huxley.unop.uk/时,下面截图中的细节在 Firefox 中显示。这种差异也显示在之前的 Chrome 截图中:

此外,在您的证书中包含完整的证书链也很重要。如果您未能包含所有中间证书,那么浏览器将需要下载它们,直到在其受信任的根存储中找到一个。你也可以使用一种叫做在线证书状态协议 ( OCSP )装订的技术,通过嵌入撤销数据,所以浏览器不需要检查证书撤销列表。

这两种证书技术都可能增加有效载荷的大小,如果带宽是一个问题,这可能是一个问题。但是,它们将减少消息数量,如果延迟是主要问题,这将提高性能,这通常是这种情况。保持密钥大小小也有助于增加带宽。很难推荐一种通用的方法。因此,一如既往,测试你的独特情况。

还有一种可选的流密码叫做 ChaCha/Poly ,对于移动设备特别有用。这使用了 ChaCha20 流密码和 Poly1305 消息认证码 ( MAC )算法来创建 RC4 的安全替代方案。AES 是一种分组密码,在硬件支持下速度很快,但是许多移动设备和一些旧计算机没有这种加速。ChaCha/Poly 仅使用软件时速度更快。因此,这对电池寿命更好。这在 Chrome 中得到支持,包括安卓的 Chrome 和火狐。你可以在https://caniuse.com/#feat=chacha20-poly1305看到最新的浏览器支持。

As all algorithms are different, you can't directly compare key sizes as a measure of how secure they are. For example, a 256-bit ECDHE key is equivalent to a 3072-bit RSA key. AES is very secure with relatively small keys, but you cannot use it for key exchange. ChaCha/Poly is more comparable to the security of AES 256 than AES 128.

在下面安卓上 Chrome 的截图中,可以看到连接到https://huxley.unop.uk/时,Chrome 使用 CHACHA20_POLY1305 作为流密码,ECDHE 作为密钥交换,ECDSA 作为签名:

The new version of TLS (1.3) is currently a draft, but it is still supported in many places. It will only allow Authenticated Encryption with Additional Data (AEAD ) ciphers. AES-GCM and ChaCha/Poly are the only two ciphers that currently meet these criteria. It will also remove some other obsolete features, such as TLS compression. The good news is that TLS 1.3 not only improves security but also delivers higher performance. It is already supported by the stable versions of Firefox and Chrome. You can check support at https://caniuse.com/#feat=tls1-3 .

有时听起来使用 TLS 并不总是值得的,但是在你的整个网站上使用 HTTPS 是一个很好的主意,包括你加载的任何第三方资源。通过这样做,您将能够利用 HTTP/2 的性能增强特性,其中包括的技术意味着为来自多个域的资源(如 JavaScript 库)提供服务不再至关重要。您可以自己安全地托管所有内容,并避免额外请求的 DNS、TCP 和 TLS 开销。

所有这些都是免费的,因为让我们加密Cloudflare 以零成本提供证书。 Cloudflare 也有 TLS 1.3 测试版,但使用时可能会有安全隐患。让我们加密允许您生成证书以在自己的服务器上使用,从 2018 年开始,它也将提供通配符证书。现在让我们详细看看 HTTP/2。

HTTP/2

顾名思义,HTTP/2 是 HTTP 的新版本。它包含了现代网络的一些显著的性能改进。它的前身是 SPDY ,此后被弃用,取而代之的是 HTTP/2。

如前所述,使用 HTTP/2 的第一步是在整个网站上使用 HTTPS。虽然技术上没有要求,但大多数客户端(所有主要浏览器)都要求使用顶级域名来启用 HTTP/2。这主要得益于 TLS 提供的应用层协议协商 ( ALPN ),可以轻松支持 HTTP/2。它还能防止代理服务器弄乱数据,许多互联网服务提供商用这些数据来降低成本,记录客户的在线行为。

HTTP/2 在许多方面提高了性能。它使用压缩,甚至是头和多路复用,允许多个请求共享同一个连接。它还允许服务器在客户端意识到它需要资源之前推送它知道客户端将需要的资源。但是,这需要一些配置来设置正确的头,如果过度使用,可能会浪费带宽。

多路复用对捆绑和图像拼接(精灵)有影响,我们将在本章后面的压缩部分讨论。这也意味着你不需要在多个域上分割资产(碎片),额外的开销甚至会减慢速度。但是,您可能仍然希望使用一个没有 cookie 的子域来服务没有 cookie 的静态资产,即使新的头压缩意味着带宽节省会更少。如果你使用一个裸域(没有www),那么你可能需要一个新的域名用于无 cookie 使用。

您可以使用浏览器开发工具来确定使用什么版本的 HTTP 来交付您的资产。在 Firefox 中,您可以在“网络”选项卡的详细信息面板上看到这一点。当使用旧协议时,您会看到版本被列为 HTTP/1.1。

下图截图显示https://www.packtpub.com/使用 HTTP/1.1:

In Chrome, you can right-click on the column headers in the network inspector and add a Protocol column. You can also see more detailed network information by entering chrome://net-internals into the address bar. This displays things, such as sessions for HTTP/2 and Quick UDP Internet Connections (QUIC ), an experimental multiplexed stream transport.

下图截图显示https://emoncms.org/也使用 HTTP/1.1,虽然 TLS 配置不同。加密传输层对 HTTP 是透明的:

当使用 HTTP/2 时,您会看到版本被列为 HTTP/2.0。下面的截图显示了https://huxley.unop.uk/的情况,还显示了 CORS、缓存和内容压缩头:

求转发到

网络套接字协议(也通俗地称为网络套接字)是一种不同于 HTTP 的协议。然而,HTTP 启动它,它使用相同的端口,所以我们将在这里简要讨论它。此 HTML5 功能对于推送通知实时通信 ( RTC )应用非常有用。网络套接字使用ws://wss://协议前缀,而不是http://https://。一旦由现有的 HTTP 连接建立,该协议是全双工和二进制的,与 HTTP/1 相反。

在 WebSockets 之前,如果您的 web 服务器想要通知客户端一个变化,那么您将不得不使用一种技术,比如长轮询。

这是一个 web 请求被服务器打开的地方,以防它想要发送什么。当请求得到响应或超时时,它会重新建立。不用说,投票从来都不是很有效率。

从用户的角度来看,推送通知可以提高性能,因为它们一出现就收到更新。用户不需要刷新任何东西或继续检查。当一个长时间运行的进程启动时,您可以立即响应用户,异步运行它,并在完成后立即通知他们。

插座。IO 是 Node.js 的一个流行的 WebSocket 库,要看到它的实际应用,可以在使用它的网站上查看浏览器开发工具。例如,如果您打开开发工具并转到https://www.opentraintimes.com/maps/signalling/staines,您将看到连接从 HTTPS 升级到 WSS(或者如果您使用不安全的版本,则从 HTTP 升级到 WS)。

WebSockets 早于 HTTP/2,但是尽管有了新的服务器推送技术,它们仍然是相关的。这两个特性看起来相似,但它们的用途不同。网络套接字用于实时和双向数据传输,服务器推送目前只是为了预加载。

In addition to HTTP/2 server push preloading, there is a new browser feature that is currently supported in Android, Chrome, and Opera, which allows you to declare resource preloading in markup by using rel="preload" on a link tag. You can read the spec at https://w3c.github.io/preload/ and check the current state of browser support at http://caniuse.com/#feat=link-rel-preload .

在 Chrome 中,协议开关看起来像下面的截图。您看不到网络套接字连接的内容,因此您将无法从开发工具中查看正在传输的数据:

ASP.NET 有一个微软图书馆,叫做信号员。该库允许您使用网络套接字执行推送通知。如果客户端或服务器不支持长轮询,它也会退回到长轮询。要使用网络套接字,您需要一个相当新的版本的 Windows Server (2012 或更高版本)和 IIS (8.0 及更高版本)。

遗憾的是,最新的稳定版本(SignalR 2)并没有正式支持.NET Core 2.0。新版本正在编写中,将在 ASP.NET Core 2.0 之后发布。

You may also wish to look at StackExchange.NetGain as a WebSocket server.

压缩

数据压缩是一个宽泛的话题,我们不能指望覆盖所有内容。在这里,我们将学习无损压缩以及如何在 HTTP 中使用它。我们还将在本章后面介绍图片的有损图像压缩。压缩很重要,因为如果我们能使文件变小,我们就能缩短通过网络传输文件的时间。

无损压缩算法

您可能已经注意到之前一些截图中的 HTTP 头与编码有关。HTTP 最常见的压缩算法是 GZIP 和 DEFLATE ,非常相似。这些都与 ZIP 文件中使用的算法有关。如果您还没有使用 HTTP 压缩,那么这是一个快速的胜利,如果您启用它,它将提高您的网站的性能。

还有很多其他更高级的压缩算法,比如 xz ,类似于 7-Zip ( 7z )格式,使用莱姆佩尔-齐夫-马氏链算法****(LZMA/lzma 2)。然而,目前只有两种额外的算法在主要浏览器中被普遍使用。分别是 BrotliHTTP 共享字典压缩 ( SDCH )。两者都来自谷歌,但只有 Chrome 支持 SDCH,而且需要大字典。

Brotli 更有趣,因为现在大多数现代浏览器都支持它。许多浏览器需要使用 HTTPS 来支持 Brotli(使用 TLS 的另一个很好的理由),并且在标题中使用的编码标记是br。Brotli 有显著的性能提升,尤其是对于慢速连接的移动设备。你可以在https://caniuse.com/#search=brotli看到最新的支持数据。

如果您通过 HTTP 访问网站,您将在 Chrome dev tools 网络检查器中的请求详细信息中看到以下请求标题:

Accept-Encoding: gzip, deflate

但是,如果您使用 HTTPS,您将会看到:

Accept-Encoding: gzip, deflate, br

然后,服务器可以使用这个响应头用 Brotli 编码的内容进行响应:

Content-Encoding: br 

例如,如果您在受支持的浏览器中访问https://www.bayden.com/test/brotliimg.aspx,那么 Brotli 将传送内容(明星的图像)。这里是 Chrome 请求头的一个子集(为了简洁明了):

GET /test/brotliimg.aspx HTTP/1.1 
Host: www.bayden.com 
Connection: keep-alive 
Accept-Encoding: gzip, deflate, sdch, br 

这是相应响应头的子集:

HTTP/1.1 200 OK 
Content-Type: img/png 
Content-Encoding: br 
Server: Microsoft-IIS/7.5 
X-AspNet-Version: 4.0.30319 
YourAcceptEncoding: gzip, deflate, sdch, br 

Fiddler(我们之前提到的 Eric Lawrence 的令人敬畏的 HTTP 调试代理)也通过一个简单的插件支持 Brotli(将https://bayden.com/dl/Brotli.exe放到fiddler2\tools中并重启)。您可以使用它来轻松测试对您的站点的影响,而无需向您的 web 服务器部署任何东西。

You can try out Brotli compression in .NET with the System.IO.Compression.Brotli package. You could use this in combination with the ASP.NET Core response compression middleware (https://docs.microsoft.com/en-us/aspnet/core/performance/response-compression?tabs=aspnetcore2x ). Hopefully Brotli will be included in future versions of the core .NET Framework and then the response compression middleware can support it by default.

捆绑和缩小

当开发一个 web 应用时,您通常会得到许多需要交付给浏览器的静态文件。这些文件包括样式表、脚本和图像。处理许多小文件比处理一个大文件更容易(尤其是作为团队的一部分时),但这并不总是提高性能的最佳方法。

捆绑和缩小是加速这些静态资产交付的两种技术。它们通常用于文本文件,如 JavaScript 和 CSS 内容。但是,您也可以缩小图像,我们稍后会介绍这一点。

集束

捆绑是将多个文件组合或连接在一起的技术,这样它们就可以作为一个文件交付。当使用 HTTP/1.1 时,这是一个好主意,因为并发连接数是有限的。但是,捆绑对于 HTTP/2 来说就没那么必要了,事实上,它会降低性能。HTTP/2 中的新多路复用意味着,当您请求多个文件而不是一个包含所有相同内容的文件时,不会再有很大的损失。您可以利用这一点,只交付页面所需的内容,而不是每个页面的整个客户端代码库。即使你有选择地捆绑每页,这也可能是低效的。

例如,您可以包含一个用于表单的验证库。但是,因为这是捆绑的,它将被发送到所有页面,包括那些没有要验证的表单的页面。如果您有一个单独的已验证页面捆绑包,那么公共核心代码中可能会有重复,这些代码也会被发送。通过将事物分开,客户端可以单独缓存它们并重用组件。这也意味着,如果你改变了什么,你只需要使这一部分的缓存无效。客户端可以继续使用其他未修改的部分,而不必再次下载它们。

像往常一样,您应该为您的特定用例进行度量。您可能会发现捆绑仍然会减小总文件大小。HTTP/2 的开销要低得多,但仍然不是零,压缩可以更好地处理更大的文件。但是,请记住缓存和可重用性的含义。

缩小

缩小是减少文本静态资产的文件大小的过程。我们通过各种方法做到这一点,包括去掉注释、删除空白和缩短变量名。对代码进行模糊处理也是有用的,这样更难进行逆向工程。

当您使用 HTTP/2 时,缩小仍然很有用,但是在应用无损压缩后,当测试比较缩小前和缩小后的文件大小时,您应该小心。

如前所述,您应该使用 HTTP 内容压缩,至少使用 GZIP 或 DEFLATE 算法,最好使用 Brotli。这些算法在缩小文件方面非常有效。您可能会发现,当压缩时,您的缩小文件并不比压缩的原始源文件小多少。

ASP.NET Core 的变化

完整的.NET Framework 和以前版本的 MVC,平台内置了一个集成的捆绑和缩小系统,可以在运行时动态工作。对于 ASP.NET Core 来说,这种情况发生了变化,现在有了新的工具来执行这项工作。

现在的默认选项是BuildBundlerMinifier NuGet 包。但是,如果您愿意,您可以使用其他工具,包括任务管理器、大口和咕噜。还有包管理器,如 Bowernpm ,它们类似于 NuGet,但用于前端库。例如,NuGet 不再提供 jQuery 和 Twitter Bootstrap,默认情况下,它们使用 Bower。

这些工具大部分是用 JavaScript 编写的,运行在 Node.js 上,Node.js 的包管理器是 npm。这些工具在其他开源 web 框架中很受欢迎,并且已经很成熟。他们对这个场景并不陌生,只是对. NET 不熟悉

静态资产的缩小现在是在构建时完成的,而不是像以前那样在请求时完成。它更像是一个静态网站生成器,而不是一个动态 web 应用。项目根目录中的配置文件(bundleconfig.json)定义了 JSON 的绑定和缩小行为。

新工具不仅仅限于 ASP.NET Core,您还可以在 Visual Studio 中将这些功能用于传统的 ASP.NET 应用。这是一个很好的例子,说明了新框架可以为现有框架提供的交叉授粉和好处。

图像优化

数字媒体压缩比我们之前谈到的无损文件压缩复杂得多,即使我们只关注图像。上一章我们简单提到了什么时候用 PNG,什么时候用 JPEG。在这里,我们将深入探讨更多细节,并探索一些其他奇特的选择。

我们介绍了经验法则,即 PNG 是图标的最佳图像格式,JPEG 更适合照片。这两种格式分别是无损和有损图像压缩中最常见的。

我们将在后面更多地讨论其他图像格式,但是您通常会受到浏览器支持的流行格式的限制。那么,如何从常见的选择中获得更多呢?

巴布亚新几内亚

便携式网络图形 ( PNG )是一种无损图像压缩格式,内部工作方式类似于 ZIP 文件(使用 DEFLATE 算法)。对于包含纯色块的图像来说,这是一个很好的选择,并且它比旧的图形交换格式 ( GIF )具有更高的图像质量(具有更多的颜色)。

PNG 支持所有现代浏览器中的透明度,因此您应该使用它来代替静态图像的 GIF。这不是问题,除非你需要支持 Internet Explorer 6,在这种情况下,这可能是你最少的麻烦。PNG 也支持带有动画便携式网络图形 ( APNG )文件的动画。这些就像动画 gif,但质量要高得多。遗憾的是,只有火狐和 Safari 支持 APNGs。

A great site to look up the browsers that support a particular feature is http://caniuse.com/ . You can search for feature support and then check this against the user agent analytics of your site. For example, you could search for PNG-alpha, Brotli, or APNG.

一些 ZIP 算法实现比其他算法更好,它们产生的文件更小,仍然可以被每个人解码。例如,7-ZIP 比 Windows 上大多数其他 Zip 压缩软件要高效得多,即使使用 Zip 格式,而不是其原生的7z格式。同样,您可以更紧凑地压缩一个 PNG,而不会丢失任何数据,并且仍然可以在所有浏览器中工作。这通常会带来较高的前期计算成本。然而,如果你压缩静态资产,这很少改变,那么这是非常值得的。

您可能已经使用 PNGOUT 工具无损地缩小了 PNG 图像的大小。如果你不是,那你也许应该。你可以在http://advsys.net/ken/utils.htm了解更多,下载。

然而,有一种新的算法叫做 Zopfli,它提供了更好的压缩,但是压缩速度非常慢。解压缩同样快速,因此对于预编译资源来说,这只是一个单一的优化成本。Zopfli 是 Brotli 的前身,但它与 DEFLATE 和 GZIP 兼容,因为它不是新格式。

您可以从https://github.com/google/zopfli获得 Zopfli,但是您应该始终用您的图像进行测试,并验证文件大小确实有所减少。您可能会发现,这些工具可以帮助您更快地交付资产并实现更高的性能。

您也可以使用将许多精灵组合成一个图像的做法。与捆绑一样,当使用 HTTP/2 时,这是不太必要的。然而,同样的警告也适用于压缩,您应该始终测试您的图像集。

联合图像专家组

JPEG 是一种有损图像压缩格式,这意味着它通常会丢弃图片中的数据以使其变小。它最适合自然渐变和连续色调,例如照片中的色调。JPEG 不像 PNG 那样支持透明度,所以如果你想在不同的背景上使用,那么你需要预先渲染它们。

It's a good space-saving idea to remove the Exchangeable image file format (Exif ) metadata from your JPEG files for the web. This contains information about the camera used and geographic data of where the photo was taken.

JPEG 有一个质量设置,它会影响图像文件的大小和细节级别。质量越低,文件越小,但看起来越差。您可以对图像进行测试,看看哪些设置提供了可接受的折衷。至关重要的是,此质量设置的最佳值会因图像而异,具体取决于内容。有一些工具可以让你自动检测最佳质量水平,比如谷歌的 butteraugli。

火狐浏览器的制造者 Mozilla 有一个有趣的项目,叫做 mozjpeg 。这旨在更好地压缩 JPEG 图像,类似于 PNGOUT 和 Zopfli 对 PNG 图像所做的。您可以使用 mozjpeg 将您的 jpeg 图像压缩到比正常情况下更小的大小,而不会影响解压缩或质量。它可以在https://github.com/mozilla/mozjpeg找到,但是你需要自己编译。和往常一样,结果可能会有所不同,所以在你的网站上测试一下照片。

JPEG Archive(https://github.com/danielgtaylor/jpeg-archive)是一款便捷的工具,使用各种比较指标,使用 mozjpeg 压缩 JPEG 图像。另一个类似的工具是imgmin(http://github.com/rflynn/imgmin),稍老一些。

其他图像格式

许多其他的图像格式都是可用的,但是你在网络上通常会受到浏览器支持的限制。

如前一章所述,您不应该在浏览器中缩放图像,否则您将获得较差的性能。这通常意味着必须保存较小图像的多个单独副本,例如,在显示缩略图时。显然,这会导致重复,这是低效的。这些新的图像格式中有一些为响应性和可伸缩性图像问题提供了巧妙的解决方案。

BPG 是天才法布里斯·贝拉的一个图像格式,你可以在http://bellard.org/bpg阅读更多关于它的内容。它有一个支持浏览器的 JavaScript polyfill,在任何浏览器中添加本机支持之前。

WebP 是一种来自谷歌的图像格式,只有 Chrome、Android 和 Opera 支持。与 JPEG 相比,它节省了大量空间,如果它得到更广泛的支持,这将是一个不错的选择,所以查看 http://caniuse.com/的最新采用统计数据。

JPEG 2000 是 JPEG 的改进版本,尽管它可能受到软件专利的阻碍,因此它在医学成像之外还没有被广泛采用。只有 Safari 支持 JPEG 2000,还有 JPEG XR,只有 IE 才支持。

JPEG 使用的是离散余弦变换 ( 离散余弦变换,而 JPEG 2000 是基于小波变换的。它提供的属性之一是渐进式下载。这意味着图像的存储方式是,如果您从文件的开头下载一小部分,则完整图像的版本会更小,质量也会更低。这在响应性和可伸缩性图像方面有明显的应用。浏览器只需要下载足够的图像来填充它要渲染到的区域,并且文件只需要存储一次。缩略图不需要调整大小和复制。该技术也用于自由无损图像格式 ( FLIF )。

FLIF 是一个更令人兴奋的即将到来的图像格式,因为它是进步和响应,但免费的,没有专利。FLIF 还在开发中,但是如果浏览器支持它,它将非常有用。你可以在http://flif.info/了解更多。

JPEG and PNG can support progressive download, but this isn't normally useful for responsive images. Progressive JPEG subjectively loads more gracefully and can even make files smaller, but interlaced PNG usually makes files bigger.

问题是,这些渐进式图像格式中的大多数还没有为主流做好准备,因为所有主要的浏览器都不支持它们。关注未来是一个好主意,但目前,我们需要调整图像大小以获得高性能。

调整图像大小

在新的图像格式得到广泛采用之前,仍然需要调整大小,并且您可能需要针对不同的设备动态调整大小。也许,您也有用户提交的图像内容,尽管从安全角度来看,您需要非常小心。有些图像库不安全,特制的图像可能会利用您的系统。事实上,许多图像处理库在 web 环境中使用时都存在问题。

如果你不是非常勤奋和小心,那么你很容易以内存泄漏而告终,这可能会关闭你的网络服务器。将处理大型媒体文件的进程分开并用沙箱保护总是一个好主意。

从. NET 的角度来看,使用 WinForms System.Drawing或其 WPF 继任者(System.Windows.Media)可能很有诱惑力。但是,这些是为桌面软件设计的,微软强烈建议不要在服务或网络应用中使用它们。微软推荐视窗映像组件 ( WIC ),但这是一个组件对象模型 ( COM )应用编程接口,用于 C 或 C++应用。除此之外,这些成像库都不是跨平台的,因此不适合在中使用.NET Core。

如果你用的是 Windows,那么你可以试试 Imazen(http://www.imazen.io/)从http://imageresizing.net/用 ImageResizer。虽然它仍然使用 GDI+ System.Drawing,但它相当久经沙场,所以大部分 bug 应该都已经解决了。还有dynamic mage,它包装了较新的 WPF 图像函数并使用了着色器。可以在http://dynamicimage.apphb.com/了解更多,虽然有一段时间没有更新,不支持.NET Core。

开源圈子里比较流行的一个选项是我们之前提到过的 ImageMagick,还有一个叫做 GraphicsMagick 的分叉,号称效率更高。另一个流行的图像库是 LibGD ,它适合服务器使用。你可以在http://libgd.github.io/阅读更多。虽然是用 C 写的,但是也有其他编程语言的包装器,比如 DotnetGD targeting.NET Core。

其中一个特点是.NET Core 缺少的是还没有一个令人信服的图像处理选项。完成后,ImageResizer 5 和 Imageflow(https://www.imageflow.io/)可能会对此有所帮助。开源的 ImageProcessor 库(http://imageprocessor.org/)还有一个新的跨平台版本,叫做 ImageSharp(http://imagesharp.net/,不过这还是一个正在进行的工作。如果您想尝试一下,那么您可以从 MyGet 获得预发布包,或者从源代码构建它。

Platform support and compatibility changes rapidly, so check https://github.com/jpsingleton/ANCLAFS for the latest information. Feel free to contribute to this list or to the projects.

就目前而言,安装一个开源服务可能更容易,比如 Thumbor,或者使用一个基于云的成像服务,比如我们已经提到的 ImageEngine(http://wurfl.io/)或者 Cloudinary。图像处理是一项常见的任务,也是一个有效解决的问题。最好使用现有的解决方案,而不是重新发明轮子,除非它是你核心业务的一部分,或者你有非常不寻常的需求。

Once you have your resized images, you can load them responsively with the picture and source tags using the srcset and sizes attributes. You can also use this technique to provide newer image formats (such as WebP), with a fallback for browsers that don't yet support them. Or you can use Client Hints (refer to http://httpwg.org/http-extensions/client-hints.html and http://caniuse.com/#feat=client-hints-dpr-width-viewport ).

贮藏

人们常说(最初是菲尔·卡尔顿说的),缓存是计算机科学中最难的问题之一,命名也是如此。这可能有点夸张,但是缓存确实很难。如果你的方法不系统和不精确,调试也会非常令人沮丧。

使用多种不同的技术,缓存可以应用于从浏览器到服务器的不同级别。即使您没有意识到,也很少使用单个缓存。多个缓存并不总是能很好地协同工作,如果你不能清除一个,那就很麻烦了。

我们在前一章中简单地讨论了缓存,我们将在第 9 章学习缓存和消息队列中更详细地讨论。然而,由于缓存对网络性能有影响,我们也将在这里讨论它。

浏览器

很多缓存都发生在 web 浏览器中,这很不方便,因为你无法直接控制它(除非是你的浏览器)。要求用户清除缓存对许多人来说是不令人满意和困惑的。然而,通过仔细控制您设置的 HTTP 头和您使用的 URL,您可以对浏览器缓存资源的方式施加影响。

如果你不能声明哪些资源是可缓存的,以及可以缓存多长时间,那么很多浏览器只会猜测这一点。这方面的启发在不同的实现之间可能有很大的不同。因此,这将导致次优性能。您应该是显式的,并且总是通过将资产标记为不可缓存来声明缓存信息,即使是(尤其是)不应该缓存的资产。

You need to be vigilant with what you advertise as cacheable because if you are careless, then you can get yourself into a situation where you're unable to update a resource. You should have a cache-busting strategy in place, and tested, before using caching.

浏览器中有各种各样的缓存技术。可以设置很多不同的 HTTP 头,比如AgeCache-ControlETag(实体标签)、ExpiresLast-Modified。这些来自几个不同的标准,交互可能很复杂,或者在不同的浏览器之间有所不同。我们将在第 9 章学习缓存和消息队列中更详细地解释这些。

另一种技术是为内容使用唯一的网址。如果一个网址改变了,那么浏览器会把它当作一个不同的资源,但是如果它是相同的,那么它可能会从它的本地缓存加载它。一些框架计算文件内容的散列,然后将其用作查询字符串参数。这样,当文件内容改变时,网址也随之改变。

还有其他更现代的功能可以用来缓存,比如 HTML5 应用缓存(或者 AppCache )。这是为离线 web 应用设计的,不是很灵活。委婉地说,破坏缓存很复杂。AppCache 已经被弃用,您应该使用服务人员来代替。这些提供了更多的灵活性,尽管支持是最近才出现的。

有许多改进即将到来,在最新的浏览器中,给你更多的控制,我们也将在第 9 章学习缓存和消息队列中向你展示如何使用它们。

计算机网络服务器

网络服务器是缓存的好地方,因为它通常在你的完全控制之下。然而,除了生成正确的报头之外,它实际上不是网络性能的一部分。就提高生成页面的速度而言,服务器端缓存还可以带来其他巨大的性能优势,但我们将在后面的章节中介绍这些优势。

如果你用传统的.NET 框架在微软的互联网信息服务 ( IIS )网络服务器上,那么你可以在你的应用中使用输出缓存。这将负责设置正确的标题并向浏览器请求发送304 (Not Modified)响应。它还会将服务器上的输出缓存在内存、磁盘或使用 Memcached/Redis。您可以向控制器操作方法添加属性来控制缓存选项,但是也可以使用其他方法,例如在配置文件中。

输出缓存现在在 ASP.NET Core 中可用,因此如果您愿意,您可以在基础架构中存储输出的副本。由于本节是关于网络的,我们在这里只讨论ResponseCache属性,它是输出缓存的一个子集。访问作者在https://unop.uk/的网站,了解更多关于其他主题的信息。

如果要禁用 ASP.NET Core 页面上的缓存,请将此注释添加到您的控制器操作中:

[ResponseCache(NoStore = true, Duration = 0)] 

这将在 HTTP 响应上设置以下标头,并确保不缓存该标头:

Cache-Control: no-store 

要缓存一个小时的页面,请添加以下内容,Duration以秒为单位:

[ResponseCache(Duration = 3600, VaryByHeader = "Accept")] 

缓存控制头将如下所示:

Cache-Control: public,max-age=3600 

关于其他缓存配置选项和配置文件,还有很多要说的。因此,如果你感兴趣,那就阅读后面的章节。这是一个复杂的话题,我们这里只触及了表面。

You can read the documentation about response caching in ASP.NET Core at https://docs.microsoft.com/en-gb/aspnet/core/performance/caching/response .

这些缓存指令不仅指示浏览器,还指示途中的任何代理。如果您有缓存代理,其中一些可能在您的基础架构中,例如 Squid、Varnish 或 HAProxy。或者,您可能有一个终止于 TLS 的负载平衡器(如 Azure 应用网关)来减少您的网络服务器上的负载,该服务器也缓存。您可以强制刷新您控制的服务器的缓存,但是在您和您的用户之间可能还有其他缓存,您不能这样做。

您和您的用户之间的代理服务器

您和您的用户之间可能有许多您无法直接控制的代理服务器。他们可能会忽略您的缓存请求,阻止您网站的部分内容,甚至修改您的内容。解决这些问题的方法是使用 TLS,我们已经讨论过了。TLS 创建了一个安全的隧道,这样您的基础设施和浏览器之间的连接就不会被轻易篡改。

企业代理,通常是“中间人” ( 【米特姆】),攻击您与用户的连接,以便他们可以监视员工在网上做什么。这包括在用户的工作站上安装一个自定义的受信任根证书,以便可以伪造您的证书。不幸的是,除了教育用户,你对此无能为力。证书锁定在原生应用中很有效,但在网络应用中就没那么有用了。 HTTP 公钥锁定 ( HPKP )是可用的,但是由于这是一种首次使用信任 ( 豆腐)技术,初始连接可能会被拦截。客户端证书是另一种选择,但是它们很难分发,并且不常用。

如果你使用 HSTS 并在浏览器中预装了你的域名,那么这有助于避免拦截,但是你的网站可能会被屏蔽。例如,火狐自带证书存储,不像 Chrome 那样使用操作系统提供的证书存储。您不能为启用了 HSTS 的站点添加安全例外,火狐在连接之前知道这一点,对于包含在浏览器安装中的 HSTS 站点也是如此。

如果您信任第三方并保持控制,MitM 可能会很有用。一些内容交付网络(cdn)使用这个来加速你的站点。但是,您需要仔细评估这种情况的风险影响。阅读上一章提到的 Cloudbleed 事件,这是一个尖锐的例子。

cdn

cdn 可以通过将内容的副本存储在离用户更近的位置来提高网站的性能。诸如 Cloudflare 提供的服务,在您的连接上执行 MitM,并在世界各地的数据中心保存副本。与未经授权的代理的区别在于,您可以控制配置,并且可以随时清除缓存。

您应该小心,因为如果您不使用缓存功能,那么由于涉及额外的跳转,这可能会降低您的站点的响应能力。确保您监控有无 CDN 的响应时间,并且您需要一个后备计划以防响应时间减少。

cdn 的另一个常见用例是分发流行的库,例如 jQuery JavaScript 库。有来自 jQuery (MaxCDN)、谷歌、微软和 CDNjs (Cloudflare)的免费 cdn 可以做到这一点。假设用户缓存中可能已经有这些库中的一个库。但是,您应该非常小心地信任提供者和连接。当您将第三方脚本加载到您的站点中时,您实际上是在让他们完全控制它,或者至少依赖他们始终可用。

如果您选择使用 CDN,那么请确保它使用 HTTPS 来避免篡改脚本。你应该在你的安全页面上使用明确的https://网址,或者至少是协议不可知的网址(//),永远不要使用http://。否则,您将收到混合内容警告,有些浏览器显示为完全未加密,甚至被阻止。

SRI 可以帮助避免脚本在某些浏览器中被篡改。不要担心散列脚本的任何潜在性能开销。使用的 SHA-2 算法非常快,这就是为什么它们不应该用于散列密码或密钥扩展。有关此脚本完整性检查功能的更多信息,请参见上一章。

无论如何,您都需要在自己的服务器上托管一个后备,以防 CDN 关闭。如果您使用 HTTP/2,那么您可能会发现使用 CDN 没有任何优势。显然,永远要根据你的情况进行测试。

ASP.NET Core 视图中有一些有用的功能,可以轻松地为 CDN 资源启用本地回退。我们将在后面的章节中向您展示如何使用它们。

摘要

在本章中,您学习了如何提高基础架构边缘和用户之间的网络性能。您现在更了解您的应用下的互联网协议,以及如何优化它们的使用以获得最佳效果。

我们在较高的层次上覆盖了网络堆栈的许多层,从 IP 和 TCP 到 HTTP 和 HTTP/2。我们还展示了如何使用 TLS 来保护通信,以及密钥交换是如何工作的。我们强调了可以使用的不同密码算法,并涉及到替代协议,如网络套接字。

您学习了如何利用压缩来缩小文本和图像文件。这将减少带宽并加快资产交付。我们还强调了缓存,现在您应该知道它有多重要了。我们将在第 9 章学习缓存和消息队列中详细介绍缓存。

在下一章中,您将学习如何优化基础架构内部的性能。您将看到如何处理输入/输出延迟,以及如何编写高性能的 SQL。

七、优化输入/输出性能

本章讨论了当您将经过功能测试的应用拆分成几部分进行部署时经常出现的问题。您的网络服务器托管前端代码,您的数据库在数据中心的其他地方,您可能有一个用于集中文件的存储区域网络 ( 存储区域网络),一个用于应用接口的应用服务器,并且虚拟磁盘也在不同的机器上。

这些变化给许多常见操作增加了显著的延迟,您的应用现在变得超级慢,可能是因为它在网络上太健谈了。在本章中,您将学习如何通过将查询批处理在一起并在最适合该作业的服务器上执行工作来解决这些问题。即使一切都在一台机器上运行,您将在这里学习的技能也将有助于通过提高效率来提高性能。

本章涵盖的主题包括以下内容:

  • 输入/输出
  • 网络诊断
  • 批处理应用编程接口请求
  • 高效的数据库操作
  • 模拟和测试

您将了解不应该同步使用的各种操作,以及如何以有效的方式从数据库中只返回您需要的数据。您还将看到如何驯服您的 O/RM,并学习使用 Dapper 编写高性能 SQL。

我们在第 5 章修复常见性能问题中简要介绍了其中一些主题,但在这里我们将深入探讨更多细节。本章的前半部分将着重于背景知识和诊断工具的使用,而后半部分将向您展示您可能遇到的问题的解决方案。您还将了解一些更不寻常的问题,以及如何修复或缓解它们。

我们将首先关注于理解问题,因为如果你不理解问题的根本原因,那么它可能很难修复。你不应该盲目地应用你读到的建议,并期望它成功地发挥作用。诊断一个问题通常是困难的部分,一旦做到这一点,通常很容易解决它。

输入/输出

I/O 是代码与外部世界交互的任何操作的总称。有很多东西可以算作输入/输出,并且在您的软件内部可以有大量的输入/输出,尤其是如果您的应用具有分布式体系结构。

科技公司越来越多地使用.io 【顶级域名】(【TLD】)例如 http://github.io ,可以部分归因于它代表 I/O,但这不是它的真正含义。和其他顶级域名一样,它实际上是一个国家代码。其他例子包括利比亚的.ly和图瓦卢的.tv(像邻国基里巴斯一样,由于气候变化,可能很快会被淹没在太平洋下面)。TLD .io是为英属印度洋领地 ( BIOT )设计的,这是一系列有着可耻历史的微小但具有战略意义的岛屿。因此,尽管 BIOT 只不过是美国的一个军事基地,但 TLD 的 T4 却由一个总部设在英国的注册处控制。

在这一章中,我们将着重于提高输入/输出的速度,而不是避免它。因此,我们在这里不讨论缓存。输入/输出优化和缓存本身都是强大的技术,当它们结合在一起时,您可以获得令人印象深刻的性能。有关缓存的更多信息,请参见第 9 章学习缓存和消息队列

输入输出的类别

第一个挑战是识别触发输入/输出的操作.NET 是这样的,如果一个方法有一个异步 API ( MethodAsync()变体),那么它就是在声明它可能很慢,并且可能在做 I/O 工作。让我们仔细看看不同类型的输入/输出

磁盘

我们将讨论的第一种类型的输入/输出是从持久存储读取和向持久存储写入。这通常是某种磁盘驱动器,例如旋转盘片硬盘驱动器 ( 硬盘驱动器),或者,正如现在更常见的,基于闪存的固态驱动器 ( 固态硬盘)。

硬盘在随机读写方面比固态硬盘慢,但在大数据块传输方面具有竞争力。其原因是,硬盘驱动器上的臂必须将磁头物理移动到磁盘上的正确位置,然后才能开始读或写操作。如果磁盘断电,则可能需要更长时间,因为盘片必须从静止位置旋转到正确的每分钟转数 ( rpm )。

You may have heard the term spin up in reference to provisioning a generic resource. This historically comes from the time taken to spin the platters on a rotating disk up to the operational speed. The term is still commonly used, even though these days there may not be any mechanical components present. Terminology like this often has a historical explanation. As another example, a floppy disk icon is normally used to represent the save function. Yet floppy disks are no longer in use, and many younger users may never have encountered one.

了解代码运行在什么类型的驱动器上很重要。如果进行大量的小型读写,硬盘的性能会很差。他们更喜欢批量操作,所以写一个大文件比写很多小文件要好。

磁盘的性能类似于网络的性能,因为它既有延迟又有吞吐量,用网络术语来说,通常称为带宽。硬盘的延迟很高,因为启动需要相对较长的时间,但一旦启动,吞吐量可能相当可观。如果数据都在磁盘上的一个位置,您可以快速读取数据,但是如果数据分散在所有位置,即使数据总量较少,读取速度也会较慢。例如,将单个大文件从磁盘复制到磁盘很快,但尝试同时启动多个程序很慢。

固态硬盘遇到的这些问题较少,因为它们的延迟更低,但将随机写入保持在最低水平仍然是有益的。固态硬盘基于闪存(类似于手机和相机存储卡中使用的芯片),只能写入固定次数。固态硬盘上的控制器可以为您管理这些,但固态硬盘的性能会随着时间的推移而下降。激进的写作会加速这种退化。

可以组合多个磁盘来提高它们的性能和可靠性。这通常使用一种称为独立磁盘冗余阵列 ( RAID )的技术来实现。数据被分割到多个磁盘上,以便更快地访问数据,并且更能容忍硬件故障。RAID 在服务器硬件中很常见,但会增加启动时间,因为加速旋转有时会错开,以减少峰值功耗。

硬盘的容量比固态硬盘大得多,因此是存储不常用文件的好选择。您可以获得混合驱动器,将硬盘和固态硬盘结合在一起。

这些产品号称两全其美,而且比同等尺寸的固态硬盘便宜。然而,如果你能负担得起,如果你能在固态硬盘上存储所有数据,那么你应该使用一个。您还可以降低电力和冷却需求,并且可以随时添加额外的硬盘用于大容量存储或备份。

虚拟文件系统

如前所述,由于存储数据的磁盘的物理特性,即使在最好的情况下,文件访问也会很慢。在云托管基础架构等虚拟化环境中,这个问题会变得更加复杂。存储磁盘通常与虚拟机不在同一个主机服务器上,并且通常作为网络共享来实现,即使它们看起来是本地装载的。无论如何,总会有一个额外的问题,无论磁盘是在虚拟机主机上还是在网络上的其他位置,这就是争用。

在虚拟化基础架构上,例如 AWS 和 Azure 提供的基础架构,您可以与其他用户共享硬件,但是物理磁盘一次只能服务一个请求。如果多个租户想同时访问同一个磁盘,那么他们的操作需要排队和分时进行。不幸的是,这种抽象对性能的不利影响与读取大量随机文件一样。用户可能会将其数据存储在与其他客户不同的磁盘位置。这将导致驱动器上的 arm 频繁地移动到不同的扇区,从而降低系统上每个人的吞吐量并增加延迟。

所有这些都意味着,在共享虚拟主机上,使用固态硬盘会比正常情况下产生更大的积极性能影响。更好的是有一个本地固态硬盘,它直接连接到虚拟机主机,而不是网络上的另一台机器。如果磁盘必须联网,则存储机器应尽可能靠近使用它的虚拟机。

如果您是唯一的租户,您可以为专用虚拟机主机额外付费。但是,您也可以在裸机上运行,并获得降低成本和提高性能的好处。如果您不需要轻松配置和维护虚拟机,那么裸机专用服务器可能是一个不错的选择。

许多云托管提供商现在提供固态硬盘,但大多数只提供短暂的本地磁盘。这意味着本地磁盘只在虚拟机运行时存在,而在关闭时消失,如果您想让虚拟机恢复到相同的状态,则不适合存储操作系统。

您必须以不同的方式编写应用,以利用短暂的本地驱动器,因为它随时可能消失,因此只能用于临时工作存储。这被称为不可变服务器,这意味着它不会改变,并且是一次性的。当操作系统是 Linux 时,这通常会更好地工作,因为在运行 Windows 时引导新实例可能会很棘手。

数据库

数据库可能很慢,因为它们依赖磁盘来存储数据,但也有其他开销。然而,与磁盘上的平面文件相比,数据库通常是存储重要数据的更好方式。如果对任意数据进行索引,则可以快速检索这些数据,这比强力扫描文件要快得多。

关系数据库是一项成熟且令人印象深刻的技术。然而,它们只有在正确使用时才会发光,并且如何查询它们对性能有很大影响。数据库是如此方便,以至于它们经常被过度使用,并且通常是 web 应用的瓶颈。

一个不幸的常见的反模式是需要一个数据库调用来渲染一个网站的主页。一个例子是当你试图访问电视直播中提到的一个网站时,却发现它已经因 MySQL 数据库过载而崩溃。这种网站最好设计成静态网站,客户端代码访问缓存和排队的 web APIs。

慢速数据库的病态情况是,网络服务器位于一个数据中心,数据库服务器位于另一个数据中心,数据库的磁盘位于第三个数据中心。此外,所有服务器都可以与其他用户共享。显然,最好不要在这种情况下结束,并以明智的方式构建您的基础架构,但您总是会有一些延迟。

有一些应用编程技术可以让您将网络和数据库的振动降到最低。这些有助于提高软件的性能和响应能力,尤其是在高延迟虚拟化环境中。我们将在本章后面演示其中的一些技巧。

蜜蜂

现代 web 应用编程通常涉及使用第三方服务及其相关的 API。了解这些 API 的位置和延迟是有益的。他们是在同一个数据中心,还是在地球的另一端?除非你发现了一些令人兴奋的新物理,否则光只会传播得这么快。

Today, almost all intercontinental data travels by fiber optic cables. Satellites are rarely used anymore, as the latency is high, especially for geostationary orbits. Many of these cables are under the oceans, and are hard to fix. If you rely on an API on a different continent, not only can it slow you down, but it also exposes you to additional risk. You probably shouldn't build an important workflow that can be disrupted by a fisherman trawling in the wrong place. You also need to further secure your data, as some countries (such as the UK) are known to tap cables and store the communications, if they cross their borders.

API 的问题之一是延迟会加剧。您可能需要调用许多应用编程接口,或者一个应用编程接口在内部调用另一个应用编程接口。这些情况通常不是以这种方式设计的,但是随着新特性的添加,它们会有机地增长,尤其是如果没有定期执行重构来清理任何混乱的话。

延迟的一种常见形式是启动时间。如果不使用网站,尤其是使用默认的互联网信息服务 ( IIS )设置,网站可以进入睡眠状态。如果一个网站需要花费不可忽略的时间来唤醒,并且所有需要的 API 也需要唤醒,那么对于第一个请求来说,延迟会很快增加到一个显著的延迟。它甚至可能超时。

这个初始滞后问题有几种解决方案。如果使用 IIS,则可以将应用池配置为不进入睡眠状态。IIS 中的默认设置是为共享托管而优化的,因此需要对专用服务器进行调整。第二种选择是通过运行状况检查或正常运行时间监控工具定期轮询来保持站点的活动状态。无论如何,您应该这样做,以便知道您的站点何时关闭,但是您也应该确保您正在使用所有必需的依赖项(例如 API 和 DBs)。如果您只是简单地检索一个静态页面或者只是检查一个 200 状态代码,那么服务可能会在您没有意识到的情况下停止。

同样,缩放也会有滞后。如果你需要扩大规模,那么你应该预热你的负载平衡器和网络服务器。如果使用 AWS 弹性负载平衡 ( ELB ),这一点尤其重要。如果你正在期待一个大的流量高峰,那么你可以要求自动气象站让你的 ELBs 预热。另一种方法是使用 Azure 负载平衡器Azure 应用网关,或者自己运行 HAProxy,这样你就有更多的控制权。您还应该运行负载测试,我们将在第 11 章监控性能回归中介绍。

网络诊断工具

正如我们之前发现的,虚拟化或云托管基础架构中的几乎所有 I/O 操作现在都是网络操作。磁盘和数据库很少是本地的,因为这将阻止横向扩展。有各种命令行工具可以帮助您发现应用编程接口、数据库或您正在使用的任何其他服务器的位置,以及连接上存在多少延迟。

虽然所有这些命令都可以从您的工作站运行,但是当通过安全外壳 ( SSH )或远程桌面协议 ( RDP )连接从服务器运行时,它们是最有用的。这样,您可以检查数据库、应用编程接口和存储服务器相对于 web 服务器的位置。不幸的是,主机提供商通常会将您的服务器在地理上分开,并将它们放在不同的数据中心。

例如,如果使用 AWS,那么您可能希望将您的服务器配置为至少位于相同的区域,并且最好位于相同的可用性区域 ( AZ ),这通常意味着相同的数据中心。您可以跨 AZs(甚至跨区域)复制(集群化)您的数据库或文件服务器,以便您的 web 服务器始终与其本地网络上的服务器进行通信。这也增加了冗余,因此除了提高性能之外,它还将使您的应用对硬件故障或电源故障更具弹性。

Ping 是一个简单的网络诊断工具,几乎适用于所有操作系统。它在 IP 级别运行,并向指定的主机发送互联网控制消息协议 ( ICMP )回应消息。

不是所有的机器都会响应 pings,或者请求可能会被防火墙阻止。然而,允许服务器出于调试目的做出响应是一种很好的网络礼仪,大多数服务器都会遵守。例如,打开命令提示符或终端,并键入以下内容:

 ping ec2.eu-central-1.amazonaws.com 

这将平安亚马逊网络服务 ( AWS )在德国的数据中心。在响应中,您将看到以毫秒为单位的时间。从英国来说,这个往返时间 ( RTT )可能有点像33ms,但你们的结果会有所不同。

On Windows, by default, ping performs four attempts, then exits. On a Unix-like OS (such as macOS, BSD, or Linux), by default it continues indefinitely. Press Ctrl + C to stop and quit.

接下来尝试以下命令,除了澳大利亚的一个 AWS 数据中心之外,它将执行相同的操作:

 ping ec2.ap-southeast-2.amazonaws.com 

从英国开始,延迟现在几乎增加了一个数量级,达到大约 300 毫秒。要 ping 通英国托管提供商,请输入以下内容:

    ping bytemark.co.uk

延迟现在降低到平均水平23ms,因为我们的连接(可能)没有离开这个国家。显然,你的结果会因你所在的位置而异。接下来,我们将了解如何发现我们的数据所走的路线,因为重要的不总是距离。跳跃的次数同样可以很大。

下面的截图显示了我们刚刚对德国、澳大利亚和英国执行的三个ping操作的输出。注意计时的不同;然而,你的结果会有所不同,所以你自己试试吧。

IPv4 addresses starting with 54 (the ones in the form 54.x.x.x ) are a clue that the server may be running on an AWS Elastic Compute Cloud (EC2 ) virtual server. Perform a reverse DNS lookup with nslookup or ping (covered later in this chapter) to confirm if this is the case. AWS provides IP address ranges at the following link:

http://docs.aws.amazon.com/general/latest/gr/aws-ip-ranges.html

Tracert

Tracert(或类似 Unix 的操作系统上的 traceroute)是一种工具,顾名思义,它跟踪到目的主机的路由。输入以下命令:

    tracert www.google.com

你应该可以看到连接离开你的互联网服务提供商 ( ISP )的网络,进入域1e100.net,这是谷歌的域。 1.0 x 10 100 是一个 googol ,和他们同名。下面的屏幕截图显示了您可能看到的该跟踪的输出:

接下来,让我们通过运行下面的命令来跟踪到澳大利亚的路由,该命令与前面的示例具有相同的 AWS 主机名,如下所示:

    tracert ec2.ap-southeast-2.amazonaws.com

这可能需要一些时间来运行,特别是如果一些主机没有响应 pings,traceroute 必须超时。如果您看到星号(* * *),那么这可能表示存在防火墙。

您的结果可能如下图所示:

在上例中,我们可以看到连接离开英国电信 ( BT )网络,进入日本电报局和电信 ( NTT )全球 IP 网络。我们甚至可以看到从伦敦到悉尼,途经阿姆斯特丹、阿什伯恩(美国东部,弗吉尼亚州)和洛杉矶的路线。这些主机名表明,连接已经通过伦敦圣保罗大教堂附近的法拉第电话交换大楼(以电气先驱迈克尔·法拉第的名字命名),并进入了亚马逊在洛杉矶的网络。

This isn't the whole story as it only shows the IP level. At the physical level, the fiber likely comes back to the UK from the Netherlands (possibly via Porthcurno, Goonhilly Satellite Earth Station, or more likely Bude, where GCHQ conveniently has a base). Between LA and Australia, there will also probably be a stopover in Hawaii (where the NSA base that Edward Snowden worked at is located). There are maps of the connections available at http://submarinecablemap.com/ and http://www.us.ntt.net/about/network-map.cfm . It's good idea to have at least a basic understanding of how the internet is physically structured, in order to achieve high performance.

如果我们现在追踪到韩国和日本的 AWS 数据中心的路线,我们可以看到,最初,它们都采用相同的路线。他们从伦敦到纽约,然后到西雅图,最后到达日本大阪。韩国的追踪接着又进行了 11 次跳跃,但是日本的追踪是在 6 次跳跃中完成的,这是合乎逻辑的。

下面的截图显示了首先追踪到韩国,然后追踪到日本的典型结果:

您可以利用跳跃之间的时间差来计算出大致的地理距离。但是,如果某些系统比其他系统响应更快,偶尔会出现异常。

If you're on a flight with free Wi-Fi, then a traceroute is an interesting exercise to perform. The internet connection is likely going via satellite, and you'll be able to tell the orbit altitude from the latency. For example, a geostationary orbit will have a large latency of around 1,000 ms, but a Low Earth Orbit (LEO ) will be much smaller. You should also be able to work out where the ground station is located.

网络管理命令行工具

Nslookup 是一个直接查询 DNS 服务器的工具。是另一个类似的工具,这里就不赘述了。ping和 traceroute 都执行了 DNS 查找,但是你可以直接用nslookup来做,这可能会非常有用。可以用命令行参数调用nslookup,但也可以交互使用。为此,只需在控制台或命令提示符下键入工具的名称,如下所示:

    nslookup

现在,您将从程序中获得略有不同的命令提示符。默认情况下,使用您所在计算机的域名服务器,这将绕过本地主机文件中的任何条目。

A hosts file can be very useful for testing changes prior to adding them to DNS. It can also be used as a crude blocker for adverts and trackers by setting their addresses to 0.0.0.0 . There are local DNS servers you can run to do this for your whole network. One such project is http://pi-hole.net/ , which is based on dnsmasq simplifies setting it up and updating the hosts on a Raspberry Pi .

输入服务器的主机名以解析其 IP 地址;例如,键入以下内容:

    polling.bbc.co.uk

结果显示,该主机名是polling.bbc.co.uk.edgekey.net的 CNAME(真实规范名的别名),解析为e3891.g.akamaiedge.net,目前该主机名的 IP 地址为23.67.87.132。我们可以通过输入以下内容对该 IP 地址执行反向 DNS 查找:

    23.67.87.132

然后我们得到那台机器的主机名,方便地包含 IP 地址,也就是a23-67-87-132.deploy.static.akamaitechnologies.com。该域名归 Akamai 所有,Akamai 是一个用来分发负载的内容分发网络 ( CDN )。

如果您在本地网络上使用 DNS 服务器,可能路由器运行的是 dnsmasq,那么它可能会缓存结果,并给你陈旧的数据。通过将服务器更改为更快传播更改的核心服务器,您可以看到更多最新信息。例如,要使用谷歌的公共域名系统服务器之一,请输入以下内容(但请注意,如果您正常使用,谷歌将记录您访问的所有互联网网站):

    server 8.8.8.8

然后再次运行相同的查询。请注意主机名现在如何解析为不同的 IP 地址。这是 cdn 的常见行为,记录会随着时间的推移而改变,即使在同一个 DNs 服务器上也是如此。它经常被用作一种平衡网络负载的技术。改变域名系统服务器有时也可以用来绕过幼稚的内容过滤器或位置限制。

要退出nslookup交互模式,输入exit并按返回。下面的屏幕截图显示了前面命令的输出:

IPv4 is the version of IP that you will probably be most familiar with. It uses 32-bit addresses that are usually represented as four dotted decimal octets, such as 192.168.0.1 . However, we have run out of IPv4 addresses, and the world is (slowly) moving to the new version of IP, called IPv6 . The address length has increased fourfold, to 128-bit, and is usually represented in hexadecimal such as 2001:0db8:0000:0000:0000:ee00:0032:154d . Leading zeros can be omitted, such as 2001:db8::ee00:32:154d , to make them easier to write. Localhost loopback (127.0.0.1 ) is now simply ::1 in IPv6.

在 Windows 上,可以使用ping -a x.x.x.x进行反向 DNS 查找,将 IP 地址解析为主机名。在 Linux(以及其他类似 Unix 的系统,如 OS X)上,该功能不可用,并且-a标志有不同的用途。您必须在这些操作系统上使用nslookupdig进行反向域名系统。

打造自己的

您可以使用 C#构建自己的工具(例如控制台应用),并且您需要的功能由. NET 提供。底层实现是特定于平台的,但是框架将负责在您使用的操作系统上调用本机代码。

您通常不需要以编程方式解析主机名,因为大多数网络命令都会自动解析。但是,它偶尔会很有用,尤其是当您想要执行反向 DNS 查找并找到 IP 地址的主机名时。

The .NET Dns class differs from nslookup , as it includes entries from the local hosts file rather than just querying a DNS server. It is, therefore, more representative of the IP addresses that other processes are resolving.

要以编程方式执行 DNS 查找,请执行以下步骤:

  1. 添加System.Net命名空间。
        using System.Net; 
  1. 要将主机名解析为 IP 地址,请使用以下静态方法。这将返回一组 IP 地址,尽管通常只有一个。
        var results = await Dns.GetHostAddressesAsync(host);
  1. 要将 IP 地址解析为主机名,请改用以下方法:
        var revDns = await Dns.GetHostEntryAsync(result); 
  1. 如果成功,则主机名将作为revDns.HostName可用。

要以编程方式 ping 主机,请执行以下步骤:

  1. 添加System.Net.NetworkInformation命名空间。
        using System.Net.NetworkInformation; 
  1. 然后,您可以实例化一个新的Ping对象。
        var ping = new Ping(); 
  1. 使用此对象,您现在可以使用以下方法 ping 一个 IP 地址或主机名(将在内部执行 DNS 查找):
        var result = await ping.SendPingAsync(host); 
  1. 您可以通过result.Status获得 ping 的状态,如果成功,则可以通过result.RoundtripTime获得毫秒级的 RTT。

The source code for our console application, illustrating how to use the .NET Core Dns and Ping classes, is available for download along with this book.

反向域名系统查找通常可以揭示网站使用的托管公司。通常,每个主机名只有一个 IP 地址,如下图所示.NET Core 控制台应用输出:

在前面的截图中,我们可以看到https://emoncms.org/正在使用http://redstation.com/作为主机。低延迟表明服务器与我们的计算机位于同一个国家。

DNS 通常用于负载平衡。在这种情况下,您将看到为单个域名返回了许多 IP 地址,如下图所示:

我们可以在前面的截图中看到,专注于隐私的搜索引擎duckgo(它不像谷歌那样跟踪用户)正在使用 AWS。DNS 被用来平衡各种实例的负载;在这种情况下,它们都在都柏林数据中心,因为那是最近的一个。请注意,在前面的示例中,ping 时间现在比英国主机略高。

很可能他们使用的是 AWS DNS 服务路由 53 (之所以这样命名是因为 DNS 使用端口 53)。这可以平衡跨区域的负载,而 DuckDuckGo 似乎没有使用)只能在一个区域内平衡(但是在 AZs 内部和跨 AZs)。Azure 为 DNS 负载平衡提供了一个类似的服务,叫做流量管理器

解决方法

现在,您对基于延迟的问题的原因以及如何分析它们有了更多的了解,我们可以演示一些潜在的解决方案。您使用前面说明的工具进行的测量将帮助您量化问题的规模,并选择要应用的适当修复。

批处理应用编程接口请求

呈现一个典型的网页可能需要调用许多不同的应用编程接口(或数据库表)来收集所需的数据。由于 C#(以及许多其他语言)鼓励的面向对象编程风格,这些 API 调用通常是串行执行的。然而,如果一个调用的结果不影响另一个,那么这是次优的,调用可以并行执行。我们将在本章后面介绍数据库表,因为有更好的方法。

如果您实现了一个微服务架构(与传统的整体或大球泥相反),并且有许多不同的分布式端点,那么并发调用可能会更有针对性。在许多情况下,消息队列有时是比 HTTP APIs 更好的选择,也许使用发布和订阅模式。然而,也许你不负责 API,而是与第三方集成。事实上,如果应用编程接口是你的,那么无论如何你都可以修改它来提供你需要的所有数据。

考虑调用两个彼此没有依赖关系的独立 API 的例子。这方面的序列图可能如下所示:

调用 A ,然后在完成 A 时调用 B 的这个线性过程很简单,因为它不需要复杂的编排来争论 API 调用的结果,但是它比必要的要慢。通过同步和顺序调用两个 API,我们浪费时间等待它们返回。如果我们不要求第一个应用编程接口向第二个应用编程接口请求结果,那么更好的方法可能是异步调用它们。

这个新流程的序列图可能如下所示:

这个新序列有两个变化,一个明显,另一个微妙。我们异步和并行调用两个 API,以便它们同时运行。第一个变化将产生最大的影响,但还有一个更小的调整也有帮助,那就是首先调用最慢的应用编程接口。

在原序列图中,我们先调用 API A ,再调用 API B ,但是 B 比较慢。同时调用 AB 影响会比较大,但是调用 B 然后 A 稍微好一点。 B 主宰着时间线,我们会消磨掉(相对)大量的时间等待它。

我们可以利用这段停机时间来调用 A ,因为任何 API 方法调用都会有一些小的固定开销。这两个变化加在一起,意味着我们现在只等 B ,从时间上来说,实际上是免费接到了 A 的电话。

我们可以用一个简单的控制台应用来说明这个原理。模拟 API 的方法有两种,两种都很相似。 API A 有以下代码:

private static async Task CallApiA() 
{ 
    Thread.Sleep(10); 
    await Task.Delay(100); 
} 

Thread.Sleep模拟固定开销,Task.Delay模拟等待 API 调用返回。 API B 的返回时间是 API A 的两倍,但是固定开销是一样的,它有以下代码:

private static async Task CallApiB() 
{ 
    Thread.Sleep(10); 
    await Task.Delay(200); 
} 

现在,如果我们按顺序同步调用这些方法,我们会发现所有的延迟都像预期的那样增加了。

CallApiA().Wait(); 
CallApiB().Wait(); 

这些操作平均总共花费大约332 ms,因为在方法调用中有大约 10 ms 的额外固有开销。如果我们同时调用这两种方法,时间会大大减少。

Task.WaitAll(CallApiA(), CallApiB()); 

现在手术平均总共需要233 ms。这很好,但是如果我们交换方法的顺序,我们可以做得更好。

Task.WaitAll(CallApiB(), CallApiA()); 

这现在平均总共需要218 ms,因为我们已经把 API A 的固定开销吞进了我们等待 API B 的时间里。

The full console application that benchmarks these three variants is available for download with this book. It uses the Stopwatch class to time the operations, and averages the results over many runs.

这三种不同方法的结果如下图所示:

一如既往,衡量你的结果,以确保确实有所改善。您可能会发现,如果您对一个应用编程接口进行多次调用,或者重用同一个应用编程接口客户端,那么无论如何,您的请求都会按顺序处理。

任务的并行化不仅适用于输入/输出,还可以用于计算,我们将在下一章中发现这一点。现在,我们将继续深入研究数据库查询优化。

高效的数据库操作

虽然这不是一本针对数据库管理员 ( 数据库管理员)的书,但欣赏你正在使用的底层技术对你是有利的。与网络一样,如果您了解优势和劣势,那么您就可以更轻松地实现高性能。

这也与开发人员在专业领域之外做更多工作的趋势有关,尤其是在小型组织或初创公司。你可能听过相关的流行语全栈开发者或者 DevOps 。这些类似的概念指的是模糊传统技术界限的角色,并将开发(前端和后端)、质量保证、网络、基础设施、运营和数据库管理合并为一项工作。如今,成为一名开发人员不再仅仅是编程或发布代码。了解其他领域也有帮助。

之前在第 5 章修复常见性能问题中,我们讲述了使用微 O/RM Dapper,以及如何修复选择 N+1 的问题。现在,我们将在这些知识的基础上,重点介绍您可以整合查询和提高数据库操作性能的其他几种方法。

第 4 章测量性能瓶颈所述,第一步是分析您的应用并发现问题所在。只有到那时,您才应该继续将解决方案应用到问题区域。

数据库优化

我们将更多地关注于改进性能不佳的查询,因此我们不会过多地讨论数据库调优。这是一个复杂的话题,关于索引和数据的存储方式,你可以学到很多东西。

但是,如果您还没有运行数据库引擎优化顾问 ( DETA ),那么如果您有任何缺失的索引,这是一个很容易识别的步骤。然而,当应用推荐的更改时,您应该小心,因为总有不利的方面和权衡要考虑。例如,索引使检索数据更快,但也使添加和更新记录更慢。它们还会占用额外的空间,所以最好不要过度。您是希望在前期进行优化,还是在后期进行检索,将取决于您特定的业务用例。

第一步是使用 SQL Server Profiler 捕获跟踪并保存结果文件。如何做到这一点,请参见第 4 章测量性能瓶颈。调优配置文件是捕获跟踪的好选择,为了获得好的结果,您应该确保您捕获的是真实数据库使用的代表。您可以从与 SSMS 的 SQL Server Profiler 相同的菜单中启动 DETA,如下图所示:

然后,您可以将跟踪作为工作负载加载,在处理之后,它会给您一些建议。出于篇幅的原因,我们在这里不涉及如何做到这一点的细节,它是非常不言自明的。如果需要,网上有很多好的指南。如果在 SSMS 运行查询时启用实际的查询计划,还可以看到建议的缺失索引。

报告

一个常见的用例是报告,它经常会使数据库崩溃。报告通常运行在大量数据上,如果写得不好,可能会花费相当长的时间。

如果您已经有了一种从数据库中检索单个项目的方法,那么为了报告的目的和计算应用中的度量,很容易重复使用这个方法。但是,最好避免这种情况,报告通常应该有自己的专用查询。

更好的方法是使用另一个数据库,专门用于报告,该数据库由主数据库或备份填充。这样,密集的报告不会以不包括实时数据为代价影响主应用的性能。这有时被称为数据仓库数据集市,出于简单性或性能原因,数据偶尔会被反规格化。填充另一个数据库也是测试备份的一种方便方法,因为应该始终测试数据库备份,以确保它们可以正确恢复,并且包含所有必需的数据。

In the ensuing examples, we will focus on Microsoft SQL Server (MS SQL ) and the Transact-SQL (T-SQL ) dialect of SQL. Other databases, such as PostgreSQL, are available as well. PostgreSQL has its own dialect called Procedural Language / PostgreSQL (PL/pgSQL ), which is similar to Oracle PL/SQL . These dialects all support basic SQL commands, but use different syntax for some operations, and contain different or additional features. The latest version of MS SQL Server, SQL Server 2017, supports running on Linux and in Docker containers, in addition to running on Windows.

总计

聚合函数是关系数据库管理系统的一个非常有用的特性。您可以计算汇总数据库中许多记录的值,并且只返回结果,将源数据行保留在数据库中。

如果不是以前的话,你会熟悉本书前面的COUNT聚合函数。这给出了查询返回的记录总数,但没有返回它们。您可能已经读到过COUNT(1)的性能比COUNT(*)更好,但现在已经不是这样了,因为 SQL Server 现在对后者进行了优化,使其性能与前者相同。

By default, SQL Server will return a message detailing the count of the records returned, along with the result of every query. You can turn this off by prefixing your query with the SET NOCOUNT ON command, which will save a few bytes in the Tabular Data Stream (TDS ) connection, and increase performance slightly. This is significant only if a cursor is being used, which is bad practice anyway for locking reasons. It's good practice to re-enable row count after the query, even though it will be reset outside the local scope anyway.

第 5 章修复常见性能问题中,我们通过连接数据库中的表并使用COUNT而不是在应用代码中执行这些计算来解决我们的选择 N+1 问题。还有许多其他可用的聚合函数可用于提高性能,尤其是用于报告目的。

回到我们前面的例子,假设我们现在想要找出博客文章的总数。我们还希望找到评论总数、平均评论数、单个帖子的最低评论数和单个帖子的最高评论数。

第一部分很容易,只需将COUNT应用于帖子而不是评论,但第二部分更难。由于我们已经有了每个帖子都有评论数的帖子列表,所以重用它并简单地用 C#代码解决所有问题可能很有诱惑力。

这将是一个坏主意,而且查询的性能会很差,尤其是在帖子数量很多的情况下。更好的解决方案是使用如下查询:

;WITH PostCommentCount AS( 
SELECT  
    bp.BlogPostId, 
    COUNT(bpc.BlogPostCommentId) 'CommentCount' 
FROM BlogPost bp 
LEFT JOIN BlogPostComment bpc 
    ON bpc.BlogPostId = bp.BlogPostId 
GROUP BY bp.BlogPostId 
) SELECT  
    COUNT(BlogPostId) 'TotalPosts', 
    SUM(CommentCount) 'TotalComments', 
    AVG(CommentCount) 'AverageComments', 
    MIN(CommentCount) 'MinimumComments', 
    MAX(CommentCount) 'MaximumComments' 
FROM PostCommentCount 

该查询使用一个通用表表达式 ( CTE ),但是您也可以使用一个嵌套的SELECT将第一个查询嵌入到第二个查询的FROM子句中。它说明了可用的聚合函数的选择,并且之前测试数据库的结果如下所示:

The semicolon at the start is simply a good practice to avoid errors and remove ambiguity. It ensures that any previous command has been terminated, as WITH can be used in other contexts. It isn't required for the preceding example, but might be if it were part of a larger query. CTEs can be very useful tools, especially if you require recursion. However, they are evaluated every time, so you may find that temporary tables perform much better for you, especially if querying one repeatedly in a nested SELECT .

现在假设我们希望执行相同的查询,但是只包括至少有一个注释的帖子。在这种情况下,我们可以使用如下查询:

;WITH PostCommentCount AS( 
SELECT  
    bp.BlogPostId, 
    COUNT(bpc.BlogPostCommentId) 'CommentCount' 
FROM BlogPost bp 
LEFT JOIN BlogPostComment bpc 
    ON bpc.BlogPostId = bp.BlogPostId 
GROUP BY bp.BlogPostId 
HAVING COUNT(bpc.BlogPostCommentId) > 0 
) SELECT  
    COUNT(BlogPostId) 'TotalPosts', 
    SUM(CommentCount) 'TotalComments', 
    AVG(CommentCount) 'AverageComments', 
    MIN(CommentCount) 'MinimumComments', 
    MAX(CommentCount) 'MaximumComments' 
FROM PostCommentCount 

我们增加了HAVING条款,确保我们只统计评论超过零的帖子。这类似于WHERE子句,但与GROUP BY一起使用。查询结果现在看起来如下所示:

抽样

有时,您不需要使用所有的数据,并且可以对其进行采样。这种技术特别适用于您可能希望绘制的任何时间序列信息。在 SQL Server 中,执行随机采样的传统方法是使用NEWID()方法,但这可能会很慢。例如,考虑以下查询:

SELECT TOP 1 PERCENT * 
FROM [dbo].[TrainStatus] 
ORDER BY NEWID() 

该查询返回 1%的随机分布的行。当对包含 1,205,855 个条目的表运行时,它在大约 4 秒钟内返回 12,059 个结果,这很慢。更好的方法可能是使用TABLESAMPLE,它可以在任何合理的最新版本的 SQL Server (2005 年以后)中使用,如下所示:

SELECT * 
FROM [dbo].[TrainStatus] 
TABLESAMPLE (1 PERCENT) 

前面的这个查询要快得多,当对与前面示例相同的数据运行时,它几乎立即完成。缺点是它比早期的方法更粗糙,它不会返回 1%的结果。它将返回大约 1%,但每次运行时该值都会改变。例如,对同一个测试数据库运行时,当连续三次执行查询时,它返回了 11,504、13,441 和 11,427 行。

插入数据

查询数据库可能是最常见的用例,但是您通常需要首先将一些数据放入其中。将记录插入关系数据库时最常用的功能之一是标识列。这是一个由数据库生成的自动递增的标识,在添加数据时不需要提供。

例如,在我们之前的BlogPost表中,BlogPostId列被创建为INTIDENTITY(1,1)。这意味着它是一个整数值,从 1 开始,每增加一行就增加 1。你INSERT进入这个表格,没有指定BlogPostId,就这样:

INSERT INTO BlogPost (Title, Content) 
VALUES ('My Awesome Post', 'Write something witty here...') 

身份可能非常有用,但是您通常想要知道新创建的记录的 ID。通常希望存储一行,然后立即检索它,以便该 ID 可以用于未来的编辑操作。你可以像这样用OUTPUT子句一次性完成;

INSERT INTO BlogPost (Title, Content) 
OUTPUT INSERTED.BlogPostId 
VALUES ('My Awesome Post', 'Write something witty here...') 

除了插入行,还将返回标识。

You may see SCOPE_IDENTITY() (or even @@IDENTITY ) advocated as a way of retrieving the identity, but these are outdated. The recommended way of doing this, on modern versions of SQL Server, is to use OUTPUT .

OUTPUT工作在INSERTUPDATEDELETE,MERGE命令上。它甚至可以在一次操作多行时工作,就像在这个大容量插入两个博客文章的例子中一样:

INSERT INTO BlogPost (Title, Content) 
OUTPUT INSERTED.BlogPostId 
VALUES ('My Awesome Post', 'Write something witty here...'), 
       ('My Second Awesome Post', 'Try harder this time...') 

前面的查询将返回两个身份的结果集,例如 3003 和 3004。为了使用 Dapper 执行这些插入,可以对单个记录使用以下方法。首先,让我们创建一个博客文章对象,我们将在这里硬编码,但通常来自用户输入:

var post = new BlogPost 
{ 
    Title = "My Awesome Post", 
    Content = "Write something witty here..." 
}; 

要插入这篇文章,并设置 ID,您可以使用以下代码。这将执行INSERT语句,并返回一个整数值。

post.BlogPostId = await connection.ExecuteScalarAsync<int>(@" 
    INSERT INTO BlogPost (Title, Content) 
    OUTPUT INSERTED.BlogPostId 
    VALUES (@Title, @Content)", 
    post); 

然后,您可以将返回的标识分配给帖子,并将该对象返回给用户进行编辑。假设插入成功并且没有抛出异常,则不需要再次选择记录。

通过使用 execute 方法,可以用同一个 SQL 一次插入多条记录。但是,该 SQL 将被执行多次,这可能会导致效率低下,并且您只能获得插入的行数,而不是它们的标识。下面的代码为上一个示例中使用的同一 SQL 提供了一个帖子数组:

var numberOfRows = await connection.ExecuteAsync(@" 
    INSERT INTO BlogPost (Title, Content) 
    OUTPUT INSERTED.BlogPostId 
    VALUES (@Title, @Content)", 
    new[] { post, post, post }); 

如果您想要返回多个标识,那么您将需要使用查询方法来返回一组值,就像我们之前所做的那样。但是,如果不动态构建 SQL,很难使这种方法适用于可变数量的记录。下面的代码执行大容量插入,对查询使用多个单独的参数:

var ids = await connection.QueryAsync<int>(@" 
    INSERT INTO BlogPost (Title, Content) 
    OUTPUT INSERTED.BlogPostId 
    VALUES (@Title1, @Content1), 
           (@Title2, @Content2)", new 
    { 
        Title1 = post.Title, 
        Content1 = post.Content, 
        Title2 = post.Title, 
        Content2 = post.Content 
    }); 

这将在一个命令中执行所有工作,并返回标识的可枚举集合。但是,这段代码的可伸缩性不是很好(甚至不够优雅),您最多只能以这种方式插入 1,000 条记录。

There are many helper methods in the Dapper.Contrib package that can assist you with inserting records and other operations. However, they suffer from the same limitations as the examples here, and you can only return a single identity or the number of rows inserted.

向导

整数标识在内部对数据库很有用,但是也许您不应该在外部使用标识密钥,尤其是当您有多个 web 服务器或者向用户公开标识时。另一种方法是使用全球唯一标识符 ( GUID ,在 SQL Server 中称为UNIQUEIDENTIFIER。我们已经谈到了这些,因为它们是由次优采样示例中使用的NEWID()函数生成的。

GUIDs 被广泛使用,长度为 16 字节,是整数的四倍。GUID 的大小意味着在向已经填充的表中插入随机 GUID 时,不太可能得到唯一约束冲突。

People sometimes worry about GUID collisions, but the numbers involved are so staggeringly huge that collisions are incredibly unlikely. The GUIDs generated by .NET (using a COM function on Windows) are, specifically, Universally Unique Identifiers (UUIDs ) version 4. These have 122 bits of randomness, so you would need to generate nearly three quintillion GUIDs before having even half a chance of a collision. At one new record per second, that would take over 90 billion years. Even at two million GUIDs per second (what an average computer today could produce), it would still take over 45 thousand years (more time than human civilization has been around). If you think that this will be an issue for your software, then you should probably be future-proofing it by using five-digit years, such as 02017, to avoid the Y10K (or YAK in hexadecimal) problem.

这个唯一性属性允许您在应用代码中生成一个 GUID,并将其存储在分布式数据库中,而无需担心以后合并数据。它还允许您向用户公开标识,而不用担心有人会枚举它们或试图猜测一个值。整数标识通常公开一个表中有多少条记录,根据您的用例,这些记录可能是敏感信息。

随机 GUIDs 需要注意的一点是将它们用作密钥,而不是 id。这两个术语之间的区别是微妙的,它们经常是同一个。关键字(聚集)是数据库用来寻址记录的,而标识是用来检索记录的。这些可以是相同的值,但不一定是。使用随机 GUID 作为密钥可能会导致索引性能问题。随机性可能会导致碎片,这将降低查询速度。

你可以使用连续的 guid,例如NEWSEQUENTIALID()可以是一列的DEFAULT值,但是这样你就失去了 guid 的大部分好处,这些好处主要来自于随机性。你现在实际上只有一个很大的整数,如果你只关心一个表中的最大行数,并且需要超过 20 亿,那么一个big int(两次一个int,但是半个 GUID)就足够了。

一个很好的折衷方案是将int标识作为主键,但使用 GUID 作为 ID(并通过对列的唯一约束来实现这一点)。最好使用Guid.NewGuid()在应用代码中生成 GUID,因为使用数据库中的默认列值意味着您必须在插入后检索标识,如前所示。

使用这种策略的表格可能部分类似于下面的截图。这是来自成员预引导用户管理和身份验证库的部分表格的屏幕截图:

高级数据库主题

我们可以在这里介绍许多其他数据库性能增强技术,但是没有空间。我们将简要提及一些额外的主题,以防您想进一步了解它们。访问作者在https://unop.uk/的网站,了解更多关于其他主题的信息。

保存数据时的一个常见要求是,如果记录尚不存在,则插入该记录,或者如果已经有一行包含该标识,则更新现有记录。这有时被称为追加,这是一个结合了更新和插入的复合词。

您可以使用MERGE命令在一次操作中完成此操作,而不必担心在UPDATEINSERT之间进行选择(包装在存储过程内的事务中)。但是MERGE的表现不如 UPDATE 或者INSERT,所以一定要针对你的用例测试效果,谨慎使用。

存储过程、事务和锁定本身都是大主题。理解它们很重要,不仅仅是从性能的角度。

我们不能把所有这些都放在这里,但是我们将在本书的后面讨论维护存储过程。

您可以研究的其他一些技术是跨数据库连接,它可以让您避免查询多个数据库。还有反规格化数据的做法,这涉及提前展平关系记录以提供单个行,而不是为了一个查询需要连接多个表。我们在前面的数据仓库中简要提到了这一点。

You can find lots of useful SQL documentation and information on MSDN (http://msdn.microsoft.com/ ) and TechNet (http://technet.microsoft.com/ ). Additionally, there are many good books on advanced SQL techniques.

最后,关于家政的一个注意事项。有一个管理数据库大小的计划很重要。不能简单地一直往表中插入数据,希望它永远继续表现良好。在某个时候,它会变得如此之大,以至于即使从其中删除记录也会导致您的数据库崩溃(即使您使用TRUNCATE,这比简单的DELETE性能更好)。您应该提前知道如何删除或归档行,最好定期小批量地这样做。

模拟和测试

为了总结这一章,让我们重申能够在现实的基础设施上测试您的应用的重要性。您的测试环境应该尽可能像真实的一样。如果您没有在同等的数据库和网络上进行测试,那么您可能会在部署时得到一个令人讨厌的惊喜。

当使用云托管提供商时(如果你自动化你的服务器构建),那么这很容易。您可以简单地启动一个与生产相匹配的分段系统。只要所有部件都在同一位置,就不必将它设置为完全相同的规模。为了进一步降低成本,您只需要在测试期间保持它。

或者,您可以创建一个新的实时环境,部署并测试它,然后切换,并销毁或重用旧的实时环境。这种交换技术被称为蓝绿色部署。另一个选择是在功能开关后面部署新代码,这允许您在运行时切换该功能,并且仅适用于某些用户。这不仅允许您在功能上与测试用户一起验证特性,您还可以逐步推出新特性,并在这样做时监控性能影响。我们在第 11 章监控性能回归中介绍了这两种技术。

摘要

这一章我们已经讲了很多,希望大家清楚,成为一名高性能开发人员需要的不仅仅是一名有能力的程序员。在你的代码中有许多外部因素需要考虑。

现在,您已经了解了输入/输出的基础知识,以及存在的内在限制的物理原因。您可以分析和诊断网络在逻辑上是如何结合在一起的。我们已经介绍了如何更好地利用网络来降低总体延迟,这一点很重要,因为现在网络上发生了更多的输入/输出操作。您还看到了如何更好地利用数据库,以及如何用更少的操作做更多的事情。

在下一章中,我们将详细挖掘代码执行,并向您展示如何加快您的 C#速度。我们拭目以待.NET Core 和 ASP.NET Core 的性能比以前的版本好得多,以及如何利用这些改进。

八、理解代码执行和异步操作

本章介绍如何解决代码执行中的性能问题,这通常不是出现速度问题的第一个地方,但却是一个很好的额外优化。我们将讨论哪些领域需要性能,哪些领域可以(甚至需要)慢下来。将比较各种数据结构的优点,从标准的内置泛型集合到更奇特的。我们将演示如何并行计算操作,以及如何利用您的中央处理器可能提供的额外指令集。我们将深入 ASP.NET Core 的内部.NET Core 来强调您应该注意的改进。

本章涵盖的主题包括以下内容:

  • .NET Core 和标准库
  • 公共语言运行时 ( CLR )服务
  • ASP.NET Core 和红隼网络服务器
  • 通用集合和布隆过滤器
  • 序列化和数据表示格式
  • 散列函数的相对性能
  • 并行化(SIMD、第三方物流和 PLINQ)
  • 多线程、并发和锁定
  • 要避免的不良实践

您将学习如何并行计算结果,并在最后组合输出。这包括如何避免不正确的方法,这会让事情变得更糟。您还将学习如何降低服务器利用率,以及如何选择最合适的数据结构,以便针对您的特定情况高效地处理信息。

开始核心项目

使用有很多好处.NET Core 和 ASP.NET Core 优于旧的完整版本的框架。主要的增强是开源开发和跨平台支持,但也有显著的性能改进。开放开发很重要,不仅源代码可用,开发工作也在 GitHub 上公开进行。鼓励社区做出贡献,如果他们通过了代码审查,这些贡献可能会被合并到上游;流动不仅仅是一种方式。这导致了性能的提高和来自微软外部的额外平台支持。如果你在一个框架中发现一个 bug,你现在可以修复它,而不是绕过这个问题并希望得到一个补丁。

构成框架的多个项目在 GitHub 上被分成两个组织。驱动原则之一是将框架拆分成模块,这样您就可以得到您需要的东西,而不是整个整体安装。较低级别的框架.NET Core,可以和其他项目一起在https://github.com/dotnet下找到。更高级别的网络应用框架,ASP.NET Core,可以在https://github.com/aspnet下找到。

让我们快速浏览一下各种.NET Core 存储库以及它们如何组合在一起。

.NET Core

有几个项目构成了.NET Core 这些是 CoreCLR 和 CoreFX 。CoreCLR 包含.NET Core CLR 和基础核心库,mscorlib。CLR 是运行您的的虚拟机.NET 代码。CoreCLR 包含一个即时 ( JIT )编译器( RyuJIT )、垃圾收集器 ( GC )以及 mscorlib 中的基类和类。CoreFX 包括基础库,位于 CoreCLR 之上。这包括大多数不是简单类型的内置组件。还有罗斯林编译器,它可以把你的 C#(或其他.NET 语言代码)转换成通用中间语言 ( CIL )字节码。RyuJIT 然后在运行时将其转换为本机机器代码。

您可能听说过即将推出的原生工具链。这保证了更多的性能改进,因为编译不必快速、实时地发生,并且可以进一步优化。例如,可以牺牲编译速度来调整构建的执行速度。它在概念上类似于原生图像生成器 ( NGen )工具,该工具已经处于完整状态.NET 框架自诞生以来。不幸的是。核心项目的. NET 原生版本被延迟并推迟到.NET Core 2。目前,它仅适用于 UWP 视窗商店应用,但扩展后将值得考虑。

虽然.NET Core 的性能通常比前一个更好.NET 框架中,一些性能设置的配置不同。例如,您可能熟悉用于设置 GC 模式的<gcServer>元素,该元素在具有两个以上处理器的机器上表现更好。英寸 NET Core 您可以在您的.csproj文件中将ServerGarbageCollection设置为true,如下所示:

<PropertyGroup>

    <ServerGarbageCollection>true</ServerGarbageCollection>

</PropertyGroup>

ASP.NET Core

ASP.NET Core 运行在.NET Core,虽然它也可以满负荷运行.NET 框架。我们只讨论跑步.NET Core,因为这表现得更好,并确保增强的平台支持。有很多项目组成了 ASP.NET Core,值得简单提一下其中的一些。

不包含任何框架引用是很有用的.NET 4,这样你就可以确定你只是在使用.NET Core,不要意外引用任何尚不支持的依赖项。这不是什么大问题.NET 标准 2.0,所以如果你坚持支持的 API,那么你的项目应该在所有实现它们的框架上工作。

ASP.NET Core 包括模型-视图-控制器 ( MVC )、 Web API、Razor Pages (一种用 Razor 视图引擎制作简单页面的方式,类似于 PHP,是经典 ASP 的精神继承者)。这些特性都合并在一起了,所以你不再需要把 MVC 和 Web API 看作是独立的框架了。还有很多其他的项目和存储库,包括 EF Core ,但是我们这里要重点介绍的是红隼 HTTP 服务器。

红隼

红隼是 ASP.NET Core 的一个新的网络服务器,它表现出色。它基于 libuv ,这是一个异步 I/O 抽象和支持库,也在 Node.js. Kestrel 下面运行,速度惊人,基准测试非常令人印象深刻。然而,这是非常基本的,对于生产托管,您应该将其放在反向代理的后面,这样您就可以使用缓存和其他功能来服务更多的用户。为此,您可以使用许多 HTTP 或代理服务器,如 IIS、nginx、Apache 或 HAProxy。

You should be careful with your configuration if using nginx as a reverse proxy, as by default it will retry POST requests if the response from your web server times out. This could result in duplicate operations being performed, especially if used as a load balancer across multiple backend HTTP servers.

数据结构

数据结构是可以用来存储正在处理的信息的对象。为如何使用数据选择最佳实现会对执行速度产生巨大影响。在这里,我们将讨论一些您可能喜欢使用的常见和更有趣的数据结构。

因为这是一本关于 web 应用编程的书,而不是一本关于复杂算法实现或微优化的书,所以我们不会对数据结构和算法进行大量的详细描述。正如简介中简要提到的,其他因素会使网络环境中的执行速度相形见绌,我们假设您不是在编写游戏引擎。然而,好的算法可以帮助加速应用,所以,如果你在一个执行性能很重要的领域工作,你应该阅读更多关于它们的内容。

我们更感兴趣的是整个系统的性能,而不一定是代码运行的速度。考虑您的代码表达了什么(以及这如何影响其他系统)通常比它如何执行更重要。尽管如此,为这项工作选择合适的工具仍然很重要,并且您不想有不必要的缓慢代码。当它已经执行得足够快时,小心过度优化,最重要的是,尽量保持它的可读性。

列表

a.NET List<T>是 C#中编程的主要部分。它是类型安全的,所以你不必为施法或拳击而烦恼。您可以指定您的泛型类型,并确保只有该基元或类的对象(或从它们继承的对象)可以在您的列表中。列表实现了标准列表和可枚举接口(IListIEnumerable),尽管在具体的列表实现上您会得到更多的方法,例如,添加一个范围。也可以使用语言集成查询 ( LINQ )表达式轻松查询,使用流畅的 lambda 函数时比较琐碎。然而,尽管 LINQ 通常是一个解决问题的好办法,但它可能会导致性能问题,因为它并不总是优化的。

列表实际上只是一个增强的一维数组。事实上,它在内部使用数组来存储数据。在某些情况下,为了提高性能,直接使用数组可能会更好。如果您确切地知道会有多少数据,并且需要在一个紧密的循环中迭代,那么数组偶尔会是一个更好的选择。如果您需要更多维度,或者计划使用数组中的所有数据,它们也会很有用。

您应该小心使用数组;只有当基准测试在遍历列表时显示出性能问题时,才能谨慎使用它们。虽然它们可以更快,但它们也使并行编程和分布式体系结构变得更加困难,这是可以显著提高性能的地方。

现代高性能意味着扩展到多个内核和多台机器。使用不变的不可变的状态更容易做到这一点,这在更高级别的抽象中比在数组中更容易实现。例如,您可以通过接口帮助强制执行只读列表。

如果您在一个大列表的中间插入或删除了很多项目,那么使用LinkedList<T>类可能会更好。这与列表有不同的性能特征,因为它不是真正的列表,它更像是一个链。对于某些特殊情况,它的性能可能比列表更好,但在大多数常见情况下,它会更慢。例如,使用索引访问列表很快(因为它是数组支持的),但是使用链表很慢(因为您将不得不遍历整个链)。

通常,最好首先关注代码的内容和原因,而不是方式。LINQ 在这方面做得非常好,因为您只需声明您的意图,而不需要担心实现细节和循环。过早优化是一个坏主意,所以只有在测试显示出性能问题时才这样做。在大多数情况下,列表是正确的选择,除非您只需要从一个大集合中选择几个值,在这种情况下,字典可以表现得更好。

字典

字典类似于列表,但擅长用键快速检索特定值。在内部,它们是用哈希表实现的。有遗留的Hashtable类(在System.Collections.NonGeneric包中),但是这不是类型安全的,而Dictionary<T>是一个泛型类型,所以您可能不应该使用Hashtable,除非您将旧代码移植到.NET Core。这同样适用于ArrayList,它是List的遗留非通用版本。

字典可以非常快速地查找带有关键字的值,而列表需要遍历所有的值,直到找到关键字。然而,你仍然可以按照字典的顺序来列举它,尽管这并不是它的真正用途。如果不需要点餐,那么可以使用HashSet。所有这些数据结构都有分类版本,您可以再次使用只读接口来使它们难以变异。

收集基准

准确的基准测试很难,有很多事情会扭曲你的结果。编译器非常聪明,会进行优化,这会降低琐碎基准测试的价值。编译器可以为不同的实现输出非常相似的代码。

您在数据结构中放入的内容也会对它们的性能产生很大影响。通常最好测试或分析您的实际应用,不要过早优化。代码的可读性非常有价值,不应该为了运行时的效率而牺牲它,除非有明显的性能问题(或者它已经不可读了)。

有一些基准测试框架可以用来帮助你的测试,比如 BenchmarkDotNet,它可以在https://github.com/dotnet/BenchmarkDotNet找到。然而,这些可能是一个矫枉过正,有时很难设置。其他选项包括简易速度测试仪(可在http://theburningmonk.github.io/SimpleSpeedTester/)和迷你本(可从https://github.com/minibench获得)。

You can read more about the benchmarks of the ASP.NET Core framework at https://github.com/aspnet/benchmarks .

我们将执行一些简单的基准测试来展示您可能会如何做。然而,不要假设这里得出的结论总是成立的,所以要根据你的情况进行测试。这里的例子故意保持简单,以便您可以轻松地运行它们,尽管我们确实试图避免一些明显的错误。

首先,我们将定义一个简单的函数来运行我们的测试:

private static long RunTest(Func<double> func, int runs = 1000) 
{ 
    var s = Stopwatch.StartNew(); 
    for (int j = 0; j < runs; j++) 
    { 
        func(); 
    } 
    s.Stop(); 
    return s.ElapsedMilliseconds; 
} 

我们在这里使用Stopwatch是因为使用DateTime会导致问题,即使使用 UTC 也是如此,因为时间可能会在测试过程中发生变化,并且分辨率不够高。我们还需要进行多次运行才能获得准确的结果。然后,我们将定义我们的数据结构,用随机数据测试和预填充它们:

var rng = new Random(); 
var elements = 100000; 
var randomDic = new Dictionary<int, double>(); 
for (int i = 0; i < elements; i++) 
{ 
    randomDic.Add(i, rng.NextDouble()); 
} 
var randomList = randomDic.ToList(); 
var randomArray = randomList.ToArray(); 

我们现在有一个数组、一个列表和一个字典,包含相同的一组 100,000 个键/值对。接下来,我们可以对它们进行一些测试,看看什么结构在各种情况下表现最好:

var afems = RunTest(() => 
{ 
    var sum = 0d; 
    foreach (var element in randomArray) 
    { 
        sum += element.Value; 
    } 
    return sum; 
}, runs); 

前面的代码乘以在foreach循环中迭代一个数组所花费的时间,并对双精度浮点值求和。然后,我们可以将它与其他结构和访问它们的不同方式进行比较。例如,在for循环中迭代字典的操作如下:

var dfms = RunTest(() => 
{ 
    var sum = 0d; 
    for (int i = 0; i < randomDic.Count; i++) 
    { 
        sum += randomDic[i]; 
    } 
    return sum; 
}, runs); 

这表现得很糟糕,因为字典不应该这样使用。字典擅长通过关键字快速提取记录。事实上,速度如此之快,以至于您需要多次运行测试才能获得实际值:

var lastKey = randomList.Last().Key; 
var dsms = RunTest(() => 
{ 
    double result; 
    if (randomDic.TryGetValue(lastKey, out result)) 
    { 
        return result; 
    } 
    return 0d; 
}, runs * 10000); 
Console.WriteLine($"Dict select {(double)dsms / 10000} ms"); 

TryGetValue从字典中获取一个值是非常快的。您需要将要设置的变量作为out参数传递到方法中。您可以通过测试方法返回的布尔值来查看这是否成功以及该项是否在字典中。

相反,一个接一个地向字典添加条目可能会很慢,所以这完全取决于你在优化什么。

下面的屏幕截图显示了一个非常简单的控制台应用的输出,该应用测试数据结构的各种组合排列及其用途:

上图中显示的这些结果提供了丰富的信息,但是您应该持怀疑态度,因为许多事情会扭曲输出,例如测试的顺序。如果差距很小,那么这两种变体之间可能没有太多的选择,但是你可以清楚地看到很大的差异。

To get more realistic results, be sure to compile in release mode and run without debugging. The absolute results will depend on your machine and architecture but the relative measures should be useful for comparisons, if the differences are large.

这里的主要课程是为您的特定应用进行测量,并为您正在执行的工作选择最合适的数据结构。标准图书馆的藏书应该能很好地为你服务;这里没有提到的其他方法有时也很有用,例如QueueStack

你可以在 MSDN 找到更多关于内置收藏和数据结构的信息。你也可以在。在 GitHub 页面上的. NET Core 文档网站。

然而,还有一些更罕见的数据结构,不在标准集合中,您可能偶尔希望使用。我们现在将展示其中一个例子。

布鲁姆过滤器

布隆过滤器是一种有趣的数据结构,可以提高某些用例的性能。它们使用多个重叠的散列函数,可以快速告诉你一个项目是否肯定不存在于集合中。然而,他们不能肯定地说一个项目是否存在,只能说它有可能存在。它们作为预过滤器很有用,这样您就可以避免执行查找,因为您肯定知道项目不会在那里。

下图显示了布隆过滤器的工作原理。 ABC 已经被散列并插入到过滤器中。 D 被散列以检查它是否在过滤器中,但是,由于它映射到零位,我们知道它不在过滤器中:

布隆过滤器比保存集合中每个项目的所有数据甚至散列列表要小得多。它们也可以快得多,因为查找时间对于任何大小的集合都是恒定的。这个恒定时间可能比在大型列表或字典中查找项目的时间要短,尤其是在文件系统或远程数据库中。

布隆过滤器的一个示例应用可以是本地 DNS 服务器,它有一个要覆盖的域列表,但是将大多数请求转发给上游服务器。如果自定义域的列表很大,那么从条目中创建一个布隆过滤器并将其保存在内存中可能是有利的。

当一个请求进来时,它会根据过滤器进行检查,如果它不存在,那么这个请求会被转发到上游服务器。如果该条目确实存在于过滤器中,则查询本地主机文件;如果条目在那里,则使用它的值。即使筛选器认为该条目不在列表中,它也很有可能不在列表中。在这种情况下,当没有找到时,请求被转发,但是这种方法仍然避免了为每个请求查询列表的需要。

使用布隆过滤器的另一个例子是在缓存节点中,可能作为代理或 CDN 的一部分。您不想缓存只被请求一次的资源,但是如果您没有缓存它,您怎么知道第二次请求何时发生?如果您将请求添加到 Bloom 过滤器,您可以很容易地判断第二个请求何时发生,然后缓存资源。

布隆过滤器也用于一些数据库,以避免昂贵的磁盘操作,并用于缓存摘要,允许代理声明其缓存的内容。HTTP/2 将来可能会支持缓存摘要,但这可能会使用 Golomb 编码的集合 ( GCS ),它们类似于 Bloom 过滤器,但更小,代价是查询速度更慢。

中有一个布隆过滤器的开源实现。除其他外,网络可在http://bloomfilter.codeplex.com/获得。你应该自己测试性能,以确保它提供了改进。

哈希和校验和

哈希是一个重要的概念,通常用于快速确保数据完整性或查找值,因此它被优化为快速。这就是为什么不应该单独使用一般的散列函数来安全地存储密码。如果算法很快,那么可以在合理的短时间内猜出密码。哈希算法的复杂性、执行速度、输出长度和冲突率各不相同。

一种非常基本的错误检测算法叫做奇偶校验。这将一个位添加到一个数据块中,并且很少在编程中直接使用。然而,它在硬件级别被广泛使用,因为它非常快。然而,当出现偶数个损坏时,它可能会遗漏许多错误。

循环冗余校验是一种稍微复杂一点的错误检测算法。 CRC-32 (也写为 CRC32 )版本通常用于软件,尤其是压缩格式,作为校验和

You may be familiar with the built-in support for hash codes in .NET (with the GetHashCode method on all objects), but you should be very careful with this. The only function of this method is to assist with picking buckets in data structures that use hash tables internally, such as a dictionary, and also in some LINQ operations. It is not suitable as a checksum or key, because it is not cryptographically secure and it varies across frameworks, processes, and time.

您过去可能使用过消息摘要 5 ( MD5 )算法,但今天强烈建议不要使用它。MD5 的安全性严重受损,很容易产生冲突。因为它非常快,所以它可能有一些非安全的用途,例如非恶意错误检查,但是有更好的算法足够快。

如果你需要一个强大但快速的散列函数,那么安全散列算法 ( SHA )家族是一个不错的选择。但是, SHA-1 不被认为是防未来的,所以对于新代码 SHA-256 一般是比较好的选择。

在对消息进行签名时,您应该使用专用的消息认证码 ( MAC ),例如基于哈希的 MAC ( HMAC ),这样可以避免哈希函数一次通过的漏洞。一个很好的选择是内置在. NET 中的HMACSHA256类。各种 API,比如一些 AWS REST APIs,使用 HMAC-SHA256 来验证请求。这确保了即使请求是通过未加密的 HTTP 通道执行的,API 密钥也不会被截获和恢复。

正如第 2 章为什么性能是一个特性中简要提到的,密码哈希是一个特例,通用哈希算法不适合它,因为它们太快了。一个很好的选择是基于密码的密钥派生函数 2 ( PBKDF2 ),我们在第 4 章测量性能瓶颈中以此为例。PBKDF2 是特别受欢迎的选择.NET,因为它被构建到框架中,所以实现更有可能是正确的。它是根据一个 RFC 构建的,并由微软审查,对于网上发现的任何随机代码,你都不能说它是 RFC。例如,您可以为下载 bcrypt 的实现。但是你必须相信它被正确编码或者自己验证它。

哈希基准

让我们对各种散列函数做一些简单的基准测试,看看它们在性能方面是如何比较的。在下面的代码片段中,我们定义了一个运行测试的方法,类似于前面集合基准测试的方法:

private static long RunTest(Action func, int runs = 1000) 
{ 
    var s = Stopwatch.StartNew(); 
    for (int j = 0; j < runs; j++) 
    { 
        func(); 
    } 
    s.Stop(); 
    return s.ElapsedMilliseconds; 
} 

我们包括以下using声明:

using System.Security.Cryptography; 

接下来,我们定义一个短的私有常量字符串(hashingData)在类中进行散列,并以 8 位 Unicode ( UTF8)格式获取其字节:

var smallBytes = Encoding.UTF8.GetBytes(hashingData); 

我们还想得到一个更大的字节块来散列,看看它在性能方面如何比较。为此,我们使用加密安全的随机数生成器:

var largeBytes = new byte[smallBytes.Length * 100]; 
var rng = RandomNumberGenerator.Create(); 
rng.GetBytes(largeBytes); 

我们的一些函数需要一个key,所以我们使用相同的技术来生成这个:

var key = new byte[256]; 
var rng = RandomNumberGenerator.Create(); 
rng.GetBytes(key); 

接下来,我们创建一个排序的算法列表来测试和执行每个算法的测试:

var algos = new SortedList<string, HashAlgorithm> 
{ 
    {"1\.          MD5", MD5.Create()}, 
    {"2\.        SHA-1", SHA1.Create()}, 
    {"3\.      SHA-256", SHA256.Create()}, 
    {"4\.   HMAC SHA-1", new HMACSHA1(key)}, 
    {"5\. HMAC SHA-256", new HMACSHA256(key)}, 
}; 
foreach (var algo in algos) 
{ 
    HashAlgorithmTest(algo); 
} 

我们的测试方法对每个算法运行以下测试。它们都是从HashAlgorithm继承的,所以我们可以对大小字节数组分别运行ComputeHash方法:

var smallTimeMs = RunTest(() => 
{ 
    algo.Value.ComputeHash(smallBytes); 
}, runs); 
var largeTimeMs = RunTest(() => 
{ 
    algo.Value.ComputeHash(largeBytes); 
}, runs); 

然后我们计算两种尺寸的平均时间。我们将长整数转换为双精度浮点数,这样就可以表示 1 到 0 之间的小值:

var avgSmallTimeMs = (double)smallTimeMs / runs; 
var avgLargeTimeMS = (double)largeTimeMs / runs; 

然后,前面的方法将这些平均时间输出到控制台。我们需要单独测试 PBKDF2,因为它不继承HashAlgorithm:

var slowRuns = 10; 
var pbkdf2 = new Rfc2898DeriveBytes(hashingData, key, 10000); 
var pbkdf2Ms = RunTest(() => 
{ 
    pbkdf2.GetBytes(256); 
}, slowRuns); 

PBKDF2 非常慢,执行 100,000 次运行需要相当长的时间(这就是使用它的意义)。在内部,这个 RFC2898 密钥拉伸算法的实现运行了 HMAC·SHA-1 10,000 次。默认值为 1,000,但是由于目前可用的计算能力,建议将其至少设置高一个数量级。例如,保护无线电脑网络安全系统 II ( WPA2 )使用 4,096 轮迭代产生一个 256 位密钥,其中服务集标识符 ( SSID )作为

输出如下所示:

从前面的输出中,您可以看到一次散列所花费的时间从小 MD5 的大约 720 纳秒到大 HMAC SHA-256 的 32 微秒和具有典型参数的小 PBKDF2 的 125 毫秒不等。

基准测试的结果可能会有很大的不同,所以你不应该过多地解读绝对值。例如,BenchmarkDotNet 工具在同一台机器上比较 MD5 和 SHA-256 的输出如下所示:

您可以在前面的截图中看到,结果与我们自制的基准不同。但是,这使用了完整的.NET Framework,计算平均时间的中位数而不是平均值,并在调试模式下运行(这有助于警告我们),等等。

更快的机器将具有更高的吞吐量(如 GitHub 上的 BenchmarkDotNet README.md所示)。专用硬件如图形处理单元 ( 图形处理器)现场可编程门阵列(FPGA)和专用集成电路 ( 专用集成电路)可以快得多。这些往往用于比特币(和其他加密货币)的挖掘,因为这些是基于哈希作为工作证明。比特币使用 SHA-256,但其他货币使用不同的哈希算法。

相同的算法构成了 TLS 的基础,因此速度更快的硬件可以处理更多的安全连接。又如,谷歌构建了一个名为张量处理单元 ( TPU )的定制 ASIC,以加速他们的机器学习云服务。

BenchmarkDotNet 中提供了其他基准测试示例,当您第一次运行它时,您将看到以下菜单:

前一个基准是第二个选项(编号#1 Algo_Md5VsSha256)。

基准测试很难,所以如果可以的话,使用像 BenchmarkDotNet 这样的库是个好主意。从我们的基准测试中,我们可以得出的唯一结论是,SHA-256 比 MD5 慢。然而,对于大多数应用来说,SHA-256 应该足够快,并且对于完整性检查来说更安全。但是,它仍然不适合存储密码。

SHA-256 可用于提供签名以验证下载的文件(为了安全起见,必须通过 HTTPS 检索)和签署证书。当作为 HMAC 的一部分使用时,它还可以用于安全地验证消息,如应用编程接口请求。只有知道正确的应用编程接口密钥才能成功连接。

序列化

序列化是将对象转换成适合通过网络传输或存储的数据的过程。我们还包括反序列化,这是相反的,在这个保护伞下。序列化不仅对网络传输速度,而且对计算都有显著的性能影响,因为它可以弥补 web 服务器上大多数昂贵的处理。您可以在https://docs . Microsoft . com/en-us/dotnet/cs harp/programming-guide/concepts/serialization/index上阅读更多关于序列化的内容。

序列化格式可以是基于文本的,也可以是二进制的。一些流行的基于文本的格式是可扩展标记语言 ( XML )和 JavaScript 对象符号 ( JSON )。一种流行的二进制格式是在谷歌开发的协议缓冲区。有一种. NET 二进制序列化格式(BinaryFormatter),现在在中得到支持.NET Core 2。

XML 在开发人员中已经过时了,现在 JSON 通常是首选。这部分是由于等效 JSON 有效载荷的尺寸较小,但也可能是由于在最初命名的简单对象访问协议 ( SOAP )中使用了 XML。这是在视窗通讯基金会 ( WCF )中使用的,但是 SOAP 不再是首字母缩略词,因为开发人员发现它远不简单。

JSON 之所以受欢迎,是因为它紧凑且可读,并且很容易被 JavaScript 使用,尤其是在网络浏览器中。有许多不同的 JSON 序列化程序.NET,具有不同的性能特征。然而,因为 JSON 不像 XML 那样严格定义,所以实现中可能存在差异,这使得它们不兼容,尤其是在处理日期等复杂类型时。例如,非常流行的Json.NET国际标准化组织 ( ISO )格式表示日期,而 ASP.NET MVC 旧版本中使用的 JSON 序列化程序将日期表示为自 Unix 纪元以来的毫秒数,用 JavaScript 日期构造函数包装。

那个.NET 开发人员社区已经聚集在 Json.NET,兼容性总是比性能更好。ASP.NET Web API 已经用 Json.NET 做默认有一段时间了,ASP.NET Core 也用 Json.NET。有一个序列化程序是名为服务堆栈的服务堆栈框架的一部分。文字,号称更快,但你可能更看重兼容性和文档而不是速度。这同样适用于其他 JSON 库,如 Jil(https://github.com/kevin-montrose/Jil)和NetJSON(https://github.com/rpgmaker/NetJSON),在基准测试中甚至可以比 ServiceStack 更快。

如果您追求纯性能,并且控制所有端点,那么您可能会想要使用二进制协议。但是,这可能会限制未来与您无法控制的第三方端点的互操作性。因此,最好只在内部使用这些。

在 UDP 之上构建自己的定制消息协议是一个坏主意。所以,如果你想使用二进制序列化,你应该看看像 protobuf-net 这样的东西,这是一个为. net 缓冲实现的协议,你也可能希望考虑微软的 Bond 框架(https://github.com/Microsoft/bond)或者亚马逊的 Ion(https://amzn.github.io/ion-docs/index.html)。您可能需要调整这些工具以获得最佳性能,例如,通过更改默认缓冲区大小。

SIMD 中央处理器指令

单指令多数据 ( SIMD )是一种在许多现代处理器上可用的技术,即使在一个内核上的单线程中,也可以通过并行化计算来加速执行。SIMD 利用 CPU 上可用的额外指令来操作值集(向量),而不仅仅是单个值(标量)。

最常见的指令集叫做流式 SIMD 扩展 2 ( SSE2 ),自奔腾 4 问世以来已经存在了 15 年多。一个新的指令集叫做高级向量扩展 ( AVX )提供了优于 SSE2 的性能,并且已经存在了五年多。因此,如果您使用的是相当新的 x86-64 CPU,那么您应该可以访问这些额外的指令。

Some ARM CPUs (such as those in the Raspberry Pi 2 and 3) contain a similar technology called NEON , officially known as Advanced SIMD . This is not currently supported in .NET, but may be in the future. An official open source library project in C is hosted at http://projectne10.org/ .

您可以使用以下布尔属性来测试是否支持 SIMD:

Vector.IsHardwareAccelerated 

该属性是 JIT 固有的,该值由 RyuJIT 在运行时设置。

您可以实例化泛型类型Vector或使用二维/三维/四维便利类之一。例如,要创建单精度浮点向量,可以使用以下通用代码:

var vectorf = new Vector<float>(11f); 

要创建单精度浮点三维向量,可以使用以下代码:

var vector3d = new Vector3(0f, 1f, 2f); 

A two-dimensional double precision floating point vector can be a good substitute for a Complex structure. It will give higher performance on hardware-accelerated systems. Vector2 only supports single precision floating point numbers, but you can use the generic type to specify the real and imaginary components of the complex number as a double. Complex only supports double precision floating point numbers but, if you don't need high precision, you could still use the Vector2 convenience class. Unfortunately, this means that it's not simply a drop-in replacement, but the math is different anyway.

您现在可以使用标准的向量数学,但是修改您的算法以利用向量可能很复杂,并且不是您通常应该在 web 应用中做的事情。它对于桌面应用可能很有用,但是,如果一个进程在 web 请求中花费了很长时间,通常最好在后台运行它,然后它不需要那么快。

我们将在下一章中介绍这种分布式体系结构方法。由于这个原因,我们不会对 SIMD 做更多的详细介绍,但是如果你愿意,你可以阅读更多关于它的内容,现在你已经尝到了它的味道。你可以在https://en.wikipedia.org/wiki/SIMD阅读一些背景信息,你可以找到的文件。在http://msdn.microsoft.com/en-us/library/dn858218的 MSDN 上 NET 实现。您还可以看一下示例控制台应用,它可以与本书一起下载,作为十的简单入门。

并行编程

虽然 SIMD 擅长提高在一个内核上运行的单线程的性能,但它不能在多个内核或处理器上工作,并且它的应用受到限制。现代扩展意味着增加更多的 CPU,而不是简单地让单线程更快。我们不只是想像 SIMD 那样并行化我们的数据;实际上,我们应该更加关注处理的并行化,因为这样可以更好地扩展。

有各种各样.NET 技术有助于并行处理,这样您就不必编写自己的线程代码。两个这样的并行扩展是并行 LINQ ( PLINQ ),它扩展了您熟悉的 LINQ 操作,以及任务并行库 ( 第三方物流)。

任务并行库

第三方物流的主要特征之一是扩展循环以并行运行。但是,您需要小心并行处理,因为它实际上会使您的软件在执行简单任务时变慢。编组多个任务所涉及的开销会使琐碎操作的工作负载的执行相形见绌。

例如,采取以下简单的for循环:

for (int i = 0; i < array.Length; i++) 
{ 
    sum += array[i]; 
} 

前面的for循环中的数组包含 100,000 个整数,但是增加整数是 CPU 可以做的最简单的事情之一,在数组上使用for循环是一种非常快速的枚举方式。在一台相当现代化的机器上,这种积累将在不到十分之一毫秒的时间内完成。

您可能认为您可以通过并行化操作来加快速度。也许你可以拆分数组,在两个内核上并行运行求和,然后将结果相加。

您可以使用以下代码来尝试:

Parallel.For(0, array.Length, i => 
{ 
    Interlocked.Add(ref sum, array[i]); 
}); 

You must use an interlocked add or you will get an incorrect summation result. If you don't, the threads will interfere with each other, corrupting the data when writing to the same location in memory.

然而,这段代码实际上比第一个例子慢了 42 倍。在这种情况下,额外的开销、运行许多线程的复杂性以及锁定变量以便一次只有一个线程可以向其写入的做法都是不值得的。

并行化对于更复杂的进程非常有用,尤其是如果循环体执行一些缓慢的操作,例如访问文件系统。但是,使用异步访问可以更好地处理阻塞输入/输出操作。并行访问可能会导致争用,因为访问最终可能不得不在某个时间点(例如,在硬件级别)串行执行。

如果我们想执行一个处理器更密集的操作,比如散列多个密码,那么并行运行这些任务可能是有益的。下面的代码对列表中的每个密码执行 PBKDF2 哈希,然后计算结果的 Base64 表示形式:

foreach (var password in passwords) 
{ 
    var pbkdf2 = new Rfc2898DeriveBytes(password, 256, 10000); 
    Convert.ToBase64String(pbkdf2.GetBytes(256)); 
} 

我们没有使用这个例子中的输出,但是您可以通过将数据库中的密码迁移到一个更有弹性的密钥扩展算法来升级密码的安全性。输入可以是明文密码或传统单向散列函数的输出,例如 MD5 或未加密的 SHA。

我们可以通过使用Parallel.ForEach循环来提高多核系统的速度,使用如下代码:

Parallel.ForEach(passwords, password => 
{ 
    var pbkdf2 = new Rfc2898DeriveBytes(password, 256, 10000); 
    Convert.ToBase64String(pbkdf2.GetBytes(256)); 
}); 

这将加快这一过程,但加快多少将取决于许多因素,如列表中的密码数量、逻辑处理器数量和中央处理器内核数量。例如,在具有两个内核但四个逻辑处理器的 Core i5 CPU 上,列表中只有两个密码不会带来巨大的改进(仅快 1.5 倍)。列表中有四个(或更多)密码时,改进更好(大约快 1.9 倍)。还是有一些开销的,所以用两倍的 CPU 内核是无法得到双倍的速度的。

我们可以通过查看基准测试期间任务管理器中的 CPU 利用率来了解这种差异的原因。

只有两个密码需要散列,CPU 图如下所示:

在上图中,我们可以看到,最初,当串联散列时,中央处理器以大约 25%的速度运行,完全使用一个逻辑中央处理器。当并行散列两个密码时,它使用 50%,运行在两个逻辑处理器上。由于固有的开销和超线程的特性,这并没有转化为速度的两倍增长。

Hyper-threading is a technology that typically exposes two logical processors to the OS for each physical core. However, the two logical CPUs still share the execution resources of their single core.

虽然 CPU 芯片上有两个内核,但是超线程会将四个逻辑 CPU 暴露给操作系统。因为我们只有两个线程,因为我们在散列两个密码,所以我们只能使用两个处理器。如果线程在不同的内核上执行,那么速度提升会很好。但是如果它们在共享同一个内核的处理器上执行,那么性能就不会那么令人印象深刻。由于调度的改进,它仍然比单线程散列要好,这也是超线程的设计目的。

当我们同时散列四个密码时,CPU 图如下所示:

我们可以看到,现在最初的 25%使用率跃升至几乎完全利用率,我们正在利用大部分处理器。这意味着与按顺序散列相比,性能几乎提高了一倍。仍然有大量的开销,但是,由于主操作现在要快得多,这种权衡是值得的。

平行 LINQ

还有其他方法可以利用并行编程,例如 LINQ 表达式。我们可以将前面的例子重写为 LINQ 表达式,它可能如下所示:

passwords.AsParallel().ForAll(p => 
{ 
    var pbkdf2 = new Rfc2898DeriveBytes(p, 256, 10000); 
    Convert.ToBase64String(pbkdf2.GetBytes(256)); 
}); 

您可以使用AsParallel()方法启用这些功能。ForAll()方法的目的与前面示例中的循环相同,如果顺序不重要,则该方法很有用。如果订购很重要,那么有一种AsOrdered()方法可以帮助解决这个问题。然而,这可能会由于涉及额外的处理而降低性能增益。

这个例子的执行类似于上一个使用并行循环的例子,这并不奇怪。我们也可以限制可以并行发生的操作数量,使用WithDegreeOfParallelism()方法如下:

passwords.AsParallel().WithDegreeOfParallelism(2).ForAll(p => 
{ 
    var pbkdf2 = new Rfc2898DeriveBytes(p, 256, 10000); 
    Convert.ToBase64String(pbkdf2.GetBytes(256)); 
}); 

前面的示例将哈希限制为一次两个,并且执行类似于列表中只有两个密码的情况,这是意料之中的。如果您不想最大化 CPU,这可能会很有用,因为还有其他重要的进程在其上运行。

通过在ParallelOptions类的实例上设置MaxDegreeOfParallelism属性,您可以使用第三方物流实现相同的效果。然后,该对象可以作为参数与主体一起传递到循环方法的重载版本中。

It's important, when you're using parallel LINQ to query datasets, that you don't lose sight of the best place for the query to be performed. You may speed up a query in the application with parallelization, but the best place for the query to occur may still be in the database, which can be even faster. To read more on this topic, refer back to Chapter 7 , Optimizing I/O Performance , and Chapter 5 , Fixing Common Performance Problems .

并行基准测试

让我们来看看一个简单的.NET Core 控制台应用,它对我们刚刚讨论的技术进行了基准测试。它显示了并行化没有帮助的一种情况,实际上使事情变得更糟。

然后,它显示了另一种情况,它确实有所帮助。

当通过累加 0 到 10 之间的 100,000 个随机整数来计算总和时,最快的方法是在简单的foreach循环中使用数组。在这里使用并行化会使过程慢得多,如果天真地使用,没有锁定,会给出不正确的结果,这要糟糕得多。

当执行计算量更大的工作时,例如对多个密码执行 PBKDF2 哈希函数,并行化会有很大帮助。时间几乎减半,因为它运行在两个内核上。限制线程数量的最终操作可能会在不同的运行中花费不同的时间。这可能是由于线程有时共享一个内核,有时在不同的内核上运行。根据工作情况,它几乎可以像使用所有逻辑内核一样快。

基准测试的 CPU 图如下所示:

最初的并行求和最大化了中央处理器,效率非常低。接下来,单线程哈希仅使用一个逻辑处理器(25%),但另一个后来的哈希几乎充分利用了两个内核。最终的散列法,一次限于两个密码,只利用了一半的 CPU 能力。

并行编程限制

web 应用的性能问题通常并不意味着单独提高系统中单个用户的速度。当只有一个用户时,让一个网络应用运行起来很容易,但挑战在于随着用户数量的增加和你的请求越来越多,保持单个用户的性能。

本节讨论的并行编程技术在扩展 web 应用方面的用途有限。您已经有了一个并行化的系统,因为每个用户请求都将被分配自己的资源。您可以简单地添加更多实例、代理或机器来满足需求。问题在于如何在他们之间有效地分配工作,避免共享资源的瓶颈。我们将在下一章更深入地讨论这个问题。

多线程和并发

在前面关于并行编程的部分中,我们简要介绍了使用Interlocked.Add来避免从多个线程向内存中的同一位置写入时出错。当涉及多线程编程时,并发和锁定是最难处理的事情。出错会损害性能,甚至导致不正确的计算。

由于锁定是一个经常出错的领域,我们将包括一个简短的入门。这绝不是广泛的,但将帮助你确定你可能需要小心的领域。

锁定是一种确保资源一次只能由一个线程使用的方式。每个希望使用锁定项的线程都必须等待轮到它,以便操作按顺序执行。这一系列处理确保了一致性,但如果有大量的锁等待释放,也会降低系统速度。

锁定也适用于数据库,您可能在 SQL 中使用过它,但是在这里我们将结合. NET 中的 C#和多线程来介绍它。有许多低级构造和原语可以用于锁定。但是,我们将采用更实用的方法,并展示中常用的高级锁定语法.NET 编程。

在 C#中实现锁定的标准方式是使用lock语句。这类似于using语句,因为它是包装底层实现的语法糖。它帮助您避免忘记实现所有必要的样板代码,类似于using语句如何确保对象的处置。很容易忘记异常处理的要求,导致内存泄漏或死锁。

A deadlock is where you make a mistake in your locking implementation and create a situation where nothing can acquire or release a lock. This can cause your program to get stuck in an infinite loop and hang.

让我们检查一个简单的锁定示例。考虑以下简单的方法。

private static void DoWork(ref int result)

{

    for (int i = 0; i < 1000000; i++)

    {

        result++;

    }

}

我们在不同的线程上调用这个方法两次,并等待它们都完成,使用下面的代码。

var result = 0;

var t1 = Task.Run(() => { DoWork(ref result); });

var t2 = Task.Run(() => { DoWork(ref result); });

Task.WaitAll(t1, t2);

在这之后result变量会是什么?应该不会是两百万。在没有锁定的情况下这样做将导致损坏和错误的结果。

We have used Task.WaitAll(t1, t2) here so that the code can run in the Main method of a console app. However, it is generally preferable to use await Task.WhenAll(t1, t2) if within an async method. In much the same way, await Task.Delay(100) is preferable to Thread.Sleep(100) . In C# 7.1 you can have an async Main method for a console application, so this is not a concern.

要使用一个lock,首先我们需要声明并初始化一个对象供其使用。我们已经在类级别上做到了这一点,并这样定义了它。

private static object l = new object();

我们现在可以使用这个对象来lock一个操作,但是我们应该在哪里执行这个锁来获得最好的性能呢?让我们在循环中尝试以下代码。

for (int i = 0; i < 1000000; i++)

{

    lock (l)

    {

        result++;

    }

}

这执行正确,我们得到了正确的结果,但它是缓慢的。在这个例子中,更好的方法是锁定在循环之外。下面的代码显示了这个改进的版本。

lock (l)

{

    for (int i = 0; i < 1000000; i++)

    {

        result++;

    }

}

这执行得非常快,因为我们只锁定了两次,而不是两百万次。这是一个极端的例子,对于一个真实的工作负载,应该有一个调优练习来找到最佳的锁定策略。

在引擎盖下,一个lock语句在内部使用一个Monitor来实现锁定。你通常不需要担心这个,除非你有特殊要求。使用lock比直接使用Monitor更简单安全。还有Mutex(用于跨进程)和Semaphore(锁定资源池而不是单个对象)。

之前使用的Interlocked.Add包装了一个特殊的 CPU 指令。这允许在处理器级原子地改变一个值。如果不使用它,那么相同值的不同字节可能会被不同的线程改变,从而破坏结果。在我们前面的例子中,使用Interlocked.Increment(ref result)会更快。然而,我们的代码只是模拟一个复杂得多的工作负载,这是不适用的,使用Interlocked不是一个合适的解决方案。

有多个线程时不锁定是需要避免的,就像在错误的地方锁定一样。接下来让我们看看其他一些明智的避免的事情。

避免的做法

我们已经展示了一些加速软件开发的方法,但是更好的方法通常是说明什么不应该做,以及事情如何会出错。如果没有不良实践,Web 应用通常会表现良好,这里我们将强调一些您应该注意的事情。

反射

反射是用其他代码以编程方式检查您的代码,并在运行时挖掘其内部的过程。例如,您可以在加载程序集时检查它,以查看它实现了哪些类和接口,以便您可以调用它们。一般不鼓励这样做,如果可能的话应该避免。通常有其他不需要反射的方法来达到同样的结果,尽管偶尔有用。

反射通常不利于性能,这是有据可查的,但是,像往常一样,这取决于您使用它的目的。新的是,反思有了重大变化.NET Core。该应用编程接口已经改变,现在是可选的。所以,如果不使用反射,就不用支付性能罚金。

现在在反射 API 上有一个额外的方法,所以,尽管以前您会调用类似myObject.GetType().GetMembers()的东西,但是现在您需要通过插入新的GetTypeInfo()方法来将其称为myObject.GetType().GetTypeInfo().GetMembers(),该方法位于System.Reflection命名空间中。

如果必须使用反射,那么最好不要重复执行或以紧密循环的方式执行。然而,在应用启动期间使用它一次是很好的。但是,如果您可以完全避免使用它,您可以从中的一些新改进中受益.NET Core。

正则表达式

一个正则表达式 ( 正则表达式)可能非常有用,但是可能表现不佳,并且通常被误用在另一个解决方案会更好的情况下。例如,当有更可靠的方法时,正则表达式通常用于电子邮件验证。

如果重复重用一个正则表达式,最好通过在构造函数中指定RegexOptions.Compiled选项来编译它以提高性能。这只有在您经常使用正则表达式并且这样做涉及到初始性能损失的情况下才有帮助。所以,确保你检查是否真的有改进,并且现在没有变慢。

RegexOptions.IgnoreCase选项也可以影响性能,但它实际上可能会减慢速度,因此请始终测试您的输入。编译对此也有影响,您可能希望使用RegexOptions.CultureInvariant来避免比较问题。

小心不要将用户输入信任给正则表达式。有可能让他们执行大量的回溯并使用过多的资源。您不应该允许对正则表达式进行不受约束的输入,因为它们可以运行几个小时。

Regexes 通常用于电子邮件地址验证,但这通常是一个坏主意。完全验证电子邮件地址的唯一方法是向它发送电子邮件。然后,您可以让用户单击电子邮件中的链接,以表明他们有权访问该邮箱并已收到该邮箱。电子邮件地址可能与人们经常接触的普通地址有很大不同,随着新的顶级域名的加入,这一点更加真实。

许多在线电子邮件地址验证的正则表达式会拒绝完全有效的电子邮件地址。如果你想帮助用户,并在表单上执行一些基本的电子邮件地址验证,那么你所能做的就是明智地检查其中是否有一个@符号(以及其后的一个.,以便电子邮件处于表单x@y.z中。您可以通过简单的字符串测试做到这一点,并避免正则表达式的性能损失和安全风险。

紧密循环中的字符串连接

由于字符串是不可变的,不能更改,当您连接一个字符串时,会创建一个新的对象。如果在一个紧密的循环中大量使用内存,这可能会导致性能问题和内存使用问题。

您可能会发现使用字符串生成器或其他方法更好。然而,不要对此过于担心,因为它只适用于大规模。总是测试看看它是否真的是一个问题,不要在不需要的地方进行微优化。

这是一个很好的建议,找出你的代码大部分时间花在哪里,并把你的优化集中在那里。显然,优化在一个循环中执行数百万次的代码要比只偶尔运行的代码好得多。

动态打字

C#是一种静态类型的语言,变量类型在编译时被检查,但是它确实有一些动态特性。您可以使用dynamic类型和对象,如ExpandObject来获得动态类型语言的一些特征。var类型实际上不是动态的,只是在编译时推断出来的。

动态打字有性能和安全方面的损失,所以如果你能找到另一种方法来解决你的问题,最好避免。例如,ASP.NET MVC 中的ViewBag是动态的,所以最好不要使用ViewBag,而是使用定义良好的视图模型。除了性能之外,这还有许多其他好处,例如安全和方便。

同步操作

同步方法会阻止执行,如果可能的话应该避免,尤其是当它们很慢或者访问输入输出的时候。了解如何使用 async 对于现代高性能编程非常重要,新的语言特性使它比以往任何时候都更容易访问。如果有async方法,一般应该优先使用同步阻塞版本。

asyncawait关键词使得异步编程比过去容易得多,但是,正如第 5 章修复常见性能问题中所述,对 web 应用的影响对于一个单独的用户来说并不总是可见的。这些方便的功能允许您在等待操作完成的同时,通过在停机期间将线程返回到池中来同时服务更多的用户。然后,线程可以用来服务其他用户的请求,这允许您用更少的服务器来处理更多的用户。

异步方法可能很有用,但是最大的好处不是来自于编写异步代码,而是来自于拥有异步架构。我们将在下一章讨论消息队列时讨论分布式体系结构。

例外

顾名思义,例外应保留给特殊情况。异常既慢又贵,如果知道某个事件可能发生,就不应该在业务逻辑中用作流控制。

这并不是说你不应该像应该的那样使用异常处理。然而,它应该保留给真正意想不到和罕见的事件。如果你能提前预测到某个情况可能会发生,那么你就应该明确地处理它。

例如,磁盘变满,而您的代码因为没有空间而无法写入文件,这是一种例外情况。你不会期望这种情况正常发生,你可以只try文件操作和catch任何异常。但是,如果您试图解析日期字符串或访问字典,那么您可能应该使用特殊的TryParse()TryGetValue()方法并检查空值,而不是仅仅依赖异常处理。

摘要

在本章中,我们讨论了一些可以提高代码执行性能的技术,并深入研究了组成的项目。网芯和 ASP.NET 芯。我们探索了数据结构、序列化、散列、并行编程,以及如何进行基准测试来衡量相对性能。我们还介绍了如何用 C#执行多线程、并发和锁定。

线性性能特征更容易扩展,当负载增加时,没有表现出这种行为的代码可能会变慢。具有指数级性能特征或具有不稳定异常值(这种异常值很少出现,但出现时非常慢)的代码会导致性能问题。通常更好的做法是,代码虽然在正常情况下稍慢,但更具可预测性,并且在大范围的负载下表现一致。

这里的主要教训是不要盲目应用并行编程和其他潜在的性能增强技术。总是测试以确保它们产生积极的影响,因为它们很容易使事情变得更糟。我们的目标是一切都很棒的情况,但是,如果我们不小心,我们可能会误把一切都搞得很糟糕。

在下一章中,您将学习缓存和消息队列。这是两种可以显著提高系统性能的高级技术。

九、学习缓存和消息队列

缓存非常有用,几乎可以应用于应用堆栈的所有层。然而,很难总是让缓存正常工作。因此,在本章中,我们将讨论 web、应用和数据库级别的缓存。我们将向您展示如何使用反向代理服务器来存储呈现的网页和其他资产的结果。我们还将介绍较低级别的缓存,使用内存中的数据存储来加速访问。如果需要强制传播更新,您将学习如何确保总是能够刷新(或破坏)缓存。

本章还介绍了使用消息队列和抽象的异步架构设计,它们封装了各种消息模式。您将学习如何在后台执行长时间运行的操作(如视频编码),同时让用户了解其进度。

您将学习如何应用缓存和消息队列软件设计模式来降低操作的执行速度,从而不必实时执行操作。您还将了解这些模式可能增加的复杂性,并了解其中的权衡。我们将在第 10 章性能增强工具中看到如何克服这些复杂性并减轻其负面影响。

本章涵盖的主题包括以下内容:

  • Web 缓存背景
  • JavaScript 服务人员
  • 清漆代理和 IIS 网络服务器
  • Redis 和 Memcached 内存中应用缓存
  • 消息队列和消息模式
  • RabbitMQ 及其各种客户端库

为什么缓存很难

缓存并不难,因为缓存某些东西很难。缓存是无限容易的;当您想要进行更新时,困难的部分是使缓存无效。网景公司已故的菲尔·卡尔顿有一句话用得很好,内容如下:

"There are only two hard things in Computer Science: cache invalidation and naming things."

它也有许多幽默的变体,正如之前在本书中使用的那样。这种情绪可能有点夸张,但它凸显了从你的“T2”速食盒 2.0“T3”中移除你的“T0”成品电脑“T1”是多么复杂。不过,给事物命名确实很难。

缓存是存储一些数据的临时快照的过程。然后可以使用这个临时缓存,而不是每次需要时都重新生成原始数据(或从规范源中检索)。这样做有明显的性能优势,但它会使您的系统更加复杂,更难概念化。当您有许多缓存交互时,结果可能看起来几乎是随机的,除非您在方法上有纪律。

当您在推理缓存(和消息队列)时,消除数据只以单一一致状态存在的想法是有帮助的。如果你接受数据有新鲜感并且总是有一定数量的陈旧的概念,那就更容易了。事实上,情况总是如此,但是一个小系统所涉及的短时间框架意味着你通常可以忽略它,以简化你的思维。然而,当涉及到缓存时,时间尺度更长,所以新鲜度更重要。一个大规模的系统最终只能是一致的,它的各个部分将有不同的数据时间视图。你需要接受数据是可以运动的;否则,你只是没有四维思维!

举个简单的例子,考虑一个传统的静态网站。访问者在他们的浏览器中加载一个页面,但是这个页面现在立即过期了。服务器上的页面本可以在访问者检索后立即更新,但他们不会知道,因为旧版本将保留在他们的浏览器中,直到他们刷新页面。

如果我们将这个例子扩展到一个数据库支持的 web 应用,比如 ASP.NET 或 WordPress 网站,那么同样的原则也适用。用户检索从数据库中的数据生成的网页,但它可能在加载后就过期了。基础数据可能已经更改,但包含旧数据的页面仍保留在浏览器中。

默认情况下,网络应用通常会在每次页面加载时从数据库中重新生成 HTML,但是如果数据没有改变,这将会非常低效。这样做只是为了在进行更改时,在页面刷新时立即显示出来。

但是,用户的浏览器中可能有一个旧页面,而您对此的控制有限。因此,您也可以将该页面缓存在服务器上,并且只在数据库中的底层数据发生变化时删除它。像这样缓存呈现的 HTML 通常对于在不仅仅是少数用户的规模上保持性能至关重要。

Web 缓存

我们将讨论的第一类缓存是在 web 级别。这包括存储您的网络堆栈的最终输出,因为它将被发送给用户,这样,当再次请求时,它就准备好了,不需要重新生成。此阶段的缓存消除了在应用层进行昂贵的数据库查找和 CPU 密集型渲染的需要。这减少了延迟并减少了服务器上的工作负载,使您能够处理更多用户并快速为每个用户提供服务。

Web 缓存通常发生在您的 web 服务器或反向代理服务器上,您已经将它们放在 web 服务器的前面,以保护它们免受过度负载的影响。您也可以选择将此任务交给第三方,如 CDN。在这里,我们将介绍两个网络服务器和代理服务器软件,IIS 和清漆。然而,更多的网络缓存和负载平衡技术是可用的,例如,NGINX 或 HAProxy。

web 层的缓存最适合静态资产和资源,如 JavaScript、CSS 和图像。然而,它也可以用于匿名的 HTML,这是很少更新,但经常访问,如主页或登陆页面,这是未经认证的,不是为用户定制的。

我们在第 5 章中讨论了代理服务器,在第 6 章中讨论了常见的性能问题,并稍微介绍了 web 层缓存。然而,在本章中,我们将更详细地讨论 web 缓存。

缓存背景

在我们深入研究实现细节之前,了解一下缓存在 web 上是如何工作的会有所帮助。如果您花时间研究工作中的机制,那么缓存将不会那么令人困惑,也不会像您刚刚直接跳入一样令人沮丧。

阅读和理解相关的 HTTP 规范是有帮助的。然而,不要假设软件总是严格遵守这些网络标准,即使它声称如此。

首先,让我们来看看一个典型的网络设置,您可能会通过您的 HTTP 流量。下图说明了 web 应用的常见配置示例:

如上图所示,笔记本电脑和平板电脑用户通过缓存转发代理服务器(可能位于公司网络或互联网服务提供商)进行连接。移动用户通过互联网直接连接。但是,所有用户在到达您的基础架构之前都要经过一个 CDN。

在防火墙(未显示)之后,有一个设备可以终止 TLS 连接,平衡 web 服务器之间的负载,并充当缓存反向代理。这些功能通常由不同的设备执行,但我们在这里保持简单。

您的资源副本将保存在您的网络服务器、您的反向代理、您的 CDN、任何正向代理以及所有用户设备的浏览器中。控制这些资源的缓存行为的最简单方法是使用带内信令,并向您的内容添加 HTTP 头,只在一个地方声明缓存控制元数据。

将相同的标准 HTTP 缓存技术应用于您自己的 web 服务器和代理是一个很好的实践,即使您可以随意定制它们并刷新它们的缓存。这不仅减少了您必须做的配置量,避免了重复的工作,而且还确保了任何不受您控制的缓存也应该正确运行。即使使用 HTTPS,浏览器仍会执行缓存,并且可能会有透明的公司代理或干涉的 ISP 专属门户。

HTTP 头

HTTP 缓存包括在响应中设置缓存控制头。有许多这样的报头,这些报头是多年来从不同的标准和协议的不同版本中添加的。您应该知道这些是如何使用的,但是您也应该了解如何确定可缓存资源的唯一性,例如,通过更改网址或仅更改其中的一部分,如查询字符串参数。

这些头中的许多可以根据它们引入的函数和 HTTP 版本进行分类。有些头有多种功能,有些是非标准的,但几乎普遍使用。我们不会涵盖所有这些标题,但我们会挑选一些最重要的标题。

大体上有两种类型的缓存头类别。第一个定义了一个绝对时间,在此时间内可以重用缓存,而无需与服务器进行检查。第二个定义了规则,客户端可以使用这些规则来测试服务器缓存是否仍然有效。

大多数指令头(发出缓存命令的那些)都属于这两个头类别之一。除此之外,还有许多纯粹的信息头,它们提供了有关原始连接和客户端的详细信息,否则这些信息可能会被缓存所掩盖(例如,原始客户端的 IP 地址)。

有些头文件,如Cache-Control,是最新标准的一部分,但另一些头文件,如Expires,通常只用于向后兼容,以防有一个古老的浏览器或旧的代理服务器挡道。然而,随着基础设施和软件的升级,这种做法变得越来越没有必要。

The latest caching standard in this case is HTTP/1.1, as HTTP/2 uses the same caching directives (RFC 7234). Some headers date from HTTP/1.0, which is considered a legacy protocol. Very old software may only support HTTP/1.0. Standards may not be implemented correctly in all applications. It is a sensible idea to test that any observed behavior is as expected.

Age头用于指示资源在缓存中的时间(以秒为单位)。另一方面,ETag头用于指定单个对象的标识符或该对象的特定唯一版本。

Cache-Control头告诉缓存资源是否可以被缓存。它可以有许多值,包括max-age(以秒为单位)或no-cacheno-store指令。no-cacheno-store之间令人困惑但又微妙的区别在于,no-cache表示客户端应该在使用资源之前与服务器进行检查,而no-store表示根本不应该缓存资源。为了防止缓存,一般应该使用no-store

ASP.NET CoreResponseCache动作属性设置Cache-Control标题,包含在第 6 章寻址网络性能中。但是,此标头可能会被一些较旧的缓存忽略。PragmaExpires是用于向后兼容的旧标题,它们执行一些与Cache-Control标题现在处理的功能相同的功能。

X-Forwarded-*报头用于提供更多关于到代理或负载平衡器的原始连接的信息。这些都是非标准的,但被广泛使用,并被标准化为组合的Forwarded表头( RFC 7239 )。Via头也提供了一些代理信息,Front-End-Https是非标微软头,类似于X-Forwarded-Proto。这些协议头有助于告诉您,当负载平衡器剥离原始连接时,该连接是否使用了 HTTPS。

If you are terminating the TLS connections at a load balancer or proxy server and are also redirecting users to HTTPS at the application level, then it is important to check the Forwarded headers. You can get stuck in an infinite redirection loop if your web servers desire HTTPS but only receive HTTP from the load balancer. Ideally, you should check all varieties of the headers, but if you control the proxy, you can decide what headers to use.

缓存中涉及到许多不同的 HTTP 头。下面的列表包括我们在这里没有涉及到的内容。大量的头应该让您了解缓存有多复杂:

  • If-Match
  • If-Modified-Since
  • If-None-Match
  • If-Range
  • If-Unmodified-Since
  • Last-Modified
  • Max-Forwards
  • Proxy-Authorization
  • Vary

缓存破坏

缓存破坏(也称为缓存爆发、缓存刷新或缓存无效)是缓存的难点。将一个项目放入缓存很容易,但是如果你没有提前制定策略来管理不可避免的变化,那么你可能会失败。

对于 web 级缓存来说,纠正缓存破坏通常更为重要。这是因为,使用服务器端缓存(我们将在本章后面讨论),您可以完全控制,并且如果出错可以重置。网络上的一个错误可能会持续存在,并且很难补救。

除了设置正确的标题之外,当资源的内容发生变化时,改变资源的网址也是有帮助的。这可以通过添加时间戳来完成,但通常,一个方便的解决方案是使用资源内容的散列并将其作为参数追加。包括 ASP.NET Core 在内的许多框架都使用这种方法。例如,考虑网页中的以下 JavaScript 标记:

<script src="js/site.js"></script> 

如果您对site.js进行更改,那么浏览器(或代理)不会知道它已经更改,可能会使用以前的版本。但是,如果输出更改为如下所示,它将重新请求:

<script src="js/site.js?v=EWaMeWsJBYWmL2g_KkgXZQ5nPe-a3Ichp0LEgzXczKo"> 
</script> 

这里的v(版本)参数是 Base64 URL 编码的,SHA-256 哈希内容的site.js文件。由于雪崩效应,对文件做一个小的改变将从根本上改变散列。

Base64 URL encoding is a variant of standard Base64 encoding. It uses different non-alphanumeric characters (+ becomes - while / changes to _ ) and percent encodes the = character (which is also made optional). Using this safe alphabet (from RFC 4648) makes the output suitable for use in URLs and filenames.

在 ASP.NET Core 中,您可以通过在剃刀视图中添加值为trueasp-append-version属性来轻松使用该功能,如下所示:

<script src="~/js/site.js" asp-append-version="true"></script> 

服务人员

如果您正在编写客户端 web 应用,而不是简单的动态网站,那么您可能希望使用新的浏览器功能对缓存进行更多控制。您可以通过用 JavaScript 编写缓存控制指令来做到这一点(技术上是 ECMAScript )。当访问者离线使用你的网络应用时,这给了你更多的选择。

一个服务工作者给你比以前的 AppCache API 更大的控制权。它还为移动网络应用安装横幅(提示用户将你的网络应用添加到他们的主屏幕)等功能打开了大门。然而,它仍然是一项相对较新的技术。

Service workers are a new experimental technology, and as such, are currently only supported in some recent browsers (partially in Chrome, Firefox, and Opera). You may prefer to use the previous deprecated AppCache method (which is almost universally supported) until adoption is more widespread. Information on current browser support is available at http://caniuse.com/#feat=serviceworkers and http://caniuse.com/#feat=offline-apps (for AppCache). A more detailed service worker breakdown is available at https://jakearchibald.github.io/isserviceworkerready/ .

一个服务工作者可以做很多有用的事情(比如后台同步和推送通知),但是从我们的角度来看,有趣的部分是可脚本化的缓存,它支持离线使用。它有效地充当浏览器中的代理服务器,除了允许在没有互联网连接的情况下进行交互(当然是在初始安装之后)之外,还可以用来提高 web 应用的性能。

There are other types of web workers apart from service workers (for example, audio workers, dedicated workers, and shared workers), but we won't go into these here. All web workers allow you to offload work to a background task so that you don't make the browser unresponsive (by blocking the main UI thread with your work).

服务人员是异步的,并且严重依赖于 JavaScript 承诺,我们假设您对此很熟悉。如果你不是,那么你应该仔细阅读它们,因为它们在涉及异步和并行脚本的许多其他环境中是有用的。

服务人员需要使用 HTTPS(在您的整个网站上使用顶级域名的另一个很好的理由)。但是localhost有个例外,你还是可以在本地开发。

服务人员示例

要安装一个服务人员,首先为其创建一个文件(通过 HTTPS 提供)。在下面的例子中,这个文件被称为service-worker.js。然后,在您的 HTML 页面上的一个<script>标签中(也在 HTTPS 提供),添加以下 JavaScript 代码:


    navigator.serviceWorker.register('service-worker.js', { 
        scope: '/' 
    }); 
} 

前面的代码片段首先检查是否支持服务工作人员,如果支持,它会注册您的工作人员。现在,您可以获取资源并将其添加到缓存中。一个有趣的性能增强用例是提前预取资源(用户可能需要的)并将其放入缓存中。范围是一个可选参数,在这种情况下并不是绝对必要的,因为文件位于域的根目录。我们展示它只是为了演示用法,但是如果文件位于子文件夹中,那么指定它可能会很有用。

在继续之前,您应该检查您的工作人员是否已经正确安装。在 Chrome 中,你可以打开特殊的网址 chrome://inspect/#service-workers 来查看任何活跃的服务人员。例如,在一个选项卡中打开https://instabail.uk/后,可以在另一个选项卡中打开服务人员检查器;您应该会看到如下截图:

您也可以访问 Chrome 中的 c hrome://serviceworker-internals 查看所有已注册的服务人员的状态,即使这些网站仍然没有打开。例如,即使在关闭https://instabail.uk/后,您也应该会继续看到类似以下截图的内容:

您可以通过单击取消注册按钮来删除服务人员。如果服务正在运行,您将使用“停止”和“检查”按钮代替“启动”。在未来版本的 Chrome 中,该页面可能会被移除或合并到检查器中。

现在,您可以开始将内容添加到您的服务人员 JavaScript 文件中。我们首先需要安装 worker 并缓存一些文件,这是通过事件侦听器完成的,如以下代码所示:

self.addEventListener('install', function (event) { 
    event.waitUntil( 
        caches.open('cache-v01').then(function (cache) { 
            return cache.addAll([ 
                '/', 
                '/Content/bootstrap.min.css' 
            ]); 
        }) 
    ); 
}); 

我们已经命名了我们的缓存cache-v01,并提供了一个资源数组来缓存。您可能会在这里有更多的条目,并在函数之外定义数组,但是为了清楚起见,我们在这里保持了简单。

Don't cache your homepage if it dynamically renders live content. You may also want to use cache-busting parameters for resources, as mentioned previously.

然后,我们可以添加一个fetch事件侦听器来执行缓存和获取资源的神奇功能:

self.addEventListener('fetch', function (event) { 
    event.respondWith( 
        caches.match(event.request) 
            .then(function (response) { 
                if (response) return response; 
                var myReq = event.request.clone(); 
                return fetch(myReq).then( 
                    function (response) { 
                        var myResp = response.clone(); 
                        caches.open('cache-v01') 
                            .then(function (cache) { 
                                cache.put(event.request, myResp); 
                            }); 
                        return response; 
                    } 
                ); 
            } 
        ) 
    ); 
}); 

首先,我们检查请求的资源是否在缓存中,如果在,我们返回这个。有了承诺,你就可以将then的功能链接在一起,并通过它们而失效。如果由于资源不在缓存中而导致缓存未命中,我们对服务器执行fetch以获取资源并返回。然后,我们通过将资源放在同一个缓存中,将它添加到其他资源中。我们克隆请求和响应,因为它们是流,只能使用一次。

The fetch function is the modern version of an XMLHttpRequest (XHR ) and is used to retrieve data over the network. You can't use a synchronous XHR inside of a service worker, as they're designed to be asynchronous.

您可以使用浏览器开发工具( F12 )更详细地检查您的服务人员和缓存。在“资源”选项卡上,选择“服务人员”,您将看到如下截图:

如果选择缓存存储,您将看到缓存的内容,如下所示:

您可以通过右键单击来刷新缓存并删除项目。缓存存储上方的应用缓存将显示不推荐使用的应用缓存资源。当您四处导航时,您的站点页面将被添加到缓存中(这些页面应该适合缓存,因为如果使用我们的演示代码,将不会再次从服务器请求它们)。

此后,刷新缓存视图后,您应该会看到列出了更多条目,可能类似于下面的截图:

您可以看到缓存条目是按字母顺序列出的,而不是按照它们被添加到缓存中的顺序。这些页面现在将成为快照,在检索它们的时间点进行修复。这可能不是您想要的功能!

为了简单起见,我们在这里构建的服务人员是一个微不足道的例子,您可能希望通过添加一个catch语句来扩展它,以至少处理网络fetch失败的情况。例如,您可以替代先前缓存的离线回退页面。您还应该检查您没有缓存来自服务器的错误页面,因此测试响应状态代码。

您还需要仔细考虑缓存失效策略。服务人员会给你构建这个的工具,因为他们不会像 HTML5 AppCache 那样做太多假设。例如,您现在可以通过编程方式从缓存中删除条目。

我们将把它留在这里,用于客户端脚本控制的缓存,但是您可能希望更详细地了解这一点,尤其是在规范稳定下来并且浏览器支持更加广泛之后。JavaScript 中现在有许多其他新特性,这使得像这样的异步编程比以前更容易。例如,箭头函数,它类似于 C#中的 LINQλ表达式。

记得衡量你的绩效。如果您不使用服务人员的功能,他们可能会减慢速度,因为他们会给流程增加额外的步骤。

Web 和代理服务器

从服务器的角度来看,缓存与浏览器中的客户端缓存密切相关。除了在服务器上存储资源之外,您设置的头还将用于控制任何地方的缓存。

代理服务器和浏览器都使用您设置的 HTTP 头,不仅包括标准浏览,还包括从服务工作人员处获取。例如,如果Cache-Control头指定了no-store,那么您将无法从您的工作人员那里将资源添加到缓存中。

(同 ImmigrationInspectors 移民检查)

互联网信息服务 ( IIS )是微软的网络服务器。它可以用来提供来自 ASP.NET 应用的内容,或者作为代理服务器,以及许多其他东西,如 FTP。IIS 支持输出缓存,带有OutputCache动作属性。您也可以使用ResponseCache设置正确的报头,如第 6 章、寻址网络性能所述。

ASP.NET Core 2.0 现在支持在 Windows 上使用 IIS 时从 Visual Studio 2017 进行调试。您可以附加到流程中,并像在传统的 ASP.NET 项目中一样逐步执行代码。如果您计划在生产中使用 IIS 的完整版本,那么在该版本上进行测试是一个好主意。用红隼或 IIS Express 进行测试只能进行到此为止,您应该尽早将目标定为尽可能像真人一样。

IIS 也可以用作代理,例如,在单台机器上的红隼网络服务器前面。但是,当缓存多个网络服务器时,您最好使用专用的代理服务器软件,如清漆。

光泽面

清漆是一个免费的反向代理服务器,运行在类似 Unix 的操作系统上,如 LinuxFreeBSD 。你可以用你的包管理器(例如aptyum)安装它,或者用 DevOps 软件配置代理服务器,如厨师木偶。要配置清漆,您可以使用名为清漆配置语言 ( VCL )的特定领域语言 ( DSL )。

You can read more about Varnish at http://varnish-cache.org/ .

如果您正确使用了 HTTP 缓存头,就不需要过多地配置清漆。但是,您也可以使用自定义的 HTTP PURGE方法从缓存中删除条目,这也适用于 Squid 代理软件。如果清漆没有正确配置,您可能偶尔会看到一个神秘的大师冥想错误,但是您应该能够在清漆日志中找到问题。这可能表明没有健康的网络服务器可用。

清漆配置超出了本书的范围,但它在清漆网站上有很好的记录。如果你不想运行自己的代理服务器,那么你可以使用 CDN。除了使用 CDN 之外,您可能还想要自己的代理,因为大型 CDN 具有许多存在点 ( PoP ),可能会通过每个 PoP 请求相同的资源,而不会在它们之间共享资产。如果你为带宽付出很多,这可能是一个问题,尽管一些 cdn 有一个功能(通常称为原点屏蔽)可以帮助解决这个问题。

使用内容交付网络

CDN 通常以两种方式使用,一种是作为卸载内容的代理,另一种是作为公共第三方库和框架的托管提供商。对于第一种使用情况,您可以使用动态 CDN 服务,例如 CloudFlare 或 Akamai,但是第二种情况(使用谷歌或微软的静态 CDN)更常见,这就是我们将在这里介绍的内容。

尽管随着 HTTP/2 的采用,为您的库(如 jQuery 和 Twitter bootstrap)使用 CDN 变得不那么有用,但它仍然有助于降低您的托管成本。此外,如果您使用流行的 CDN 和库,那么用户可能已经拥有了一些所需资产的副本。例如,如果用户去过另一个使用来自谷歌 CDN 的 jQuery 的网站,那么它可能已经在他们的浏览器缓存中了。

无论您需要从 CDN 获得什么文件,都必须有一个后备副本。这比以往任何时候都更容易与剃刀视图引擎支持内置于 ASP.NET Core。

下面的代码展示了对于非开发环境,jQuery 是如何包含在默认的 MVC Razor 布局中的。CDN 和本地版本都与测试一起指定:

<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"

        asp-fallback-src="~/lib/jquery/dist/jquery.min.js"

        asp-fallback-test="window.jQuery"

        crossorigin="anonymous"

        integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">

</script>

前面的代码片段不仅呈现了微软 CDN 的标准<script>标记,还在之后添加了以下内联 JavaScript,如果 CDN 加载失败,则包括本地版本:

(window.jQuery||document.write("\u003Cscript src=\u0022\/lib\/jquery\/dist\/jquery.min.js\u0022 crossorigin=\u0022anonymous\u0022 integrity=\u0022sha384-K\u002BctZQ\u002BLL8q6tP7I94W\u002BqzQsfRV2a\u002BAfHIi9k8z8l9ggpc8X\u002BYtst4yBo\/hH\u002B8Fk\u0022\u003E\u003C\/script\u003E"));

以前,您必须手动完成这项工作,通常是在您的 CDN 关闭时匆忙完成。这个新的助手也适用于其他脚本和 CSS 文件。更多例子请看默认模板中的_Layout.cshtml

It's important to use a secure HTTPS connection to CDN resources in order to avoid mixed content warnings or script loading errors in browsers. Most popular CDNs support HTTPS and for added safety you should use SRI hashes (as in the preceding examples). For additional information on CDNs, see Chapter 6 , Addressing Network Performance .

何时不缓存

在某些情况下,您不应该缓存页面,或者至少您需要非常小心地处理页面。根据一般经验,缓存授权页面的渲染输出是一个坏主意。换句话说,如果一个用户已经登录了你的网站,而你正在为他们提供定制的内容(这很容易敏感),那么你需要非常仔细地考虑缓存。

如果您不小心将一个用户的缓存内容提供给了另一个用户,那么充其量,这将是令人讨厌的,因为个性化将是不正确的。最坏的情况是,你可能会把私人信息暴露给错误的人,并有可能陷入法律麻烦。

这类似于一般规则,即如果您提供经过身份验证的内容,就不能普遍启用 CORS。它可以成功完成,但是您需要了解机制,以便配置它安全工作。

Cross-Origin Resource Sharing (CORS ) is generally used as a way of allowing JSON APIs to be accessed from JavaScript in a webpage on another domain. Without this, the browser will prevent communication between scripts that don't share the same origin.

对于缓存,您需要在 URL 中有一个不可猜测的唯一标识符。网络设备和 cdn 使用的一些动态缓存控制系统可以为此使用 cookies,但这超出了正常的基于 HTTP 的缓存控制。这类似于您可能需要负载平衡器上的粘性会话,因为您的应用不是为无状态而设计的。

对于经过身份验证的缓存,最好不要在 web 级别缓存,而是在应用级别及以下级别缓存。这允许您缓存较小的离散块,而不是整个页面,这可以提高可重用性。

应用层缓存

应用级(或层)缓存意味着将可重用数据临时存储在基础架构中,而不是主数据库中。这可能在您的应用或 web 服务器的内存中,但是对于多个服务器,这往往在分布式内存存储中,如 Memcached 或 Redis。

当然,您可以同时使用 web 服务器上的内存存储和集中式缓存。但是,如果您有多个 web 服务器,那么您将需要一种方法来同步缓存。您可以为此使用发布/订阅 ( 发布/订阅)消息,我们将在本章稍后介绍。

下图显示了一个简单的集中式缓存设置。在实际情况下,您可能会有多个集群缓存服务器:

web 服务器现在可以在转到数据库之前询问缓存他们需要的数据是否在那里。这减少了数据库的负载,并且通常更快,因为如果存在数据,数据将在内存中。如果缓存未命中,并且确实需要查询数据库,则可以将结果写入缓存,供其他服务器使用。

使用心得

Redis 是一种流行的内存存储,它也可以将数据持久存储到磁盘上。它在 Linux 上运行得最好,但是出于开发目的,在 Windows 上提供了一个版本。Redis 还支持发布/订阅消息,这对于缓存失效非常有用。你可以在 http://redis.io/阅读更多关于 Redis 的信息。

您可能希望使用 Windows 版本的 Redis 进行本地开发工作,但仍然部署到 Linux 上支持的版本。您可以在http://github.com/MSOpenTech/redis获得 Windows 版本,也可以在虚拟机中运行 Linux 版本,或许可以使用 Dockerfloat

Redis 缓存作为服务在 Azure 和 AWS 上提供(elastic cache同时提供 Memcached 和 Redis)。您不需要管理自己的服务器,但是因为该技术不是特定于云的,所以如果您将来想要迁移,就不会被锁定。

由于 Redis 将整个数据集保存在内存中,但也能够保存到磁盘上,因此它可以适合作为主数据存储,这与 Memcached 不同。但是,它更常用于缓存,尤其是当使用云服务并且与云数据库(如 Azure SQL Database 或 AWS 关系数据库服务 ( RDS )配对时。

有两个推荐.NET C#客户端的 Redis: ServiceStack。堆栈交换客户端在堆栈溢出等网站上使用频繁,比服务堆栈客户端更容易正确使用。你可以在http://github.com/StackExchange/StackExchange.Redis了解更多,通过 NuGet 安装。

如果在应用层使用缓存,那么您可能需要编写大量的定制代码。您还需要确定将数据序列化为何种格式以存储在缓存中。如果直接服务于浏览器,那么 JSON 可能会很有用。但是如果要在内部使用,那么您可能更喜欢二进制格式,例如 MS Bond 或协议缓冲区。

See Chapter 8 , Understanding Code Execution and Asynchronous Operations , for more on serialization formats and libraries.

数据库结果集缓存

数据库级别的缓存类似于应用级别的缓存,它使用类似的基础架构,但需要更少的自定义代码。您可以使用内置在操作系统中的缓存功能,这可能会使改装变得更加容易。

When we talk about database caching here, we are not referring to caching within the database engine itself. DBs use extensive performance-enhancing techniques, such as query caching, and hold lots of their data in memory. However, this is abstracted away from the developer and the caching we mention here refers to storing the output of a query in an application cache. This is similar to, but subtly different from, the previous section, where you will be storing custom objects.

在操作系统的上下文中(例如 NHibernate 和实体框架),这被称为二级缓存。默认情况下,一级缓存通常已经在每个会话中发生,用于帮助避免类似选择 N+1 的问题。二级缓存在比单个事务更高的级别运行,允许您在整个应用的多个数据库会话之间共享缓存的数据。

消息队列

一个消息队列 ( MQ )是一种在系统中移动数据的异步且可靠的方式。它对于将工作从 web 应用卸载到后台服务非常有用,但是也可以用于同时更新系统的多个部分。例如,将缓存失效数据分发给所有的 web 服务器。

MQs 增加了复杂性,我们将在第 10 章性能增强工具的缺点中介绍管理这一点。然而,他们也可以帮助实现一个微服务架构,在这里你可以将你的整块分割成更小的部分,与合同对接。这使得在大型组织中进行推理变得更加容易,在大型组织中,不同的团队管理应用的不同部分。我们将在下一章更详细地讨论这一点,因为队列不是实现这种架构风格的唯一方式。您可以使用许多传输技术构建微服务,例如可以使用 HTTP APIs。

咖啡店类比

如果使用 MQs,那么您可能需要为后台发生的错误实现额外的协调逻辑。这最好用咖啡店的比喻来解释。

如果你购买了一杯外卖咖啡,也许是在一家受欢迎的特许含咖啡因饮料连锁店(不喜欢纳税)的分支机构,那么你的饮料是与支付过程异步准备的。通常,你下订单,咖啡师会在你付钱之前开始准备你的咖啡。此外,您通常会在收到饮料之前付款。这里有许多可能出错的地方,但它们非常罕见,以至于额外的成本是值得的,因为它加快了普通的工作流程。

例如,您可能会发现下单后无法付款,但咖啡制作过程已经开始。这将导致库存浪费,除非有另一个客户在等待可以使用它。或者,在你付款后,咖啡师发现你点的一种关键成分不见了。他们可以给你退款,或者协商换一种饮料。

虽然更复杂,但这个过程显然优于连续执行动作。如果你必须证明你有能力支付,你的饮料已经做好,然后你完成支付,一次只能为一个顾客服务。假设有足够的工作人员,支付处理和饮料准备可以并行进行,这样可以避免客户排很长的队。

以这种方式经营咖啡店很有直观意义,然而,在网络应用中,在通知用户结果之前完成相对长时间的交易是很常见的。在某些情况下,最好假设操作会成功,立即通知用户,并有一个适当的过程以防出错。

例如,支付处理网关可能缓慢且不可靠,因此在接受订单后向用户的信用卡收费可能会更好。但是,这意味着您不能再通过向用户显示错误消息来处理失败。你将不得不使用其他的交流方式。

当你在亚马逊上订购商品时,他们会立即获取付款细节,但他们会在后台处理付款,并向你发送带有结果的电子邮件。如果付款失败,他们需要取消订单并通知你。这需要额外的逻辑,但比处理支付交易和确认订单前检查库存更快。

消息队列样式

广义地说,消息队列有两种风格。这些风格有和没有中央经纪人。有了代理,所有的消息都通过管理通信的中枢。这种风格的例子包括RabbtmqActiveMQ 和 MS BizTalk

还有无代理风格(不使用代理),节点之间的通信是直接的。这种风格的一个例子包括零 MQ(MQ),它有一个名为网络 MQ 的本机 C#端口。

还提供云排队服务,包括 Azure 服务总线、Azure 队列存储、AWS 简单队列服务 ( SQS )。但是,与所有非通用云服务一样,您应该警惕被锁定。有标准 RabbitMQ 托管的云提供商,如果您最初不想运行自己的服务器,这使得迁移到自己的基础架构变得更加容易。例如,CloudAMQP 在多个云平台上提供 RabbitMQ 托管。

RabbitMQ 实现了高级消息队列协议 ( AMQP ),这有助于确保不同 MQ 代理之间的互操作性,例如,允许与 Java 消息服务 ( JMS )进行通信。Azure Service Bus 也支持 AMQP,但是 RabbitMQ 的一个很大的好处是你可以把它安装在你的开发机器上供本地使用,而不需要互联网连接。

还有微软消息队列 ( MSMQ ,内置 Windows。虽然这对于单台机器上的进程之间的通信很有用,但是要让它在多台服务器之间可靠地工作可能很困难。

常见的消息模式

有两种常见的消息模式:点对点单播和发布/订阅。它们分别向单个收件人和多个收件人发送邮件。

单播

单播是标准的消息队列方法。消息从一个服务进程或软件代理发送到另一个服务进程或软件代理。排队框架将确保这种情况可靠地发生,并将提供一定的交付保证。

这种方法是可靠的,但是不能随着系统的增长而扩展,因为每个节点都需要知道它的所有接收者。最好将系统组件松散地耦合在一起,这样它们就不需要了解任何其他组件。

这通常通过使用代理来实现,代理有三个主要优点:

  • 通过使用代理,您可以将进程相互分离,这样它们就不需要知道系统架构或者同时存在。他们只关心消息类型,代理负责将消息路由到正确的目的地。
  • 代理队列使得工作模式的分布变得容易,尤其是当组合多个生产者时。您可以让多个进程使用同一个队列,代理将以循环方式向它们分配消息。这是一种构建并行系统的简单方法,无需进行任何异步编程或担心线程。您可以只运行代码的多个副本,如果受硬件限制,可能会在不同的机器上运行,它们将同时运行。
  • 您可以轻松地广播或多播特定类型的消息,也许是为了指示某个事件已经发生。关心此事件的其他进程可以在发布者不知道的情况下监听消息。这就是众所周知的发布/订阅模式。

酒吧/酒馆

顾名思义,发布/订阅是软件代理将消息发布到队列中的地方,其他代理可以订阅这种类型的消息来接收它。当消息被发布时,所有的订阅者都会收到它,但关键是发布者不需要知道订阅者的任何信息,甚至不需要知道有多少(或者即使有)。

发布/订阅最好用消息队列体系结构的代理风格来完成。没有经纪人也可以做,但不是特别可靠。如果您的用例能够容忍消息丢失,那么您就可以在没有代理的情况下离开。但是,如果你需要保证交货,那么你应该使用一个。使用 RabbitMQ 代理还允许您利用可以执行复杂消息路由的交换。

如果您不想丢失消息,那么您需要仔细设计您的发布/订阅系统(即使使用经纪人)。没有订阅者的已发布消息可能会消失得无影无踪,这可能不是您想要的。

下图显示了简单消息转发、工作分配和发布/订阅之间的区别:

显然,如果你需要一个可靠的经纪人,那么它需要高度可用。通常,您会将多个代理聚集在一起以提供冗余。使用代理还允许您编写自定义规则来定义哪些订阅者接收哪些消息。例如,您的支付系统可能只关心订单,但您的日志服务器可能希望从所有系统获取所有消息。

您不仅要监控代理服务器,还要监控队列的长度。换句话说,每个队列中的消息数量应该总是稳定且接近于零。如果队列中的消息数量稳步增长,则这可能表明存在问题,您的运营团队需要解决该问题。这可能是因为你的消费者处理消息的速度不能超过生产者发送消息的速度,你必须构造额外的消费者。这可以是自动化的,您的监控软件可以增加一个额外的实例来扩展您的系统,以满足暂时的需求高峰。

拉比特

RabbitMQ 是一个免费的开源消息队列服务器。它是用 Erlang 编写的,Erlang 是 WhatsApp 用于其消息传递后端的同样健壮的语言。

RabbitMQ 目前由 Pivotal 维护(其实验室也制作 Pivotal Tracker 敏捷项目管理工具),但最初是由 LShift 制作的。随后,它被 VMware 收购,随后被分拆为一家合资企业。它是在火狐浏览器使用的旧版本许可的 Mozilla 公共许可 ( MPL ) v1.1 下发布的。

消息服务器可以从许多不同的语言和框架中使用,如 Java、Ruby 和. NET。这有助于将不同的应用链接在一起,例如,您希望与 ASP.NET Core 应用或 C#服务接口的 Rails 应用。

You can read more about RabbitMQ and download builds from http://www.rabbitmq.com/ .

RabbitMQ 比 MSMQ 等系统更现代,包括 HTTP API 和网络管理界面等功能。除了 HTTP 和 AMQP,它还支持简单文本消息协议 ( STOMP )和 MQTT ,这对轻量级物联网 ( 物联网)硬件应用非常有用。所有这些协议都提高了与其他消息传递系统的互操作性,并且通常可以使用标准 TLS 来保护它们。

web 管理界面向您显示有多少消息流经您的队列,以及它们是如何配置的。

它还允许您管理队列(任务,如清除或删除邮件),类似于下面的截图:

排队框架和库

您通常希望使用预构建的客户端库或框架来与消息队列进行交互。各种排队系统有许多不同语言的官方图书馆,提供低级别的访问。例如,RabbitMQ 有一名官员.NET/C#客户端库。

然而,还有其他固执己见的客户端和框架,它们为常见的消息传递任务提供了更高级别的抽象。例如, NServiceBus ( NSB ,支持 RabbitMQ、MSMQ、SQL Server 和 Azure,是一个商业产品。

NSB 的一个免费替代品是mass transit(http://masstransit-project.com/),这是一个轻量级的服务总线和分布式应用框架。它还推出了超级方便的 Topshelf 框架(http://topshelf-project.com/,这使得创建视窗服务变得非常容易。

两者都还没有开始。但是移植这两个项目的工作正在进行中。

Library and framework support for .NET Core and ASP.NET Core is progressing rapidly, so check http://anclafs.com/ for the latest information. Feel free to help out and contribute to this list or to any of the open source projects that need porting.

MassTransit(和 NSB)的一个有趣特性是支持传奇。传奇是一个复杂的状态机,它允许你为整个工作流程建模。您可以隐式地捕捉一个传奇中的黄金路径和错误流,而不是定义单个消息并记录它们是如何结合在一起的。

有一个优秀的开源库叫做 EasyNetQ ,它使得在 RabbitMQ 上实现 pub/sub 变得微不足道。你可以在http://easynetq.com/看到。官方的 RabbitMQ 客户端和 EasyNetQ 现在都支持.NET Core。

你也可以考虑使用 RestBus ,这是一个支持 ASP.NET Core 的 RabbitMQ 库。您可以在http://restbus.org/了解更多,它还支持网络应用编程接口和服务堆栈。

摘要

在本章中,我们研究了用于缓存和消息队列的各种工具和方法。这两种技术通过将数据移动到其他位置,而不是让一个巨大的整体做所有的事情,提供了提高系统性能的不同方法。

这些都是高级话题,在这么小的篇幅里很难涵盖。希望你已经被介绍了一些新的想法,可以帮助你用不同的方式解决问题。您也可以访问作者在https://unop.uk/的网站,获取更多话题的报道。如果您发现了一种您认为有帮助的新技术,我们鼓励您阅读所有实现细节的文档和规范。

但是,在深入研究之前,您应该了解高级技术是复杂的,并且有缺点,这会降低您的开发速度。在下一章中,我们将了解这些缺点,并发现管理复杂性的方法,例如微服务。

十、性能增强工具的缺点

我们在本书中涉及的许多主题都是以一定的代价来提高性能的。你的申请会变得更复杂,更难理解或推理。本章讨论这些权衡以及如何减轻它们的影响。

只有当你需要的时候,而不仅仅是因为它们有趣或有挑战性的时候,你才应该实施你在本书中学到的许多方法。如果现有的性能足够好,通常最好保持简单。

你将学习如何对你应该使用的技术和技巧做出务实的选择。如果您选择使用高级方法,您还将看到如何管理复杂性。

本章涵盖的主题包括以下内容:

  • 用框架和架构管理复杂性
  • 建立健康的文化以提供高性能
  • 分布式调试和性能日志记录
  • 了解统计数据和陈旧数据

许多书籍和指南只关注新工具和框架的积极方面。然而,没有什么是免费的,总是有惩罚,这可能不会立即显而易见。

你可能感觉不到你长期以来所做选择的影响,尤其是在技术架构方面。你可能不会发现一个决定是坏的,直到你试图在它的基础上,也许几年后。

管理复杂性

性能增强技术的主要问题之一是它们通常会使系统更加复杂。这可能会使系统更难修改,也可能会降低您的工作效率。因此,虽然您的系统运行得更快,但您的开发现在更慢。

我们通常在企业软件中发现这种复杂性问题,尽管通常是出于不同的原因。通常,会使用许多不必要的抽象层,以保持软件的灵活性。具有讽刺意味的是,这实际上会使添加新功能的速度变慢。在你意识到简单让改变变得更容易之前,这看起来可能是违背直觉的。

There's a satirical enterprise edition of the popular programmer interview coding test FizzBuzz , which is available on GitHub (via the short URL http://www.fizzbuzz.enterprises/ ) . It's a good inspiration for how to not do things.

如果你还不需要某个特性,那么最好把它放在一边,而不是构建它,以防将来你可能需要它。你写的代码越多,它就会有越多的错误,并且越难理解。过度设计是一种常见的负面心理特征,如果你没有意识到,它很容易成为受害者,营销人员经常利用这一点。

举个非软件的例子,四轮驱动 SUV 卖给那些永远不需要越野能力的人,他们的错误假设是这种能力有一天可能会派上用场。然而,财务、安全、环境和停车便利成本超过了这种假定的好处,因为它从未被使用过。

我们通常将这种开发建议称之为“极限编程” ( XP )哲学,你不需要它 ( YAGNI )。虽然我们有时候用的词略有不同,但意思是一样的。YAGNI 主张保持简单,只建立你急需的东西。

这并不意味着你应该让你的软件很难修改。保持灵活性仍然很重要,只是不要在需要之前添加功能。例如,当只有一个实现时,添加一个抽象基类可能是一种过度杀伤。如果您构建它,您可以很容易地将它与第二个实现一起添加。

做的时候很难动作快又不摔东西。除了持续良好的开发速度之外,如何实现高可靠性还取决于许多特定于您的情况的因素,例如您的团队规模、组织结构和公司文化。

一种方法是接受变化并开发一个系统,在这个系统中你可以自信地重构你的代码。使用像 C#这样的静态编译语言是一个好的开始,但是您也应该有一个全面的测试套件来避免回归。

您应该设计一个松散耦合的系统,这意味着您可以在没有很多连锁反应的情况下独立地更换部件。这也使得构建单元测试变得更加容易,这对于在重构时防止功能退化是非常宝贵的。

在下一章中,我们将使用持续集成 ( CI )工作流程来涵盖测试和自动化。在本章中,我们将更多地讨论可以帮助您维护应用的各种架构风格。

理解复杂性

当学习新的做事方法时,你应该避免在不了解原因的情况下去做。你应该知道好处和坏处,然后衡量这些变化,以证明它们是你所期望的。不要只是盲目地执行某件事,并假设它会改善情况。尽量避免货邪教编程,始终客观评价一种新的做法。

Cargo cult programming is the practice of emulating something successful but failing to understand the reasons of why it works. Its name comes from the cargo cults of the Pacific, who built dummy airstrips after World War II to encourage cargo delivery. We use it to describe many things where correlation has been confused with causation. One example is a company encouraging long hours to deliver a project because they have heard of successful projects where employees worked long hours. However, they fail to understand that the successful project and long hours are both independent byproducts of a highly motivated and competent workforce, and they are not directly related.

保持代码的可读性很重要,不仅仅是为了团队中的其他人或新成员,也是为了你未来的自己(他们会忘记某件事是如何工作的,以及它为什么以原来的方式编写)。这不仅仅意味着在代码中编写有用的解释性注释,尽管这是一个非常好的实践。它也适用于采购控制意见和保持文件的最新。

可读性还包括保持事物的简单性,只让它们变得必要的复杂,而不在不必要的抽象层中隐藏功能,例如,当标准结构(例如,循环或if语句)可读性更强且长度稍长时,不使用巧妙的编程技术来减少文件行数。

在你的团队中有一个标准的做事方式有助于避免意外。在任何地方使用相同的方法可能比找到一种更好的方法,然后有很多不同的方法更有价值。如果有共识,那么你可以回去,在你需要的地方改进更好的方法。

复杂性降低

有各种解决方案来管理性能增强技术可能增加的复杂性。这些通常通过隐藏复杂性来减少你需要随时思考的逻辑量。

一种选择是使用标准化应用编写方式的框架,这样可以更容易推理。另一种方法是使用一种架构,该架构允许您只孤立地考虑代码库的一小部分。通过将一个复杂的应用分解成可管理的块,它变得更容易使用。

This idea of modularity is related to the single responsibility principle (SRP ), which is the first of the SOLID principles (the others are open/closed, Liskov substitution, interface segregation, and dependency inversion). It is also similar to the higher-level Separation of Concerns (SoC ) and to the simplicity of the Unix philosophy. It is better to have many tools that each do one thing well, rather than one tool that does many things badly.

结构

近年来,前端框架变得流行起来。脸书的 React 是一个库,旨在用 JavaScript 可靠地构建 web 应用视图,尽管专利许可有争议。目的是通过简化数据流和标准化方法来帮助大型团队完成项目。React 可以通过ReactJS.NET项目(https://reactjs.net/)与 ASP.NET 芯整合。

This is not to be confused with React Native , used to build cross-platform apps that share code across iOS, Android, and the Universal Windows Platform (UWP ). If you prefer coding in C#, then you can build your cross-platform mobile apps with Xamarin . However, we won't go further into any of these technologies in this book. Visit the author's website (https://unop.uk/ ) to read about ReactJS.NET and Xamarin.Forms.

在后端,我们有的服务器端框架。网芯和 ASP.NET 芯。除了 C#特性,这些特性还提供了简化历史上复杂特性的便捷方式。例如asyncawait关键字隐藏了很多与异步编程相关的复杂逻辑,lambda 函数简洁地表达了意图。

我们在本书的前面已经介绍了其中的许多特性,因此在这里不再赘述。我们还强调了一些库,通过隐藏用于复杂操作的样板代码,可以让您的生活更轻松,例如EasyNetQRestBus

隐藏一个复杂的过程从来都不是完美的,您偶尔会遇到一些抽象,它们会泄露一些实现细节。例如,在处理异常时,您可能会发现您感兴趣的问题现在被包装在一个聚合异常中。如果你不小心,你的错误日志可能不再包含你想要的细节。

It's good practice not to log the individual properties of an exception, as exceptions already have built-in logic to output all of the relevant information. Simply log an exception as a string and you will get the message and stack trace, including any inner exceptions.

我们还需要详细讨论的是 web 应用的体系结构。将一个庞大的系统分解成几个独立的部分不仅可以提高性能,如果处理得当,还可以让它更容易维护。

体系结构

在前一章中,当讨论消息队列时,我们简要介绍了微服务体系结构。这种风格是对传统的面向服务架构 ( SOA )的更现代的重新想象,虽然使用可靠的 MQ 通信是首选,但是我们也可以使用表示状态转移(REST)HTTP API 来执行这一操作。

通常,我们将传统的 web 应用构建为单个应用或整体。如果应用在很长一段时间内有机增长,这是很常见的,这是完全可以接受的做法。在没有任何需求之前过早地过度设计是一个糟糕的决定,这可能永远不会实现。

过度流行是一个很好的问题,但不要过早地为此进行优化。这不是让事情变得不必要的缓慢的借口,所以一定要理解其中的权衡。

Using a monolithic architecture is not an excuse to build something badly, and you should plan for expansion, even if you do not implement it immediately. You can keep things simple while still allowing for future growth. Although the application is a single unit, you should split the code base into well-organized modules, which are linked together in a simple, logical, and easy-to-understand way. Refer to the SOLID principles mentioned previously.

如果一个单一的应用不能很容易地扩展以满足用户需求,并因此表现不佳,那么您可以将其拆分成更小的独立服务。如果一个应用变得太麻烦而无法快速迭代,并且开发速度变慢,你也可能希望将其拆分。

整体服务与微服务

下图显示了典型的整体式和微服务架构之间的一些差异:

这里,用户向运行在 web 服务器场上的应用发出请求。为了清楚起见,我们省略了防火墙、负载平衡器和数据库,但是显示了多个 web 服务器来说明同一代码库在多台机器上运行。

在最初的整体架构中,用户直接与单个网络服务器通信。理想情况下,这是每个请求/响应对。但是,如果应用设计不佳,并且将状态保存在内存中,那么粘性会话可能会导致负载集中在某些服务器上。

图中的第二个例子,即微服务架构,显然更复杂,但也更灵活。用户再次向 web 服务器发送请求,但是服务器并不做所有的工作,而是将消息放入队列。

这个队列中的工作分布在多个后端服务之间,其中第一个服务很忙,所以第二个服务接收消息。当服务完成时,它向消息代理上的一个交换发送消息,该交换使用发布/订阅广播来通知所有的 web 服务器。

一个增加的复杂性是,架构应该已经发送了对用户原始 web 请求的响应,因此您需要更仔细地考虑用户体验 ( UX )。例如,您可以显示一个进度指示器,并使用异步网络套接字连接更新其状态。

架构比较

整体方法很简单,你可以添加更多的网络服务器来处理更多的用户。然而,这种方法会变得很麻烦,因为应用(和开发团队)变得越来越大,因为最微小的变化都需要完全重新部署到每个 web 服务器。

此外,单块很容易纵向(向上)扩展,但很难横向(向外)扩展,这一点我们之前已经讨论过了。然而,单块更容易调试,所以你需要小心,并有良好的监控和记录。您不希望任何随机停机调查因为不必要的微服务实现而变成谋杀之谜搜索。

Historically, Facebook had a deployment process that consisted of slowly compiling their PHP code base to a gigantic gigabyte-scale binary (for runtime performance reasons). They then needed to develop a modified version of BitTorrent to efficiently distribute this huge executable to all of their web servers. Although this was impressive infrastructure engineering, it didn't address the root cause of their technical debt problem, and they have since moved on to better solutions, such as the HipHop Virtual Machine (HHVM ), which is similar to the .NET CLR

如果您希望练习连续交付,并且一周部署多次(甚至一天部署多次),那么将您的 web 应用拆分开来是非常有利的。然后,您可以分别维护和部署每个部分,使用消息根据约定的应用编程接口相互通信。

这种分离也可以帮助您使用敏捷开发方法——例如,使用许多小团队而不是大团队,因为小团队表现更好。

你控制整块石头的工具非常粗糙,因为所有的工作都是在一个地方完成的。您不能将应用的某些部分独立扩展到应用的其他组件。即使高负荷集中在一小部分,你也只能缩放整件事。这类似于中央银行只控制一个单一的利率作为杠杆,这会同时影响许多事情。如果您的应用是分布式的,那么您只需要扩展需要它的部分,避免过度配置并降低成本。

亚伯拉罕·马斯洛的一句被广泛引用的话是这样说的:

"I suppose it is tempting, if the only tool you have is a hammer, to treat everything as if it were a nail."

这就是所谓的仪器定律,与确认偏差有关。如果你只有一个工具,那么你很可能什么都用这个工具,只看到支持你现有想法的东西。这通常适用于工具和框架,但也适用于缩放技术。

模块化的第一步可能是将一个大的 web 应用拆分成许多较小的 web 应用,这样您就可以分别部署它们。这种策略可以有一些好处,但也有很多局限性。某些逻辑可能不适合托管在 web 应用中,例如,长时间运行的进程,如监控文件系统或用于操作媒体的进程。

您可能还会发现,您需要复制原始单块中共享的代码。如果你坚持不重复自己 ( DRY )的原则,那么你可能会把这个功能提取到一个库中。然而,现在您需要管理依赖关系和版本。您还需要在私有存储库中构建、打包和托管您的库的过程,所有这些都会减慢开发速度并降低您的敏捷性。

Sometimes, we also refer to DRY as Duplication Is Evil (DIE ), and this is the sensible idea that an implementation should only occur in a unique location. If other code requires this functionality, then it should call the original function and not have copied code pasted in. This means that you only need to make a change in a single place to apply it everywhere.

更高级的方法是将功能提取到一个单独的服务中,该服务可以处理许多不同的 web 应用。如果设计正确,那么这项服务不需要对这些应用有任何了解,只需对消息做出响应。这允许您添加新的 web 应用,而无需更改其他应用或服务的代码。

重构

如果应用已经成为一个紧密耦合的大泥球,那么将一个整体重构为服务可能会很棘手,但是如果它已经构建良好(具有大量的测试覆盖和松散耦合的代码),那么它应该不会太费力。一个建造得很好的整体和一个不加思考的大泥球有很大的区别。

值得在测试覆盖范围上进行扩展,因为单元测试对于成功的重构是必不可少的,而不会引入回归或新的错误。单元测试允许您自信地重构和整理,并且它们防止了脆弱的代码的创建,这是开发人员不敢触及的。我们将在第 11 章监控性能回归中更详细地介绍测试,包括自动化。

这两种设计模式表面上看起来很相似,但从内部来看却完全不同。下图说明了结构良好的整体建筑和杂乱无章的大泥球之间的区别。

大方框代表每个网络服务器上运行的代码(上图中整体示例中的网络服务器 N 方框)。从外部来看,它们看起来是一样的,但区别在于代码的内部耦合。

大泥球是一团混乱,代码在整个应用中引用了其他函数。整体结构良好,不同模块之间的关注点清晰分离。

对大泥球中代码的更改可能会有意想不到的副作用,因为它的其他部分可能依赖于您正在修改的实现细节。这使得它易碎,难以更改,开发人员可能害怕接触它。

构建良好的整体很容易重构,并分离成独立的服务,因为代码组织得很整齐。它使用抽象接口在模块之间进行通信,代码不会触及另一个类的具体实现细节。它还具有出色的单元测试覆盖率,在每次签入/推送时都会自动运行。

Tools are also very useful in refactoring and testing. We covered the Visual Studio IDE and ReSharper plugin previously, but there are many more tools for testing. We will cover more testing tools, including automation, in Chapter 11 , Monitoring Performance Regressions .

虽然两者都是单一的代码库,但整体的质量要高得多,因为它在内部分布良好。好的设计对未来的验证很重要,因为它允许扩展。整块石头没有过早地被分割(在需要缩放之前),但是设计使得这在以后很容易做到。相比之下,大泥球已经积累了大量的技术债务,在我们取得任何进一步进展之前,它需要还清这些债务。

Technical debt (or tech debt for short) is the concept of not finishing a job or cutting corners, which could lead to more difficulties until it is paid back. For example, failing to properly document a system will make it more difficult for new developers. Tech debt is not necessarily a bad thing if it is deliberately taken on in full knowledge, logged, and paid back later. For example, waiting to write the documentation until after a release can speed up the delivery. However, tech debt that is not paid back or is accumulated without knowledge (simply due to sloppy coding) will get worse over time and cause bigger issues later.

交付高质量且灵活的应用(易于重构)的最佳方式是拥有一个称职且尽职尽责的开发团队。然而,这些属性往往更多的是关于文化、交流和动机,而不仅仅是技能或天赋。虽然这不是一本关于团队管理的书,但是拥有一个健康的文化是非常重要的,所以我们将在这里介绍一点。运营和软件开发部门的每个人都可以帮助创造一种积极友好的文化,即使你通常也需要上级的支持。

高效的文化

如果你想获得高绩效,那么培养一种鼓励这一点并承认绩效至关重要的公司文化是很重要的。文化不能只是来自底层(只涉及工程师)。文化需要来自高层,管理层必须接受绩效特权。

This section is not very technical, so feel free to skip it if you don't care about management or the human side of software development.

无可指责的文化

高绩效文化最重要的属性是它应该是开放和无可指责的。每个人都需要专注于通过测量和学习来实现最好的结果。将错误归咎于个人对交付优秀的软件是有害的,这不仅仅是在性能方面。

如果出了问题,那么这就是一个过程问题,重点应该放在改进它和防止将来重复错误上,例如,通过自动化它。这类似于航空旅行等对安全至关重要的行业的行为,因为他们认识到,在灾难发生之前,责备人们会阻止他们尽早提出问题。

一个相关的哲学是日本的改善过程,它鼓励每个人不断改进。汽车制造商丰田开创了改善实践,以提高其生产线的效率,此后大多数汽车公司和许多其他不同行业都采用了这些实践。

一些行业也有鼓励举报的程序,以保护引起关注的个人。然而,如果这在 web 应用开发中是必需的,那么这肯定是文化需要工作的标志。开发人员应该觉得他们能够绕过他们的直线经理直接提出问题,而没有任何后果。如果每个人的意见都得到尊重,那么这甚至没有必要。

智力不诚实

如果团队成员在想法受到挑战时采取防御态度,那么这是事情可能进展不顺利的迹象。每个人都会犯错误,他们的知识也有差距,这是最好的工程师都信奉的真理。你应该努力营造一种文化,在这种文化中,每个人都乐于接受新的想法,并且总是问问题。

如果人们不能接受建设性的批评,他们的想法受到挑战,那么他们可能缺乏信心,并可能掩盖了缺乏能力。有经验的开发人员知道,你永远不会停止学习;他们承认自己的无知,并且总是乐于接受改进的建议。

不接受建议会改变其他人的行为,他们会提前停止提出小问题。这导致了关于质量差的公开秘密,而第一个已知的问题是在发布时,此时一切都变得更糟(或着火)。

这并不是对人恶语相向的借口,所以总是要试着对人好一点,委婉地解释你批评背后的原因。任何事情都很容易挑毛病,所以一定要提出一个替代方法。如果你有耐心,那么一个通情达理的人会对学习机会心存感激,对获得经验心存感激。

一个好的规则是不要做一个混蛋对待别人就像你希望别人对待你一样;所以要善良,想想如果情况逆转,你会有什么感受。请记住,友好并不总是与做正确的事情相一致。

一点点自我贬低可以让你变得更平易近人,而不是简单地发号施令。然而,当某件事是开玩笑或开玩笑时,你应该说清楚,尤其是在与更直接的文化打交道时,或者在文本交流时。

一概而论总是一个坏主意(从来没有例外)。然而,作为一个例子,北美人通常没有英国人那么含蓄,也没有英国人那么讽刺(英国人也用不同的方式拼写一些单词,有些人会说,更正确)。显然,用你自己的判断,因为这可能是可怕的建议,可能会引起冒犯,甚至更糟糕的是,一个全面的外交事件。希望不言而喻(或者也许不是),整段都是在开玩笑。

对自己的想法有诚信和信心的人可以谦虚和自嘲,但公司内部文化也会影响这一点。一个特别糟糕的做法是通过阶梯式(也称为堆栈排名)进行绩效评估,这涉及到将每个人相对于其他人按顺序排列。这是有害的,因为它奖励的是那些更专注于推销自己的人,而不是那些认识到自己技术技能不足并试图提高这些技能的人。在病态的情况下,所有最优秀的人都被挤出去了,你最终进入了一个充满反社会的尖锐西装的公司,他们在技术上是文盲,甚至道德上是破产的。

放慢速度以走得更快

有时,公司必须允许开发团队放慢特性交付的速度,以便专注于性能和解决技术债务。他们应该有时间考虑设计决策,这样他们可以深思熟虑,避免过早的概括或粗心的优化。

性能是软件的一个重要卖点,在整个开发过程中构建质量比在最后的抛光阶段要容易得多。有大量证据表明,良好的绩效可以提高您的投资回报 ( RoI )并创造客户。在网络上尤其如此,性能差会降低转化率和搜索引擎排名。

拥有健康的文化不仅对软件的运行时性能很重要,而且对开发速度也很重要。应该鼓励团队行为严谨,编写精确但简洁的代码。

你得到你所测量的,如果你只测量特征交付的速率,或者更糟的是,仅仅是所写的行代码 ( LoC ),那么质量就会受到影响。从长远来看,这将伤害你,是一个虚假的经济。

虚假经济是指当你采取短期成本节约措施时,实际上会让你长期损失更多的钱。这方面的例子有:为开发人员节省硬件,或者在代码编写过程中打断某人做一些琐碎的事情,这很容易等到以后,迫使他们切换上下文。

另一个非软件的例子是目光短浅的政府削减研究投资,牺牲长期增长来换取短期储蓄。或者一个国家离开经济联盟时承诺会费将用于更好的事情,尽管会费与经济的长期损失相比相形见绌。

Hardware is significantly cheaper than a developer's time. Therefore, if everyone on your team doesn't have a beefy box and multiple massive monitors, then productivity is being needlessly diminished. Even over a decade ago, Facebook's software developer job adverts listed dual 24-inch widescreen monitors as a perk.

完全(或彻底)地

在一个健康和进步的文化中,从头重写低质量的软件很有诱惑力,也许是在最新的流行框架中,但这通常是一个错误。发布的软件是久经沙场的(不管它的构建有多糟糕),如果你重写它,那么你可能会再次犯同样的错误,例如,重新实现你已经修补过的 bug。如果软件不是用良好的单元测试覆盖率构建的,这一点尤其正确,这有助于防止回归。

唯一可以从头开始重写应用的情况是,你故意做了一个原型来探索问题空间,唯一的目的是扔掉它。但是,您应该非常小心,因为如果您实际上已经构建了一个最小可行产品 ( MVP ),那么这些实际上是 MVP 的原型有一个长期停留的习惯,并形成了更大应用的基础。

完全重写的一个更好的方法是向应用添加测试(如果它还没有测试的话),并逐步重构它以提高质量和性能。

如果你构建一个应用的方式使得单元测试变得困难,那么你可以从用户界面 ( UI )测试开始,也许可以使用无头网络浏览器。在下一章中,我们将讨论更多的测试,包括性能测试。

共享价值

文化实际上只是一套共同的价值观,包括开放、可持续性、包容性、多样性和道德行为等。将这些价值观正式记录下来会有所帮助,这样每个人都知道你代表什么。

你可能会有一个渐进式的公开薪资政策,这样其他人就不能用秘密的收入信息作为工具来少给人发工资了。然而,这需要普遍适用,因为有多种冲突的文化是不健康的,因为这可能会形成一种我们对他们的态度。

关于文化还有很多要说的,但是,正如你所看到的,有许多相互竞争的问题需要平衡。最重要的想法是进行有意和深思熟虑的权衡。没有一个正确的选择,但你应该始终意识到后果,并意识到微小的行动会如何改变团队的表现。

表演的代价

开发人员应该了解可用的性能预算,并了解他们编写的代码的成本,不仅仅是执行吞吐量,还包括可读性、可维护性和能效。在一个工作单元中投入更多的核心远不如重构它变得更简单。

效率变得越来越重要,尤其是随着移动设备和云计算基于时间的使用计费的兴起。并行化一个低效的算法可能会解决时域中的性能问题,但这是一种粗糙的暴力方法,改变底层实现可能会更好。

少往往就是多,有时候什么都不做是最好的方法。软件工程不仅仅是要知道要构建什么,还要知道不构建什么。保持事情简单有助于团队中的其他人使用你的工作。你应该尽量避免用不明显的行为让任何人吃惊。例如,如果你构建了一个应用编程接口,那么你应该给它常规的默认值。如果操作可能需要很长时间,那么将这些方法命名为 async 以表明这一事实。

在构建代码时,您应该致力于使成功变得容易,失败变得困难。让做错事变得困难;例如,如果一个方法不安全,命名它来记录这个事实。做一个称职的程序员只是做一个优秀开发人员的一小部分;你还需要乐于助人,并精通清晰的沟通。

事实上,如果你不刻意让事情变得简单来帮助团队中其他人的理解,成为一名专业的程序员可能会有不利的一面。您应该始终平衡性能改进和副作用,并且您不应该在没有充分理由的情况下以牺牲未来的开发效率为代价来实现它们。

分布式调试

分布式系统可能会使调试问题变得困难,您需要通过集成有助于可视性的技术来提前对此进行规划。你应该知道你想要测量什么指标,什么参数是重要的记录。

当你运行一个网络应用时,它不是一个部署和忘记的例子,就像移动应用或桌面软件一样。您需要持续自动关注您的应用,以确保它始终可用。如果您监控正确的性能指标,那么您可以获得问题的早期预警信号,并采取预防措施。如果您只测量正常运行时间或响应时间,那么您可能知道的第一个问题是停机通知,可能是在不合群的时间。

您可以将基础架构外包给云托管公司,这样您就不必担心硬件或平台故障。然而,这并不能完全免除你的责任,你的软件仍然需要持续的监控。您可能需要以不同的方式设计您的应用,以便与您的托管平台协调工作,并在出现问题时进行扩展或自我修复。

如果你正确地设计了你的系统,那么你的 web 应用就会自动运行,你很少会收到需要你注意的动作的通知。如果您能够成功地自动化所有事情,而不是临时照看一个实时部署,那么您就有更多的时间来构建未来。

在分布式架构中,您不能简单地将调试器附加到实时 web 服务器上,即使在可能的情况下,这也不是一个好主意。再也没有一台直播服务器了;现在有许多实时服务器,甚至更多的进程。为了全面了解系统,您需要同时检查多个模块的状态。

有许多工具可以帮助您集中调试信息。你可以改装其中一些。然而,为了充分利用它们,您应该决定提前测量什么,并从一开始就在软件中构建遥测功能。

记录

日志记录在高性能应用中至关重要,因此,虽然日志记录确实会增加一些开销,并会降低执行速度,但忽略它将是短视的,也是错误的节约。没有日志,你就不知道什么是慢的,什么是需要改进的。您还会有其他顾虑,例如可靠性,对于可靠性来说,日志记录是必不可少的。

错误记录

您可能熟悉名为错误记录模块和处理程序 ( ELMAH )的优秀 ASP.NET 包,用于捕获未处理的异常(http://elmah.github.io/)。ELMAH 非常适合现有的应用,因为你可以把它放到一个实时运行的网络应用中。然而,最好从一开始就将错误记录内置到软件中。

不幸的是,ELMAH 还不支持 ASP.NET Core,但是有一个类似的包叫做错误记录中间件 ( ELM )。将它添加到您的 web 应用中就像安装 scope 一样简单,但是它没有 ELMAH 的所有功能。ELM 并不真正适合生产使用,但是它在本地仍然可以帮助调试。

首先,将Microsoft.AspNetCore.Diagnostics.Elm NuGet 包添加到您的项目中。您可以使用集成开发环境的图形工具来做到这一点(方法会根据您使用的集成开发环境而有所不同,例如 Visual Studio 或 VS Mac)。或者,您可以在终端中使用以下命令,该命令适用于所有受支持的平台:

dotnet add package Microsoft.AspNetCore.Diagnostics.Elm

这将在.csproj文件中添加一行,如下所示:

<PackageReference Include="Microsoft.AspNetCore.Diagnostics.Elm" Version="0.2.2" />

中引用包的方式.NET Core 2.0 不同于.NET Core 1.0。它们不再在 JSON 文件中指定,尽管您可以手动编辑 XML 项目文件,但使用提供的工具更容易。您可以使用dotnet migrate命令将基于旧project.json的解决方案升级为基于msbuild.csproj解决方案。

然后,在Startup类的ConfigureServices方法中,添加以下代码行:

services.AddElm(); 

您还需要在同一类的Configure方法中添加以下几行:

app.UseElmPage(); 
app.UseElmCapture(); 

You may want to put these lines of code inside the if statement that tests for env.IsDevelopment() , so that they are only active on your workstation.

现在,您可以在浏览器中访问 web 应用的/Elm路径来查看日志,如下图所示。使用搜索框过滤结果,单击每行末尾的 v 展开条目的详细信息,然后单击^再次折叠它:

当使用 ELM、ELMAH 甚至默认错误页面时,您应该注意不要向真实用户显示详细的异常或堆栈跟踪。这不仅仅是因为他们不友好和没有帮助,而是因为他们是一个真正的安全问题。错误日志和堆栈跟踪可以揭示应用的内部工作方式,恶意行为者可以利用这一点。

You can use the website, ASafaWeb (https://asafaweb.com/ ), to check if you are unknowingly revealing more information about your app than you should be. It focuses more on traditional ASP.NET applications, but it's still useful.

您可能已经注意到,在终端中运行应用时,还会将任何日志条目流式传输到控制台(取决于您配置的日志记录级别)。您可以将这个输出传输到您喜欢的任何地方,这是 Unix 应用使用的一种非常常见的模式。将日志视为事件流并简单地将其写入stdout是十二因素应用的因素之一。

十二因素应用是设计网络应用的一种方式,以便它们非常适合在云托管环境中运行。这些规则是由 Heroku 的团队构想出来的,作为可以遵循的良好实践。他们专注于 Linux,但现在这是托管 ASP.NET Core 网络应用的一个很好的选择。

You can read about the other eleven factors on the website dedicated to twelve-factor apps at https://12factor.net/ . The factor regarding logs is number eleven (https://12factor.net/logs ).

ELM 是相当基本的,但它仍然是有用的。然而,默认模板中已经内置了更好的解决方案,例如对日志记录和应用洞察的集成支持。

应用洞察

Application Insights 是一项遥测服务,允许您监控应用的性能并查看请求或异常。您可以在 Azure 上使用它,但是它也可以在本地工作,不需要 Azure 帐户。但是,如果您没有在 Azure 上托管,那么您可能希望使用其他东西。

在 Visual Studio 2017 中,您可以右键单击您的 web 应用项目,单击添加,然后单击应用洞察遥测。然后,您可以单击开始按钮并注册您的应用。但是,请记住使用 Azure 的隐私和成本影响。您可以通过单击注册页面底部的低调链接在本地使用 SDK。

This practice of designing a user interface to encourage a particular behavior from users that may not be in their best interest is called a dark pattern . Microsoft is betting big on Azure, so it is understandable that much of their software is designed to funnel you into a Microsoft online account and Azure. However, you don't need to use VS or Windows to build software with .NET Core, and it's also fairly easy to switch off the telemetry in the SDK and development tools.

构建并运行项目,然后打开“应用洞察搜索”窗口查看输出。在浏览器中浏览 web 应用,您会看到记录开始出现,看起来应该如下所示:

If you don't see any records in the output, then you may need to update the NuGet package (Microsoft.ApplicationInsights.AspNetCore ) or click on the search icon.

您可以按事件类型进行筛选,例如,请求或异常,还有更详细的筛选来优化您的搜索。您可以选择单个记录来查看更多详细信息,如下所示:

这可以告诉你的一个更有趣的事情是,你的应用是否触发了任何外部事件。例如,您可以看到您的应用是否向另一个域上的网址发出了网络应用编程接口请求,以及该操作对其速度的影响。

我们没有足够的空间在这里更详细地介绍应用洞察,如果您没有使用 Azure,那么您无论如何都要考虑其他选项。你可以在https://docs . Microsoft . com/en-us/azure/application-insights/app-insights-ASP-net-core上在线阅读更多。

综合采伐

日志记录现在已经内置于 ASP.NET Core 中,依赖注入 ( DI )也是如此,两者都包含在默认模板中。这减少了进入的障碍,现在即使在最小的项目中使用这些有用的技术也是微不足道的。

以前,您需要在开始之前添加日志和 DI 库,这可能会使许多人不再使用它们。如果你还没有两者的标准选择,那么你就需要费力地完成过多的项目,并研究每一个项目的优点。

You can still use your preferred libraries if you are already acquainted with them. It's just that there are now sensible defaults, which you can override.

日志在Program类中被添加到你的应用中,但是在 ASP.NET Core 2.0 中的方式与在 ASP.NET Core 1.0 中的方式略有不同。在模板中,它是在调用CreateDefaultBuilder方法时添加的,这很容易设置合理的默认值。如果您想自定义它,那么语法与以前的版本不同。你不再叫伐木工人工厂,取而代之的是一种ConfigureLogging方法。参考文档(https://docs . Microsoft . com/en-us/aspnet/core/基本面/日志?tab = aspnetcore2x)了解完整的详细信息,如果您想使用默认配置以外的其他配置。

Startup类中配置了日志记录,如果您使用标准的 web 应用模板,那么这将已经包含在您的日志中。记录器工厂从appsettings.json文件中读取设置。在这里,您可以配置日志记录级别,默认情况下,相关部分如下所示:

"Logging": {

  "IncludeScopes": false,

  "Debug": {

    "LogLevel": {

      "Default": "Warning"

    }

  },

  "Console": {

    "LogLevel": {

      "Default": "Warning"

    }

  }

}

在开发模式下,将使用以下设置来覆盖这些设置(从appsettings.Development.json开始):

"Logging": {
  "IncludeScopes": false,
  "LogLevel": {
    "Default": "Debug",
    "System": "Information",
    "Microsoft": "Information"
  }
}

这设置了一个非常健谈的日志级别,这对开发很有用,但是当您在生产中运行时,您可能只想记录警告和错误。

Along with logging, useful development debugging tools included by default in the Startup class include a developer exception page, which is like an advanced version of the old Yellow Screen Of Death (YSOD ) ASP.NET error page. Also included is a database error page, which can helpfully apply EF migrations and which mitigates a common pitfall of the previous EF code-first migration deployments.

要通过构造函数注入将记录器添加到您的 MVC 主控制器,您可以使用以下代码(在文件顶部将using Microsoft.Extensions.Logging;添加到您的using语句之后):

private readonly ILogger Logger; 
public HomeController(ILoggerFactory loggerFactory) 
{ 
    Logger = loggerFactory.CreateLogger<HomeController>(); 
} 

之后,可以使用Logger记录动作方法内部的事件,如下所示:

Logger.LogDebug("Home page loaded"); 

有更多可用的日志记录级别和重载方法,以便您可以记录附加信息,例如异常。我们在此不再赘述,但您不必简单地记录文本事件;您还可以记录执行时间(可能使用秒表)或递增计数器,以查看特定事件发生的频率。接下来,我们将看到如何集中查看这些数字并正确阅读它们。

For more examples of how to use logging, you can examine the default account controller (if you've included individual user account authentication in the template).

集中式日志记录

伐木很棒。但是,在分布式系统中,您会希望将所有日志和异常馈送到一个位置,在那里您可以轻松地分析它们并生成警报。一个潜在的选择是 Logstash(https://www.elastic.co/products/logstash),我们在第 4 章测量性能瓶颈中短暂提到过。

如果您喜欢更模块化的方法,并且想要记录性能计数器和指标,那么有 StatsD ,它监听 UDP 数据包,并推送到石墨进行存储和绘图。你可以在https://github.com/etsy/statsd买到,还有一些.NET 客户端,以及主存储库中的一个示例 C#代码。另一个收集日志的开源选项是 Fluentd(https://www.fluentd.org/)。

您可能希望使用消息队列进行日志记录,这样您就可以快速地将记录的事件放入队列并忘记它,而不是直接命中日志服务器。如果您直接调用一个应用编程接口(并且没有使用 UDP),那么请确保它是异步的和非阻塞的。您不想因为低效的日志记录而降低应用的速度。

也有云选项可用,尽管通常关于锁定的警告适用。AWS 有 CloudWatch ,你可以在https://aws.amazon.com/cloudwatch/了解更多。Azure Diagnostics 也类似,您可以将其与 Application Insights 集成;在了解更多信息。

还有其他可用的跨平台云服务,如 New Relic 或 Stackify,但这些服务可能相当昂贵,您可能希望将日志记录保留在自己的基础架构中。你可以把数据硬塞进分析软件,比如谷歌分析或者专注于隐私的 Piwik(它是开源的,可以自我托管),但是这些不太合适,因为它们是为稍微不同的目的设计的。

统计数字

当解释您收集的指标时,了解一些基本的统计数据有助于正确阅读它们。取一个简单的意味着平均值会误导你,可能没有其他一些特征重要。

当检测您的代码以收集元数据时,您应该大致了解特定日志记录语句的调用频率。这不一定是精确的,粗略的数量级近似(或费米估计)通常就足够了。

你应该尝试回答的问题是应该收集多少数据——全部还是随机抽样?如果您需要执行采样,那么您应该计算样本大小应该有多大。我们在第 7 章优化输入输出性能中介绍了与 SQL 相关的采样,这里的思路是相似的。性能统计要求与基准测试相同的严格程度,你很容易被误导或得出不正确的结论。

StatsD 包括对采样的内置支持,但是如果您想研究它们,还有许多其他方法可用。例如,在线流算法和水库是两个选项。性能方面需要牢记的重要一点是使用快速随机数生成器 ( RNG )。至于采样,这不需要加密保护;一个伪随机数发生器 ( PRNG )就可以了。英寸 NET 中,您可以使用new Random()作为 PRNG,而不是使用更安全的RandomNumberGenerator.Create()选项。有关如何使用这两者的更多示例,请参见第 8 章了解代码执行和异步操作

当看你的结果时,异常值可能比平均值更有趣。虽然中位数比平均数更有价值,但在这种情况下,你真的应该看百分位数,比如第 90、第 95、第 99百分位数。这些数据点只能代表数据的一小部分,但在一定范围内,它们会频繁出现。您希望针对这些最坏的情况进行优化,因为如果您的用户在 10%的时间内经历了超过 5 秒钟的页面加载(即使平均速度看起来很快),那么他们可能会去其他地方。

关于统计数据还有很多要说的,但是除了基本数据之外,还有收益递减。如果你的数学生疏了,那么复习一下可能是明智的(维基百科在这方面很棒)。然后,你可以探索一些更先进的技术,例如高性能的 HyperLogLog ( HLL )算法,它可以用很少的内存来估计一大组元素的大小。

Redis 支持 HLL 数据结构(使用PFADDPFCOUNTPFMERGE命令)。有关不同数据结构的更多信息,请参考第 8 章了解代码执行和异步 操作

This is only a brief introduction to performance logging, but there is much more for you to explore. For example, if you want to standardize your approach, then you can look into APDEX (http://www.apdex.org/ ), which sets a standard method to record the performance of applications and compute scores.

管理陈旧缓存

值得提供一个快速提醒,在如此复杂的情况下仍然要考虑简单的问题。太容易迷失在复杂的 bug 或性能调整的细节中,而错过显而易见的东西。

A good technique to help with this is rubber duck debugging , which gets its name from the process of explaining your problem to a rubber duck on your desk. Most of us have experienced solving a problem after asking for help, even though the other person hasn't said anything. The process of explaining the problem to someone (or something) else clarifies it, and the solution becomes obvious.

如果修复后出现问题,首先检查简单的事情。查看补丁是否已经实际交付和部署。您可能会看到缓存中的陈旧代码,而不是新版本。

管理缓存时,版本控制是帮助您识别陈旧资产的有用技术。您可以更改文件名或添加注释以包含唯一的版本字符串。这可以是语义版本 ( 版本)、国际标准化组织日期和时间戳或内容的散列。有关缓存破坏的更多信息,请参考第 9 章学习缓存和消息队列

SemVer is a great way of versioning your code because it implicitly captures information on compatibility and breaking changes. You can read more about SemVer at semver.org .

摘要

在这一章中,我们看到了每一个决定总是有不利的一面,每一个选择都有代价,因为没有什么是免费的。总会涉及到取舍,你需要意识到自己行为的后果,这可能是微小而微妙的。

关键的一课是采取深思熟虑和严格的方法来添加任何性能增强技术。测量对于实现这一点至关重要,但你也需要知道,如果你收集或解释不正确,数据会如何误导你。

在下一章中,我们将继续度量主题,并学习如何使用测试来监控性能回归。您将看到如何使用测试(包括单元测试)、自动化和持续集成来确保一旦您解决了一个性能问题,它就会一直保持下去。

十一、监控性能回归

本章将涵盖编写自动化测试来监控性能,以及将测试添加到持续集成 ( CI )和部署系统。通过不断检查回归,您将避免意外构建一个缓慢的应用。我们还将介绍如何在不强制系统离线的情况下安全地加载测试系统,以及如何确保测试尽可能地模拟真实使用情况。

本章涵盖的主题包括:

  • 压型
  • 负载测试
  • 自动化测试
  • 性能监控
  • 持续集成和部署
  • 真实的环境和类似生产的数据
  • 硒元素和幻影无头浏览器的用户界面测试
  • 转换优化的模拟/模拟测试
  • 云服务和托管
  • DevOps

您将看到如何自动执行性能监控和测试,这样您就不需要记得一直手动执行。您将学习如何在回归造成麻烦之前及早发现回归,以及如何安全地将其退回返工。

轮廓和测量

我们在这本书的开头,通过介绍第 4 章、测量性能瓶颈中的一些简单技术,强调了测量和分析的重要性。我们从头到尾都在延续这个主题,我们也将在这本书的结尾,因为衡量和分析可靠的证据是多么重要,这一点怎么强调都不为过。

在此之前,我们已经介绍了如何使用 scope 来深入了解 web 应用的运行。我们还展示了 Visual Studio 诊断工具和应用洞察软件开发工具包 ( 软件开发工具包)。还有一个值得一提的工具——前缀分析器,你可以在https://stackify.com/prefix/获得。

前缀是一个免费的基于网络的 ASP.NET 侧写,支持 ASP.NET Core。如果你想快速查看,他们的网站上有一个现场演示(在http://demo.prefix.io/),看起来如下:

您可能还想看看微软的 PerfView 性能分析工具,它用于的开发.NET Core。您可以从http://microsoft.com/download/details.aspx?id=28567下载 PerfView 作为压缩文件,只需提取并运行即可。分析的记忆是有用的.NET 应用等。当您启动 PerfView 时,它看起来像这样:

您可以将 PerfView 用于许多调试活动,例如,拍摄堆的快照或强制 GC 运行。我们这里没有空间详细介绍,但附带的说明很好,如果您需要更多信息,可以在 MSDN 的博客上找到指南,并在https://channel9.msdn.com/Series/PerfView-Tutorial 的9 频道上找到许多视频教程。

Sysinternals tools (https://docs.microsoft.com/en-us/sysinternals/ ) can also be helpful, but as they do not focus much on .NET, they are less useful in this context.

虽然这些工具很棒,但更好的是将性能监控构建到您的开发工作流中。尽可能实现自动化,使性能检查透明且常规,并在默认情况下运行。

手动过程是不好的,因为你可以跳过步骤,你很容易出错。你不会梦想通过在生产服务器上发送文件或直接编辑代码来开发软件,所以为什么不将性能测试自动化呢?

存在变更控制流程,以确保一致性并减少错误。这就是为什么使用一个源代码管理 ( SCM )系统,比如 Git,Mercurial,甚至是遗留的 Team Foundation 版本控制 ( TFVC )是必不可少的。拥有一个构建服务器并在每次推送时执行配置项构建,甚至拥有全自动光盘部署也是非常有用的。

Git 现在是 TFS 的默认版本控制系统,支持已经内置到 Visual Studio 中。除非你有非常具体的要求,否则 TFVC 是不鼓励的。您可以在https://www . visualstudio . com/en-us/docs/tfvc/comparison-git-tfvc上阅读更多关于这两个系统之间的差异(以及它们的分布式或集中式设计的比较)。如果你想从 TFVC 迁移到 Git,那么https://www . visualstudio . com/learn/migrate-from-tfvc-to-Git/有个指南。

Source control allows multiple people to work on a file simultaneously and merge the changes later. It's like Word's track changes feature, but actually usable. We assume that we're preaching to the converted and you already use source control. If not, stop reading right now and go install an SCM system.

如果在生产中部署的代码不同于您在本地工作站上的代码,那么您成功的机会就很小。这也是为什么 SQL 存储过程 ( SPs / 存储过程)很难使用的原因之一,至少没有严格的版本控制。在开发数据库上修改一个旧版本的服务点,不小心恢复一个错误修复,并以回归告终,这太容易了。如果您必须使用 SPs,那么您将需要一个版本控制系统,例如 ReadyRoll(Redgate 现在已经获得)。

由于这不是一本关于持续交付 ( 光盘)的书,我们将假设您已经在练习 CI 并拥有一个构建服务器,如 JetBrains、TeamCity、TFS、ThoughtWorks GoCD 和 CruiseControl.net,或者一个云服务,如 AppVeyor。也许你也在使用章鱼部署或 TFS 这样的工具来自动化你的部署。也许你甚至有自己的内部 NuGet feeds,使用诸如 Motley 傻瓜的 Klondike 之类的软件或诸如 MyGet 之类的云服务(它也支持 npm、Bower 和 VSIX 包)。

NuGet packages are a great way of managing internal projects. In Visual Studio 2017, you can see the source code of packages and debug into them. This means no more huge solutions, containing a ludicrous number of projects that are hard to navigate and slow to build in Visual Studio.

即使您遵循脚本,绕过流程并手动操作也会导致问题。如果它可以自动化,那么它可能应该是自动化的,这包括测试。

测试

测试对于生产高质量和高性能的软件至关重要。高效测试的秘密是使它变得简单、可靠和常规。如果由于与软件无关的问题(例如,环境问题)而导致测试困难或测试经常失败,那么将不会执行测试或忽略结果。这将导致你错过真正的问题,并运送你本可以轻松避免的错误。

有许多不同种类的测试,您可能熟悉用于功能验证的更常见的情况。在本书中,我们将主要关注与性能相关的测试。然而,这里的建议适用于许多类型的测试。

自动化测试

如前所述,提高几乎所有东西的关键是自动化。仅在开发人员工作站上手动运行的测试没有什么价值。当然,应该可以在桌面上运行测试,但这不应该是官方结果,因为不能保证它们会在服务器上通过(在服务器上,正确的功能更重要)。

Although automation usually occurs on servers, it can be useful to automate tests that run on developer workstations too. You can do this live unit testing in Visual Studio 2017 Enterprise Edition, and in other versions, you could use a plugin such as NCrunch . This runs your tests as you work, which can be very useful if you practice test-driven development (TDD ) and write your tests before your implementations.

强制测试的一种方法是在 TFS 使用门控签入,但这可能有点苛刻,如果您使用像 Git 这样的配置管理器,那么在分支上工作就更容易了,只需阻止合并,直到所有测试都通过。您希望鼓励开发人员尽早且经常地签入,因为这使得合并更加容易。

因此,长时间(一般不超过一天)将正在进行的功能放在工作站上是一个坏主意。

连续累计

配置项系统自动构建和测试您的所有分支,并将这些信息反馈到您的版本控制系统。例如,使用 GitHub API,您可以阻止拉请求的合并,直到构建服务器报告成功测试的合并结果。

Bitbucketgitlb都提供了称为 pipelines 的免费 CI 系统,所以除了用于源代码控制的系统之外,您可能不需要任何额外的系统,因为一切都在一个地方。GitLab 还提供了一个集成的 Docker 容器注册表,并且有一个开源版本可以在本地安装。有关 Docker 的更多信息,请参考前面的章节或本章后面的托管部分。

对于 CI 构建和单元测试,您可以使用 Visual Studio 团队服务做一些类似的事情。Visual Studio 还内置了 Git 服务。

这个过程对于单元测试很有效,因为单元测试必须快速,这样你才能得到早期的反馈。缩短迭代周期是提高生产率的好方法,你会希望滞后尽可能的小。

然而,在每个构建上运行测试并不适合所有类型的测试,因为不是所有的测试都能很快完成。在这种情况下,你需要一个额外的策略,以免减慢你的反馈循环。

There are many unit testing frameworks available for .NET, for example, NUnit, xUnit, and MSTest (Microsoft's unit test framework, now with a revamped v2), along with multiple graphical ways of running tests locally, such as the Visual Studio Test Explorer and the ReSharper plugin. People have their favorites, but it doesn't really matter what you choose because most CI systems will support all of them.

缓慢测试

有些测试很慢,但是即使每个测试都很快,如果你有很多测试,它们也很容易增加很长的时间。如果它们不能被并行化,并且需要按顺序运行,这一点尤其正确,因此您应该始终致力于让每个测试台独立运行,而不依赖于其他测试台。

将您的测试划分为重要的环是一个很好的实践,这样您至少可以在每个配置项构建中运行最关键的测试的子集。然而,如果你有一个大的测试套件或者一些不可避免的慢的测试,那么你可以选择一天只运行一次(也许是一夜)或者每周运行一次(也许是周末)。

有些测试本质上很慢,性能测试经常属于这一类,例如负载测试或用户界面 ( UI )测试。我们通常将其归类为集成测试,而不是单元测试,因为它们需要将您的代码部署到环境中进行测试,并且测试不能简单地运行二进制文件。

为了利用这种自动化测试,除了您的配置项系统之外,您还需要一个自动化部署系统。如果你对你的测试系统有足够的信心,那么你甚至可以让实时部署自动发生。如果您也使用功能切换来控制新功能的推出,这将非常有效。

在本书中,我们将不讨论配置项或自动化部署的实施细节。但是,我们将讨论功能切换、如何将性能测试应用于配置项流程,以及当您发现回归时该怎么做。

修复性能回归

如果您在单元测试阶段发现了一个性能问题,那么您可以简单地重做这个特性,但是这些问题更有可能在稍后的测试阶段出现。这使得补救问题变得更具挑战性,因为工作可能已经建立在它之上,并且可能在它之上还有其他提交。

正确的做法通常是在发现后立即或至少尽快退出回归。延迟只会让问题在以后更难解决,这就是为什么获得快速反馈和快速突出问题很重要。

重要的是要有纪律,并始终消除倒退,即使这可能是痛苦的。如果你偶尔让小的倒退进来,那么随着时间的推移,你很容易变得草率,让更严重的倒退进来,因为它开创了先例。

负载测试

负载测试是发现您的 web 应用可以支持多少并发用户的过程。您通常使用逐渐增加模拟负载的工具在测试环境中执行它,例如 JMeter(http://jmeter.apache.org/)。也许,如果你的测试系统面向网络,你会更喜欢使用兼容 JMeter 的云服务,比如 BlazeMeter,或者另一种选择,比如 Loader.io

负载测试可能需要很长时间,这取决于服务的规模,因为它可以被配置为持续进行,直到测试环境对用户来说变得慢得不可接受,或者崩溃并变得没有响应。您需要非常小心地进行负载测试,而不仅仅是从在使用过程中意外测试您的实时系统到破坏的角度。

您还需要警惕得到错误的结果,这可能会误导您得出结论,您的系统可以处理比实际更多的负载。平衡安全和现实这两个相互冲突的问题可能很困难。获得真实的结果很重要,但是您需要在这一点和不影响您的生产环境以及真实用户体验之间取得平衡。

现实主义

保持真实是性能测试的一个重要原则。如果你不使用现实的环境和工作量,那么你的结果可能比没有数据更糟糕,因为它们会误导你做出错误的决定。当你没有信息的时候,你至少知道自己在黑暗中,只是猜测。

我们将很快介绍工作负载和功能切换,包括如何从头开始实现自己的简单版本的示例。首先,让我们看看如何让您的测试环境代表产品。

现实环境

使用尽可能接近生产环境(或尽可能真实)的测试环境是确保可靠结果的良好步骤。您可以尝试使用一组较小的服务器,然后扩展您的结果以获得实时性能的估计。但是,这假设您对应用如何扩展以及什么样的硬件限制是瓶颈有着深入的了解。

更好的选择是使用您的实时环境,或者更确切地说,使用将成为您的生产堆栈的环境。首先创建一个与 live 相同的临时环境,然后将代码部署到其中,然后运行完整的测试套件,包括一个全面的性能测试,确保其行为正确。一旦你高兴了,你就可以简单地交换试运行和生产,也许可以使用 DNS 或 Azure 试运行槽

下图显示了如何通过将登台变成生产来首次发布到登台并投入使用:

您的旧实时环境现在要么成为您的测试环境,要么如果您使用不可变的云实例,那么您可以简单地终止它并启动一个新的转移系统。这个概念被称为蓝绿色部署,但遗憾的是,具体的实现说明超出了本书的范围。有关 AWS 和 Azure 中云托管的更多信息,请参见作者在https://unop.uk/的网站。

你不一定非要在一次大爆炸中把所有的用户都转移过去;你可以先移动几个,以测试是否一切都是正确的。我们将在本章的功能切换部分简要介绍这一点。

现实的工作量

执行真实测试的另一个重要部分是使用真实数据和真实工作负载。合成数据通常不会完全运行一个系统,也不会发现奇异的边缘案例。您可以使用模糊化来生成随机数据,但是如果您有一个生产系统,那么您可以简单地使用来自这个系统的数据并解析您的日志来生成一个真实的工作负载来重放。

显然,不要在实时系统上重放可能修改用户数据的操作,并警惕数据隐私问题或任何可能产生外部事件的操作,例如发送大量电子邮件或向信用卡收费。

您可以使用一个专用的测试系统,但是您仍然需要小心地排除任何没有测试版本的外部 API。

另一种方法是使用虚拟用户进行测试,如果你不介意你的用户发现假货并可能与他们互动的话。这种方法的一个例子是网飞的邪教“示例秀”,这是一个员工拿着笔记本电脑在办公室里跑来跑去的自制视频。

功能切换

假冒用户或测试环境的一种替代方法是在真实的实时环境中运行性能测试,在真实用户与您的 web 应用交互时使用他们。您可以使用功能切换来逐步向一小部分用户推出新功能,并监控系统是否负载过大。如果一切看起来正常,那么可以继续卷展栏;否则,您可以回滚。

You may hear similar ideas referred to by other names, such as feature flags , feature toggles , canary releases , or branch by abstraction . Some of these may have subtly different meanings, but the general guiding principle of gradually releasing a change is much the same.

下图显示了如何通过部署一个新功能而不是最初启用它来逐步将用户迁移到该功能。您的第一批用户可能是工作人员,以便及早发现任何问题:

一旦所有用户开始使用新功能,您就可以安全地删除旧功能(以及功能开关本身)。定期这样整理是个好主意,这样可以避免杂乱,让以后的工作更轻松。

如果您发现您的系统在您慢慢增加新功能的用户百分比时负载过大,那么您可以停止部署并防止您的服务器过载和无响应。然后,您有时间进行调查,要么退出变更,要么增加可用资源。

这个主题的一个变化是,当一个新特性需要数据迁移时,这种迁移是计算或网络密集型的,例如,将用户提交的视频迁移到一个新的家庭,并在此过程中对它们进行转码。在这种情况下,您正在监控的过度负载只是暂时的,您不需要退出某个特性。您只需确保迁移率足够低,不会对您的基础架构造成过度负担。

虽然新功能通常在代码中分支到,但是如果使用蓝绿色部署,您可以在网络级别执行切换。这被称为金丝雀版本,可以在 DNS 或负载均衡器级别完成。然而,具体的实现细节也超出了本书的范围。关于域名系统的更多信息,请参见作者在https://unop.uk/的网站。

你可以在网上找到许多开源特性转换库,或者你可以自己编写,我们稍后会告诉你如何做。FeatureToggle是的功能切换库.NET,并且它支持.NET Core 从第 4 版开始(https://github.com/Jason-roberts/FeatureToggle)。也有付费云服务可用,如LaunchDarkly,提供简单的管理界面。

Library and framework support for .NET Core and ASP.NET Core change rapidly, so check https://github.com/jpsingleton/ANCLAFS for the latest information.

为了说明功能切换,让我们为 ASP.NET Core 网应用构建我们自己的极其简单的实现。首先,我们采用默认的现有主页视图(Index.cshtml)并进行复制(IndexOld.cshtml)。然后我们对Index.cshtml进行修改,但是这些对于本演示的目的来说并不重要。

HomeController.cs中,我们改变逻辑,在相对较小的时间百分比内返回新视图或旧视图,否则。原始代码简单如下:

public IActionResult Index() 
{ 
    return View(); 
}

我们更改此操作,以便在四分之一的情况下返回新视图,如下所示:

public IActionResult Index() 
{ 
    var rand = new Random(); 
    if (rand.Next(99) < 25) 
    { 
        return View(); 
    } 
    return View("IndexOld"); 
} 

这段代码从一百个中挑选一个随机数,如果它小于25,那么它就加载新的视图。显然,您不会像这样硬编码值,您可能会使用数据库来存储配置,并使用 web 管理页面来控制它。

如果您在浏览器中加载页面,那么四分之三的情况下,您应该会得到原始页面,如下所示:

然而,大约每四页就有一页加载;您将获得新页面,如下所示:

我们移除了转盘,将图像加载数量减半。您可以看到浏览器开发工具的不同之处,例如,火狐对原始的性能分析如下:

然而,新版本的性能分析如下所示:

可以看到图像的数量减少了,这就减少了页面大小和所需的请求总数。

These measurements were taken with the hosting environment set to Production . If you remain with the default of Development , then your results will differ. Refer to the following documentation page to know how to change this:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments

一旦新视图逐步向所有用户推出(没有性能问题),操作方法代码就可以恢复到原始状态并删除旧视图。显然,这是一个微不足道的例子。希望您现在可以看到如何执行功能切换。

使用功能切换以这种方式展开对于性能测试来说很有效,但是您需要确信您的新代码在功能上是正确的,或者至少可以安全部署。但是,在某些情况下,您也可以通过扩展这种方法并执行实验来测试功能。

科学实验

如果您将特性切换卷展栏带到它的逻辑结论,那么您可以为一小部分用户打开一个新的重构版本,但不会实际公开输出。您将作为控件并行运行现有版本,并向用户显示这一点。但是,您将收集两个版本的结果,包括性能数据。然后,您可以比较它们,以确保新版本(希望具有更高的性能)是正确的,并且与现有行为一致。

GitHub 有一个开源的 Ruby 库,叫做Scientist,可以用来成功重构权限和合并代码。在这个过程中,GitHub 发现了现有的遗留 bug,发现了缺失的特性,优化了数据库查询,并交换了一个搜索系统。Scientist不仅显示新代码的正确性,还显示其相对于旧实现的性能。

An important concern with Scientist (or any method using this experimentation approach) is to not change data. All operations should be read-only and should have no side effects; otherwise, you will perform the modification more than once, which could have undesirable consequences.

GitHub 员工 Phil Haack 有一个Scientist的. NET 端口,叫Scientist.NET,可以在https://github.com/github/Scientist.net获取。它支持.NET Core,你可以通过 NuGet 来安装,那里只是叫Scientist。查看http://ANCLAFS.com了解最新信息。

这种在用户身上做实验的想法,类似于 A/B 测试的营销理念,用来发现转化有效性。但是,使用Scientist,您通常不会向用户显示不同的输出,甚至不会改变输出,您只需为您启用的用户子集记录新的输出。

空调测试

A/B 测试类似于功能切换,但我们通常使用它来测试不同网页设计的有效性,以及它们如何影响转换漏斗分析。我们通常将它用于数字营销,而不是软件开发,因为它不太关注什么是正确的,而更多地关注客户喜欢什么。然而,潜在的技术原理可与功能切换和实验相媲美。

在一个简单的例子中,你为一半的访问者提供旧版本的页面,另一半,一个旨在增加参与度的新版本。然后记录有多少访问者点击或对每个变体执行一些操作。如果新版本表现更好,那么你就保留它,并向所有人推广。但是,如果它转换得更差,您可以回滚到旧版本,然后重试。

您可以看到,这与功能切换的工作原理非常相似,您可以使用相同的工具来完成这两项工作。唯一的区别是关于你在衡量什么——用户分析还是你的基础设施的健康状况。

A/B 测试通常不用于后端功能,仅用于评估 UI 替代方案。但是,功能测试您的 web 界面是不同的,所以现在让我们来介绍 UI 测试的基础知识。

用户界面测试

应用中有一个领域传统上很难测试,那就是 UI 。在网络应用中使用的图形用户界面 ( 图形用户界面)尤其如此。其中一个原因是用户在显示界面时通常有很大的灵活性,尤其是对于网络浏览器。一个简单的基于像素的测试方法将会非常脆弱。

您需要将应用设计为易于测试,UI 也不例外。虽然测试设计不佳的遗留应用的用户界面是可能的,这甚至可能是最简单的测试方法,但是如果您考虑提前进行用户界面测试,那么生活将会容易得多。例如,在你的 HTML 文档对象模型 ( DOM )元素中包含合理的标识,使得测试更加简单,也不那么脆弱。

在自动化单元和集成测试的基础上,从 web 浏览器的角度检查您的应用可能是一个有用的附加步骤。您不仅可以使用它来测试功能正确性和回归,还可以测量客户端性能,这一点越来越重要。有许多可用的用户界面测试工具,其中大多数可以集成到您的配置项构建管道中,以实现测试自动化。

网络用户界面测试工具

最受欢迎的网络测试工具之一是,它允许你使用网络驱动程序轻松编写测试和自动化网络浏览器。除了测试之外,硒还可以用于其他许多任务,你可以在http://docs.seleniumhq.org/上了解更多。

WebDriver is a protocol to remotely control web browsers, and you can read about it at https://w3c.github.io/webdriver/webdriver-spec.html .

Selenium 使用真正的浏览器,也就是你的用户用来访问你的 web 应用的版本。这使得获得有代表性的结果变得非常好,但是如果它以无人值守的方式从命令行运行,就会引起问题。例如,您可能会发现测试服务器的内存中充满了已经超时的死浏览器进程。

您可能会发现使用专用的无头测试浏览器更容易,虽然它与用户看到的不完全相同,但更适合自动化。当然,最好的方法是两者结合使用,也许可以先运行无头测试,然后在带有 WebDriver 的真实浏览器上运行相同的测试。

最著名的无头测试浏览器之一是 PhantomJS 。这是基于网络工具包引擎,所以它应该会给你类似于 Chrome 和 Safari 的结果。除了测试之外,PhantomJS 对很多事情都很有用,比如捕捉截图,你可以用很多不同的测试框架来驱动它。顾名思义,你可以用 JavaScript 控制 PhantomJS,你可以在http://phantomjs.org/了解更多。

WebKit is an open source engine for web browsers, which was originally part of the KDE Linux desktop environment. It is mainly used in Apple's Safari browser, but a fork called Blink is used in Google Chrome, Chromium, and Opera. You can read more at webkit.org .

基于不同引擎的其他自动测试浏览器也是可用的,但是它们有一些限制。比如slimmerjs(https://slimerjs.org/)是基于火狐使用的壁虎引擎,但并不是完全无头。

您可能希望使用更高级别的测试工具,而不是直接编写浏览器引擎的脚本。提供许多有用抽象的实用工具之一是Casperjs(http://casperjs.org/),它支持在 PhantomJS 和 SlimerJS 上运行。

另一个是水豚,可以让你在 Ruby 中轻松模拟用户交互。它支持 Selenium、WebKit、 Rack 和 PhantomJS(通过 Poltergeist),尽管它更适合 Rails 应用。你可以在http://teamcapybara.github.io/capybara/阅读更多。

还有TrifleJS(http://triflejs.org/)使用了.NET WebBrowser类(Internet Explorer 三叉戟引擎),不过这是一个正在进行的工作。此外,还有Watir(http://watir.com/),这是一套针对 Internet Explorer 和 WebDriver 的 Ruby 库。不过这两个都没更新过一段时间,最近 IE 变化很大。

Microsoft Edge (codenamed Spartan) is the new version of IE, and the Trident engine was forked to EdgeHTML . The JavaScript engine (Chakra) was open sourced as ChakraCore (https://github.com/Microsoft/ChakraCore ).

您使用什么浏览器引擎应该没有太大关系,作为自动化测试的第一步,PhantomJS 会很好地工作。在使用无头浏览器之后,您总是可以使用真正的浏览器进行测试,可能使用 Selenium 或者使用 WebDriver 的 PhantomJS。

When we refer to browser engines (WebKit/Blink, Gecko, or Trident/EdgeHTML), we generally mean only the rendering and layout engine, not the JavaScript engine (SFX/Nitro/FTL/B3, V8, SpiderMonkey, or Chakra/ChakraCore).

您可能仍然希望使用一个实用工具(如 CasperJS)来简化编写测试,并且您可能还需要一个测试框架,如Jasmine(https://jasmine.github.io/)或QUnit(http://qunitjs.com/)。您也可以使用同时支持 Jasmine 和 QUnit 的测试运行程序,例如Chutzpah(http://mmanela.github.io/chutzpah/)。

您可以将您的自动化测试与许多不同的配置项系统集成在一起,例如詹金斯或捷脑团队城。如果你更喜欢云托管的选项,那么还有 Travis CI(https://travis-ci.org/)和 AppVeyor(http://appveyor.com/),也适合打造.NET 应用。

您可能更喜欢从部署系统中运行集成和用户界面测试,例如,验证章鱼部署中的成功部署。也有专用的、基于云的网络应用用户界面测试服务,如 BrowserStack(https://www.browserstack.com/)。

自动化用户界面性能测试

自动化的用户界面测试显然非常适合检查功能回归,但是对于性能测试也很有用。您可以通过编程方式访问浏览器开发工具中网络检查器提供的相同信息。

您可以将YSlow(http://yslow.org/)性能分析器与 PhantomJS 集成,使您的 CI 系统能够在每次提交时检查常见的 web 性能错误。YSlow 来自雅虎!,并且它提供了用于识别不良做法的规则,这些不良做法会降低用户的 web 应用速度。这与谷歌的 PageSpeed Insights 服务(你可以通过它的应用编程接口实现自动化)类似。

然而,YSlow 已经很老了,最近在网络开发方面事情有了进展,例如 HTTP/2。一个现代的替代方案是的长途汽车,你可以在https://github.com/sitespeedio/coach了解更多。你也应该看看他们的其他开源工具,比如使用石墨和 Grafana 的https://dashboard.sitespeed.io/的仪表板。

您还可以导出网络结果(以行业标准的 HAR 格式)并以您喜欢的方式进行分析,例如,以瀑布格式以图形方式可视化它们,就像您可以使用浏览器开发工具手动进行的那样。

The HTTP Archive (HAR ) format is a standard way of representing the content of monitored network data for exporting to other software. You can copy or save as HAR in some browser developer tools by right-clicking a network request.

保持警惕

无论何时执行测试,尤其是 UI 或性能测试,都会得到嘈杂的结果。可靠性不是完美的,总会有不是由于代码中的错误而导致的失败。你不应该让这些误报导致你忽略失败的测试,尽管最简单的方法可能是禁用它们,但正确的做法是让它们更可靠。

The scientifically minded know that there is no such thing as a perfect filter in binary classification, and always look at the precision and recall of a system. Knowing the rate of false positives and negatives is important to get a good idea of the accuracy and tradeoffs involved.

为了避免测试疲劳,让开发人员参与进来并灌输修复失败测试的责任是有帮助的。你不希望每个人都认为这是别人的问题。应该很容易看出谁在版本控制中通过提交破坏了构建,然后他们的工作就是修复失败的测试(并购买饼干)。

你可以从中获得一些乐趣,并创造出比壁挂式仪表盘更有趣的东西。虽然拥有信息辐射器是有用的,如果你对它们不敏感的话。今天有很多便宜的物联网 ( 物联网)硬件,可以让你把一些有趣的东西变成建筑故障警报。例如一个阿尔杜伊诺控制的交通灯,一个 ESP8266 操作的泡沫导弹炮塔,或者一个树莓 Pi 动力的动画人物。思路见作者网站(https://unop.uk/)。

DevOps

DevOps 的实践是开发、运营和质量保证测试团队都无缝协作。虽然 DevOps 更多的是一种文化上的考虑,但是仍然有很多工具可以帮助自动化。我们之前已经介绍了其中的大部分内容,现在唯一缺少的部分是调配和配置基础架构,然后在使用过程中对其进行监控。

当使用自动化和技术(如功能切换)时,有一个良好的环境视图是非常重要的,这样您就知道所有硬件的利用率。监控时,良好的工具很重要,您希望能够轻松查看每台服务器的重要统计数据。这将至少包括中央处理器、内存和磁盘空间消耗,如果这些指标偏离其允许的范围,您将需要设置警报来提醒您。

DevOps 工具

DevOps 工具的主要主题之一是将基础设施定义为代码。这个想法是,你不应该手动执行一项任务,比如设置一个服务器,当你可以创建软件来为你做的时候。然后,您可以重用这些配置脚本,这不仅可以节省您的时间,还可以确保所有机器都是一致的,没有错误或遗漏的步骤。

准备金提取

有许多系统可用于调试和配置新机器。一些流行的配置管理自动化工具有ansi ble(https://www.ansible.com/)厨师(http://chef.io/)和木偶(http://puppet.com/)。

并非所有这些工具在 Windows 服务器上都很好用,部分原因是 Linux 更容易自动化。但是,您可以在 Linux 上运行 ASP.NET Core,并且仍然在 Windows 上开发,使用 Visual Studio 并在虚拟机或容器中进行测试。为虚拟机开发是一个很好的想法,因为它解决了环境设置中的问题以及“在我的机器上工作”但不在生产中的问题。

Vagrant (http://vagrantup.com/ ) is a great command-line tool to manage developer VMs. It allows you to easily create, spin up and share developer environments.

如果您将基础架构创建为代码,那么您的脚本就可以像应用代码一样进行版本控制和测试。我们会在离题太远之前停下来,但重点是,如果您有可靠的环境,可以轻松地验证、实例化和执行测试,那么配置项就容易得多。

监视

监控是必不可少的,尤其是对于 web 应用,有很多工具可以帮助它。一个流行的开源基础设施监控系统是 Nagios(T2)和 http://nagios.org/。另一个更现代的开源警报和度量工具是普罗米修斯(https://prometheus.io/)。

如果你使用云平台,那么会有内置的监控,比如 AWS CloudWatch 或者 Azure Diagnostics。还有直接监控你的网站的云服务,比如Pingdom(https://www.pingdom.com/)Uptime Robot(https://uptimerobot.com/)Datadog(https://www.datadoghq.com/)PagerDuty(https://www.pagerduty.com/)。

您可能已经有了一个系统来衡量可用性,但是您也可以使用相同的系统来监控性能。这不仅有助于确保快速响应的用户体验,还可以提供即将发生故障的早期预警信号。如果你积极主动,采取预防措施,那么你就能省去很多被动灭火的麻烦。

这不是一本关于操作的书,但它有助于在设计时考虑应用支持需求。开发、测试和操作并不是相互竞争的学科,如果你作为一个团队工作,而不是简单地把一个应用扔过栅栏,说它“在测试中工作,现在操作有问题”,你会更经常地成功。

作战

非常值得考虑各种托管选项的含义,因为如果开发人员不能轻松访问他们需要的环境,那么这将降低他们的敏捷性。他们可能不得不解决可用性问题,并最终使用不足或不合适的硬件,这将阻碍进展并导致未来的维护问题。否则他们的工作将会受阻,交货时间将会推迟。

除非您是一个非常大的组织,否则出于可靠性、灵活性和成本的原因,内部托管通常不是一个好主意。除非您有一些非常敏感的数据,否则您可能应该使用数据中心。

您可以将服务器放在数据中心的同一个位置,但是您需要工作人员随时待命来解决硬件问题。或者你可以租一台物理服务器,在裸机上运行你的应用,但你可能仍然需要远程手在机器上执行重置或其他任务。

最灵活的情况是租用自助虚拟机,俗称云托管。有许多主机公司提供这种服务,但大公司是亚马逊、谷歌和微软。

微软的 Azure 显然是. NET 商店的选择,自推出以来,与最初的产品相比,它有了很大的改进。其平台即服务 ( PaaS )为主机.NET 应用是最完善的,也是最容易快速运行的。它还提供了许多超越简单虚拟机的额外服务。

然而.NET Core 和 ASP.NET Core 是新类型的框架,它们不仅仅针对 C#开发人员,他们通常会选择微软提供的默认选项。目标市场是可能习惯于选择替代平台和开源框架的开发人员。因此,涵盖人们不熟悉的其他选项是有意义的.NET 可能更熟悉。

The difference between PaaS and Infrastructure as a Service (IaaS ) is that PaaS is higher level and provides a service to run applications, not computers. IaaS simply provides you with a VM. However, with PaaS, this is abstracted away, and you don't need to worry about setting up and updating an instance.

亚马逊网络服务 ( AWS )是最老牌的云主机,它一开始只提供 IaaS,尽管现在他们提供 PaaS 选项。比如 Elastic Beanstalk 支持. NET,即使 Windows(和 SQL Server)支持有所提升,但仍然不是一流的,Linux 显然是他们的主要平台。

然而现在.NET Core 和 SQL Server 在 Linux 上运行,这可能不是什么大问题。您将为 Windows 服务器实例支付额外费用,但您也将为一些企业 Linux 发行版支付更多费用(红帽SUSE )。但是,您可以运行其他 Linux 发行版,如 UbuntuCentOS ,而无需支付额外费用。

谷歌云平台过去由应用引擎 ( GAE )组成,这是一个相当严格的 PaaS,但它变得越来越灵活。他们现在提供名为计算引擎 ( GCE )的通用 IaaS,还有一个新的灵活版本的 GAE,更像 GCE。这些新产品很有用,因为传统上你无法运行.NET,他们还提供其他产品,如可优先购买的虚拟机和云 CDN(HTTPS 无需额外费用)。

Azure 可能仍然是您的最佳选择,它与其他微软产品(如 Visual Studio)集成良好。然而,有健康的竞争是值得的,因为这让每个人都不断创新。经常查看所有期权的定价(定期变化)肯定是个好主意,因为根据你运行的工作量,你可以节省很多钱。

If you are eligible for Microsoft's BizSpark program, then you can get three years of Azure credits (suitable to host a small application). You also get a Visual Studio subscription for free software licenses (previously called an MSDN subscription).

无论您选择使用哪种主机提供商,明智的做法是避免供应商锁定,并使用通用服务,您可以轻松地将其替换为其他服务。例如,使用托管开源软件,如 PostgreSQL、Redis 或 RabbitMQ,而不是等效的定制云提供商产品。您还可以采用弹性多云方法来保护自己免受单个提供商中断的影响。有关云托管提供商的比较,请参见作者的网站(https://unop.uk/)。

Docker 是一项很好的技术,因为许多不同的云服务都支持它。例如,您可以在 Azure 容器服务、Docker Cloud、AWS EC2 容器服务和 Google 的 Kubernetes 容器引擎上运行同一个容器。

Docker 也在 Windows 上运行(使用 Hyper-V),在 Visual Studio 2017 中,您可以部署并调试一个容器。这可以在 Linux 上运行 ASP.NET Core,当您准备好部署时,您可以直接投入生产,并相信它会像在您的机器上一样工作。你可以在http://docker.com/Microsoft阅读更多关于视窗 Docker 的内容。

When choosing a cloud (or even a region), it's important to not only consider the monetary cost. You should also factor in environmental concerns, such as the main fuel used for the power supply. For example, some regions can be cheaper, but this may be because they use dirty coal power, which contributes to climate change and our future security.

摘要

在本章中,您看到了如何将自动化测试集成到配置项系统中,以便监控性能回归。您还学习了一些策略来推出变化,并确保测试准确反映现实生活。我们还简要介绍了 DevOps 实践和云托管提供商的一些选项,它们一起使持续的性能测试变得更加容易。

在下一章中,我们将总结本书涵盖的所有内容,并建议一些需要进一步研究的领域。我们将巩固我们目前所学的知识,给你一些有趣的想法去思考,思考未来的可能.NET,并考虑平台正在采取的令人兴奋的方向。

十二、前方的路

这一章总结了你在这本书里学到的东西。它刷新了性能的主要原则,并提醒您应该始终保持务实。我们将回顾为什么你不应该仅仅为了优化而优化,为什么你应该总是衡量问题和结果。本章还介绍了一些更先进、更奇特的技术,如果您需要更快的速度或者是一个认真的性能爱好者,您可能会考虑学习这些技术。

本章涵盖的主题包括以下内容:

  • 前几章的摘要
  • 本地工具链
  • 可选的中央处理器架构,如 ARM
  • 高级硬件(图形处理器、FPGAs 和专用集成电路)
  • 机器学习和人工智能
  • 大数据和 MapReduce
  • 奥尔良虚拟演员模型
  • 自定义传输层
  • 高级散列函数
  • 库和框架支持
  • ASP.NET Core 的未来

我们将通过刷新您对前几章课程的记忆来强化如何评估和解决性能问题。您还将了解其他可帮助您提供高性能的先进技术,您可能希望进一步研究这些技术。最后,我们将强调库和框架支持什么.NET Core 和 ASP.NET Core,我们将尝试假设这些令人兴奋的平台未来可能的发展方向。

回顾我们学到的东西

让我们简要回顾一下本书前面的内容。

第 1 章ASP.NET Core 2 有什么新进展?,我们介绍了与第一个主要版本相比,ASP.NET Core 第二个版本发生了哪些变化,并重点介绍了 C# 6.0 和 C# 7.0 中提供的一些新功能。然后,在第二章为什么性能是一个特性中,我们讨论了这本书的基本前提,并向您展示了为什么您需要关心软件的性能。在第 3 章设置您的环境中,我们演示了如何在 Windows、macOS 和 Linux 上开始使用 ASP.NET Core。

第 4 章测量性能瓶颈中,我们向您展示了解决性能问题的唯一方法是仔细测量您的应用。然后,在第 5 章修复常见性能问题中,我们查看了一些最常见的性能错误以及如何修复它们。

在此之后,我们在第 6 章解决网络性能中进行了更深入的探讨,并深入到了支撑所有网络应用的网络层。然后,在第 7 章优化输入/输出性能中,我们重点讨论了输入/输出以及这对性能的负面影响。

第 8 章理解代码执行和异步操作中,我们跳到了错综复杂的 C#代码中,研究了它的执行如何改变性能。然后在第九章学习缓存和消息队列中,我们初步看了缓存,大家普遍认为缓存相当难。然后,我们研究了消息队列作为一种构建分布式可靠系统的方法。

第 10 章性能增强工具的缺点中,我们集中讨论了我们之前讨论过的技术的缺点,因为没有什么是免费的。然后在第 11 章监控性能回归中,我们再次查看了测量性能,但是在这种情况下,从自动化、持续集成 ( CI )和 DevOps 的角度来看。

进一步阅读

如果你已经读了这么多,那么你可能会想要一些其他东西的指针来研究和阅读。在本章的剩余部分,我们将突出一些有趣的主题,您可能想进一步研究,但我们无法在本书中完全涵盖。你也可以访问作者的网站(在https://unop.uk/)获取更多话题的报道。

土生土长

老 ASP.NET 的问题之一是它真的很慢,这就是为什么 ASP.NET Core 的主要指导原则之一是性能。已经取得了令人印象深刻的进展,但是还有很多进一步增强的机会。

最有希望的领域之一是本地工具链,不幸的是,它被延迟了。但是,应该在之后发货.NET Core 2.0,并且它已经在 Windows 商店上用于 UWP 应用。这不同于中已有的自包含发布和交叉编译.NET Core,因为它编译成机器本地的二进制文件,而不是可移植的 IL 指令。

以前,如果您想从托管调用非托管本机代码.NET 代码,您将不得不使用平台调用 ( PInvoke ,但这有性能开销和安全问题。即使您的原生代码更快,开销通常意味着它不值得操心。

本地工具链应该提供本地级别的性能,但是要有托管运行时的安全性和便利性。超前编译很吸引人,也很有技术含量,但结果是,如果我们知道目标体系结构,那么它可以提供性能提升。我们也可以用更慢的编译来换取更快的执行,因为它不必在运行时发生。

还可以针对可能提供特殊性能特性和指令的不同处理器进行优化,例如,瞄准低能耗 ARM 芯片,而不是通常的英特尔风格处理器。

处理器架构

通常,在编写桌面或服务器软件时,您会以基于英特尔的体系结构(如 x86 或 x64)为目标。然而,基于 ARM 的芯片越来越受欢迎,它们可以提供惊人的能效。如果软件是专门为他们优化的,那么他们也可以提供出色的性能。

例如,用于教授计算的 Scratch 图形编程语言已经针对树莓 Pi 3 进行了优化,现在它的运行速度大约是英特尔 Core i5 的两倍。其他软件应用也针对 ARM 处理器进行了优化,例如 Kodi 开源媒体播放器。

ARM Holdings 只是一家知识产权公司,他们自己不生产任何处理器。其他公司,如苹果和博通,许可设计或架构,并制造他们的片上系统产品。

这意味着有许多不同的芯片可以使用,它们运行 ARM 架构和指令集的多个版本。除非您选择特定的平台,否则这种碎片化会使支持变得更加困难。

Windows 10 IoT(物联网)核心运行在树莓 Pi(版本 2 和 3)上,可以使用标准的新开箱软件 ( NOOBS )安装程序进行设置。Windows 10 IoT Core 不是一个运行普通应用的完整桌面环境,但它确实允许您制作硬件项目,并使用 C#和. NET 对其进行编程。但是,对于 web 应用,您会希望运行。一个 Linux 发行版上的 NET Core,比如 Raspbian(主树莓 Pi OS,基于 Debian )。

硬件很难

我们之前在第 8 章理解代码执行和异步操作中提到了可以用于计算的附加硬件,包括图形处理单元 ( 图形处理器)、现场可编程门阵列(FPGA)和专用集成电路 ( 专用集成电路)。

这些设备不仅可以用于特定的处理任务,还可以用于存储。例如,如果主内存耗尽,您可以从图形处理器借用内存。然而,这种技术不再像过去内存更有限时那样需要了。

你可能听说过随机存取存储器,这是一种使用标准随机存取存储器作为永久存储的存储器(带有备用电池)。然而,随着固态硬盘(基于闪存)速度的提高和容量的增加,这些变得不那么重要,以至于在许多常见任务中取代了机械驱动器。

您仍然可以购买高性能存储区域网络,但它们可能基于闪存而不是内存。如果你使用内存作为你的主存储器(例如,使用 Redis),那么使用纠错码 ( ECC )内存是很重要的。ECC 内存更贵,但更适合服务器使用。然而,一些云实例不使用它,或者很难找到它是否被提供,因为它没有在规范中列出。如果没有这些,你很可能会逃脱惩罚,因为在现实中,腐败是非常罕见的,但是你仍然应该有和解的程序来抓住这些和其他错误。

定制计算硬件的一个应用是机器学习 ( ML ),特别是 ML 的使用多级神经网络的深度学习分支。近年来,这项技术取得了令人印象深刻的进步,这导致了自动驾驶汽车等事物的出现。ML 应用可以很好地利用非 CPU 处理,尤其是 GPU,NVIDIA 提供了许多工具和库来帮助实现这一点。

谷歌构建了一个定制的专用集成电路,称为张量处理单元,以加速他们的张量流机器学习库和云服务。你可以在https://www.tensorflow.org/https://cloud.google.com/ml-engine/了解更多。

机器学习

不仅可以用人工智能 ( AI )代替司机等工作,比如呼叫中心工作人员甚至一些医生的任务,还可以在自己的 web 应用中使用一些基本的 ML 提供与客户相关的产品建议或者分析营销效果,就像亚马逊或者网飞做的那样。

你甚至不需要自己构建 ML 系统,因为你可以使用 Azure ML 这样的云服务。这允许您使用图形拖放界面来构建您的 ML 系统,尽管您也可以使用 Python 和 r。

你仍然需要了解一点数据科学,比如二进制分类和训练数据的基本原理,但即使这样,它也大大降低了进入的障碍。然而,如果你想充分探索 ML 和大数据的可能性,那么你可能需要一个专门的数据科学家。

你可以在https://studio.azureml.net/试用 Azure ML,甚至不需要注册。下面的截图显示了一个类似的例子:

如果你不想为自己构建一个推荐引擎,那么在 Azure 中有一个现成的推荐 API。Azure 还提供用于面部和语音识别、情感和情绪分析、内容调节、文本/语音翻译等的 API。其中一些系统中的模型已经在大量数据上进行了训练,因此可以在不构建自己的语料库的情况下使用它们。

大数据和 MapReduce

如今,大数据可能是一个被过度使用的术语,有时被描述为大的东西通常更像是中等数据。大数据是指当你有太多的信息,很难在一台机器上处理,甚至存储。传统的方法经常随着大数据而崩溃,因为它们不适用于当今常见的自动获取的巨大数据集。例如,物联网传感器或我们与在线服务的交互不断收集的数据量可能非常大。

大数据和最大似然的一个警告是,尽管它们擅长在大数据点集中寻找相关性,但你不能用它们来寻找原因。您还需要注意数据隐私问题,并非常小心,不要在某人采取行动之前就对其做出判断,而只是基于预测的倾向。

Anonymizing data is incredibly difficult and is not nearly as simple as removing personal contact details. There have been many cases of large "anonymized" datasets being released, where individuals were later easily identified from the records.

一种对分析大数据有用的技术是 MapReduce ,这是一种简化操作的方法,适合在分布式基础设施上并行运行。MapReduce 的一个流行实现是 Apache Hadoop,您可以在 Azure 中使用它和 HDInsight ,它也支持相关工具,包括 Apache Spark 和 Apache Storm 。处理大型数据集的其他选项包括谷歌的云大表或大查询

您可以在门户中看到 Azure HDInsight 的可用选项。火花如下图所示:

从这个截图可以看出,Spark 只在 Linux 上可用。Hadoop 更加成熟,也可以在 Windows 上使用,如下图所示:

下一张截图显示,Storm 也可用,但不在高级集群层(与 Spark 相同):

You can read more about HDInsight at https://docs.microsoft.com/en-us/azure/hdinsight/ .

奥尔良

另一个有趣的项目是来自微软的一个名为 Orleans 的开源框架,这是一个分布式虚拟演员模型,用于为一些 Halo Xbox 游戏的云服务提供动力。这意味着,如果您通过将逻辑分成独立的参与者来构建您的系统,这将允许它根据需求轻松扩展。

在奥尔良,演员被称为谷物,你可以通过从接口继承来用 C#编写他们。然后由一个名为筒仓的奥尔良服务器执行。谷物可以保存到存储器中,如 SQL 或 Azure Tables ,以保存其状态并在以后重新激活。奥尔良还可以利用债券串行化器提高效率。

不幸的是,奥尔良目前不支持.NET Core。计划支持奥尔良 2.0 版本,这是目前正在进行的工作。奥尔兰允许你用低延迟编写简单且可扩展的系统,你可以在http://dotnet.github.io/orleans/阅读更多。

定制运输

第 6 章寻址网络性能中,我们从介绍 TCP/IP 开始,简要提到了用户数据报协议 ( UDP )。我们还介绍了传输层安全性 ( TLS )加密以及如何在获得性能优势的同时最大限度地降低安全连接的影响。

UDP 比传输控制协议 ( TCP )更简单快捷,但是你要么不需要关心可靠的交付(多人游戏和语音/视频聊天),要么建立自己的层来提供这一点。在第 10 章性能增强工具的缺点中,我们重点介绍了 StatsD ,它使用 UDP 来避免登录远程中央服务器时的阻塞延迟。

如果您不局限于浏览器,那么 TLS 还有其他选择,但是如果您正在开发一个 web 应用,这可能只适用于您的服务器基础结构。例如,除了端到端加密的信号协议之外,WhatsApp 消息应用还在智能手机应用及其服务器之间使用来自噪声协议框架(【http://noiseprotocol.org/】)的噪声管道和曲线 25519。

使用噪声管道代替 TLS 可以提高性能,因为建立连接所需的往返次数更少。另一个具有类似优势的选项是安全管道守护程序(spid,由安全 Linux/BSD 备份软件 Tarsnap 使用和创建。但是,您确实需要预共享密钥,但是您可以在http://www.tarsnap.com/spiped.html了解更多信息。

高级哈希

我们在这本书里对散列函数做了相当多的介绍,尤其是在第 8 章理解代码执行和异步操作中。这个领域在不断发展,关注未来,看看未来会发生什么是很有用的。虽然今天使用 SHA-2 家族的成员进行快速散列和使用 PBKDF2 进行慢速(密码)散列是合理的,但这不太可能总是这样。

对于快速散列,有一个新的算法家族叫做 SHA-3 ,不应该与 SHA-384 或 SHA-512(都属于 SHA-2 家族)混淆。SHA-3 基于一个名为 Keccak 的算法,该算法是为新标准寻找合适算法的竞赛的获胜者。其他入围的还有 Skein(http://skein-hash.info/)和 BLAKE2(http://blake2.net/),比 MD5 快但实际上很安全。

一个名为 Argon2 的算法赢得了类似的密码散列比赛(http://password-hashing.net/)。要了解这一点的重要性,您可以访问https://haveibeenpwned.com/(顺路.NET 安全大师特洛伊·亨特)查看您的详细信息是否是大量数据泄露之一。例如,LinkedIn 被攻破,没有使用安全密码哈希(只有一个未加密的 SHA-1 哈希)。因此,大多数纯文本密码被破解和恢复。因此,如果 LinkedIn 帐户密码在其他网站上被重复使用,那么这些帐户可以被接管。

使用密码管理器并为每个站点创建强唯一的密码是一个非常好的主意。如果可用,使用双因素身份验证(有时也称为两步验证)也是有益的。例如,除了密码之外,您还可以通过智能手机应用输入代码来实现这一点。这对电子邮件帐户特别有用,因为它们通常可以用于恢复其他帐户。

库和框架支持

发生了一些重大变化。1.0 和 2.0 版本之间的. NET Core 和 ASP.NET Core。明智的是,许多流行的库和框架正在等待.NET 标准 2.0,在添加支持之前。

显然,一本书不是跟上变化的好地方,所以作者建立了一个 GitHub 存储库来显示最新的兼容性信息。您可以在http://anclafs.com/找到ASP.NET Core 库和框架支持列表。

如果您想更新任何内容或添加库或框架,请发送拉取请求。该存储库位于https://github.com/jpsingleton/ANCLAFS,它包括许多有用的工具、库、框架等等。我们在本书前面提到了其中的许多,下面的示例只是所列内容的一小部分,因为包支持会随着时间的推移而增长:

  • Scientist.NET
  • 功能切换
  • MiniProfiler
  • 一瞥
  • 前缀
  • 衣冠楚楚的
  • 很简单。数据
  • 英孚核心
  • 杭火
  • 图像调整器
  • 动态图像
  • ImageSharp
  • RestBus
  • EasyNetQ
  • rabbitmq 客户端
  • 公共交通
  • 高层货架
  • 南茜
  • 奥尔良

今后

一句经常被认为是物理学家尼尔斯·玻尔的名言如下:

Prediction is very difficult, especially about the future.

然而,无论如何,我们将从更简单的部分开始。官方的 ASP.NET Core 路线图列出了 2.1 版的 SignalR 支持。SignalR 正在被重写,因为从大规模部署中学到了很多经验,需要改进。

还有持续的进步.NET 标准规范,创建该规范是为了增强.NET Core.NET 框架和 Mono。例如,.NET Core 2.0 和.NET 框架 4.6.1 都实现了.NET 标准 2.0。这意味着在任一中编写的项目都可以使用遵循.NET 标准 2.0 规范。作为.NET Standard 2.0 现已最终确定,API 表面积的任何增加都将使用更高的版本号。那个.NET 标准类似于 HTML5 规范,由不同的浏览器在不同程度上实现。除非您正在编写一个库或一个 NuGet 包,否则您可能不需要太担心这个问题。如果是,请参考https://docs . Microsoft . com/en-GB/dotnet/standard/net-standard的文档。

你可能想知道为什么。. NET 4.7 目前没有在.NET 标准文档。预计这种情况将会改变,各种框架将继续趋同。但是,您可能不想过早升级到新版本,因为新版本中有一些严重的错误。例如,4.6 中有一个令人讨厌的错误,它将错误的值传递给了方法参数。4.6.2 中还有一个可怕的错误,最大年龄头被设置为一个巨大的值,这可能导致资产被缓存在代理中超过 2000 年!热修复(或质量更新)通常是针对这些问题发布的,所以请确保您掌握了这些问题,而不要仅仅依赖于版本号。

从语言的角度来看,C# 7.1 和 7.2 提供了一些小的改进,8.0 将包含一些更大的新特性。其目的是添加一些高性能、低级别的代码特性,用于优化游戏和其他对性能敏感的操作。这些是你以前不得不使用不安全代码的事情,计划是让它们使用起来安全但仍然快速。其他需要包含的语言特性是接口的默认实现,在 Java 和 Swift 中都有。这允许您在不破坏实现接口的所有类的情况下更新接口。这也使得从 Xamarin 到安卓和 iOS 平台 API 的映射更加清晰。C# 7.1 允许一个async主方法,在未来,你可能也可以await一个foreach循环或者using语句。最终列表可能会有变化,所以在更近的时间查看 GitHub(https://github.com/dotnet/csharplang)上的 C#语言设计库,甚至参与进来。

微软曾表示,会听取用户反馈,用它来推动平台的方向,所以你有话语权。他们还使用从软件开发工具包工具中收集的遥测数据来决定平台支持,并定期发布一些数据。由于代码都是开源的,您可以通过添加您想要的功能来帮助塑造未来。F#的大部分开发来自社区,看到它在 ASP.NET Core 和相关工具中得到更好的支持将是件好事。例如,FSharp。数据网络抓取工具是一个社区贡献。看看 GitHub,跳进去。

更远的未来更难预测,但微软已经有了源源不断的开源项目,从早期的 ASP.NET MVC 到跨平台应用开发的 Xamarin 框架。XAML 标准正在成形,一个适用于所有主要桌面和移动操作系统的通用用户界面框架前景诱人。

使用 C#和是一个令人兴奋的时刻.NET,尤其是如果你想从 web 开发中扩展。Unity 游戏引擎是.NET 基础,在虚拟现实 ( VR )和增强现实 ( AR )硬件方面也有一些有趣的发展。例如,微软 Hololens、Oculus Rift、三星 Gear VR 和 HTC Vive 都比几十年前问世的基本 VR 好得令人难以置信。

这也是一个关注物联网的好时机,尽管它可能仍在寻找它的杀手级应用,但它有如此多廉价和强大的硬件可供使用。一个树莓皮零瓦只要 10 美元,带摄像头接口和 WiFi/蓝牙。有了像树莓 Pi 3 这样的电脑,它提供了几乎桌面级的性能和 35 美元的 WiFi/蓝牙,任何人现在都可以很容易地学会编码(也许用 C#或.NET)并制作东西。

以下是艾伦·凯的智慧:

The best way to predict the future is to invent it.

所以,快去吧!一定要分享你所做的事情。

摘要

我们希望您喜欢这本书,并学习了如何使您的网络应用性能良好且易于维护,尤其是当您使用 C#、ASP.NET Core 和时.NET Core。我们试图保留尽可能多的适用于一般 web 应用开发的建议,同时温和地向您介绍来自微软和其他公司的最新开源框架。

很明显,像这样的话题变化很快,所以保持在线关注更新。希望这本书里的很多课都是好主意,在未来很长一段时间里它们仍然是明智的。

永远记住,为了自身而优化是没有意义的,除非它是一个学术练习。你需要衡量和权衡利弊;否则,你可能会让事情变得更糟。很容易让事情变得更复杂,更难理解,更难维护,或者所有这些。

将这些务实的绩效理念灌输到团队文化中很重要,否则团队不会一直坚持这些理念。不过,最重要的是,记得还是要玩得开心!

posted @ 2025-10-21 10:42  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报