docker目录挂载、文件挂载问题集锦

docker提供服务时,为了保证宿主机与容器内部的数据同步,经常需要将需要的文件挂载到容器。docker启动时可以用-v完成,或在编排docker-compose的yml文件的volumes中添加挂载文件。

宿主机和容器文件不同步问题

问题描述

我们用如下所示nvidia/cuda作为基础镜像,主要说明挂在中遇到的问题及问题分析。

docker目录挂载、文件挂载问题集锦_文件系统

我们用nvidia/cuda创建容器并完成挂载,挂载文件为普通txt文本。

docker目录挂载、文件挂载问题集锦_docker_02

如下,我们用vim编辑宿主机的test.txt文本,添加一行测试文本并保存退存。

docker目录挂载、文件挂载问题集锦_文件系统_03

奇怪的是容器内的文件,并没有随着宿主机文件的改变而改变。

docker目录挂载、文件挂载问题集锦_文件系统_04

同样的道理,我们可以用vim修改容器中的文件,也会观察到宿主机的文件并没有随着容器中文件的改变而改变。

我们重新启动容器,换一种方式,在宿主机中用echo来给文件中追加内容。

docker目录挂载、文件挂载问题集锦_文件系统_05

如下图所示容器中的相应文件改变。

docker目录挂载、文件挂载问题集锦_centos_06

问题定位分析

通过查阅资料可知,docker在进行文件挂载时,并不是仅仅挂载文件名到对应位置,而是将文件对应的inode 进行映射。用vim进行文件的编辑并保存时,系统采用的是备份、替换的策略,文件用vim等工具编辑的过程实质是,备份原来的文件,当新文件编辑完成后,再将新文件替换文原件,这会导致文件的inode变化,所以docker内外的文件并不会同步。而用echo等重定向操作修改文件时,文件的inode保持不变,所以不会发生类似现象。

Docker文件挂载时大家需留意此类问题!

参考链接:https://zhuanlan.zhihu.com/p/82311376

inode解释不同步问题

在启动​ ​docker​ ​​容器时,为了保证一些基础配置与宿主机保持同步,通常需要将这些配置文件挂载进​ ​docker​ ​​容器,例如​ ​/etc/resolv.conf​ ​​/​ ​/etc/hosts​ ​​/​ ​/etc/localtime​ ​等。

当这些配置变化时,我们通常会修改这些文件。但是此时遇到了一个问题:

当在宿主机上修改这些文件后,​ ​docker​ ​容器内查看时,这些文件并未发生对应的修改。

然后通过查阅相关资料,发现该问题是由​ ​docker -v​ ​挂载文件和某些编辑器存储文件的行为共同导致 的。


  • docker 挂载文件时,并不是挂载了某个文件的路径,而是实打实的挂载了对应的文件,即挂载了某 个指定的​ ​inode​ ​文件。
  • 某些编辑器(vi)在编辑保存文件时,采用了​ ​备份、替换​ ​​的策略,即编辑过程中,将变更写入新文件, 保存时,再将备份文件替换原文件,此时会导致文件的​ ​inode​ ​发生变化。
  • 原​ ​inode​ ​对应的文件其实并没有发生修改。

因此,我们从宿主机上修改这些文件时,应该采用​ ​echo​ ​​重定向等操作,避免文件的​ ​inode​ ​发生变化。

附 ​ ​inode:​ ​http://www.ruanyifeng.com/blog/2011/12/inode.html

通过 ​ ​inode 这篇文章能很好的理解 Linux 的软链接和硬链接​

docker目录挂载、数据卷和文件复制

一、宿主机目录挂载

在使用vmware或是vbox时,我们可能经常会把物理机上的一个目录挂载成为虚拟机的一个目录或者盘符,作为更加轻量级的docker同样支持。docker也可以支持把一个宿主机上的目录挂载到镜像里。

docker run -it -v /:/usr/proot centos /bin/bash

通过-v参数,冒号前为宿主机目录,必须为绝对路径,冒号后为镜像内挂载的路径。

ls /usr/proot/

显示:

bin  boot  data  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

这样可以看到,直接把物理机的整体目录可以映射到docker容器中了。

默认挂载的路径权限为读写。如果指定为只读可以用:ro,如下所示:

docker run -it -v /:/usr/proot:ro centos /bin/bash

这样就可以限制为只读了。

二、数据卷

docker还提供了一种高级的用法,叫数据卷。

数据卷:其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的”。感觉像是由一个容器定义的一个数据挂载信息。其他的容器启动可以直接挂载数据卷容器中定义的挂载信息。

先创建一个数据卷:

docker run -itd --name zoleet-centos -v /data:/data centos

然后挂载:

docker run -it --volumes-from zoleet-centos --name zoleet-volumes-centos centos /bin/bash

这样我可以先进入第一个容器中创建并输入内容:

docker exec -it zoleet-centos /bin/bash

然后我们在/data目录下创建并输入一段文字:

echo 'this is docker volumes test...'>/data/test.txt

然后我们可以切换容器或进入使用卷的容器中:

docker exec -it zoleet-volumes-centos /bin/bash

然后在容器中查看刚刚我们的输入文件的信息:

cat /data/test.txt

就能看到我们刚刚输入的文件内容了。给我感觉卷就像是分布式文件系统一样,其他容器想要使用的时候只需要挂载就可以了。

挂载时也只需要通过–volumes-from参数指定要从哪个数据卷来挂载数据。

三、文件复制

很多时候我们需要从宿主机到容器或容器到宿主,但docker不支持容器到容器间进行文件复制。

从容器内拷贝文件到宿主机上:

docker cp zoleet-centos:/data/test.txt /root/

从宿主机内拷贝文件到容器上:

docker cp test.sql zoleet-centos:/data/

从容器A内拷贝文件到容器B上:

docker cp zoleet-volumes-centos:/testA.txt zoleet-centos:/data/

将显示:

copying between containers is not supported

文件复制整体来说,和hdfs文件操作差不多,但不支持容器与容器间的文件复制,但我们可以通过卷来解决这个问题。

给一个正在运行的Docker容器动态添加Volume

之前有人问我Docker容器启动之后还能否再挂载卷,考虑到mnt命名空间的工作原理,我一开始认为这很难实现。不过现在Petazzoni通过使用nsenter和绑定挂载实现了这个需求,你可以在你的环境中测试下。

之前有人问我Docker容器启动之后还能否再挂载卷,考虑mnt命名空间的工作原理,我一开始认为这很难实现。不过现在我认为是它实现的。

简单来说,要想将磁盘卷挂载到正在运行的容器上,我们需要:


  • 使用​ ​nsenter​ ​将包含这个磁盘卷的整个文件系统mount到临时挂载点上;
  • 从我们想当作磁盘卷使用的特定文件夹中创建绑定挂载(bind mount)到这个磁盘卷的位置;
  • umount第一步创建的临时挂载点。
  • 注意事项
  • 在下面的示例中,我故意包含了$符号来表示这是Shell命令行提示符,以帮助大家区分哪些是你需要输入的,哪些是机器回复的。有一些多行命令,我也继续用>。我知道这样使得例子里的命令无法轻易得被拷贝粘贴。如果你想要拷贝粘贴代码,请查看文章最后的示例脚本。
  • 详细步骤
  • 下面示例的前提是你已经使用如下命令启动了一个简单的名为charlie的容器:
  • ​$ docker run --name charlie -ti ubuntu bash ​
  • 我们需要做的是将宿主文件夹/home/jpetazzo/Work/DOCKER/docker挂载到容器里的/src目录。好了,让我们开始吧。
  • nsenter
  • 首先,我们需要
  • nsenter
  • 以及 docker-enter帮助脚本。为什么?因为我们要从容器中mount文件系统。由于安全性的考虑,容器不允许我们这么做。使用nsenter,我们可以突破上述安全限制,在容器的上下文(严格地说,是命名空间)中运行任意命令。当然,这必须要求拥有Docker宿主机的root权限。
  • nsenter
  • 最简单的安装方式是和docker-enter脚本关联执行:
  • ​$ docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter ​
  • 更多细节,请查看
  • nsenter
  • 项目主页。
  • 找到文件系统
  • 我们想要在容器里挂载包含宿主文件夹(/home/jpetazzo/Work/DOCKER/docker)的文件系统。那我们就需要找出哪个文件系统包含这个目录。
  • 首先,我们需要canonicalize(或者解除引用)文件,以防这是一个符号链接,或者它的路径包含符号链接:
  • ​$ readlink --canonicalize /home/jpetazzo/Work/DOCKER/docker /home/jpetazzo/go/src/github.com/docker/docker ​
  • 哈,这的确是一个符号链接!让我们将其放入一个环境变量中:
  • ​$ HOSTPATH=/home/jpetazzo/Work/DOCKER/docker $ REALPATH=$(readlink --canonicalize $HOSTPATH) ​
  • 接下来,我们需要找出哪个文件系统包含这个路径。我们使用一个有点让人意想不到的工具来做,它就是df:
  • ​$ df $REALPATH Filesystem 1K-blocks Used Available Use% Mounted on /sda2 245115308 156692700 86157700 65% /home/jpetazzo ​
  • 使用-P参数(强制使用POSIX格式,以防是exotic df,或者是其他人在Solaris或者BSD系统上装Docker时运行的df),将结果也放到一个变量里:
  • ​$ FILESYS=$(df -P $REALPATH | tail -n 1 | awk '{print $6}') ​
  • 找到文件系统的设备(和sub-root)
  • 现在,系统里已经没有绑定挂载(bind mounts)和BTRFS子卷了,我们仅仅需要查看/proc/mounts,找到对应于/home/jpetazzo文件系统的设备就可以了。但是在我的系统里,/home/jpetazzo是BTRFS池的子卷,要想得到子卷的信息(或者bind mount信息),需要查看/proc/self/moutinfo。
  • 如果你从来没有听说过mountinfo,可以查看内核文档的
  • proc.txt

  • 首先,得到文件系统设备信息:
  • ​$ while read DEV MOUNT JUNK > do [ $MOUNT = $FILESYS ] && break > done </proc/mounts $ echo $DEV /dev/sda2 ​
  • 接下来,得到sub-root信息(比如,已挂载文件系统的路径):
  • ​$ while read A B C SUBROOT MOUNT JUNK > do [ $MOUNT = $FILESYS ] && break > done < /proc/self/mountinfo $ echo $SUBROOT /jpetazzo ​
  • 很好。现在我们知道需要挂载/dev/sda2。在文件系统内部,进入/jpetazzo,从这里可以得到到所需文件的剩余路径(示例中是/go/src/github.com/docker/docker)。
  • 让我们计算出剩余路径:
  • ​$ SUBPATH=$(echo $REALPATH | sed s,^$FILESYS,,) ​



注意:这个方法只适用于路径里没有符号“,”的。如果你的路径里有“,”并且想使用本文方法挂载目录,请告诉我。(我需要调用Shell Triad来解决这个问题:​ ​jessie​ ​,​ ​soulshake​ ​,​ ​tianon​ ​?)



在进入容器之前最后需要做的是找到这个块设备的主和次设备号。可以使用stat:
​$ stat --format "%t %T" $DEV ​ ​ 注意这两个数字是十六进制的,我们之后需要的是二进制。可以这么转换:
​$ DEVDEC=$(printf "%d %d" $(stat --format "0x%t 0x%T" $DEV)) 总结
还有最后一步。因为某些我无法解释的原因,一些文件系统(包括BTRFS)在挂载多次之后会更新/proc/mounts里面的设备字段。也就是说,如果我们在容器里创建了名为/tmpblkdev的临时块设备,并用其挂载我们自己的文件系统,那么文件系统(在宿主机器里!)会显示为/tmpblkdev,而不是/dev/sda2。这听起来无所谓,但实际上这会让之后试图得到文件系统块设备的操作都失败。
长话短说,我们想要确保块设备节点在容器里位于和宿主机器上的同一个路径下。
需要这么做:
​$ docker-enter charlie -- sh -c \ > "[ -b $DEV ] || mknod --mode 0600 $DEV b $DEVDEC" ​ ​ 创建临时挂载点挂载文件系统:
​$ docker-enter charlie -- mkdir /tmpmnt $ docker-enter charlie -- mount $DEV /tmpmnt ​ ​ 确保卷挂载点存在,bind mount卷:
​$ docker-enter charlie -- mkdir -p /src $ docker-enter charlie -- mount -o bind /tmpmnt/$SUBROOT/$SUBPATH /src ​ ​ 删除临时挂载点:
​$ docker-enter charlie -- umount /tmpmnt $ docker-enter charlie -- rmdir /tmpmnt ​ ​ (我们并不清除设备节点。一开始就检查设备是否存在可能有点多余,但是现在再检查就已经很复杂了。)
大功告成!
让一切自动化
下面这段可以直接拷贝粘贴了。
​#!/bin/sh set -e CONTAINER=charlie HOSTPATH=/home/jpetazzo/Work/DOCKER/docker CONTPATH=/src REALPATH=$(readlink --canonicalize $HOSTPATH) FILESYS=$(df -P $REALPATH | tail -n 1 | awk '{print $6}') while read DEV MOUNT JUNK do [ $MOUNT = $FILESYS ] && break done </proc/mounts [ $MOUNT = $FILESYS ] # Sanity check! \while read A B C SUBROOT MOUNT JUNK \do [ $MOUNT = $FILESYS ] && break \done < /proc/self/mountinfo [ $MOUNT = $FILESYS ] # Moar sanity check! SUBPATH=$(echo $REALPATH | sed s,^$FILESYS,,) DEVDEC=$(printf "%d %d" $(stat --format "0x%t 0x%T" $DEV)) docker-enter $CONTAINER -- sh -c \ "[ -b $DEV ] || mknod --mode 0600 $DEV b $DEVDEC" docker-enter $CONTAINER -- mkdir /tmpmnt docker-enter $CONTAINER -- mount $DEV /tmpmnt docker-enter $CONTAINER -- mkdir -p $CONTPATH docker-enter $CONTAINER -- mount -o bind /tmpmnt/$SUBROOT/$SUBPATH $CONTPATH docker-enter $CONTAINER -- umount /tmpmnt docker-enter $CONTAINER -- rmdir /tmpmnt</pre> 状态和限制 上述方法不适用于不基于块设备的文件系统,只有在/proc/mounts能正确得到块设备节点 容器目录不可为相对路径

Docker容器启动的时候,如果要挂载宿主机的一个目录,可以用-v参数指定。

比如启动一个centos容器,宿主机的/test目录挂载到容器的/soft目录,可通过以下方式指定:​ ​docker run -it -v /test:/soft centos /bin/bash​

这样在容器启动后,容器内会自动创建/soft的目录。

注意:

容器目录不可以为相对路径,必须以下斜线“/”开头。宿主机的目录最好也是绝对路径。

挂载宿主机已存在目录后,在容器内对其进行操作,报“Permission denied”。可通过指定–privileged参数来解决:​ ​docker run -it --privileged=true -v /test:/soft centos /bin/bash​

[root@localhost /]# docker run -it -v /storage:/leader-us java /bin/bash
root@c9c916b9a171:/# ls
bin boot dev etc home leader-us lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@c9c916b9a171:/#