容器作为镜像的实例,是镜像中无状态应用的运行时。所谓没有状态,因为容器的生命周期非常灵活。当一个容器的生命周期结束,期间产生的数据不会持久化,而是随着容器的删除而被删除。但是,大多数应用都是为数据服务了,那么,这里就探讨下容器的持久化问题。
容器的存储
在讨论容器的持久化之前,先探索下,没有持久化,也就是一个普通的容器的数据存储是什么样子的。
我们先run一个容器:
# docker run -d ubuntu sleep 100000
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3a8fed62029c ubuntu "sleep 100000" About a minute ago Up About a minute hopeful_kilby
因为容器的本质也就是一个进程,那么,容器内产生的所有的文件肯定也是通过某种方式存储到宿主机上的,我们可以exec进入这个容器,创建一个文件出来:
# docker exec -it 3a8fed62029c bash
root@3a8fed62029c:/# cd home/
root@3a8fed62029c:/home# echo "dao" > hello
root@3a8fed62029c:/home# ls
hello
root@3a8fed62029c:/home# cat hello
dao
创建完成后,可以在/var/lib/docker/aufs/mnt
下找到这个文件:
root@ubuntu:/var/lib/docker/aufs/mnt# find -name hello
./24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf/home/hello
而进入找到这个路径/var/lib/docker/aufs/mnt/24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf
就会发现,这就是容器下的整个文件系统。
如果尝试在宿主机下的这个目录中创建若干文件,则在容器中也同样是可以找到的。
其中的原理也很简单,我们可以在/var/lib/docker/aufs/layers
下找到答案:
root@ubuntu:/var/lib/docker/aufs/layers# cat 24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf
24c7a79f3a3bcb9320e028dab081eec7f55a5a8fb8eb2201d9cfe7b1d9d0e7bf-init
d7b377dff0a5d7b84ae6f30cab5d94b668406e30e8f1584e0eab567b45e27a60
e00588b1d53b6376da8ac07a5176bf44c246a5c1077fd56cf41979565ab8e290
5c05060aac0e7a11db24402fc2639d3fd47dc3ab8a996d279a28ac6d73d1217b
3cba5f639a5bca0a7d7d4b28b68151d8a10101583f1b59f48328ade9f7c668dd
ac5fe0af9b19fe9bb88448932b3c7866e942dbdc9666457195183a2af7caf9f9
正如所知道的,镜像是分层存储的,而容器只不过是以镜像的若干层为基础,在上面有增加了一可读可写层作为数据的存储。
因此,很明白,容器的存储只不过就是把容器内的文件映射到主机的某一个目录上,而这个目录就是这一层可读可写的layer。不过,当容器的生命周期结束,这个宿主机的文件夹也就不复存在。
也因为这个原因,我们需要考虑如何把容器产生的数据持久化。
持久化与Volume
把容器产生的数据持久化有两个中方法,基本就是两个完全不同的思路。第一种就是通过docker commit,把和容器的生命周期一样的临时的layer持久化:
# docker commit 3a8fed62029c
sha256:c82ef37ba95e8cc6b03a487294385e76d774f46bb225496f9278b50c0137175c
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> c82ef37ba95e 3 seconds ago 126.6 MB
这样,已经存有数据的layer便永久的存储到新生成的镜像中,想再使用数据时可以通过实例一个新的镜像就可以。
但是,这样的局限性很明显,也更不能满足运行时的需求,因此,我们可以考虑下第二种的持久化方式,Volume。
Volume使用起来也非常简答,在run的时候加一个-v参数就可以了,我们从使用-v参数的三种方式探究下volume。
第一种方式,也就是最简单的,直接挂载一个volume到容器。
我们run一个容器,并挂在一个volume:
# docker run -d -v /volume ubuntu sleep 100000
通过inspect命令,我们可以看到这个容器的volume信息:
"Mounts": [
{
"Name": "8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e",
"Source": "/var/lib/docker/volumes/8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e/_data",
"Destination": "/volume",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
我们可以看到,docker把本地/var/lib/docker/volumes下的一个文件件挂载到了容器的/volume,这个目录上。
当我们再次去/var/lib/docker/aufs/mnt下找打容器对应的目录,并在/volume这个目录下进行修改时,容器里的文件并不会发生什么变化,然而,在/var/lib/docker/volumes/8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e/_data这个目录下进行修改,在容器内就可以看到相应的变化。
但是,这次虽然使用了volume,但是到底和没有使用有什么区别呢?
区别就在于volume这个文件夹并不会和容器一样有相同的生命周期,而是在容器的生命周期结束后保留在宿主机上:
# docker volume ls
DRIVER VOLUME NAME
local 8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e
# docker rm -f f5390aec14bc
f5390aec14bc
# docker volume ls
DRIVER VOLUME NAME
local 8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e
那么这样就满足了对于没有状态的容器,持久化在他生命周期期间产生的数据的问题。不过这样还是有一些不方便,比如不方便管理volume,以及再run一个容器来继承使用前面的容器产生的数据。
而第二种方式便满足了这个问题。第二种方式就是把宿主机的一个目录挂在到容器。
我们可以run一个容器,把本地的/root/data这个目录挂载到容器的/volume下:
# docker run -d -v /root/data:/volume ubuntu sleep 1000000
9548d507859b6a331a167a8f60d6d96ccdf2b0244c6bdd24033ccf277b89d162
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9548d507859b ubuntu "sleep 1000000" 54 seconds ago Up 53 seconds drunk_swanson
我们依然可以通过inspect命令获得volume相关的信息:
"Mounts": [
{
"Source": "/root/data",
"Destination": "/volume",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
这时候就发现,docker十分省事的并没有在volume下创建一个文件夹,而是直接创建了/root/data,然后,直接把这个文件夹挂载到了容器内的/volume。
第三种方式,实例化一个数据卷容器,然后把这个数据卷容器挂载到新建的容器上。
而实例化一个数据卷容器,就有上面提到的两种方式,我们可以分别看下两种不同的数据卷对新创建的容器有什么影响。
我们先run一个容器,并以包含普通volume的容器作为数据卷容器(见上面第一种方式):
# docker run -d --volumes-from f5390aec14bc ubuntu sleep 100000
8cfe0eb466e1935cafe353195deebd0debd677433625d05770b2458755c1a32b
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8cfe0eb466e1 ubuntu "sleep 100000" 8 seconds ago Up 8 seconds evil_nobel
然后,带这个容器进行inspect:
"Mounts": [
{
"Name": "8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e",
"Source": "/var/lib/docker/volumes/8aa697c21e65e104dcb9f8b8507c905715d457ad88ee3c2e79a0c72ef07fff0e/_data",
"Destination": "/volume",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
我们可以看到和前面一样的信息,这个容器并没有创建任何volume,只不过是复用了数据卷容器的volume。
相似的,from一个第二种方式创建的数据卷容器,也会得到类似的结果:
"Mounts": [
{
"Source": "/root/data",
"Destination": "/volume",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
新创建的容器并没有创建volume,依然是直接使用了原有容器的volume,因此,如果多个容器需要共享一个文件目录,完全可以由一个容器创建一个volume,然后其他容器通过volume-from关联这个volume进行使用。