本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。
DockerCon 的 Quake 演示是如何工作的?
Docker 在 2013 年发布后不久,便成为 Linux 上一种非常受欢迎的开源容器管理工具。Docker 拥有一套丰富的命令来控制容器的执行,例如 start、stop、restart、kill、pause 和 unpause。然而,仍然缺少通过 Docker 本身原生实现容器检查点和恢复 (C/R) 的能力。
我们一直积极与上游和社区开发者合作,以在 Docker 中添加对原生 C/R 的支持,并希望在 Docker 1.8 中引入检查点和恢复命令。撰写本文时,由于此功能最近已合并到 libcontainer 中,因此可以通过外部方式对容器进行 C/R。
DockerCon 2015 上演示了外部容器 C/R
容器 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} 设置为目标,其源文件位于容器挂载命名空间之外。
CRIU 添加了 --ext-mount-map 命令行选项,用于指定外部绑定挂载的路径。例如,假设是默认 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)。由于一个 bug,/proc/<pid>/map_files 的 AUFS 符号链接路径指向 AUFS 分支内部,而不是它们相对于容器根目录的路径名。这个问题已在 AUFS 源代码中修复,但尚未推广到所有发行版。CRIU 会在物理位置(在分支中)和逻辑位置(从挂载命名空间的根目录)看到相同的文件而感到困惑。
以前只在恢复期间使用的 --root 命令行选项被泛化,以便在检查点期间理解挂载命名空间的根目录,并自动“修复”暴露的 AUFS 路径名。
Cgroups
检查点后,Docker 守护程序会删除容器的 cgroups 子目录(因为容器已“退出”)。这会导致恢复失败。
CRIU 添加了 --manage-cgroups 命令行选项,用于转储和恢复进程的 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 守护程序)之间建立管道并非易事。
为了解决这个问题,CRIU 添加了 --inherit-fd 命令行选项。使用此选项,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 分支合并到 master。使用新引入的轻量级 runC 容器运行时,容器迁移在 DockerCon15 上进行了演示。在这段(第 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,但其不在 /proc/<pid>/mountinfo 中
- /proc/<pid>/fd/<fd> 不包含已打开文件的绝对路径
这两个问题都已在此补丁中修复,但该补丁尚未合并到上游。
AIO
如果您使用的内核版本早于 3.19 且您的容器使用 AIO,则需要 3.19 中的以下内核补丁:
- torvalds: bd9b51e7 作者:Al Viro
- torvalds: e4a0d3e72 作者:Pavel Emelyanov