写在前面的话

容器的默认状态难以从容器中搬运出来。

容器的默认状态是基于联合文件系统的,也就是需要存储驱动的支持,效率会比直接写到宿主机文件系统里要差一点。

所以此处 storage driver,就正对应 network driver 了。

把数据写入到宿主机上的方法有:volumes 和 bind mounts。如果在 linux 上,还有 tmpfs mount。注意,这些东西不是 storage driver。

他们之间的关系是:

在操作系统和 docker 层面,可以认为 volumes 是有实际存储的设备(虽然被虚拟化)的抽象(在 k8s 抽象里是 volumeMounts),而 mount 是带有 path 的一个挂载点(在 k8s 抽象里是 mountPath)。

Volumes

在宿主机文件系统上,Volumes 使用一块专属的路径来存储 docker 文件,如/var/lib/docker/volumes/,其他进程不应该碰这些文件。

Volumes 是 Docker 持久化数据最佳的选择。

常见创造数据卷的方法

只能迟至容器创建时把一个数据卷和容器关联起来。

  1. docker volume create
  2. 容器/服务创建时,使用 -v/--mount 参数

也就是说,不能追加在容器里创建数据卷。

有数据共享需求,可以考虑使用docker cp一类的命令来解决。

数据共享

多个容器可以通过共享数据卷的方式来共享数据。即使所有的容器都不在使用卷了,卷对于 docker 进程而言依然是活跃的,除非使用docker volume prune命令来打扫卷。也就是说,卷的生命周期,和容器的生命周期是互不干涉的。

命名与匿名

当创造卷的时候,卷可以是命名的,也可以是匿名的。匿名的卷只是没有显式地被命名,但 docker 本身还是会给它起一个随机名字。

远程存储数据

卷允许使用卷驱动(volume driver,不是 storage driver),这样我们可以在远程文件系统,或者云系统上存储数据。

常用命令

创建容器的时候创建卷,新用户应该使用 mount 选项而不是使用v 选项。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 创造卷
docker volume create my-vol
# 列出卷
docker volume ls
# 检查卷
docker volume inspect my-vol
# 清除卷
docker volume rm my-vol
# 使用 mount 来创立一个容器附属的卷
docker service create \
--mount 'type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>,"volume-opt=o=addr=<nfs-address>,vers=4,soft,timeo=180,bg,tcp,rw"'
--name myservice \
<IMAGE>
# 指定 mount 的 source 和 destination
docker run -d \
--name devtest \
--mount source=myvol2,target=/app \
nginx:latest

# 其结果如下:
"Mounts": [
{
"Type": "volume",
"Name": "myvol2",
"Source": "/var/lib/docker/volumes/myvol2/_data",
"Destination": "/app",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],

# 创建一个只读的卷
docker run -d \
--name=nginxtest \
--mount source=nginx-vol,destination=/usr/share/nginx/html,readonly \
nginx:latest

# 使用专有的卷驱动
docker plugin install --grant-all-permissions vieux/sshfs
docker volume create --driver vieux/sshfs \
-o sshcmd=test@node2:/home/test \
-o password=testpassword \
sshvolume
docker run -d \
--name sshfs-container \
--volume-driver vieux/sshfs \
--mount src=sshvolume,target=/app,volume-opt=sshcmd=test@node2:/home/test,volume-opt=password=testpassword \
nginx:latest

# 备份数据卷
docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata
# 恢复数据卷
docker run -v /dbdata --name dbstore2 ubuntu /bin/bash
docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1"

如果是创建容器的时候,容器内部的目录已经有数据,而绑定一个 volume 进去,则会自动把内部容器的数据拷贝到 volume 里面。

bind mounts

可以让数据在宿主机文件系统中的所有部分,也可以让其它进程修改。它是 docker 早期就存在的技术。

bind mounts 在宿主机上的路径并不被要求早已存在,它会在需要的时候被创建出来,也就是所谓的pre-populate data。当然这也要求宿主机有一些特定的目录结构存在,如果这个目录结构不存在,那么还是要回来寻求 volume 的帮助,因为 volume 使用的路径完全可以被 docker 守护进程把控。

额外的副作用

docker 容器有完全地修改 bind mount 内文件内容的权限。所以如果宿主机里有什么敏感文件的话,不要放在这里面,否则会产生安全隐患。这也是为什么推荐使用 volumes 的原因。

常见命令

1
2
3
4
5
6
7
# 创建 mount 一样使用 mount options。但是使用不同的 type。
docker run -d \
-it \
--name devtest \
# source 是宿主机上的路径,target 是容器内的路径
--mount type=bind,source="$(pwd)"/target,target=/app \
nginx:latest

volume vs bind mount

  1. 如果只是简单地想在把容器的状态持久化到宿主机上,应该直接使用数据卷,这样新的容器也能继承老容器的状态。更抽象地说,如果需要在容器和容器之间转移状态,可以使用数据卷。
  2. 如果只是要备份数据,可以考虑停下容器,把数据卷里的内容拷贝出来。
  3. 数据卷在不同的宿主系统上是通用的。
  4. 如果想进一步在宿主机和容器之间共享状态,那么可以考虑使用 bind mount。

tmpfs mounts

这个是在内存里面做 bind mounts。所以本质上是不持久化的。

存储驱动(storage driver)相关

分层架构

存储驱动总是基于联合文件系统,其基本架构如图:

可以看到,多个容器之所以能够基于一个镜像诞生,是因为镜像的层次是 RO 的,而每个容器的 RW layer 是彼此独立的。

如果有跨镜像的同份数据共享,请使用 volume。

大小计算

使用 docker ps -s 命令,我们可以看到两个 size:

  1. size。容器的可写层在磁盘上的数据大小。
  2. virtual size。镜像的只读数据加上 size 的大小。

其他更细致的计算请查看《Container size on disk》

docker 除了 layer 以外,还有其他消耗磁盘的地方。比如写日志,有专门的日志驱动,这需要另外计算大小。

COW 问题

为了最大效率地(for maximum efficiency)利用磁盘,跨层之间是使用 COW(Copy On Write)策略来利用文件的。也就是说,当上层(包括可写层在内)的层要使用下层的文件,应该先直接访问,在有需要修改的需求时,才将该文件拷贝一份放到本层里。

通常,应该让容器自己的 writable layer 变得非常薄。

层次存储的位置

通常在目录/var/lib/docker/下,例如:

1
2
3
4
5
6
ls /var/lib/docker/aufs/layers

1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24
5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7
bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73
ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a

常见的存储驱动

  1. aufs
  2. overlay
  3. overlay2
  4. btrfs
  5. ZFS
  6. others

docker 选择驱动的优先级列表:

btrfs/zfs -> overlay2 -> overlay -> aufs/devicemapper

在当代,默认的存储驱动总是 overlay2,而以前则是 aufs。

选择存储驱动要考虑的因素主要有:

  1. 是否零配置。
  2. 操作系统是否支持。
  3. 稳定性。