Kubernetes 滚动发布与回滚:保障 CI/CD 变更安全
滚动发布、暂停继续与回滚的标准动作,以及一次真实发布事故的完整复盘。
这篇文章要解决的问题是:每次发布都自动部署了,怎么保证"发得动、发得完、发坏了 60 秒内回得来"。背后最基础的事实是:Deployment 并不直接管理 Pod,而是通过 ReplicaSet 管理;"发布"本质是换一份 Pod 模板,控制器据此在多个 ReplicaSet 之间做有边界的扩缩容,"回滚"就是把 Pod 模板恢复成历史版本,再执行一次同样的替换。把这条因果链拆清楚,滚动发布、revision 历史、progress deadline 超时、undo 回滚就都能从机制上解释,而不只是一堆要背的命令。
#原理层:发布与回滚为什么是这样工作的
#Deployment 不直接管 Pod:三层结构
最底层的事实:Deployment 描述"期望状态"——要多少个副本、每个副本长什么样(Pod 模板)。它自己不做调度,而是通过 ReplicaSet(RS)维持副本数:
Deployment(期望状态:replicas、Pod 模板)
└─ ReplicaSet(版本快照:一份固定的 Pod 模板 + 副本计数)
└─ Pod(实际运行的容器)ReplicaSet 的职责是"让带特定标签的 Pod 数量等于期望值",它创建的每个 Pod 都来自自己持有的模板;Deployment 的职责是"决定现在应该有几个 RS、各自多少个副本"。关键推论:Deployment 从不直接改 Pod,只改变 RS 的规模,Pod 由 RS 控制器补齐。
#什么算"一次发布":Pod 模板是唯一触发点
只要 spec.template 内容变化(镜像、环境变量、资源请求、探针、卷、模板注解),Deployment 控制器就认为"这是一次新的发布",机制动作是:用新模板生成一个 RS(模板不变则复用 hash 相同的旧 RS),新 RS 副本数从 0 涨、旧 RS 往 0 降,直到新 RS 完全接管。反过来,没落在模板上的变更不触发滚动——kubectl scale 只改 replicas,直接调整当前 RS 规模,不产生新版本。很多人把"发布"理解成"任何一次 Deployment 变更",这是错的。
#maxSurge / maxUnavailable:一对边界约束
滚动节奏由 strategy.type: RollingUpdate 下的两个参数约束:
- maxUnavailable:更新过程中最多允许多少副本不可用,是"可用副本数的下限保证";
- maxSurge:最多允许超出期望副本数多少,是"总副本数的上限保证"。
官方字段定义(以 Kubernetes 1.29 文档为例)明确了计算规则:两者可写绝对值或期望副本数的百分比(默认 25%);百分比换算时 maxUnavailable 向下取整、maxSurge 向上取整;二者不能同时为 0。滚动期间,可用副本数始终被约束在 [replicas - maxUnavailable, replicas + maxSurge] 区间内。
控制器每轮协调做同一件事:在新 RS 上扩一点、在旧 RS 上缩一点,每次推进先查两条边界——总副本数不超过 desired + maxSurge,可用副本数不低于 desired - maxUnavailable。由此观察到的现象:
- maxUnavailable: 0, maxSurge: 1(保守配置):先起一个新 Pod,等 readiness 探针通过、变成 Ready,才缩掉一个旧 Pod,循环到全部替换。容量始终不降,代价是更新期间资源占用约为原来的两倍。
- maxUnavailable: 1, maxSurge: 1(追求快):允许先缩旧再起新或同时多起一个,滚动更快,但存在一个副本窗口期不可用;多副本通常无感,单副本会闪断。
关键点:这两个参数不是"目标"而是"边界"。控制器不会一口气把新 RS 扩满,而是在边界内逐步推进,每一步都等新 Pod 真正就绪。滚动节奏的真正闸门是 readiness 探针——新 Pod 不就绪,控制器就停在原地,既不扩新也不缩旧。
#rollout 如何跟踪历史:pod-template-hash 与 revision
每次模板变化产生新 RS 时,RS 被基于模板内容打上哈希标签 pod-template-hash,并写进 RS 名字(如 demo-web-65b9c8d4f7);相同模板永远得到相同哈希,这是"模板是否真的变了"的机器判断依据。revision 是控制器维护的单调递增整数,记录在 Deployment 与每个 RS 的注解 deployment.kubernetes.io/revision 上。kubectl rollout history 就是把保留着的 RS 按 revision 排列,配合人工注解 kubernetes.io/change-cause 显示每次发布的说明。所以历史不是玄学:它就是集群里还保留着的那些 RS(默认保留最近 10 个,由 revisionHistoryLimit 控制;被清理后对应 revision 就回不去了)。revision 从 1 递增,每次模板变化 +1;回滚也是一次模板变化,通常会在历史里生成一个更高编号的新 revision,而不是"回到"旧编号。
#progressDeadlineSeconds:rollout 什么时候算"卡死"
progressDeadlineSeconds 默认 600 秒,语义是:一次发布开始后,如果该时间窗口内没有取得进展(没创建新 RS,或已创建但没有副本变可用),控制器把状态条件 Progressing 置为 False,原因记为 ProgressDeadlineExceeded。三点澄清:
- 它不会自动回滚,只是打"超时"标记;控制器之后仍可能继续尝试,有进展则条件被清除。
- 它配合 kubectl rollout status 才有自动化价值:后者 watch 到该条件时停止等待并以非零退出码返回——这正是 CI 判断"发布失败"的信号。
- 发布被暂停(paused)期间控制器不推进,进度计时也不会继续走;不能用 pause 无限期拖延 deadline。
#回滚的实质:恢复模板,而不是"时光倒流"
kubectl rollout undo 做的事:把 Deployment 的 spec.template 恢复成目标 revision 对应的模板。模板一变,控制器按前述滚动机制再执行一次替换。两个推论:
- 回滚也是一次滚动发布,受当前 maxSurge / maxUnavailable 约束逐步替换,回滚后一样要等 rollout 完成。
- 控制器按模板哈希复用历史 RS:恢复的模板与某旧版本相同时,控制器不新建 RS,而是重新激活(从 0 扩容)那个 hash 匹配的旧 RS——前提是它没被 revisionHistoryLimit 清理。这也是回滚通常很快的原因:旧 RS 的镜像可能还在节点上。
审计记录来自三个层次:rollout history 列出保留的 revision;change-cause 是每次发布人工打的说明(undo 不会自动打,回滚前建议手动补一条,否则 CHANGE-CAUSE 显示
#pause / resume:暂停的是控制器协调
kubectl rollout pause 把 spec.paused 置为 true,效果是控制器停止推进滚动:新 RS 不再扩容、旧 RS 不再缩容,新旧副本比例被冻结,直到 resume。它的语义是"暂停对这次发布的协调",不是"把 10% 流量切到新版本"。流量由 Service / Ingress 在 Pod Ready 后转发,pause 控制不了流量比例;按比例放量是 Flagger、Argo Rollouts 这类 canary 工具通过多版本 Service 加权实现的。rollout pause 只适合"先更新一半、人工观察、再手动继续"的粗粒度场景。
#操作层:完整可复现的发布、回滚与 CI/CD 集成
#前置条件
本文命令以以下环境为例;滚动更新机制自 apps/v1 稳定以来未变,新版集群命令完全一致:
- Kubernetes 1.24 及以上(apps/v1 Deployment 为 GA API),任意发行版(kind、k3s、minikube、托管集群均可);
- kubectl 与集群差一个次要版本以内;
- 演练命名空间 demo(没有则先创建)。
kubectl version --client
kubectl cluster-info
kubectl create namespace demo#准备一个示例应用
为可复现,演练用公开镜像 nginx(一定存在、可拉取),Deployment 名为 demo-web,3 副本,配好 readiness 探针与优雅退出:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-web
namespace: demo
labels:
app: demo-web
spec:
replicas: 3
revisionHistoryLimit: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: demo-web
template:
metadata:
labels:
app: demo-web
spec:
terminationGracePeriodSeconds: 30
containers:
- name: web
image: nginx:1.25-alpine
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 2
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi这里 maxUnavailable: 0、maxSurge: 1 即原稿的保守配置:先起再收,容量不降。拉起并确认:
kubectl apply -f demo-web.yaml
kubectl rollout status deployment/demo-web -n demo --timeout=120s
kubectl get deployment demo-web -n demo
kubectl get rs -n demo -o wide期望 rollout status 输出 successfully rolled out,get rs 里只有一个 RS(demo-web-xxxxxxxxxx),READY 3/3。
#触发滚动更新的几种方式
只要修改 spec.template,以下方式都会触发滚动;不改模板的(如 kubectl scale)不触发:
- 命令式更新镜像(CI 里最常见):
kubectl set image deployment/demo-web web=nginx:1.26-alpine -n demo声明式整文件更新(GitOps / IaC 标准做法):修改 demo-web.yaml 中 image 后执行 kubectl apply -f demo-web.yaml。
局部 patch(脚本定点改字段):
kubectl patch deployment demo-web -n demo \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"web","image":"nginx:1.26-alpine"}]}}}}'交互式编辑:kubectl edit deployment demo-web -n demo。
其他命令式子命令:kubectl set env、kubectl set resources 等改的是模板字段,同样触发。
注意:改的是模板注解也会触发滚动,不要在模板里放每次都变的注解(时间戳、随机值),否则每次 apply 都产生一次无意义的全量重建。
#发布的标准动作:打标记、更新、等 rollout
原稿的标准动作三连,对应真实 API 服务(镜像以 ghcr.io/acme/demo-api 为例,实际换成自己的):
# 1. 打 change-cause,历史里能看出每次改了什么
kubectl annotate deployment demo-api \
kubernetes.io/change-cause="release 1.2.0: fix order rounding"
# 2. 触发更新
kubectl set image deployment/demo-api api=ghcr.io/acme/demo-api:1.2.0
# 3. 等 rollout 完成(超时即失败)
kubectl rollout status deployment/demo-api --timeout=180skubectl rollout status 会一直等到新副本全部就绪或超时,超时返回非零退出码——CI 里等于发布失败,这是自动化发布里最重要的一个动作。不要用"sleep 30 然后查 Pod":sleep 猜不到发布实际要多久,探针失败时你只看到一批没就绪的 Pod,却没有统一失败信号。rollout status 是标准答案。
发布过程中看实时状态:
# 实时观察:新名字的 Pod 逐个起来、旧名字的 Pod 逐个消失
kubectl get pods -l app=demo-web -n demo -w
# 历史:revision 从 1 递增,CHANGE-CAUSE 显示每次的标记
kubectl rollout history deployment/demo-web -n demo对应原稿的 kubectl get pods -l app=demo-api -w 与 kubectl rollout history deployment/demo-api。前者是流量变化的直接代理(新 Pod Ready 后 Service 才导流),后者是审计入口。
#用 status 字段看发布节奏
升级到 nginx:1.26-alpine 后,两个终端分别跑:
kubectl get pods -l app=demo-web -n demo -w
kubectl get deployment demo-web -n demo \
-o jsonpath='updatedReplicas={.status.updatedReplicas}{"\n"}readyReplicas={.status.readyReplicas}{"\n"}availableReplicas={.status.availableReplicas}{"\n"}unavailableReplicas={.status.unavailableReplicas}{"\n"}'maxUnavailable: 0, maxSurge: 1 下会观察到:总 Pod 数先变 4,新 Pod Ready 后旧 Pod 才删一个、回落到 3,如此三轮;updatedReplicas 从 0 涨到 3,availableReplicas 始终是 3——"容量不降"的数字体现。
#pause / resume:粗粒度人工观察窗口
kubectl rollout pause deployment/demo-web -n demo
# 观察新版本 Pod 的错误率、延迟等指标,注意流量比例不受 pause 控制
kubectl rollout resume deployment/demo-web -n demopause 后新 RS 副本数被冻结,resume 后滚动继续。不要在 paused 状态下顺手 set image——模板又变,会积压一次语义不清的延迟发布。按比例灰度请用 Flagger / Argo Rollouts(多版本 Service 加权),与 rollout pause 是两个层级的机制。
#失败注入演练:从卡死到回滚
按原稿真实事故的模式做两次失败演练。重点不是看报错,而是体会三个机制信号:新 Pod 不就绪 → 滚动卡住;progressDeadlineSeconds 超时 → rollout status 给出失败信号;undo 恢复模板 → 新滚动把老模板换回来。
#场景一:镜像 tag 不存在(ImagePullBackOff)
先配一个短的进度超时方便触发(生产不要这么短):
kubectl patch deployment demo-web -n demo \
-p '{"spec":{"progressDeadlineSeconds":60}}'注入一个肯定不存在的 tag:
kubectl annotate deployment demo-web -n demo \
kubernetes.io/change-cause="bad release: image tag typo"
kubectl set image deployment/demo-web web=nginx:no-such-tag-9.9 -n demo
kubectl rollout status deployment/demo-web -n demo --timeout=90s预期:新 RS 被创建,新 Pod 拉镜像失败,状态 ImagePullBackOff,永远不 Ready;maxUnavailable: 0 保证 3 个旧 Pod 原封不动,线上无损失。rollout status 在 60 秒左右报错:
error: deployment "demo-web" exceeded its progress deadline三个信号齐了:新 RS 卡住(新旧 RS 并存)、Progressing 条件为 False + Reason=ProgressDeadlineExceeded、rollout status 非零退出。回滚:
kubectl rollout undo deployment/demo-web -n demo
kubectl rollout status deployment/demo-web -n demo --timeout=120s
kubectl rollout history deployment/demo-web -n demo回滚后 rollout status 输出 successfully rolled out,get rs 看到新 RS 已缩到 0。注意 history 里出现更高的 revision(如 revision 4),CHANGE-CAUSE 是
#场景二:readiness 探针路径不存在(原稿事故复现)
原稿事故的机制是:readiness 探针配的是新接口路径,而新版本把老路径移除了,新 Pod 永远不就绪,滚动卡死。用 nginx 复现:把探针路径临时改成 /healthz(nginx 上不存在,404):
kubectl annotate deployment demo-web -n demo \
kubernetes.io/change-cause="broken probe: /healthz not served"
kubectl patch deployment demo-web -n demo \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"web","readinessProbe":{"httpGet":{"path":"/healthz","port":80},"initialDelaySeconds":3,"periodSeconds":2}}]}}}}'
kubectl rollout status deployment/demo-web -n demo --timeout=90s现象与场景一类似,但 Pod 不是 CrashLoopBackOff,而是 Running 但一直 NotReady(探针连续失败)。探针失败不杀容器,只是不让它进 Service 端点。看细节:
kubectl describe deployment demo-web -n demo
kubectl get events -n demo --sort-by=.lastTimestamp | tail -20describe 能看到滚动被阻塞(新副本一直 NOT Ready),events 里能看到探针失败 Warning。查历史、回滚、看现场:
kubectl rollout history deployment/demo-web -n demo
# 回滚到上一个 revision(等价于不带参数的 undo)
kubectl rollout undo deployment/demo-web -n demo --to-revision=<上一个编号>
kubectl rollout status deployment/demo-web -n demo --timeout=120s原稿对应命令是 kubectl rollout undo deployment/demo-api 与 kubectl rollout undo --to-revision=3,两者都真实可用:不带参数回退到上一个 revision,带 --to-revision 回退到指定编号。
查故障现场——注意 --previous 的适用条件:它读"同容器上一次已终止实例"的日志,只有容器曾经启动过且后来退出/重启才有内容;镜像拉取失败从未启动时,--previous 没有输出:
kubectl get pods -l app=demo-web -n demo
kubectl logs <新Pod> --previous -n demo原稿事故的根因是"配置里引用了不存在的配置项",容器启动报错反复重启,--previous 能看到第一次启动的报错栈。回滚只把线上恢复到旧版本,根因排查还得靠 logs、describe、events 和配置对比。
#回滚后必做:补审计、留证据
# 1. 补 change-cause,否则历史里 CHANGE-CAUSE 是 <none>
kubectl annotate deployment/demo-web -n demo \
kubernetes.io/change-cause="rollback to revision 3: bad release broke readiness"
# 2. 记录当时的 revision 与现象(发到故障单/群公告,别只存在本地终端)
kubectl rollout history deployment/demo-web -n demo回滚救急,但根因必须继续查。只 undo 不修根因,下次发布同一个坑还会炸。回滚后要做的是原稿说的三件:保留当时 Pod 日志(--previous,趁容器还有日志时抓)、记录 revision、开问题单。
#在 CI/CD 中集成 rollout 检查
把命令串进流水线,就是最小闭环。核心语义:rollout status 的超时非零退出码就是发布失败信号,流水线应把它当硬失败,而不是"报警后继续"。
set -e
kubectl -n demo annotate deployment/demo-web \
kubernetes.io/change-cause="release ${APP_VERSION}"
kubectl -n demo set image deployment/demo-web web=${REGISTRY}/demo-web:${APP_VERSION}
kubectl -n demo rollout status deployment/demo-web --timeout=180s- set -e 让任何一条命令失败都终止流水线;
- rollout status 三分钟没完成 → 退出非零 → 流水线红;
- 失败后进入"人工审批 → 自动/手动 undo"的恢复路径:自动 undo 适合低风险服务,高价值服务建议落到人工,避免自动回滚引发二次事故(回滚时数据库 schema 不兼容,见边界与坑)。
原稿还强调一重更稳妥的防线:生产环境用 GitHub Environments 审批门禁,别让"merge 即发布"直接进 prod。做法:流水线先打镜像、跑测试、推 staging,等人工在 GitHub Environments 审批通过后才执行 set image + rollout status。同时发布窗口留足 terminationGracePeriodSeconds(Pod 级,默认 30 秒)和 preStop 钩子,让旧 Pod 优雅退出、连接不中断:
containers:
- name: web
image: nginx:1.26-alpine
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]preStop 在 Pod 收到终止信号后、SIGTERM 发给主进程前执行,sleep 5 让 Service 端点先从 EndpointSlice 摘除,再真正停服;加上 terminationGracePeriodSeconds 兜底,滚动过程中客户端连接不会被硬切。这是"maxSurge/maxUnavailable 保证副本数、探针与优雅退出保证连接质量"的完整组合。
#验证层:怎么确认发布真的成功
#rollout status 的输出语义
成功标志只有一种:输出 deployment "demo-web" successfully rolled out 且退出码 0。看到 exceeded its progress deadline、waiting for rollout to finish,或非零退出码,都说明发布未成功,不要以"Pod 好像多了几个"为准。
#Deployment status 字段核对
kubectl get deployment demo-web -n demo \
-o jsonpath='{.status.observedGeneration}{"\t"}{.metadata.generation}{"\n"}{.status.updatedReplicas}{"\t"}{.status.readyReplicas}{"\t"}{.status.availableReplicas}{"\t"}{.status.unavailableReplicas}{"\n"}'
kubectl get deployment demo-web -n demo \
-o jsonpath='{.status.conditions[?(@.type=="Progressing")].reason}{"\n"}'通过标准:
- observedGeneration 等于 metadata.generation(控制器已看到最新变更);
- updatedReplicas 等于 readyReplicas 等于 availableReplicas 等于期望副本数;
- unavailableReplicas 为 0;
- Progressing 条件 Reason 为 NewReplicaSetAvailable(最新 RS 已完全可用)。
#ReplicaSet 与 Pod 层核对
kubectl get rs -n demo -o wide
kubectl get pods -n demo -o wide --show-labels通过标准:旧 RS 的 DESIRED 为 0、新 RS 的 DESIRED 等于期望副本数;所有运行中 Pod 都带新 RS 的 pod-template-hash 后缀,READY 1/1。若新旧 RS 并存且旧 RS 还有副本,说明滚动未结束或被卡住。
#探针与优雅退出验证
- readiness:发布期间用
kubectl get pods -w观察,新 Pod 从 Running 到 Ready 期间,应用日志里能看到探针请求,对照 404/200 判断就绪逻辑是否正确。 - 优雅退出:
kubectl delete pod后 Pod 先变 Terminating,preStop 的 sleep 期间仍有日志输出,随后消失;对比不配 preStop 时 Terminating 立即结束的行为。
#边界与坑
#maxSurge / maxUnavailable 组合的取舍
- maxUnavailable: 0, maxSurge: 1:容量恒定,适合对可用性敏感的核心链路;代价是资源占用翻倍(大副本或大内存服务的发布峰值成本要算进去)。
- maxUnavailable: 1, maxSurge: 1:速度快一倍,能容忍一个副本短暂不可用,适合多副本、可水平扩展的服务。
- 百分比注意取整:maxUnavailable 向下、maxSurge 向上。3 副本 25% 默认值下,maxUnavailable 算出来是 0、maxSurge 是 1,效果等价保守配置;副本数小时务必按实际值验证,别凭直觉。
- 两者不能同时为 0,否则 Deployment 校验直接拒绝。
#单副本陷阱
replicas: 1 时默认 maxUnavailable 为 0(向下取整),表现为"先起新、再停旧";但若显式配 maxUnavailable: 1 且 maxSurge: 0,控制器会先删掉唯一的旧副本再等新副本就绪——新副本起不来就是全量不可用。单副本要么保持 maxUnavailable: 0、maxSurge: 1,要么接受换用停机发布,别在 maxUnavailable 上冒险。
#没有 readinessProbe 的后果
readiness 缺失时容器一 Running 就被视为 Ready,立即进 Service 端点,滚动以"每批秒就绪"的速度推进,坏版本几分钟扩散到全部副本——等 500 报警时滚动已完成。readiness 探针不是可选项,是滚动发布的刹车:探针要覆盖真实业务就绪条件(连不上 DB、配置加载失败都不算 Ready),配合 maxUnavailable: 0 形成"新版本不行就停在原地"的安全态。
#pause 不是灰度
kubectl rollout pause 暂停的是控制器协调,在"先更新一半"这个粒度上有用;它控制不了 Service 流量比例。想放 10% 流量验证、按成功率自动放量,请用 Flagger / Argo Rollouts(金丝雀)或蓝绿发布;滚动更新只回答"如何分批替换"。
#回滚救急,但根因必须继续查
原稿事故的经验:只 undo 不修根因,下次发布同一个坑还会炸。回滚后三件事——保留日志(--previous,仅当容器曾启动过才有效)、记录 revision、开问题单——是避免"回滚成功但下次继续踩雷"的纪律。排查顺序:kubectl describe → events → 应用/上游依赖日志 → 配置对比(事故根因就是配置项不存在)。
#回滚不动数据,也不回退数据变更
rollout undo 只恢复 Pod 模板(代码、镜像、环境变量、探针),不涉及数据库 schema、缓存键结构、消息队列拓扑。若新版本启动时执行了不可逆迁移(删列、改索引),回滚只让旧代码跑起来,数据已变,旧代码可能照样挂。这类发布必须把"向前兼容的迁移"作为发布前置条件(先迁移、后发代码、回滚时保留兼容层),而不是指望 undo 兜底。
#revisionHistoryLimit:历史不是无限保留
默认只保留最近 10 个 RS(revisionHistoryLimit: 10)。回滚到更早的 revision 时,若对应 RS 已被清理,kubectl rollout undo --to-revision=N 会报找不到该 revision。审计归档要及时从集群外拿(Git 提交 + 变更单),别把集群里的 revision 历史当唯一档案。
#终止中的 Pod 与容量峰值
滚动期间 Terminating 的 Pod 不计入 availableReplicas,但节点资源直到其真正退出(terminationGracePeriodSeconds 到期)才释放。maxSurge: 1 不意味着"最多只多占一份资源":旧 Pod 未清、新 Pod 已起时,瞬时资源占用可能超过期望值的两倍(官方文档明确提醒过)。容量紧张时先看节点余量再发版。
#收尾:发布能力由回滚能力决定
自动部署不等于自动成功。每次发布前确认三件事:change-cause 打了、rollout status 有超时、undo 命令在非生产环境演练过。等真出事的时候,你不想第一次用 kubectl rollout undo。机制的意义也在此:maxSurge/maxUnavailable 决定替换节奏,readiness 探针决定滚动能否推进,progressDeadlineSeconds 给自动化失败信号,undo 把模板恢复成历史版本——因果链通了,发布时间、卡死判断、回滚动作就不再依赖临场反应。下一步建议:在非生产环境把本文的失败注入演练完整跑一遍,然后把"打标记 → set image → rollout status → 失败入审批 → undo 恢复"固化进流水线,你才算真正拥有了安全的发布能力。
相关阅读
Related留言讨论
0 条还没有留言,来说点什么吧。