Microsoft Graph 的 .NET 6 之旅

这是一篇发布在dotnet 团队博客上由微软Graph首席软件工程师 Joao Paiva 写的文章,原文地址:https://devblogs.microsoft.com/dotnet/microsoft-graph-dotnet-6-journey/。

Microsoft Graph 是一个 API 网关,它提供了对 Microsoft 365 生态系统中数据和智能的统一访问。该服务需要实现两大目标:以非常高的规模运行并有效利用 Azure 计算资源。我们使用 .NET 构建云原生的应用已经能够实现这两个目标。我将向您详细介绍我们是如何将 Microsoft Graph 构建到现在这样海量服务中的过程。

.NET 6 之旅

四年前,该服务采用 .NET Framework 4.6.2 上的 ASP.NET 运行在 IIS 上。现在该服务采用 .NET 6 上ASP.NET Core 运行在 HTTP.sys 上。  从 .NET Core 3.1 到 .NET 5 ,随着每次升级我们观察到 CPU 利用率有所提高,尤其是在 .NET Core 3.1 和最近使用 .NET 6。

  1. 从 .NET Framework 升级到 .NET Core 3.1,在相同的流量下,我们观察到 CPU 减少了 30%。

  2. 从 .NET Core 3.1 到 .NET 5,我们没有观察到有意义的差异。

  3. 从 .NET 5 到 .NET 6,对于相同的流量,我们观察到 CPU 又减少了 10%。

CPU 利用率的大幅降低转化为更低的延迟、更高吞吐量和计算容量时的有意义的成本节约,有效地帮助我们实现了目标。

该服务覆盖全球,目前部署在全球 20 个地区。四年前,该服务每天处理 10 亿个请求,运营成本极高。如今,它每天处理大约 700 亿个请求,增长了 70 倍,每处理 10 亿个请求,运营成本就降低了 91%。这反映了过去 4 年的增长和改进步伐,其中从.NET Framework迁移到 .NET Core 发挥了重要作用。

.NET Core 的影响

从 .NET Framework 4.6.2 (IIS + ASP.NET) 到 .NET Core 3.1 (Kestrel + ASP.NET Core;以及后来的 HTTP.sys) 的初始迁移过程中,我们的基准测试显示吞吐量显着提高。下图比较了堆栈,并绘制了使用 Standard_D3_v2 虚拟机和合成流量的每秒请求数 (RPS) 和 CPU 利用率。

6a68763d52edc970e7c15481349dcb3e.png

当我们比较两个.NET 运行时堆栈,该图表说明了 RPS 相对于相同 CPU 利用率的显着增加。在 60% CPU 时,老的.NET Framework 4.6.2(橙色)中的 RPS 约为 350,新的.NET Core 3.1(蓝色)中的 RPS 约为 850。.NET Core 在更高的 CPU 阈值下性能明显更好。

重要的一点是要注意此基准测试使用的是合成流量,并且观察到的改进不一定直接转化为具有真实流量的更大规模生产环境。在生产中,我们观察到 CPU下降了 30%(对于相同的流量)。

构建系统的现代化‎

‎我们的构建系统的现代化是 迁移到 .NET Core 成为可能的一项重大任务。‎

‎ 我们使用的是内部构建系统的时候,构建系统工具链与 .NET Core 不兼容。因此,在我们的案例中,第一步是使构建系统现代化。我们迁移到了一个更新的现代构建系统,主要使用具有‎‎MSBuild‎‎和‎‎dotnet‎‎支持的Visual Studio工具链。新的工具链支持.NET Framework和.NET Core,并为我们提供了所需的灵活性。‎

‎ 对构建系统进行现代化改造的投资虽然一开始很困难,但它通过更快的构建和项目,更容易创建和维护,大大提高了我们的生产力。

整体情况

每次 .NET 升级都有许多改进,即使 Graph 团队没有执行任何显式工作来提高性能也是如此。每个新的 .NET 版本都改进了底层运行时 API、通用算法和数据结构,从而导致 CPU 周期和 GC 工作的减少。对于像 Microsoft Graph 这样受计算约束的服务,使用新的运行时和算法来减少时间和空间复杂性至关重要,并且是使服务快速且可缩放的最有效方法之一。在 .NET 团队的朋友的帮助下,我们能够提高吞吐量、减少延迟开销和计算运营成本。谢谢!

迁移的另一个原因是使代码库现代化。现代的代码库更能吸引了人才(招聘),并使我们的开发人员能够使用更新的语言功能和API来编写更好的代码。像.NET Core中引入的 spans 这样的构造是无价的。我使用 span 的常见方法之一是字符串操作。字符串操作是老的 .NET 代码库中的常见陷阱。由于无休止的连接给GC带来了压力,最终反映在更高的CPU成本上,旧模式通常会导致字符串分配的爆炸式增长。开发人员甚至没有意识到这种分配的实际成本和影响。.NET Core 所引入的Spans 和 string.Create  为我们提供了一个操作字符串的工具,避免了堆上不必要的字符串分配成本。

此外,我们依靠可观察性工具来监视在 CPU、内存、文件和网络 I/O 等维度上代码的成本。这些工具帮助我们识别回归和机会,以改善处理延迟、运营成本和可扩展性。

我们通过新的 API 和 C# 特性获得了非常显著的优势:

  1. 通过array pooling 减少缓冲区分配。

  2. 减少与内存和span相关的类型的缓冲区和字符串分配。

  3. 减少使用静态匿名函数从封闭上下文中捕获状态的委托分配。

  4. 使用 ValueTask 减少任务分配。

  5. 使用 nullable 删除整个代码库中冗余的 null 检查。

  6. 使用null-coalescing assignment 或 using declarations编写简洁的代码,仅举两例。

此列表未涵盖许多其他改进,包括算法和数据结构以及重要的体系结构和基础结构改进。最终,.NET Core和语言功能使我们能够提高工作效率,并编写算法和数据结构,以减少时间和空间的复杂性,这对于实现我们的长期目标至关重要。

最后但并非最不重要的一点是,.NET Core使我们的服务准备好在Windows和Linux中运行,并使我们能够通过HTTP/3和gRPC等传输协议快速创新。‎

迁移指南

本节介绍从 ASP.NET 迁移到 ASP.NET 核心环境所采用的策略,旨在作为高级指导。

步骤 1 — 构建现代化

第一个先决条件是允许您构建 .NET Framework 和 .NET Core 程序集的生成系统(如果情况并非如此)。

对于 Graph 团队来说,对生成系统进行现代化改造不仅使迁移到 .NET Core 成为可能,而且还通过更快的生成和更易于创建和维护的项目,大大提高了我们的工作效率。

第 2 步 — 架构就绪

拥有良好的体系结构来执行迁移非常重要。让我们使用图表作为我们将要经历的三个主要阶段的插图。

d7af22ef788f6eabf6c7c3f8bbf77c5b.png

  • 在第 1 阶段,我们有 ASP.NET Web 服务器程序集和面向 .NET Framework(黄色)的所有库。

  • 在第 2 阶段,我们有两个 Web 服务器程序集,每个程序集都面向各自的 .NET 运行时,而库现在面向 .NET Standard(蓝色)。这样可以进行 A/B 测试。

  • 在第 3 阶段,我们有一个 Web 服务器程序集和所有面向 .NET Core(绿色)的库。

如果你的解决方案尚未在多个程序集中分解(阶段 1),则现在是执行此操作的好机会。ASP.NET 程序集应该是 Web 服务器的非常薄的存根,从主机中抽象出应用程序。此 ASP.NET 程序集应特定于主机,并引用实现各个组件(如控制器、模型、数据库访问等)的下游库。重要的是要有一个具有关注点分离的体系结构模式,因为这有助于简化依赖关系链和迁移工作。

在我们的服务中,这是通过单个 HTTP 应用程序处理程序来完成的,该处理程序是特定于主机的传入请求。该处理程序将传入的转换为与主机无关的等效对象,该对象将传递到下游程序集,这些程序集使用该对象读取传入的请求并写入响应。我们使用的接口分别抽象了每个主机环境所使用的传入 System.Web.HttpContext 和 Microsoft.AspNetCore.Http.HttpContext 。此外,我们在下游程序集中实现路由规则,与主机无关,这也简化了迁移。该服务没有 UI 或视图组件。如果您有一个具有 MVC 和模型绑定的视图组件,则解决方案必然会更加复杂。

步骤 3 — .NET Framework 依赖项的清单

创建服务使用的所有依赖项的清单,这些依赖项仅属于 .NET Framework,并标识所有者以在需要时与它们进行交互。

根据相关性和投资回报对每个依赖关系进行分类。使用和维护依赖关系会带来一些包袱和税收,它们是值得的。通常,良好的依赖关系遵循以下原则:

  1. 它不携带隐式依赖项,除了 .NET 运行时或扩展。

  2. 它解决了一个不容易解决的有意义的问题,或者逻辑非常敏感,不需要重复。

  3. 它具有良好的质量,可靠性和性能,特别是在热路径中存在时。

  4. 它得到了积极的维护。

如果不满足这些前提中的任何一个,则可能是时候找到替代方案了,要么通过找到另一个执行该工作的依赖项,要么通过实现它。

大多数流行的库已经是以.NET Standard为目标,许多甚至以.NET Core为目标。对于任何专门针对 .NET Framework 的库,通常已经在所有者的雷达中在 .NET Standard 中构建它们。大多数人都非常乐于接受这样的工作。可以与库的所有者联系,了解提供 .NET Core 兼容版本的时间表。

步骤 4 — 从项目库中摆脱 .NET Framework 依赖项

开始逐个迁移依赖项,移动到 .NET Standard 中的等效项。如果解决方案中有许多项目,请按照自下而上的方法开始处理位于依赖项链底部的项目,因为它们通常具有最少数量的依赖项并且更易于迁移。

面向 .NET Framework 的项目可以继续这样做,而迁移工作正在进行中。一旦项目不再引用任何 .NET Framework 依赖项,请将其设置为 .NET Standard。

第 5 步 — 避免被阻止

如果服务具有旧版或规模很大,则可能会发现隐藏了难以摆脱的依赖项。不要放弃。

请考虑以下选项:

  1. 自愿帮助所有者将依赖项构建为 .NET Standard,以便自行取消阻止。

  2. 将代码分叉,并将其代码放到你的代码库中生成为 .NET Standard,作为临时的解决方案,直到兼容的版本可用。

  3. 将依赖项作为单独的控制台应用程序或与 .NET Framework 一起运行的后台服务运行。现在,你的服务可以在 ASP.NET Core 中运行,而控制台应用程序或后台服务可以在 .NET Framework 中运行。

  4. 作为最后的手段,请尝试从 .NET Core 项目中引用依赖项,包括 .NET Framework ProjectReference 或 PackageReference .NET Core 运行时使用兼容性填充程序,允许您加载和使用某些 .NET Framework 程序集。但是,不建议将此作为永久性措施。必须(在运行时)对此方法进行详尽的测试,因为即使生成成功,也无法保证程序集兼容(在所有代码路径中)。NoWarn="NU1702"

在 Microsoft Graph 迁移的案例中,我们在不同的时间和不同的依赖项中使用了所有这些选项。目前,我们仍然将一个控制台应用程序作为 .NET Framework 运行,并使用兼容性填充程序在服务中加载一个 .NET Framework 程序集。

步骤 6 — 为 ASP.NET Core 创建新的 Web 服务器项目

使用等效设置,为 ASP.NET Core 创建一个新项目,与当前 ASP.NET 框架项目并行。新 ASP.NET Core 项目默认使用 Kestrel。它非常好,是大多数.NET团队投资的地方。这是他们的跨平台Web服务器。但是,您可以考虑其他选择,例如HTTP.sys,IIS甚至NGINX。

请确保在 .NET Core 中启用较新的性能计数器。花点时间来启用它们,特别是与CPU,GC,内存和线程池相关的。还要为所选的 Web 服务器启用性能计数器(例如,请求队列)。当您开始实施时,这些对于检测任何回归或异常非常重要。

此时,您应该已完成第 2 阶段(在我上面图片中),并准备好执行 A/B 测试并开始实施。

步骤 7 — A/B 测试和实施计划

创建一个实施计划,该计划允许在通过所有预生产关口后,在某些生产容量中进行 A/B 测试(例如,将新运行时部署到一个规模集)。使用真实流量进行大规模测试是最终的大门和关键时刻。

您可以使用以下启发式方法测量应用程序之前和之后的效率,测量 A/B 位之间的差异:

Efficiency = (Requests per second) / (CPU utilization)

在第一次实施期间,尽量减少在有效负载中引入的更改,以减少可能导致意外回归的变量数。如果我们在有效负载中引入太多变量,我们就会增加引入其他可能与新运行时无关的错误的可能性,但仍会浪费工程师的时间来确定和根本原因。

一旦初始部署在小规模内成功并经过审查,请按照现有的安全部署实践逐步实施,计划使用逐步推出来启用新位。重要的是要遵循逐步实施,这样您就可以及时检测和缓解可能随着数量和规模的增加而出现的问题。

步骤 8 — 在所有项目中以 .NET Core 为目标

一旦服务在 ASP.NET Core 中运行,大规模部署并经过审查,就可以删除 .NET Framework 中仍然存在的最后一个片段了。删除用于 ASP.NET 的 Web 服务器项目,并将所有项目库显式移动到 .NET Core 而不是 .NET Standard,以便您可以开始使用较新的 API 和语言功能,使开发人员能够编写更好的代码。有了这个,你已经成功地完成了第3阶段

升级技巧

应用了一些主要的学习和升级技巧。

URI 编码中的怪癖

该服务的一个核心功能是分析传入的 URI。多年来,我们最终在整个代码库中都有不同的点,对传入请求的编码方式进行了严格的假设。当我们从 ASP.NET 转移到 ASP.NET Core时,许多这些假设都被违反了,导致许多问题和边缘情况。经过长时间的修复和分析,我们整合了以下规则,用于将 ASP.NET Core Path和Query转换为代码不同部分所需的老的 ASP.NET 格式。

  • 按主机列出的被拒绝的编码 ASCII 字符百分比。

    e8d11584ee6c1069f451ff802263a58f.png

  • 按主机自动解码百分比编码字符。

    a1c8082fd54f06c32eb926fa2c928567.png

使用 .NET 6 启用动态 PGO

在.NET 6中,我们启用了动态PGO,这是.NET 6.0最令人兴奋的功能之一。PGO 可以通过最大限度地提高稳态性能而使 .NET 6.0 应用程序受益。

动态 PGO 是 .NET 6.0 中的一项选择加入功能。需要设置 3 个环境变量才能启用动态 PGO:

  • set DOTNET_TieredPGO=1.此设置利用方法的初始 Tier0 编译来观察方法行为。在 Tier1 重新设置方法时,将从 Tier0 执行收集的信息用于优化 Tier1 代码。

  • set DOTNET_TC_QuickJitForLoops=1.此设置为包含循环的方法启用分层。

  • set DOTNET_ReadyToRun=0. 默认情况下,.NET 附带的核心库都启用了 ReadyToRun。ReadyToRun允许更快的启动,因为JIT编译较少,但这也意味着ReadyToRun映像中的代码不会经过支持动态PGO的Tier0分析过程。通过禁用 ReadyToRun,.NET 库还可以参与动态 PGO 过程。

这些设置使 Azure AD 网关的应用程序效率提高了 13%。

其他参考资料

有关更多了解,请参阅 Azure AD 网关姊妹团队发布的以下博客:

  • Azure Active Directory 的网关采用 .NET Core 3.1!

  • Azure Active Directory 的网关采用 .NET 6.0!

总结

每个新版本的 .NET 都带来了巨大的生产力和性能改进,这些改进继续帮助我们实现构建可扩展服务的目标,这些服务具有高可用性、安全性、最小的延迟开销和最佳路由,同时具有尽可能低的运营成本。

请放心,没有银弹。在大多数情况下,迁移需要团队的认真承诺和辛勤工作。但从长远来看,这项工作无疑会带来许多红利。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/287911.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小鱼便签_同样是写便签,这样更酷

每天那么多的工作,总喜欢写一些Tips提醒自己。但是如果全部都写在小纸条上,贴在电脑边上,每日身边飞扬着无数小纸条,实在是太讨厌啦!这时候,不妨试试一款电脑桌面便签软件——小鱼便签,让你的办…

ArcGIS实验教程——实验十一:影像拼接与提取

ArcGIS实验视频教程合集:《ArcGIS实验教程从入门到精通》(附配套实验数据) 【实验描述】本实验讲述在ArcGIS软件中进行日常影像(栅格)数据的拼接与提取(裁剪)的工具与方法。影像的拼接常用到的工具是Mosaic和Mosaic To New Raster;影像提取分为按属性提取、形状或位置提…

php基础教程 第九步 自定义函数

自定义函数 在之前的课程我们有使用到php的系统函数,这些函数由系统定义。在开发中,有时候系统自带的函数满足不了我们开发时(或其它情况),我们可以自定义函数。顾名思义,自定义函数就是自己定义函数。 自…

如何使用 C# 隐藏 Console 窗口?

咨询区 Stefan Steiger我有一个 Console 程序,它主要用来重启 IIS 以及删除临时文件,我现在期望它启动后隐藏自身,我在网上找了下面这段代码做了隐藏。static void Main(string[] args) {var currentProcess System.Diagnostics.Process.Get…

编写登陆接口

用任意一种语言实现登陆接口 作者:尹正杰 版权声明: 本篇博客仅仅用于交流学习使用,对一些小功能的实现,来练习对代码的熟练度。同时,本篇博客允许转载,用于互相交流学习,转载必须在博客声明原链…

php基础教程 第十步 阶段性知识补充

简 在php教程中前九步已经了解了php的一些基础知识。大多数人在了解完以上知识后脑子还是迷茫的,最多懂的一些我所讲解过的知识如何实现(对象为零基础读者),如何搭建一个网站还是不知晓。没关系,接下来我们即将进入ph…

ArcGIS实验教程——实验十三:栅格空间插值分析

ArcGIS实验视频教程合集:《ArcGIS实验教程从入门到精通》(附配套实验数据) >>> 实验前必读:《完美解决ArcGIS10.x栅格空间插值报错无法进行和导出插值栅格结果出错的问题》 一、实验描述 一般情况下采集到的数据都是以离散点的形式存在的,只有在这些采样点上才有…

ARM路线的投机性和欺骗性

过去十年里,在自主CPU取得了长足进步的同时,一批企业积极引进英特尔、AMD、VIA、IBM、ARM、高通等公司的技术,或是成立合资公司,相对于X86、Power等处理器很容易被识别为技术引进CPU,ARM的欺骗性则要强很多&#xff0c…

营业执照在线一键生成_如何在线制作证件照?证件照一键生成的方法

如何在线制作证件照?每一年的毕业季也是就业季,几百多万的大学生都面临着找工作,而在工作之前必须准备好证件照,这样方便参加面试和入职。随着市场需求的逐步扩大,很多便捷的软件和网站都被设计出来了,比如…

php基础教程 第十一步 面向对象

面相对象 面向对象程序设计简称OOP(Object-oriented programming)。是一种类型的抽象,或者说是一种类型以编码的方式进行描述。概念上理解并不能直观的说明问题,以下将会有示例为读者直观的感受面向对象。 类代表一个类别&#…

Android NDK开发Crash错误定位[转]

使用 ndk-stack 的时候需要你的 lib 编译为 debug版的,通常需要下面的修改: 1. 修改 android.mk,增加,为 LOCAL_CFLAGS 增加 -g 选项 2. 修改 application.mk,增加 APP_OPTIM : debug 3. nkd-build -B 4. 从 obj/local…

实验报告类与对象水井问题_物业设施设备巡检检查对象、周期和频次

点击上方“物业精英俱乐部”,关注公众号,中国物业管理行业精英联盟,中国百强物业企业精英学习平台。来源:百度文库版权归原创作者所有一巡检对象、周期和频次所谓巡检,并不是说将大大小小的所有物业设施都列入巡检范围&#xff0c…

龙芯2K1000LA处理器流片成功,龙芯业务全面转向LoongArch架构

2022年4月底,龙芯2K1000处理器完成了改版芯片(代号龙芯2K1000LA)的功能和性能测试,正在开展用户试用。龙芯2K1000LA在实现与原有版本2K1000引脚和接口兼容的基础上,处理器核更新为基于龙芯自主指令系统架构LoongArch&a…

php基础教程 第十一步 面向对象补充

继承 上一节中学习了php类,以及类方法、类成员变量相关的一些知识,接下来开始学习php类中的继承。本节是上一节的补充。(其实也就是为了保持日更随便发的一篇文) 开始 php继承是继承与父类的所有属性,就像你继承了你…

【Ubuntu】ubuntu物理机安装方法:wubi

在之前的文章里我们曾经详细介绍了如何在虚拟机里安装ubuntu操作系统,但是一些小伙伴希望在自己的电脑里安装一个ubuntu,来感受一下ubuntu的硬体验。下面给大家介绍一种通过wubi的安装方法。 wubi是 Windows Ubuntu-Based Installer 缩写,是一…

server如何调用 thrift_Thrift总结(二)如何快速创建自己的RPC服务

前面介绍了thrift 基础的东西,怎么写thrift 语法规范编写脚本,如何生成相关的语言的接口。不清楚的可以看这个《Thrift总结(一)介绍》。做好之前的准备工作以后,下面就开始如何用Thrift写RPC接口。如何用Thrift写RPC接口1. 打开之前下载的thr…

Hello Playwright:(2)简化部署方式

前言上次的文章Hello Playwright:(1)从开发到部署发表后,有网友留言问,有不有简化部署的方式:下面,提供 2 种可行的方案。方案 1. Copy其实 Playwright 默认是到C:\Users\用户名\AppData\Local\ms-playwright文件夹下查…

C++入门指南及实战 第一步 概述及经典HelloWorld

本系列文章环境及学习门槛 编程基础:无年龄:具有一定的逻辑思维英文:不要求数学:不要求学习时长:每天一篇,一周4-5篇即可,每篇最多1小时死记硬背:不需要,理解至上本机环…

为什么云服务器没西南的_去年“双11“我买的那台云服务器

一、为什么会买云服务器为什么初学者需要一台云服务器?从我自己那仅有的一点经验来看,重点无非这一个词:实践。细数一下初学者拥有一台云服务器的好处:学习操作系统、熟悉环境大部分初学者在本地使用的是windows系统,在…

Java线程安全以及线程安全的实现方式和内存模型(JMM)

一、了解几个概念 1)临界区: 临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进…