更新和回滚应用

KubernetesBeginner
立即练习

简介

现在,你已经可以部署、暴露并扩展应用了。接下来需要解决的运维问题是:如何替换应用版本,同时避免让整个服务下线?

Kubernetes Deployment 通过滚动更新解决这一问题。当 Pod 模板发生变化时,Deployment 会创建一个新的 ReplicaSet,逐步启动新 Pod,并且只有在替代 Pod 可用后才会删除旧 Pod。在此过程中,Service 始终保持同一个稳定地址。

在本实验中,你将把 NGINX 应用从一个固定版本的镜像切换到另一个版本,了解 Deployment 修订版本、ReplicaSet 和 Pod 之间的关系,主动尝试一次失败的发布,然后回滚到上一个健康的修订版本。你还会让保存的清单文件与恢复后的线上状态保持一致——这是一个非常重要的习惯,可以避免之后执行 kubectl apply 时再次引入故障。

查看初始环境

环境启动: 本实验会为你启动一个完整的 Kubernetes 集群。配置控制平面、节点和网络组件通常需要 2~3 分钟。请耐心等待环境加载完成后再开始操作。

在此步骤中,你将确认集群连接,并准备一个集群内的 HTTP 客户端。

本课程环境已经包含一个正在运行的 Kubernetes 集群,因此不需要创建集群。首先,确认 kubectl 将把命令发送到哪里。config current-context 会显示当前使用的连接,get node 会读取节点健康状态,而 version 会报告本地客户端和可访问的 API 服务器版本:

kubectl config current-context
kubectl get node
kubectl version

上下文和节点名称均为 labex-v135,节点状态为 Ready,服务器报告的 Kubernetes 版本为 v1.35.x。上下文是 kubeconfig 中的一组选项,用于将 kubectl 连接到特定的集群、用户和默认命名空间。

现在创建一个小型客户端 Pod。稍后你将使用它通过 Service 访问应用。kubectl run 会创建 Pod;--image 选择 BusyBox;--restart=Never 让它保持为独立 Pod;-- 分隔符之后是容器中持续运行的 sleep 3600 命令。随后,kubectl wait 最多等待 30 秒,直到 Pod 满足 Ready 条件:

kubectl run release-client --image=busybox:1.36 --image-pull-policy=IfNotPresent \
  --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/release-client --timeout=30s

这样可以将调用方与 Web Pod 分离,更接近集群内一个工作负载调用另一个工作负载的实际场景。

部署稳定基线

在此步骤中,你将先建立一个已知正常的应用修订版本,然后再进行任何更改。

在练习更新之前,先建立一个已知正常的版本。创建一个清单文件,其中包含一个三副本 Deployment 和一个稳定的 ClusterIP Service。

cd 用于进入准备好的工作目录。Here Document 语法 cat <<'EOF' > release-web.yaml 会把内容写入文件,直到遇到结尾的 EOF--- 行用于分隔同一个 YAML 文件中的两个 Kubernetes 对象:

cd /home/labex/project/update-lab
cat <<'EOF' > release-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: release-web
  annotations:
    kubernetes.io/change-cause: "Initial release: nginx 1.26"
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: release-web
  template:
    metadata:
      labels:
        app: release-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.26-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: release-web
spec:
  selector:
    app: release-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment,service,pods -l app=release-web

Deployment 负责管理 Pod,而 Service 则通过标签独立选择这些 Pod。更新 Deployment 不会改变 Service 的地址。

通过客户端确认基线状态。在 kubectl exec POD -- COMMAND 中,-- 用于分隔 kubectl 参数和容器内执行的命令。wget -qO- 会安静地将内容获取到标准输出,管道再把响应传给 head,因此只显示响应开头部分:

kubectl exec release-client -- wget -qO- http://release-web | head

以声明方式发布新镜像

在此步骤中,你将修改保存的 Pod 模板,并让 Deployment 执行滚动更新。

当 Deployment 的 Pod 模板发生变化时,就会开始发布。镜像位于 Pod 模板中,因此修改镜像会创建新的修订版本和新的 ReplicaSet。

同时更新保存的清单文件中的镜像和易于阅读的变更原因。每个 sed -i 's/old/new/' file 替换命令都会直接修改文件。随后,grep -nE 显示包含两个扩展正则表达式项(change-causeimage:)的行号,便于你在应用前检查这两处修改:

cd /home/labex/project/update-lab
sed -i 's/Initial release: nginx 1.26/Release nginx 1.27/' release-web.yaml
sed -i 's/nginx:1.26-alpine/nginx:1.27-alpine/' release-web.yaml
grep -nE 'change-cause|image:' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s

kubectl apply 会修改期望状态。随后,Deployment 控制器会异步工作,直到三个更新后的副本全部可用。若要查看精简的 Pod 表格,-l 用于选择应用标签,-o custom-columns 则将列标题映射到对象字段路径:

kubectl get deployment release-web
kubectl get pods -l app=release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready'

当前三个副本都使用了新镜像。滚动更新成功后,你可能还会短暂看到一个使用旧镜像的 Pod 处于 Terminating 状态。该 Pod 已不再是期望副本,Kubernetes 会在后台完成它的优雅终止。

关联修订版本、ReplicaSet 和 Pod

在此步骤中,你将检查成功发布背后的对象,并确认 Service 始终保持可用。

滚动更新已经完成,但理解具体发生了什么,比单纯看到「成功」更有价值。kubectl rollout history 会读取 Deployment 保存的修订版本。下面的 get replicasets 命令使用 -l 保留相关对象,并通过 custom-columns 对比它们的副本数量和镜像:

kubectl rollout history deployment/release-web
kubectl get replicasets -l app=release-web \
  -o custom-columns='NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'

你应该会看到两个 ReplicaSet。新的 ReplicaSet 管理三个 Pod;旧的 ReplicaSet 保持零副本,以便其 Pod 模板可用于回滚。ReplicaSet 名称中包含一个根据 Pod 模板生成的哈希值,因此镜像变化后,Pod 名称也会发生变化。

检查线上镜像和 Service 的后端数量。两个 JSONPath 表达式都使用 range,以便对列表中的每个对象重复输出模板。字面空格和 {"\n"} 让每个 Pod 或端点各占一行,便于阅读:

kubectl get pods -l app=release-web \
  -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.spec.containers[0].image}{"\n"}{end}'
kubectl get endpointslices -l kubernetes.io/service-name=release-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
kubectl exec release-client -- wget -qO- http://release-web | head

修订版本发生了变化,但 Service 仍然拥有三个已就绪的后端,并且名称保持稳定。

诊断失败的发布

在此步骤中,你将引入一个可控的镜像错误,并通过 Pod 事件确定滚动更新无法完成的原因。

现在模拟一个常见的发布错误:容器镜像标签不存在。让这个故障发生在同一个 Deployment 上,这样滚动更新机制就能展示一个重要的安全特性。下面两个 sed -i 命令会直接修改注解和镜像文本。正常情况下,预期的滚动更新超时会返回失败状态;|| true 会有意让课程在此之后继续:

cd /home/labex/project/update-lab
sed -i 's/Release nginx 1.27/Broken release: missing image/' release-web.yaml
sed -i 's/nginx:1.27-alpine/nginx:does-not-exist-course/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=20s || true
kubectl get deployment release-web
kubectl get pods -l app=release-web

由于新 Pod 无法拉取镜像,滚动更新无法完成。|| true 让课程在预期的超时之后继续执行。

找出失败的 Pod 并检查其事件。$(...) 会将命令输出保存到 BAD_POD。第一个管道将 Kubernetes JSON 传给 jqselect(...) 保留使用错误镜像的 Pod,-r 则以纯文本形式返回其名称。第二个管道连接到 head -n1,只保留一个名称。最后,sed -n '/Events:/,$p' 会从 Events: 开始打印 describe 输出,直到末尾:

BAD_POD=$(kubectl get pods -l app=release-web \
  -o json | jq -r '.items[] | select(.spec.containers[0].image == "nginx:does-not-exist-course") | .metadata.name' | head -n1)
echo "$BAD_POD"
kubectl describe pod "$BAD_POD" | sed -n '/Events:/,$p'
kubectl get replicasets -l app=release-web

查找 ErrImagePullImagePullBackOff。注意,之前健康的 ReplicaSet 仍然拥有可用 Pod,因此 Service 仍可以继续响应请求:

kubectl exec release-client -- wget -qO- http://release-web | head

这说明失败的滚动更新期间服务仍保持可用,但不能证明新版本发布正常。

回滚并协调清单文件

在此步骤中,你将恢复到之前健康的修订版本,然后修复保存的清单文件。

线上 Deployment 保留着修订历史,因此 Kubernetes 可以恢复之前的 Pod 模板。rollout undo 会要求 Deployment 控制器重新使用上一个修订版本;rollout status 会等待恢复完成;custom-columns 则会输出恢复后的镜像和就绪副本数量:

kubectl rollout history deployment/release-web
kubectl rollout undo deployment/release-web
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image,READY:.status.readyReplicas'

镜像已经恢复正常,三个副本也都已就绪。rollout undo 会根据较早的 Pod 模板创建一个新的修订版本,而不是回退修订版本计数器。

还有一项工作需要完成。YAML 文件中仍然包含错误镜像,因此之后再次执行 kubectl apply 会让 Deployment 再次出错。请让保存的期望状态与恢复后的线上状态保持一致。下面的替换命令会修复文件,apply 会同步状态,最后的历史命令则确认生成的修订版本轨迹:

cd /home/labex/project/update-lab
sed -i 's/Broken release: missing image/Rollback to nginx 1.27/' release-web.yaml
sed -i 's/nginx:does-not-exist-course/nginx:1.27-alpine/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
grep -nE 'change-cause|image:' release-web.yaml
kubectl rollout history deployment/release-web

现在,集群和文件都描述着同一个健康的发布版本。

选择更安全的更新资源配置

在此步骤中,你将明确 Deployment 在滚动更新期间允许使用的临时容量和可用性限制。

Deployment 策略设置会控制未来更新期间允许的临时资源范围:

  • maxUnavailable 表示滚动更新期间最多允许多少个期望副本不可用。
  • maxSurge 表示滚动更新期间最多允许超出期望副本数运行多少个额外 Pod。

对于这个拥有三个副本的小型应用,要求三个期望副本始终保持可用,并允许额外运行一个 Pod。

sed 命令使用 /type: RollingUpdate/ 作为地址来定位策略行。其 a\ 操作会追加下面这些带有换行和正确缩进的 YAML 内容。应用后,JSONPath 会提取两个策略值,因此你无需浏览完整对象即可验证修改:

cd /home/labex/project/update-lab
sed -i '/type: RollingUpdate/a\    rollingUpdate:\n      maxUnavailable: 0\n      maxSurge: 1' release-web.yaml
kubectl apply -f release-web.yaml
kubectl get deployment release-web \
  -o jsonpath='maxUnavailable={.spec.strategy.rollingUpdate.maxUnavailable}{"\n"}maxSurge={.spec.strategy.rollingUpdate.maxSurge}{"\n"}'
kubectl get deployment release-web

只修改策略字段不会替换 Pod,因为 Pod 模板并未发生变化。在未来更新镜像时,Kubernetes 最多可以创建一个额外 Pod,并且不会主动将可用副本数降到三个以下。

这种设置更重视可用性,但需要集群具备额外容量。不存在适用于所有场景的统一最佳值:规模更大的应用通常会使用百分比,实际选择取决于资源容量、启动时间以及可接受的服务中断程度。

总结

你完整体验了应用发布的运维生命周期:

  • 建立健康的 Deployment 和稳定的 Service 基线;
  • 以声明方式修改固定版本镜像,并等待滚动更新完成;
  • 将 Deployment 修订版本与新旧 ReplicaSet 关联起来;
  • 在旧版本继续提供服务的同时,诊断镜像拉取失败;
  • 回滚到上一个健康的 Pod 模板;
  • 协调清单文件,避免之后再次应用时恢复错误镜像;以及
  • 为未来更新配置明确的可用性和额外容量预算。

核心理念是:Deployment 会随着时间推移持续管理期望状态。滚动更新并不只是修改镜像,而是在 ReplicaSet 之间进行受控切换;你应当观察和验证这一过程,并随时准备在必要时将其逆转。