Docker registry GC 原理分析

作者: 木子才云

编辑:Bach才云

校对:bot才云

此前,《Docker 容器镜像是怎么炼成的》一文简单提到了容器镜像的一些知识,并介绍了镜像在 registry 中存储的目录结构。本文从文件系统层面分析了 registry GC 的原理,相比源码分析更加直观。

部署 registry 容器

首先我们在本地部署一个 registry 容器,再使用 skopeo 工具替代 Docker 命令行客户端进行 copy 镜像和 delete 镜像。

自签 SSL 证书

这样我们在使用 skopeo 时不用加额外参数。

信任证书,根据不同的发行版选择相应的路径和命令行即可。

创建密码 auth 认证 auth.htpasswd 文件

由于 push 镜像和 delete 镜像是通过 HTTP 请求 registry 的 API 完成的,每个请求都需要一个 token 才能完成操作,这个 token 则需要使用 AUTH 文件进行鉴权。我们使用 htpasswd 来生成一个明文的用户、密码即可。

启动 registry 容器,docker run!

  • -v /var/lib/registry:/var/lib/registry ,将本地的存储目录挂载到容器内的 registry 存储目录下。
  • -v pwd/certs:/certs,将生成的 SSL 证书挂载到容器内。
  • -e REGISTRY_STORAGE_DELETE_ENABLED=true,添加该参数才能进行 DELETE 镜像操作,不然的话会提示 Error in deleting repository in a private registry V2 #1573 这种错误。

docker login

这一步是为了在 ~/.docker/.config.json 中添加 auth 认证,方便后面使用 skopeo。

copy 镜像到 registry

registry 存储目录

这是registry 容器内的 /var/lib/registry/docker/registry/v2 存储目录。结合上图,通过 tree 目录我们可以清晰地看到:registry 存储目录下只有两种文件名的文件,一个是 data 文件,一个是 link 文件。其中 link 文件是普通的文本文件,存放在 repositories 目录下,其内容是指向 data 文件的 sha256 digest 值。

data 文件存放在 blobs 目录下,文件又分为了三种文件,一个是镜像每一层的 layer 文件和镜像的 config 文件,以及镜像的 manifest 文件。

repositories 目录下每个镜像的 _layers/sha256 目录下文件夹名是镜像的 layer 和 config 文件的 digest ,该目录下的 link 文件就是指向对应 blobs 目录下的 data 文件。当我们 pull 一个镜像的 layer 时,是通过 link 文件找到 layer 在 registry 中实际的存储位置的。

_manifests 文件夹下的 tags 和 revisions 目录下的 link 文件则指向该镜像的 manifest 文件,保存在所有历史镜像 tag 的 manifest 文件 的 link。当删除一个镜像时,只会删除该镜像最新的 tag 的 link 文件。

tags 目录下的文件夹名例如 3.10,就是该镜像的 tag,在其子目录下的 current/link 文件则记录了当前 tag 指向的 manifest 文件位置。例如 alpine:latest 镜像,每次 push 新的 latest 镜像时,current/link 都会更新并指向最新镜像的 manifest 文件。

我们观察删除镜像时文件的变化,就可以得知通过 registry API 进行 delete 操作可以转换成文件系统层面上对 link 文件的删除操作。

blobs 存储目录,存放了镜像的三个必须文件,layermanifestconfig。通过文件大小我们可以大致推算出最大的 2.7M 是镜像的 layer 。

image layer 文件,gzip 格式的 tar 包,是镜像层真实内容的 tar.gzip 格式存储形式。

image manifest 文件,json 文件格式,用于存放该镜像 layerimage config 文件的索引。

image config 文件,json 格式,是构建时生成的。根据 Dockerfile 和宿主机的一些信息,以及一些构建过程中的容器,可以生成 digest 唯一的 image config 文件。仔细观察 image config 文件,我们可以发现无论是 manifest 还是 config 文件里面的内容都没有镜像的名称和 tag。其实,镜像就好比一个文件,文件的内容和文件名毫无关系。在 registry 中,是通过路径名的方式来对一个镜像进行命名的。当我们往 registry 中 push 一个镜像时,以 localhost/library/alpine:3.10 为例,localhost 就是该 registry 的域名或者 URL ;library 就是 project;alpine:3.10 就是镜像名和镜像的 tag。registry 会根据 localhost/library/alpine:3.10repositories 目录下依次创建相应的目录。

我们再往 registry 中 copy 一个镜像,方便后面的分析过程。

这个 registry 中只有 alpine:3.10debian:buster-slim 这两个基础镜像,此时的 registry 存储目录的结构如下:

DELETE 镜像

这里通过 skopeo delete 删除镜像,注意,通过 registry 的 API 删除镜像每次只能删除一个 tag 镜像。

观察删除后的 registry 存储目录下,alpine 目录里都少了哪些东西?

我们可以看到,通过 skopeo delete 一个镜像的时候,只对 _manifests 下的 link 文件进行了操作,删除的都是该 tag 镜像 manifest 文件夹下的 link 文件,实际上 manifest 文件并没有从 blobs 目录下删除,只是删除了该镜像的 manifest 文件的引用。删除一个镜像后,tags 目录下的 tag 名目录就被删除了,_manifests/revisions 目录下的 link 文件也被删除了。实际上两者删除的是同一个内容,即对该镜像 manifest 文件的 link 文件。

从上面文件的变化可以得出,通过 registry API 来 delete 一个镜像实质上是删除 repositories 元数据文件夹下的 tag 名文件夹和该 tag 的 revisions 下的 link 文件。

K8sMeetup

registry GC 原理

上面讲了文件系统层面后,我们再回到本文主题 registry GC 原理。

GC 是什么?

GC(Garbage collection)指垃圾回收。此前,《Kubernetess 中的垃圾回收》一文对 GC 的概念、策略以及实现方法有过简单的介绍。现在,我们通过 Docker 官方文档 Garbage collection 的例子对其进一步了解。

假如有镜像 A 和镜像 B,分别引用了layer a、b 和 a、c。

通过 registry API 删除镜像 B 之后,layer c 并没有删掉,只是删掉了对它的引用,所以 c 是多余的。

GC 之后,layer c 就被删掉了,这样就没有无用的 layer 了。

GC 的过程

通过 registry GC 的源码 garbagecollect.go,我们可以看到 GC 主要分两个阶段,marking 和 sweep。

marking

marking 阶段是扫描所有的 manifest 文件。根据上文提到的 link 文件,扫描所有镜像 tags 目录下的 link 文件就可以得到这些镜像的 manifest,在 manifest 中保存在该镜像所有的 layer 和 config 文件的 digest 值,把这些值标记为不能清除。

这一阶段可以用 shell 脚本来实现:使用 shell 遍历 manifest,然后再 grep 出所有的 sha256 值就能得到这个镜像所有的 blobs 目录下的 data 文件。

sweep

第二阶段是删除操作,marking 后,没有标记 blob(layer 和 config 文件)就会被清除掉。

GC 做了什么?

接下来我们进行实际的 GC 操作,进入到 registry 容器中,使用 registry garbage-collect 这个子命令进行操作。

marking

sweep

GC 之后的 registry 存储目录是什么样子?

根据 GC 后的 registry 存储目录我们可以看到,原本 blobs 目录下有 6 个 data 文件,现在已经变成了 3 个,alpine:3.10 这个镜像相关的 layer、config、manifest 这三个文件都已经被 GC 掉,但是在 repositories 目录下,该镜像的 _layers 下的 link 文件依旧存在。

总结

总结以上,用下面这三张图片就能直观地展示这些过程。

delete 镜像之前的 registry 存储目录结构

delete 镜像之后的 registry 存储目录结构

GC 之后的 registry 存储目录结构

shell 实现

根据上面的 GC 原理和过程,实际上我们可以使用不到 25 行的 shell 脚本来实现一个简单的 GC。

  • 遍历所有镜像的 tag 下最新的 link 文件指向的 manifest。
  • 根据 manifest 文件 grep 出 sha256 值的 image config 和 layer 文件,保存到 all_blobs.list 文件中。
  • 使用 findfor 循环遍历所有 blobs 下的的 data 文件,判断它是否在 all_blobs.list 中,不在的话直接 rm -rf 删除。
  • 最后重启 registry 容器。

这个脚本可以继续优化,将所有的 blob 的 sha256 值截取前 12 位保存在一个变量中,再通过 =~ 来判断包含关系来替代 grep。

K8sMeetup

避免踩坑

The operation is unsupported.(405 Method Not Allowed)

在 registry 容器启动的时候添加变量开启 REGISTRY_STORAGE_DELETE_ENABLED=true 即可,或者修改容器内的配置文件 /etc/docker/registry/config.yml,在 storage: 下添加下面的参数。

GC 不彻底,残留 link 文件

从上面我们可以得知,registry 无论是删除一个镜像还是进行 GC 操作,都不会删除 repositories 目录下的 _layers/sha256/digest/link 文件,在进行 GC 之后,一些镜像 layer 和 config 文件已经在 blobs 存储目录下删除了,但指向它的 layers/link 文件依旧保存在 repositories 目录下。GitHub 上有 PR Remove the layer’s link by garbage-collect #2288 可以清理这些无用的 layer link 文件的。

已经被 GC 的 blob layer link 文件可以使用下面这个脚本删除。根据 layer link 的值 blobs 目录下查看该文件是否存在,不存在的话就 rm -rf 删除,存在的话就留着。这样就能清理干净。

GC 后要重启!

GC 之后一定要重启,因为 registry 容器缓存了镜像 layer 的信息,在删除掉一个镜像 A ,后边 GC 掉该镜像的 layer 之后,如果不重启 registry 容器,当重新 push 镜像 A 的时候就会提示镜像 layer 已经存在,不会重新上传 layer ,但实际上已经被 GC 掉了,最终会导致镜像 A 不完整,无法 pull 到该镜像。

GC 不是事务性操作

GC 的时候最好暂停 push 镜像,以免把正在上传的镜像 layer 给 GC 掉。

原文地址:https://mp.weixin.qq.com/s/D8...

你可能感兴趣的:(docker,gc,垃圾回收机制,kubernetes)