如何从Docker Registry中导出镜像

前言

Docker RegistryDocker容器系统中的扮演的角色十分重要,所有的docker deamon都需要从registry下载镜像,而这个下载过程是什么样的呢?镜像又是如何存放在registry中,而本地的镜像存储又和registry中存储的有何异同?本文将结合docker和docker registry的代码来解读这些问题。最终实现了不通过docker pull命令新增一个镜像。

为了方便解读,我们使用centos官方镜像作为栗子;docker版本是1.9,docker版本有点旧,新版的改动了一些数据结构,所以如果搞懂了1.9,之后的版本也会相对轻松一些。registry版本是最新的V2版。

一、目录结构

Registry的配置文件中可以指定registry的运行目录(实验用本地文件系统作为后端存储),registry会在这个目录中建立相应的目录结构,我在本地启动一个registry服务,然后只push一个centos镜像上去。镜像名称是localhost:5000/library/centos:latest,然后registry在本地创建了如图1所示的目录。

 

20170118100740

 

图 1 registry目录

为了显示方便,我只截取了64位ID的前一部分。可以看到,目录大体分为两个:一个是blobs,一个是repositories。blobs中主要存放数据文件,可以看出都是经过sha256计算后的ID。repositories目录中放镜像的描述信息,记录了一个镜像有哪些layer,tag对应的manifest文件,link文件是一个文本文件,内容是一个形如“sha256:cf34a09a90b54c…”的64位ID,这个ID对应在blob中的文件其实就是这个image的manifest文件。

二、Manifest文件

manifest文件描述了一个镜像的元信息,包括了layer的数据ID,layer的配置等,文件格式是json形式的文本文件。

docker镜像可以分为V1和V2,在1.9以后镜像格式有一些变化。为了向前兼容V1版本的docker,docekr registryV2使用的manifest也对应地分为Schema1和Schema2,两者可以通过官方对于manifest的解释可以参考[1]和[2]。这里实验都是在schema1上做的。

Schema1主要包含如下信息:

  1. name:image的仓库(repository)名,比如localhost:5000/library/centos:latest这个镜像的repository name是library/centos
  2. tag:该镜像的tag
  3. architecture:指该镜像的宿主机的操作系统架构,如“amd64”
  4. fsLayers:该字段是一个数组,数组中的元素分别指明了各层对应的数据文件的sha256ID,数组的第1个就是镜像的最顶层,第2个是次顶层…以此类推,值得注意的是,不同层的fsLayer ID 有可能一样,是因为有些层是空的,只有一些配置信息。当执行了一个不涉及文件操作的命令,这时候就会形成空fsLayer,空fsLayer计算出来的sha256ID也都是一样的了。镜像的一个layer,是由文件系统(比如新增的文件)fsLayer以及配置信息构成的,layer在docker的代码层面又被称为image,因为任意一个layer都可以作为顶层layer,被docker image信息引用,从而成为一个image。所以需要区分fsLayer与layer。
  5. history:该字段也是一个数组,是为了兼容v1而设置的,指明了每个layer的配置信息,数组第一项对应的是镜像的最顶层,与fslayer一起构成了一个layer。数组元素是一个json格式的map对象,key为“v1Compatibility”,值为一个字符串,该字符串就是layer的配置信息,可以直接用json.Unmrashal成为一个V1Image结构体(定义可以参考代码github.com/docker/docker/image/image.go L31)
  6. schemaVersion:该manifest的版本,一个int型,如 1。

三、Tar包形式的镜像

Docker中有个save和load命令。save命令可以将一个docker镜像导出,把这个镜像从最顶层到最底层的所有layer一起导出到一个tar包中,然后就可以随意拷贝、发送这个tar包到别的机器,最后可以用load命令把这个镜像重新加载进docker。

如果我们把一个镜像从registry里拿出来,按照save成的tar包格式来组织,然后使用load命令加载,这样就实现了不通过docker pull命令来下载镜像,可以根据这个原理做第三方镜像下载工具。

所以我们先来看一下镜像的tar包形式是什么样的,我使用save命令导出了centos镜像,解压后目录结构如图2所示:

 

20170118100749

 

图 2 镜像tar包解压后的目录

下面对各文件进行解释:

根目录下的repositories文件,描述了这个镜像的名字,tage,还有顶层layer的id

不同的文件夹代表了不同的layer。

json:layer的配置信息,如创建时间,执行命令等。

layer.tar:layer中包含的文件,如果是空layer,layer.tar解压后就是空的。

VERSION: 版本信息。

四、从registry导出镜像

我们对比tar包中的文件和registry中的文件,不难发现其中的对应关系,json、VERSION还有repositories文件都是可以从manifest中导出。

json文件其实就是之前提到的history字段中v1Compatibilitiy,不同的是manifest中的这个字段中有很多转义符,我们需要去掉这些转义符,方法是先Unmarshal成为一个V1Image结构体,然后在json.Marshal转回字符串就好。

layer.tar其实就是blobs中对应的data文件,直接复制出来然后改个名字就可以。

VERSION是manifest中的schemaVersion。

repositories文件内容很简单,格式是{“imageName”:{“tag”:”topLayerID”}},所以按照这个格式从manifest中找到对应的数据填进去就ok。

这些文件都准备好了以后,就可以准备打包成tar包了,直接使用linux中自带的tar命令,这里需要注意的一点是,应该使用“只打包不压缩”的选项。生成tar包后就可以直接使用docker load命令导入了。

我这么做了,是可以成功地导入一个镜像。但是发现存在一个问题:使用上述方式导入的镜像,每个layer的ID和我直接用docker pull命令下载下的不一样,而且docker pull得到的ID从未在manifest和registry中的任何地方出现过。而且不管我使用新的机器还是重新pull,得到的ID都是一样的。经过阅读docker的代码我才发现,layerID不是随机生成的,也不是manifest中写道的id,而是算出来的。下面就说一下计算过程。

我们最终需要的layerID在docker源码中叫做StrongID,StrongID是把一个byte数组做Hash后得到的,这个byte数组的生成需要三个对象:v1Compatibility,blobSum(manifest中的fsLayerID),parent(父layer的StrongID),数组生成方法参考image.go中的MakeImageConfig方法(docker 1.9),基本操作就是把一个json对象转成字节数组。因为有parent字段的存在,需要从最底层的layer开始计算,逐步迭代,最终的到top layer的ID。最后要做的工作就是替换json文件中的id字段和parent字段成为新计算出来的ID即可。同样地,文件夹名也要做相应改变。

增量导入:如果本地已经存在某些layer的情况,我们只用打包新的layer即可,因为导入时候docker会检测这个layer是否存在,而且有parent信息来保证layer之间的关系。

五、总结

本文讲述了如何从docker registry中直接导出docker image的方法,这在做一些第三方docker镜像下载工具十分有用。并且通过阅读docker的源代码,了解了layerID的生成方法。

分享到:更多 ()