Athens 的设计
本文档旨在详细阐述 Athens 的设计理念。我们希望为您提供一个良好的开端,通过文字和图表而非代码来描述 Athens 的架构。您可以阅读源码并提出疑问(我们始终很乐意回答)!
本节内容概览
我们将本节分为两个核心部分:
如何阅读本节
与其他部分不同,本文档作为参考资料,无需按顺序通读即可发挥其价值。我们希望您在参与项目贡献时能最大程度地利用本节内容。
本文档旨在详细阐述 Athens 的设计理念。我们希望为您提供一个良好的开端,通过文字和图表而非代码来描述 Athens 的架构。您可以阅读源码并提出疑问(我们始终很乐意回答)!
我们将本节分为两个核心部分:
与其他部分不同,本文档作为参考资料,无需按顺序通读即可发挥其价值。我们希望您在参与项目贡献时能最大程度地利用本节内容。
Athens 代理有两个主要用途:
本文档详细介绍了 Athens 代理的功能,您可以使用这些功能来实现任一用途。
将代理主要部署在企业内部可以:
重要的是,Athens 代理并不打算作为上游代理的完整镜像。对于公共模块,其角色是在本地存储模块并提供访问控制。
当用户从代理请求模块 MxV1,而 Athens 代理的存储中没有 MxV1 时,它首先确定 MxV1 是私有模块还是非私有模块。
如果是私有模块,它立即从内部 VCS 将模块存储到代理存储中。
如果不是私有模块,Athens 代理会查询其排除列表以获取非私有模块(见下文)。如果 MxV1 在排除列表上,Athens 代理返回 404 并且终止其他流程。如果 MxV1 不在排除列表上,Athens 代理执行以下算法:
upstreamDetails := lookUpstream(MxV1)
if upstreamDetails == nil {
return 404 // if the upstream doesn't have the thing, just bail out
}
return upstreamDetails.baseURL这个算法的重要部分是 lookUpstream。该函数查询上游代理上的一个端点:
MxV1,则返回 404MxV1,则返回 MxV1 的 base URL在项目的更高版本中,我们可能会在代理上实现事件流,任何其他代理都可以订阅并监听其关心的模块的删除/弃用信息
为了适应私有(企业)部署,Athens 代理维护两个重要的访问控制机制:
私有模块过滤器是字符串通配符模式,告诉 Athens 代理什么是私有模块。例如,字符串 github.internal.com/** 告诉 Athens 代理:
github.internal.com 下载模块代码(在其存储机制中)公共模块排除列表也是通配符模式,告诉 Athens 代理它永远不应该从任何上游代理下载这些模块。例如,字符串 github.com/arschles/** 告诉 Athens 代理始终向客户端返回 404 Not Found。
代理提供了一个 /catalog 服务端点,用于获取本地存储中包含的所有模块及其版本。该端点接受一个分页Token和一个页面大小参数来分页查询。
查询格式为:
https://proxyurl/catalog?token=foo&pagesize=47
其中 token 是可选的分页参数,pagesize 是返回页大小。
首次调用时不需要 token 参数,它用于分页查询。
结果是以下结构的 json:
{"modules": [{"module":"github.com/athens-artifacts/no-tags","version":"v1.0.0"}],
"next":""}如果没有返回 next token,则表示当前是最后一页。默认分页大小为 1000。
在软件构建过程中,不可重现性一直是困扰开发者的难题。Athens 旨在解决这一问题,其存储机制尤为关键。
初始状态下,Athens 代理的存储为空。
用户在此阶段发出请求时,工作方式如下图所示:
go get 来获取新模块。整个过程为同步流程。
此刻 Athens 代理已知晓模块 M 的 v1.0 版本,可立即将该模块提供给用户,无需再从 VCS 获取。
您阅读了代理、通信文档,然后打开代码库并对自己说:这并不像文档描述的那么简单。
Athens 有一系列架构组件,负责处理 Go 模块从 VCS 进入存储再到用户的整个过程。如果您对这些组件如何协同工作感到困惑,请继续阅读!
从通信中,您知道当模块没有在存储中找到时,它会从 VCS(如 github.com)下载,然后提供给用户。您还知道整个过程是同步的。但阅读源码时,您会遇到模块获取器和下载协议暂存器等组件,难以区分其功能。本文将帮助您理解整个流程。
请参阅下图了解各组件架构:
如图所示,存在多层包装器。您在代码中首先遇到的组件是 Storage 和 Fetcher,下面我们首先从这两个组件开始介绍。
Storage 名称即其功能描述。通过 proxy/storage.go 的 GetStorage 函数创建存储实例。
基于传入的存储类型环境变量,可创建内存、文件系统、MongoDB 等多种存储。
模块存储于此。
Fetcher 是我们介绍的第一个组件。从名称可以推断,Fetcher(pkg/module/fetcher.go)负责从 VCS 获取源代码。
为此,需要两项要素:go 二进制文件和 afero.FileSystem,在初始化期间传递给 Fetcher。
mf, err := module.NewGoGetFetcher(goBin, fs)
if err != nil {
return err
}app_proxy.go
当请求新模块时,会调用 Fetch 函数。
Fetch(ctx context.Context, mod, ver string) (*storage.Version, error)fetch 函数
Fetcher 的工作流程如下:
FileSystem 创建一个临时目录main.go 和 go.mod,以便使用 go CLIgo mod download -json {module}此命令将模块下载到存储中。下载完成后:
Fetch 函数从存储中读取模块数据并返回给调用者go mod 命令在返回的 JSON 响应中会包含模块文件的确切路径。为保持组件精简和可读,我们不愿将存储功能放到 Fetcher中使其膨胀。对于将模块保存到存储中,我们使用 Stasher,这是 Stasher 的唯一职责。
我们认为保持组件小而正交很重要,所以 Fetcher 和 storage.Backend 不相互交互。相反,Stasher 将它们组合在一起,并协调获取代码并存储的过程。
New 方法接受 Fetcher 和 storage.Backend 以及一组包装器(稍后解释)。
New(f module.Fetcher, s storage.Backend, wrappers ...Wrapper) Stasherstasher.go
pkg/stash/stasher.go 中的代码并不复杂,但很重要。其主要完成两项工作:
Fetcher 获取模块数据storage 存储数据仔细阅读代码,您会注意到传递给基本 Stasher 实现的包装器。这些包装器添加了更高级的逻辑,有助于保持组件整洁。
新方法返回一个包装器包装后的 Stasher 。
for _, w := range wrappers {
st = w(st)
}stasher.go
由于下载模块是资源密集型(内存)操作,Pool(pkg/stash/with_pool.go)帮助我们控制并发下载数量。
它使用 N-worker 模式,启动指定数量的 worker,然后等待任务完成。Worker 完成任务后返回结果,等待下一个任务。
在这种情况下,一个任务就是对底层 Stasher 的 Stash 函数调用。
我们知道模块获取是资源密集型操作,我们刚刚限制了并行下载的数量。为了帮助我们节省更多资源,我们希望避免多次处理同一个模块。
SingleFlight 包装器(pkg/stash/with_singleflight.go)在内部使用 map 跟踪当前下载,避免重复处理。
如果任务到来且 map[moduleVersion] 为空,用回调通道初始化它,并在底层 Stasher 上开启一个 Stash 任务。
s.subs[mv] = []chan error{subCh}
go s.process(ctx, mod, ver)如果请求的模块已有条目,SingleFlight 将订阅结果:
s.subs[mv] = append(s.subs[mv], subCh)一旦任务完成,模块被传递至上一层 download protocol(或可能包装的 stasher)。
最外层是 download protocol 下载协议。
dpOpts := &download.Opts{
Storage: s,
Stasher: st,
Lister: lister,
}
dp := download.New(dpOpts, addons.WithPool(protocolWorkers))它包含我们前面提到的两个组件:Storage 和 Stasher,以及一个额外的组件:Lister。
Lister 用于在 List 和 Latest 函数中用于在上游代理中查找可用版本。
Storage 在这里又出现了,之前在 Stasher 中用于保存。在 Download protocol 中,其用于检查模块是否已存在。如果已存在,则直接从 storage 获取。
否则,Download protocol 使用 Stasher 下载模块,将其存储到 storage,然后返回给用户。
您还可以在上面的代码片段中看到 addons.WithPool。这个 addon 类似于 Stash wrapper - Pool。它控制代理可以处理的并发请求数量。
