通常我们在使用集群或者容器的时候,都会接触到存储在本地的镜像,也或多或少对本地镜像存储有一定的了解。但是服务端的镜像存储细节呢?本文主要介绍容器镜像的服务端存储结构,对于自建镜像服务或是对容器镜像底层原理或优化有兴趣的同学可以了解一下。
目前容器镜像服务相关的开源项目主要有以下两个。
Registry具有基本的镜像上传、下载以及对接第三方鉴权的能力。Harbor则基于Registry做了相应的企业级扩展的项目。提供了更多权限、审计、镜像等功能,目前是CNCF孵化项目之一。其他详情参考相关文章。这篇文章主要讲解Registry项目的存储细节。
在了解服务端之前,我们来了解一下客户端的镜像容器的存储环境。
Docker的存储驱动的实现是基于UnionFS。简单列举一下UnionFS下存储镜像的一些特点。
首先,UnionFS是一个分层的文件系统。一个Docker镜像可能有多个层组成(注意他们是有顺序的)。
其次,只有顶层是可写的,其它层都是只读的。这样的机制带来的好处是镜像层可以被多个镜像共享。对于Docker镜像来说,所有层都是只读的。当一个镜像运行时,会在该镜像上增加一个容器层。十个相同的镜像启动,仅仅是增加十个容器层。销毁容器时也仅仅是销毁一个容器层而已。
UnionFS是一个分层的文件系统。一个Docker镜像可能有多个层组成(注意他们是有顺序的)。
只有顶层是可写的,其它层都是只读的。这样的机制带来的好处是镜像层可以被多个镜像共享。对于Docker镜像来说,所有层都是只读的。当一个镜像运行时,会在该镜像上增加一个容器层。十个相同的镜像启动,仅仅是增加十个容器层。销毁容器时也仅仅是销毁一个容器层而已。
由此可以思考很多安全和镜像优化上的问题。
UnionFS一般有两种实现方案:1. 基于文件实现。文件整体的覆盖重写。2. 基于块实现,对文件的修改只修改少量块。
提供一个镜像元信息(manifest)用于参考:
? ~ docker pull ccr.ccs.tencentyun.com/paas/service-controller:7b1c981c7b1c981c: Pulling from paas/service-controllerDigest: sha256:e8b84ce6c245f04e6e453532d676f7c7f0a94b3122f93a89a58f9ae49939e419Status: Image is up to date for ccr.ccs.tencentyun.com/paas/service-controller:7b1c981cccr.ccs.tencentyun.com/paas/service-controller:7b1c981c
{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 4671, "digest": "sha256:785f4150a5d9f62562f462fa2d8b8764df4215f0f2e3a3716c867aa31887f827" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 44144090, "digest": "sha256:e80174c8b43b97abb6bf8901cc5dade4897f16eb53b12674bef1eae6ae847451" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 529, "digest": "sha256:d1072db285cc5eb2f3415891381631501b3ad9b1a10da20ca2e932d7d8799988" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 849, "digest": "sha256:858453671e6769806e0374869acce1d9e5d97f5020f86139e0862c7ada6da621" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 170, "digest": "sha256:3d07b1124f982f6c5da7f1b85a0a12f9574d6ce7e8a84160cda939e5b3a1faad" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 8461461, "digest": "sha256:994dade28a14b2eac1450db7fa2ba53998164ed271b1e4b0503b1f89de44380c" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 22178452, "digest": "sha256:60a5bd5c14d0f37da92d2a5e94d6bbfc1e2a942d675aee24f055ced76e8a208f" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 22178452, "digest": "sha256:60a5bd5c14d0f37da92d2a5e94d6bbfc1e2a942d675aee24f055ced76e8a208f" } ]}
接下来是本文最为重要的内容,通过对上面这张图的理解,我们就可以了解到Registry服务端存储的细节。\
图中蓝色的是服务端存储的目录。文字是目录名称,这个名称是固定的。
图中紫色的是服务端存储的文件。文字是文件名称,link
文件的内容都是一个sha256的哈希值。data
文件存储了真正的元文件和镜像层。
整个图是从上往下的。举个例子,我们上面描述的manifest如果是存储在服务端的话(文件哈希:sha256:e8b84ce6c245f04e6e453532d676f7c7f0a94b3122f93a89a58f9ae49939e419
)。它存储的路径应该是:/docker/registry/v2/blobs/sha256/e8/e8b84ce6c245f04e6e453532d676f7c7f0a94b3122f93a89a58f9ae49939e419/data
。对应图上应该是沿着左侧一直向下。
我们开始拆解分析其结构细节。
左侧是镜像所有内容的实际存储,其几乎占据的绝大部分储存的空间,包括了镜像层和镜像元信息Manifest。
sha256:e80174c8b43b97abb6bf8901cc5dade4897f16eb53b12674bef1eae6ae847451
的存储位置,应该在/docker/registry/v2/blobs/sha256/e8/e80174c8b43b97abb6bf8901cc5dade4897f16eb53b12674bef1eae6ae847451/data
右侧是镜像元信息存储的地方。镜像元信息是按照命名空间和仓库名称分两级目录存储的。
每一个仓库下面又分为_layers
、_manifests
两个部分
_layers
负责记录该仓库引用了哪些镜像层文件。
_manifests
负责记录镜像的元信息
revisions
包含了仓库下曾经上传过的所有版本的镜像元信息tags
包含了仓库中的所有标签
current
记录了当前标签指向的镜像index
目录则记录了标签指向的历史镜像。sha256:e8b84ce6c245f04e6e453532d676f7c7f0a94b3122f93a89a58f9ae49939e419
,这个元信息的存储位置应该在/docker/registry/v2/blobs/sha256/e8/e8b84ce6c245f04e6e453532d676f7c7f0a94b3122f93a89a58f9ae49939e419/data
举个镜像下载的例子:
我们想要知道ccr.ccs.tencentyun.com/paas/service-controller:7b1c981c
这个镜像现在的元信息,如何在服务端存储中找到。
/docker/registry/v2/paas/service-controller/_manifests/tags/7b1c981c/current/link
文件。里面有元信息的sha256信息。内容应该是sha256:e8b84ce6c245f04e6e453532d676f7c7f0a94b3122f93a89a58f9ae49939e419
/docker/registry/v2/blobs/sha256/e8/e8b84ce6c245f04e6e453532d676f7c7f0a94b3122f93a89a58f9ae49939e419/data
)。前文中给出了该文件的json内容。ImageConfig
sha256:785f4150a5d9f62562f462fa2d8b8764df4215f0f2e3a3716c867aa31887f827
ImageLayer
sha256:e80174c8b43b97abb6bf8901cc5dade4897f16eb53b12674bef1eae6ae847451 sha256:d1072db285cc5eb2f3415891381631501b3ad9b1a10da20ca2e932d7d8799988 sha256:858453671e6769806e0374869acce1d9e5d97f5020f86139e0862c7ada6da621 sha256:3d07b1124f982f6c5da7f1b85a0a12f9574d6ce7e8a84160cda939e5b3a1faad sha256:994dade28a14b2eac1450db7fa2ba53998164ed271b1e4b0503b1f89de44380c sha256:60a5bd5c14d0f37da92d2a5e94d6bbfc1e2a942d675aee24f055ced76e8a208f
Tips:
根据UnionFS的特性,针对性的进行优化:
需要,在Registry的设计中仓库是权限的最小单位,用户是根据仓库进行权限管理与隔离的。考虑如果这里忽略了这一块的设计,镜像层存在就避免重复上传的话,客户端可以通过构造虚假镜像元信息的方式,越权获取到其他用户的镜像。_layers
中记录了仓库有权限获取的所有镜像层的目的就在于此。
复制镜像的场景和上传场景的区别在于,源镜像是用户确实已经拥有的。这里可以通过上述问题的思考进行复制的优化,当镜像层在目的地址已经存在时,直接标记仓库拥有该层避免不必要的上传。
根据存储结构的特点,可以较为轻松的回答这个问题。理论上只要不做Registry GC,不删除仓库元信息,仓库历史版本的镜像都会在仓库中一直保存的。
这里不做详细讲解了,有兴趣的同学可以参考以下文档:
Registry身为一个开源软件,适配各种云存储产品属于标配功能了。Registry提供了标准的存储驱动接口,只要实现了这一套接口就能适配运行起来了。
原文:https://blog.51cto.com/14120339/2541632