Deployment 从不直接管 Pod,那你的 Pod 是谁创建的?

0 阅读4分钟

你写了几年 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 里。