Docker存储驱动—Device Mapper「译」

Docker 的 devicemapper 存储驱动利用了内核 device mapper 的 thin provisioning(自动精简配置)和 snapshotting(快照)的能力来管理镜像和容器

devicemapper使用Docker专用的块设备,操作为块级而不是文件级。可以通过为Docker主机添加物理存储来扩充这些设备

这篇文章是对Docker官网中 devicemapper 存储驱动使用文档的翻译


配置使用devicemapper

loop-lvm 模式

这个模式仅仅适用于测试。loopback(回路)设备非常慢并且资源敏感,他们在磁盘中创建的文件需要有固定大小,他们还会引入竞争。配置这种模式的步骤十分简单:

  1. 停止Docker服务
1
sudo service docker stop
  1. 编辑/etc/docker/daemon.json
1
2
3
{
"storage-driver": "devicemapper"
}
  1. 开启Docker服务
1
sudo service docker start
  1. docker info进行确认
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ docker info

Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.03.1-ce
Storage Driver: devicemapper
Pool Name: docker-202:1-8413957-pool
Pool Blocksize: 65.54 kB
Base Device Size: 10.74 GB
Backing Filesystem: xfs
Data file: /dev/loop0
Metadata file: /dev/loop1
Data Space Used: 11.8 MB
Data Space Total: 107.4 GB
Data Space Available: 7.44 GB
Metadata Space Used: 581.6 kB
Metadata Space Total: 2.147 GB
Metadata Space Available: 2.147 GB
Thin Pool Minimum Free Space: 10.74 GB
Udev Sync Supported: true
Deferred Removal Enabled: false
Deferred Deletion Enabled: false
Deferred Deleted Device Count: 0
Data loop file: /var/lib/docker/devicemapper/data
Metadata loop file: /var/lib/docker/devicemapper/metadata
Library Version: 1.02.135-RHEL7 (2016-11-16)
<output truncated>

Data loop fileMetadata loop file文件位于/var/lib/docker/devicemapper目录下,表明当前已经运行在loop-lvm模式下

direct-lvm 模式

生产环境中需要使用这种模式。这种模式使用块设备来创建 thin pool,这比 loopback(回环)设备更快、资源利用更有效,并且块设备能按需增长。但是配置也相对更复杂一些

1)让Docker来配置 direct-lvm 模式

如果使用Docker 17.06或者更高版本,Docker能够为我们管理块设备。这种方式适用于简单地配置,只能使用一个块设备。如果需要使用多个块设备,参考后面的手动配置部分

配置选项如下表:

Option Description Required? Default Example
dm.directlvm_device 块设备的路径 Yes dm.directlvm_device="/dev/xvdf"
dm.thinp_percent 用于数据存储的空间百分比 No 95 dm.thinp_percent=95
dm.thinp_metapercent 用于元数据存储的空间百分比 No 1 dm.thinp_metapercent=1
dm.thinp_autoextend_threshold lvm自动扩充thin pool的阈值百分比 No 80 dm.thinp_autoextend_threshold=80
dm.thinp_autoextend_percent 当发生自动扩充时,增加thin pool的存储空间的百分比 No 20 dm.thinp_autoextend_percent=20
dm.directlvm_device_force 是否格式化块设备,即使块设备上已经存在文件系统。如果设为false并且文件系统存在,将会报错 No false dm.directlvm_device_force=true

使用上面的选项来设置/etc/docker/daemon.json,然后重启Docker服务:

1
2
3
4
5
6
7
8
9
10
11
{
"storage-driver": "devicemapper",
"storage-opts": [
"dm.directlvm_device=/dev/xdf",
"dm.thinp_percent=95",
"dm.thinp_metapercent=1",
"dm.thinp_autoextend_threshold=80",
"dm.thinp_autoextend_percent=20",
"dm.directlvm_device_force=false"
]
}

2)手动配置 direct-lvm 模式

  1. 选择一个大小足够的块设备
  2. 停止Docker服务
1
sudo service docker stop
  1. 安装下列包

    • RHEL/CentOSdevice-mapper-persistent-datalvm2和所有依赖
    • Ubuntu/Debianthin-provisioning-toolslvm2和所有依赖
  2. 使用pvcreate命令创建PV(物理卷)

1
2
3
$ sudo pvcreate /dev/xvdf

Physical volume "/dev/xvdf" successfully created.
  1. 使用vgcreate命令创建一个名为docker的VG(卷组)
1
2
3
$ sudo vgcreate docker /dev/xvdf

Volume group "docker" successfully created
  1. 使用lvcreate命令创建2个名为thinpoolthinpoolmeta的LV(逻辑卷),这2个卷都使用docker卷组。最后一个参数指定了可用于自动扩充的空闲空间占VG的百分比
1
2
3
4
5
6
7
$ sudo lvcreate --wipesignatures y -n thinpool docker -l 95%VG

Logical volume "thinpool" created.

$ sudo lvcreate --wipesignatures y -n thinpoolmeta docker -l 1%VG

Logical volume "thinpoolmeta" created.
  1. 使用lvconvert将逻辑卷thinpool转换为一个 thin pool,将逻辑卷thinpoolmeta指定为 thin pool 的元数据逻辑卷
1
2
3
4
5
6
7
8
9
10
$ sudo lvconvert -y \
--zero n \
-c 512K \
--thinpool docker/thinpool \
--poolmetadata docker/thinpoolmeta

WARNING: Converting logical volume docker/thinpool and docker/thinpoolmeta to
thin pool's data and metadata volumes with metadata wiping.
THIS WILL DESTROY CONTENT OF LOGICAL VOLUME (filesystem etc.)
Converted docker/thinpool to thin pool.
  1. 编辑lvm profile文件配置thin pool的自动扩展。然后使用lvchange命令使配置文件中的配置生效:
1
2
3
4
5
6
7
8
9
10
$ sudo vim /etc/lvm/profile/docker-thinpool.profile

activation {
thin_pool_autoextend_threshold=80
thin_pool_autoextend_percent=20
}

$ sudo lvchange --metadataprofile docker-thinpool docker/thinpool

Logical volume docker/thinpool changed.
  1. 开启逻辑卷监视。没有这一步的话即使编辑好了profile配置文件也无法触发自动扩充
1
2
3
4
$ sudo lvs -o+seg_monitor

LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert Monitor
thinpool docker twi-a-t--- 95.00g 0.00 0.01 monitored
  1. 备份Dokcer根目录中的数据,然后使用新创建的 LVM pool 来存储镜像和容器
1
2
$ mkdir /var/lib/docker.bk
$ mv /var/lib/docker/* /var/lib/docker.bk
  1. 编辑/etc/docker/daemon.json,并配置devicemapper需要的选项
1
2
3
4
5
6
7
8
{
"storage-driver": "devicemapper",
"storage-opts": [
"dm.thinpooldev=/dev/mapper/docker-thinpool",
"dm.use_deferred_removal=true",
"dm.use_deferred_deletion=true"
]
}
  1. 开启Docker服务
1
sudo service docker start
  1. 使用docker info进行确认:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ docker info

Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.03.1-ce
Storage Driver: devicemapper
Pool Name: docker-thinpool
Pool Blocksize: 524.3 kB
Base Device Size: 10.74 GB
Backing Filesystem: xfs
Data file:
Metadata file:
Data Space Used: 19.92 MB
Data Space Total: 102 GB
Data Space Available: 102 GB
Metadata Space Used: 147.5 kB
Metadata Space Total: 1.07 GB
Metadata Space Available: 1.069 GB
Thin Pool Minimum Free Space: 10.2 GB
Udev Sync Supported: true
Deferred Removal Enabled: true
Deferred Deletion Enabled: true
Deferred Deleted Device Count: 0
Library Version: 1.02.135-RHEL7 (2016-11-16)
<output truncated>

如果配置正确的话,Data fileMetadata file会是空,并且Pool Namedocker-thinpool


为运行中的设备扩容

不要仅仅依赖于LVM的自动扩容机制。VG自动扩容,但是LV可能还是满的。可以使用lvslvs -a查看LV的空闲空间

可以使用journalctl查看LVM的日志:

1
$ journalctl -fu dm-event.service

如果重复遇到某个问题,可以在/etc/docker.daemon.json中设置dm.min_free_space选项,比如设置成10(表示百分比),从而确保当空闲空间不足10%时操作会失败并返回提示信息

改变 loop-lvm 模式设备的大小

有下列2种方法:

1. 使用社区共享的device_tool.go脚本(链接)。使用这个工具最为方便,但不一定奏效

1)首先将链接中的仓库克隆,然后重命名为contrib/docker-device-tool。接着按照README.md中的说明编译工具

2)使用类似$ ./device_tool resize 200GB这样的指令扩容

2. 使用操作系统工具

在 loop-lvm 模式下,一个 loopback(回环)设备被用于存储数据,另一个 loopback(回环)设备被用于存储元数据。这种模式仅适合测试,因为性能和稳定性都不好

假设原来的 loopback(回环)设备大小为100GB,现在要扩容到200GB

1)查看2个 loopback(回环)设备:

1
2
3
4
5
$ sudo ls -lh /var/lib/docker/devicemapper/

total 1175492
-rw------- 1 root root 100G Mar 30 05:22 data
-rw------- 1 root root 2.0G Mar 31 11:17 metadata

2)使用truncate命令将数据文件的大小修改成200G

1
$ sudo truncate -s 200G /var/lib/docker/devicemapper/data

3)再次查看2个 loopback(回环)设备:

1
2
3
4
5
$ sudo ls -lh /var/lib/docker/devicemapper/

total 1.2G
-rw------- 1 root root 200G Apr 14 08:47 data
-rw------- 1 root root 2.0G Apr 19 13:27 metadata

4)loopback 文件已经在磁盘上发生了改变,但是内存中没有。列出内存中 loopback 设备的大小,重载,然后再次列出它的大小,此时内存中的大小也得到更新:

1
2
3
4
5
6
7
8
9
$ echo $[ $(sudo blockdev --getsize64 /dev/loop0) / 1024 / 1024 / 1024 ]

100

$ sudo losetup -c /dev/loop0

$ echo $[ $(sudo blockdev --getsize64 /dev/loop0) / 1024 / 1024 / 1024 ]

200

5)重载 devicemapper thin pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# a)首先得到 thin pool 的名字
$ sudo dmsetup status | grep ' thin-pool ' | awk -F ': ' {'print $1'}

docker-8:1-123141-pool

# b)dump device mapper table for the thin pool
$ sudo dmsetup table docker-8:1-123141-pool

0 209715200 thin-pool 7:1 7:0 128 32768 1 skip_block_zeroing

# c)使用上一步输出中的第2个域计算 thin pool 所有的扇区。如果扩容为200G,即原来的2倍,那么扇区数也变成原来的2倍,即419430400

# d)使用新的扇区数重载 thin pool
$ sudo dmsetup suspend docker-8:1-123141-pool

$ sudo dmsetup reload docker-8:1-123141-pool --table '0 419430400 thin-pool 7:1 7:0 128 32768 1 skip_block_zeroing'

$ sudo dmsetup resume docker-8:1-123141-pool

改变 direct-lvm 模式设备的大小

首先要附加一块新的块设备,假设这块新的设备为/dev/xvdg

  1. 使用pvdisplay找到当前被 thin pool 使用的PV和VG:
1
2
3
4
$ sudo pvdisplay |grep 'VG Name'

PV Name /dev/xvdf
VG Name docker
  1. 执行vgextend命令使用新的设备/dev/xvdg来扩充VG:
1
2
3
4
$ sudo vgextend docker /dev/xvdg

Physical volume "/dev/xvdg" successfully created.
Volume group "docker" successfully extended
  1. 使用下列命令扩充docker/thinpool LV(逻辑卷)。下面的命令会使用所以100%的可用空间。如果要扩充元数据 thin pool, 使用docker/thinpool_tmeta代替docker/thinpool
1
2
3
4
$ sudo lvextend -l+100%FREE -n docker/thinpool

Size of logical volume docker/thinpool_tdata changed from 95.00 GiB (24319 extents) to 198.00 GiB (50688 extents).
Logical volume docker/thinpool_tdata successfully resized.
  1. 查看docker info输出中的Data Space Available域确认扩容完成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Storage Driver: devicemapper
Pool Name: docker-thinpool
Pool Blocksize: 524.3 kB
Base Device Size: 10.74 GB
Backing Filesystem: xfs
Data file:
Metadata file:
Data Space Used: 212.3 MB
Data Space Total: 212.6 GB
Data Space Available: 212.4 GB
Metadata Space Used: 286.7 kB
Metadata Space Total: 1.07 GB
Metadata Space Available: 1.069 GB
<output truncated>

如果重启主机后发现Docker服务启动失败,然后报错:”Non existing device“,需要调用命令sudo lvchange -ay docker/thinpool来重新激活逻辑卷


devicemapper如何工作

使用 lsblk 命令查看设备和 thin pool :

1
2
3
4
5
6
7
8
9
10
$ sudo lsblk

NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda 202:0 0 8G 0 disk
└─xvda1 202:1 0 8G 0 part /
xvdf 202:80 0 100G 0 disk
├─docker-thinpool_tmeta 253:0 0 1020M 0 lvm
│ └─docker-thinpool 253:2 0 95G 0 lvm
└─docker-thinpool_tdata 253:1 0 95G 0 lvm
└─docker-thinpool 253:2 0 95G 0 lvm

使用 mount 命令查看容器使用的挂载点:

1
2
$ mount | grep devicemapper
/dev/xvda1 on /var/lib/docker/devicemapper type xfs (rw,relatime,seclabel,attr2,inode64,noquota)

当使用devicemapper存储驱动时,Docker将镜像和层数据存储在 thinpool 中,然后通过将它们挂载在/var/lib/docker/devicemapper/的子目录下来暴露给容器

磁盘中的镜像和容器层

  • /var/lib/docker/devicemapper/metadata/目录包含 devicemapper 配置的元数据以及每个镜像和容器层的元数据。devicemapper 使用快照,这些元数据包含这些快照的信息,文件格式为JSON
  • /var/lib/docker/devicemapper/mnt/目录为每个镜像和容器层创建了一个挂载点。镜像层的挂载点为空,而容器层的挂载点展示了容器的文件系统

镜像的分层与共享

devicemapper 存储驱动使用专用的块设备,而不是格式化的文件系统。块级操作可以为CoW操作带来最大的性能

快照

devicemapper 的另一个特征是快照。它将每一层引入的差异存储为非常小的,轻量级的 thin pools。快照提供很多好处:

  • 多个容器共享的层只在磁盘中存储一次,除非它们可写
  • 快照是CoW机制的一种实现。这意味着当一个文件或者目录被修改时,它们会被拷贝到容器的可写层然后再修改
  • 因为操作是块级,所以可写层的多个块的修改可同时进行
  • 快照能用OS级的备份工具来备份:拷贝/var/lib/docker/devicemapper/

工作流

当使用 devicemapper 存储驱动时,所有和镜像及容器有关的对象被存储在/var/lib/docker/devicemapper/中,这个目录的后端是一个或多个块设备,如 loopback 设备或物理磁盘

  • 基础设备是最底层的对象,也就是 thin pool。可以使用docker info查看,它包含了1个文件系统。这个基础设备是每个镜像和容器层的起点。这个基础设备是一个Device Mapper的详细实现,而不是一个Docker的层
  • 关于基础设备和镜像及容器层的元数据以JSON格式存储在/var/lib/docker/devicemapper/metadata/中,它们都是CoW快照,意味着在和父层表现出差异之前,它们都是空
  • 每个容器的可写层挂载在/var/lib/docker/devicemapper/mnt/目录中的一个挂载点上。每个镜像层和停止的容器在/var/lib/docker/devicemapper/mnt/中有一个空目录

每个镜像层是它下层的一个快照。每个镜像的最底层是pool中基础设备的一个快照。当运行一个容器时,会根据容器使用的镜像创建1个快照


容器中的读写

读文件

读为块级。下图展示了读一个块(0x44f)的处理流程:

一个应用发起一个对块0x44f的读请求,因为容器是镜像的一个 thin snapshot,它并没有这个块,但是它有一个指向最近父镜像中这个块的指针,所以会从父镜像中读取这个块。这个块现在存在于容器的内存中

写文件

  • 写一个新文件:将新数据写入容器是通过按需分配操作完成的。新文件的每个块都分配在容器的可写层中,然后对可写层中的块进行写
  • 更新一个已有文件:文件相关的块从存有该文件的最近层读取。当容器写这个文件时,只有修改的块会被写到容器的可写层
  • 删除一个文件或目录:当删除容器可写层中的文件或目录时,或者当镜像层删除1个位于父镜像层中的文件时,devicemapper存储驱动会拦截以后对该文件或目录的读取操作并响应”此文件不存在“
  • 写然后删除一个文件:如果容器先写一个文件,然后删除它,所以这些操作发生在容器的可写层。这种情况下,如果使用 direct-lvm 模式,块会被释放。如果使用 loop-lvm 模式,块不会被释放。这是不在生产环境中使用 loop-lvm 模式的另一个原因


性能与最佳实践

性能

  • allocate-on demand(按需分配):devicemapper存储驱动使用按需分配操作从 thin pool 中分配新的块。每个块64KB,因此这是一个写使用的最小的空间
  • CoW性能影响:容器第一次修改1个特定块时,块被写入容器的可写层。因为这些写为块级而不是文件级,所以性能影响最小化。然而,写大量的块仍会对性能产生消极影响,此时,devicemapper可能比其他存储驱动表现出更差的性能。对于写密集的负载,应该使用数据卷

最佳实践

  • 使用 direct-lvm 模式:loop-lvm模式不适用与生产环境
  • 使用更快的存储介质:SSD提供更好的读写性能
  • 内存使用:devicemapper比其他存储驱动使用更多的内存。每个启动的容器将一份或多份文件的拷贝装载到内存中,取决于相同文件中有多少个块在同一时刻被修改。由于内存压力,devicemapper存储驱动可能不适应与高密集型负载的使用场景
  • 在写繁重的场景中使用数据卷:卷 bypass(旁路)了存储驱动,不会引入由 thin provisioning(自动精简配置)和 CoW 带来的潜在性能开销


其它资料

文章目录
  1. 1. 配置使用devicemapper
    1. 1.1. loop-lvm 模式
    2. 1.2. direct-lvm 模式
      1. 1.2.1. 1)让Docker来配置 direct-lvm 模式
      2. 1.2.2. 2)手动配置 direct-lvm 模式
  2. 2. 为运行中的设备扩容
    1. 2.1. 改变 loop-lvm 模式设备的大小
    2. 2.2. 改变 direct-lvm 模式设备的大小
  3. 3. devicemapper如何工作
    1. 3.1. 磁盘中的镜像和容器层
    2. 3.2. 镜像的分层与共享
    3. 3.3. 快照
    4. 3.4. 工作流
  4. 4. 容器中的读写
    1. 4.1. 读文件
    2. 4.2. 写文件
  5. 5. 性能与最佳实践
    1. 5.1. 性能
    2. 5.2. 最佳实践
  6. 6. 其它资料
|