Understanding Docker Volume
Preface
在Docker的使用过程中,data persist,share一直是比较令人困惑,比如:
- container内部被改动的文件保存在哪里?
- container运行结束或者被删除后,如何读取感兴趣的数据?
- 如何在host及container,container之间共享数据?
本文的目标通过介绍docker相关背景知识(union file system, volume),给出完成常见volume操作的示例,最终达到解答这些问题的目的。
Where does data reside in container
当我们在container中写入数据时,这部分数据保存在哪里呢?
我们首先看一下当前host磁盘容量:
1 | df -h |
然后启动一个container,写入size为4G的文件:
1 | # ti 交互terminal |
这时再在host运行df -h
1 | 文件系统 容量 已用 可用 已用% 挂载点 |
正如我们所料,新写入的4g.bin占了host上4G的空间,然后停止container:
1 | 文件系统 容量 已用 可用 已用% 挂载点 |
可见虽然停止了container,所占用的磁盘空间仍然存在,如何访问已停止的container呢:
1 | # i interactive |
为什么要访问已经停止的container的文件呢?典型的应用场景是:运行在container中的service crash,但是需要看已经stop的container存储的log,如果是用 –rm 启动的container,停止后会自动释放磁盘,因为container被remove了,磁盘容量因此会被回收。
Docker file system
知道了container写入的文件具有与其相同的生命周期之后,我们来看一看docker的文件系统:
以如下Dockerfile为例:
1 | FROM ubuntu:18.04 |
查看输出
1 | Sending build context to Docker daemon 4.096kB |
直观上看到docker构建时是分层的,每一条命令都会改变在前一步成功的基础上增加一层。由Dockerfile build之后的image由一系列的read-only层组成,最上面是一个read-write层,container运行起来之后,对文件系统做的改动,写入的数据等均会保存在最上面的读写层。
docker image格式:自底向上,由一系列只读层,加上最上面的读写层,称之为Union File System
当container被删除后,再重新启动同样的image,会在最上面构建一个全新的读写层,之前container的数据被丢弃。
Union File System 并不能与外界(宿主机,NFS)共享file/directory,以及分离数据与container的生命周期,如何解决?简单的bind机制应运而生。
Use bind to share
何为bind? 将host的file/directory与container共享,任何一方的修改都对对方立刻可见。bind命令可能大家并不陌生
1 | # 将host的/tmp directory mount到container的/host_tmp directory |
运行bind后,查看此container的Mounts选项:
1 | docker inspect eager_cray -f '{{json .Mounts}}' | jq |
注: jq是一个很好用的Command-line JSON processor,安装:
1 | sudo apt-get install jq |
可以看到Type, source, destination
1 | [ |
另外:官方建议大家使用–mount option,避免-v src:dst 时bind和volume弄混的pitfall,虽然繁琐一点,但是清晰可读大于一切。接下来的例子,将尽量采用建议的做法,因为之前自己也被-v的灵活语法弄得很是混乱,而且对理解概念没有帮助。
1 | # docker官方建议使用mount option,更verbose |
这样container只要将想persist的数据写入与host bind的mount point,就可以同时实现与host数据共享以及生命周期分离。
这样做非常方便,我们在build image后,运行docker时指定要挂载的host file/folder即可。开发自己docker container管理系统的同学一定熟悉类似的命令:
1 | docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer |
这是bind的一个典型用法:将host的docker daemon的unix sock文件,共享给了某个container,这样此container就可以通过读写本container的docker.sock文件,来调用host的docker API,实现host的container环境的管理了。
bind的一些典型用法:
- 在host与container之间共享配置,例如container bind host的/etc/resolv.conf,实现DNS
- 共享source code及release file,方便container运行
借助bind,实现了数据快速共享及persist,但这是在借助host的文件系统而实现的,有没有更通用的共享方案呢?于是就引出了Volume。
Persist and share using volume
先看下两段官方描述:
volumes are managed by Docker and are isolated from the core functionality of the host machine. A given volume can be mounted into multiple containers simultaneously. When no running container is using a volume, the volume is still available to Docker and is not removed automatically.
A data volume is a specially-designated directory within one or more containers that bypasses the Union File System.
我们先直观的感受一下volume:
创建一个名为 frank_test_vol 的volume
1 | docker volume create frank_test_vol |
可以看到刚刚创建的volume
1 | DRIVER VOLUME NAME |
查看volume详细信息:
1 | docker volume inspect frank_test_vol | jq |
得到
1 | [ |
发现Mountpoint在host上的一个路径,这里volume存储在host上:
1 | sudo ls -l /var/lib/docker/volumes/frank_test_vol/ |
得到
1 | drwxr-xr-x 3 root root 4096 11月 10 10:33 frank_test_vol |
如何使用这个volume呢:
1 | docker run -ti --rm --name frank_ubuntu \ |
这个命令–mount是在runtime将已经建立的volume mount到container的/my_vol处,而真正存储是在host的/var/lib/docker/volumes/frank_test_vol/
可以查看container的mount信息感受一下与bind的区别:
1 | docker inspect -f '{{json .Mounts}}' frank_ubuntu |
得到
1 | [ |
可以看到Type是volume而不是之前的bind,同样能看到mount的source和destination信息
目录,这样共享就完成了,而且任何一方对volume的改动都会被对方立刻看到:
1 | # in container |
查看host/container上的对应目录,均会发现两个文件
1 | -rw-r--r-- 1 root root 0 Nov 10 00:15 hello_from_container |
可能你会问,假如container运行时想mount到某处,可以image在此处已经有entry,或者说有folder/file存在,会怎么样? 答案就是会覆盖image的,以运行时挂载的为准。
我们可以看到,volume是独立于container而创建和manage的,container可以在运行时mount volume,而且此volume可以同时mount到多个container
总结一下,volume有如下feature:
- 跨平台,支持windows和linux
- Volume本身bypass了Union File System
- Data volumes can be shared and reused among containers
- Changes to a data volume are made directly
- Changes to a data volume will not be included when you update an image
- Data volumes persist even if the container itself is deleted
- Volume drivers let you store volumes on remote hosts or cloud providers, to encrypt the contents of volumes, or to add other functionality.
- New volumes can have their content pre-populated by a container(下面会谈到)
如果container有想独立于image的数据,可以将其存储于volume上,就像独立的u盘,于自己独立,而且可以很方便的与他人分享。
那这样每次我们想使用volume得先在命令行创建volume,再在runtime通过命令行参数绑定此volume,步骤比较繁琐。有时我们需要container运行起来,自动创建其container独有的volume,存放此次运行的log, data。这时匿名volume就会非常有帮助。
volume分为named和anonymous两种,区别是named就像我们之前演示的那样,我们手动创建,并赋予其name,而anonymous是在container运行时pre-populated,也就是自动创建的,并非没有名字,而是一串保证不会重复的随机串作为其名字,既然想自动化,而非命令行手动创建并在docker run时mount,那就需要我们记录在Dockerfile中,这就是Dockerfile的VOLUME命令出现的原因:
如下Dockerfile
1 | FROM ubuntu:18.04 |
VOLUME命令会干3件事:
- 创建一个匿名volume
- 拷贝 container 当前 /my_tmp位置的内容到此匿名volume
- mount volume到container的/my_tmp位置
build并运行
1 | docker build --no-cache -t frank/ubuntu . |
查看系统volume会发现匿名volume
1 | DRIVER VOLUME NAME |
inspect container, 仅显示Mounts
1 | docker inspect -f '{{json .Mounts}}' frank_ubuntu | jq |
得到
1 | [ |
我们可以看到volume在host上存储的位置,如果查看会发现docker image中建立的文件/文件夹已经被加入到新建立的匿名卷
1 | /var/lib/docker/volumes/9cf0b9e60303d021796ee4fa2c661e8287fcfab43257bcd6d7ffe3c9a07717ba/_data/ |
但是并没有 /my_tmp/eg_test_2/should_not_in_volume,在我们的Dockerfile中,此文件是在Volume命令之后加入到docker image的,因为docker构建时顺序执行,这时自然不会将还没有建立的文件拷贝到匿名volume中。
用docker命令行–mount也可以达到一样的效果
去掉上面Dockerfile的VOLUME命令:
1 | FROM ubuntu:18.04 |
1 | docker run -ti --rm --name frank_ubuntu --mount type=volume,target=/my_tmp |
和VOLUME效果一样,先创建一个空匿名Volume,如果mount的target不存在,创建一个空folder;如果存在,则把当前存在的内容拷贝至创建的Volume中。
clean
最后我们清理一下刚才测试过程中产生的垃圾
1 | # 测试环境无所谓,干活时慎重使用! |
Conclusion
本文从container数据持久化说起,引出bind: share host及container folder;之后提到更通用的方案: volume;有两种类型的volume: named及anonymous; 对于anonymous volume,两种使用方式: cmdline及Dockerfile的Volume指令。
- bind用在简单共享文件,配置,源代码
- 仅想expose, persist container的数据(如程序log),anonymous volume是首选,最方便的是Dockerfile的VOLUME命令,每次启动container会自动创建匿名volume
- docker rm –rm 选项会删除随container创建的匿名volume
- 尽量用–mount而非-v选项,-v选项几种支持的模式语法上很模糊,容易混淆。
- volume是存储的抽象,对container提供了同样的访问接口。不仅支持存放在host的文件系统,也支持NFS等。
常见的Volume命令总结:
1 | docker volume create my-vol |
Reference
Manage data in Docker
Understanding Volumes in Docker
Use bind mounts
Use volumes
Dockerfile reference
VOLUME 定义匿名卷