译 | .NET Core 基础架构进化之路(一)

原文:Matt Mitchell

翻译:Edi Wang

随着 .NET Core 3.0 Preview 6 的推出,我们认为简要了解一下我们基础设施系统的历史以及过去一年左右所做的重大改进会很有用。

如果您对构建基础结构感兴趣,或者想要了解我们如何构建与 .NET Core 一样大的产品,那么此帖子将很有趣。它不描述应在下一个应用程序中使用的新功能或示例代码。如果您喜欢这些类型的帖子,请告诉我们。我们有几个类似计划,但希望知道此类信息是否对你有帮助。

一点历史

.NET Core 项目始于 3 多年前,与传统的微软项目相比,这是一个重大转变。

  • 在 GitHub 上公开开发
  • 多个集成在一起的独立 Git 仓库,而不是一个单独的庞大仓库
  • 面向多个平台
  • 其组件可能以多个"车辆"的形式发布(例如,Roslyn 作为 Visual Studio 和 SDK 的组件发布)

我们早期的基础设施决策是围绕必要性和权宜之计做出的。我们使用 Jenkins 进行 GitHub PR 和 CI 验证,因为它支持跨平台 OSS 开发。我们的官方版本位于 Azure DevOps(当时称为 VSTS)和 TeamCity(由ASP.NET核心使用),其中存在签名和其他关键运输基础结构。

我们使用手动更新包依赖项版本和有点自动化的 GitHub PRs 的组合将存储库集成在一起。团队独立构建了包装、布局、本地化和所有其他工具所需的工具,这些在大型开发项目中出现的任务。

虽然并不理想,但从某种意义上说,这在早期就足够有效了。随着项目从 .NET Core 1.0 和 1.1 发展到 2.0 及之后,我们希望投资一个更加集成的开发栈、更快的发布节奏和更简单的服务。我们希望生成一个新的带有最新运行时的 SDK,每天发布多次。我们希望在不降低独立存仓库的开发速度的情况下进行所有这些工作。

.NET Core 面临的许多基础结构挑战源于仓库结构的隔离、分布式性质。虽然多年来它变化很大,但该产品由 20-30 个独立 Git 仓库(ASP.NET Core 直到最近还拥有更多)组成。一方面,有许多独立的开发孤岛往往使这些孤岛的开发非常高效:开发人员可以在库中快速迭代,而不必担心技术栈的其余部分。另一方面,它使整个项目的创新和集成效率降低得多。一些示例:

  • 如果我们需要推出新的签名或打包功能,那么在使用不同工具的众多独立存储库中执行此操作的成本非常高。
  • 跨栈移动更改速度很慢且成本高昂。对于"低"位置栈中的修复和功能(例如 corefx 库)可能在几天内在 SDK(栈的"顶部")中看不到。如果我们在 dotnet/corefx 中进行修复,则必须构建该更改,并将新版本流入引用它的任何上栈组件(例如 dotnet/core 设置和ASP.NET Core),在那里将测试、提交和构建该更改。然后,这些新组件将需要将这些新输出进一步向上流,依此类推,直到达到头。

译者注:[栈] 的原文为 Stack,不是指栈数据结构,而是描述组成整个.NET Core的各种组件,它们一起,是一个栈。

在所有这些情况下,在许多层面上都有失败的机会,进一步减缓了这一进程。随着 .NET Core 3.0 规划的认真开始,很明显,如果不对我们的基础结构进行重大更改,我们就无法创建我们想要的范围的产品发布。

三管齐下的方法

我们开发了一个三管齐下的方法来减轻我们的痛苦:

  • 共享工具(又名Arcade) – 在我们的存储库中投资共享工具。
  • 系统整合 (Azure DevOps) - 抛弃 Jenkins 并拥抱集成 GitHub CI 的 Azure DevOps。将我们的官方版本从经典 VSTS 时代的流程移动到现代配置即代码。
  • 自动依赖项流和发现 (Maestro) – 显式跟踪依赖项,并快速更新它们。

Arcade

在 .NET Core 3.0 之前,有 3-5 种不同的工具实现分散在不同的仓库中,具体取决于您计数的方式。

  • 核心运行时仓库 (dotnet/coreclr, dotnet/corefx 以及dotnet/core-setup) 包含 dotnet/buildtools 工具。
  • ASP.NET核心的仓库 有 aspnet/KoreBuild
  • 使用 Repo Toolset 的各种仓库,如dotnet/symreader
  • 其他几个孤立的仓库具有独立的实现。

虽然在这个世界上,每个团队可以自定义他们的工具,并只构建他们需要的,但它确实有一些显著的缺点:

开发人员在仓库之间奔波的效率较低

示例:当开发人员从 dotnet/corefx 跑到 dotnet/core-sdk 时,存储库的"语言"是不同的。她键入什么来编译和测试?日志放在何处?如果她需要向回购中添加新项目,这是如何做到的?

每个必需的功能都被开发 N 次

示例:.NET Core 产生成吨的 NuGet 包。虽然有一些变化(例如,使用 dotnet/core-setup 生成的 Microsoft.NETCore.App 共享运行时包,与 Microsoft.AspNet.WebApi.Client 等"普通"软件包的构建方式不同),但生成它们的步骤相当类似。

遗憾的是,由于仓库的布局、项目结构等存在分歧,因此这些打包任务需要实现的方式不同。存储库如何定义应生成哪些包、这些包中的内容、其元数据等。如果没有共享工具,团队通常更容易实现另一个打包任务,而不是重用另一个打包任务。这当然对资源造成压力。

通过 Arcade,我们努力将所有仓库放在一个通用布局、仓库"语言"和任务集(如果可能的话)。这并非没有陷阱。任何类型的共享工具最终都解决了一些"金发(Goldilocks)"问题。如果共享工具过于规范,则任何重大规模的项目所需的自定义类型将变得困难,并且更新该工具变得非常困难。

使用新更新很容易破坏仓库。BuildTools 因此遭受损失。使用它的仓库与它紧密耦合,以至于它不仅不能用于其他仓库,而且在 BuildTools 中的任何更改通常以意想不到的方式使使用者崩溃。如果共享工具的规范性不够,则存储库在工具的使用上往往会出现偏差,而推出更新通常需要在每个单独的存储库中进行大量工作。在这一点上,为什么我们还需要共享工具?

Arcade 实际上尝试同时使用这两种方法。它将通用仓库"语言"定义为一组脚本(请参阅 eng/common)、通用仓库布局以及作为 MSBuild SDK 推出的通用生成目标集。选择完全采用 Arcade 的仓库具有可预测的行为,这使得更改易于跨仓库推出。不希望这样做的仓库可以从各种提供基本功能(如签名和打包)的 MSBuild 任务包中进行选择,这些功能在所有存仓库看起来都相同。当我们对这些任务进行更改时,我们会尽力避免重大更改。

让我们来看看 Arcade 提供的主要功能,以及它们如何集成到我们更大的基础架构中。

常规编译任务包

这些是 MSBuild 任务的基本层,可以独立使用,也可以作为 Arcade SDK 的一部分使用。他们是"付费才能玩"("Arcade"因此得名)。它们提供了大多数 .NET Core 仓库中所需的一组通用功能:

签名:

Microsoft.DotNet.SignTool

发布编译产物(跨仓库订阅源):

Microsoft.DotNet.Build.Tasks.Feed

打包:

Microsoft.DotNet.Build.Tasks.Packaging

常见的仓库目标和行为

这些是作为称为"Arcade SDK"的 MSBuild SDK 的一部分提供的。通过利用它,仓库选择加入默认的 Arcade 编译行为、项目和项目布局等。

通用仓库"语言"

一组使用依赖项流在所有 Arcade 存储库之间同步的通用脚本文件(稍后将介绍更多)。这些脚本文件引入了采用 Arcade 的仓库的通用"语言"。对于开发人员来说,在这些存储库之间移动变得更加无缝。此外,由于这些脚本在存储库之间同步,因此对 Arcade 存储库中的原始副本进行新更改可以快速将新功能或行为引入完全采用共享工具的存储库。

共享 Azure DevOps 作业和步骤模板

虽然定义公共存储库"语言"的脚本主要针对与人交互,但 Arcade 还有一组 Azure DevOps 作业和步骤模板,允许 Arcade 存储库与 Azure DevOps CI 系统进行接口。与常规编译任务包一样,步骤模板构成了一个基础层,几乎每个仓库都可以使用(例如,发送生成遥测)。作业模板形成更完整的单元,使存储库能够减少对 CI 流程细节的担心。

迁移到 Azure DevOps

如上所述,更大的团队在 2.2 版本中使用了 CI 系统的组合:

  • AppVeyor 和 Travis 用于 ASP.NET Core 的 GitHub PR
  • TeamCity 用于官方 ASP.NET 编译
  • Jenkins 用于其他 .NET Core 的 GitHub PR 和滚动验证。
  • 经典(非 YAML)Azure DevOps 工作流用于官方的非ASP.NET Core项目

许多区别只是为了必要性。Azure DevOps 不支持公共 GitHub PR/CI 验证,因此ASP.NET Core 转向 AppVeyor 和 Travis 来填补空白,而 .NET Core 则投资 Jenkins。经典 Azure DevOps 对构建业务流程没有很多支持,因此ASP.NET Core 团队转向 TeamCity,而 .NET Core 团队在 Azure DevOps 上构建了名为 PipeBuild 的工具来提供帮助。所有这些分歧都非常昂贵,即使在一些不明显的方式:

  • 虽然 Jenkins 是灵活的,但维护大量任务(6000-8000)是一项严肃的工作。
  • 在经典 Azure DevOps 之上构建我们自己的业务流程需要很多折衷。已检查的管道作业描述并非真正是人类可读的(它们刚刚导出了手动创建的生成定义的 json 描述),密钥管理很丑陋,在我们尝试处理生成要求的广泛差异。
  • 当正式编译与夜间(nightly)验证与 PR 验证过程在不同的系统中定义时,共享逻辑就变得困难。开发人员在进行流程更改时必须额外小心,因为很容易爆。我们在一个特殊的脚本文件中定义了 Jenkins PR 作业,TeamCity 有许多手动配置的作业,AppVeyor 和 Travis 使用自己的 yaml 格式,Azure DevOps 具有我们在它之上构建的模糊自定义系统。很容易在 PR 中更改生成逻辑并中断官方的 CI 构建。为了缓解这种情况,我们确实努力在正式 CI 和 PR 构建中通用的脚本中保留尽可能多的逻辑,但差异总是随着时间的推移而逐渐减少。某些差异(如在构建环境中)基本上不可能完全消除。
  • 更改工作流的做法差别很大,而且往往难以理解。开发人员了解了 Jenkins 用于更新 PR 逻辑的 netci.groovy 文件,但并未转换为用于正式 CI 构建的 PipeBuild json 文件。因此,对系统的知识通常被隔离到少数团队成员中,这在大型组织中并不理想。

当 Azure DevOps 开始推出基于 YAML 的构建管道,并在 .NET Core 3.0 开始启动时对公共 GitHub 项目的支持,我们认识到我们具有独特的机会。有了这种新的支持,我们可以将所有现有的工作流从单独的系统移动到现代 Azure DevOps 中,还可以对如何处理正式的 CI 和 PR 工作流进行一些更改。我们从以下工作大致概要出发:

  • 将所有逻辑保存在代码中,在 GitHub 中。随时随地使用 YAML 管道。
  • 有一个公开和私有项目。
    • 公开项目将通过 GitHub 存储库和 PR 运行所有公共 CI,正如我们始终拥有的
    • 私有项目将运行官方 CI 是我们需要进行的任何私人更改的场所,在存储库中匹配公共 GitHub 仓库
    • 只有私有项目才能访问受限制的资源。
  • 在官方 CI 和 PR 生成之间共享相同的 YAML。使用模板表达式来区分公共项目和私有项目,其中行为必须分,或者仅访问私有项目中可用的资源。虽然这通常使整个 YAML 定义更混乱一些,但这意味着:
    • 进行流程更改时,爆掉的可能性较低。
    • 开发人员只需更改一组位置来更改官方 CI 和 PR 流程。
  • 为常见任务构建 Azure DevOps 模板,以将样板 YAML 的重复降至最低,并启用使用依赖项流轻松推出更新(例如遥测)。

到目前为止,所有主 .NET Core 3.0 仓库都在 Azure DevOps 上,用于其公共 PR 和官方 CI。一个很好的例子管道是 dotnet/arcade 自己本身的官方编译/PR管道。

译者注:Arcade 自己的编译管道 https://github.com/dotnet/arcade/blob/master/azure-pipelines.yml

(文章翻译未完待续)

本文分享自微信公众号 - 汪宇杰博客(ediwangblog)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励