一、前言

docker 镜像是由一系列的只读层组合而来的,当启动一个容器时,Docker 加载镜像的只读层,并在最上面加入一个读写层。为了满足容器间文件的交互共享问题又整出了数据卷 volume。

linxu 系统上面的文件有很多类型,不同存储驱动对应不同的文件类型。虽然很多,但是主流的就那么几种,并且他们都是围绕这如何实现分层展开的,不同方案在磁盘的存储空间、IO 读写方面表现不同。抓住问题的核心本质,其他的都是细枝末节。

文件系统类型查看

# cat /proc/filesystems
nodev    sysfs
nodev    rootfs
nodev    ramfs
nodev    bdev
nodev    proc
nodev    cgroup
nodev    cpuset
nodev    tmpfs
nodev    devtmpfs
nodev    debugfs
nodev    securityfs
nodev    sockfs
nodev    dax
nodev    bpf
nodev    pipefs
nodev    configfs
nodev    devpts
nodev    hugetlbfs
nodev    autofs
nodev    pstore
nodev    mqueue
        ext3
        ext2
        ext4
nodev    overlay
nodev    binfmt_misc

二、文件分层

2.1 镜像层

Docker 技术的兴起很重要的功劳要归于镜像的设计,分层的镜像架构在构建、存储、分发方面有着很高的效率。一个镜像修改了要重新上传到镜像仓库,只需要上传改动过的层。

一个镜像可以实例出很多容器,这些容器如果修改了某个文件肯定不能影响到别的容器,所以镜像层是只读的。镜像在宿主机中有2个重要的东西组成:镜像层+元数据

镜像层
具体的镜像层位于:/var/lib/docker/[存储驱动]/,里面有 3 个目录:

  1. diff:具体镜像层数据
  2. layers:层依赖的描述文件
  3. mnt:挂载目录

元数据可以分为下面 3 类

  1. repository 元数据
    存放了镜像对仓库信息,持久化文件存位于:/var/lib/docker/image/[存储驱动]/repositories.json
  2. image 元数据
    存放了镜像对架构(如 amd64)、操作系统(如 linux)、镜像对容器ID、创建时间、版本、构建历史、根系统等,持久化文件位于:/var/lib/docker/image/[存储驱动]/imagedb
  3. layer 元数据
    记录了层与镜像的关系,可以检索出镜像包含的所有的层信息,持久化文件位于:/var/lib/docker/image/[存储驱动]/layer

2.2 容器层

不同容器要有自己实例特定的东西,所以容器层不能跟镜像层混合在一起。为了实现容器层的读写能力,有了 写时拷贝,用时分配 的理论指导方案。容器启动的时候加载镜像层,要修改某个文件时复制一个层后更改,如果是新增文件就分配空间新创建一个,如果是删除就隐藏原来的文件(隐藏只是对当前容器而言,对其他相同镜像的容器还是存在)

容器层物理存放位置跟镜像层是一样的 /var/lib/docker/[存储驱动],这些目录里面有很多数字命名的目录,如果是镜像层这些数字叫做 cacheId,如果是容器层叫做 mountID(容器的文件都是要具体挂载的)。

三、存储驱动

上面对于镜像层目录的描述对于不同存储驱动不尽相同,但是作为了解分层的原理问题不大,下面主要介绍当前最主流的 3 种存储驱动。

3.1 aufs

aufs(advanced multi layered unification filesystem)是一种支持联合挂载的文件系统,简单说就是支持将不同目录挂载到同一个目录下,但是这些挂载操作对用户是透明的,用户操作该目录的时候不会觉得与其他目录不同。

该驱动的目录存储逻辑基本就是我上面介绍的了:

  • 镜像元数据:/var/lib/docker/image/aufs
  • 镜像层数据:/var/lib/docker/aufs

3.2 overlayFS

一种新型文件挂载系统(需要 3.18 的 linux 内核),允许不同文件系统之间做堆叠(overlay),在上层文件系统中记录更改不改变下层文件系统。相比 aufs,overlayFS 在设计上更为简单,理论上性能更好。aufs 是多层的,但是 overlay 只有 2 层(容器层和镜像层)。

cd /var/lib/docker/overlay2
[overlay2]# tree -L 2
.
├── 0108facc91502de01b0800dca6ac6b12eae3e5befb84cee96da44aab3cc383d6 【镜像层】
│   ├── committed
│   ├── diff
│   └── link
├── 02e74f1b347a3540b3b8627bd6cd67b269fd4e1beb0af909ddeaa98c253bf2fa 【镜像层】
│   ├── committed
│   ├── diff
│   └── link
├── 057cd2eef2e34f4dc1e5d9ba9f489a73d44f72fc99071e7ddd2f4dcbdb2e6f42 【容器层】
│   ├── diff
│   ├── link
│   ├── lower 【只读层、下层】
│   ├── upper 【可写层、上层】
│   └── work
├── 057cd2eef2e34f4dc1e5d9ba9f489a73d44f72fc99071e7ddd2f4dcbdb2e6f42-init 【容器层】

底层实现

  1. 使用命令 lsmod | grep overlay 确认内核是否存在 overlay 模块
  2. 如果不存在需要升级到 3.18 以上的内核版本,并使用 `modprobe overlay 加载
  3. 然后创建 4个必要的文件夹,并执行 mount 命令挂载
  4. 通过查看 mount 命令的输出确认挂载结果

实验

// 环境检查
[root@linux~]# lsmod |grep overlay
overlay                91659  7

// 创建必要的文件夹
[root@linux~]# mkdir upper lower merged work

// 创建相关文件进行测试
[root@linux~]# echo "I'am from lower" > lower/in_lower.txt
[root@linux~]# echo "I'am from upper" > upper/in_upper.txt
[root@linux~]# echo "I'am from lower" > lower/in_both.txt
[root@linux~]# echo "I'am from upper" > upper/in_both.txt

[root@linux~]# sudo mount -t overlay overlay \
    -o lowerdir=/root/temp/overlaytest/lower,upperdir=/root/temp/overlaytest/upper,workdir=/root/temp/overlaytest/work \
    /root/temp/overlaytest/merged

// 结果观察
[root@linux~]# cat merged/in_both.txt
I'am from upper

[root@linux~]# find lower/ upper/ merged/
lower/
lower/in_both.txt
lower/in_lower.txt
upper/
upper/in_both.txt
upper/in_upper.txt
merged/
merged/in_both.txt
merged/in_lower.txt
merged/in_upper.txt

// 新建文件实验
[root@linux~]# echo 'new file' > merged/new_file
[root@linux~]# ls -l */new_file
-rw-r--r-- 1 root root 9 3月  15 09:57 merged/new_file
-rw-r--r-- 1 root root 9 3月  15 09:57 upper/new_file

// 删除文件实验
ls -l */in_both.txt
-rw-r--r-- 1 root root 16 3月  15 09:53 lower/in_both.txt
-rw-r--r-- 1 root root 16 3月  15 09:53 merged/in_both.txt
-rw-r--r-- 1 root root 16 3月  15 09:53 upper/in_both.txt

[root@linux~]# rm merged/in_both.txt

// merged 里面没了,lower 不变,upper 里面变成字符设备文件
ls -l */in_both.txt
-rw-r--r-- 1 root root   16 3月  15 09:53 lower/in_both.txt
c--------- 1 root root 0, 0 3月  15 10:03 upper/in_both.txt

3.2 devicemapper

aufs 和 overlay 都是基于文件的堆叠,devicemapper 是基于块设备的堆叠。要修改一个文件不需要复制整个文件然后堆叠,之需要将相应的变动存储块堆叠就行。Device Mapper 自 Linux 2.6.9 后编入 Linux 内核,所有基于 Linux 内核 2.6.9 以后的发行版都内置 Device Mapper。

注:Device Mapper是Linux的一种技术框架,而devicemapper是Docker Engine基于Device Mapper提供的一种存储驱动。

Device Mapper 包括 3 个概念:映射设备、映射表、目标设备。Device Mapper 对外提供一个虚拟设备供使用,而这块虚拟设备可以通过映射表找到相应的地址,该地址可以指向一块物理设备,也可以指向一个虚拟设备。当映射设备接收到 IO 请求到时候,这个 IO 请求会根据映射表逐级转发,直到这个请求最终传到最底层的物理设备上。

devicemapper 驱动将每一个 Docker 镜像和容器存储在它自身的具有精简置备(thin-provisioned)、写时拷贝(copy-on-write)和快照功能(snapshotting)的虚拟设备上。它有两种配置模式:loop-lvm和direct-lvm。

loop-lvm 是默认的模式,它使用OS层面离散的文件来构建精简池(thin pool)。该模式主要是设计出来让Docker能够简单的被”开箱即用(out-of-the-box)”而无需额外的配置。但如果是在生产环境的部署Docker,官方明文不推荐使用该模式。

direct-lvm 是 Docker 推荐的生产环境的推荐模式,他使用真正的块设备来构建精简池来存放镜像和容器的数据。

四、小结

overlay2 与 devicemapper 性能的对比上没有谁占据决定优势,不同场景下各有所长,社区总体上推荐 overlay2。

  • devicemapper 驱动在第一次对文件读写时性能稍微优越于 overlay2 ,但是对已有文件的重复读写来看,overlay2 性能优越于 devicemapper。
  • 对于随机读写来说,overlay2 性能优越于 devicemapper。
  • 由于 devicemapper 是基于 block 复制,并且每次修改数据时,都会进行复制,这会造成磁盘空间和内存的浪非甚至会出现磁盘和内存溢出等问题,对于 overlay2 驱动来说,只在第一次修改的时候复制文件,后续更在都在该文件上进行。
  • 如果有空的镜像层(是指没有数据的层,如expose, env), overlay2 存储驱动基本不耗时,而 devicemapper 需要消耗一定的时间。
  • 对于大文件的修改,overlay 需要将该文件完整复制,占用的磁盘空间比较大,devicemapper 只需要局部块复制。

其他

dmsetup

dmsetup 命令是一个用来与 Device Mapper 沟通的命令行封装器(wrapper)

mount

mount 命令用来挂载文件系统。其基本命令格式为:

mount -t type [-o options] device dir

  • device:指定要挂载的设备,比如磁盘、光驱等。
  • dir:指定把文件系统挂载到哪个目录。
  • type:指定挂载的文件系统类型,一般不用指定,mount 命令能够自行判断。
  • options:指定挂载参数,比如 ro 表示以只读方式挂载文件系统。

点赞(1) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
发表
评论
返回
顶部