一、为什么我们需要“优雅”的更新与回滚?

想象一下,你负责的在线购物网站正在运行着成百上千个服务实例。现在,你需要发布一个新版本,修复了一个重要的商品显示bug。如果采用传统方式:先停掉所有老版本的服务,然后一起启动新版本,网站就会在更新期间彻底无法访问,这无疑是灾难性的。

Kubernetes(常简称为K8s)为我们提供了更聪明的办法:滚动更新。它就像给一列高速行驶的火车更换零件,一次只换一小部分,确保火车始终在前进,乘客(也就是用户)几乎感觉不到颠簸。同时,万一新换的零件有问题(新版本有缺陷),我们还能迅速把刚换上的新零件拆下来,装回老的、可靠的零件,这就是回滚。两者的结合,是实现服务不中断部署和快速故障恢复的基石。

简单来说,滚动更新的目标是“平滑过渡”,而回滚策略是我们的“安全气囊”。接下来,我们就一步步拆解这背后的完整流程。

二、理解核心概念:Deployment与Pod

在深入流程之前,我们需要认识Kubernetes中的两个关键角色:Deployment和Pod。

你可以把 Pod 理解为服务运行的最小“集装箱”,里面封装着我们的应用容器(比如一个运行着网站后台的Docker容器)。一个服务通常由多个一模一样的Pod副本同时运行,以分担流量和提高可靠性。

Deployment 则是管理这些Pod的“总管”或“蓝图”。我们不直接指挥Pod,而是告诉Deployment:“我需要3个这样的Pod副本。”Deployment就会确保始终有3个健康的Pod在运行。更重要的是,当我们想更新应用时,我们只需要更新Deployment里的“蓝图”(比如更换成新的容器镜像),Deployment就会自动地、按照我们设定的策略,去用新Pod替换旧Pod,这个过程就是滚动更新。

因此,我们的所有操作,几乎都是围绕配置Deployment来进行的。

三、滚动更新:一步步实现“无缝切换”

现在,让我们通过一个完整的示例,来看看如何配置和触发一次滚动更新。

技术栈声明: 本文所有示例均基于 Kubernetes 原生资源定义,使用 YAML 格式进行描述。

假设我们有一个名为my-web-app的简单Web服务,最初使用my-web-app:v1.0镜像。现在我们开发了v2.0版本,准备上线。

首先,这是我们最初的Deployment定义文件 deployment-v1.yaml

# deployment-v1.yaml
apiVersion: apps/v1          # 使用的Kubernetes API版本
kind: Deployment             # 资源类型是Deployment
metadata:
  name: my-web-app           # Deployment的名称,非常重要
spec:
  replicas: 3                # 期望的Pod副本数量,这里指定需要运行3个副本
  selector:                  # 标签选择器,用于找到由这个Deployment管理的Pod
    matchLabels:
      app: my-web-app
  template:                  # Pod的模板,也就是“蓝图”的具体内容
    metadata:
      labels:                # 给Pod打上标签,必须和上面的selector匹配
        app: my-web-app
    spec:
      containers:            # 定义Pod里运行的容器
      - name: web-container  # 容器的名字
        image: my-web-app:v1.0  # 使用的容器镜像,这里是v1.0版本
        ports:
        - containerPort: 8080   # 容器内部监听的端口

我们通过命令 kubectl apply -f deployment-v1.yaml 将其部署到集群。此时,Deployment会创建3个运行着v1.0镜像的Pod。

几天后,v2.0版本准备好了。我们不需要写全新的YAML,只需要更新镜像标签并再次应用。但更规范的做法是创建一个新的文件 deployment-v2.yaml,只修改镜像版本:

# deployment-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-web-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-web-app
  # 以下是滚动更新的关键策略配置
  strategy:
    type: RollingUpdate        # 更新类型,指定为“滚动更新”
    rollingUpdate:
      maxSurge: 1              # 在更新过程中,允许临时超出期望副本数的最大数量。
                               # 值为1表示更新时最多可以有3+1=4个Pod同时运行。
      maxUnavailable: 0        # 在更新过程中,允许不可用Pod的最大数量。
                               # 值为0表示更新时必须始终有3个可用的Pod。
  template:
    metadata:
      labels:
        app: my-web-app
    spec:
      containers:
      - name: web-container
        image: my-web-app:v2.0  # 唯一的变化:将镜像版本从v1.0改为v2.0
        ports:
        - containerPort: 8080

执行更新命令:kubectl apply -f deployment-v2.yaml

接下来,Kubernetes会按照strategy里的配置开始工作:

  1. 创建新Pod:由于maxSurge: 1,它先创建1个新的v2.0 Pod。此时总共有4个Pod(3个v1.0,1个v2.0)。
  2. 等待新Pod就绪:Kubernetes会等待这个新Pod通过“就绪探针”检查,确认它能正常服务。
  3. 替换旧Pod:新Pod就绪后,由于maxUnavailable: 0,它需要先保证有3个可用Pod,所以会删除1个旧的v1.0 Pod。
  4. 循环往复:再创建1个新的v2.0 Pod,删除1个旧的v1.0 Pod,直到所有3个Pod都替换为v2.0版本。

在这个过程中,服务的总可用实例数从未低于3个(maxUnavailable: 0),因此用户的请求始终有地方处理,实现了不中断部署。maxSurgemaxUnavailable的配置,让你可以在更新速度和稳定性之间做权衡。

四、回滚:当更新出问题时的“后悔药”

滚动更新虽然平滑,但新版本上线后可能隐藏着只有在生产环境才会触发的bug。比如v2.0版本上线后,监控系统发现错误率飙升。这时,我们需要立刻回退到稳定的v1.0版本。

Kubernetes的Deployment完美记录了每次更新。我们可以查看更新历史:

kubectl rollout history deployment/my-web-app

这个命令会列出该Deployment的所有修订版本。每个更新(kubectl apply)都会创建一个新的修订记录。

假设我们看到v2.0是第2版,而稳定的v1.0是第1版。回滚命令非常简单:

# 回滚到上一个版本
kubectl rollout undo deployment/my-web-app

# 或者明确指定回滚到历史中的某个特定修订版本
kubectl rollout undo deployment/my-web-app --to-revision=1

执行回滚命令后,Kubernetes会立刻启动一次反向的滚动更新!它会将Pod的镜像从有问题的v2.0,按照同样的滚动策略,逐步替换回v1.0。这个过程通常非常快,因为不需要重新拉取镜像(v1.0镜像已经在节点本地缓存了),能在几十秒内完成故障恢复。

为了让历史记录更清晰,我们可以在每次更新时添加注解,说明更新原因:

# 在Deployment的metadata.annotations或spec.template.metadata.annotations中添加
template:
  metadata:
    annotations:
      kubernetes.io/change-cause: "更新至v2.0,修复商品详情页图片显示问题"
    labels:
      app: my-web-app

这样,在kubectl rollout history中就能看到清晰的变更说明,方便判断该回滚到哪个版本。

五、让流程更完善:就绪探针与健康检查

在上面的滚动流程中,我们提到了“等待新Pod就绪”。Kubernetes如何知道一个Pod是否就绪呢?这依赖于一个关键配置:就绪探针

没有就绪探针,Kubernetes认为容器启动后立即就能提供服务。但现实是,应用启动后可能需要加载配置、连接数据库、预热缓存,这可能需要几秒到几十秒。如果在新Pod还未真正准备好时就切流量给它,用户就会遇到错误。

因此,我们必须为容器定义就绪探针。下面是一个添加到Pod模板中的示例:

# 这是deployment-v2.yaml中containers部分的一个补充示例
containers:
- name: web-container
  image: my-web-app:v2.0
  ports:
  - containerPort: 8080
  # 定义就绪探针
  readinessProbe:
    httpGet:                # 使用HTTP GET请求进行检查
      path: /health         # 检查的URL路径,你的应用需要提供这个健康检查接口
      port: 8080
    initialDelaySeconds: 5  # 容器启动后,等待5秒才开始第一次探测
    periodSeconds: 5        # 每隔5秒执行一次探测
    successThreshold: 1     # 连续1次探测成功则认为就绪
    failureThreshold: 3     # 连续3次探测失败则认为未就绪

有了这个配置,在滚动更新时,新的v2.0 Pod启动后,Kubernetes会等待5秒,然后尝试访问该Pod的http://:8080/health。只有当这个接口返回成功状态码(2xx或3xx),Kubernetes才认为这个Pod“就绪”了,才会进行下一步操作(如删除旧Pod)。这确保了只有健康的新实例才会接收用户流量。

除了就绪探针,还有存活探针,用于检查容器是否崩溃,如果失败Kubernetes会重启容器。两者结合,构成了应用在K8s中稳定运行的保障。

六、应用场景、优缺点与注意事项

应用场景:

  • Web服务/API后端持续发布:这是最典型的场景,需要7x24小时不间断服务。
  • 微服务架构更新:在复杂的微服务系统中,每个服务独立更新,滚动更新和回滚是保证整体稳定的关键。
  • 配置热更新:不仅限于代码,更新ConfigMap或Secret中的配置时,也可以通过滚动更新让Pod逐步应用新配置。
  • 数据库或中间件版本升级:对于支持滚动升级的StatefulSet(管理有状态应用的类似Deployment的资源),原理也相通。

技术优点:

  1. 服务不中断:通过精细控制Pod替换节奏,最大程度保证服务可用性。
  2. 快速故障恢复:一键回滚,能在分钟级甚至秒级内从有问题的版本恢复。
  3. 自动化与声明式:只需声明“最终状态”(如镜像v2.0),K8s自动完成复杂的过程。
  4. 风险可控:可以分批次替换(如先替换20%的实例),观察监控指标,确认无误后再继续。

潜在缺点与挑战:

  1. 版本兼容性:新版本Pod和旧版本Pod同时运行时,需要确保它们之间的接口(如果存在调用)和对外接口是兼容的。
  2. 数据一致性:对于有状态服务,滚动更新期间多个版本同时访问数据,需要应用层面处理好数据格式兼容问题。
  3. 探针配置至关重要:如果就绪探针配置不当(如检查路径不对或阈值太松),可能导致不健康的Pod被误认为就绪,引发故障。
  4. 资源需求:更新过程中,由于maxSurge的存在,可能会临时消耗更多CPU和内存资源。

重要注意事项:

  • 务必配置就绪探针:这是滚动更新能正确工作的前提,否则更新可能引发服务抖动。
  • 合理设置策略参数maxUnavailable: 0最安全但更新最慢;maxUnavailable: 50%更新更快,但可能影响部分容量。需根据业务容忍度调整。
  • 回滚前先调查:回滚是止损措施,但事后一定要分析新版本失败的原因,避免下次更新重蹈覆辙。
  • 结合CI/CD流水线:将kubectl applykubectl rollout status/undo集成到Jenkins、GitLab CI等工具中,实现自动化发布和自动化回滚判断。

七、总结

Kubernetes的滚动更新与回滚机制,将原本高风险、需要人工干预的发布和故障恢复操作,变成了一个标准化、自动化、可预测的流程。其核心思想在于“渐进”与“可控”。

通过Deployment这个“总管”,我们只需关心目标状态(用哪个镜像),而复杂的替换过程交给系统。通过精细的策略(maxSurge, maxUnavailable)和健康检查(就绪探针),我们能在更新速度与稳定性间取得平衡。而内置的版本历史与一键回滚功能,则为我们提供了坚实的安全网。

掌握这套流程,意味着你能够自信地对生产环境进行频繁、可靠的更新,这是拥抱DevOps和持续交付理念的关键一步。从今天起,尝试为你管理的服务配置好滚动更新和探针,告别“停机发布”的焦虑,享受“平滑如丝”的部署体验吧。