理解docker镜像,容器和存储驱动

一. 镜像

镜像作为docker中最基本的概念,有以下几个特性:

  1. 分层,每个镜像都由一个或多个镜像层组成
  2. 可通过在某个镜像加上一定的镜像层得到新镜像(此过程可通过编写dockerfile或在容器中commit实现)
  3. 每个都分层拥有唯一ID
  4. 镜像在存储和使用时共享相同的镜像层(根据ID),所以在pull镜像时,已有的镜像层会自动跳过下载
  5. 每个镜像层都是只读,即使启动成容器,也无法对其真正的修改,修改只会作用于最上层的容器层 

二. 镜像 vs容器

rdocker镜像保留版本数量 docker镜像存储驱动_rdocker镜像保留版本数量

 

  • 镜像加上一层可读写层(容器层)组成该镜像对应容器的文件系统
  • 在容器中通过commit可将最上层的可写层固化成镜像层,形成新镜像。此时,生成的新镜像与旧镜像共享底层镜像层,所以在宿主机上只不过是多了个新的镜像层

 

rdocker镜像保留版本数量 docker镜像存储驱动_存储驱动_02

 

 

 

三.  Copy-On-Write,COW

 

       写时拷贝,多个程序共享同一份文件,读操作时对这份文件无影响,当程序需要进行写操作时才把要写的部分拷贝一份给对应程序。COW只是一种策略,有多种方法去实现。根据这种思想可以大大减少存储冗余。docker实现了COW策略,所以容器会共享相同的镜像层,如下图(同一个镜像启动成多个容器时)


 

rdocker镜像保留版本数量 docker镜像存储驱动_docker_03

 

生成新镜像的方法有,从容器中commit,或者使用dockerfile。dockerfile其实本质是也是启动容器去commit,只是这些操作都是docker自动化去完成。

 

dockerfile中每一个命令就对应生成一个镜像,即每个命令对应一个镜像层,如下图

rdocker镜像保留版本数量 docker镜像存储驱动_rdocker镜像保留版本数量_04

为什么要这么细粒度的划分层是因为,细粒度的镜像层被共用到的几率越大。举个极端的例子,如果把基于一个ubuntu镜像上所做的操作都归到一层的话,那么这一层共用到的几率几乎为零,因为定制化程度太高,每个人所需的环境不同。

 

四.  docker storage driver

 

         docker将容器的一些底层运行环境进行抽象,从复杂的实现中分离出了driver组件,让用户得以专注于业务相关的容器使用。用户可以通过选定不同的driver组件来对docker中容器大环境的定制,具体的实现操作则交由driver去进行。

          driver分为三类:

  1. exec-driver,实现对容器运行环境的隔离和资源的限制,可选方案:lxc或native(libcontainer);
  2. network-driver,实现容器网络相关的操作;
  3. graph-driver,实现镜像存储相关操作,可选方案:devicemapper,aufs,btrfs,zfs等。

 

storage driver,docker中的一个组件,也称graphdriver。

         功能:

  1. 镜像的存储管理,包括从hub上pull的镜像和通过dockerfile生成的新镜像等;
  2. 容器启动时,为容器准备文件系统

 

        对于graphdriver选择何种方案去实现,并不影响docker功能方面的使用。每种方案都能实现镜像的分层结构和COW策略,但不同的方案对于docker的存储速度和稳定性是有影响的。目前相对比较成熟的有,aufs和devicemapper(WAE使用中)

 

下图为各个方案的特点:

rdocker镜像保留版本数量 docker镜像存储驱动_镜像_05

官网对于选何种graphdriver的建议:https://docs.docker.com/engine/userguide/storagedriver/selectadriver/

 

 

五.  devicemapper

 

     基于linux内核中的DeviceMapper框架实现,该框架实现了物理存储设备与虚拟存储设备的映射,或虚拟设备与虚拟设备之间的映射。这使得用户能够用该框架来实现磁盘的自由、动态的划分。LVM2,software RAIDs和dm-cryptdisk encryption等都是基于该框架的实现。更为重要的是,Device Mapper框架中提供两个功能,thin-provisioning和snapshot。

 

        thin-provisioning,类似于虚拟内存,提供给使用者的空间只有在使用者进行写操作时才真正进行分配。假设使用者有一块100g的thin块设备,使用了20g,实际系统提供的大小就是20g,只有当使用者进行存储操作时才分配更多的空间,直到100g。

        snapshot,快照,是一种COW策略的实现,假设从A设备做快照得到B设备时,并未对A进行完整拷贝,而是当对B设备进行写操作时,才将需要改变的那部分做属于B设备的拷贝。

 

         devicemapper利用了该框架中的thin-provisioning和snapshot实现了镜像的分层结构和存储优化。在用devicemapper作为graphdriver的docker中,每个镜像和容器都对应一个设备,通过对镜像做snapshot操作得到容器,所以容器中拥有镜像的内容且操作这些内容不影响镜像本身,因为容器和镜像对应不同的设备。

 

       devicemapper有两种模式可选,loop-lvm和direct-lvm。devicemapper的大致思路是,先通过虚拟化技术得到一个thin-pool设备(可理解成一个资源池),接着在thin-pool上建立一个基础设备,此后docker上所有镜像和容器都是基于此设备的snapshot。两个模式的区别就在与建立thin-pool的方法不同。

 

loop-lvm:

  1. 创建两个稀疏文件data和metadata
  2. 将这两个文件映射成两个块设备(loopback块设备)
  3. 将两个设备通过内核中Device Mapper映射成thin-pool

注意!该模式不推荐用于生产环境,因为该模式在宿主机跑高密度容器数量的话,性能下降急剧,生产环境中一台宿主机不可能只跑单个容器


direct-lvm:

(利用了基于Device Mapper的LVM)


  1. 将空余块设备(可以是分区)创建成physical volume(pv)
  2. 在由这些PV组成volume group(vg)
  3. 从vg中建立两个logical volume(lv),data和matedata
  4. 将data和matedata映射成thin-pool


*这些步骤可由已有脚本docker-storage-setup自动执行,docker启动前会自动执行该脚本,脚本位置/usr/bin/docker-storage-setup,只要在对应配置文件中提供用于创建VG的PV(对应配置文件中DEVS),或者提供已有的VG,便可让脚本自动执行上述步骤。具体可通过service docker-storage-setup status查看,对应执行脚本内有使用说明。也可手动建立(Configure direct-lvm mode forproduction)

 

以下通过一些命令区别两种模式:

loop-lvm:

docker详情


rdocker镜像保留版本数量 docker镜像存储驱动_docker_06

列出块设备

rdocker镜像保留版本数量 docker镜像存储驱动_容器_07

 

 

direct-lvm模式:

 docker详情

rdocker镜像保留版本数量 docker镜像存储驱动_rdocker镜像保留版本数量_08

列出块设备

rdocker镜像保留版本数量 docker镜像存储驱动_镜像_09

列出所有logical volume的信息

rdocker镜像保留版本数量 docker镜像存储驱动_容器_10

六. 目录相关

宿主机上容器、镜像等存储相关的主目录:/var/lib/docker

 

[root@bogon docker]# tree ./
 ./
├── containers                              <---容器相关
 │   ├──f1a04d3537edd5741f39ff262277362b35bbf17776fcafa31e67412f97e4d9f9
 │   │   ├── config.json
 │   │   ├──f1a04d3537edd5741f39ff262277362b35bbf17776fcafa31e67412f97e4d9f9-json.log
 │   │   ├── hostconfig.json
 │   │   ├── hostname
 │   │   ├── hosts
 │   │   ├── resolv.conf
 │   │   ├── resolv.conf.hash
 │   │   └── secrets
├── devicemapper                       <---存储驱动相关
│   ├── metadata                          <---镜像、容器对应块设备的元数据
 │   │   ├──0288ae931294ce04f5d69c60146faca7d9be8de4004421d650f4227fa60bd92b
 │   │   ├──05c2ea2d81bceeddc2881beb63d2ee26537593de9f47b4613c0d5afb1353f7df
 │   │   ├──f1a04d3537edd5741f39ff262277362b35bbf17776fcafa31e67412f97e4d9f9
 │   │   ├──f1a04d3537edd5741f39ff262277362b35bbf17776fcafa31e67412f97e4d9f9-init
 │   │   ├──f6d195bba85ba6b9c075186ee62fe56f5d3b27e29ad9a31da8753820f3e2586b
 │   │   ├──f6d195bba85ba6b9c075186ee62fe56f5d3b27e29ad9a31da8753820f3e2586b-init
 │   │   ├── base
 │   │   ├── deviceset-metadata
 │   │   └── transaction-metadata
│   └── mnt                              <---容器对应设备的挂载目录,docker会自动mount和umount,所以手动打开为空,可自行mount,对应设备在/dev/mapper/
 │       ├──0288ae931294ce04f5d69c60146faca7d9be8de4004421d650f4227fa60bd92b
 │       ├──05c2ea2d81bceeddc2881beb63d2ee26537593de9f47b4613c0d5afb1353f7df
├── graph                                 <---镜像目录,存储镜像层元数据
 │   ├──0288ae931294ce04f5d69c60146faca7d9be8de4004421d650f4227fa60bd92b
│   │  ├── checksum                       <---镜像校验和,用于判断镜像是否损坏
│   │  ├── json                                   <---描述镜像基础信息
│   │  ├── layersize                          <---镜像层大小
│   │  └── tar-data.json.gz           <---描述镜像层内文件在磁盘中的位置
 │   ├──05c2ea2d81bceeddc2881beb63d2ee26537593de9f47b4613c0d5afb1353f7df
 │   │   ├── checksum
 │   │   ├── json
 │   │   ├── layersize
 │   │   └── tar-data.json.gz
 │   └── _tmp
 ├── linkgraph.db
 ├── repositories-devicemapper
 ├── tmp
 ├── trust
 └── volumes