checkpoint和restore是一个实验性的功能,它允许讲一个正在运行的容器冻结,并且将其状态转换成磁盘上的文件集合,使你可以从checkpoint恢复容器的状态。checkpoint基于CRIU实现,CRIU是一个Linux应用程序,在用户空间将一个进程树冻结并将其状态保存到磁盘,使得容器或应用程序可以实时迁移,远程调试和快照等。CRIU的工作过程是:

  • 冻结正在运行的应用程序
  • 将整个进程树的地址空间和状态检查到镜像文件的集合
  • 从检查点恢复进程树
  • 从冻结点恢复应用程序

CRIU与Docker集成的现状以及使用方式

从Docker1.17开始,CRIU是Docker的实验性功能。

  1. 安装,由于CRIU是Docker的外部依赖,所以在使用Docker checkpoint之前,需要安装CRIU。

Debian 10

1
2
3
4
wget http://github.com/checkpoint-restore/criu/archive/v3.18/criu-3.18.tar.gz
apt install libprotobuf-dev libprotobuf-c-dev protobuf-c-compiler protobuf-compiler python3-protobuf
apt install pkg-config python-ipaddress libbsd-dev libnftables-dev libcap-dev libnet1-dev libaio-dev libgnutls28-dev python3-future iproute2 libnl-3-dev python3-pip
cd ~/criu-3.18 && make

此时已经编译完成,你可以直接运行~/criu-3.18/criu/criu或者:

1
2
apt install asciidoc xmlto
make install

完成以后,运行criu -h不报错即安装成功。

  1. 由于checkpoint/restore是一个实验性功能,我们需要手动启用:
1
2
echo "{\"experimental\": true}" >> /etc/docker/daemon.json
systemctl restart docker
  1. 运行一个测试容器
1
docker run -d --name looper busybox /bin/sh -c 'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done'

运行docker logs -f 容器ID可以查看i在增加。

运行

1
docker checkpoint create looper checkpoint1

可以看到容器已经停止运行:

1
2
3
root@VM-12-4-debian:~# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
04b7a82b2c86 busybox "/bin/sh -c 'i=0; wh…" About a minute ago Exited (137) 5 seconds ago looper
  1. 恢复容器

我们恢复容器的状态:

1
docker start --checkpoint=checkpoint1 looper

这时候再次查看容器日志,我们可以发现i没有从0开始,而是从checkpoint的状态继续递增。

这个时候,如果我们查看/var/lib/docker/containers/<容器ID>/checkpoints,我们可以看到目录下的一堆img文件,这便是criu生成的镜像文件。

  1. --checkpoint-dir选项自定义存储路径
1
2
3
4
# 新建一个目录来存储restore后的镜像文件
mkdir /tmp/ck/
# 创建checkpoint
docker checkpoint create --checkpoint-dir=/tmp/ck/ looper checkpoint2

查看/tmp/ck/,我们可以发现,/tmp/ck/checkpoint2/目录下有一堆img文件。

  1. 使用自定义目录恢复容器状态

我们可以使用--checkpoint-dir选项来指定目录,如:

1
docker start --checkpoint-dir=/tmp/ck/ --checkpoint=checkpoint2 looper
  1. 需要注意的点
  • 当docker run 添加了–security-opt选项之后,可能会出现如下报错
1
Error (criu/cr-dump.c:203): 9143 has rseq but kernel lacks get_rseq_conf feature

当不添加--security-opt seccomp:unconfined可以checkout。除此之外,将内核升级到v5.15可以进行checkout。

  • Error response from daemon: custom checkpointdir is not supported

使用自定义checkpointdir发现不支持。可能是你的内核不支持,CRIU支持的必须开启的内核Feature

一个解决方案是,将checkpointdir目录内容复制到/var/lib/docker/containers/<容器ID>/checkpoints内,在恢复时去除–checkpoint-dir选项,即可启动,如:

1
2
cp -r /tmp/ck/* /var/lib/docker/containers/<容器ID>/checkpoints
docker start --checkpoint=checkpoint2 looper3

即可恢复成功。

原理

Checkpoint过程

该过程严重依赖/proc目录,CRIU从/proc目录搜集:

  • 文件描述符信息(通过/proc/$pid/fd和/proc/$pid/fdinfo)
  • 管道参数
  • 内存映射(通过/proc/$pid/maps和/proc/$pid/map_files/)
  • 等等

搜集进程树并冻结它

进程组组长的pid通过命令行(–tree)获取。通过/proc/$pid/task/目录搜集正在运行的线程,并且通过/proc/$pid/task/$tid/children递归地搜集子线程。使用ptrace的PTRACE_SEIZE命令停止task。

搜集task资源并且进行存储

CRIU通过读取所有已经搜集的task的信息,并且将它们写到文件中,主要是通过:

  • 虚拟内存区域从/proc/$pid/smaps解析,映射文件从/proc/$pid/map_files链接读取
  • 文件描述符通过/proc/$pid/fd读取
  • task的内核参数通过ptrace接口和/proc/$pid/stat读取

CRIU通过ptrace接口注入寄生代码到task当中,一开始,我们只为CS:IP的mmap系统调用注入了几个字节,任务在启动的时候就有了。然后ptrace允许我们运行一个注入的系统调用,我们分配足够的内存给我们需要转储的寄生虫代码块。之后,寄生代码被复制到dumpee地址空间内的新位置,CS:IP设置分别指向我们的寄生代码。

从上下文解析来看,CRIU提供更多的信息,比如Credentials和Contents of memory

清除

在所有东西都转储之后(比如内存页,只能从dumpee地址空间内写出),我们再次使用trace工具,通过删除所有的寄生代码和恢复原始代码来治愈dumpee。然后CRIU从任务中分离出来,它们继续运行。

Restore过程

The restore procedure (aka restorer) is done by CRIU morphing itself into the tasks it restores. On the top-level it consists of 4 steps

共享资源

读取img文件并找到哪些进程共享哪些资源,然后共享资源被其中一个进程恢复,其他进程要么在第二阶段继承一个资源,要么以其他方式获取(如会话)。例如,通过unix套接字与SCM_CREDS消息一起发送的共享文件,或通过memfd文件描述符恢复的共享内存区域。

Fork进程树

在这一步,CRIU多次调用fork()来重新创建需要恢复的进程。注意,线程不是在这里恢复的,而是在第四步。

恢复基本任务资源

这里 CRIU 恢复所有资源,但是

  • 内存映射精确位置
  • 定时器
  • 凭据
  • 线程

上述资源是最后恢复的,原因是在这个阶段,CRIU打开文件,准备命名空间,映射(并填充数据)私有内存区域,创建套接字,调用chdir()和chroot(),并做一些其他工作。

切换到恢复上下文,恢复其余部分并继续

恢复器blob的原因很简单。由于 criu 会转变为目标进程,因此它必须取消映射其所有内存并放回目标内存。这样做时,内存中应该存在一些代码(执行 munmap 和 mmap 的代码)。因此,引入了恢复程序 blob。这是一小段代码,与 criu 映射和目标映射不相交。在第 2 阶段结束时,criu 跳入该 blob 并恢复内存映射。
在同一位置,我们恢复计时器,以免它们过早触发,在这里,我们恢复凭据,让criu执行特权操作(例如 fork-with-pid)和线程,以免它们遭受突然的内存布局更改。

See also: restorer context, tree after restore

参考文档