主机或容器中的目录树是由一组挂载点创建的,这些挂载点描述了如何对一个或多个文件系统进行拼接。
一个 Volume 就是主机目录树上的一部分挂载到容器目录树上的一个挂载点。下图中,一个 Volume 挂载到了 /data 目录,故对 /
的写操作会导向到已挂载的 UFS,而对 /data
的写操作,通过 Volume,会直接在主机的文件系统上操作。
Volume 能用来保存和共享数据,其所属领域和生命周期都独立于单个容器。
类似的数据有:
Volume 有助于架构组件的模块化。映像适合打包和分发相对静态的文件,比如程序; 而 Volume 用来保存动态数据或定制内容。这种区别使得能复用映像。
例如,MySQL 程序都是相同的,可以只在一个容器中运行,而不同的数据库内容可以使用 Volume 来注入。
Apache Cassandra 是一个列数据库 (column database),它内置有聚类 (clustering), 最终一致性(eventual consistency) 和线性写入可扩展性(linear write scalability) 等功能。该数据库也将数据保存在硬盘的文件上。
在本节中,我们先用其官方的映像创建一个单节点的 Cassandra 簇,创建一个 keyspace,删除容器,再在另一个容器中将原来的 keyspace 恢复到一个新的节点上。
先创建一个随带 Volume 的容器,这种定义了 Volume 的容器叫 volume container:
$ docker run -d \
--volume /var/lib/cassandra/data \ # 指定容器内的 Volume 挂载点
--name cass-shared \
alpine echo Data Container
该容器会立即中止。先不要删除,下面开启的 Cassandra 容器要使用该容器创建的 Volume:
$ docker run -d \
--volumes-from cass-shared \
--name cass1 \
cassandra:2.2
该 Cassandra 容器会从 cass-shared 容器复制 Volume 的定义。之后,这两个容器的 Volume 都会挂载到容器的 /var/lib/cassandra/data
,并且都指向主机目录树上的相同位置。
再基于 cassandra:2.2 映像开启一个新的容器,并在其中运行一个 Cassandra 客户端工具,并连接到运行的服务器:
$ docker run -it --rm \
--link cass1:cass \
cassandra:2.2 cqlsh cass
现在,可以在 CQLSH 命令行中对 Cassandra 数据库进行操作了。首先查找一个名为 docker_hello_world 的 keyspace:
cqlsh> select * from system.schema_keyspaces WHERE keyspace_name = 'docker_hello_world';
keyspace_name | durable_writes | strategy_class | strategy_options
---------------+----------------+----------------+------------------
(0 rows)
cqlsh>
Cassandra 应该返回一个空列,表示数据库还没有被修改。接着再创建该 keyspace:
create keyspace docker_hello_world
with replication = {
'class' : 'SimpleStrategy',
'replication_factor': 1
};
现在再用上面的查询语句查询应该会返回一条记录:
h> select * from system.schema_keyspaces WHERE keyspace_name = 'docker_hello_world';
keyspace_name | durable_writes | strategy_class | strategy_options
--------------------+----------------+---------------------------------------------+----------------------------
docker_hello_world | True | org.apache.cassandra.locator.SimpleStrategy | {"replication_factor":"1"}
(1 rows)
cqlsh>
退出 CQLSH,从而结束该容器。由于该容器创建时有 --rm
选项,故当其结束时会自动被删除。接着再删除掉之前创建的 Cassandra 节点:
$ docker stop cass1
$ docker rm -vf cass1
这里,删除 cass1 容器后,由于数据是通过 Volume 直接保存到主机上的,故数据应该还在。
下面通过创建一个新的 Cassandra 节点,查询该 keyspace 来验证:
$ docker run -d \
--volumes-from cass-shared \
--name cass2 \
cassandra:2.2
$ docker run -it --rm \
--link cass2:cass \
cassandra:2.2 \
cqlsh cass
cqlsh> SELECT * FROM system.schema_keyspaces WHERE keyspace_name ='docker_hello_world';
keyspace_name | durable_writes | strategy_class | strategy_options
--------------------+----------------+---------------------------------------------+----------------------------
docker_hello_world | True | org.apache.cassandra.locator.SimpleStrategy | {"replication_factor":"1"}
(1 rows)
以上查询返回了之前保存的 keyspace。
退出并删除这些测试的容器:
$ docker rm -vf cass2 cass-shared
共有两种 Volume 类型。每种 Volume 都是主机目录树的一个位置对应到容器内的目录树上的一个挂载点,其不同只在于主机上的位置。
例如,可在容器中运行 Apache2 服务,并将主机上的一个目录挂载到容器中的 /usr/local/apache2/htdocs/
,这样当项目内容修改后,无需重新创建或开启 Apache2 容器。这种使用方式最适合用于开发阶段。
$ docker run -d --name bmweb \
-v ~/example-docs:/usr/local/apache2/htdocs \
-p 80:80 http:latest
上面的 -v ~/example-docs:/usr/local/apache2/htdocs
创建了一个绑定的 Volume。-v
的规则是 -v path_on_host:path_in_container[:ro]
,如果选项最后加上 :ro
,则表示这个 Volume 是只读的,容器不可对其进行修改。
Bind mount volumes 存在的问题:
绑定时使用的主机上的位置都是由 Docker 创建并管理的,例子:
$ docker run -d \
-v /var/lib/cassandra/data \
--name cass-shared \
alpine echo Data Container
由于 -v
没有指定主机位置,这就创建了一个 Docker-managed volume。如果要找出这个 Volume 绑定到主机上的确切哪个位置,可以用 docker inspect
命令:
$ docker inspect -f "" cass-shared
{"/var/lib/cassandra/data":"/mnt/sda1/var/lib/docker/vfs/dir/632fa59c..."}
Docker-managed volume 解耦了 volume 和主机上具体文件系统位置,很适合在大型系统中用于组织数据。
多个容器通过 Volume 绑定到主机上的相同位置:
$ mkdir ~/web-logs-example
$ docker run --name plath -d \
-v ~/web-logs-example:/data \
dockerinaction/ch4_write_a # 容器日志写到该目录
$ docker run --rm \
-v ~/web-logs-example:/reader-data \
alpine:latest \
head /reader-data/logA # 绑定到相同的位置,进行读操作
$ cat ~/web-logs-example/logA # 直接在主机上查看
$ docker stop plath
下面这个例子中,4 个容器的 Volume 都绑定到主机上的相同位置,2 个读,2 个写:
$ docker run --name woolf -d \
--volume ~/web-logs-example:/data \
dockerinaction/ch4_write_a
$ docker run --name alcott -d \
--volume ~/web-logs-example:/data \
dockerinaction/ch4_write_b
$ docker run --rm --entrypoint head \
-v ~/web-logs-example:/towatch:ro \
alpine:latest \
/towatch/logA
$ docker run --rm \
-v ~/web-logs-example:/toread:ro \
alpine:latest \
head /towatch/logB
这种类型的共享方式维护成本较高。
docker run
参数的 volumes-from
选项可以将一个或多个其它容器上的 volume 定义直接复制到当前新建的容器上,例如:
$ docker run --name fowler \
-v ~/example-books:/library/PoEAA \
-v /library/DSL \
alpine:latest \
echo "Fowler collection created."
$ docker run --name knuth \
-v /library/TAoCP.vol1 \
-v /library/TAoCP.vol2 \
-v /library/TAoCP.vol3 \
-v /library/TAoCP.vol4.a \
alpine:latest \
echo "Knuth collection created."
$ docker run --name reader \
--volumes-from fowler \
--volumes-from knuth \
alpine:latest ls -l /library/ # 会列出 fowler 和 knuth 的所有 Volume
$ docker inspect --format "" reader # 检查 reader 的 Volume
也可以先将要整合的 Volume 先集合到一个单一容器,再从该容器复制:
$ docker run --name aggregator \
--volumes-from fowler \
--volumes-from knuth \
alpine:latest \
echo "Collection Created." # Create an aggregaton
$ docker run --rm \
--volumes-from aggregator \
alpine:latest ls -l /library/ # 会列出 fowler 和 knuth 的所有 Volume
通过 -volumes-from
选项进行的是完全一致的复制,不可对绑定位置、ro 只读权限等进行配置或修改, 并且当多个 Volume 定义中绑定的位置相同时,会有覆盖情况发生。
它们的生命周期独立于任何的容器,并且只能通过使用它的容器才可对其进行引用。
Managed volumes 是二等实体 (second-class entities),无法共享或删除某个特定的 managed volume,因为无法对其进行标识 (只能通过使用它的容器进行引用)。
最可靠的方式是对应每个 Managed volume 开启一个容器。当多个容器使用同一个 Managed volume 时,volume 的引用计数会增加,这种操作和编程语言中的变量引用类似。只有当引用计数到 0 时,才会删除该 Managed volume。
Docker 不会自己删除 Managed volume。当删除容器时,docker rm -v
会尝试一并删除它的 Managed volume,如果该 Volume 也被其它容器使用,则不会删除,只会减少对它的引用计数。
没有被使用的 Managed volume 为孤儿 Volume,可以使用一些脚本来清理。因此,在删除容器时使用 -v
选项是一个好习惯。同时,还建议使用下节介绍的 Volume 容器模式。
下面的命令可以删除所有已关闭的容器,并清理掉它们的 Managed Volume:
$ docker rm -v $(docker ps -aq)
创建一个容器(假设容器名为 vc_data)专门用于定义 Volumes,这个 vc_data 容器可以不运行(在上面的例子中可看也,这种容器只运行了下 echo "Container Created"
),因为停止的容器也会保留对 Volume 的引用。然后其它的容器在创建或运行时通过 --volumes-from
从 vc_data 容器复制 Volume 定义。
使用这种模式要注意:
vc_
,表示 volume container这种模式中,vc 容器保持了一个对数据的引用,从而便于进行备份、恢复和数据迁移操作。
用例:假设要升级一个数据库软件,如果数据库容器将数据写入到了一个 Volume,而该 Volume 是由一个 vc 容器定义的,那么在数据迁移时,只需关闭原来的数据库容器,然后再将原来的 vc 容器作为 Volume 源,再开启新的数据库容器即可。
这种模式扩展至 Volume container 模式。Data-packed volume container 不仅定义 Volume,而且将从本容器的映像中的一些数据(如静态文件、配置数据、代码等)复制到这个定义的 Volume 中,从而可与其它容器共享。
$ docker run --name dpvc \
-v /config \
dockerinaction/ch4_packed \
/bin/sh -c 'cp /packed/* /config/' # copy image content into a volume
$ docker run --rm --volumes-from dpvc \
alpine:latest ls /config # list shared material
$ docker run --rm --volumes-from dpvc \
alpine:latest cat /config/packedData
$ docker rm -v dpvc # remember to use -v when clean up
多态模式下,只提供一个统一的接口,但是能有不同的实现。具体到 Volume,可以在不对映像进行修改的情况下,注入不同的行为。
一个多态容器中的一些功能可以通过 Volumes 来替代。例如,假设一个 Node.js 映像默认会执行命令 /app/app.js
,该映像中的默认实现是输出 This is a Node.JS application
。
要修改该容器的行为,在创建该容器时,只需将自己的 app.js
实现通过 Volume 注入到 /app/app.js
即可。这种方式的适合使用情况:
可以将一些工具做成一个 data-packed volume container,然后和其它容器共享,在其它容器中使用这些工具来进行一些事件处理操作,例如:
$ docker run --name tools \
dockerinaction/ch4_tools # create data-packed volume container with tools
$ docker run --rm \
--volumes-from tools \
alpine:latest \
ls /operations/* # list shared tools
$ docker run -d --name important_app \
--volumes-from tools \
dockerinaction/ch4_ia # start another container with shared tools
$ docker exec important_app \
/operations/tools/someTool # use shared tool in running container
$ docker rm -vf important_app
$ docker rm -v tools
也可以将文件注入到只读的容器中。通常会用多态容器来注入应用的配置信息。
开发阶段:
$ docker run --name devConf \
-v /config \
dockerinaction/ch4_packed_config:latest \
/bin/sh -c 'cp /development/* /config/'
$ docker run --name devApp \
--volumes-from devConf \
dockerinaction/ch4_polyapp
生产环境中:
$ docker run --name prodConf \
-v /config \
dockerinaction/ch4_packed_config:latest \
/bin/sh -c 'cp /production/* /config/'
$ docker run --name prodApp \
--volumes-from prodConf \
dockerinaction/ch4_polyapp
参考文献: