CRIU(Checkpoint/Restore In Userspace)运行在linux操作系统上的一个软件工具,其功能是在用户空间实现Checkpoint/Restore功能。使用这个工具,你可以冻结一个正在运行的程序,并且checkpoint它到一系列的文件,然后你就可以使用这些文件在任何主机重新恢复这个程序到被冻结的那个点(白话就是实现对已运行程序的备份和恢复)。所以criu通常被用在程序或者容器的热迁移、快照、远程调试等。CRIU 起初是Virtuozzo的一个项目,随着开源社区的帮助,现在也被整合到OpenVZ(它是 Virtuozzo 的开源版本), LXC/LXD, Docker, Podman等软件项目里。
Licence: GPLv2
官 网:http://criu.org
源码地址:https://github.com/checkpoint-restore/criu
贡献方式:Pull Request
首先我编写了一个程序test,功能很简单,就是循环打印变量count++的值,count初始值为1。运行此程序:
[root@localhost gysun]# ./test &
25105
count=1
count=2
count=3
这时test程序已经运行,进程号是25105。然后我们在另一个shell里,先创建一个目录imgdir,用于存放备份进程时所生成的镜像文件。然后执行下面的命令:
[root@localhost gysun]# ./criu dump -D imgdir -j -t 25105
[1]+ 已杀死 /home/gysun/test
这个命令就是暂停test进程的执行,备份相关数据到imgdir目录后杀死test进程。同时要注意的是criu需要root权限才可以运行。
我们可以看到imgdir目录中生成很多*.img文件。如下所示:
core-25105.img fs-25105.img mm-25105.img pstree.img stats-restore
fdinfo-2.img ids-25105.img pagemap-25105.img seccomp.img timens-0.img
files.img inventory.img pages-1.img stats-dump tty-info.img
这些文件保存了进程被criu杀死之前的所有信息。对这些信息的介绍可以参考我的另一篇文章:
Docker热迁移工具CRIU原理系列:images
riu将用这些文件在任何其他同体系架构的主机上对test程序做恢复。比如我就在本机恢复test程序的运行,命令如下:
[root@localhost gysun]# ./criu restore --restore-detached -D imgdir -j
count=4
count=5
count=6
count=7
可以看到,criu恢复test程序运行后,count值的打印是距被criu杀死之前的值3继续打印,所以是从4开始。这就实现了程序的热迁移。
生产中的程序可并非如此简单。很多程序都是功能复杂,比如程序中可能有多个进程、有多个线程、有共享内存的使用、有tty设备的使用、有网络的使用、有ipc的使用等等。criu对此都可以成功的对相关状态进行备份和恢复。
criu的主要功能就是可以实现程序的热迁移,但是需要手动编写很多命令。P.Haul项目提供了一个Go语言版本的库,封装了criu命令的复杂性,可以让我们更轻松的完成程序的热迁移工作。其源码地址是 https://github.com/checkpoint-restore/go-criu
为了获取已运行进程的当前状态,CRIU需要让此进程执行一些代码去捕获当前进程相关信息。为了在不杀死此进程的情况下而实现这个功能,CRIU实现了代码动态注入(parasite code injection)技术。这个技术的详细介绍请参考我的另一篇文章:
同时这个功能被criu封装成一个独立的库libcompel。
CRIU另一个特性之一就是有能力在不破坏一个TCP链接的情况下,备份和恢复它的状态。这个功能不仅对CRIU本身有用,同时还可以通过它封装的库libsoccr单独使用。
CRIU的功能的实现基本分为两个过程,checkpoint和restore。在checkpoint过程,criu主要通过ptrace机制把一段特殊代码动态注入到dumpee进程(待备份的程序进程)并运行,这段特殊代码就实现了收集dumpee进程的所有上下文信息,然后criu把这些上下文信息按功能分类存储为一个个镜像文件。在restore过程。criu解析checkpoint过程产生的镜像文件,以此来恢复程序备份前的状态没,让程序从备份前的状态继续运行。
下面详细介绍checkpoint和restore这两个过程。
checkpoint的过程基本依赖ptrace功能实现。程序严重依赖**/proc**文件系统,它从/proc收集的信息包括:
checkpoint过程中,criu做的工作由如下步骤组成:
说明:在描述checkpoint中,我们把criu进程称为dumper进程,简称dumper。把要备份的进程称为dumpee进程,简称dumpee。
dumper通过dumpee的pid遍历**/proc/%pid/task/路径收集线程tid,并且递归遍历/proc/ p i d / t a s k / pid/task/ pid/task/tid/children**,然后通过ptrace函数的PTRACE_ATTACH和PTRACE_SEIZE命令冻结dumpee程序。
在这个阶段,dumper获取dumpee的所有可获取的资源信息并写到文件里。这些资源的获取通过如下步骤:
dumper获取到dumpee所有信息(比如内存页,它只能从被监控程序内部地址空间写出)后,我们使用ptrace的系列参数去掉步骤2中对dumpee进程的修改。主要是对被注入代码的清理并并恢复dumpee的地址空间。基本通过PTRACE_DETACH 和 PTACE_CONT。然后criu可以选择杀死dumpee或者让dumpee继续运行。上面的test实例中选择的就是在备份dumpee后杀死进程,实际工作中,如果要对程序做差分备份(或者叫增量备份)时可以选择继续运行dumpee。
恢复程序的过程完全依赖checkpoint过程后产生的镜像文件,主要过程分如下4步:
在这个步骤里,criu读取*.img镜像文件并找出哪些(子)进程共享了哪些资源,比如共享内存。如果有共享资源存在,稍后共享资源由这个程序的某个(子)进程还原,其他进程要么在第2阶段继承一个(如会话),要么以其他方式获取。例如,后者是通过unix套接字与SCM-CREDS消息一起发送的共享文件,或者是通过memfd文件描述符还原的共享内存区域。
在这一步,CRIU会调用fork()函数一次或多次来重新创建所需进程。
在此阶段CRIU打开文件、准备namespaces、重新映射所有私有内存区域、创建sockets、调用chdir() 和 chroot()等等。
通过将restorer.built-in.bin的代码注入到dumpee进程,来完成余下的内存区域、timers、credentials、threads的恢复。
CRIU不仅提供了命令行工具CLI供使用,还提供远程调用方式的RPC接口。同时还封装了一个本地接口库API供开发使用。这3种方式的介绍和使用请参阅我的另一篇文章:Docker热迁移工具CRIU原理系列: CLI, RPC and C API
openvz是在内核态实现的c/r工具,BLCR、DMTCP和CRIU都是在用户态实现的c/r工具。从功能完善性、api支持、发展趋势上,criu都更具优势。他们之间的对比可以参考我的另一篇文章Docker热迁移工具CRIU原理系列:竞品分析CRIU、DMTCP、BLCR、OPENVZ
CRIU虽是用户态工具,但是它的成功运行离不开kernel的配合。当我们使用命令“criu check”验证当前系统是否正常使用criu时,如果显示"Look good",那么恭喜你,当前系统可以正常使用criu工具,否则出现类似如下错误:
$ criu check
Warn (criu/kerndat.c:792): Can't load /run/criu.kdat
Error (criu/sockets.c:136): Diag module missing (-2)
Error (criu/util.c:803): exited, status=1
Error (criu/util.c:803): exited, status=1
Warn (criu/net.c:2770): Unable to get socket network namespace
Error (criu/tun.c:82): Unable to create tun: No such file or directory
那么就需要你修改内核配置来支持criu。CRIU需要内核开启的配置项请参看
https://criu.org/Linux_kernel
验证criu对docker容器的热迁移功能,可以参看官网demo