
在 2023 年的开源活动中,李锐同学参加了 Nydus 容器开源生态集成课题的相关工作。
大家好!我是来自国防科大的李锐,在开源之夏 2023 中报名参与了 Nydus 项目,在导师的指导下完成了社区的两个改进性题目,体验了如何从零开始参与一个开源项目。
Nydus GitHub:
https://github.com/dragonflyoss/nydus
01
容器运行时与镜像
1.1 OCI 容器运行时
相较于虚拟机操作系统级的隔离,容器是一种更轻量的进程级隔离技术。容器的广泛应用促进了该领域的生态繁荣,目前主流容器生态如图所示(摘自参考 [1]),容器的运行时有 Containerd,CRI-O,以及底层的 Runc,前者接受容器创建请求,OCI 镜像处理与 runtime spec 准备等,后者是对 namespace 和 cgroups 等资源隔离技术的封装。

1.2 OCI 容器镜像
容器镜像技术是容器得到广泛应用的核心技术,它是一种轻量、独立的软件包,包含了运行应用所需的所有内容。容器运行时,文件内容及相关配置文件共同组成了容器的静态文件系统 —— RootFS,实现了“构建一次,到处运行”。现有的容器镜像采用分层的方式构建,每个镜像由一组层组成。然而标准的 OCI 容器镜像格式无法实现按需拉取,也有数据去重粒度等问题。

1.3 Nydus 简要介绍
Nydus 镜像加速框架项目是 CNCF 开源项目 Dragonfly 的子项目,它是对 OCI 镜像格式的探索改进,Nydus 提供了容器镜像与多种数据的按需加载的能力,它已在生产环境支撑了每日百万级别的容器创建,将容器或代码包的端到端冷启动时间从分钟级降低到了秒级。Nydus 目前由蚂蚁集团、阿里云、字节跳动联合研发,也是 Kata Containers 与 Linux 内核态原生支持的镜像加速方案,Nydus 的具体介绍可参考链接 [2] 和 [3]。
1.4 Nydus 生态及运行原理
以 Nydus 支持的 FUSE 用户态实现为例,镜像加速运行原理如图所示 (摘自链接 [4] ), Containerd 支持以 Remote Snapshotter 插件的形式扩展镜像层的挂载,具体可参考链接 [5],Nydus Snapshotter 这种插件的实现,用于支持 Containerd 运行 Nydus 镜像。Containerd 在拉取镜像层前,会调用 Snapshotter 接口,Snapshotter 会告知 Containerd 只拉取 Nydus 镜像元数据层(通常非常小),并跳过数据层的下载,接下来启动新的 Nydusd 进程来挂载 Nydus 镜像元数据。当容器启动后,有文件数据请求时,Nydus 的进程再从镜像中心懒加载数据,以此做到按需加载实现容器启动的加速。

02
Nydusify Mount 子命令改进
2.1 问题描述
Nydusify 是 Nydus 生态提供的工具,可用于转换、挂载、校验 Nydus 镜像,其中 mount 子命令可以将 Nydus 镜像挂载到本地目录中,以提供给用户快速查看镜像内容。在之前的实现中,当用户挂载来自镜像中心的镜像时,需要在命令行参数中提供 --backend-type 与 --backend-config 选项以指定镜像的数据源,实际上可以从本地 Docker Config 中拿到镜像中心的相关信息,以避免提供这两个选项,对于默认的 Registry Backend 使用更加友好。
2.2 解决方案及实现
当命令行未指定 --backend-type 和 --backend-config 选项时,可以尝试从本地 Docker Config 中拿到镜像中心的相关信息,然后创建 Registry Backend 的相关配置信息,具体如函数NewRegistryBackendConfig 中所示,其核心流程为:
从默认的 Docker Config(通常是
~/.docker/config.json) 配置文件中加载认证信息,以获取与指定镜像仓库 Host 相关的认证配置。如果没有找到认证信息,将返回一个错误;
如果找到了认证信息,将用户名和密码拼接为
username:password格式的字符串,然后将其编码为 Base64 格式的认证字符串;
最后,将认证字符串设置到
backendConfig的Auth字段,并返回backendConfig对象以及可能的错误。
代码实现见 PR 链接 [6] ,效果如下所示:
Nydus 镜像本地直接挂载

进入挂载点查看镜像内容

03
Registry 鉴权兼容性改进
3.1 问题描述
当容器通过 Nydusd 按需从镜像仓库拉取数据时,需要先进行鉴权,各大厂商(例如 Docker Hub,Aliyun ACR,Harbor, Github GHCR)的镜像中心 Registry 鉴权实现并不完全一致,具体分为以下两种方式:
Basic:
将
auth(即username和password)编码后, 放入 http 请求头 中(也即:Authorization: base64(username:password)),直接请求,不需要先从 Token Server 获取 token;
OAuth:
客户端请求 Blob 资源,镜像中心 Server 返回 Token Server 地址与应该申请的权限,然后客户端携带 Auth 信息(username/password)向 Token Server 发起请求获取 token,最后客户端再携带 token 向镜像中心重新发起请求获取 Blob 资源。
问题:Nydusd 中 OAuth 鉴权方式中只实现了 GET 方式向 Token Server 获取 Token,然而各种镜像中心的实现有差异,当 GET 失败时,应该 Fallback 到 POST 的方式以进行兼容。

3.2 鉴权流程分析
本小节针对鉴权流程做简要分析,具体参考[7]、[8] 以及 containerd (https://containerd.io/)的实现。
3.2.1 起始
开始时, Nydusd 请求镜像中心获取 blob 数据,镜像中心则会在 http 请求头中返回具体的鉴权方式与信息。
如果镜像仓库已经配置成了 OAuth 的认证模式,那么它将返回一个
401的错误,头部的Www-Authenticate字段会被设置为值 Bearer,并且其返回信息中会包含完成认证所需要的信息;
如果镜像仓库未配置 OAuth 的认证模式,则返回头部的
Www-Authenticate字段会被设置为值Basic,此时采用 Basic auth 认证方式。

3.2.2 Basic auth 方式
在 Basic auth 认证方式中,客户端收到第一次请求的回复信息,并确认为 Basic auth 方式后,会将 username 和 passward 进行编码并封装放在请求头部,发起第二次请求,具体对应图示的 [3-4] 步骤。
/// # Basic authenticate workflow: /// /// step1 & step2 /// Request: GET https://my-registry.com/namespace/repo/blobs/sha256:<blob_id> /// Response: status: 401 Unauthorized /// header: www-authenticate: Basic /// /// step3 & step4 /// Request: GET https://my-registry.com/namespace/repo/blobs/sha256:<blob_id> /// header: authorization: Basic base64(<username:password>) /// Response: status: 200/301/307
3.2.3 OAuth auth 方式
在 OAuth auth 认证方式中,客户端收到第一次请求的回复信息,并确认为OAuth auth方式时,客户端会做如下操作,具体对应图示的 [3-6] 部分:
3)客户端会会从 header 中获取认证信息,向 Token Server 发起请求;
4)Token Server 会返回一个临时 token;
5)客户端将带有客户访问权限标识的 token 放入请求头,向镜像仓库再一次发起请求;
6)镜像仓库将尝试验证 token ,如果成功,则会返回客户端所请求的资源。
/// # Bearer token authenticate workflow:////// step1 & step2/// Request: GET https://my-registry.com/namespace/repo/blobs/sha256:<blob_id>/// Response: status: 401 Unauthorized/// header: www-authenticate: Bearer realm="https://auth.my-registry.com/token",service="my-registry.com",scope="repository:namespace/repo:pull,push"////// step3 & step4 /// Request: POST https://auth.my-registry.com/token/// body: "service=my-registry.com&scope=repository:namespace/repo:pull,push&grant_type=password&username=x&password=x&client_id=nydus-registry-client"/// Response: status: 200 Ok/// body: { "token": "<token>" }////// step5 & step6 /// Request: GET https://my-registry.com/namespace/repo/blobs/sha256:<blob_id>/// header: authorization: Bearer <token>/// Response: status: 200/301/307
3.3 解决方案及实现
根据上述认证流程的分析,我们需要实现 POST 与 GET 两种获取 token 的方式,并且实现自动 fallback。
let resp = if http_get { self.get_token_with_get(&auth, connection)?} else { match self.get_token_with_post(&auth, connection) { Ok(resp) => resp, Err(_) => { warn!("retry http GET method to get auth token"); let resp = self.get_token_with_get(&auth, connection)?; // Cache http method for next use. self.cached_auth_using_http_get.set(self.host.clone(), true); resp } }};3.4 优化
可以进一步针对访同一镜像仓库时采用 GET 还是 POST 方式进行缓存,这样当再次访问时,就可以直接从缓存中进行读取,而不必重试,可以有效减少访问次数,如下所示:
struct RegistryState { //... // Cache for the HTTP method when getting auth, it is "true" when using "GET" method. // Due to the different implementations of various image registries, auth requests // may use the GET or POST methods, we need to cache the method after the // fallback, so it can be reused next time and reduce an unnecessary request. cached_auth_using_http_get: HashCache<bool>, // ...}#[derive(Default)]struct HashCache<T>(RwLock<HashMap<String, T>>);impl<T> HashCache<T> { fn new() -> Self { HashCache(RwLock::new(HashMap::new())) } fn get(&self, key: &str) -> Option<T> where T: Clone, { let cached_guard = self.0.read().unwrap(); cached_guard.get(key).cloned() } fn set(&self, key: String, value: T) { let mut cached_guard = self.0.write().unwrap(); cached_guard.insert(key, value); } fn remove(&self, key: &str) { let mut cached_guard = self.0.write().unwrap(); cached_guard.remove(key); }}04
项目总结
4.1 为何参加活动
一开始参加开源课题的原因是被奖金吸引,但参加后发现项目会使用更前沿的技术,能拓展视野了解技术趋势,代码质量要求也更高。可以改善我的编程习惯,看到自己的代码能够被合并,也体会到了开源协作的乐趣。
4.2 遇到的困难与挑战
万事开头难,庞大的项目代码量较难一开始就找到切入点。通过阅读文档与导师沟通,搭建环境与测试,解决了很多问题,这也让我对容器领域有了进一步的了解。
4.3 收获与展望
感谢社区同学和项目导师的帮助,他们帮忙在问题定位,实现参考,文档格式与代码 Review 上给了很多指导,我希望在后续的学习和工作里,可以继续参与 Nydus 开发工作,让自己更加有社区参与感!
05
致谢
非常感谢本题目 Mentor 严松导师和周会组织姚胤楠以及袁卓同学在本次项目中的指导和帮助,尤其是导师在项目中细致入微的解答以及对待代码的严谨态度,还有亲切近人的鼓励态度,让我在这个过程中不断思考自己的不足之处,使得大受裨益。
在后续的学习和工作中,我希望能持续参与到 Nydus 相关的开发工作中,继续为社区贡献 issue 和代码。
参考
[1].How to Build a Docker Engine-like Custom Container without Any Software
https://www.freecodecamp.org/news/build-your-on-custom-container-without-docker/
[2].Nydus —— 下一代容器镜像的探索实践
https://developer.aliyun.com/article/971522
[3].Nydus | 容器镜像基础
https://developer.aliyun.com/article/1071132?spm=5176.26934562.main.3.1c5b43c9uZVdTg#slide-0
[4].GitHub - containerd/nydus-snapshotter: A containerd snapshotter with data deduplication and lazy loading in P2P fashion
https://github.com/containerd/nydus-snapshotter
[5].containerd/docs/PLUGINS.md at main · containerd/containerd
https://github.com/containerd/containerd/blob/main/docs/PLUGINS.md
[6].Nydusify: fix some bug about the subcommand mount of nydusify by lihuahua123 · Pull Request #1328 · dragonflyoss/nydus
https://github.com/dragonflyoss/nydus/pull/1328
[7].containerd/docs/PLUGINS.md at main · containerd/containerd
https://github.com/containerd/containerd/blob/main/docs/PLUGINS.md
[8].为 Docker 的镜像仓库实现 OAuth 认证服务
https://zhuanlan.zhihu.com/p/58370332

本文介绍了作者在开源之夏2023中参与Nydus项目的工作,涉及OCI容器技术、Nydus镜像加速框架、Registry鉴权兼容性改进等内容,重点讲述了如何改进Nydusify的Mount子命令和OAuth鉴权流程。
336

被折叠的 条评论
为什么被折叠?



