这篇文章已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不再正确。

DockerCon 的 Quake 演示是如何工作的?

Docker 于 2013 年发布后不久,便成为 Linux 上非常流行的开源容器管理工具。Docker 有一套丰富的命令来控制容器的执行,例如 start、stop、restart、kill、pause 和 unpause。然而,目前仍然缺少通过 Docker 本身原生 Checkpoint 和 Restore (C/R) 容器的能力。

我们一直在积极与上游和社区开发者合作,以在 Docker 中添加对原生 C/R 的支持,并希望 checkpoint 和 restore 命令能在 Docker 1.8 中引入。截至本文撰写时,由于该功能最近已合并到 libcontainer 中,因此可以外部 C/R 容器。

外部容器 C/R 在 DockerCon 2015 上进行了演示

Screen Shot 2015-06-30 at 3.37.46 PM.png

容器 C/R 提供了许多优点,包括以下几点

  • 在停止和重启 Docker 守护进程(例如进行升级)时,无需杀死正在运行的容器并从头开始重启它们,从而避免丢失它们在停止时完成的宝贵工作。
  • 重启系统时无需从头开始重启容器。与上述用例 1 具有相同的优点。
  • 加速慢启动应用程序的启动时间。
  • 通过检查容器中运行的进程的 checkpoint 镜像(打开的文件、内存段等)进行“取证调试”。
  • 通过在不同的机器上 restore 容器来实现容器迁移。

CRIU

从头开始实现 C/R 功能是一项重大的、令人畏惧的任务。幸运的是,有一个强大的开源工具,用 C 编写,已在生产环境中用于 checkpoint 和 restore Linux 中的整个进程树。这个工具叫做 CRIU,代表 Checkpoint Restore In Userspace(用户空间中的检查点恢复)(http://criu.org)。CRIU 的工作原理是:

  • 冻结正在运行的应用程序。
  • 将整个进程树的地址空间和状态 checkpoint 到一组“镜像”文件中。
  • 从 checkpoint 镜像文件中 restore 进程树。
  • 从冻结的点恢复应用程序。

2014 年 4 月,我们决定探究 CRIU 是否能够 checkpoint 和 restore Docker 容器,以促进容器迁移。

阶段 1 - 外部 C/R

这项工作的第一阶段是直接调用 CRIU 来转储容器内运行的进程树,并确定 checkpoint 或 restore 操作失败的原因。有相当多的问题导致了 CRIU 失败。以下三个问题是其中较具挑战性的。

外部绑定挂载 (Bind Mounts)

Docker 将 /etc/{hostname,hosts,resolv.conf} 设置为目标,其源文件位于容器的 mount namespace 之外。

CRIU 中添加了 --ext-mount-map 命令行选项来指定外部绑定挂载的路径。例如,假设是默认的 Docker 配置,容器的 mount namespace 中的 /etc/hostname 是从 /var/lib/docker/containers/<container-id>/hostname 源路径绑定挂载的。进行 checkpoint 时,我们告诉 CRIU 将 /etc/hostname 的“映射”记录为(比如说)etc_hostname。进行 restore 时,我们告诉 CRIU,之前记录为 etc_hostname 的文件应该从 /var/lib/docker/containers/<container-id>/hostname 的外部绑定挂载处映射。

ext_bind_mount.png

AUFS 路径名 (Pathnames)

Docker 最初使用 AUFS 作为其首选文件系统,该文件系统目前仍广泛使用(现在首选的文件系统是 OverlayFS)。由于一个 bug,/proc/<pid>/map_files 的 AUFS 符号链接路径指向 AUFS 分支内部,而不是相对于容器根目录的路径名。这个问题已在 AUFS 源代码中修复,但尚未普及到所有发行版。CRIU 会因为在文件的物理位置(分支中)和逻辑位置(从 mount namespace 的根目录)看到同一个文件而感到困惑。

原本只在 restore 时使用的 --root 命令行选项被通用化,使其在 checkpoint 时也能理解 mount namespace 的根目录,并自动“修复”暴露的 AUFS 路径名。

Cgroups

checkpoint 后,Docker 守护进程会移除容器的 cgroups 子目录(因为容器已“退出”)。这会导致 restore 失败。

CRIU 中添加了 --manage-cgroups 命令行选项,用于转储和 restore 进程的 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  

在阶段 1 结束时,可以使用 CRIU v1.3 外部 checkpoint 和 restore Docker 1.0 容器,支持 VFS、AUFS 或 UnionFS 存储驱动。

阶段 2 - 原生 C/R

虽然外部 C/R 成功地验证了容器 C/R 的概念,但它存在以下限制:

  1. checkpoint 的容器状态会显示为 "Exited"。
  2. 在 restore 的容器上,Docker 命令(如 logs, kill 等)将无法工作。
  3. restore 的进程树将是 /etc/init 的子进程,而不是 Docker 守护进程的子进程。

因此,这项工作的第二阶段集中在为 Docker 添加原生的 checkpoint 和 restore 命令。

libcontainer, nsinit

Libcontainer 是 Docker 的原生执行驱动。它提供了一组 API 用于创建和管理容器。添加原生支持的第一步是向 libcontainer 引入 checkpoint() 和 restore() 这两个方法,并向 nsinit 添加相应的 checkpoint 和 restore 子命令。Nsinit 是一个用于测试和调试 libcontainer 的简单实用工具。

docker checkpoint, docker restore

在 libcontainer 中支持 C/R 后,下一步是向 Docker 本身添加 checkpoint 和 restore 子命令。这一步中的一个巨大挑战是重建容器和守护进程之间的“管道连接 (plumbing)”。当守护进程最初启动一个容器时,它会在自身(父进程)和容器(子进程)的标准输入、输出和错误文件描述符之间建立单独的管道。这就是 docker logs 能够显示容器输出的方式。

当容器在 checkpoint 后退出时,它与守护进程之间的管道会被删除。在容器 restore 期间,实际是 CRIU 作为父进程。因此,在子进程(容器)和一个不相关的进程(Docker 守护进程)之间建立所需的管道连接并非易事。

为了解决这个问题,CRIU 中添加了 --inherit-fd 命令行选项。使用此选项,Docker 守护进程告诉 CRIU,让 restore 的容器“继承”从守护进程传递给 CRIU 的某些文件描述符。

原生 C/R 的第一个版本于 2014 年 10 月在 Linux Plumbers Conference (LPC) 上进行了演示 (http://linuxplumbersconf.org/2014/ocw/proposals/1899)。

external_cr.png

LPC 演示是使用一个不需要网络连接的简单容器进行的。对 restore 网络连接的支持于 2015 年初完成,并在这段 2 分钟的视频剪辑中进行了演示。

容器 C/R 的当前状态

2015 年 5 月,libcontainer 的 criu 分支合并到了 master 分支。使用新引入的轻量级容器运行时 runC,容器迁移在 DockerCon15 上进行了演示。在这段演示(23:00 处)演示中,一个正在运行 Quake 的容器被 checkpoint 并在另一台机器上 restore,有效地实现了容器迁移。

截至本文撰写时,GitHub 上有两个具有 Docker 原生 C/R 支持的仓库:

将 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 的内核补丁: