转自:1 :   https://www.csdn.net/article/2015-08-21/2825511

2:    http://blog.51cto.com/wzlinux/2044797

第一部分:Docker镜像的基本知识

1.1 什么是Docker镜像

从整体的角度来讲,一个完整的Docker镜像可以支撑一个Docker容器的运行,在 Docker容器运行过程中主要提供文件系统视角。例如一个ubuntu:14.04的镜像,提供了一个基本的ubuntu:14.04的发行版,当然此 镜像是不包含操作系统Linux内核的。

以上内容是从宏观的角度看看Docker镜像是什么,我们再从微观的角度进一步深入 Docker镜像。刚才提到了“Debian镜像中安装MySQL 5.6,就成了mysql:5.6镜像”,其实在此时Docker镜像的层级概念就体现出来了。底层一个Debian操作系统镜像,上面叠加一个 mysql层,就完成了一个mysql镜像的构建。层级概念就不难理解,此时我们一般debian操作系统镜像称为mysql镜像层的父镜像。

Docker有两方面的技术非常重要,第一是Linux 容器方面的技术,第二是Docker镜像的技术。从技术本身来讲,两者的可复制性很强,不存在绝对的技术难点,然而Docker Hub由于存在大量的数据的原因,导致Docker Hub的可复制性几乎不存在,这需要一个生态的营造。

1.2 Docker镜像的内容

大致介绍了Docker镜像是什么,我们来看看Docker镜像中有哪些内容?

介绍之前,我先分享一下,我个人在接触Docker的两年时间中,对Docker镜像内容认识的变化。

第一阶段:初步接触Docker。相信很多爱好者都会和我一样,有这样一个认识:Docker 镜像代表一个容器的文件系统内容;

第二阶段:初步接触联合文件系统。联合文件系统的概念,让我意识到镜像层级管理的技术,每一层镜像都是容器文件系统内容的一部分。

第三阶段:研究镜像与容器的关系:容器是一个动态的环境,每一层镜像中的文件属于静态内 容,然而 Dockerfile 中的 ENV、VOLUME、CMD 等内容最终都需要落实到容器的运行环境中,而这些内容均不可能直接坐落到每一层镜像所包含的文件系统内容中,那此时每一个Docker镜像还会包含 json文件记录与容器之间的关系。

因此,Docker镜像的内容主要包含两个部分:第一,镜像层文件内容;第二,镜像json文件。

1.3 Docker镜像存储位置

既然是说镜像存储的位置,那么应该包含:镜像层文件和镜像json文件。如一个ubuntu:14.04镜像,包含4个镜像层,在aufs存储驱动的情况下,在磁盘上的情况可以如以下图所示:

1.3.1 查看镜像层组成:

我们可以通过命令 docker history ubuntu:14.04 查看 ubuntu:14.04,结果如下:

1.3.2 镜像层文件内容存储

Docker 镜像层的内容一般在 Docker 根目录的 aufs 路径下,为 /var/lib/docker/aufs/diff/,具体情况如下:

图中显示了镜像 ubuntu:14.04 的 4 个镜像层内容,以及每个镜像层内的一级目录情况。需要额外注意的是:镜像层 d2a0ecffe6fa 中没有任何内容,也就是所谓的空镜像。

1.3.3 镜像 json 文件存储

对于每一个镜像层,Docker 都会保存一份相应的 json 文件,json 文件的存储路径为 /var/lib/docker/graph,ubuntu:14.04 所有镜像层的 json 文件存储路径展示如下:

除了 json 文件,大家还看到每一个镜像层还包含一个 layersize 文件,该文件主要记录镜像层内部文件内容的总大小。既然谈到了镜像 json 文件,为了给下文铺垫,以下贴出 ubuntu:14.04 中空镜像层 d2a0ecffe6fa 的 json 文件:

Docker镜像存储,就和大家一起先看到这。同时介绍Docker镜像的基本知识也告一段落。以下我们进入此次分享的第二部分。

第二部分 Dockerfile、Docker镜像和Docker容器的关系

Dockerfile 是软件的原材料,Docker 镜像是软件的交付品,而 Docker 容器则可以认为是软件的运行态。从应用软件的角度来看,Dockerfile、Docker 镜像与 Docker 容器分别代表软件的三个不同阶段,Dockerfile 面向开发,Docker 镜像成为交付标准,Docker 容器则涉及部署与运维,三者缺一不可,合力充当 Docker 体系的基石。

简单来讲,Dockerfile构建出Docker镜像,通过Docker镜像运行Docker容器。

我们可以从Docker容器的角度,来反推三者的关系。首先可以来看下图:

我们假设这个容器的镜像通过以下Dockerfile构建而得:

FROM ubuntu:14.04  

ADD run.sh /

VOLUME /data

CMD ["./run.sh"]  

2.1 Dockerfile与Docker镜像

首先,我们结合上图来看看Dockerfile与Docker镜像之间的关系。

FROM ubuntu:14.04:设置基础镜像,此时会使用基础镜像 ubuntu:14.04 的所有镜像层,为简单起见,图中将其作为一个整体展示。

ADD run.sh /:将 Dockerfile 所在目录的文件 run.sh 加至镜像的根目录,此时新一层的镜像只有一项内容,即根目录下的 run.sh。

VOLUME /data:设定镜像的 VOLUME,此 VOLUME 在容器内部的路径为 /data。需要注意的是,此时并未在新一层的镜像中添加任何文件,即构建出的磁层镜像中文件为空,但更新了镜像的 json 文件,以便通过此镜像启动容器时获取这方面的信息。

CMD ["./run.sh"]:设置镜像的默认执行入口,此命令同样不会在新建镜像中添加任何文件,仅仅在上一层镜像 json 文件的基础上更新新建镜像的 json 文件。

因此,通过以上分析,以上的Dockerfile可以构建出一个新的镜像,包含4个镜像层,每一条命令会和一个镜像层对应,镜像之间会存在父子关系。图中很清楚的表明了这些关系。

2.2 Docker镜像与Docker容器的关系

Docker镜像是Docker容器运行的基础,没有Docker镜像,就不可能有Docker容器,这也是Docker的设计原则之一。

可以理解的是:Docker镜像毕竟是镜像,属于静态的内容;而Docker容器就不一样了,容器属于动态的内容。动态的内容,大家很容易联想到进程,内存,CPU等之类的东西。的确,Docker容器作为动态的内容,都会包含这些。

为了便于理解,大家可以把Docker容器,理解为一个或多个运行进程,而这些运行进程将占有相应的内存,相应的CPU计算资源,相应的虚拟网络设备以及相应的文件系统资源。而Docker容器所占用的文件系统资源,则通过Docker镜像的镜像层文件来提供。

那么作为静态的镜像,如何才有能力转化为一个动态的Docker容器呢?此时,我们可以想象:第一,转化的依据是什么;第二,由谁来执行这个转化操作。

其实,转化的依据是每个镜像的json文件,Docker可以通过解析Docker镜像的json的文件,获知应该在这个镜像之上运行什么样的进程,应该为进程配置怎么样的环境变量,此时也就实现了静态向动态的转变。

谁来执行这个转化工作?答案是Docker守护进程。也许大家早就理解这样一句 话:Docker容器实质上就是一个或者多个进程,而容器的父进程就是Docker守护进程。这样的,转化工作的执行就不难理解了:Docker守护进程 手握Docker镜像的json文件,为容器配置相应的环境,并真正运行Docker镜像所指定的进程,完成Docker容器的真正创建。

Docker容器运行起来之后,Docker镜像json文件就失去作用了。此时Docker镜像的绝大部分作用就是:为Docker容器提供一个文件系统的视角,供容器内部的进程访问文件资源。

再次回到上图,我们再来看看容器和镜像之间的一些特殊关系。首先,之前已经提及Docker镜像是分层管理的,管理Docker容器的时候,Docker镜像仍然是分层管理的。由于此时动态的容器中已经存在进程,进程就会对文件系统视角内的文件进行读写操作,因此,就会涉及一个问题:容器是否会篡改Docker镜像的内容?

答案自然是不会的。统一来讲,正如上图,所有的Docker镜像层对于容器来说,都是只读的,容器对于文件的写操作绝对不会作用在镜像中。

既然如此,实现的原理就很重要,究其根本:Docker守护进程会在Docker镜像的 最上层之上,再添加一个可读写层,容器所有的写操作都会作用到这一层中。而如果Docker容器需要写底层Docker镜像中的文件,那么此时就会涉及一 个叫Copy-on-Write的机制,即aufs等联合文件系统保证:首先将此文件从Docker镜像层中拷贝至最上层的可读写层,然后容器进程再对读 写层中的副本进行写操纵。对于容器进程来讲,它只能看到最上层的文件。

那最后我们再来说说:Docker容器的文件系统视角中,到底是不是存在一些内容,不是存储于Docker镜像中的?

这次的答案依旧是肯定的。

再次重申一点,Docker镜像中存储的都是一些静态文件。这些文件原则上应该和容器具体信息以及主机信息完全解藕。那么Docker容器中不存在Docker镜像中的内容主要有以下几点:

1./proc以及/sys等虚拟文件系统的内容

2.容器的hosts文件,hostname文件以及resolv.conf文件,这些事具体环境的信息,原则上的确不应该被打入镜像。

3.容器的Volume路径,这部分的视角来源于从宿主机上挂载到容器内部的路径

4.部分的设备文件

QA选集:

问:为什么一个ubuntu:14.04镜像的镜像层的数量是4个,前三层的内容似乎有相同的,如etc?

孙宏亮:这一点,细心的大家肯定发现了。首先,虽然三层 都有,但是会存在两种情况,etc的子目录下有相同路径的文件,那么上层的会覆盖下层的文件;如果内部的文件路径不相同,那么都会存在,都会呈现给最上 层。[可别较真,说目录也是文件哈,意会]

问:关于docker安全性问题,对于安全是怎样处理的,如果我从hub下载镜像,能判别是否安全么2.层级之间的依赖会导致一个崩了整个docker 都崩了么?

孙宏亮:从流程上来讲,如果一切可控的话,我认为是安全的。但是依然会存在一些隐患,比如Dockerfile中基于的base images是否完全受信;镜像的传输过程是否受信;自己的private docker resgitry的安全级别达到什么样的层次,这些都有影响。

问:如何保证仅有的一个deamon的稳定性健壮性?

孙宏亮:这个问题首先需要知道docker daemon的稳定性在哪些方面,那种场景下比较差?的确,docker daemon存在弊病。比如,daemon和容器的耦合等,目前general来讲,docker daemon保证绝对的稳定应该还做不到。

问:生产环境中怎么用docker备份mysql数据?

孙宏亮:数据存储上docker,我目前的建议是:三思。举个简单的例子,官方的mysql镜像运行出来的 容器,密码是明文的,明文的密码存在于:docker inspect container_name, container.json文件中,容器的环境变量中,甚至在日志文件中都会存在,just think about it。当然也有办法解决,缓解这种情况。

问:如果是多层构建,中间的一个层做了升级或者bugfix,会潜在影响上层吧?

孙宏亮:这个bugfix会在上层有体现,但是使用效果是不会有影响的,还有之前的bug会永远留在下层,但是没有影响。

///********************************************************************************************************///

一、base镜像

base 镜像简单来说就是不依赖其他任何镜像,完全从0开始建起,其他镜像都是建立在他的之上,可以比喻为大楼的地基,docker镜像的鼻祖。

base 镜像有两层含义:(1)不依赖其他镜像,从 scratch 构建;(2)其他镜像可以之为基础进行扩展。

所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。

我们以 CentOS 为例查看 base 镜像包含哪些内容。

下载及查看镜像:


  1. root@ubuntu:~# docker pull centos
  2. Using default tag: latest
  3. latest: Pulling from library/centos
  4. d9aaf4d82f24: Pull complete
  5. Digest: sha256:4565fe2dd7f4770e825d4bd9c761a81b26e49cc9e3c9631c58cfc3188be9505a
  6. Status: Downloaded newer image for centos:latest

  1. root@ubuntu:~# docker images
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. centos latest d123f4e55e12 3 weeks ago 197MB
  4. ubuntu 12.04 5b117edd0b76 7 months ago 104MB

我们看到CentOS的镜像大小不到200MB,平时我们安装一个CentOS至少是几个GB,怎么可能才 200MB !

下面我们来解释这个问题,Linux 操作系统由内核空间和用户空间组成。

典型的Linux启动到运行需要两个FS,bootfs + rootfs,如下图:

1、rootfs

内核空间是 kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。用户空间的文件系统是 rootfs,包含我们熟悉的 /dev, /proc, /bin 等目录。

对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。

而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux 发行版,CentOS 的 rootfs 已经算臃肿的了,alpine 还不到 10MB。

我们平时安装的 CentOS 除了 rootfs 还会选装很多软件、服务、图形桌面等,需要好几个 GB 就不足为奇了。

2、base 镜像提供的是最小安装的 Linux 发行版。

下面是 CentOS 镜像的 Dockerfile 的内容:


  1. FROM scratch
  2. ADD centos-7-docker.tar.xz /
  3. LABEL name="CentOS Base Image" </div>
  4. vendor="CentOS" </div>
  5. license="GPLv2" </div>
  6. build-date="20170911"
  7. CMD ["/bin/bash"]

第二行 ADD 指令添加到镜像的 tar 包就是 CentOS 7 的 rootfs。在制作镜像时,这个 tar 包会自动解压到 / 目录下,生成 /dev, /porc, /bin 等目录。

3、支持运行多种 Linux OS

bootfs (boot file system) 主要包含 bootloader 和 kernel, bootloader主要是引导加载kernel, 当boot成功后 kernel 被加载到内存中后 bootfs就被umount了.

rootfs (root file system) 包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。

由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs。

比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。

所以 Docker 可以同时支持多种 Linux镜像,模拟出多种操作系统环境。


上图 Debian 和 BusyBox(一种嵌入式 Linux)上层提供各自的 rootfs,底层共用 Docker Host 的 kernel。

这里需要说明的是:

base 镜像只是在用户空间与发行版一致,kernel 版本与发型版是不同的。

CentOS 7 使用 3.x.x 的 kernel,如果 Docker Host 是 Ubuntu 16.04,那么在 CentOS 容器中使用的实际是是 Host 4.x.x 的 kernel。


  1. root@ubuntu:~# uname -r
  2. 4.4.0-62-generic
  3. root@ubuntu:~# docker run -ti centos /bin/bash
  4. [root@06f13ef13853 /]# uname -r
  5. 4.4.0-62-generic

容器只能使用 Host 的 kernel,并且不能修改。

所有容器都共用 host 的 kernel,在容器中没办法对 kernel 升级。如果容器对 kernel 版本有要求(比如应用只能在某个 kernel 版本下运行),则不建议用容器,这种场景虚拟机可能更合适。

二、镜像的分层结构

Docker 支持通过扩展现有镜像,创建新的镜像。

实际上,Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,Dockerfile 如下:


  1. # Version: 0.0.1
  2. FROM debian 1.新镜像不再是从 scratch 开始,而是直接在 Debian base 镜像上构建。
  3. MAINTAINER wzlinux
  4. RUN apt-get update && apt-get install -y emacs 2.安装 emacs 编辑器。
  5. RUN apt-get install -y apache2 3.安装 apache2。
  6. CMD ["/bin/bash"] 4.容器启动时运行 bash。

构建过程如下图所示:


可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

问什么 Docker 镜像要采用这种分层结构呢?

最大的一个好处就是 - 共享资源。

比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。

这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?

答案是不会!
修改会被限制在单个容器内。
这就是我们接下来要说的容器 Copy-on-Write 特性。

  1. 新数据会直接存放在最上面的容器层。
  2. 修改现有数据会先从镜像层将数据复制到容器层,修改后的数据直接保存在容器层中,镜像层保持不变。
  3. 如果多个层中有命名相同的文件,用户只能看到最上面那层中的文件。

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。

典型的Linux在启动后,首先将 rootfs 置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite” 供用户使用。在docker中,起初也是将 rootfs 以readonly方式加载并检查,然而接下来利用 union mount 的将一个 readwrite 文件系统挂载在 readonly 的rootfs之上,并且允许再次将下层的 file system设定为readonly 并且向上叠加, 这样一组readonly和一个writeable的结构构成一个container的运行目录, 每一个被称作一个Layer。如下图所示。


所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。

下面我们深入讨论容器层的细节。

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。

  1. 添加文件:在容器中创建文件时,新文件被添加到容器层中。
  2. 读取文件:在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存。
  3. 修改文件:在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
  4. 删除文件:在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。

只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。

参考文档:<http://developer.51cto.com/art/201312/424374_all.htm>;
<https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/>;