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

之前有人问我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
8 2

注意这两个数字是十六进制的,我们之后需要的是二进制。可以这么转换:

$ 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

状态和限制

上述方法不适用于不基于块设备的文件系统,只有在/proc/mounts能正确得到块设备节点(上面谈到,并不总是能正确得到)的时候才能起作用。另外,我只测试了我自己的环境,没有在云实例之类的环境里测试过,但是我很想知道在那里是否适用。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 第一次构建、运行、发布、获取docker镜像的步骤详解

    1.前言 过去,如果您要开始编写Python应用程序,第一步就是把Python的运行环境安装到您的机器上,而且安装的环境还要和线上的一致,比较麻烦. 使用Docker,您可以从docker的官方registry或者其他仓库,获取一个可移植的Python运行环境镜像,无需安装.然后,你可以基于这个镜像开发你的应用程序,这样可以确保您的应用程序,依赖项和运行时都一起运行. 2.构建一个python镜像 2.1.为了构建您自己的镜像,首先需要创建一个名称为Dockerfile的文件,用于定义创建镜像并

  • Docker数据存储总结

    阅读本文前,希望你已经对Volumes,Bind mounts和tmpfs mounts有了初步的了解,具体可以参考以下文章: Docker数据存储之Volumes Docker数据存储之Bind mounts Docker数据存储之tmpfs mounts 下图展示了Volumes,Bind mounts和tmpfs mounts三种存储技术的不同: Volumes的使用场景 在多个容器间共享数据. 无法确保Docker主机一定拥有某个指定的文件夹或目录结构,使用Volumes可以屏蔽这些宿主

  • Docker容器和本机之间的文件传输方法

    主机和容器之间传输文件的话需要用到容器的ID全称. 获取方法如下: 1.先拿到容器的短ID或者指定的name. 2.然后根据这两项的任意一项拿到ID全称. 有了这个长长的ID的话,本机和容器之间的文件传输就简单了. docker cp 本地文件路径 ID全称:容器路径 进入容器之后就能够看到刚才上传进来的文件了. 如果是容器传输文件到本地的话,反过来就好了: docker cp ID全称:容器文件路径 本地路径 进行挂载的话可以参考这篇:利用Volume在主机和Docker容器文件传输. 以上就

  • docker完整配置nginx+php+mysql的方法步骤

    首先了解一个方法: 使用docker exec进入Docker容器 docker在1.3.X版本之后还提供了一个新的命令exec用于进入容器,这种方式相对更简单一些,下面我们来看一下该命令的使用: sudo docker exec --help 接下来我们使用该命令进入一个已经在运行的容器 $ sudo docker ps $ sudo docker exec -it 775c7c9ee1e1 /bin/bash 一. 配置nginx 查找Docker Hub上的 nginx 镜像 runoob

  • Docker数据存储之Bind mounts详解

    阅读本文前,希望你已经对Volumes有了初步的了解,具体可以参考这篇文章: Docker数据存储之Volumes详解 默认容器的数据的读写发生在容器的存储层,当容器被删除时其上的数据将会丢失.所以我们应该尽量保证容器存储层不发生写操作,为了实现数据的持久化存储我们需要选择一种方案来保存数据,当前有以下几种方式: Volumes Bind mounts tmpfs mounts 下图展示了这三种技术: Bind mounts Bind mounts模式和Volumes非常相似,不同点在于Bind

  • Docker数据存储之tmpfs mounts详解

    阅读本文前,希望你已经对Volumes和Bind mounts有了初步的了解,具体可以参考以下文章: Docker数据持久化之Volumes Docker数据持久化之Bind mounts tmpfs mounts Volumes和Bind mounts模式使我们能够在宿主机和容器间共享文件从而我们能够将数据持久化到宿主机上,以避免写入容器存储层带来的容器停止后数据的丢失的问题. 如果你使用linux运行Docker,那么避免写入数据到容器存储层还有一个方案:tmpfs mounts. tmpf

  • 详解docker私有仓库搭建与使用实战

    hub.docker.com上可以保存镜像,但是网速相对较慢,在内部环境中搭建一个私有的公共仓库是个更好的方案,今天我们就来实战搭建私有docker仓库吧: 环境规划 需要两台机器:docker私服仓库的server和使用docker的普通机器,这两个机器都是ubuntu16版本的server,ip信息如下: 机器名 ip 功能 docker-registry 192.168.119.148 docker私有仓库服务器 docker-app 192.168.119.155 运行docker服务的

  • Docker数据存储之Volumes详解

    默认容器的数据的读写发生在容器的存储层,当容器被删除时其上的数据将会丢失.所以我们应该尽量保证容器存储层不发生写操作,为了实现数据的持久化存储我们需要选择一种方案来保存数据,当前有以下几种方式: Volumes Bind mounts tmpfs mounts 下图展示了这三种技术: Volumes Volumes(数据卷)是一个可供一个或多个容器使用的位于宿主机上特殊目录,它拥有以下特性: 数据卷可以在容器间共享和重用 对数据卷的写入操作,不会对镜像有任何影响 数据卷默认会一直存在,即使容器被

  • 阿里云esc服务器Docker部署单节点Mysql的讲解

    1.下载加速版msyql   docker pull hub.c.163.com/library/mysql:5.7 2.更名 docker tag hub.c.163.com/library/mysql:5.7 mysql:5.7 3.启动 docker run -it --rm --name mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql 4.设置mysql远程登录 docker exec -it mysql bash my

  • Docker容器间通讯直接路由方式实现网络通讯

    概述 就目前Docker自身默认的网络来说,单台主机上的不同Docker容器可以借助docker0网桥直接通信,这没毛病,而不同主机上的Docker容器之间只能通过在主机上用映射端口的方法来进行通信,有时这种方式会很不方便,甚至达不到我们的要求,因此位于不同物理机上的Docker容器之间直接使用本身的IP地址进行通信很有必要.再者说,如果将Docker容器起在不同的物理主机上,我们不可避免的会遭遇到Docker容器的跨主机通信问题.本文就来尝试一下. 此时两台主机上的Docker容器如何直接通过

随机推荐