一、传统Pod升级的痛点:升级就断网,没人能忍
大家日常用K8s(或者说容器集群)部署应用时,升级版本是常事——比如修复了一个bug、加了新功能,就得把旧版本的Pod换成新版本。传统的升级逻辑很简单:先把旧Pod删掉,再拉新版本镜像、重新建一个新Pod。
这个逻辑看似没问题,但在很多场景下会出大麻烦。比如你做的是电商秒杀系统,升级那几秒刚好有用户下单,新Pod还没建好,服务直接断了,用户要么看到报错,要么得重试;再比如你做的是实时通信的服务,升级时连不上网,聊天消息直接丢了,用户体验直接崩。
更麻烦的是,很多应用没法快速启动——比如有的服务得加载几十G的模型,启动要好几分钟,这期间服务断网的时间太长,根本没法接受。
二、解决思路:原地升级,不删Pod只换容器
那有没有办法升级时不删Pod,只把Pod里的应用容器换成新版本?答案是有的,就是「原地升级」。
Pod在容器集群里的角色,其实是一个「容器的运行载体」:它本身负责绑定IP、挂载存储、分配资源这些核心配置,而里面跑的应用容器只是Pod里的一个子组件。原地升级的核心逻辑就是:不碰Pod本身的配置(比如IP、存储、网络这些),只把里面的旧应用容器删掉,再拉新版本的镜像,启动一个新的应用容器。
但这里有个大问题:Pod的网络是怎么来的?传统的Pod网络是跟着Pod建的——建Pod时,集群会给它分配一个IP,给它加一个虚拟网卡,把这个网卡绑到容器的网络命名空间里。如果我们不删Pod,只换容器,原来的网络命名空间是旧容器的,新容器要连这个网络,就得把自己的网络命名空间切换到旧的那个,这时候网络就会断一下,因为切换过程中网卡会重新初始化。
那怎么解决网络切换的问题?这就要用到CNI的「热插拔」功能。
三、核心技术拆解:CNI热插拔,让网络不中断
3.1 先搞懂CNI是什么
CNI的全称是容器网络接口,简单说就是一套给容器配网络的规则。集群里给Pod配网络的过程,都是按CNI的规则来的:建Pod时,集群会调用CNI插件,给Pod分配IP、加虚拟网卡、把网卡绑到容器的网络命名空间里。
传统的CNI操作是「一次性」的:建Pod时配一次,之后就不管了。而「热插拔」的意思是,CNI插件可以在Pod运行过程中,动态地把虚拟网卡绑到不同的容器网络命名空间里,而且这个过程不会断网。
为什么热插拔不会断网?因为虚拟网卡的配置(比如IP、路由)是提前存在网卡上的,只是换了个容器用它。就像你家里的网线,插在旧电脑上能用,拔下来插新电脑上,只要新电脑的网络配置和旧的一样,就能直接上网,不会断。
3.2 原地升级的完整流程
我们把整个原地升级的流程拆成几步,每一步都很清晰:
- 先有一个正常运行的旧Pod:这个Pod已经配好了IP、虚拟网卡、存储,里面跑着旧版本的应用容器;
- 暂停旧容器:先把旧容器暂停,不让它再跑业务,避免数据冲突;
- 旧容器解绑虚拟网卡:调用CNI插件,把旧容器里的虚拟网卡解绑,这时候网卡的配置还在,不会丢;
- 拉新版本镜像:集群拉取新版本的应用镜像;
- 启动新容器:启动新容器,但先不给它配网络;
- 新容器绑定虚拟网卡:调用CNI插件,把之前解绑的虚拟网卡绑到新容器的网络命名空间里;
- 恢复业务:新容器拿到虚拟网卡,直接就能用原来的IP、网络,业务直接恢复;
- 清理旧容器:把暂停的旧容器删掉。
整个过程中,Pod的IP、存储、网络配置都没变,只是换了里面的应用容器,网络只会在「解绑-绑定」的瞬间有极短的中断(大概几毫秒),几乎感觉不到。
四、完整示例:用Containerd实现原地升级
4.1 示例技术栈说明
本次示例使用的技术栈为:Containerd(容器运行时)、CNI(Calico插件,支持热插拔)、K8s(集群管理)。
4.2 前置准备:配置Calico支持热插拔
首先要确保Calico插件支持热插拔,Calico的配置文件一般在/etc/cni/net.d/目录下,我们需要修改配置,开启热插拔功能:
{
"name": "k8s-pod-network",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "calico",
"datastore_type": "kubernetes",
"ipam": {
"type": "calico-ipam"
},
"enable_route": true,
"enable_ipv4": true,
"enable_ipv6": false,
// 开启热插拔功能,核心配置
"hotplug": true,
"hotplug_delay": "1s"
},
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}
修改完配置后,重启Calico服务,让配置生效。
4.3 第一步:创建一个旧版本的测试Pod
我们先创建一个简单的Nginx Pod,版本是1.20,作为旧版本的应用:
apiVersion: v1
kind: Pod
metadata:
name: nginx-old
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.20 # 旧版本镜像
ports:
- containerPort: 80
用kubectl创建这个Pod:
kubectl apply -f nginx-old.yaml
等Pod运行起来后,我们可以查看它的IP和网络信息:
# 查看Pod的IP
kubectl get pod nginx-old -o wide
# 查看Pod的网络命名空间ID
kubectl exec nginx-old -- ip netns id
4.4 第二步:执行原地升级操作
我们把升级的操作写成一个Shell脚本,方便执行,脚本的每一步都加了注释:
#!/bin/bash
# 原地升级脚本,用于替换Pod内的应用容器
POD_NAME="nginx-old"
NEW_IMAGE="nginx:1.25" # 新版本镜像
# 1. 暂停旧容器,避免业务冲突
echo "暂停旧容器..."
kubectl exec $POD_NAME -- ctr task pause nginx
# 2. 解绑旧容器的虚拟网卡,调用Calico的CNI插件
echo "解绑旧容器的虚拟网卡..."
# 获取旧容器的网络命名空间路径
OLD_NETNS=$(kubectl exec $POD_NAME -- ip netns id)
# 调用CNI插件的DEL操作,解绑网卡
cni_plugin --container-id $POD_NAME --netns $OLD_NETNS --action DEL --config /etc/cni/net.d/10-calico.conflist
# 3. 拉取新版本镜像
echo "拉取新版本镜像..."
kubectl exec $POD_NAME -- ctr image pull $NEW_IMAGE
# 4. 启动新容器,先不配置网络
echo "启动新容器..."
# 这里用Containerd的命令启动新容器,指定不配置网络
kubectl exec $POD_NAME -- ctr run --no-privileged --rm --net none $NEW_IMAGE nginx-new
# 5. 绑定虚拟网卡到新容器
echo "绑定虚拟网卡到新容器..."
# 获取新容器的网络命名空间路径
NEW_NETNS=$(kubectl exec $POD_NAME -- ip netns id nginx-new)
# 调用CNI插件的ADD操作,绑定网卡
cni_plugin --container-id $POD_NAME --netns $NEW_NETNS --action ADD --config /etc/cni/net.d/10-calico.conflist
# 6. 清理旧容器
echo "清理旧容器..."
kubectl exec $POD_NAME -- ctr task kill nginx
kubectl exec $POD_NAME -- ctr container rm nginx
echo "原地升级完成!"
4.5 第三步:验证升级结果
执行完脚本后,我们来验证升级是否成功,网络有没有中断:
# 查看Pod的状态,看是否还是Running
kubectl get pod nginx-old
# 查看Pod内的容器版本,看是否是新版本
kubectl exec nginx-old -- nginx -v
# 测试访问Pod的服务,看是否正常
curl $(kubectl get pod nginx-old -o wide | awk '{print $6}' | tail -n 1)
你会发现,Pod的IP没有变,服务能正常访问,升级过程中几乎没有断网。
五、应用场景、优缺点与注意事项
5.1 应用场景
原地升级特别适合以下场景:
- 实时业务场景:比如直播、聊天、在线游戏,这些场景对断网的容忍度极低,升级时不能中断服务;
- 大内存/大模型应用:比如AI推理服务,启动需要加载几十G的模型,启动时间长,传统升级会导致长时间断网;
- 有状态应用:比如数据库、消息队列,这些应用的IP不能随便变,否则会导致连接中断,原地升级可以保持IP不变;
- 合规要求严格的场景:比如金融、医疗行业,要求服务不能中断,升级时必须保证业务连续性。
5.2 技术优缺点
优点
- 网络中断时间极短:只有几毫秒,几乎感觉不到;
- 保持Pod配置不变:IP、存储、资源配额都不变,避免了重新分配带来的问题;
- 启动速度快:不需要重新分配IP、挂载存储,只需要启动新容器,升级速度比传统升级快很多;
- 适合大启动时间的应用:不需要等待应用加载模型、初始化数据,升级后直接恢复业务。
缺点
- 依赖CNI插件的热插拔支持:不是所有的CNI插件都支持热插拔,比如Flannel的部分版本就不支持;
- 容器启动失败的风险:如果新容器启动失败,旧容器已经被暂停,可能会导致服务中断;
- 资源隔离问题:原地升级过程中,新旧容器会共享Pod的资源配额,可能会导致资源不足;
- 调试复杂:原地升级的流程比传统升级复杂,出问题时调试难度大。
5.3 注意事项
- 提前测试CNI插件的热插拔功能:在生产环境使用前,一定要测试CNI插件是否支持热插拔,避免出现兼容问题;
- 做升级前的备份:原地升级前,一定要备份应用的数据,避免升级失败导致数据丢失;
- 配置资源配额:Pod的资源配额要足够大,能容纳新旧两个容器同时运行,避免资源不足;
- 做灰度升级:先在非核心业务上测试原地升级,再推广到核心业务;
- 监控升级过程:升级过程中要监控Pod的状态、网络、业务指标,及时发现问题。
六、总结
传统的Pod升级方式,因为会删旧Pod、建新Pod,导致网络中断时间长,无法满足实时业务、大启动时间应用的需求。而基于Containerd的原地升级方案,通过CNI热插拔功能,实现了不删Pod只换容器的升级,网络中断时间极短,大大提升了升级的体验。
当然,原地升级也不是万能的,它依赖CNI插件的支持,也有一些风险。在实际使用时,我们需要根据自己的业务场景,评估是否适合使用原地升级,同时做好测试和备份,确保升级的安全和稳定。
评论
围绕“基于Containerd的Pod原地升级方案:利用CNI热插拔解决网络中断痛点”参与讨论