容器的持久化方式

2016/9/9 posted in  Docker

容器作为镜像的实例,是镜像中无状态应用的运行时。所谓没有状态,因为容器的生命周期非常灵活。当一个容器的生命周期结束,期间产生的数据不会持久化,而是随着容器的删除而被删除。但是,大多数应用都是为数据服务了,那么,这里就探讨下容器的持久化问题。

容器的存储

在讨论容器的持久化之前,先探索下,没有持久化,也就是一个普通的容器的数据存储是什么样子的。
我们先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

正如所知道的,镜像是分层存储的,而容器只不过是以镜像的若干层为基础,在上面有增加了一可读可写层作为数据的存储。

2016-09-11_21:41:49.jpg

因此,很明白,容器的存储只不过就是把容器内的文件映射到主机的某一个目录上,而这个目录就是这一层可读可写的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进行使用。