本文已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
DockerCon 上的 Quake 演示是如何工作的?
Docker 在 2013 年发布后不久,就成为 Linux 上非常流行的开源容器管理工具。Docker 拥有一组丰富的命令来控制容器的执行,例如 start、stop、restart、kill、pause 和 unpause。然而,仍然缺少的是通过 Docker 本身以原生方式检查点和恢复 (C/R) 容器的能力。
我们一直在积极与上游和社区开发人员合作,以在 Docker 中添加对原生 C/R 的支持,并希望检查点和恢复命令将在 Docker 1.8 中引入。在撰写本文时,可以在外部对容器进行 C/R,因为此功能最近已合并到 libcontainer 中。
外部容器 C/R 在 DockerCon 2015 上进行了演示
容器 C/R 提供了许多好处,包括以下内容
- 停止并重新启动 Docker 守护程序(例如为了升级),而无需杀死正在运行的容器并从头开始重新启动它们,从而避免损失它们停止时完成的宝贵工作
- 重新启动系统,而无需从头开始重新启动容器。与上面用例 1 相同的优点
- 加快启动较慢的应用程序的启动时间
- 通过检查它们的检查点映像(打开的文件、内存段等)对容器中运行的进程进行“取证调试”
- 通过在不同的机器上恢复容器来迁移容器
CRIU
从头开始实现 C/R 功能是一项重大而艰巨的任务。幸运的是,有一个强大的开源工具是用 C 编写的,已在生产中用于检查点和恢复 Linux 中的整个进程树。该工具称为 CRIU,代表用户空间中的检查点恢复(http://criu.org)。CRIU 的工作原理是
- 冻结正在运行的应用程序。
- 将整个进程树的地址空间和状态检查点保存到一系列“映像”文件中。
- 从检查点映像文件恢复进程树。
- 从冻结的点恢复应用程序。
在 2014 年 4 月,我们决定找出 CRIU 是否可以检查点和恢复 Docker 容器以促进容器迁移。
第一阶段 - 外部 C/R
这项工作的第一阶段是直接调用 CRIU 来转储在容器内运行的进程树,并确定检查点或恢复操作失败的原因。导致 CRIU 失败的问题很多。以下三个问题是其中比较具有挑战性的问题。
外部绑定挂载
Docker 将 /etc/{hostname,hosts,resolv.conf} 设置为目标,其源文件位于容器的挂载命名空间之外。
将 --ext-mount-map 命令行选项添加到 CRIU,以指定外部绑定挂载的路径。例如,假设默认的 Docker 配置,容器的挂载命名空间中的 /etc/hostname 从 /var/lib/docker/containers/<container-id>/hostname 的源进行绑定挂载。在检查点时,我们告诉 CRIU 记录 /etc/hostname 的“映射”,例如 etc_hostname。恢复时,我们告诉 CRIU,先前记录为 etc_hostname 的文件应从 /var/lib/docker/containers/<container-id>/hostname 的外部绑定挂载映射。
AUFS 路径名
Docker 最初使用 AUFS 作为其首选文件系统,该文件系统仍被广泛使用(现在首选的文件系统是 OverlayFS)。由于一个错误,/proc/<pid>/map_files 的 AUFS 符号链接路径指向 AUFS 分支内部,而不是它们相对于容器根的路径名。此问题已在 AUFS 源代码中修复,但尚未应用到所有发行版中。CRIU 会混淆,因为它在物理位置(在分支中)和逻辑位置(从挂载命名空间的根)中看到同一个文件。
仅在恢复期间使用的 --root 命令行选项被推广为在检查点期间理解挂载命名空间的根,并自动“修复”暴露的 AUFS 路径名。
Cgroups
在检查点后,Docker 守护程序将删除容器的 cgroups 子目录(因为容器已“退出”)。这会导致恢复失败。
将 --manage-cgroups 命令行选项添加到 CRIU,以转储和恢复进程的 cgroups 及其属性。
下面显示了简单容器的 CRIU 命令行
$ docker run -d busybox:latest /bin/sh -c 'i=0; while true; do echo $i \>\> /foo; i=$(expr $i + 1); sleep 3; done'
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
168aefb8881b busybox:latest "/bin/sh -c 'i=0; 6 seconds ago Up 4 seconds
$ sudo criu dump -o dump.log -v4 -t 17810 \
-D /tmp/img/\<container\_id\> \
--root /var/lib/docker/aufs/mnt/\<container\_id\> \
--ext-mount-map /etc/resolv.conf:/etc/resolv.conf \
--ext-mount-map /etc/hosts:/etc/hosts \
--ext-mount-map /etc/hostname:/etc/hostname \
--ext-mount-map /.dockerinit:/.dockerinit \
--manage-cgroups \
--evasive-devices
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
168aefb8881b busybox:latest "/bin/sh -c 'i=0; 6 minutes ago Exited (-1) 4 minutes ago
$ sudo mount -t aufs -o br=\
/var/lib/docker/aufs/diff/\<container\_id\>:\
/var/lib/docker/aufs/diff/\<container\_id\>-init:\
/var/lib/docker/aufs/diff/a9eb172552348a9a49180694790b33a1097f546456d041b6e82e4d7716ddb721:\
/var/lib/docker/aufs/diff/120e218dd395ec314e7b6249f39d2853911b3d6def6ea164ae05722649f34b16:\
/var/lib/docker/aufs/diff/42eed7f1bf2ac3f1610c5e616d2ab1ee9c7290234240388d6297bc0f32c34229:\
/var/lib/docker/aufs/diff/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158:\
none /var/lib/docker/aufs/mnt/\<container\_id\>
$ sudo criu restore -o restore.log -v4 -d
-D /tmp/img/\<container\_id\> \
--root /var/lib/docker/aufs/mnt/\<container\_id\> \
--ext-mount-map /etc/resolv.conf:/var/lib/docker/containers/\<container\_id\>/resolv.conf \
--ext-mount-map /etc/hosts:/var/lib/docker/containers/\<container\_id\>/hosts \
--ext-mount-map /etc/hostname:/var/lib/docker/containers/\<container\_id\>/hostname \
--ext-mount-map /.dockerinit:/var/lib/docker/init/dockerinit-1.0.0 \
--manage-cgroups \
--evasive-devices
$ ps -ef | grep /bin/sh
root 18580 1 0 12:38 ? 00:00:00 /bin/sh -c i=0; while true; do echo $i \>\> /foo; i=$(expr $i + 1); sleep 3; done
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
168aefb8881b busybox:latest "/bin/sh -c 'i=0; 7 minutes ago Exited (-1) 5 minutes ago
docker\_cr.sh
由于 CRIU 的命令行参数很长,因此在 CRIU 源代码树中提供了一个名为 docker_cr.sh 的辅助脚本来简化该过程。因此,对于上面的容器,只需按如下方式对容器进行 C/R 即可(有关详细信息,请参阅 http://criu.org/Docker)
$ sudo docker\_cr.sh -c 4397
dump successful
$ sudo docker\_cr.sh -r 4397
restore successful
在第一阶段结束时,可以使用 CRIU v1.3 使用 VFS、AUFS 或 UnionFS 存储驱动程序在外部检查点和恢复 Docker 1.0 容器。
第二阶段 - 本地 C/R
尽管外部 C/R 成功地证明了容器 C/R 的概念,但它具有以下局限性
- 检查点的容器的状态将显示为“已退出”。
- 诸如 logs、kill 等 Docker 命令将无法在恢复的容器上工作。
- 恢复的进程树将是 /etc/init 的子级,而不是 Docker 守护程序。
因此,这项工作的第二阶段集中在向 Docker 添加本机检查点和恢复命令。
libcontainer, nsinit
Libcontainer 是 Docker 的本机执行驱动程序。它提供了一组用于创建和管理容器的 API。添加本机支持的第一步是在 libcontainer 中引入了两个方法 checkpoint() 和 restore(),以及在 nsinit 中引入了相应的 checkpoint 和 restore 子命令。Nsinit 是一个简单的实用程序,用于测试和调试 libcontainer。
docker checkpoint, docker restore
借助 libcontainer 中的 C/R 支持,下一步是将检查点和恢复子命令添加到 Docker 本身。此步骤中的一个巨大挑战是重建容器和守护程序之间的“管道”。当守护程序最初启动容器时,它会在自身(父级)和容器(子级)的标准输入、输出和错误文件描述符之间设置单独的管道。这就是 docker logs 可以显示容器输出的方式。
当容器在检查点后退出时,它与守护程序之间的管道将被删除。在容器恢复期间,实际上是 CRIU 作为父级。因此,在子级(容器)和不相关的进程(Docker 守护程序)之间设置管道并非易事。
为了解决此问题,将 --inherit-fd 命令行选项添加到 CRIU。使用此选项,Docker 守护程序告诉 CRIU 让恢复的容器“继承”从守护程序传递给 CRIU 的某些文件描述符。
第一个版本的本机 C/R 在 2014 年 10 月的 Linux Plumbers Conference (LPC) 上进行了演示(http://linuxplumbersconf.org/2014/ocw/proposals/1899)。
LPC 演示是使用不需要网络连接的简单容器完成的。对恢复网络连接的支持在 2015 年初完成,并在此 2 分钟的 视频剪辑中进行了演示。
容器 C/R 的当前状态
2015 年 5 月,libcontainer 的 criu 分支合并到主分支中。在 DockerCon15 上,使用新引入的轻量级 runC 容器运行时演示了容器迁移。在此 (第 23:00 分钟),运行 Quake 的容器在不同的机器上进行了检查点和恢复,从而有效地实现了容器迁移。
在撰写本文时,GitHub 上有两个存储库在 Docker 中具有本机 C/R 支持
- Docker 1.5(旧的 libcontainer,相对稳定)
- Docker 1.7(较新,不太稳定)
目前正在努力将 C/R 功能合并到 Docker 中。您可以使用上述任一存储库来试验 Docker C/R。如果您正在使用 OverlayFS 或您的容器工作负载使用 AIO,请注意以下事项
OverlayFS
当 OverlayFS 支持正式合并到 Linux 内核 3.18 版本中时,它成为首选的存储驱动程序(而不是 AUFS)。但是,3.18 中的 OverlayFS 存在以下问题
- /proc/<pid>/fdinfo/<fd> 包含 mnt_id,但该 mnt_id 不在 /proc/<pid>/mountinfo 中
- /proc/<pid>/fd/<fd> 不包含打开文件的绝对路径
这两个问题已在此 补丁中修复,但该补丁尚未向上游合并。
AIO
如果您使用的内核版本低于 3.19,并且您的容器使用 AIO,则需要来自 3.19 的以下内核补丁
- torvalds: bd9b51e7 by Al Viro
- torvalds: e4a0d3e72 by Pavel Emelyanov