Docker镜像
Docker所宣称的用户可以随心所欲地“Build、Ship and Run”应用的能力,其核心是由Docker image(Docker镜像)来支撑的。
- 镜像这一核心概念介绍具体操作;
- 包括如何使用pull命令从Docker Hub仓库中下载镜像到本地;
- 如何查看本地已有的镜像信息和管理镜像标签;
- 如何在远端仓库使用search命令进行搜索和过滤;
- 如何删除镜像标签和镜像文件;
- 如何创建用户定制的镜像并且保存为外部文件
- 如何往Docker Hub仓库中推送自己的镜像。
理解Docker镜像
Docker通过把应用的运行时环境和应用打包在一起,解决了部署环境依赖的问题;通过引入分层文件系统这种概念,解决了空间利用的问题。它彻底消除了编译、打包与部署、运维之间的鸿沟,与现在互联网企业推崇的DevOps理念不谋而合,大大提高了应用开发部署的效率。
Docker镜像概念介绍
Docker image是用来启动容器的只读模板,是容器启动所需要的rootfs,类似于虚拟机所使用的镜像。首先需要通过一定的规则和方法表示Docker image。
典型的Docker镜像的表示方法,可以看到其被“/”分为了三个部分,其中每部分都可以类比Github中的概念。下面按照从左到右的顺序介绍这几个部分以及相关的一些重要概念。
Remote-dockerhub.com/namespace/bar:latest
- Remote docker hub:集中存储镜像的Web服务器地址。该部分的存在使得可以区分从不同镜像库中拉取的镜像。若Docker的镜像表示中缺少该部分,说明使用的是默认镜像库,即Docker官方镜像库。
- Namespace:类似于Github中的命名空间,是一个用户或组织中所有镜像的集合。
- Repository:类似于Git仓库,一个仓库可以有多个镜像,不同镜像通过tag来区分。
- Tag:类似Git仓库中的tag,一般用来区分同一类镜像的不同版本。
- Layer:镜像由一系列层组成,每层都用64位的十六进制数表示,非常类似于Git仓库中的commit。
- Image ID:镜像最上层的layer ID就是该镜像的ID,Repo:tag提供了易于人类识别的名字,而ID便于脚本处理、操作镜像。
镜像库是Docker公司最先提出的概念,非常类似应用市场的概念。用户可以发布自己的镜像,也可以使用别人的镜像。Docker开源了镜像存储部分的源代码(Docker Registry以及Distribution),但是这些开源组件并不适合独立地发挥功能,需要使用Nginx等代理工具添加基本的鉴权功能,才能搭建出私有镜像仓库。 本地镜像则是已经下载到本地的镜像,可以使用docker images等命令进行管理。这些镜像默认存储在/var/lib/docker路径下,该路径也可以使用docker daemon–g参数在启动Daemon时指定。
使用Docker镜像
Docker内嵌了一系列命令制作、管理、上传和下载镜像。可以调用REST API给Docker daemon发送相关命令,也可以使用client端提供的CLI命令完成操作。下面就从Docker image的生命周期角度说明Docker image的相关使用方法。
列出本机的镜像
下面的命令可以列出本地存储中镜像,也可以查看这些镜像的基本信息。
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu 14.04.2 2103b00b3fdf 5 months ago 188.3 MB
ubuntu latest 2103b00b3fdf 5 months ago 188.3 MB
ubuntu trusty 2103b00b3fdf 5 months ago 188.3 MB
ubuntu trusty-20150228.11 2103b00b3fdf 5 months ago 188.3 MB
ubuntu 14.04 2d24f826cb16 5 months ago 188.3 MB
busybox buildroot-2014.02 4986bf8c1536 7 months ago 2.43 MB
busybox latest 4986bf8c1536 7 months ago 2.43 MB
此外,通过–help参数还可以查询docker images的详细用法,如下:
$ docker images --help
Usage: docker images [OPTIONS] [REPOSITORY]
List images
-a, --all=false Show all images (default hides intermediate images)
--digests=false Show digests
-f, --filter=[] Filter output based on conditions provided
--help=false Print usage
--no-trunc=false Don't truncate output
-q, --quiet=false Only show numeric IDs
其中,–filter用于过滤docker images的结果,过滤器采用key=value的这种形式。目前支持的过滤器为dangling和label。 –filter”dangling=true”会显示所有“悬挂”镜像。“悬挂”镜像没有对应的名称和tag,并且其最上层不会被任何镜像所依赖。docker commit在一些情况下会产生这种“悬挂”镜像。下面第一条命令产生了一个“悬挂”镜像,第二条命令则根据其特点过滤出该镜像了。
$ docker commit 0d6cbf57f660
$ docker images --filter "dangling=true"
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
<none> <none> d08407d841f3 3 hours ago 2.43 MB
在上面的命令中,–no-trunc参数可以列出完整长度的Image ID。若添加参数-q则会只输出Image ID,该参数在管道命令中很有用处。一般来说悬挂镜像并不总是我们所需要的,并且会浪费磁盘空间。可以使用如下管道命令删除所有的“悬挂”镜像。
$ docker images --filter "dangling=true" -q | xargs docker rmi
Deleted: 8a39aa048fe3f2e319651b206073b2a2e437dcf85c15fedb6f437cfe86105145
这里的–digests比较特别,这个参数是伴随着新版本的Docker Registry V2(即Distribution)产生的
Build:创建一个镜像
创建镜像是一个很常用的功能,既可以从无到有地创建镜像,也可以以现有的镜像为基础进行增量开发,还可以把容器保存为镜像。
获取镜像
可以使用docker [image] pull命令直接从Docker Hub镜像源来下载镜像。该命令的格式为docker [image] pull [NAME:TAG]。
其中,NAME是镜像仓库名称(用来区分镜像),TAG是镜像的标签(往往用来表示版本信息)。通常情况下,描述一个镜像需要包括“名称+标签”信息。
例如,获取一个Ubuntu 18.04系统的基础镜像可以使用如下的命令:
$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
...
Digest: sha256:e27e9d7f7f28d67aa9e2d7540bdc2b33254b452ee8e60f388875e5b7d9b2b696
Status: Downloaded newer image for ubuntu:18.04
对于Docker镜像来说,如果不显式指定TAG,则默认会选择latest标签,这会下载仓库中最新版本的镜像。
下面的例子将从Docker Hub的Ubuntu仓库下载一个最新版本的Ubuntu操作系统的镜像:
$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
...
Digest: sha256:e27e9d7f7f28d67aa9e2d7540bdc2b33254b452ee8e60f388875e5b7d9b2b696
Status: Downloaded newer image for ubuntu:latest
该命令实际上下载的就是ubuntu:latest镜像。 一般来说,镜像的latest标签意味着该镜像的内容会跟踪最新版本的变更而变化,内容是不稳定的。因此,从稳定性上考虑,不要在生产环境中忽略镜像的标签信息或使用默认的latest标记的镜像。
下载过程中可以看出,镜像文件一般由若干层(layer)组成,6c953ac5d795这样的串是层的唯一id(实际上完整的id包括256比特,64个十六进制字符组成)。使用docker pull命令下载中会获取并输出镜像的各层信息。当不同的镜像包括相同的层时,本地仅存储了层的一份内容,减小了存储空间。
读者可能会想到,在不同的镜像仓库服务器的情况下,可能会出现镜像重名的情况。
严格地讲,镜像的仓库名称中还应该添加仓库地址(即registry,注册服务器)作为前缀,只是默认使用的是官方Docker Hub服务,该前缀可以忽略。
例如,docker pull ubuntu:18.04命令相当于docker pull registry.hub.docker.com/ubuntu:18.04命令,即从默认的注册服务器Docker Hub Registry中的ubuntu仓库来下载标记为18.04的镜像。
如果从非官方的仓库下载,则需要在仓库名称前指定完整的仓库地址。例如从网易蜂巢的镜像源来下载ubuntu:18.04镜像,可以使用如下命令,此时下载的镜像名称为hub.c.163.com/public/ubuntu:18.04:
$ docker pull hub.c.163.com/public/ubuntu:18.04
pull子命令支持的选项主要包括:
-
-a,–all-tags=true false:是否获取仓库中的所有镜像,默认为否; - –disable-content-trust:取消镜像的内容校验,默认为真。 另外,有时需要使用镜像代理服务来加速Docker镜像获取过程,可以在Docker服务启动配置中增加–registry-mirror=proxy_URL来指定镜像代理服务地址(如https://registry.docker-cn.com)。
下载镜像到本地后,即可随时使用该镜像了,例如利用该镜像创建一个容器,在其中运行bash应用,执行打印“Hello World”命令:
$ docker run -it ubuntu:18.04 bash
root@65663247040f:/# echo "Hello World"
Hello World
root@65663247040f:/# exit
搜寻镜像
使用docker search命令可以搜索Docker Hub官方仓库中的镜像。语法为docker search [option] keyword。支持的命令选项主要包括:
- -f,–filter filter:过滤输出内容;
- –format string:格式化输出内容;
- –limit int:限制输出结果个数,默认为25个;
- –no-trunc:不截断输出结果。
例如,搜索官方提供的带nginx关键字的镜像,如下所示:
$ docker search --filter=is-official=true nginx
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
nginx Official build of Nginx. 7978 [OK]
kong Open-source Microservice & API Management la… 159 [OK]
再比如,搜索所有收藏数超过4的关键词包括tensorflow的镜像:
$ docker search --filter=stars=4 tensorflow
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
tensorflow/tensorflow Official docker images for deep learning fra… 760
xblaster/tensorflow-jupyter Dockerized Jupyter with tensorflow 47 [OK]
jupyter/tensorflow-notebook Jupyter Notebook Scientific Python Stack w/ … 46
romilly/rpi-docker-tensorflow Tensorflow and Jupyter running in docker con… 16
floydhub/tensorflow tensorflow 8 [OK]
erroneousboat/tensorflow-python3-jupyter Docker container with python 3 version of te… 8 [OK]
tensorflow/tf_grpc_server Server for TensorFlow GRPC Distributed Runti… 5
可以看到返回了很多包含关键字的镜像,其中包括镜像名字、描述、收藏数(表示该镜像的受欢迎程度)、是否官方创建、是否自动创建等。默认的输出结果将按照星级评价进行排序。
删除镜像
使用docker rmi或docker image rm命令可以删除镜像,命令格式为docker rmi IMAGE[IMAGE...],其中IMAGE可以为标签或ID。
支持选项包括:
- -f,-force:强制删除镜像,即使有容器依赖它;
- -no-prune:不要清理未带标签的父镜像。
例如,要删除掉myubuntu:latest镜像,可以使用如下命令:
$ docker rmi myubuntu:latest Untagged: myubuntu:latest读者可能会想到,本地的ubuntu:latest镜像是否会受到此命令的影响。无须担心,当同一个镜像拥有多个标签的时候,docker rmi命令只是删除了该镜像多个标签中的指定标签而已,并不影响镜像文件。因此上述操作相当于只是删除了镜像0458a4468cbc的一个标签副本而已。
再次查看本地的镜像,发现ubuntu:latest镜像(准确地说,0458a4468cbc镜像)仍然存在:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
Ubuntu 18.04 452a96d81c30 5 weeks ago 79.6MB
Ubuntu latest 452a96d81c30 5 weeks ago 79.6MB
但当镜像只剩下一个标签的时候就要小心了,此时再使用docker rmi命令会彻底删除镜像。
例如通过执行docker rmi命令来删除只有一个标签的镜像,可以看出会删除这个镜像文件的所有文件层:
$ docker rmi busybox:latest
Untagged: busybox:latest
Untagged: busybox@sha256:1669a6aa7350e1cdd28f972ddad5aceba2912f589f19a090ac75b7083da748db
Deleted: sha256:5b0d59026729b68570d99bc4f3f7c31a2e4f2a5736435641565d93e7c25bd2c3
Deleted: sha256:4febd3792a1fb2153108b4fa50161c6ee5e3d16aa483a63215f936a113a88e9a
当使用docker rmi命令,并且后面跟上镜像的ID(也可以是能进行区分的部分ID串前缀)时,会先尝试删除所有指向该镜像的标签,然后删除该镜像文件本身。 注意,当有该镜像创建的容器存在时,镜像文件默认是无法被删除的,例如:先利用ubuntu:18.04镜像创建一个简单的容器来输出一段话:
$ docker run ubuntu:18.04 echo 'hello! I am here!'
hello! I am here!
使用docker ps-a命令可以看到本机上存在的所有容器:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a21c0840213e ubuntu:18.04 "echo 'hello! I am he" About a minute ago Exited (0) About a minute ago romantic_euler
可以看到,后台存在一个退出状态的容器,是刚基于ubuntu:18.04镜像创建的。
试图删除该镜像,Docker会提示有容器正在运行,无法删除:
$ docker rmi ubuntu:18.04
Error response from daemon: conflict: unable to remove repository reference "ubuntu:18.04" (must force) - container a21c0840213e is using its referenced image 8f1bd21bd25c
如果要想强行删除镜像,可以使用-f参数:
$ docker rmi -f ubuntu:18.04
Untagged: ubuntu:18.04
Deleted: sha256:8f1bd21bd25c3fb1d4b00b7936a73a0664f932e11406c48a0ef19d82fd0b7342
注意,通常并不推荐使用-f参数来强制删除一个存在容器依赖的镜像。正确的做法是,先删除依赖该镜像的所有容器,再来删除镜像。 首先删除容器a21c0840213e:
$ docker rm a21c0840213e
然后使用ID来删除镜像,此时会正常打印出删除的各层信息:
$ docker rmi 8f1bd21bd25c
Untagged: ubuntu:18.04
Deleted: sha256:8f1bd21bd25c3fb1d4b00b7936a73a0664f932e11406c48a0ef19d82fd0b7342
Deleted: sha256:8ea3b9ba4dd9d448d1ca3ca7afa8989d033532c11050f5e129d267be8de9c1b4
Deleted: sha256:7db5fb90eb6ffb6b5418f76dde5f685601fad200a8f4698432ebf8ba80757576
Deleted: sha256:19a7e879151723856fb640449481c65c55fc9e186405dd74ae6919f88eccce75
Deleted: sha256:c357a3f74f16f61c2cc78dbb0ae1ff8c8f4fa79be9388db38a87c7d8010b2fe4
Deleted: sha256:a7e1c363defb1f80633f3688e945754fc4c8f1543f07114befb5e0175d569f4c
清理镜像
使用Docker一段时间后,系统中可能会遗留一些临时的镜像文件,以及一些没有被使用的镜像,可以通过docker image prune命令来进行清理。
支持选项包括:
- -a,-all:删除所有无用镜像,不光是临时镜像;
- -filter filter:只清理符合给定过滤器的镜像;
- -f,-force:强制删除镜像,而不进行提示确认。
例如,如下命令会自动清理临时的遗留镜像文件层,最后会提示释放的存储空间:
$ docker image prune -f
...
Total reclaimed space: 1.4 GB
创建镜像
创建镜像的方法主要有三种:基于已有镜像的容器创建、基于本地模板导入、基于Dockerfile创建。
基于已有容器创建
该方法主要是使用docker[container]commit命令。 命令格式为docker[container]commit[OPTIONS]CONTAINER[REPOSITORY[:TAG]],主要选项包括:
- -a,–author=”“:作者信息;
-
-c,–change=[]:提交的时候执行Dockerfile指令,包括CMD ENTRYPOINT ENV EXPOSE LABEL ONBUILD USER VOLUME WORKDIR等; - -m,–message=”“:提交消息;
- -p,–pause=true:提交时暂停容器运行。
下面将演示如何使用该命令创建一个新镜像。
首先,启动一个镜像,并在其中进行修改操作。例如,创建一个test文件,之后退出,代码如下:
$ docker run -it ubuntu:18.04 /bin/bash root@a925cb40b3f0:/# touch test root@a925cb40b3f0:/# exit记住容器的ID为a925cb40b3f0。 此时该容器与原ubuntu:18.04镜像相比,已经发生了改变,可以使用docker[container]commit命令来提交为一个新的镜像。提交时可以使用ID或名称来指定容器:
$ docker [container] commit -m "Added a new file" -a "Docker Newbee" a925cb40b3f0 test:0.1 9e9c814023bcffc3e67e892a235afe61b02f66a947d2747f724bd317dda02f27顺利的话,会返回新创建镜像的ID信息,例如9e9c814023bcffc3e67e892a235afe61b02f66a947d2747f724bd317dda02f27。
此时查看本地镜像列表,会发现新创建的镜像已经存在了:
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
test 0.1 9e9c814023bc 4 seconds ago 188 MB
Docker提供的docker commit命令可以增量地生成一个镜像,该命令可把容器保存为一个镜像,还能注明作者信息和镜像名称,这与git commit类似。当镜像名称为空时,就会形成“悬挂”镜像。当然,使用这种方式每新增加一层都需要数个步骤(比如,启动容器、修改、保存修改等),所以效率是比较低的,因此这种方式适合正式制作镜像前的尝试。当最终确定制作的步骤后,可以使用docker build命令,通过Dockerfile文件生成镜像。
基于本地模板导入
用户也可以直接从一个操作系统模板文件导入一个镜像,主要使用docker [container] import命令。命令格式为docker [image] import [OPTIONS] file|URL|-[REPOSITORY[:TAG]]
要直接导入一个镜像,可以使用OpenVZ提供的模板来创建,或者用其他已导出的镜像模板来创建。OPENVZ模板的下载地址为http://openvz.org/Download/templates/precreated。
例如,下载了ubuntu-18.04的模板压缩包,之后使用以下命令导入即可:
$ cat ubuntu-18.04-x86_64-minimal.tar.gz | docker import - ubuntu:18.04
然后查看新导入的镜像,已经在本地存在了:
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu 18.04 05ac7c0b9383 17 seconds ago 215.5 MB
docker import用于导入包含根文件系统的归档,并将之变成Docker镜像。因此,docker import常用来制作Docker基础镜像,如Ubuntu等镜像。与此相对,docker export则是把一个镜像导出为根文件系统的归档。
基于Dockerfile创建
基于Dockerfile创建是最常见的方式。Dockerfile是一个文本文件,利用给定的指令描述基于某个父镜像创建新镜像的过程。
下面给出Dockerfile的一个简单示例,基于debian:stretch-slim镜像安装Python 3环境,构成一个新的python:3镜像:
FROM debian:stretch-slim
LABEL version="1.0" maintainer="docker user <docker_user@github>"
RUN apt-get update && \
apt-get install -y python3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
创建镜像的过程可以使用docker[image]build命令,编译成功后本地将多出一个python:3镜像:
$ docker [image] build -t python:3 .
...
Successfully built 4b10f46eacc8
Successfully tagged python:3
$ docker images|grep python
python 3 4b10f46eacc8 About a minute ago 95.1MB
存出镜像
如果要导出镜像到本地文件,可以使用docker[image]save命令。该命令支持-o、-output string参数,导出镜像到指定的文件中。
例如,导出本地的ubuntu:18.04镜像为文件ubuntu_18.04.tar,如下所示:
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu 18.04 0458a4468cbc 2 weeks ago 188 MB
...
$ docker save -o ubuntu_18.04.tar ubuntu:18.04
用户就可以通过复制ubuntu_18.04.tar文件将该镜像分享给他人。
载入镜像
可以使用docker[image] load将导出的tar文件再导入到本地镜像库。支持-i、-input string选项,从指定文件中读入镜像内容。
例如,从文件ubuntu_18.04.tar导入镜像到本地镜像列表,如下所示:
$ docker load -i ubuntu_18.04.tar
或者:
$ docker load < ubuntu_18.04.tar
这将导入镜像及其相关的元数据信息(包括标签等)。导入成功后,可以使用docker images命令进行查看,与原镜像一致。 docker load一般只用于导入由docker save导出的镜像,导入后的镜像跟原镜像完全一样,包括拥有相同的镜像ID和分层等内容。
上传镜像
可以使用docker[image]push命令上传镜像到仓库,默认上传到Docker Hub官方仓库(需要登录)。命令格式为docker[image]push NAME[:TAG]|[REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG]。
例如,用户user上传本地的test:latest镜像,可以先添加新的标签user/test:latest,然后用docker[image]push命令上传镜像:
$ docker tag test:latest user/test:latest
$ docker push user/test:latest
The push refers to a repository [docker.io/user/test]
Sending image list
Please login prior to push: Username: Password: Email: 第一次上传时,会提示输入登录信息或进行注册,之后登录信息会记录到本地~/.docker目录下。
Ship:传输一个镜像
镜像传输是连接开发和部署的桥梁。可以使用Docker镜像仓库做中转传输,还可以使用docker export/docker save生成的tar包来实现,或者使用Docker镜像的模板文件Dockerfile做间接传输。目前托管在Github等网站上的项目,已经越来越多地包含有Dockerfile文件;同时Docker官方镜像仓库使用了github.com的webhook功能,若代码被修改就会触发流程自动重新制作镜像。
Run:以image为模板启动一个容器
启动容器时,可以使用docker run命令。
现阶段Docker镜像相关的命令存在一些问题,包括:
- 命令间逻辑不一致,比如列出容器使用的是docker ps,列出镜像使用的是docker images。
- 混用命令导致命令语义不清晰,比如查看容器和镜像详细信息的命令都是docker inspect。
所以基于这些考虑,Docker项目的路标中提到会把相关命令归类,并使用二级命令来管理。
Docker镜像的组织结构
数据的内容
Docker image包含着数据及必要的元数据。数据由一层层的image layer组成,元数据则是一些JSON文件,用来描述数据(image layer)之间的关系以及容器的一些配置信息。 下面使用overlay存储驱动对Docker image的组织结构进行分析,首先需要启动Docker daemon,命令如下:
# docker daemon -D –s overlay –g /var/lib/docker
这里从官方镜像库下载busybox镜像用作分析。由于前面已经下载过该镜像,所以这里并没有重新下载,而只是做了简单的校验。可以看到Docker对镜像进行了完整性校验,这种完整性的凭证是由镜像仓库提供的。
$ docker pull busybox
Using default tag: latest
latest: Pulling from library/busybox
cf2616975b4a: Already exists
8c2e06607696: Already exists
Digest: sha256:38a203e1986cf79639fb9b2e1d6e773de84002feea2d4eb006b52004ee8502d
Status: Image is up to date for busybox:latest
$ docker history busybox #为了排版对结果做了一些整理
IMAGE CREATED CREATED BY SIZE
8c2e06607696 4 months ago 0 B
6ce2e90b0bc7 4 months ago /bin/sh -c #(nop) ADD file 2.43 MB
cf2616975b4a 4 months ago /bin/sh -c #(nop) MAINTAINER 0 B
该镜像包含cf2616975b4a、6ce2e90b0bc7、8c2e06607696三个layer。让我们先到本地存储路径一探究竟吧。
# ls -l /var/lib/docker
total 44
drwx------ 2 root root 4096 Jul 24 18:41 containers #存放容器运行相关信息
drwx------ 3 root root 4096 Apr 13 14:32 execdriver
drwx------ 6 root root 4096 Jul 24 18:43 graph #Image各层的元数据
drwx------ 2 root root 4096 Jul 24 18:41 init
-rw-r--r-- 1 root root 5120 Jul 24 18:41 linkgraph.db
drwxr-xr-x 5 root root 4096 Jul 24 18:43 overlay #Image各层数据
-rw------- 1 root root 106 Jul 24 18:43 repositories-overlay #Image总体信息
drwx------ 2 root root 4096 Jul 24 18:43 tmp
drwx------ 2 root root 4096 Jul 24 19:09 trust #验证相关信息
drwx------ 2 root root 4096 Jul 24 18:41 volumes #数据卷相关信息
总体信息
从repositories-overlay文件可以看到该存储目录下的所有image以及其对应的layer ID。为了减少干扰,实验环境之中只包含一个镜像,其ID为8c2e06607696bd4af,如下。
# cat repositories-overlay |python -m json.tool
{
"Repositories": {
"busybox": {
"latest": "8c2e06607696bd4afb3d03b687e361cc43cf8ec1a4a725bc96e39f05ba97dd55"
}
}
}
数据和元数据
graph目录和overlay目录包含本地镜像库中的所有元数据和数据信息。对于不同的存储驱动,数据的存储位置和存储结构是不同的,本章不做深入的讨论。可以通过下面的命令观察数据和元数据中的具体内容。元数据包含json和layersize两个文件,其中json文件包含了必要的层次和配置信息,layersize文件则包含了该层的大小。
# ls -l graph/8c2e06607696bd4afb3d03b687e361cc43cf8ec1a4a725bc96e39f05ba97dd55/
total 8
-rw------- 1 root root 1446 Jul 24 18:43 json
-rw------- 1 root root 1 Jul 24 18:43 layersize
# ls -l overlay/8c2e06607696bd4afb3d03b687e361cc43cf8ec1a4a725bc96e39f05ba97dd55/
total 4
drwxr-xr-x 17 root root 4096 Jul 24 18:43 root
可以看到Docker镜像存储路径下已经存储了足够的信息,Docker daemon可以通过这些信息还原出Docker image:先通过repositories-overlay获得image对应的layer ID;再根据layer对应的元数据梳理出image包含的所有层,以及层与层之间的关系;然后使用联合挂载技术还原出容器启动所需要的rootfs和一些基本的配置信息。
数据的组织
通过repositories-overlay可以找到某个镜像的最上层layer ID,进而找到对应的元数据,那么元数据都存了哪些信息呢?可以通过docker inspect得到该层的元数据。
docker inspect并不是直接输出磁盘中的元数据文件,而是对元数据文件进行了整理,使其更易读,比如标记镜像创建时间的条目由created改成了Created;标记容器配置的条目由container_config改成了ContainerConfig,但是两者的数据是完全一致的。
$ docker inspect busybox:latest
[
{
"Id": "8c2e06607696bd4afb3d03b687e361cc43cf8ec1a4a725bc96e39f05ba97dd55",
"Parent": "6ce2e90b0bc7224de3db1f0d646fe8e2c4dd37f1793928287f6074bc451a57ea",
"Comment": "",
"Created": "2015-04-17T22:01:13.062208605Z",
"Container": "811003e0012ef6e6db039bcef852098d45cf9f84e995efb93a176a11e9aca6b9",
"ContainerConfig": {
"Hostname": "19bbb9ebab4d",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": null,
"PublishService": "",
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": null,
"Cmd": [
"/bin/sh",
"-c",
"#(nop) CMD [\"/bin/sh\"]"
],
"DockerVersion": "1.6.0",
"Author": "Jevome Petazzoni \u003cjerome@docker.com\u003e",
"Config": {
"Hostname": "19bbb9ebab4d",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": null,
"PublishService": "",
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": null,
"Cmd": [
"/bin/sh"
],
"Architecture": "amd64",
"Os": "linux",
"Size": 0,
"VirtualSize": 2433303,
"GraphDriver": {
"Name": "aufs",
"Data": null
}
}
]
对于上面的输出,有几项需要重点说明一下:
- Id:Image的ID。通过上面的讨论,可以看到image ID实际上只是最上层的layer ID,所以docker inspect也适用于任意一层layer。
- Parent:该layer的父层,可以递归地获得某个image的所有layer信息。
- Comment:非常类似于Git的commit message,可以为该层做一些历史记录,方便其他人理解。
- Container:这个条目比较有意思,其中包含哲学的味道。比如前面提到容器的启动需要以image为模板。但又可以把该容器保存为镜像,所以一般来说image的每个layer都保存自一个容器,所以该容器可以说是image layer的“模板”。
- Config:包含了该image的一些配置信息,其中比较重要的是:“env”容器启动时会作为容器的环境变量;“Cmd”作为容器启动时的默认命令;“Labels”参数可以用于docker images命令过滤。
- Architecture:该image对应的CPU体系结构。现在Docker官方支持amd64,对其他体系架构的支持也在进行中。
通过这些元数据信息,可以得到某个image包含的所有layer,进而组合出容器的rootfs,再加上元数据中的配置信息(环境变量、启动参数、体系架构等)作为容器启动时的参数。至此已经具备启动容器必需的所有信息。
Docker镜像相关概念
据Docker官网的技术文档描述,Image(镜像)是Docker术语的一种,代表一个只读的layer。而layer则具体代表Docker Container文件系统中可叠加的一部分。 那么理解之前,先让我们来认识一下与Docker镜像相关的4个概念:rootfs、Union mount、image以及layer。
rootfs
Rootfs:代表一个Docker Container在启动时(而非运行后)其内部进程可见的文件系统视角,或者是Docker Container的根目录。当然,该目录下含有Docker Container所需要的系统文件、工具、容器文件等。
传统来说,Linux操作系统内核启动时,内核首先会挂载一个只读(read-only)的rootfs,当系统检测其完整性之后,决定是否将其切换为读写(read-write)模式,或者最后在rootfs之上另行挂载一种文件系统并忽略rootfs。Docker架构下,依然沿用Linux中rootfs的思想。当Docker Daemon为Docker Container挂载rootfs的时候,与传统Linux内核类似,将其设定为只读(read-only)模式。在rootfs挂载完毕之后,和Linux内核不一样的是,Docker Daemon没有将Docker Container的文件系统设为读写(read-write)模式,而是利用Union mount的技术,在这个只读的rootfs之上再挂载一个读写(read-write)的文件系统,挂载时该读写(read-write)文件系统内空无一物。
举一个Ubuntu容器启动的例子。假设用户已经通过Docker Registry下拉了Ubuntu:14.04的镜像,并通过命令docker run –it ubuntu:14.04 /bin/bash将其启动运行。则Docker Daemon为其创建的rootfs以及容器可读写的文件系统
drwxr-xr-x 1 root root 4096 Feb 13 01:54 .
drwxr-xr-x 1 root root 4096 Feb 13 01:54 ..
-rwxr-xr-x 1 root root 0 Feb 13 01:54 .dockerenv
lrwxrwxrwx 1 root root 7 Nov 1 21:15 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Apr 18 2022 boot
drwxr-xr-x 5 root root 360 Feb 13 01:54 dev
drwxr-xr-x 1 root root 4096 Feb 13 01:54 etc
drwxr-xr-x 2 root root 4096 Apr 18 2022 home
lrwxrwxrwx 1 root root 7 Nov 1 21:15 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Nov 1 21:15 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 Nov 1 21:15 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 Nov 1 21:15 libx32 -> usr/libx32
drwxr-xr-x 2 root root 4096 Nov 1 21:15 media
drwxr-xr-x 2 root root 4096 Nov 1 21:15 mnt
drwxr-xr-x 2 root root 4096 Nov 1 21:15 opt
dr-xr-xr-x 399 root root 0 Feb 13 01:54 proc
drwx------ 2 root root 4096 Nov 1 21:18 root
drwxr-xr-x 5 root root 4096 Nov 1 21:18 run
lrwxrwxrwx 1 root root 8 Nov 1 21:15 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Nov 1 21:15 srv
dr-xr-xr-x 11 root root 0 Feb 13 01:54 sys
drwxrwxrwt 2 root root 4096 Nov 1 21:18 tmp
drwxr-xr-x 14 root root 4096 Nov 1 21:15 usr
drwxr-xr-x 11 root root 4096 Nov 1 21:18 var
正如read-only和read-write的含义那样,该容器中的进程对rootfs中的内容只拥有读权限,对于read-write读写文件系统中的内容既拥有读权限也拥有写权限。容器虽然只有一个文件系统,但该文件系统由“两层”组成,分别为读写文件系统和只读文件系统。这样的理解已然有些层级(layer)的意味。 简单来讲,可以将Docker Container的文件系统分为两部分,而上文提到是Docker Daemon利用Union Mount的技术,将两者挂载。那么Union mount又是一种怎样的技术?
Union mount
Union mount:代表一种文件系统挂载的方式,允许同一时刻多种文件系统挂载在一起,并以一种文件系统的形式,呈现多种文件系统内容合并后的目录。
一般情况下,通过某种文件系统挂载内容至挂载点的话,挂载点目录中原先的内容将会被隐藏。而Union mount则不会将挂载点目录中的内容隐藏,反而是将挂载点目录中的内容和被挂载的内容合并,并为合并后的内容提供一个统一独立的文件系统视角。通常来讲,被合并的文件系统中只有一个会以读写(read-write)模式挂载,而其他的文件系统的挂载模式均为只读(read-only)。实现这种Union mount技术的文件系统一般被称为Union Filesystem,较为常见的有UnionFS、AUFS、OverlayFS等。
Docker实现容器文件系统Union mount时,提供多种具体的文件系统解决方案,如Docker早版本沿用至今的的AUFS,还有在docker 1.4.0版本中开始支持的OverlayFS等。 更深入的了解Union mount,可以使用AUFS文件系统来进一步阐述上文中ubuntu:14.04容器文件系统的例子。
使用镜像ubuntu:14.04创建的容器中,可以暂且将该容器整个rootfs当成是一个文件系统。上文也提到,挂载时读写(read-write)文件系统中空无一物。既然如此,从用户视角来看,容器内文件系统和rootfs完全一样,用户完全可以按照往常习惯,无差别的使用自身视角下文件系统中的所有内容;然而,从内核的角度来看,两者在有着非常大的区别。追溯区别存在的根本原因,那就不得不提及AUFS等文件系统的COW(copy-on-write)特性。
COW文件系统和其他文件系统最大的区别就是:从不覆写已有文件系统中已有的内容。由于通过COW文件系统将两个文件系统(rootfs和read-write filesystem)合并,最终用户视角为合并后的含有所有内容的文件系统,然而在Linux内核逻辑上依然可以区别两者,那就是用户对原先rootfs中的内容拥有只读权限,而对read-write filesystem中的内容拥有读写权限。
既然对用户而言,全然不知哪些内容只读,哪些内容可读写,这些信息只有内核在接管,那么假设用户需要更新其视角下的文件/etc/hosts,而该文件又恰巧是rootfs只读文件系统中的内容,内核是否会抛出异常或者驳回用户请求呢?答案是否定的。当此情形发生时,COW文件系统首先不会覆写read-only文件系统中的文件,即不会覆写rootfs中/etc/hosts,其次反而会将该文件拷贝至读写文件系统中,即拷贝至读写文件系统中的/etc/hosts,最后再对后者进行更新操作。如此一来,纵使rootfs与read-write filesystem中均由/etc/ hosts,诸如AUFS类型的COW文件系统也能保证用户视角中只能看到read-write filesystem中的/etc/hosts,即更新后的内容。
当然,这样的特性同样支持rootfs中文件的删除等其他操作。例如:用户通过apt-get软件包管理工具安装Golang,所有与Golang相关的内容都会被安装在读写文件系统中,而不会安装在rootfs。此时用户又希望通过apt-get软件包管理工具删除所有关于MySQL的内容,恰巧这部分内容又都存在于rootfs中时,删除操作执行时同样不会删除rootfs实际存在的MySQL,而是在read-write filesystem中删除该部分内容,导致最终rootfs中的MySQL对容器用户不可见,也不可访。 掌握Docker中rootfs以及Union mount的概念之后,再来理解Docker镜像,就会变得水到渠成。
image
Docker中rootfs的概念,起到容器文件系统中基石的作用。对于容器而言,其只读的特性,也是不难理解。神奇的是,实际情况下Docker的rootfs设计与实现比上文的描述还要精妙不少。
继续以ubuntu 14.04为例,虽然通过AUFS可以实现rootfs与read-write filesystem的合并,但是考虑到rootfs自身接近200MB的磁盘大小,如果以这个rootfs的粒度来实现容器的创建与迁移等,是否会稍显笨重,同时也会大大降低镜像的灵活性。而且,若用户希望拥有一个ubuntu 14.10的rootfs,那么是否有必要创建一个全新的rootfs,毕竟ubuntu 14.10和ubuntu 14.04的rootfs中有很多一致的内容。
Docker中image的概念,非常巧妙的解决了以上的问题。最为简单的解释image,就是 Docker容器中只读文件系统rootfs的一部分。换言之,实际上Docker容器的rootfs可以由多个image来构成。多个image构成rootfs的方式依然沿用Union mount技术。
基于以上的概念,Docker Image中又抽象出两种概念:Parent Image以及Base Image。除了容器rootfs最底层的image,其余image都依赖于其底下的一个或多个image,而Docker中将下一层的image称为上一层image的Parent Image。
通过image的形式,原先较为臃肿的rootfs被逐渐打散成轻便的多层。Image除了轻便的特性,同时还有上文提到的只读特性,如此一来,在不同的容器、不同的rootfs中image完全可以用来复用。
layer
Docker术语中,layer是一个与image含义较为相近的词。容器镜像的rootfs是容器只读的文件系统,rootfs又是由多个只读的image构成。于是,rootfs中每个只读的image都可以称为一层layer。
除了只读的image之外,Docker Daemon在创建容器时会在容器的rootfs之上,再mount一层read-write filesystem,而这一层文件系统,也称为容器的一层layer,常被称为top layer。
因此,总结而言,Docker容器中的每一层只读的image,以及最上层可读写的文件系统,均被称为layer。如此一来,layer的范畴比image多了一层,即多包含了最上层的read-write filesystem。 有了layer的概念,大家可以思考这样一个问题:容器文件系统分为只读的rootfs,以及可读写的top layer,那么容器运行时若在top layer中写入了内容,那这些内容是否可以持久化,并且也被其它容器复用?
上文对于image的分析中,提到了image有复用的特性,既然如此,再提一个更为大胆的假设:容器的top layer是否可以转变为image? 答案是肯定的。Docker的设计理念中,top layer转变为image的行为(Docker中称为commit操作),大大释放了容器rootfs的灵活性。Docker的开发者完全可以基于某个镜像创建容器做开发工作,并且无论在开发周期的哪个时间点,都可以对容器进行commit,将所有top layer中的内容打包为一个image,构成一个新的镜像。Commit完毕之后,用户完全可以基于新的镜像,进行开发、分发、测试、部署等。不仅docker commit的原理如此,基于Dockerfile的docker build,其追核心的思想,也是不断将容器的top layer转化为image。
docker镜像原理图
[容器层] =》 应用程序,
[基础镜像] =》基础镜像依赖包,例如JDK Tomcat等
[Rootfs] =》Docker获取基础镜像容器内Linux发行版
[bootfs] =》宿主机提供的Linux内核
下面详细解释一下图中各个分层:
- 首先我们看最底层的bootfs:Bootfs全名boot-file system,即引导文件系统;主要包含bootloader(系统加载)和kernel(内核);bootloader主要用于引导加载kernel,kernel内核主要是宿主机提供的linux内核。Linux刚启动时会加载bootfs文件系统,也就是加载宿主机的linux内核。所以,我们的docker容器在运行时,第一步就是加载宿主机的linux内核。linux内核加载完成后,就会启动第二层。
- 第二层是叫Rootfs,即root-file system;Rootfs在bootfs之上,包含的就是典型的linux系统中/dev、/proc、/etc等标准目录和文件;rootfs就是各种不同操作系统的发行版,比如ubuntu、centos等;所以,第二步就是由Rootfs,负责docker获取基础镜像;即进行完第二步,我们就获取到了基础的linux发行版(比如是centos还是ubuntu等),例如我们本例中获取的就是centos发行版。
- 第三层是依赖层,我们在依赖层就可以定制化安装我们所需要的各种依赖环境了;此容器是用来运行什么的,我们就得安装什么依赖了。比如我们的镜像是用来运行tomcat的,而tomcat想要运行,就必须得先有jdk环境(java环境),即第三层安装jdk,安装完jdk后才能去第四层安装tomcat;tomcat也装好了后,必须得放入文件才能运行,这个文件时需要我们自己写的;所以最后一层就是容器层(container层)。
- 最后一层是容器层(container层),它的作用是就是本容器所要实现的具体功能了;比如在本例中就是运行具体的tomcat程序。
参考资料
http://www.csdn.net/article/2014-09-24⁄2821832
http://en.wikipedia.org/wiki/Cgroups
http://www.infoq.com/cn/articles/docker-future
https://docs.docker.com/terms/image/
https://docs.docker.com/terms/layer/#layer
http://en.wikipedia.org/wiki/Union_mount
https://www.usenix.org/legacy/publications/library/proceedings/neworl/full_papers/mckusick.a
http://www.qnx.com/developers/docs/660/index.jsp?topic=%2Fcom.qnx.doc.neutrino.sys_arch%2Ftopic%2Ffsys_COW_filesystem.html