# 节省镜像库空间的一个思路

最近遇到一个有趣的状况，某镜像仓库占用了大量的磁盘空间。通常要解决这种问题，给 Registry 发删除指令，并进行 GC 就可以了。然而很多时候，所有镜像都正常，在删除多个 Tag 甚至是 Repository 之后，问题仍然没能缓解，原理也很容易理解——删除的镜像虽然大，可能只是复用了一些比较大的层，删除镜像并不会真正的发出，所以还是需要对镜像库的存储进行更多的了解，进行进一步的统计，在层一级对镜像仓库进行分析，才能获取更有效的途径。

## Docker Registry Exporter

首先发现了一个有意思的项目：[DockerRegistryExporter](https://github.com/sky-uk/docker-registry-exporter)，这个项目是一个 Python 编写的 Prometheus Exporter，其中包含四个 Gauge：

\-`repository_tags_total`：按镜像计算的 Tag 数量。 -`repository_revisions_total`：按镜像计算的版本数量。 -`repository_tag_layers_total`：以镜像和 Tag 计算的 Layer 数量。 -`repository_tag_size_bytes`：以镜像和 Tag 计算的文件尺寸。

该镜像使用挂卷的方式，直接对镜像库文件系统进行扫描，例如：

```yaml
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容器共享文件系统，可以定时输出监控指标，例如：

```bash
$ 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` 中则是以镜像为单位保存的元数据。

首先看看镜像的数据

```bash
$ 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`字符串：

```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 进行过滤了。

## 相关链接

* **Docker Registry Exporter**：`https://github.com/sky-uk/docker-registry-exporter`
