OneService 前往 .NET 6

OneService是一项微软服务,为Microsoft Start,Microsoft Edge和Microsoft Windows中的各种内容体验提供支持,例如新闻和体育比赛结果。它由多个微软团队维护的30多种服务组成。在两年多的时间里,我们将大量 .NET Framework 4.7.2 应用、库和测试项目转换为 .NET 6,验证了功能和性能等效性(或更好),现在几乎完全在生产环境中的 .NET 6 上运行。该项目取得了重大成功,有助于降低运营成本并改善开发人员体验。

突出:

  • 基础架构成本降低 29%。
  • 迁移服务的 CPU 性能(平均)提高 30%。
  • 主 API 的 P95 延迟提高了 8-27%。
  • 减少了技术债务,现在可以轻松升级到年度 .NET 版本。
  • 更快乐、更高效的团队。

项目范围很大,一路上面临着各种挑战。幸运的是,其他几个微软团队更早地进行了这种迁移。我们能够复制他们方法的许多方面。在这篇文章中,我将告诉您我们的 .NET 6 之旅。

一个服务

OneService 是一组面向内容的微服务,由托管在通用平台上的多个团队维护。OneService 团队负责开发共享库,以便其他团队在通用基础结构上构建、运行和操作。OneService支持的一些体验是Windows小部件,Edge主页,必应主页,企业新闻和 start.com。

我们的使命是通过高质量和个性化的内容来取悦和吸引用户。我们将优质发布商整合到一个个性化和智能的AI提要中,将人们与他们关心的内容联系起来。我们的 API 使各种 Microsoft 产品能够向其用户公开此内容,并在多个产品之间提供一致的体验。

通用框架和特定于服务的代码作为(Microsoft 内部)NuGet 包发布,并导入到服务项目中。这些服务项目是 ASP.NET 应用程序。一开始,所有库都以.NET Framework 4.7.2为目标,我们的服务在 ASP.NET 上运行。

我们在微软听说过很多关于.NET Core的成功故事。这些团队证明,由于节省了成本、提高了性能和提高开发人员的工作效率,迁移是值得的。我们确信我们可以为OneService实现相同的结果。就像您一样,我们在 .NET 博客上看到了令人敬畏的创新步伐。我们对 .NET 团队在过去五年中取得的所有改进感到兴奋。除了这些创新之外,我们还想在未来几年内利用它们的进步。

迁移阶段

  • 在第 1 阶段,我们的 Web 服务器在 ASP.NET 上运行,所有库都面向 .NET Framework(黄色)。这是迁移前阶段。
  • 在第 2 阶段,我们将所有库迁移到 .NET standard(green),但我们的 Web 服务层仍保留在 .NET Framework 上。
  • 在第 3 阶段,我们将新式库迁移到 .NET Core(紫色),并将旧库保留在 .NET Standard 中。我们的控制器 SDK 和 Web 服务也已迁移。

注意:在迁移过程中,所有库和我们的 OneService 控制器 SDK 都是多目标的,以便我们执行 A/B 测试并确保与其他服务的向后兼容性。

初始迁移尝试

我们将一个小型微服务迁移到 .NET Core 3.1,作为我们更重要服务的概念证明。由于多个 .NET Core 版本之间没有重大的重大更改,因此我们直接从 迁移了主要服务。NET472 到 .网5.我们的印象是,任何中间的迁移步骤都会导致不必要的工作并延迟项目。

我们很快发现,迁移将比预期的更困难,并且大爆炸方法不可行。OneService 团队拥有 20 多个库,此外还有其他团队拥有的各种特定于服务的库。虽然其中一些项目迁移到 .NET Standard 2.0 是微不足道的,但其他项目被证明是极其困难的。最困难的挑战之一是对System.Web API的硬依赖,这在 ASP.NET Core中不存在。

我们还选择将测试项目多目标设置为 .NET Framework 4.7.2 和 .NET 5。我们的理由是,遵循测试驱动的方法会更安全。但是,我们在测试管道中遇到了技术债务。这包括需要将测试项目移植到新的项目样式。我们决定暂停这项工作,直到项目后期,转而专注于让我们的应用程序正常工作(没有.NET 5的测试覆盖率)。我们相信,早期的A / B测试和端到端测试将捕获大多数错误。.csproj

迁移到 .NET Framework 上的 ASP.NET Core 2.1

我们意识到.NET团队仍然支持 ASP.NET.NET Framework上的Core 2.1。这似乎可以帮助我们作为移民工作的中点国家。我们能够快速将主要服务移植到 ASP.NET Core 2.1,而无需解决 .NET Framework 依赖项问题。

这种方法使我们能够更快地开始A / B测试我们的更改,并验证我们的广泛重构。由于我们没有在 .NET 6 上运行,因此我们没想到在此步骤中看到任何性能改进。目标是不引入任何回归并捕获错误,而不会在组合中进行框架升级的复杂性。

我们在此配置中将主要服务部署到生产环境。虽然我们不得不回滚一次,但几乎所有的错误和错误都在A / B测试时被捕获(稍后将详细介绍我们的A / B测试策略)。新闻服务充当了概念证明,验证了我们的服务将继续在 ASP.NET Core 上按预期运行,即使仍在 .NET Framework 上运行。

迁移到 .NET 6

一旦我们 ASP.NET Core 2.1,我们就将注意力转向解决从.NET Framework到.NET 6的阻塞程序。这涉及跨多个服务重构数十个组件,并使它们面向 .NET Standard 2.0。对于某些服务,我们决定在中间步骤迁移到 .NET 5,以限制我们对 .NET 6 更改的暴露。我们更关注可预测的前进进度,而不是快速完成项目。

在某些情况下,我们可以让其他团队迁移他们的组件,特别是如果对.NET Framework特定API的依赖性最小。在其他情况下,OneService 团队需要承担迁移组件的成本。在最有问题的情况下,我们创建了新的服务边界,以便我们可以将处理硬迁移问题推迟到以后,同时保持工作的整体速度。

完成新闻服务的 A/B 测试后,我们将在 .NET 5 上交付到生产环境。同时,我们开始在 .NET 6 上对同一服务进行 A/B 测试。这使我们能够一次收集大量信息,并稍微折叠一下时间表。我们还同时将其他服务升级到 .NET 5。

我们决定快速将服务从 .NET 5 迁移到 .NET 6,因为我们已经确信我们的系统可以与 .NET 5 配合使用。我们没有看到两个版本之间有太大的区别。不出所料,我们看到的大多数性能改进都是从.NET Framework 4.7.2到.NET 5,而不是从.NET 5到.NET 6。其他微软团队已经从.NET 5迁移到.NET 6取得了重大胜利。这取决于服务。无论如何,我们非常高兴,现在我们处于采用 .NET 7 的有利位置。

性能改进

我们看到各种关键服务和产品指标都有所改进。在服务方面,我们看到所有服务的 CPU 利用率平均提高了 30%。这是对服务的极大影响改进,并允许我们将大多数集群缩减30%。

金融服务迁移是最成功的迁移之一,因为每个请求的 CPU 利用率和 P99 延迟提高了近 40%,如下图所示(越低越好;.NET Framework 4.7.2 是虚线,.NET 5 是实线)。

注意:该服务现在在 .NET 6 上运行,但此数据是在 .NET 5 上运行时收集的。

我们惊喜地看到延迟的改善转化为用户参与度的提高。OneService 为 Microsoft Edge Browser 新选项卡体验中的内容提供支持。我们测量到,新标签页中内容的“视觉就绪时间”指标提高了 4.5%。我们的数据显示,这种改进在用户参与度方面带来了令人印象深刻的改善。这一结果显示了后端改进的潜力,可以在不实现任何新功能的情况下大大改善用户体验。性能确实是一个功能。

节约成本

降低基础设施的运营成本是项目的亮点之一。可悲的是,这是最难跟踪的指标。30% 的平均 CPU 利用率改进带来了巨大的成本节约,因为 CPU 利用率是我们的主要成本驱动因素。我们估计,CPU 使用率每降低 1%,我们就能将服务成本降低 1%。在某种程度上,衡量成本节约的困难是由于服务负荷没有保持不变。在迁移到 .NET 5/6(2021 年 12 月至 2022 年 3 月)期间,我们看到 OneService 服务的网络请求增加了约 45%。其中一些流量是由于我们的服务进一步解耦,但其中大部分来自新用户流量。我们非常高兴,我们的成本基本持平,而交通量却大幅上升。

否则,随着流量的增加,我们的成本肯定会急剧上升。这是一个明显的胜利。但是,很难估计如果我们从未进行过此升级,我们将支付多少钱。反事实很难。我们对流量(之前和之后)进行了一些分析,并估计使用我们以前的 .NET Framework 体系结构,我们的每月基础结构成本将增加约 29%。

其他福利

.NET Core 迁移的另一个优点是能够在较小的容器中运行我们的服务。此迁移允许我们使用 .NET 团队提供的 Nano Server 容器映像进行试验。Nano Server是一个轻量级的窗口容器产品,只有64位(没有32位仿真)。

与我们使用的5 GB服务器核心映像相比,Nano Server映像约为200MB。大小改进可以提高服务构建和加载时间。从早期数据来看,我们的管道构建时间和服务加载时间减少了30%。

生产中的 A/B 测试

现在您已经了解了我们的一般方法和我们获得的好处,我想告诉您更多关于我们方法的信息。这可能是显而易见的,但迁移需要导致零停机时间。我们在生产中严重依赖 A/B 测试。

OneService 依赖于使用“功能飞行”技术推出有风险的更改。我们通常通过 Azure 应用配置设置一个配置,以便在 HTTP 请求包含特定外部测试版 ID 时启用某项功能。该功能的代码路径通常包装在 C# 代码中,并带有一个条件语句,用于检查该功能是否已启用。这种方法适用于新的代码更改,但对于我们需要执行的实验类型来说,这种方法非常不够。

我们修改的不是单个代码路径,而是托管API的基础框架。要么全有,要么全无。为了考虑风险的严重性以及团队中此类外部测试缺乏先例,我们依赖于专用的 Azure Service Fabric 群集,我们称之为“试验群集”。

我们观察到由API和组件引起的无数错误,这些API和组件没有足够的测试覆盖率。许多错误是由框架级库的基础实现中的细微变化引起的,而不是OneService中的代码变化。我们可能总是没有足够的测试覆盖率,但这种迁移远远超出了我们的覆盖范围所能解决的问题。

我们发现单元和集成测试不如E2E测试有用。我们无法保证特定组件的测试项目中存在的 NuGet 包集是生产配置的 1:1 反映。包版本中的漂移有时会导致意外行为。端到端测试在这里很有帮助,因为它们最接近组件在生产中表现的“事实来源”。

镜像生产

我们从一个专用的“金丝雀”集群开始,该集群充当生产流量的1%镜像。此群集提供与生产群集相同的流量,但不向用户返回响应。因此,我们能够估计服务在生产中的表现,而不会产生任何相关风险。

最初的结果并不好。我们努力与生产的可用性相匹配,并看到了严重的延迟回归。我们通过识别错误和修复逻辑来解决这些问题。一旦 .NET 5/6 版本与 .NET Framework 变体处于奇偶校验(或更好),我们确定这些版本已准备好逐步为一小部分实时流量提供服务,并将响应返回给最终用户。

部署到生产环境

我们使用了四个专用集群,分别横跨美国西部、美国东部、北欧和东南亚。然后,我们将 Azure 前门配置为仅在请求包含特定航班 ID 时将流量路由到这些试验群集。然后,我们配置了 Microsoft 内部实验性服务 Control Tower,以将航班 ID 附加到 Windows 和浏览器客户端中的一小部分请求中。

我们从1%的流量开始,逐渐上升到25%。这是一个非常安全有效的过程,用于捕获以前未检测到的回归。假设与 .NET Framework 群集提供的对照组相比,某个特定指标(例如体育内容的点击次数)在实验群集所服务的处理组中经历了具有统计显著性的回归。在这种情况下,我们可以仔细查看该逻辑以确定是否存在错误。

然而,权衡是这种方法非常慢(在日历时间内)。当我们到达这个阶段时,我们已经捕获了大多数低垂的错误,这些错误表现为可用性下降或延迟下降。在飞行时捕获的虫子并不明显,通常需要几天的时间才能进行调查。

从成本和维护的角度来看,托管专用的 Azure 基础结构也很昂贵。我们花了几个月的时间在这种缓慢的反馈循环和不明显的错误状态下。从士气的角度来看,这个阶段尤其困难。

我们根据来自 ASP.NET 核心集群的数据来控制前进。每当新数据到达时,我们都希望它能告诉我们,我们可以自信地发布我们已经投入近一年时间创建的升级。许多上午的会议都是在希望破灭和结果模糊的情况下度过的。更糟糕的是,一些错误迫使我们完全停止实验,因为我们无法再安全地为生产流量提供服务。

我们很高兴我们遵循了这种方法。在如此危险的迁移中,安全比后悔要好得多。虽然很耗时,但我们可能会以这种方式节省更多的时间和精力,而不是过早地交付并尝试在生产中进行调试,而无需立即切断生产流量。拥有良好的指标还帮助我们量化了由于性能的提高而在用户参与度方面获得的收益。

从这个角度来看,我们正在对一个主要的微软服务进行普遍的更改。这很难。正如我之前所说,我们没有所需的测试覆盖率来引导我们更自信地完成此迁移。这并不理想。尽管如此,我们还是以最好的方式去做了。

从 ASP.NET 迁移到 ASP.NET 核心

从 ASP.NET 迁移到 ASP.NET Core 暴露了我们当前基础架构中的几个测试漏洞。虽然外部测试对于检测功能回归、延迟差距或组件级故障(例如,用户位置变化)非常有用,但它无法检测框架级故障。

ASP.NET 核心是围绕“中间件”的概念构建的,这种范式转变导致了迁移单元测试时的差距。这些差距本可以通过我们服务的端到端测试来捕获,但它告诉我们,测试的迁移与功能迁移一样重要。

这种测试差距的一个例子是我们的响应压缩。在初始迁移之后,我们不再使用GZip或Brotli压缩响应。直到我们看到 Azure 前门/Akamai 的成本因响应大小增加而显著增加,才发现此更改的影响。

如果我们的端到端测试还包括框架级检查(例如,响应压缩标头包含 gzip),则此问题可能在生产之前被捕获。事后看来,我们认为将来执行此迁移的最佳方法是针对迁移的服务和当前服务重放生产请求,并检查响应是否相同。显然,某些信息(例如,内部请求 ID、时间戳等)将是非确定性的,但这些信息应明确允许列出,并且响应的其余部分应相同。

API 挑战

我们有两类重要的 API 挑战,我将介绍这些挑战。

OData

OneService 在系统的多个部分中硬依赖 OData。OData版本控制被证明是一个挑战,因为.NET 5仅支持OData V7,而.NET 6仅支持OData V8。某些 OData 版本中存在重大更改,我们需要确保不受这些更改的影响。由于我们的代码库中已经对 OData V7 有许多依赖项,并且为了降低风险,我们决定先迁移到 .NET 5,然后再迁移到 .NET 6。

ServicePointManager 弃用

在 .NET 框架中,可以通过 配置全局和单个 API 连接。这个静态类允许我们对框架中的连接设置限制,我们花了很多个月的时间来微调这个值,以避免单个组件过度消耗资源。在 .NET Core 中,已被弃用。确切地说,API 仍然存在,但它对连接管理没有影响。.NET core 中的新模式是在通过 创建时设置可用连接。ServicePointManagerServicePointManagerHttpClientSocketsHttpManager

幸运的是,我们的许多外部连接都是通过我们团队拥有的一个 RestClient 库来处理的。这为我们提供了一个全局点,以使用我们之前设置的连接限制更新大多数。我们确定了最关键的依赖关系,并使用新的连接限制更新了这些客户端。我们为总连接数添加了 TCP 级别监视,并确认没有单个 IP 耗尽其连接池,这使我们能够自信地说,我们已经使用新的 .NET core 实现正确更新了必要的 s。HttpClientServicePointManagerHttpClient

结论

现在,在 OneService 上运行的几乎所有主要服务都面向 .NET 6。达到这一点是一项巨大的努力,跨越了1.5年的开发工作,一个实习生项目,两个月的飞行,数百次提交和数十个新的端到端测试。如果没有多个团队之间的密切合作,这项工作是不可能完成的。

虽然困难重重,但其中许多挑战使我们的团队变得更好。对 .NET 基础知识、测试和外部测试基础结构更改的更多了解已应用于其他项目。我们现在也有实现长期迁移的先例,并且对绩效工作的影响有了清晰的认识。我们在权衡何时转向或坚持方面积累了宝贵的经验。这些经验教训指导了我们的路线图,并影响了我们如何解决许多其他问题。

给TA打赏
共{{data.count}}人
人已打赏
.NET

增量ASP.NET 迁移到 ASP.NET Core

2022-6-9 10:21:00

.NET

宣布推出 .NET 7 预览版 5

2022-6-15 17:24:13

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索