CRIU和Pod在线迁移
对于IaaS平台来说,虚拟机在线迁移是普遍实现的特性。所谓在线迁移,就是把虚拟机从一台物理机透明的移动到另外一台物理机上,(几乎)不会导致服务中断。
在线迁移的价值是,当宿主机操作系统需要升级,或者硬件出现故障需要停机处理时,用户的工作负载不会受到影响。如果在IaaS之上部署Kubernetes,自然可以使用现有的虚拟机在线迁移,间接实现Pod的在线迁移。然而,直接在裸金属之上部署Kubernetes,避免虚拟化的开销,是一种趋势,这意味着需要在PaaS层实现Pod状态的保存、通过网络传输、在另一台宿主机上恢复。
目前Kubernetes是不支持Pod在线迁移的,相关的讨论从K8S项目诞生初期就在进行。但是,容器运行时,例如Docker、Podman,都基于CRIU(Checkpoint Restore in Userspace)实现了容器状态保存/恢复的功能。
目前Docker支持基于CRIU的Checkpoint/Restore功能,可以冻结运行中的容器,将其状态保存为磁盘上的一系列文件,并可以后续基于这些文件恢复容器。这些功能针对的应用场景包括:
- 重启主机,但是不需要重启容器
- 为启动速度慢的应用提速,做法是启动后Checkpoint,以后从检查点启动容器
- Rewinding进程到先前某个时刻
基于Docker实现在线迁移,也是可以实现的,但是目前Docker的实现逻辑并没有基于在线迁移的需求进行优化。
作为外部依赖的CRIU,需要你手工安装:
1 2 3 4 5 |
# 需要Ubuntu 16.04 sudo add-apt-repository ppa:criu/ppa sudo apt-get update apt install criu |
你需要为dockerd启用试验特性:
1 2 3 |
{ "experimental": true } |
Docker的Checkpoint/Restore示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 运行一个容器 docker run --security-opt=seccomp:unconfined --name cr -d busybox /bin/sh -c 'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done' # 设置一个检查点 docker checkpoint create cr checkpoint1 # 检查点的各种镜像文件位于/var/lib/docker/containers/$CONTAINER_ID/checkpoints/checkpoint1目录下 # 从检查点恢复 docker start --checkpoint checkpoint1 cr # 删除检查点 docker checkpoint rm cr checkpoint1 |
在线Pod迁移,需要经过以下步骤:
- 保存Pod中所有容器,生成检查点(Checkpoint)
- 将Pod状态传输到目的节点
- 在源节点上删除Pod
- 在新节点上恢复检查点,并组装新Pod,设置Pod状态
Pod由一系列容器组成,每个容器的状态都需要从源Pod传输到目的Pod,可以使用CRIU将容器的状态保存为可传输的格式。
CRIU的工作流程是:
- 冻结运行中的程序
- 为进程树的状态、地址空间创建Checkpoint
- 从Checkpoint恢复进程树
- 程序从冻结之处恢复
在K8S中,Pod首先由Scheduler调度到某个节点,然后,相关控制器联系目标节点的Kubelet,创建/修改/删除Pod,维护Pod和创建容器之类的底层工作,是由Kubelet完成的。
Kubelet会和Dockershim这样的CRI运行时交互,拉取镜像、启动容器、监控容器生命周期。缺失的Checkpoint/Restore特性需要修改Kubelet来实现。
要尽量减少Pod不可用的时间,可能需要分多阶段进行迁移,就像VM迁移那样:
- 传输Pod的绝大部分状态信息,同时保持源Pod运行
- 暂停Pod运行,将阶段1过程中产生的脏状态、内存页迁移走
捕获了容器状态之后,还需要一种机制,将Checkpoint镜像从源节点传输到目的节点。 目前,在容器编排的层面,节点之间是不需要相互通信的。而Pod迁移不但需要通信,而且需要传输大量的数据,这意味着需要修改Kubelet,实现:
- 在源节点中,调用容器运行时的接口,按照容器的之间的依赖,按序Checkpoint Pod中的所有容器
- 和目标节点的Kubelet创建信道,并逐步将检查点数据传输过去
- emptyDir等本地临时存储也需要传输到目标节点
- 接收到所有Checkpoint的镜像文件后,按依赖顺序恢复容器和Pod
- 删除源节点上的Pod和容器
容器被迁移后,主机名、IP地址、活动端口列表,必须在源、目标之间保持相同。IP地址可能需要CNI插件的配合,对于Calico来说,需要同步正确的路由规则。
CRIU(读音kree-oo)在用户空间中实现了Checkpoint/Restore功能,它能够冻结运行中的容器(或者其它独立应用程序),并将其状态保存到磁盘。这些状态可以用来恢复应用程序,将其还原到被冻结的那一刻。
通过CRIU,可以实现容器/应用程序在线迁移、快照、远程调试、启动加速、无缝内核升级等多种应用场景。
在容器领域,Docker、Podman、LXC等项目都使用了CRIU。
选项 | 说明 |
-v | 日志级别,0-4 |
--config | 传递配置给CRIU的配置文件 |
--no-default-config | 不解析默认配置文件 |
--pidfile | 将root task、service、page-server的PID到文件中 |
-o, --log-file | 将日志输出到文件 |
--display-stats |
在Dump、restore期间,CRIU会收集统计信息,例如Dump/restore所需的时间、需要处理的页面数 这些信息会存放在stats-dump文件中,并可以随时用crit命令查看 此选项,会导致上述统计信息在Dump/restore结束之前,打印信息到控制台 |
-D, --images-dir | 镜像文件存放之处 |
--prev-images-dir path | Base镜像文件存放之处,用于实现增量Dump |
-W, --work-dir | 工作目录,存放日志、pidfile、统计信息。如果不指定,使用-D选项指定的路径 |
--close fd | 在执行任何操作之前,关闭fd |
-L, --libdir | 插件目录位置 |
--enable-fs |
逗号分隔的、需要检测的文件系统名称。值all表示自动检测所有文件系统 警告:自动检测文件系统(的mount)假设挂载点可以被mount命令恢复。此选项和--external dev不兼容 |
--action-script |
添加在特定stage执行的外部脚本 在脚本中,你可以通过环境变量CRTOOLS_SCRIPT_ACTION来感知当前所处的stage: pre-dump dump开始前 |
Dump前操作,在此操作中,CRIU会创建相对于前一个pre-dump的内存变更、用于加速restore过程的fsnotify缓存。选项:
选项 | 说明 |
--track-mem |
打开内核中的内存变更跟踪器。默认隐含打开 内存变更跟踪是增量Dump的技术基础,增量Dump是降低在线迁移时冻结时长的关键 实现方式是打内核补丁:
|
--pre-dump-mode=mod | 取值splice / read,后者基于process_vm_readv系统调用,具有减少冻结时间、减少内存压力的优势。默认splice |
生成一个检查点,选项:
选项 | 说明 |
-t, --tree pid | 需要生成检查点的进程数的根 |
-R, --leave-running |
在生成检查点之后,维持任务在运行状态,而不是杀掉它 这个选项可能很危险,因为让任务继续运行,它就可能修改TCP连接、删除文件,这些都可能导致检查点变得没用,从而无法正确的恢复任务 |
-s, --leave-stopped | 让任务保持在停止状态(不会被调度,以后可恢复),而非杀死 |
--external type[id]:value | Dump外部资源,type为资源类型,支持mnt, dev, file, tty, unix |
--external mnt[mountpoint]:name | Dump外部绑定挂载的内容,存储到镜像中,以name为标识 |
--external mnt[]:flags |
Dump所有外部绑定挂载 flags可以指定m表示同时dump外部master挂载,s表示同时dump外部shared挂载,默认行为遇到这些挂载会中止dump |
--external dev[major/minor]:name | Dump指定块设备的内容 |
--external file[mnt_id:inode] | Dump外部文件,所谓外部文件,时无法从当前mount命名空间解析的文件 |
--external tty[rdev:dev] | Dump外部TTY |
-external unix[id] | 提示CRIU,UNIX套接字对(通过socketpair创建)的一端可以被断开 |
--freeze-cgroup | 利用cgroups的freezer来收集进程 |
--manage-cgroups | 将cgroups信息收集到镜像中。默认不收集任务关联的Cgroup信息 |
--tcp-established | 为已经创建的TCP连接创建检查点 |
--skip-in-flight | 跳过in-flight(尚未完全建立)的TCP连接 |
--evasive-devices | 如果设备不可访问,则在此指定设备文件路径 |
--page-server | 将内存页发往page-server,然后可以从目的主机访问page-server,获取这些内存页 |
--force-irmap | 为inotify/fsnotify watch强制解析名称 |
--auto-dedup | 自动去除内存页镜像中的重复(和上一次pre-dump对比)数据,隐含意味着增量Dump |
-l, --file-locks | Dump文件锁,必须保证所有持有文件锁的用户也被Dump,在封闭的容器环境下可以安全使用 |
--link-remap | 如果可能,将unlink的文件重新link |
-j, --shell-job | 允许Dump所有Shell Job,意味着恢复的进程会从CRIU自身继承session/ 进程组ID。也可以用于迁移单个外部tty连接 |
--cpu-cap [cap[,cap...]] | 写入镜像文件中的CPU特性列表 |
--cgroup-root [controller:]/newroot | 改变cgroups控制器将被Dump到的根目录 |
--lazy-pages | 不将内存页写入到镜像文件,而是准备通过网络传输 |
从检查点恢复进程,选项:
选项 | 说明 |
--inherit-fd fd[N]:resource |
继承文件描述符,用已经打开的文件描述符N来恢复resource代表的文件。可用于恢复--external file/tty/unix保存的外部资源 resource参数格式: tty[rdev:dev] |
-d, --restore-detached |
当恢复完成后,Detach CRIU自身 |
-s, --leave-stopped | 恢复之后,保持任务为停止状态,而非kick to run |
-S, --restore-sibling | 将根任务恢复为sibling,必须联用-d |
--log-pid | 为每个PID分开写日志 |
--external type[id]:value | 恢复外部资源 |
--external dev[name]:/dev/path | 将镜像中的name从/dev/path回复 |
--external mnt[name]:mountpoint | 外部绑定挂载,name为镜像中引用绑定挂载的名字 |
--external mnt[] | 所有外部绑定挂载 |
--external veth[inner_dev]:outer_dev@bridge | 将VETH对本端的inner_dev配对给outer_dev。如果指定@bridge,则outer_dev被加到网桥 |
--external macvlan[inner_dev]:outer_dev | 当恢复包含了MacVLAN的镜像时,该选项指定inner_dev需要绑定到的外部设备的名字 |
--manage-cgroups [mode] |
恢复关联的cgroups配置,cgroups控制器总是以优化的方式恢复,如果已经存在于系统,CRIU重用之,否则,创建之。取值: none 不恢复cgroups信息,但是要求依赖的cgroups预先存在 props 恢复cgroups属性,要求cgroups预先存在 soft 仅当cgroups由CRIU创建,才恢复属性,否则不恢复。默认值 full 完全恢复cgroups及其属性 strict 完全恢复cgroups及其属性,要求相关cgroups不存在 ignore 忽略已经存在的cgroups,假设它们不存在 |
--cgroup-root [controller:]/newroot | 修改cgroups根目录 |
--tcp-established | 恢复已建立的TCP连接 |
--tcp-close | 将已建立的TCP设置为关闭状态 |
--veth-pair IN=OUT | 关联VETH对 |
-l, --file-locks | 恢复文件锁 |
--auto-dedup | 自动去重,一旦内存页恢复,就会从镜像中punched out |
-j, --shell-job | 恢复Shell Jobs,也就是从CRIU自身继承Session ID / Process group ID |
--cpu-cap | 指定CPU特性 |
检查内核是否支持CRIU所需要的特性。
以页服务器模式启动CRIU,选项:
选项 | 说明 |
--daemon | 以守护进程的形式运行 |
--status-fd | 当此服务器可以处理请求后,写入\0且关闭FD |
--address address | 服务器的IP地址或主机名 |
--port number | 服务器的监听端口 |
--ps-socket fd | 将指定的文件描述符用于入站连接,忽略--address和--port |
--lazy-pages | 将本地内存Dump提供给远程lazy-pages守护程序。在此模式下,服务器读取本地内存Dump,并且允许远程lazy-pages守护程序以随机顺序请求内存页 |
--tls-cacert | 用于校验客户端证书的CA |
--tls-cacrl | 指向证书吊销列表文件 |
--tls-cert | 服务器端证书 |
--tls-key | 服务器端私钥 |
--tls | 启用TLS |
以lazy-pages模式启动CRIU。lazy-pages守护程序能按需为被恢复进程准备内存页。当进程第一次请求页时,此守护进程会从检查点目录查找内容,注入到进程的地址空间。
以RPC模式启动CRIU,通过套接字接收RPC命令。这种情况下,服务器运行在特权(超级用户)模式下,而客户端不需要。
启动页面映射(pagemap)数据去重流程,CRIU会扫描所有pagemap文件,尝试从父pagemap镜像中获得引用,来最小化pagemap条目的数量。
在源、目的主机之间迁移虚拟机内存状态,有两种技术:
技术 | 说明 |
预拷贝(Pre-copy)内存迁移 |
分为两个阶段:
缺点:
优点:
|
后拷贝(Post-copy)内存迁移 |
首先,暂停源虚拟机,并将虚拟机执行状态最根本的子集,包括CPU状态、寄存器信息、(可选的)不可分页内存,传递到目的主机,然后直接在目的主机上启动虚拟机 与此同时,源主机的内存页被源源不断的推送到目的主机,此行为成为pre-paging。在目标主机上,虚拟机访问尚未推送过来的页时,会产生一个page fault,捕获到的page fault被重定向给源主机,由源主机立即推送缺失的页 上述page fault的量很大的话,虚拟机性能会很差,因此需要设计一个良好的pre-paging算法,根据page fault来适配页面推送顺序,让和最近一个page fault相邻的页面尽快推送。具体效果,取决于工作负载的内存访问模式 缺点:
|
这些技术可以给容器迁移带来灵感。
使用pre-dump命令,进行任务的预迁移:
1 |
criu pre-dump --tree <pid> --images-dir <path-to-existing-directory-A> |
预迁移完成后,源进程继续运行,而不是默认被kill掉。镜像目录可以使用一个共享存储,避免不必要的拷贝开销。
pre-dump可以反复执行,每次都会生成一个镜像集,其中包含从上一次pre-dump以来,发生变化的内存页。 多次pre-dump可能会减少迁移导致的冻结时间,其效果类似于虚拟机迁移时的pre-copy。
执行后续的pre-dump时,你需要指定上一次镜像的存放目录:
1 2 |
criu pre-dump --tree <pid> --images-dir <path-to-existing-directory-B> \ --prev-images-dir <path-to-directory-A> |
基于pre-dump产生的镜像进行dump,你需要指定pre-dump镜像的想对(于Dump目录)路径:
1 2 |
criu dump --tree <pid> --images-dir <path-to-existing-directory-C> \ --prev-images-dir <path-to-directory-B-relative-to-C> --leave-stopped --track-mem |
如有必要,将镜像文件拷贝到目的主机。
在目的主机上执行:
1 |
criu restore --images-dir <path-to-images> |
在源主机上杀掉处于停止状态的源进程。
可以在不使用磁盘空间的前提下,完成进程在线迁移:
- 挂载内存文件系统(tmpfs),将内存镜像存储其中
- 在目的节点启动page-server,接受源节点的页推送,存储到上述tmpfs
在源节点、目标节点上都创建tmpfs:
1 2 |
mount -t tmpfs none <dir> mount -t tmpfs none <dir> |
源节点上的tmpfs仅仅存放非内存镜像文件,通常很小,不会产生内存压力。目的节点的tmpfs则需要存放完整镜像,但是它应该提供这些内存空间,因为它需要运行迁移后的进程。
在目标节点上,启动CRIU页面服务器,接受源节点的页面推送:
1 |
criu page-server --auto-dedup --images-dir <dir> --port <port> |
在源节点上,执行Dump:
1 2 |
criu dump --tree <pid> --images-dir <dir> --leave-stopped \ --page-server --address <dst> --port <port> |
将源节点tmpfs中,非内存镜像文件拷贝到目的节点。
在目标节点上恢复进程:
1 |
criu restore --images-dir <dir> |
在源主机上杀掉处于停止状态的源进程。
Leave a Reply