目录隐藏 |
容器磁盘可以限制配额么? | |
对于 devicemapper, btrfs, zfs 来说,可以通过 --storage-opt size=100G 这种形式限制 rootfs 的大小。 docker create -it --storage-opt size=120G fedora /bin/bash 参考官网文档: https://docs.docker.com/engine/reference/commandline/run/#/set-storage-driver-options-per-container |
|
容器内的数据该保存在镜像里还是物理机里? | |
如果所谓数据是指运行时动态的数据,那么这部分数据文件不应该保存于镜像内。在运行时要保持容器基础文件不可变的特性,而变化部分使用挂载宿主目录,或者数据卷来解决。 建议看一下官网 docker volume 的文档: https://docs.docker.com/engine/tutorials/dockervolumes/ |
|
看到总说要保持容器无状态,那什么是无状态? | |
这里说到的有两个层面的无状态: 容器存储层的无状态 这里提到的存储层是指用于存储镜像、容器各个层的存储,一般是 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-plugins |
|
数据容器、数据卷、命名卷、匿名卷、挂载目录这些都有什么区别? | |
首先,挂载分为挂载本地宿主目录 和 挂载数据卷(Volume)。而数据卷又分为匿名数据卷和命名数据卷。 绑定宿主目录的概念很容易理解,就是将宿主目录绑定到容器中的某个目录位置。这样容器可以直接访问宿主目录的文件。其形式是 docker run -d -v /var/www:/app nginx 这里注意到 -v 的参数中,前半部分是绝对路径。在 docker run 中必须是绝对路径,而在 docker-compose 中,可以是相对路径,因为 docker-compose 会帮你补全路径。 另一种形式是使用 Docker Volume,也就是数据卷。这是很多看古董书的人不了解的概念,不要跟数据容器(Data Container)弄混。数据卷是 Docker 引擎维护的存储方式,使用 docker volume create 命令创建,可以利用卷驱动支持多种存储方案。其默认的驱动为 local,也就是本地卷驱动。本地驱动支持命名卷和匿名卷。 顾名思义,命名卷就是有名字的卷,使用 docker volume create --name xxx 形式创建并命名的卷;而匿名卷就是没名字的卷,一般是 docker run -v /data 这种不指定卷名的时候所产生,或者 Dockerfile 里面的定义直接使用的。 有名字的卷,在用过一次后,以后挂载容器的时候还可以使用,因为有名字可以指定。所以一般需要保存的数据使用命名卷保存。 而匿名卷则是随着容器建立而建立,随着容器消亡而淹没于卷列表中(对于 docker run 匿名卷不会被自动删除)。对于二代 Swarm 服务而言,匿名卷会随着服务删除而自动删除。 因此匿名卷只存放无关紧要的临时数据,随着容器消亡,这些数据将失去存在的意义。 此外,还有一个叫数据容器 (Data Volume) 的概念,也就是使用 --volumes-from 的东西。这早就不用了,如果看了书还在说这种方式,那说明书已经过时了。按照今天的理解,这类数据容器,无非就是挂了个匿名卷的容器罢了。 在 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,不过一般不需要、也不应该访问这个位置。 |
|
卷和挂载目录有什么区别? | |
卷 (Docker Volume) 是受控存储,是由 Docker 引擎进行管理维护的。因此使用卷,你可以不必处理 uid、SELinux 等各种权限问题,Docker 引擎在建立卷时会自动添加安全规则,以及根据挂载点调整权限。并且可以统一列表、添加、删除。另外,除了本地卷外,还支持网络卷、分布式卷。 而挂载目录那就没人管了,属于用户自行维护。你就必须手动处理所有权限问题。特别是在 CentOS 上,很多人碰到 Permission Denied,就是因为没有使用卷,而是挂载目录,而且还对 SELinux 安全权限一无所知导致。 |
|
为什么绑定了宿主的文件到容器,宿主修改了文件,容器内看到的还是旧的内容啊? | |
在绑定宿主内容的形式中,有一种特殊的形式,就是绑定宿主文件,既: 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 对应的那个文件,也就是旧的内容。 这就出现了之前的那个问题,在宿主内修改绑定文件的内容,结果发现容器内看不到改变,其原因就在于宿主的那个文件已不是原来的文件了。 这类问题解决办法很简单,如果文件可能改变,那么就不要绑定宿主文件,而是绑定一个宿主目录,这样只要目录不跑,里面文件爱咋改就咋改。 |
|
多个 Docker 容器之间共享数据怎么办?NFS ? | |
如果是同一个宿主,那么可以绑定同一个数据卷,当然,程序上要处理好并发问题。 如果是不同宿主,则可以使用分布式数据卷驱动,让分布在不同宿主的容器都可以访问到的分布式存储的位置。如S3之类: https://docs.docker.com/engine/extend/plugins/#volume-plugins |
|
既然一个容器一个应用,那么我想在该容器中用计划任务 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 文件可执行权限。 |
|
如何初始化卷? | |
卷(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 文件了,初始化成功了。 |
|
为什么说数据库不适合放在 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 |
|
如何列出容器和所使用的卷的关系? | |
要感谢强大的 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 |