我们严重依赖大型Go monorepo 进行后端开发,它提供了代码可重用性和可发现性等优势。然而,随着我们的不断发展,管理大型 monorepo 带来了一系列独特的挑战。

例如,使用 Go 命令(如 go get 和) go list 在获取驻留在大型多模块存储库中的 Go 模块时可能会非常慢。这种缓慢状态会影响开发人员的工作效率,给我们的持续集成 (CI) 系统带来负担,并使我们的版本控制系统主机 (VCS) GitLab 不堪重负。

在这篇博文中,我们将了解 Go模块代理 Athens 如何帮助改善我们使用大型 Go monorepo 工程师的开发人员体验。

主要亮点

  • 在获取 monorepo Go 模块时,我们将 go get 执行命令的时间从 ~18 分钟减少到 ~12 秒。
  • 通过利用 Athens 的fallback网络模式和 Golang GOVCS 模式,我们将整个 Athens 集群缩减并缩减了 70%,从而节省了成本并提高了效率。

问题和解决方案

1. Go 命令性能缓慢

问题摘要:在我们的 monorepo 中运行 go get 命令需要相当长的时间,并可能导致我们的 VCS 性能下降。

使用 Go 编程语言时, go get 是你每天最常用的命令之一。除了开发人员之外,CI 系统也使用此命令。

go get 做了什么 ?

go get 命令用于在 Go 中下载和安装包及其依赖项。请注意具体取决于它是在传统 GOPATH 模式下还是在模块感知模式下运行它的运行方式会有所不同。我们在多模块存储库设置中使用模块感知模式。

go get 每次运行时,它都会使用 Git 命令(如 git ls-remote 、、 git tag git fetch 等)来搜索和下载整个工作树。在我们的 monorepo 上过度使用这些 Git 命令会导致处理时间过长,并且可能会对我们的 VCS 造成压力。

我们的 monorepo 有多大?

为了充分掌握我们的工程团队面临的挑战,了解我们每天使用的 monorepo 的庞大规模至关重要。为此,我们使用 git-sizer 来分析我们的 monorepo。

以下是我们的发现:

  • 整体存储库大小:monorepo 的总未压缩大小为 69.3 GiB,这是一个相当大的数字。从这个角度来看,以其庞大而闻名的 Linux 内核存储库目前为 55.8 GiB。
  • Trees:Trees总数为3.21M,Trees条目为99.8M,消耗3.65 GiB。这可能会导致某些 Git 操作期间出现性能问题。
  • References:总共 10.7k References。
  • 最大的checkouts:我们的monorepo中有64.7k目录。这会影响 和 git checkoutgit status 操作。此外,我们的 monorepo 的最大路径深度为 20。这会导致 Git 上的处理时间变慢,并对开发人员体验产生负面影响。文件数量 (354k) 和文件总大小 (5.08 GiB) 也是值得关注的问题,因为它们对存储库性能的潜在影响。

要进行比较,请参阅 Linux 存储库的 git-sizer 输出。

“慢”有多慢?

为了进一步说明这个问题,我们将比较各种 Go 命令以 10 MBps 的下载速度在我们的 monorepo 中获取单个模块所需的时间。

这是在我们的 monorepo 中如何构建模块的示例:

gitlab.company.com/monorepo/go
  |-- go.mod
  |-- commons/util/gk
        |-- go.mod
Go 命令GOPROXY是否有缓存描述结果(所用时间)
go get -x gitlab.company.com/monorepo/go/commons/util/gkproxy.golang.org,directYes下载并安装最新版本的模块。这是开发人员经常遇到的常见方案。18:50.71 分钟
go get -x gitlab.company.com/monorepo/go/commons/util/gkproxy.golang.org,directNo下载并安装最新版本的模块,无需任何模块缓存1:11:54.56 小时
go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gkproxy.golang.org,directYes列出有关模块的信息3.873 秒
go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gkproxy.golang.org,directNo列出有关没有任何模块缓存的模块的信息3:18.58分钟

在此示例中,用 go get 获取模块需要 18 分钟以上才能完成。如果我们需要在 monorepo 中检索多个模块,这可能会非常耗时。

为什么在 monorepo 中很慢?

在大型 Go monorepo 中, go get 由于以下几个因素,命令可能会很慢:

  1. 大量文件和目录:运行时 go get ,该命令需要搜索并下载整个工作树。在大型多模块 monorepo 中,大量的文件和目录使此搜索过程非常昂贵和耗时。
  2. 引用数量:我们的 monorepo 中的大量引用(分支或标签)会影响性能。引用广告 ( git ls-remote ),其中包含我们 monorepo 中的每个引用,是任何远程 Git 操作的第一阶段,例如 git clone or git fetch .对于大量引用,在执行这些操作时性能会受到影响。
  3. 提交历史记录遍历:需要遍历存储库的提交历史记录并考虑每个引用的操作在 monorepo 中会很慢。 monorepo 越大,这些操作就越耗时。

后果:扼杀生产力和系统压力

开发人员和 CI

当 Go 命令操作go get很慢时,它们会导致软件开发工作流程中的严重延迟和效率低下。这会导致生产力降低和开发人员的积极性下降。

优化 Go 指挥操作的速度对于确保高效的软件开发工作流程和高质量的软件产品至关重要。

版本控制系统

还值得注意的是,过度使用 go get 命令也会导致 VCS 的性能问题。当经常使用 go get 下载 Go 软件包时,我们发现它在我们的 VCS 集群中造成了瓶颈,这可能导致性能下降甚至导致速率限制队列问题。

这会对 VCS 基础架构的性能产生负面影响,导致某些用户和 CI 出现延迟或有时不可用。

解决方案: Athens + fallback 网络模式+ GOVCS +自定义缓存刷新解决方案

问题摘要:通过不从我们的 VCS 获取来加快 go get 命令速度

我们通过使用 Athens 解决了速度问题, Athens 是 Go 模块的代理服务器(阅读更多关于 GOPROXY 协议的信息)。

Athens 如何运作?

以下序列图描述了 Athens 的默认 go get 命令流。

Athens将存储系统用于Go模块包,也可以配置为使用各种存储系统,例如Amazon S3和Google Cloud Storage等。

通过将这些模块包缓存在存储中,Athens 可以直接从存储中提供包,而不是在提供 Go 命令(如 go mod download 和某些 go 构建模式)的同时从上游 VCS 请求它们。但是,仅使用 Go 模块代理并不能完全解决我们的问题,因为 go get and go list 命令仍然通过代理命中我们的 VCS。

考虑到这一点,我们想“如果我们可以直接从 Athens 的存储中为 Go 模块提供服务会怎样 go get ?这个问题让我们发现了 Athens 网络模式。

什么是 Athens 网络模式?

Athens NetworkMode 配置 Athens 将如何返回 Go 命令的结果。它可以从自己的存储和上游 VCS 组装。从 Athens v0.12.1 开始,它目前支持这 3 种模式:

  1. stricy:将 VCS 版本与存储版本合并,但如果其中任何一个版本失败,则会失败。
  2. offline:仅获取存储版本,切勿联系 VCS。
  3. fallback:如果 VCS 出现故障,仅返回存储版本。回退模式尽最大努力为您提供请求版本时的可用内容。

我们的 Athens 集群最初设置为使用 strict 网络模式,但这对我们来说并不理想。因此,我们探索了其他网络模式。

探索 offline 模式

我们最初试图探索将 Athens 置于 offline 网络模式的想法,这将允许 Athens 仅从其存储中为 Go 请求提供服务。这一概念符合我们的目标,即减少 VCS 命中率,同时显著提高 Go 工作流程的性能。

然而,在实践中,这不是一个理想的方法。当用户请求新的模块版本时,默认的 Athens 设置( strict 模式)会自动更新模块版本。但是,将 Athens 切换到 offline 模式将禁用自动更新,因为它不会连接到VCS。

自定义缓存刷新解决方案

为了解决这个问题,我们实现了一个 CI 管道,每当在我们的 monorepo 中发布新模块时,它都会刷新 Athens 的模块缓存。将此与 offline 模式一起使用使Athens对monorepo有效,但它导致其他存储库的自动更新丢失

恢复此功能需要将我们的自定义缓存刷新解决方案应用于所有其他 Go 存储库。但是,实施此解决方法可能非常麻烦,并且需要花费大量额外的时间和精力。我们决定寻找另一种从长远来看更容易维护的解决方案。

平衡的方法: fallback 模式和 GOVCS

此方法基于我们前面提到的自定义缓存刷新,该刷新是专门为 monorepo 设计的。

通过 GOVCS 环境变量,我们将其与 fallback 网络模式结合使用,以有效地仅将 monorepo 置于“offline”模式。

设置为 GOVCS gitlab.company.com/monorepo/go:off 时,Athens 在尝试从 VCS 获取模块时会遇到错误:

gitlab.company.com/monorepo/go/commons/util/gk@v1.1.44: unrecognized import path "gitlab.company.com/monorepo/go/commons/util/gk": GOVCS disallows using git for private gitlab.company.com/monorepo/go; see 'go help vcs'

如果 Athens 网络模式设置为 strict ,则 Athens 将向用户返回 404 错误。通过切换到 fallback 模式,Athens 会在 GOVCS 发生故障时尝试从其存储中检索模块。

以下是更新的 Athens 配置(示例默认配置):

GoBinaryEnvVars = ["GOPROXY=direct", 
"GOPRIVATE=gitlab.company.com", 
"GOVCS=gitlab.company.com/monorepo/go:off"]

NetworkMode = "fallback"

通过自定义缓存刷新解决方案与这种方法相结合,我们不仅加快了 monorepo 中 Go 模块的检索速度,而且还允许自动更新非 monorepo Go 模块。

最终结果

此解决方案为我们的开发人员显著提高了 Go 命令的性能。在 Athens ,同样的命令只需~12秒(低于~18分钟),速度非常快。

Go 命令戈代理是否有缓存描述结果(所用时间)
go get -x gitlab.company.com/monorepo/go/commons/util/gkgoproxy.company.comYes下载并安装最新版本的模块。这是开发人员经常遇到的常见方案。11.556 seconds
go get -x gitlab.company.com/monorepo/go/commons/util/gkgoproxy.company.comNo下载并安装最新版本的模块,无需任何模块缓存1:05.60分钟
go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gkgoproxy.company.comYes列出有关 monorepo 模块的信息0.592 seconds
go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gkgoproxy.company.comNo列出有关没有任何模块缓存的 monorepo 模块的信息1.023 秒

平均集群 CPU 使用率|

平均集群内存利用率|

此外,对 Athens 集群的这一更改还导致平均集群 CPU 和内存利用率大幅降低。这也使我们能够将整个 Athens 集群缩小和缩小 70%,从而节省成本并提高效率。最重要的是,我们还能够有效地消除 VCS 的速率限制问题,同时使 monorepo 的命令操作速度大大加快。

2. GitLab 子组中的 Go 模块

问题摘要:Go 模块无法在 GitLab 子组下与私有或内部存储库本机配合使用。

在管理代码存储库和包时,GitLab 子组和 Go 模块已成为 Grab 开发过程中不可或缺的一部分。Go 模块有助于组织和管理依赖项,GitLab 子组提供了一个额外的结构层,将相关的存储库分组在一起。

但是,使用 Go 模块时的一个常见问题是它们无法与 GitLab 子组下的私有或内部仓库本机配合使用(请参阅此 GitHub 问题)。

例如,用于 go get 从中 gitlab.company.com/gitlab-org/subgroup/repo 检索模块将导致失败。此问题并非特定于 Go 模块,子组下的所有存储库都将面临相同的问题。

繁琐的解决方法

为了克服这个问题,我们必须使用解决方法。一种解决方法是通过向 .netrc 计算机上的文件添加身份验证详细信息来对 GitLab 的 HTTPS 调用进行身份验证。

可以将以下行添加到 .netrc 文件中:

machine gitlab.company.com
login user@company.com
password <personal-access-token>

在我们的例子中,我们使用的是个人访问令牌(PAT),因为我们启用了2FA。如果未启用 2FA,则可以改用 GitLab 密码。但是,这种方法意味着在每个 Go 开发人员的计算机上配置 .netrc CI 环境以及文件。

解决方案: Athens + .netrc

一个可行的解决方案是在 Go 代理服务器中设置 .netrc 文件。此方法消除了 N 个开发人员配置自己的 .netrc 文件的需要。相反,此任务的责任委托给 Go 代理服务器。

3. 共享公共库

问题摘要:在不授予直接存储库访问权限的情况下在 monorepo 中分发内部公共库可能具有挑战性。

在Grab,我们与各种跨职能团队合作,有些人可能像不同的VPN一样拥有不同的网络访问权限。这增加了与他们共享我们的 monorepo 内部公共库的复杂性。为了维护我们 monorepo 的安全性和完整性,我们使用 Go 代理来控制对必要库的访问。

通过 VCS 授予对 monorepo 的直接访问权限和使用 Go 代理之间的主要区别在于,前者允许用户读取存储库中的所有内容,而后者使我们能够仅授予对用户在 monorepo 中需要的特定库的访问权限。这种方法可确保跨不同网络配置的安全高效协作。

没有 Go 模块代理

如果没有Athens,我们需要创建一个单独的存储库来存储我们想要共享的代码,然后使用构建系统自动将代码从monorepo镜像到公共存储库。

此过程可能很麻烦,并导致两个存储库之间的代码版本不一致,最终使维护共享库变得具有挑战性。

此外,复制代码可能会导致错误,并通过暴露机密或敏感信息来增加安全漏洞的风险。

解决方案: Athens +下载模式文件

为了解决这个问题,我们利用 Athens 的下载模式文件功能,使用允许列表方法来指定用户可以下载哪些存储库。

以下是 Athens 下载模式配置文件的示例:

downloadURL = "https://proxy.golang.org"

mode = "sync"

download "gitlab.company.com/repo/a" {
    mode = "sync"
}

download "gitlab.company.com/repo/b" {
    mode = "sync"
}

download "gitlab.company.com/*" {
    mode = "none"
}

在配置文件中,我们为每个所需的存储库指定允许列表条目,包括它们各自的下载模式。例如,在上面的代码段中, repo/a 并且 repo/b 允许 ( mode = “sync” ),而其他所有内容都使用 mode = “none” .

最终结果

在这种情况下,通过使用 Athens 的下载模式功能,好处是显而易见的。 Athens 提供了一个安全、集中的地方来存储 Go 模块。这种方法不仅提供了一致性,而且还提高了可维护性,因为所有代码版本都在一个位置进行管理。

Go 代理的其他好处

正如我们已经谈到了在Grab上实施Athens Go代理所取得的令人印象深刻的结果,探索这个强大解决方案附带的补充优势至关重要。

这些无名的好处虽然可能被忽视,但在丰富Grab的整体开发人员体验和促进更强大的软件开发实践方面发挥着至关重要的作用:

  1. 模块不变性:随着软件世界继续面临库更改或消失的问题,Athens 通过为复制的 VCS 代码提供不可变存储来缓解构建中断的有用工具。使用 Go 代理还可以确保构建保持确定性,从而提高我们软件的一致性。
  2. 不间断开发:Athens 允许用户在 VCS 关闭时获取依赖项,从而确保连续和无缝的开发工作流程。
  3. 增强的安全性:Athens 通过启用阻止 Grab 中的特定软件包来提供访问控制。这一额外的安全层保护我们的工作免受恶意第三方软件包的潜在风险。
  4. 供应商目录删除:Athens为最终删除供应商目录做好了准备,从而在未来促进更快的工作流程。

下一步是什么?

自从采用 Athens 作为 Go 模块代理以来,我们观察到了相当大的好处,例如:

  1. 加速 Go 命令操作
  2. 降低基础设施成本
  3. 缓解了 VCS 负载问题

此外,其鲜为人知的优势,如模块不变性、不间断开发、增强的安全性和供应商目录转换,也有助于改进开发实践,丰富 Grab 工程师的开发人员体验。

如今,导出三个环境变量的简单过程极大地影响了我们开发人员在 Grab 的体验。

export GOPROXY="goproxy.company.com|proxy.golang.org,direct"

export GONOSUMDB="gitlab.company.com"

export GONOPROXY="none"

我们一直在寻找改进和优化我们工作方式的方法,因此我们为像 Athens 这样的开源项目做出贡献,在那里我们帮助修复错误。如果您有兴趣设置 Go 模块代理,请尝试一下 Athens