一、传统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 原地升级的完整流程

我们把整个原地升级的流程拆成几步,每一步都很清晰:

  1. 先有一个正常运行的旧Pod:这个Pod已经配好了IP、虚拟网卡、存储,里面跑着旧版本的应用容器;
  2. 暂停旧容器:先把旧容器暂停,不让它再跑业务,避免数据冲突;
  3. 旧容器解绑虚拟网卡:调用CNI插件,把旧容器里的虚拟网卡解绑,这时候网卡的配置还在,不会丢;
  4. 拉新版本镜像:集群拉取新版本的应用镜像;
  5. 启动新容器:启动新容器,但先不给它配网络;
  6. 新容器绑定虚拟网卡:调用CNI插件,把之前解绑的虚拟网卡绑到新容器的网络命名空间里;
  7. 恢复业务:新容器拿到虚拟网卡,直接就能用原来的IP、网络,业务直接恢复;
  8. 清理旧容器:把暂停的旧容器删掉。

整个过程中,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 应用场景

原地升级特别适合以下场景:

  1. 实时业务场景:比如直播、聊天、在线游戏,这些场景对断网的容忍度极低,升级时不能中断服务;
  2. 大内存/大模型应用:比如AI推理服务,启动需要加载几十G的模型,启动时间长,传统升级会导致长时间断网;
  3. 有状态应用:比如数据库、消息队列,这些应用的IP不能随便变,否则会导致连接中断,原地升级可以保持IP不变;
  4. 合规要求严格的场景:比如金融、医疗行业,要求服务不能中断,升级时必须保证业务连续性。

5.2 技术优缺点

优点

  1. 网络中断时间极短:只有几毫秒,几乎感觉不到;
  2. 保持Pod配置不变:IP、存储、资源配额都不变,避免了重新分配带来的问题;
  3. 启动速度快:不需要重新分配IP、挂载存储,只需要启动新容器,升级速度比传统升级快很多;
  4. 适合大启动时间的应用:不需要等待应用加载模型、初始化数据,升级后直接恢复业务。

缺点

  1. 依赖CNI插件的热插拔支持:不是所有的CNI插件都支持热插拔,比如Flannel的部分版本就不支持;
  2. 容器启动失败的风险:如果新容器启动失败,旧容器已经被暂停,可能会导致服务中断;
  3. 资源隔离问题:原地升级过程中,新旧容器会共享Pod的资源配额,可能会导致资源不足;
  4. 调试复杂:原地升级的流程比传统升级复杂,出问题时调试难度大。

5.3 注意事项

  1. 提前测试CNI插件的热插拔功能:在生产环境使用前,一定要测试CNI插件是否支持热插拔,避免出现兼容问题;
  2. 做升级前的备份:原地升级前,一定要备份应用的数据,避免升级失败导致数据丢失;
  3. 配置资源配额:Pod的资源配额要足够大,能容纳新旧两个容器同时运行,避免资源不足;
  4. 做灰度升级:先在非核心业务上测试原地升级,再推广到核心业务;
  5. 监控升级过程:升级过程中要监控Pod的状态、网络、业务指标,及时发现问题。

六、总结

传统的Pod升级方式,因为会删旧Pod、建新Pod,导致网络中断时间长,无法满足实时业务、大启动时间应用的需求。而基于Containerd的原地升级方案,通过CNI热插拔功能,实现了不删Pod只换容器的升级,网络中断时间极短,大大提升了升级的体验。

当然,原地升级也不是万能的,它依赖CNI插件的支持,也有一些风险。在实际使用时,我们需要根据自己的业务场景,评估是否适合使用原地升级,同时做好测试和备份,确保升级的安全和稳定。