清理鏡像庫(kù)空間的一個(gè)思路
最近遇到一個(gè)有趣的狀況,某鏡像倉(cāng)庫(kù)占用了大量的磁盤空間。通常要解決這種問題,給 Registry 發(fā)刪除指令,并進(jìn)行 GC 就可以了。然而很多時(shí)候,所有鏡像都正常,在刪除多個(gè) Tag 甚至是 Repository 之后,問題仍然沒能緩解,原理也很容易理解——?jiǎng)h除的鏡像雖然大,可能只是復(fù)用了一些比較大的層,刪除鏡像并不會(huì)真正的發(fā)出,所以還是需要對(duì)鏡像庫(kù)的存儲(chǔ)進(jìn)行更多的了解,進(jìn)行進(jìn)一步的統(tǒng)計(jì),在層一級(jí)對(duì)鏡像倉(cāng)庫(kù)進(jìn)行分析,才能獲取更有效的途徑。
Docker Registry Exporter
首先發(fā)現(xiàn)了一個(gè)有意思的項(xiàng)目:DockerRegistryExporter,這個(gè)項(xiàng)目是一個(gè) Python 編寫的 Prometheus Exporter,其中包含四個(gè) Gauge:
-repository_tags_total:按鏡像計(jì)算的 Tag 數(shù)量。
-repository_revisions_total:按鏡像計(jì)算的版本數(shù)量。
-repository_tag_layers_total:以鏡像和 Tag 計(jì)算的 Layer 數(shù)量。
-repository_tag_size_bytes:以鏡像和 Tag 計(jì)算的文件尺寸。
該鏡像使用掛卷的方式,直接對(duì)鏡像庫(kù)文件系統(tǒng)進(jìn)行掃描,例如:
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容器共享文件系統(tǒng),可以定時(shí)輸出監(jiān)控指標(biāo),例如:
$ 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
...然而這并不能滿足我的要求,關(guān)于引用的數(shù)據(jù)并沒有體現(xiàn),另外前面也提到,我們需要比較精確地獲得鏡像版本、Tag 和 Layer 之間的引用關(guān)系以及各自的尺寸,用 PromQL 有點(diǎn)別扭。
我做了個(gè)奇怪的事情
這并不是一個(gè)很常見的需求,只能是一個(gè)清理之前的準(zhǔn)備動(dòng)作,目前看來我需要找到的就是引用數(shù)量少、但是體量比較大的 Layer,但是誰知道以后會(huì)需要什么新的標(biāo)準(zhǔn)呢?干脆把這些東西寫入到數(shù)據(jù)庫(kù)里算了,把這些東西寫入數(shù)據(jù)庫(kù)之后,還掌握 SQL 這樣傳統(tǒng)才藝的程序員就可以隨便搞一搞其它條件了。
關(guān)于鏡像倉(cāng)庫(kù)的一點(diǎn)基礎(chǔ)
鏡像庫(kù)根目錄中有兩個(gè)子目錄:blobs 中保存了所有的 Layer,而 repositories 中則是以鏡像為單位保存的元數(shù)據(jù)。
首先看看鏡像的數(shù)據(jù)
$ tree/org/repo/gameserver
.
├── revisions
│ └── sha256
│ └── ecfb0206e8b...
│ └── link
└── tags
└── latest
├── current
│ └── link
└── index
└── sha256
└── ecfb020...
└── link每個(gè)鏡像的 Manifests 有兩個(gè)目錄,分別承載的是版本和 Tag,正常來說 Tag 和版本是一致的,但實(shí)際上在一些特別情況下,這兩個(gè)數(shù)量可能是不一致的,就會(huì)導(dǎo)致只用 Tag 已經(jīng)無法拉取該鏡像,屬于一種半孤立狀態(tài),應(yīng)該說是需要清除的。
兩個(gè)目錄中的link文件中包含的是一個(gè)哈希碼,可以使用這個(gè)哈希碼在_layers中查找到該鏡像的版本/tag 對(duì)應(yīng)的清單層,使用這個(gè)字符串可以在根_layer中查到對(duì)應(yīng)的目錄,目錄下面的data文件中就是每個(gè)層的具體數(shù)據(jù),對(duì)于清單層,其中會(huì)是一個(gè)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"
}
]}這里看到清單中包含兩個(gè)主節(jié)點(diǎn),config 和 layer,至此,一個(gè)鏡像是由三種不同的層構(gòu)成的:清單、Config 和 Layer。我們關(guān)注的主要是 Layer,其中的 data 文件包含的就是各層的具體內(nèi)容,清單和 Config 中都是文本,Layer 通常都是二進(jìn)制的,也是我們要關(guān)注的主要內(nèi)容。
接下來的問題就順理成章了,把 Repository、Tag、Revision 以及 Layer 的關(guān)系建立起來,隨便用個(gè) SQL 語(yǔ)句,就能夠按照具體需求對(duì)“引用少、尺寸大”的 Layer 進(jìn)行過濾了。
相關(guān)鏈接
Docker Registry Exporter:
https://github.com/sky-uk/docker-registry-exporter
