最近遇到一个有趣的状况,某镜像仓库占用了大量的磁盘空间。通常要解决这种问题,给 Registry 发删除指令,并进行 GC 就可以了。然而很多时候,所有镜像都正常,在删除多个 Tag 甚至是 Repository 之后,问题仍然没能缓解,原理也很容易理解——删除的镜像虽然大,可能只是复用了一些比较大的层,删除镜像并不会真正的发出,所以还是需要对镜像库的存储进行更多的了解,进行进一步的统计,在层一级对镜像仓库进行分析,才能获取更有效的途径。
首先发现了一个有意思的项目:DockerRegistryExporter,这个项目是一个 Python 编写的 Prometheus Exporter,其中包含四个 Gauge:
-repository_tags_total
:按镜像计算的 Tag 数量。
-repository_revisions_total
:按镜像计算的版本数量。
-repository_tag_layers_total
:以镜像和 Tag 计算的 Layer 数量。
-repository_tag_size_bytes
:以镜像和 Tag 计算的文件尺寸。
该镜像使用挂卷的方式,直接对镜像库文件系统进行扫描,例如:
containers:
- image: registry:2
name: registry
ports:
- containerPort: 5000
name: http
protocol: TCP
readinessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 1
timeoutSeconds: 1
livenessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 1
timeoutSeconds: 1
volumeMounts:
- name: storage
mountPath: /var/lib/registry
- image: skyuk/docker-registry-exporter:v1.0.0
name: registry-exporter
args:
- /var/lib/registry/docker/registry/v2
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- name: storage
mountPath: /var/lib/registry
volumes:
- name: storage
persistentVolumeClaim:
claimName: registry
通过Sidecar的部署方式和Registry容器共享文件系统,可以定时输出监控指标,例如:
$ curl http://registry:8080
# HELP repository_tag_size_bytes Size of eachtag
# TYPE repository_tag_size_bytes gauge
repository_tag_size_bytes{repository="org/image1", tag="0.3.0"} 162749959.0
repository_tag_size_bytes{repository="org/image2", tag="1009140546"} 226608092.0
...
然而这并不能满足我的要求,关于引用的数据并没有体现,另外前面也提到,我们需要比较精确地获得镜像版本、Tag 和 Layer 之间的引用关系以及各自的尺寸,用 PromQL 有点别扭。
这并不是一个很常见的需求,只能是一个清理之前的准备动作,目前看来我需要找到的就是引用数量少、但是体量比较大的 Layer,但是谁知道以后会需要什么新的标准呢?干脆把这些东西写入到数据库里算了,把这些东西写入数据库之后,还掌握 SQL 这样传统才艺的程序员就可以随便搞一搞其它条件了。
镜像库根目录中有两个子目录:blobs
中保存了所有的 Layer,而 repositories
中则是以镜像为单位保存的元数据。
首先看看镜像的数据
$ tree/org/repo/gameserver
.
├── revisions
│ └── sha256
│ └── ecfb0206e8b...
│ └── link
└── tags
└── latest
├── current
│ └── link
└── index
└── sha256
└── ecfb020...
└── link
每个镜像的 Manifests 有两个目录,分别承载的是版本和 Tag,正常来说 Tag 和版本是一致的,但实际上在一些特别情况下,这两个数量可能是不一致的,就会导致只用 Tag 已经无法拉取该镜像,属于一种半孤立状态,应该说是需要清除的。
两个目录中的link
文件中包含的是一个哈希码,可以使用这个哈希码在_layers
中查找到该镜像的版本/tag 对应的清单层,使用这个字符串可以在根_layer
中查到对应的目录,目录下面的data
文件中就是每个层的具体数据,对于清单层,其中会是一个json
字符串:
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 2694,
"digest": "sha256:7929bcd70e47d3726d55a870b2ca11c25792758f3ba8b4ff136811f0809af636"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2546278,
"digest": "sha256:3db1cceb1cccb362634e914bfe76d329c64d148262a9e139a046337d82e1aeec"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32,
"digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1"
}
]}
这里看到清单中包含两个主节点,config
和 layer
,至此,一个镜像是由三种不同的层构成的:清单、Config 和 Layer。我们关注的主要是 Layer,其中的 data
文件包含的就是各层的具体内容,清单和 Config 中都是文本,Layer 通常都是二进制的,也是我们要关注的主要内容。
接下来的问题就顺理成章了,把 Repository、Tag、Revision 以及 Layer 的关系建立起来,随便用个 SQL 语句,就能够按照具体需求对“引用少、尺寸大”的 Layer 进行过滤了。
https://github.com/sky-uk/docker-registry-exporter