docker registry GC 原理分析

好久没更新博客了,在家摸鱼赶紧来水一篇😂

registry GC 原理🤔

在咱上个月写的《深入浅出容器镜像的一生》中简单提到了容器镜像的一些知识,也简单介绍了镜像在 registry 中存储的目录结构。今天还是从文件系统层面分析一下 registry GC 的原理,比从源码来分析更直观一些。

部署 registry 容器

首先我们需要在本地部署一个 registry 容器,同时为了操作的方便还需要使用到 skopeo 这个工具来替代 docker 命令行客户端进行 copy 镜像和 delete 镜像。关于 skopeo 这个工具的安装和使用可以参考咱之前写过的《镜像搬运工 skopeo 》

自签 SSL 证书

这一步为了方便在使用 skopeo 的时候不用加一堆额外的参数😂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/sh
set -e
set -o nounset
cat >ca.conf <<EOF
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
prompt = no
encrypt_key = no
x509_extensions = v3_ca
[ req_distinguished_name ]
CN = localhost
[ CA_default ]
copy_extensions = copy
[ alternate_names ]
DNS.2=localhost
[ v3_ca ]
[email protected]_names
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical,CA:true
keyUsage=keyCertSign,cRLSign,digitalSignature,keyEncipherment,nonRepudiation
EOF
mkdir -p certs
openssl req -days 365 -x509 -config ca.conf \
-new -keyout certs/domain.key -out certs/domain.crt
  • 信任证书,根据不同的发行版选择相应的路径和命令行即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
# CentOS
update-ca-trust force-enable
cp certs/domain.crt /etc/pki/ca-trust/source/anchors/localhost.crt
update-ca-trust

# Ubuntu
cp certs/domain.crt /usr/local/share/ca-certificates/localhost.crt
$ update-ca-certificates

# Debian
cp certs/domain.crt /usr/share/ca-certificates/localhost.crt
echo localhost.crt >> /etc/ca-certificates.conf
update-ca-certificates

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

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

1
htpasswd -cB -b auth.htpasswd root 123456

启动 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 这种错误(>﹏<)。
1
2
3
4
5
6
7
8
9
10
docker run -d -p 127.0.0.1:443:5000 --name registry \
-v /var/lib/registry:/var/lib/registry \
-v `pwd`/certs:/certs \
-v $(pwd)/auth.htpasswd:/etc/docker/registry/auth.htpasswd \
-e REGISTRY_AUTH="{htpasswd: {realm: localhost, path: /etc/docker/registry/auth.htpasswd}}" \
-e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
-e REGISTRY_STORAGE_DELETE_ENABLED=true \
registry

docker login

这一步是为了在 ~/.docker/.config.json ,中添加上 auth 认证,后面使用 skopeo 的时候会用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
╭─[email protected] ~/registry
╰─# docker login localhost -u root -p 123456
]WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
╭─[email protected] ~/registry
╰─# cat ~/.docker/config.json
{
"auths": {
"https://registry.k8s.li/v2": {
"auth": "VlJFpmQE43Sw=="
},
"localhost": {
"auth": "cm9vdDoxMjM0NTY="
}
},
"HttpHeaders": {
"User-Agent": "Docker-Client/19.03.5 (linux)"
},
"experimental": "enabled"
}#

COPY 镜像到 registry

1
2
3
4
5
6
7
╭─[email protected] ~/registry
╰─# skopeo copy docker://alpine:3.10 docker://localhost/library/alpine:3.10
Getting image source signatures
Copying blob 21c83c524219 done
Copying config be4e4bea2c done
Writing manifest to image destination
Storing signatures

registry 存储目录长什么样🤔

img

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

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 文件的删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
╭─[email protected] ~/registry
╰─# cd /var/lib/registry/docker/registry/v2
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# tree
.
├── blobs
│   └── sha256
│   ├── 21
│   │   └── 21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
│   │   └── data
│   ├── a1
│   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   └── data
│   └── be
│   └── be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
│   └── data
└── repositories
└── library
└── alpine
├── _layers
│   └── sha256
│   ├── 21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
│   │   └── link
│   └── be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
│   └── link
├── _manifests
│   ├── revisions
│   │   └── sha256
│   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   └── link
│   └── tags
│   └── 3.10
│   ├── current
│   │   └── link
│   └── index
│   └── sha256
│   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   └── link
└── _uploads

26 directories, 8 files
  • blobs 存储目录,存放了镜像的三个必须文件,layermanifestconfig。通过文件大小我们可以大致地推算出最大的 2.7M 是镜像的 layer 。
1
2
3
4
5
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# find . -name "data" -exec ls -sh {} \;
2.7M ./blobs/sha256/21/21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d/data
4.0K ./blobs/sha256/a1/a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590/data
4.0K ./blobs/sha256/be/be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/data
  • image layer 文件,是 gzip 格式的 tar 包,是镜像层真实内容的 tar.gzip 格式存储形式。
1
./blobs/sha256/21/21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d/data: gzip compressed data
  • image manifest 文件,json 文件格式的,存放该镜像 layerimage config 文件的索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# cat ./blobs/sha256/a1/a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590/data
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1509,
"digest": "sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2795580,
"digest": "sha256:21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d"
}
]
}#
  • 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 目录下依次创建相应的目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# cat ./blobs/sha256/be/be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/data | jq "."
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh"
],
"ArgsEscaped": true,
"Image": "sha256:d928e20e1fbe5142bb5cdf594862271673133c5354950d6a8f74afed24df4c23",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"container": "37e3972c75360676982c8f6591b66a9097719e5ad4cecd5fa63ad4f06472825f",
"container_config": {
"Hostname": "37e3972c7536",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"/bin/sh\"]"
],
"ArgsEscaped": true,
"Image": "sha256:d928e20e1fbe5142bb5cdf594862271673133c5354950d6a8f74afed24df4c23",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"created": "2020-04-24T01:05:21.571691552Z",
"docker_version": "18.09.7",
"history": [
{
"created": "2020-04-24T01:05:21.178437685Z",
"created_by": "/bin/sh -c #(nop) ADD file:66a440394c2442570f1f060e25c86613cb2d88a8af0c71c5a4154b3570e9a805 in / "
},
{
"created": "2020-04-24T01:05:21.571691552Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3"
]
}
}
  • 我们再往 registry 中 COPY 一个镜像,方便后面的分析过程。
1
skopeo copy docker://debian:buster-slim docker://localhost/library/debian:buster-slim
  • 这是 registry 中就只有 alpine:3.10debian:buster-slim这两个基础镜像,此时的 registry 存储目录的结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# tree
.
├── blobs
│   └── sha256
│   ├── 21
│   │   └── 21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
│   │   └── data
│   ├── 43
│   │   └── 43e3995ee54ac008271bfcf2d8ac7278c33f4c5e83d2f02bfcddd350034e3357
│   │   └── data
│   ├── 7c
│   │   └── 7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
│   │   └── data
│   ├── 85
│   │   └── 8559a31e96f442f2c7b6da49d6c84705f98a39d8be10b3f5f14821d0ee8417df
│   │   └── data
│   ├── a1
│   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   └── data
│   └── be
│   └── be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
│   └── data
└── repositories
└── library
├── alpine
│   ├── _layers
│   │   └── sha256
│   │   ├── 21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
│   │   │   └── link
│   │   └── be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
│   │   └── link
│   ├── _manifests
│   │   ├── revisions
│   │   │   └── sha256
│   │   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   │   └── link
│   │   └── tags
│   │   └── 3.10
│   │   ├── current
│   │   │   └── link
│   │   └── index
│   │   └── sha256
│   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   └── link
│   └── _uploads
└── debian
├── _layers
│   └── sha256
│   ├── 43e3995ee54ac008271bfcf2d8ac7278c33f4c5e83d2f02bfcddd350034e3357
│   │   └── link
│   └── 8559a31e96f442f2c7b6da49d6c84705f98a39d8be10b3f5f14821d0ee8417df
│   └── link
├── _manifests
│   ├── revisions
│   │   └── sha256
│   │   └── 7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
│   │   └── link
│   └── tags
│   └── buster-slim
│   ├── current
│   │   └── link
│   └── index
│   └── sha256
│   └── 7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
│   └── link
└── _uploads

48 directories, 16 files

DELETE 镜像

  • 通过 skopeo delete 删除镜像,注意,通过 registry 的 API 删除镜像每次只能删除一个 tag 的镜像。
1
2
3
4
5
6
7
8
9
10
11
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# skopeo delete docker://localhost/library/alpine:3.10 --debug
DEBU[0000] Returning credentials from /run/containers/0/auth.json
DEBU[0000] Using registries.d directory /etc/containers/registries.d for sigstore configuration
DEBU[0000] No signature storage configuration found for localhost/library/alpine:3.10
DEBU[0000] Looking for TLS certificates and private keys in /etc/docker/certs.d/localhost
DEBU[0000] Loading registries configuration "/etc/containers/registries.conf"
DEBU[0000] GET https://localhost/v2/
DEBU[0000] Ping https://localhost/v2/ status 401
DEBU[0000] GET https://localhost/v2/library/alpine/manifests/3.10
DEBU[0000] DELETE https://localhost/v2/library/alpine/manifests/sha256:a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
  • 再看一下删除后的 registry 存储目录下的 alpine 目录里都少了哪些东东?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# tree
.
├── blobs
│   └── sha256
│   ├── 21
│   │   └── 21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
│   │   └── data
│   ├── 43
│   │   └── 43e3995ee54ac008271bfcf2d8ac7278c33f4c5e83d2f02bfcddd350034e3357
│   │   └── data
│   ├── 7c
│   │   └── 7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
│   │   └── data
│   ├── 85
│   │   └── 8559a31e96f442f2c7b6da49d6c84705f98a39d8be10b3f5f14821d0ee8417df
│   │   └── data
│   ├── a1
│   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   └── data
│   └── be
│   └── be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
│   └── data
└── repositories
└── library
├── alpine
│   ├── _layers
│   │   └── sha256
│   │   ├── 21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
│   │   │   └── link
│   │   └── be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
│   │   └── link
│   ├── _manifests
│   │   ├── revisions
│   │   │   └── sha256
│   │   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   └── tags
│   └── _uploads

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

1
DEBU[0000] DELETE https://localhost/v2/library/alpine/manifests/sha256:a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590

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

registry GC 原理

上面巴拉巴拉扯了一通也许你现在一头雾水,这和今天的主题 registry GC 原理毛关系?😂,其实想要从文件系统层面来理解 registry GC ,上面的知识是必备的(^____^)。

GC 是弄啥咧?🤔

GC 嘛,就是垃圾回收的意思,从 docker 官方文档 Garbage collection 偷来的 example 😂来解释一下吧。

  • 假如镜像 A 和镜像 B ,他俩分别引用了layer a,b和 a,c。
1
2
3
A -----> a <----- B
\--> b |
c <--/
  • 通过 registry API 删除镜像 B 之后,layer c 并没有删掉,只是删掉了对它的引用,所以 c 是多余的。
1
2
3
A -----> a     B
\--> b
c
  • GC 之后,layer c 就被删掉了,现在就没有无用的 layer 了。
1
2
A -----> a
\--> b

GC 的过程

翻一下 registry GC 的源码 garbagecollect.go,可以看到 GC 的主要分两个阶段,marking 和 sweep。

marking

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// mark
markSet := make(map[digest.Digest]struct{})
manifestArr := make([]ManifestDel, 0)
err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
emit(repoName)

var err error
named, err := reference.WithName(repoName)
if err != nil {
return fmt.Errorf("failed to parse repo name %s: %v", repoName, err)
}
repository, err := registry.Repository(ctx, named)
if err != nil {
return fmt.Errorf("failed to construct repository: %v", err)
}

manifestService, err := repository.Manifests(ctx)
if err != nil {
return fmt.Errorf("failed to construct manifest service: %v", err)
}

manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator)
if !ok {
return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator")
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
set -x
v2=${v2:="/var/lib/registry/docker/registry/v2"}
cd ${v2}
all_blobs=/tmp/all_blobs.list
echo "" > ${all_blobs}
# marking all the blob by all images manifest
for tag in $(find repositories -name "link" | grep current)
do
link=$(cat ${tag} | cut -c8-71)
mfs=blobs/sha256/${link:0:2}/${link}/data
echo ${link} >> ${all_blobs}
grep sha256 ${mfs} |cut -d "\"" -f4 | cut -c8-71 >> ${all_blobs}
done

sweep

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sweep
vacuum := NewVacuum(ctx, storageDriver)
if !opts.DryRun {
for _, obj := range manifestArr {
err = vacuum.RemoveManifest(obj.Name, obj.Digest, obj.Tags)
if err != nil {
return fmt.Errorf("failed to delete manifest %s: %v", obj.Digest, err)
}
}
}
blobService := registry.Blobs()
deleteSet := make(map[digest.Digest]struct{})
err = blobService.Enumerate(ctx, func(dgst digest.Digest) error {
// check if digest is in markSet. If not, delete it!
if _, ok := markSet[dgst]; !ok {
deleteSet[dgst] = struct{}{}
}
return nil
})

GC 都干了啥?

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

marking

1
2
3
4
5
6
7
8
9
10
╭─[email protected]02 /var/lib/registry/docker/registry/v2
╰─# docker exec -it registry sh
/ # registry garbage-collect -m --delete-untagged=true /etc/docker/registry/config.yml
library/alpine
library/debian
library/debian: marking manifest sha256:7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
library/debian: marking blob sha256:43e3995ee54ac008271bfcf2d8ac7278c33f4c5e83d2f02bfcddd350034e3357
library/debian: marking blob sha256:8559a31e96f442f2c7b6da49d6c84705f98a39d8be10b3f5f14821d0ee8417df

3 blobs marked, 3 blobs and 0 manifests eligible for deletion

sweep

1
2
3
4
5
6
blob eligible for deletion: sha256:a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/a1/a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590 go.version=go1.11.2 instance.id=3ad15352-7cb7-46ca-a5ae-e5e16c6485a5 service=registry
blob eligible for deletion: sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/be/be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c go.version=go1.11.2 instance.id=3ad15352-7cb7-46ca-a5ae-e5e16c6485a5 service=registry
blob eligible for deletion: sha256:21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/21/21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d go.version=go1.11.2 instance.id=3ad15352-7cb7-46ca-a5ae-e5e16c6485a5 service=registry

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
╭─[email protected] /var/lib/registry/docker/registry/v2
╰─# tree
.
├── blobs
│   └── sha256
│   ├── 21
│   ├── 43
│   │   └── 43e3995ee54ac008271bfcf2d8ac7278c33f4c5e83d2f02bfcddd350034e3357
│   │   └── data
│   ├── 7c
│   │   └── 7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
│   │   └── data
│   ├── 85
│   │   └── 8559a31e96f442f2c7b6da49d6c84705f98a39d8be10b3f5f14821d0ee8417df
│   │   └── data
│   ├── a1
│   └── be
└── repositories
└── library
├── alpine
│   ├── _layers
│   │   └── sha256
│   │   ├── 21c83c5242199776c232920ddb58cfa2a46b17e42ed831ca9001c8dbc532d22d
│   │   │   └── link
│   │   └── be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c
│   │   └── link
│   ├── _manifests
│   │   ├── revisions
│   │   │   └── sha256
│   │   │   └── a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
│   │   └── tags
│   └── _uploads
└── debian
├── _layers
│   └── sha256
│   ├── 43e3995ee54ac008271bfcf2d8ac7278c33f4c5e83d2f02bfcddd350034e3357
│   │   └── link
│   └── 8559a31e96f442f2c7b6da49d6c84705f98a39d8be10b3f5f14821d0ee8417df
│   └── link
├── _manifests
│   ├── revisions
│   │   └── sha256
│   │   └── 7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
│   │   └── link
│   └── tags
│   └── buster-slim
│   ├── current
│   │   └── link
│   └── index
│   └── sha256
│   └── 7c459309b9a5ec1683ef3b137f39ce5888f5ad0384e488ad73c94e0243bc77d4
│   └── link
└── _uploads

40 directories, 10 files

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

总结

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

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

REGISTRY

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

GC 之后的 registry 存储目录结构

shell 大法好!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
set -x
v2=$1
v2=${v2:="/var/lib/registry/docker/registry/v2"}
cd ${v2}
all_blobs=/tmp/all_blobs.list
: > ${all_blobs}
# marking all the blob by all images manifest
for tag in $(find repositories -name "link" | grep current)
do
link=$(cat ${tag} | cut -c8-71)
mfs=blobs/sha256/${link:0:2}/${link}/data
echo ${link} >> ${all_blobs}
grep sha256 ${mfs} |cut -d "\"" -f4 | cut -c8-71 >> ${all_blobs}
done
# delete blob if the blob doesn't exist in all_blobs.list
for blob in $(find blobs -name "data" | cut -d "/" -f4)
do
grep ${blob} ${all_blobs}
if [[ $? != 0 ]]; then
rm -rf blobs/sha256/${blob:0:2}/${blob}
fi
done
  1. 遍历所有镜像的 tag 下最新的 link 文件指向的 manifest

  2. 根据 manifest 文件 grep 出 sha256 值的 image config 和 layer 文件,保存到 all_blobs.list 文件中。

  3. 使用 findfor 循环遍历所有 blobs 下的的 data 文件,判断它是否在 all_blobs.list 中,不再的话直接 rm -rf 干掉它!

  4. 最后重启一下 registry 容器。

就是这么简单粗暴!哈哈,rm -rf 用起来真爽(手动滑稽 。如果还想把这个脚本再优化一下的话,可以将 所有的 blob 的 sha256 值截取前 12 位保存在一个变量中。通过 =~ 来判断包含关系来替代 grep。

踩坑!

The operation is unsupported.(405 Method Not Allowed)

1
2
3
4
5
6
7
8
9
10
11
12
13
╭─[email protected] ~/registry
╰─# skopeo delete docker://localhost/library/alpine:3.10 --debug
DEBU[0000] Returning credentials from /run/containers/0/auth.json
DEBU[0000] Using registries.d directory /etc/containers/registries.d for sigstore configuration
DEBU[0000] No signature storage configuration found for localhost/library/alpine:3.10
DEBU[0000] Looking for TLS certificates and private keys in /etc/docker/certs.d/localhost
DEBU[0000] Loading registries configuration "/etc/containers/registries.conf"
DEBU[0000] GET https://localhost/v2/
DEBU[0000] Ping https://localhost/v2/ status 401
DEBU[0000] GET https://localhost/v2/library/alpine/manifests/3.10
DEBU[0000] DELETE https://localhost/v2/library/alpine/manifests/sha256:a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590
FATA[0000] Failed to delete /v2/library/alpine/manifests/sha256:a143f3ba578f79e2c7b3022c488e6e12a35836cd4a6eb9e363d7f3a07d848590: {"errors":[{"code":"UNSUPPORTED","message":"The operation is unsupported."}]}
(405 Method Not Allowed)

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

1
2
3
storage:
delete:
enabled: false

从上面我们可以得知,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 文件。根据 layer link 的值去 blobs 目录下看看该文件是否存在,不存在的话就 rm -rf 掉,存在的话就留着。这样就能清理干净啦😁。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
cd /var/lib/registry/docker/registry/v2
for link in $(find repositories -name "link" | grep -E "_layers")
do
link_sha256=$(echo ${link} | awk -F "/" '{print $6}')
link_short=$(echo ${link} | awk -F "/" '{print $6}' | cut -c1-2)
data_file=blobs/sha256/${link_short}/${link_sha256}
dir_link=$(echo ${link} | sed s'/link//g')
if [[ ! -d "${data_file}" ]]; then
rm -rf ${dir_link}
fi
done

GC 后要重启!

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

GC 不是事务性操作

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