第4章 存储问题

4.1 容器磁盘能够限制配额么?

对于 devicemapper, btrfs, zfs 来讲,能够经过 --storage-opt size=100G 这种形式限制 rootfs 的大小。php

docker create -it --storage-opt size=120G fedora /bin/bash

参考官网文档:https://docs.docker.com/engine/reference/commandline/run/#/set-storage-driver-options-per-container前端

4.2 容器内的数据该保存在镜像里仍是物理机里?

若是所谓数据是指运行时动态的数据,那么这部分数据文件不该该保存于镜像内。在运行时要保持容器基础文件不可变的特性,而变化部分使用挂载宿主目录,或者数据卷来解决。
建议看一下官网 docker volume 的文档:https://docs.docker.com/engine/tutorials/dockervolumes/node

4.3 看到总说要保持容器无状态,那什么是无状态?

这里说到的有两个层面的无状态:
容器存储层的无状态
这里提到的存储层是指用于存储镜像、容器各个层的存储,通常是 Union FS,如 AUFS,或者是使用块设备的一些机制(如 snapshot )进行模拟,如 devicemapper。
Union FS 这类存储系统,至关因而在现有存储上,再加一层或多层存储,这类存储的读写性能并很差。而且对于 CentOS 这类只能使用 devicemapper 的系统而言,存储层的读写还常常出 bug。所以,在 Docker 使用过程当中,要避免存储层的读写。频繁读写的部分,应该使用卷。须要持久化的部分,可使用命名卷进行持久化。因为命名卷的生存周期和容器不一样,容器消亡重建,卷不会跟随消亡。因此容器能够随便删了从新run,而其挂载的卷则会保持以前的数据。
服务层面的无状态
使用卷持久化容器状态,虽然从存储层的角度看,是无状态的,可是从服务层面看,这个服务是有状态的。
从服务层面上说,也存在无状态服务。就是说服务自己不须要写入任何文件。好比前端 nginx,它不须要写入任何文件(日志走Docker日志驱动),中间的 php, node.js 等服务,可能也不须要本地存储,它们所需的数据都在 redis, mysql, mongodb 中了。这类服务,因为不须要卷,也不发生本地写操做,删除、重启、不保存自身状态,并不影响服务运行,它们都是无状态服务。这类服务因为不须要状态迁移,不须要分布式存储,所以它们的集群调度更方便。
以前没有 docker volume 的时候,有些人说 Docker 只能够支持无状态服务,缘由就是只看到了存储层需求无状态,而没有 docker volume 的持久化解决方案。
如今这个说法已经不成立,服务能够有状态,状态持久化用 docker volume。
当服务能够有状态后,若是使用默认的 local 卷驱动,而且使用本地存储进行状态持久化的状况,单机服务、容器的再调度运行没有问题。可是顾名思义,使用本地存储的卷,只能够为当前主机提供持久化的存储,而没法跨主机。
但这只是使用默认的 local 驱动,而且使用 本地存储 而已。使用分布式/共享存储就能够解决跨主机的问题。docker volume 天然支持不少分布式存储的驱动,好比 flocker、glusterfs、ceph、ipfs 等等。经常使用的插件列表能够参考官方文档:https://docs.docker.com/engine/extend/legacy_plugins/#/volume-pluginspython

4.4 数据容器、数据卷、命名卷、匿名卷、挂载目录这些都有什么区别?

首先,挂载分为挂载本地宿主目录 和 挂载数据卷(Volume)。而数据卷又分为匿名数据卷和命名数据卷。
绑定宿主目录的概念很容易理解,就是将宿主目录绑定到容器中的某个目录位置。这样容器能够直接访问宿主目录的文件。其形式是mysql

docker run -d -v /var/www:/app nginx

这里注意到 -v 的参数中,前半部分是绝对路径。在 docker run 中必须是绝对路径,而在 docker-compose 中,能够是相对路径,由于 docker-compose 会帮你补全路径。nginx

另外一种形式是使用 Docker Volume,也就是数据卷。这是不少看古董书的人不了解的概念,不要跟数据容器(Data Container)弄混。数据卷是 Docker 引擎维护的存储方式,使用 docker volume create 命令建立,能够利用卷驱动支持多种存储方案。其默认的驱动为 local,也就是本地卷驱动。本地驱动支持命名卷和匿名卷。web

顾名思义,命名卷就是有名字的卷,使用 docker volume create --name xxx 形式建立并命名的卷;而匿名卷就是没名字的卷,通常是 docker run -v /data 这种不指定卷名的时候所产生,或者 Dockerfile 里面的定义直接使用的。
有名字的卷,在用过一次后,之后挂载容器的时候还可使用,由于有名字能够指定。因此通常须要保存的数据使用命名卷保存。redis

而匿名卷则是随着容器创建而创建,随着容器消亡而淹没于卷列表中(对于 docker run 匿名卷不会被自动删除)。对于二代 Swarm 服务而言,匿名卷会随着服务删除而自动删除。 所以匿名卷只存放可有可无的临时数据,随着容器消亡,这些数据将失去存在的意义。sql

此外,还有一个叫数据容器 (Data Container) 的概念,也就是使用 --volumes-from 的东西。这早就不用了,若是看了书还在说这种方式,那说明书已通过时了。按照今天的理解,这类数据容器,无非就是挂了个匿名卷的容器罢了。mongodb

在 Dockerfile 中定义的挂载,是指 匿名数据卷。Dockerfile 中指定 VOLUME 的目的,只是为了将某个路径肯定为卷。

咱们知道,按照最佳实践的要求,不该该在容器存储层内进行数据写入操做,全部写入应该使用卷。若是定制镜像的时候,就能够肯定某些目录会发生频繁大量的读写操做,那么为了不在运行时因为用户疏忽而忘记指定卷,致使容器发生存储层写入的问题,就能够在 Dockerfile 中使用 VOLUME 来指定某些目录为匿名卷。这样即便用户忘记了指定卷,也不会产生不良的后果。

这个设置能够在运行时覆盖。经过 docker run 的 -v 参数或者 docker-compose.yml 的 volumes 指定。使用命名卷的好处是能够复用,其它容器能够经过这个命名数据卷的名字来指定挂载,共享其内容(不过要注意并发访问的竞争问题)。

好比,Dockerfile 中说 VOLUME /data,那么若是直接 docker run,其 /data 就会被挂载为匿名卷,向 /data 写入的操做不会写入到容器存储层,而是写入到了匿名卷中。

可是若是运行时 docker run -v mydata:/data,这就覆盖了 /data 的挂载设置,要求将 /data 挂载到名为 mydata 的命名卷中。

因此说 Dockerfile 中的 VOLUME 其实是一层保险,确保镜像运行能够更好的遵循最佳实践,不向容器存储层内进行写入操做。

数据卷默承认能会保存于 /var/lib/docker/volumes,不过通常不须要、也不该该访问这个位置。

4.5 卷和挂载目录有什么区别?

卷 (Docker Volume) 是受控存储,是由 Docker 引擎进行管理维护的。所以使用卷,你能够没必要处理 uid、SELinux 等各类权限问题,Docker 引擎在创建卷时会自动添加安全规则,以及根据挂载点调整权限。而且能够统一列表、添加、删除。另外,除了本地卷外,还支持网络卷、分布式卷。

而挂载目录那就没人管了,属于用户自行维护。你就必须手动处理全部权限问题。特别是在 CentOS 上,不少人碰到 Permission Denied,就是由于没有使用卷,而是挂载目录,并且还对 SELinux 安全权限一无所知致使。

4.6 为何绑定了宿主的文件到容器,宿主修改了文件,容器内看到的仍是旧的内容啊?

在绑定宿主内容的形式中,有一种特殊的形式,就是绑定宿主文件,既:

docker run -d -v $PWD/myapp.ini:/app/app.ini myapp

在 myapp.ini 文件不发生改变的状况下,这样的绑定是和绑定宿主目录性质同样,一样是将宿主文件绑定到容器内部,容器内能够看到这个文件。可是,一旦文件发生改变,状况则有不一样。

简单的文件修改,好比 echo "name = jessie" >> myapp.ini,这类修改依旧仍是原来的文件,宿主(或容器)对文件进行的改动,另外一方是能够看到的。

而复杂的文件操做,好比使用 vim,或者其它编辑器编辑文件,则颇有可能会致使一方的修改,另外一方看不到。

其缘由是这类编辑器在保存文件的时候,常常会采用一种避免写入过程当中发生故障而致使文件丢失的策略,既先把内容写到一个新的文件中去,写好了后,再删除旧的文件,而后把新文件更名为旧的文件名,从而完成保存的操做。从这个操做流程能够看出,虽然修改后的文件的名字和过去同样,但对于文件系统而言是一个新的文件了。换句话说,虽然是同名文件,可是旧的文件的 inode 和修改后的文件的 inode 不一样。

$ ls -i
268541 hello.txt
$ vi hello.txt
$ ls -i
268716 hello.txt

如上面的例子能够看到,通过 vim 编辑文件后,inode 从 268541 变为了 268716,这就是刚才说的,名字仍是那个名字,文件已不是原来的文件了。
而 Docker 的 绑定宿主文件,实际上在文件系统眼里,针对的是 inode,而不是文件名。所以容器内所看到的,依旧是以前旧的 inode 对应的那个文件,也就是旧的内容。
这就出现了以前的那个问题,在宿主内修改绑定文件的内容,结果发现容器内看不到改变,其缘由就在于宿主的那个文件已不是原来的文件了😂。
这类问题解决办法很简单,若是文件可能改变,那么就不要绑定宿主文件,而是绑定一个宿主目录,这样只要目录不跑,里面文件爱咋改就咋改😁。

4.7 多个 Docker 容器之间共享数据怎么办?NFS ?

若是是同一个宿主,那么能够绑定同一个数据卷,固然,程序上要处理好并发问题。
若是是不一样宿主,则可使用分布式数据卷驱动,让分布在不一样宿主的容器均可以访问到的分布式存储的位置。如S3之类:
https://docs.docker.com/engine/extend/plugins/#volume-plugins

4.8 既然一个容器一个应用,那么我想在该容器中用计划任务 cron 怎么办?

cron 实际上是另外一个服务了,因此应该另起一个容器来进行,如需访问该应用的数据文件,那么能够共享该应用的数据卷便可。而 cron 的容器中,cron 之前台运行便可。
好比,咱们但愿有个 python 脚本能够定时执行。那么能够这样构建这个容器。
首先基于 python 的镜像定制:

FROM python:3.5.2
ENV TZ=Asia/Shanghai
RUN apt-get update \
&& apt-get install -y cron \
&& apt-get autoremove -y
COPY ./cronpy /etc/cron.d/cronpy
CMD ["cron", "-f"]

其中所说起的 cronpy 就是咱们须要计划执行的 cron 脚本。

* * * * * root /app/task.py >> /var/log/task.log 2>&1

在这个计划中,咱们但愿定时执行 /app/task.py 文件,日志记录在 /var/log/task.log 中。这个 task.py 是一个很是简单的文件,其内容只是输出个时间而已。

#!/usr/local/bin/python
from datetime import datetime
print("Cron job has run at {0} with environment variable ".format(str(datetime.now())))
这 task.py 能够在构建镜像时放进去,也能够挂载宿主目录。在这里,我以挂载宿主目录举例。
# 构建镜像
docker build -t cronjob:latest .
# 运行镜像
docker run \
--name cronjob \
-d \
-v $(pwd)/task.py:/app/task.py \
-v $(pwd)/log/:/var/log/ \
cronjob:latest

须要注意的是,应该在构建主机上赋予 task.py 文件可执行权限。

4.9 如何初始化卷?

卷(Volume),是用于动态数据持久化的。所以其内存储的都是动态数据,运行时会变化。若是这里面须要初始化里面的数据,须要在运行时进行。或者在镜像里加入初始化的脚本,好比 mysql 镜像中的初始化目录中的脚本;或者本身单独制做纯粹用于初始化卷用的镜像,单独一次性运行以将初始化数据灌入卷中。
举个例子来讲,假设你须要个卷 mydata,而后里面须要有个 hello.txt 文件是必须存在的,不然容器运行就要出大事儿了……(这需求很傻我知道……😅好吧,假设如此)。
固然,咱们得先有这个卷。

docker volume create --name mydata

那怎么把这个超重要的 hello.txt 文件放入卷中呢?有几种办法。

正常挂载该 mydata 卷,而后 docker cp 进去
这是个很傻的办法,不过若是容器运行并不依赖于 hello.txt 的话,这样作是能够的。

$ docker run -d --name web -v mydata:/data nginx
$ docker cp ./hello.txt web:/data/

这样是先让容器启动,启动后,再把所需数据导入卷里面去。之后容器就可使用 /data/hello.txt 文件了。
可是,若是容器是严重依赖于这个 hello.txt 文件的话,这样作就会出问题。容器会由于 hello.txt 文件不存在,而报错退出,致使根本没有 docker cp 的机会。
这种状况,咱们能够变通一下。

$ docker run --rm \
-v $PWD:/source \
-v mydata:/data \
busybox \
cp /source/hello.txt /data/
$ docker run -d --name web -v mydata:/data nginx

这里咱们先启动了一个 busybox 容器,分别挂载要复制的源以及目标的 mydata 卷,而后用 cp 命令将 hello.txt 复制到 mydata 中去。数据导入结束后,咱们再正式挂载 mydata 卷到正式的容器上并启动。这个时候严重依赖 /data/hello.txt 的这个容器就能够顺利运行了。
专门制做初始化镜像 #
手动的去执行 docker cp,或者 docker run ... cp ... 并非很正规。能够写个脚本让一切都标准化,可是,除了流程外,还须要确保当前环境中的初始化数据的版本必须是所指望的,不然初始化了错误的数据,也会让运行时状态达不到预期的效果。
所以,另外一种办法是专门制做一个初始化卷的镜像,这样的作法也比较方便在 CI/CD 流程中对初始化数据的过程进行测试确认。

FROM busybox
COPY hello.txt /source/
VOLUME /data
CMD ["cp", "/source/hello.txt", "/data/"]

这样的镜像只有一个生存目的,就是挂载 mydata 卷,而且把数据导入进去。假设构建好的镜像名为 volume-prepare,只须要执行下面的命令就能够完成导入:

$ docker run --rm -v mydata:/data volume-prepare

在镜像的 Dockerfile 制做中,加入初始化部分
在以前的问答中咱们已经了解到,官方镜像 mysql 中可使用 Dockerfile 来添加初始化脚本,而且会在运行时判断是否为第一次运行,若是确实须要初始化,则执行定制的初始化脚本。
咱们也可使用这种方法将 hello.txt 在初始化的时候加入到 mydata 卷中去。
首先咱们须要写一个进入点的脚本,用以确保在容器执行的时候都会运行,而这个脚本将判断是否须要数据初始化,而且进行初始化操做。

#!/bin/bash
# entrypoint.sh
if [ ! -f "/data/hello.txt" ]; then
cp /source/hello.txt /data/
fi
exec "$@"

名为 entrypoint.sh 的这个脚本很简单,判断一下 /data/hello.txt 是否存在,若是不存在就须要初始化。初始化行为也很简单,将实现准备好的 /source/hello.txt 复制到 /data/ 目录中去,以完成初始化。程序的最后,将执行送入的命令。
咱们能够这样写 Dockerfile:

FROM nginx
COPY hello.txt /source/
COPY entrypoint.sh /
VOLUME /data
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

当咱们构建镜像、启动容器后,就会发现 /data 目录下已经存在了 hello.txt 文件了,初始化成功了。

4.10 为何说数据库不适合放在 Docker 容器里运行?

不为何,由于这个说法不对,大部分认为数据库必须放到容器外运行的人根本不知道 Docker Volume 为什么物。
在早年 Docker 没有 Docker Volume 的时候,其数据持久化是一个问题,可是这已经不少年过去了。如今有 Docker Volume 解决持久化问题,从本地目录绑定、受控存储空间、块设备、网络存储到分布式存储,Docker Volume 都支持,不存在数据读写类的服务不适于运行于容器内的说法。
Docker 不是虚拟机,使用数据卷是直接向宿主写入文件,不存在性能损耗。并且卷的生存周期独立于容器,容器消亡卷不消亡,从新运行容器能够挂载指定命名卷,数据依然存在,也不存在没法持久化的问题。
建议去阅读一下官方文档:
https://docs.docker.com/engine/tutorials/dockervolumes/
https://docs.docker.com/engine/reference/commandline/volume_create/
https://docs.docker.com/engine/extend/legacy_plugins/#/volume-plugins

4.11 如何列出容器和所使用的卷的关系?

要感谢强大的 Go Template,可使用下面的命令来显示:

docker inspect --format '{{.Name}} => {{with .Mounts}}{{range .}}
    {{.Name}},{{end}}{{end}}' $(docker ps -aq)

注意这里的换行和空格是有意如此的,这样就能够再返回结果控制缩进格式。其结果将是以下形式:

$ docker inspect --format '{{.Name}} => {{with .Mounts}}{{range .}}
    {{.Name}}{{end}}{{end}}' $(docker ps -aq)
/device_api_1 =>
/device_dashboard-debug_1 =>
/device_redis_1 =>
    device_redis-data
/device_mongo_1 =>
    device_mongo-data
    61453e46c3409f42e938324d7feffc6aeb6b7ce16d2080566e3b128c910c9570
/prometheus_prometheus_1 =>
    fc0185ed3fc637295de810efaff7333e8ff2f6050d7f9368a22e19fb2c1e3c3f
相关文章
相关标签/搜索