环境
Linux | Ubuntu 22.04 |
Kubernetes | 1.26 |
CRIO | 1.26 |
CRIU | 3.17(3.16以上即可) |
前言
容器有状态迁移是指将运行中的容器实例(包含其内部状态)从一个环境迁移到另一个环境的过程,涉及到保存和迁移容器的运行状态,包括文件系统、网络连接、内存中的数据等,目前主要使用检查点\恢复(Checkpoint\Restore)技术来实现这一目的。使用带有状态迁移的机制可以有多种用途,如应用调试、历史状态保存、慢启动应用加速、服务故障转移与避免等等,相信看到这篇文章的伙伴已经有了相关的需求与调研,不再过多讨论。
有状态迁移已经在Docker、Podman等容器运行时上得到了很好的支持,但是在k8s上却迟迟不见动静。虽然社区的主流思想是使用无状态的应用,但有状态应用的使用是不可避免的。好消息是从v1.25版本开始,k8s支持了对pod内的容器进行检查点存档,虽然恢复暂时还不支持,但可以使用状态的存档文件建立有状态镜像来进行恢复操作。
需要说明的是,这篇文章涉及的技术和方案均来自k8s社区,但是社区的文章非常的简略,本人在集群的搭建过程中遇到了较多问题,浪费了许多时间。这篇文章主要是对搭建过程进行整理,避免大家再浪费时间去踩坑。
(关于Linux环境补充一点,CentOS7的版本太低,升级了内核也可能搭建不成功,最后集群能进行检查点存档,但是无法恢复)
1. 增加集群检查点功能
考虑到可能看这篇文章的许多伙伴已经搭建了集群,只需要增加集群的检查点功能,因此将这部分内容放在前面,节约大家的时间和精力。对于还没有搭建集群的伙伴,请从第2部分开始。
1.1 开启CRI-O的检查点支持(所有节点)
在开启CRI-O的检查点支持前请确保CRIU已经安装并且版本在3.16之上,如果没有安装或者版本不够,请参考第2部分的CRIU安装部分。
编辑crio的配置文件,将enable_criu_support设为true:
vi /etc/crio/crio.conf
# 找到enable_criu_support字段并修改
# enable_criu_support = false
enable_criu_support = true
重启crio ,并查看crio的启动日志:
sudo systemctl restart crio
journalctl -xefu crio
如果出现下面提示,则开启成功:
1.2 开启ContainerCheckpoint特性门控
1. 修改apiserver、controller-manager、scheduler的配置文件,加入特性门控字段(Master节点):
# kube-controller-manager.yaml、kube-scheduler.yaml同理
vi /etc/kubernetes/manifests/kube-apiserver.yaml
# 若已经有其它功能门控被开启,只需要在后面添加ContainerCheckpoint门口的字段,用“,”隔开
- --feature-gates=...,...,...,ContainerCheckpoint=true
# 否则直接添加下面字段
- --feature-gates=ContainerCheckpoint=true
完成添加后 apiserver、controller-manager、scheduler三个pod将会被重启:
2. kubelet加入门控字段(所有节点):
vi /var/lib/kubelet/config.yaml
featureGates:
ContainerCheckpoint: true
完成后保存配置文件,重启kubelet即可。
1.3 检测点存档的功能验证
为了验证集群的检测点存档功能,我们制作一个容器镜像,使用该镜像运行pod后对pod进行检查点存档。
1. pod准备
首先使用python写一个简单的计数程序,每个一秒数一次数,并记录在MigLog.txt文件中,如下:
import time
i = 0
while True:
with open('MigLog.txt', 'a') as file:
file.write(f'{i}\n')
i += 1
time.sleep(1)
然后撰写Dokerfile,生成镜像文件:
# 使用官方 Python 镜像作为基础镜像
FROM python:3.7
# 设置工作目录
WORKDIR /app
# 将项目文件复制到容器中
COPY mig.py /app/ # 计数程序
COPY MigLog.txt /app/ # 记录文件
COPY hello_migration.txt /app/ # 空文件,可以不要,用于验证文件系统中不使用的文件是否迁移
# 启动应用程序
CMD [ "python", "mig.py" ]
随后使用这个镜像文件运行Pod,进入Pod内部显示如下:
查看MigLog.txt文件看程序是否在正常计数:
2. 检查点存档
在master节点执行检查点请求,格式为:
curl -X POST "https://nodeIP:10250/checkpoint/namespace/podId/container"
其中,nodeIP是需要进行检查点存档的pod所在节点,10250是该节点上的kubelet进程,checkpoint是检查点请求,最后三个信息分别是pod所在命名空间、pod名称和pod内容器的名称。
以本文的Pod为例,发送的命令如下(请求需要加上集群的证书与密钥):
# 存档命令
curl -X POST "https://192.168.60.11:10250/checkpoint/default/migrations/migration" --insecure --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt --key /etc/kubernetes/pki/apiserver-kubelet-client.key
检查点操作完成后会提示存档成功,显示文件名称,否则会出现下面错误提示:
401 | 未经授权; |
404 | ContainerCheckpoint功能门控被禁用,或指定命名空间、pod、容器找不到; |
500 | CRI在检查点期间遇到错误,或CRI未实现检查点API; |
检查点位于 /var/lib/kubelet/checkpoints/,文件名称格式为checkpoint-<pod-name>_<namespace-name>-<container-name>-<timestamp>.tar
我们对这份存档文件进行解压,可以看到如下内容:
对这些文件的解释如下:
文件 | 说明 |
bind.mounts | 该文件包含有关绑定挂载的信息,并且需要在恢复期间将所有外部文件和目录挂载到正确的位置 |
checkpoint/ | 该目录包含 CRIU 创建的实际检查点 |
config.dump 和 spec.dump | 包含恢复期间所需的有关容器的元数据 |
dump.log | 该文件包含在检查点期间创建的 CRIU 的调试输出 |
stats-dump | 此文件包含 checkpointctl 用于通过 --print-stats 显示转储统计信息的数据 |
rootfs-diff.tar | 该文件包含容器文件系统上所有已更改的文件 |
1.4 Pod恢复
终于来到恢复阶段了,然而目前为止的k8s版本还不支持直接恢复,需要自己构建有状态镜像来进行恢复操作。另外,v1.26版才加入了识别有状态镜像的接口,而不是熟知的v1.25,伙伴们不要弄错了。我们需要buildah这个镜像构建工具,将存档文件制作成镜像。直接使用apt命令安装buildah即可,没有特殊要求,随后执行下面操作:
# 创建一个空白容器
newcontainer=$(buildah from scratch)
# 添加存档文件到容器
buildah add $newcontainer /var/lib/kubelet/checkpoints/checkpoint-<pod-name>_<namespace-name>-<container-name>-<timestamp>.tar /
# 添加注释
buildah config --annotation=io.kubernetes.cri-o.annotations.checkpoint.name=<container-name> $newcontainer
# 提交容器为镜像
buildah commit $newcontainer checkpoint-image:tagPy
# 删除该容器
buildah rm $newcontainer
这时查看images列表,可以看到我们制作的镜像。
使用该镜像运行pod,为了做区分,恢复的pod修改名称叫做m-c,进入pod内部查看文件系统内的文件和MigLog.txt的内容,与之前的做对比,如下:
原来的pod:
恢复后的pod:
程序计数并没有重新开始,而是接续前面:
原来的pod:
恢复后的pod:
由此可见,在此集群上我们实现了的pod的有状态迁移。
2. 安装CRI-O
目前官方社区只支持CRI-O,可能后续将会支持Containerd,实际上从我的使用体验上来看,CRI-O的性能是优于Containerd的,而且CRI-O命令借助cri-tools工具,大多与Containerd相同,并不会有太多不适应。
做安装准备,安装必要依赖和添加环境变量:
# 更新系统并安装依赖
sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg2 software-properties-common -y
# 添加 CRI-O 环境变量
export OS=xUbuntu_22.04
export CRIO_VERSION=1.26
安装CRI-O包,Ubuntu系统安装稍微麻烦一些,需要添加仓库和密匙
# 切换管理员身份
sudo su
# 添加 CRI-O Kubic 仓库:
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /"| sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
echo "deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$CRIO_VERSION/$OS/ /"|sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION.list
# CRI-O 仓库导入 GPG 密钥:
curl -L https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION/$OS/Release.key | sudo apt-key add -
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo apt-key add -
sudo apt update #看是否报错
# 安装
sudo apt install cri-o cri-o-runc -y
修改一些配置信息:
vi /etc/crio/crio.conf
# 修改pod沙箱init容器版本,具体看k8s的pause版本,可以下载了k8s的启动镜像后再修改
pause_image = registry.aliyuncs.com/google_containers/pause:3.9 # 具体看k8s的pause版本
然后安装cri-tools与criu,pod的有状态迁移就是靠criu工具实现的
sudo apt install -y cri-tools criu
(选做)如果CRIU版本在3.16以下,可以选择手动安装CRIU版本,将CRIU版本升至3.16以上
# 下载源码
wget https://github.com/checkpoint-restore/criu/archive/refs/tags/v3.16.1.tar.gz
tar xf v3.16.1.tar.gz
# 安装必要依赖
yum install -y gcc make protobuf protobuf-c protobuf-c-devel \
libnl libnl3-devel libcap libcap-devel protobuf-compiler \
protobuf-devel libnet-devel libnet protobuf-python
# 构建可执行程序
cd criu-3.16.1/
make
# 替换掉原来版本的criu
cp ./criu/criu /usr/sbin/criu
# 查看版本信息至3.16以上即可
criu --version
一般不需要对cni做出修改,但如果出现接口不兼容的情况,则需要更换cni的版本:
# cni自定义版本下载
https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-amd64-v1.3.0.tgz
tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.3.0.tgz
# cni直接下载
apt install containernetworking-plugins -y
# 修改crio.config配置
vi /etc/crio/crio.config
# 修改cni路径,取消注释 network_dir 和 plugin_dirs 部分,并在 plugin_dirs 下添加 /usr/lib/cni/。
# 必要的话修改接口版本(方法略过)
# 每个人遇到的问题可能不一样,因此这里不过多描述,如有相同问题且不能解决的请评论区留言
最后,启动crio
sudo systemctl start crio
sudo systemctl enable crio
sudo systemctl status crio
3. K8S安装
k8s的安装教程特别多,这里写下一般的安装步骤。
1. 系统准备(所有节点均执行):
# 主机hosts映射
cat >> /etc/hosts << EOF
192.168.60.100 master
192.168.60.101 node1
192.168.60.102 node2
EOF
# 关闭防火墙
systemctl stop firewalld
systemctl disable firewalld
# 关闭selinux
apt install selinux-utils
setenforce 0
sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
# 关闭交换分区(为了保证 kubelet 正常工作,必须禁用交换分区)
swapoff -a
sed -i 's/.*swap.*/#&/' /etc/fstab
#转发 IPv4 并让 iptables 看到桥接流量
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
lsmod | grep br_netfilter #验证br_netfilter模块
# 设置所需的 sysctl 参数,参数在重新启动后保持不变
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
# 应用 sysctl 参数而不重新启动
sudo sysctl --system
# 执行date命令,查看时间是否异常
date
# 更换时区
sudo timedatectl set-timezone Asia/Shanghai
2. 下载镜像源(所有节点均执行):
# 配置阿里云镜像站点
curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -
cat >/etc/apt/sources.list.d/kubernetes.list <<EOF
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF
apt-get update
# 安装指定版本,与CRI-O版本保持一致即可
apt install -y kubeadm=1.26.4-00 kubelet=1.26.4-00 kubectl=1.26.4-00
3. 下载后使用 kubeadm生成配置文件,根据该文件修改即可(master节点执行):
# 生成默认配置并修改
kubeadm config print init-defaults > kubeadm.yaml
vi kubeadm.yaml
生成的配置文件做出如下修改:
apiVersion: kubeadm.k8s.io/v1beta3
bootstrapTokens:
- groups:
- system:bootstrappers:kubeadm:default-node-token
token: abcdef.0123456789abcdef
ttl: 24h0m0s
usages:
- signing
- authentication
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 0.0.0.0 # 修改为kubernetes主节点IP
bindPort: 6443
nodeRegistration:
criSocket: unix:///var/run/crio/crio.sock # 将默认的containerd改为crio
imagePullPolicy: IfNotPresent
name: k8s-matser # master节点的hostname
taints: null
---
apiServer:
timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta3
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns: {}
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.aliyuncs.com/google_containers # 修改镜像仓库为阿里云
kind: ClusterConfiguration
kubernetesVersion: 1.26.4 # 指定版本
networking:
dnsDomain: cluster.local
serviceSubnet: 10.96.0.0/12
podSubnet: 10.1.0.0/16 # 增加指定pod的网段
scheduler: {}
---
# 使用ipvs
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
---
# 指定cgroup
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
4. 集群初始化:
kubeadm init --config kubeadm.yaml
初始化成功后集群要求的操作:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
5. 配置flannel网络:
vi flannel.yaml
填入下面内容:
---
kind: Namespace
apiVersion: v1
metadata:
name: kube-flannel
labels:
k8s-app: flannel
pod-security.kubernetes.io/enforce: privileged
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
labels:
k8s-app: flannel
name: flannel
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- patch
- apiGroups:
- networking.k8s.io
resources:
- clustercidrs
verbs:
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
labels:
k8s-app: flannel
name: flannel
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flannel
subjects:
- kind: ServiceAccount
name: flannel
namespace: kube-flannel
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: flannel
name: flannel
namespace: kube-flannel
---
kind: ConfigMap
apiVersion: v1
metadata:
name: kube-flannel-cfg
namespace: kube-flannel
labels:
tier: node
k8s-app: flannel
app: flannel
data:
cni-conf.json: |
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds
namespace: kube-flannel
labels:
tier: node
app: flannel
k8s-app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
hostNetwork: true
priorityClassName: system-node-critical
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni-plugin
image: rancher/mirrored-flannelcni-flannel-cni-plugin:v1.0.0
#image: docker.io/rancher/mirrored-flannelcni-flannel-cni-plugin:v1.1.2
command:
- cp
args:
- -f
- /flannel
- /opt/cni/bin/flannel
volumeMounts:
- name: cni-plugin
mountPath: /opt/cni/bin
- name: install-cni
image: lizhenliang/flannel:v0.11.0-amd64
#image: docker.io/rancher/mirrored-flannelcni-flannel:v0.21.5
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: lizhenliang/flannel:v0.11.0-amd64
#image: docker.io/rancher/mirrored-flannelcni-flannel:v0.21.5
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: EVENT_QUEUE_DEPTH
value: "5000"
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
- name: xtables-lock
mountPath: /run/xtables.lock
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni-plugin
hostPath:
path: /opt/cni/bin
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
- name: xtables-lock
hostPath:
path: /run/xtables.lock
type: FileOrCreate
执行配置文件:
kubectl apply -f flannel.yaml
6. 创建一个加入令牌,并输出一个包含该令牌的节点加入命令,以便其他节点可以使用这个命令来加入集群:
kubeadm token create --print-join-command
根据提示,在node节点上执行join命令加入集群即可。