DockerCheckPoint
checkpoint和restore是一个实验性的功能,它允许讲一个正在运行的容器冻结,并且将其状态转换成磁盘上的文件集合,使你可以从checkpoint恢复容器的状态。checkpoint基于CRIU实现,CRIU是一个Linux应用程序,在用户空间将一个进程树冻结并将其状态保存到磁盘,使得容器或应用程序可以实时迁移,远程调试和快照等。CRIU的工作过程是:
- 冻结正在运行的应用程序
- 将整个进程树的地址空间和状态检查到镜像文件的集合
- 从检查点恢复进程树
- 从冻结点恢复应用程序
CRIU与Docker集成的现状以及使用方式
从Docker1.17开始,CRIU是Docker的实验性功能。
- 安装,由于CRIU是Docker的外部依赖,所以在使用Docker checkpoint之前,需要安装CRIU。
Debian 10
1 | wget http://github.com/checkpoint-restore/criu/archive/v3.18/criu-3.18.tar.gz |
此时已经编译完成,你可以直接运行~/criu-3.18/criu/criu或者:
1 | apt install asciidoc xmlto |
完成以后,运行criu -h不报错即安装成功。
- 由于checkpoint/restore是一个实验性功能,我们需要手动启用:
1 | echo "{\"experimental\": true}" >> /etc/docker/daemon.json |
- 运行一个测试容器
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 | root@VM-12-4-debian:~# docker ps -a |
- 恢复容器
我们恢复容器的状态:
1 | docker start --checkpoint=checkpoint1 looper |
这时候再次查看容器日志,我们可以发现i没有从0开始,而是从checkpoint的状态继续递增。
这个时候,如果我们查看/var/lib/docker/containers/<容器ID>/checkpoints,我们可以看到目录下的一堆img文件,这便是criu生成的镜像文件。
--checkpoint-dir选项自定义存储路径
1 | 新建一个目录来存储restore后的镜像文件 |
查看/tmp/ck/,我们可以发现,/tmp/ck/checkpoint2/目录下有一堆img文件。
- 使用自定义目录恢复容器状态
我们可以使用--checkpoint-dir选项来指定目录,如:
1 | docker start --checkpoint-dir=/tmp/ck/ --checkpoint=checkpoint2 looper |
- 需要注意的点
- 当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 | cp -r /tmp/ck/* /var/lib/docker/containers/<容器ID>/checkpoints |
即可恢复成功。
原理
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





