Goproxy for Tencent

1.背景

近日腾讯正式对外发布 2020 年度《腾讯研发大数据报告》,这份由腾讯技术委员会出品的报告,披露了过去一年腾讯在研发投入、研发效能及开源协同等方面的重要数据。C++ 蝉联腾讯最受欢迎的编程语言。随着云计算和微服务相关技术的进一步发展,Go语言使用次数增速第一,并超越 JavaScript 成为腾讯第二受欢迎的编程语言。基于如此巨大的 Go 开发者基数,内部开发体验就显得尤为重要。

但是长久以来 Go 的版本管理确实是大家开发中遇到最头疼的问题,在 GOPATH 模式中,我们将所需依赖代码拉取到 GOPATH/src 中,这样在编译过程中,Go 会去 GOPATH 中根据 import path 寻找响应的代码库。但是如果我们重新拉取了最新的代码或者自己修改了这个依赖已有的代码,整个编译过程不会察觉到这些变更,可能会造成编译失败或者线上故障,于是从 Go 1.11 起,Go 官方引入了 Go Modules 来解决版本管理的问题,用一句话解释什么是 Go Modules 那就是:

Modules are how Go manages dependencies.

Go Modules 是一种分散式设计,尽管有很多诸如 goproxy.io 这样的公共服务,但是 Go Modules 设计中没有一个公开的注册中心,模块作者可以在代码仓库中创建版本标签来发布新版本。

$ git tag v1.2.3
$ git push --tags

其他开发者就可以立刻下载这个版本了

$ go get -d example.com/mod@v1.2.3

2. 使用 GOPROXY

我们建议默认配置使用 GOPROXY 来获取依赖模块,原因有下面几个:

2.1 企业内部证书问题收敛

很多公司内的证书都是自己签发的,在各个操作系统、浏览器、工具中都不被信任,这给内部的开发者带来了额外的配置负担,他们在使用各种工具时,需要强行去信任这个内部签发的证书,我们可以再 goproxy server 这一层做同一信任配置,这样众多开发者就无需再进行额外操作。

2.2 海外资源可用性高

诸如 golang.org/x 类似的一些常用的包托管于谷歌服务器上,我们需要用到 proxy 帮我们进行获取,这里就不展开了。

2.3 模块拉取速度快

有些人说只有海外资源才有加速的需求,其实不是这样的,即使是内网的代码依赖模块,如果你使用 proxy 也比直接拉取快很多,接下来我们分析下原因。

Go 可以通过 HTTP 协议通过 proxy 下载依赖模块,这要比直接从源代码库中拉取快 5 到 20 倍。GOPROXY 的协议是无状态的,它的设计非常简单以致于我们可以使用一个静态的文件服务来实现它。Go 内部在实现的时候也支持file:// 协议,这样避免了比如在测试场景中获取依赖时产生对网络的依赖,可以直接使用一个本地文件系统作为 proxy 服务。下图是我们使用腾讯名字服务项目(北极星)进行的一项拉取测试。

使用 goproxy 下载之所以如此之快追究其原因有下面几个:

  1. Go 无需下载整个代码仓库或者某个完整的 commit,它只需要下载相应版本的 zip 包,这个包中只包含编译所需的源码文件。
  2. 在 Go 做版本选择时,Go 只需使用到 mod 文件,可以避免下载整个代码仓库或者 zip 包。
  3. 如果这些 mod 和 zip 文件已经在 proxy 的缓存中了,获取非常快,甚至像 goproxy.io 这样的公共服务还使用了腾讯云的全球 CDN 进行了加速,并且在亚洲、美洲和欧洲部署了独立的服务进行回源。

2.4 降低了对源代码托管服务(Git)的依赖性

如果有一天公司内部的 gitlab 或者外部的 github 服务临时不可用了,虽然我们无法提交发布新版本了,但是如果proxy 缓存了你之前的依赖模块,我们已有的编译将不会受到影响, CI/CD 也不会失败。甚至有些源码仓库被删除了,我们还可以继续进行编译。

2.5 fallback 机制

如果用户配置的 goproxy 服务不可用了怎么办? 好在 Go 支持配置逗号或者管道分隔的 goproxy 列表,如果第一个服务器返回了 404 或者 410,甚至是不可用(超时),Go 可以根据用户的配置尝试使用其他 goproxy 服务或者直接使用本地直接获取的方式。这种 fallback 机制使得 Go 不依赖于某一个 goproxy 服务的可用性。

2.6 更加安全可控

使用公司内部的 goproxy 可以防止公司内部开发人员配置不当造成项目中 import path 泄露到公网中,避免给一些敏感业务和项目带来不必要的损失和麻烦。

3. Goproxy For Tencent

既然使用 goproxy 服务有这么多的好处,那么为腾讯内部提供一个稳定的 goproxy 服务势在必行了,而且很多团队的呼声也非常高。可惜谷歌实现的 proxy 不是开源的,于是我们使用了goproxy.io 开源项目。首先确保要运行的服务器是已经安装了 go 命令,goproxy 项目是开源的,用 go 语言开发,使用 Go modules 可以很方便的进行编译:

git clone https://github.com/goproxyio/goproxy.git
cd goproxy
make

编译好的文件位置是 ./bin/goproxy , 使用 ./bin/goproxy -h 查看参数使用说明:

Usage of ./bin/goproxy:
  -cacheDir string
        go modules cache dir  [指定 Go 模块的缓存目录]
  -exclude string
        exclude host pattern  [proxy 模式下指定哪些 path 不经过上游服务器]
  -listen string
        service listen address [服务监听端口,默认 8081]
  -proxy string
        next hop proxy for go modules [指定上游 proxy server,推荐 goproxy.io]

由于我们还需要加速内网代码模块,所以我们使用 router 模式:

                                         direct
                      +----------------------------------> internal repos
                      |
                 match|pattern
                      |
                  +---+---+           +----------+
go get  +-------> |goproxy| +-------> |goproxy.io| +---> golang.org/x/net
                  +-------+           +----------+
                 router mode           proxy mode

使用下面命令行启动服务:

./bin/goproxy -listen=0.0.0.0:80 -cacheDir=/data/modules -proxy https://goproxy.io -exclude "git.tencent.com"

配置好环境变量,接下来开发者就可以从这个服务上拉取代码了:

export GOPROXY=http://[你的服务器IP]:80
export GOSUMDB=off
go get github.com/pkg/errors

这样用户可以通过上面的配置拉取外部公共依赖模块和内部公共模块代码库,如果要编译的项目对内部私有代码库有依赖怎么办呢,还需要配置一个变量:

GOPRIVATE=*.corp.example.com,rsc.io/private

关于这个变量的具体介绍可以参考:https://goproxy.io/zh/docs/GOPRIVATE-env.html

由于腾讯内部开发者和 Go 项目众多,我们还实现了一个内部的 sumdb 服务,所以即使是腾讯内部代码仓库也可以被记录哈希值,防止项目依赖模块重新打标造成错误的编译,从而带来不可挽回的损失, 同时也保障了编译项目的源码安全。最后,由于这个项目支持 Promethues 监控,接下来把服务指标接入到 Promethues 监控中,并且把架构调整好,腾讯内部 goproxy 服务的架构其实也并不复杂:

结语

Go Modules 的引入确实大大提升了开发者体验,它解决了 GOPATH 模式下很多解决不了的问题。目前腾讯内部goproxy 服务每天的请求量在百万级别,它帮助了腾讯云、腾讯文档、腾讯新闻、腾讯游戏等多个 BG 和部门,为大家的编译节省了大量的等待时间,提高了内部服务的编译成功率和开发体验。