你写了几年 Deployment,可能从来没搞懂它到底在管谁。
🧠 你以为的链路 vs 真实链路
❌ Deployment ---> Pod
✅ Deployment ---> ReplicaSet ---> Pod
中间藏着的 ReplicaSet,才是滚动更新和回滚的全部秘密。
🎯 眼见为实
创建一个最简单的 Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.24
大多数人只跑 kubectl get pods,看到三个 Pod 就觉得完事了。但你试试这个:
kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deploy-5d4c6bf8b4 3 3 3 30s
👉 nginx-deploy-5d4c6bf8b4 就是 Deployment 自动创建的 ReplicaSet,后缀是 Pod template 的 hash 值。
再看 Pod 名字:nginx-deploy-5d4c6bf8b4-xxxxx。ReplicaSet 名字 + 随机后缀,层级关系一目了然。
🧩 为什么要多这一层?
一句话:ReplicaSet 是滚动更新和回滚的基石。
如果 Deployment 直接管 Pod,更新镜像只能"就地替换"。但 K8s 的做法优雅得多 -- 同时操纵两个 ReplicaSet,此消彼长,服务不中断。
⚔️ 滚动更新:两个 RS 的精密配合
把镜像从 nginx:1.24 改成 nginx:1.25,Deployment 做了四件事:
1️⃣ 创建一个全新的 ReplicaSet(新 Pod template hash)
2️⃣ 逐步扩大新 RS 的副本数
3️⃣ 同时逐步缩小旧 RS 的副本数
4️⃣ 新 RS 到位,旧 RS 归零
直接看源码 pkg/controller/deployment/rolling.go:
func (dc *DeploymentController) rolloutRolling(ctx context.Context,
d *apps.Deployment, rsList []*apps.ReplicaSet) error {
newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(ctx, d, rsList, true)
// ...
// Scale up, if we can.
scaledUp, err := dc.reconcileNewReplicaSet(ctx, allRSs, newRS, d)
// ...
// Scale down, if we can.
scaledDown, err := dc.reconcileOldReplicaSets(ctx, allRSs,
controller.FilterActiveReplicaSets(oldRSs), newRS, d)
// ...
}
👉 没有魔法,就是拿到新旧 RS,扩新的、缩旧的。
更新过程中 kubectl get rs,你会看到这个"中间状态":
NAME DESIRED CURRENT READY AGE
nginx-deploy-5d4c6bf8b4 1 1 1 10m # 旧 RS,正在缩
nginx-deploy-7c8b9d6f12 3 3 2 15s # 新 RS,正在扩
用了几年 K8s 没见过这个?正常,大多数人都没见过。
🎯 maxSurge 和 maxUnavailable:两个旋钮
滚动更新的速度由两个参数控制:
-maxSurge = 最多可以比期望副本数多出多少个 Pod
-maxUnavailable = 最多可以有多少个 Pod 不可用
源码 pkg/apis/apps/v1/defaults.go 中的默认值:
if strategy.RollingUpdate.MaxUnavailable == nil {
maxUnavailable := intstr.FromString("25%")
strategy.RollingUpdate.MaxUnavailable = &maxUnavailable
}
if strategy.RollingUpdate.MaxSurge == nil {
maxSurge := intstr.FromString("25%")
strategy.RollingUpdate.MaxSurge = &maxSurge
}
👉 默认都是 25%。4 个副本的话,更新过程中最多 5 个 Pod 同时存在,最少 3 个可用。
⚠️ 激进设置(maxSurge=100%, maxUnavailable=0)= 快但费资源。保守设置(maxSurge=0, maxUnavailable=1)= 慢但稳。
💥 回滚为什么能"秒回"?
这是 ReplicaSet 设计最巧妙的地方。
更新完成后,旧 RS 没有被删除,只是副本数缩到了 0:
NAME DESIRED CURRENT READY AGE
nginx-deploy-5d4c6bf8b4 0 0 0 1h # v1
nginx-deploy-7c8b9d6f12 0 0 0 50m # v2
nginx-deploy-9a3e7f1d56 3 3 3 10m # v3,当前版本
那些 0/0/0 的 RS 不是垃圾,是回滚快照。每个旧 RS 都保存着那个版本的完整 Pod template。
执行 kubectl rollout undo deployment/nginx-deploy,K8s 只是把上一个 RS 从 0 扩回去,当前 RS 缩到 0。本质上就是一次反向滚动更新,所以几乎秒级完成。
👉 旧 RS 保留多少个?revisionHistoryLimit 控制,默认 10:
if obj.Spec.RevisionHistoryLimit == nil {
obj.Spec.RevisionHistoryLimit = new(int32)
*obj.Spec.RevisionHistoryLimit = 10
}
设成 0?所有旧 RS 立即清理,回滚能力直接没了。
🧩 ownerReferences:级联删除的秘密
三者之间通过 ownerReferences 形成严格的所有权链:
Deployment (owner) ---> ReplicaSet (owner) ---> Pod
kubectl delete deployment nginx-deploy 时,垃圾回收器沿着这条链级联删除:Deployment -> 所有 RS -> 所有 Pod。你不需要手动清理任何东西。
🧠 控制器的分工哲学
Deployment Controller 源码注释(pkg/controller/deployment/deployment_controller.go):
DeploymentController is responsible for synchronizing Deployment objects stored in the system with actual running replica sets and pods.
👉 关键词:replica sets and pods。Deployment Controller 只管 ReplicaSet,Pod 的生命周期由 ReplicaSet Controller 负责。
这就是 K8s 控制器模式的精髓:每个控制器只管自己那一层,层层解耦,各司其职。
💡 最后一击
Deployment 不管 Pod,ReplicaSet 才管。记住这张图:
Image ✅ 滚动更新 = 同时操纵新旧两个 RS
✅ 回滚 = 保留旧 RS 快照,反向 scale
✅ 级联删除 = ownerReferences 链路
✅ 更新节奏 = maxSurge / maxUnavailable 控制两个 RS 的 scale 速度
下次排查部署问题,别只盯着 Pod。跑一下 kubectl get rs,答案可能就藏在那些你从未注意过的 ReplicaSet 里。