本文的重点是调研基于Docker的(无k8s集群的)容器迁移。在此之前,我们先简单了解下k8s集群环境中pod如何在节点间迁移。
一、k8s集群环境中pod的迁移
k8s集群环境中,如果某一节点需要停机维护,那么此时该节点上的pod应该如何处理呢?大概有如下三种处理方式:
-
默认迁移
基于Taint(污点)和Toleration(容忍)机制,在默认等待300s之后,k8s会自动将停机节点上的pod自动迁移到其他节点上。当然,默认的容忍时间是可以被修改的。
-
手动迁移
可以使用cordon、drain、uncordon三个命令实现节点的主动维护。
- cordon:标记节点不可调度,后续新的pod不会被调度到此节点,但是该节点上的pod可以正常对外服务;
- drain:驱逐节点上的pod至其他可调度节点;
- uncordon:标记节点可调度;
-
平滑迁移
当pod 副本(replicas)大于1时,使用pdb(PodDisruptionBudget)可以做到平滑迁移,也就是主动驱逐保护,可以实现节点维护期间不低于一定数量的pod正常运行,从而保证服务的可用性。
二、基于Docker的容器迁移
由于没有k8s集群环境中的控制节点和调度功能,所以本文探讨的是两个节点间主动的容器迁移操作。最终通过两个脚本化的实例,来展示在node1节点通过脚本命令来一键迁移特定容器到node2节点。
目前Docker基本上都是基于CRIU(Checkpoint Restore in Userspace)来实现容器状态保存/恢复的功能。具体地,基于CRIU的Checkpoint/Restore功能,Docker可以冻结运行中的容器,将其状态保存为磁盘上的一系列文件,并可以后续基于这些文件恢复容器。这些功能针对的应用场景包括:
- 重启主机,但是不需要重启容器
- 为启动速度慢的应用提速,做法是启动后Checkpoint,以后从检查点启动容器
- Rewinding进程到先前某个时刻
1、验证环境
- 系统环境
操作系统为ctyunos23.01,内核版本是5.10
- Docker版本
Docker的安装,本文不做讨论。本文中docker版本为20.10.12
- criu版本
criu是很重要的软件包,在容器迁移中起到关键作用。调研过程中,最终选择了源码编译安装的最新的3.18版本;当然,前提是需要安装很多个编译依赖包。
tar xf criu-3.18.tar.gz
cd criu-3.18/
make install
需要提出的是,我开始是通过yum安装的3.16版本的criu,在跨节点迁移nginx容器过程中,在目的节点进行restore操作时,有如下的报错:
“Error response from daemon: OCI runtime restore failed: read unixpacket @->@: EOF: unknown”,所以最终更换了3.18的版本。
- Docker特性
需要为Docker开启试验特性
更改配置后,需要重启docker服务:
systemctl daemon-reload
systemctl restart docker
- 各节点ip地址
源节点,主机名:node1,ip:192.168.122.194
目的节点,主机名:node2,ip:192.168.122.81
2、跨主机的容器迁移
2.1、节点间免密操作
首先,节点间网络互通,是跨主机容器迁移的前提。
其次,源节点和目的节点间需要传输数据,采用scp命令;在源节点上远程执行目的节点的命令,用到ssh命令。这两个命令都需要输入目的节点的密码,为了方便,我们进行免密处理,有以下两种方式:
(1)在源节点上,ssh-keygen命令生成密钥对,通过ssh-copy-id命令将公钥传到目的节点。
(2)源节点上,yum安装sshpass包,通过sshpass命令进行免密操作,命令格式为:sshpass -p password command parameters
本文中采用方式(2)。
2.2、源节点创建并运行容器
以busybox容器迁移为例
docker run -d --name loop-n1 --security-opt seccomp=unconfined busybox /bin/sh -c 'i=0; while true; do echo $i; i=$(($i+1)); sleep 1; done'
在源节点上运行busybox容器,实现每隔1s递增的输出一个数字的无限循环,返回的字符串为容器id。
其中,--security-opt seccomp=unconfined参数,关闭容器的(安全计算)seccomp限制。
docker logs loop-n1 -f可以看到输出数字在持续不断的递增
2.3、源节点创建checkpoint,并导出源容器镜像
docker checkpoint create loop-n1 cp1创建检查点cp1,容器会自动停止运行。
docker logs loop-n1查看最后的输出状态停留在“846”
通过commit和save命令,导出源容器镜像
2.4、传输源容器镜像到目的节点,并导入
结合sshpass命令,将上一步导出的源容器镜像免密传输到目的节点
在源节点上,结合sshpass命令,远程执行目的节点的导入镜像操作
2.5、目的节点创建而不运行对应容器
结合sshpass命令,远程执行目的节点上基于上一步导入镜像的创建容器操作,返回的字符串为目的节点上的容器id。
sshpass -p $pwd root@192.168.122.81 "docker create --name loop-n2 --security-opt seccomp=unconfined busybox:v1 /bin/sh -c 'i=0; while true; do echo $i; i=$(($i+1)); sleep 1; done'"
创建命令类似于2.2章节源节点上的命令,不同的是“create”,而不是“run -d”。
2.6、源节点checkpoint数据传输到目的节点,目的节点从checkpoint启动容器
传输checkpoint数据到目的节点:
sshpass -p $pwd scp -r
/var/lib/docker/containers/c3a9ac86002b070277f589874cd3130b7fe066fdc481c218b4099780f34be129/checkpoints/cp1/
root@192.168.122.81:/var/lib/docker/containers/9c24f5c4540782ac2161708c106d7e7c2cd10b751f4516d035578d19c7d12160/checkpoints
目的节点上从checkpoint检查点启动容器
2.7、验证迁移后的容器
登录到目的节点node2,查看容器运行情况
docker logs loop-n2 | head看到容器运行后,输出数字从“847”开始,是从源容器停止时的“断点处”延续执行的。
docker logs loop-n2 -f可以看到输出数字在持续不断的递增。
2.8、busybox容器迁移实例
基于2.2~2.6小节的步骤,我整合了一个完整脚本,来实现一键迁移busybox容器。
- 在源节点上重新运行一个busybox容器:docker run -d --name loop-n1 --security-opt seccomp=unconfined busybox /bin/sh -c 'i=0; while true; do echo $i; i=$(($i+1)); sleep 1; done';
或者
docker ps -a看到之前的busybox容器loop-n1还在,可以不用新建容器,直接启动容器即可。
- 在目的节点上,停止busybox容器loop-n2(如果存在的话)。
- 在源节点上执行~/migrate-loopbusybox脚本,即可完成容器迁移。
- 在目的节点,通过docker logs loop-n2 -f可以看到容器从源容器停止时的“断点处”延续执行。
2.9、nginx容器迁移实例
一键迁移nginx容器的脚本,跟busybox基本相同。只是运行或者创建镜像时的参数不一样。
在源节点上运行nginx容器:
docker run -d --name nginx-zyc --security-opt seccomp=unconfined -v ~/v_nginx:/usr/share/nginx/html -p 8080:80 nginx
在目的节点上创建nginx容器:
docker create --name nginx-zyc --security-opt seccomp=unconfined -v ~/v_nginx:/usr/share/nginx/html -p 8080:80 nginx:v1
3、后记
通过本次调研尝试,基于CRIU的docker容器迁移基本可以实现。但是目前Docker依然将criu标记为实验性质的功能,存在一些兼容性问题,支持的热迁移功能也很有限。另外,可能并不是所有的容器应用都可以基于criu来实现Checkpoint/Restore功能,在使用中需要注意。