


























读完本篇,你应该能回答:
DeploymentController 的结构体有哪些字段,各自承担什么职责?
NewDeploymentController 如何注册三个 Informer 的事件处理器?
Deployment → ReplicaSet → Pod 的三级级联是如何实现的?
RollingUpdate 策略的完整流程是什么?回滚机制的核心原理是什么?
ProgressDeadlineExceeded 是如何判断超时的?
为什么需要 pod-template-hash?
maxSurge 和 maxUnavailable 的比例缩放逻辑是什么?
Kubernetes Go Operator Deployment Controller RollingUpdate k8s v1.36.1
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- 三级级联架构:Deployment → ReplicaSet → Pod,每层通过 OwnerReference 建立所有权关系
- syncDeployment 主循环:Deployment → RS → Pod 的三层列表 + Pause/Rollback/Scale/Strategy 分支判断
- RollingUpdate 策略:reconcileNewReplicaSet + reconcileOldReplicaSets 的完整扩缩流程
- 回滚机制:从旧 ReplicaSet 的 PodTemplateSpec 恢复到 Deployment.Spec.Template
次重点(了解即可)
- Pod-template-hash 的计算方式和碰撞检测
- ReplicaSetControllerRefManager 的领养/认领逻辑
- ProgressDeadlineExceeded 的超时计算公式
文章目录
思考记忆提示 — 本节是全篇的"入口"——理解 Deployment 解决什么问题,才能理解 Controller 的设计决策
Kubernetes 的 Deployment 是运行在 ReplicaSet 之上的一个"声明式管理层"。ReplicaSet 能确保指定数量的 Pod 副本始终运行,但它是"无状态"的——如果你想更新这些 Pod 的镜像版本,ReplicaSet 本身并不提供任何机制。
Deployment 在 ReplicaSet 之上增加了一个关键能力:声明式地描述 Pod 模板的变更,并自动将变更以滚动更新的方式应用到集群中。当你修改 Deployment 的 .spec.template 时,Deployment Controller 会自动创建一个新的 ReplicaSet(newRS),然后逐步将流量从旧 ReplicaSet(oldRS)迁移到 newRS。
我的理解的意思是说
Deployment 的架构可以用出版社-期刊社-印刷厂来类比:
这样每一层都有自己的职责,层层级联,形成了 Kubernetes 应用管理的完整闭环。
思考记忆提示 — 本节是全篇的"地图"——DeploymentController 同时监听三个资源,理解它们的关系是关键
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ DeploymentController 三级级联架构 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ Deployment (声明式管理层) │ │
│ │ pkg/controller/deployment/deployment_controller.go │ │
│ │ ├─ spec.selector: 标签选择器(管理所有 ReplicaSet) │ │
│ │ ├─ spec.strategy: RollingUpdate / Recreate │ │
│ │ ├─ spec.template: Pod 模板(版本标识,变化时触发滚动更新) │ │
│ │ └─ status: Replicas/UpdatedReplicas/AvailableReplicas │ │
│ └─────────────────────────────┬───────────────────────────────────────────────┘ │
│ │ OwnerReference (ControllerRef) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ ReplicaSet (副本管理层) │ │
│ │ ├─ selector: 必须包含 pod-template-hash(Deployment 版本标识) │ │
│ │ ├─ replicas: 副本数 │ │
│ │ └─ template: Pod 模板副本 │ │
│ │ │ │
│ │ 滚动更新期间存在多个 ReplicaSet: │ │
│ │ ├─ newRS: 当前版本(最新 PodTemplate) │ │
│ │ └─ oldRSs[]: 历史版本(保留用于回滚) │ │
│ └─────────────────────────────┬───────────────────────────────────────────────┘ │
│ │ OwnerReference (ControllerRef) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ Pod (实际运行单元) │ │
│ │ ├─ labels: 必须包含 pod-template-hash(RS 版本标识) │ │
│ │ └─ OwnerReference: 指向 ReplicaSet │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ DeploymentController 同时监听三个 Informer │ │
│ │ ├─ DeploymentInformer → addDeployment / updateDeployment / deleteDeployment │ │
│ │ ├─ ReplicaSetInformer → addReplicaSet / updateReplicaSet / deleteReplicaSet│ │
│ │ └─ PodInformer → deletePod (Recreate 策略中 Pod 全删后触发 newRS 启动) │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────┘
设计精髓
DeploymentController 采用了三级级联的设计,每一级都通过 OwnerReference 建立明确的所有权关系。这种设计的核心优势是:
思考记忆提示 — Controller 结构体是全篇的核心骨架,每个字段都有明确的职责
// pkg/controller/deployment/deployment_controller.go(行 67~101)
// DeploymentController 负责同步 Deployment 对象到实际的 ReplicaSet 和 Pod
type DeploymentController struct {
// rsControl 用于认领(Adopt)和放弃(Release)ReplicaSet
// DeploymentController 通过 ControllerRefManager 管理哪些 RS 属于自己
rsControl controller.RSControlInterface
client clientset.Interface // Kubernetes 客户端,创建/更新 RS 和 Deployment
eventBroadcaster record.EventBroadcaster
eventRecorder record.EventRecorder
// 注入接口(用于测试)
syncHandler func(ctx context.Context, dKey string) error
enqueueDeployment func(deployment *apps.Deployment)
// 三个 Lister:分别监听 Deployment、ReplicaSet、Pod 的本地缓存
dLister appslisters.DeploymentLister
rsLister appslisters.ReplicaSetLister
podLister corelisters.PodLister
// podIndexer 允许通过 ControllerRef UID 查找 Pod
// 比 podLister 更快(O(1) 索引查找,不用遍历)
podIndexer cache.Indexer
// 三个 Synced 函数:判断各资源缓存是否已同步
dListerSynced cache.InformerSynced
rsListerSynced cache.InformerSynced
podListerSynced cache.InformerSynced
// 限速队列(namespace/name 字符串 key)
queue workqueue.TypedRateLimitingInterface[string]
}
小贴士
podIndexer 和 podLister 的关系是:Lister 是对外的高层接口,Indexer 是底层的索引存储。Lister 底层使用 Indexer。podIndexer 特别重要是因为 controller.FilterPodsByOwner 需要通过 ControllerRef UID 索引快速查找属于某个 ReplicaSet 的所有 Pod,这在 getPodMapForDeployment 中被大量使用。
思考记忆提示 — NewDeploymentController 是整个控制器的"组装工厂"——三个 Informer 的事件处理器是核心
// pkg/controller/deployment/deployment_controller.go(行 104~168)
func NewDeploymentController(
ctx context.Context,
dInformer appsinformers.DeploymentInformer,
rsInformer appsinformers.ReplicaSetInformer,
podInformer coreinformers.PodInformer,
client clientset.Interface,
) (*DeploymentController, error) {
// 1. 创建事件广播器和记录器
eventBroadcaster := record.NewBroadcaster(record.WithContext(ctx))
dc := &DeploymentController{
client: client,
eventBroadcaster: eventBroadcaster,
eventRecorder: eventBroadcaster.NewRecorder(scheme.Scheme,
v1.EventSource{Component: "deployment-controller"}),
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{Name: "deployment"},
),
}
// 2. RSControl:用于 Adoption/Orphaning
dc.rsControl = controller.RealRSControl{
KubeClient: client,
Recorder: dc.eventRecorder,
}
// ========== 3. Deployment Informer 事件处理器 ==========
dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj) { dc.addDeployment(logger, obj) },
UpdateFunc: func(old, new) { dc.updateDeployment(logger, old, new) },
DeleteFunc: func(obj) { dc.deleteDeployment(logger, obj) },
})
// ========== 4. ReplicaSet Informer 事件处理器 ==========
rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj) { dc.addReplicaSet(logger, obj) },
UpdateFunc: func(old, new) { dc.updateReplicaSet(logger, old, new) },
DeleteFunc: func(obj) { dc.deleteReplicaSet(logger, obj) },
})
// ========== 5. Pod Informer 事件处理器(仅 Delete)==========
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
DeleteFunc: func(obj) { dc.deletePod(logger, obj) },
})
// 6. 设置 Lister 和 Synced 函数
dc.dLister = dInformer.Lister()
dc.rsLister = rsInformer.Lister()
dc.podLister = podInformer.Lister()
dc.dListerSynced = dInformer.Informer().HasSynced
dc.rsListerSynced = rsInformer.Informer().HasSynced
dc.podListerSynced = podInformer.Informer().HasSynced
// 7. 为 Pod 添加 ControllerRef 索引(用于快速查找某个 RS 管理的所有 Pod)
if err := controller.AddPodControllerIndexer(podInformer.Informer()); err != nil {
return nil, err
}
dc.podIndexer = podInformer.Informer().GetIndexer()
return dc, nil
}
注意
Pod Informer 只注册了 DeleteFunc,没有 AddFunc 和 UpdateFunc。这是因为 Deployment Controller 不需要监听 Pod 的创建和更新事件——Pod 的副本数管理由 ReplicaSetController 负责。只有一个例外:Recreate 策略下,当所有 Pod 被删除后,需要触发 Deployment 创建新的 ReplicaSet。这就是 deletePod 的核心逻辑。
思考记忆提示 — 这是全篇最复杂的部分——理解三条事件链路的区别和联系
// pkg/controller/deployment/deployment_controller.go(行 201~230)
// AddFunc:Deployment 创建时触发
func (dc *DeploymentController) addDeployment(logger klog.Logger, obj interface{}) {
d := obj.(*apps.Deployment)
logger.V(4).Info("Adding deployment", "deployment", klog.KObj(d))
dc.enqueueDeployment(d) // 直接入队
}
// UpdateFunc:Deployment 更新时触发
func (dc *DeploymentController) updateDeployment(logger klog.Logger, old, cur interface{}) {
oldD := old.(*apps.Deployment)
curD := cur.(*apps.Deployment)
logger.V(4).Info("Updating deployment", "deployment", klog.KObj(oldD))
dc.enqueueDeployment(curD) // 使用新版本入队
}
// DeleteFunc:Deployment 删除时触发(附带 Tombstone 处理)
func (dc *DeploymentController) deleteDeployment(logger klog.Logger, obj interface{}) {
// 处理 DeletedFinalStateUnknown(Tombstone)
if !ok {
tombstone := obj.(cache.DeletedFinalStateUnknown)
d := tombstone.Obj.(*apps.Deployment)
}
logger.V(4).Info("Deleting deployment", "deployment", klog.KObj(d))
dc.enqueueDeployment(d) // 删除事件也入队,syncDeployment 会处理"已删除"的情况
}
// pkg/controller/deployment/deployment_controller.go(行 233~367)
// AddFunc:RS 创建时,通过 ControllerRef 追溯到 Deployment 并入队
func (dc *DeploymentController) addReplicaSet(logger klog.Logger, obj interface{}) {
rs := obj.(*apps.ReplicaSet)
if rs.DeletionTimestamp != nil {
dc.deleteReplicaSet(logger, rs) // 已在删除中,转到 deleteReplicaSet
return
}
// 通过 OwnerReference 判断该 RS 属于哪个 Deployment
if controllerRef := metav1.GetControllerOf(rs); controllerRef != nil {
d := dc.resolveControllerRef(rs.Namespace, controllerRef)
if d == nil { return }
dc.enqueueDeployment(d)
return
}
// Orphan RS(没有 OwnerReference):可能被某个 Deployment 收养
ds := dc.getDeploymentsForReplicaSet(logger, rs)
for _, d := range ds {
dc.enqueueDeployment(d)
}
}
// resolveControllerRef:根据 OwnerReference 找到对应的 Deployment
func (dc *DeploymentController) resolveControllerRef(
namespace string, controllerRef *metav1.OwnerReference) *apps.Deployment {
// 1. 验证 Kind 是 Deployment
if controllerRef.Kind != controllerKind.Kind { return nil }
// 2. 按 name 查找 Deployment(不能用 UID 直接查找)
d, err := dc.dLister.Deployments(namespace).Get(controllerRef.Name)
if err != nil { return nil }
// 3. 验证 UID 匹配(防止名称冲突)
if d.UID != controllerRef.UID { return nil }
return d
}
// UpdateFunc:RS 更新时,处理 ControllerRef 变化的情况
func (dc *DeploymentController) updateReplicaSet(logger klog.Logger, old, cur interface{}) {
// 跳过 Periodic Resync(ResourceVersion 未变)
if curRS.ResourceVersion == oldRS.ResourceVersion { return }
curControllerRef := metav1.GetControllerOf(curRS)
oldControllerRef := metav1.GetControllerOf(oldRS)
// ControllerRef 发生变化:旧 Owner 和新 Owner 都要入队
if controllerRefChanged {
if oldControllerRef != nil {
if d := dc.resolveControllerRef(oldRS.Namespace, oldControllerRef); d != nil {
dc.enqueueDeployment(d) // 触发旧 Deployment 调谐(可能需要放弃该 RS)
}
}
}
if curControllerRef != nil {
if d := dc.resolveControllerRef(curRS.Namespace, curControllerRef); d != nil {
dc.enqueueDeployment(d) // 触发新 Deployment 调谐
return
}
}
// Orphan 更新:标签变化可能导致被某个 Deployment 收养
if labelChanged {
ds := dc.getDeploymentsForReplicaSet(logger, curRS)
for _, d := range ds { dc.enqueueDeployment(d) }
}
}
// pkg/controller/deployment/deployment_controller.go(行 370~412)
// deletePod:Pod 被删除时,如果是 Recreate 策略且所有 Pod 都删完了,触发 Deployment 调谐
func (dc *DeploymentController) deletePod(logger klog.Logger, obj interface{}) {
pod := obj.(*v1.Pod)
// 找到拥有该 Pod 的 ReplicaSet
controllerRef := metav1.GetControllerOf(pod)
if controllerRef == nil { return } // 无主 Pod,忽略
if controllerRef.Kind != "ReplicaSet" { return }
rs, err := dc.rsLister.ReplicaSets(pod.Namespace).Get(controllerRef.Name)
if err != nil || rs.UID != controllerRef.UID { return }
// 找到拥有该 RS 的 Deployment
d := dc.getDeploymentForPod(logger, pod)
if d == nil { return }
// 仅 Recreate 策略需要监听 Pod 删除事件
if d.Spec.Strategy.Type == apps.RecreateDeploymentStrategyType {
rsList, _ := util.ListReplicaSets(d, util.RsListFromClient(dc.client.AppsV1()))
podMap, _ := dc.getPodMapForDeployment(d, rsList)
numPods := 0
for _, podList := range podMap { numPods += len(podList) }
// 所有 Pod 都删完了:触发 Deployment 调谐,创建新的 ReplicaSet
if numPods == 0 {
dc.enqueueDeployment(d)
}
}
}
我的理解的意思是说
三条事件链路的触发条件各不相同,理解它们的区别是关键:
思考记忆提示 — Run 是控制器的启动入口——与 sample-controller 相同的模式,但多了 PodIndexer
// pkg/controller/deployment/deployment_controller.go(行 171~199)
func (dc *DeploymentController) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash()
defer dc.queue.ShutDown()
logger := klog.FromContext(ctx)
logger.Info("Starting controller", "controller", "deployment")
// ========== 1. 等待三个缓存全部同步 ==========
// 必须等全部缓存同步完成,才能开始处理任务
// 否则可能查到空缓存,导致错误的调谐决策
if !cache.WaitForNamedCacheSyncWithContext(ctx,
dc.dListerSynced, dc.rsListerSynced, dc.podListerSynced) {
return
}
// ========== 2. 启动 Workers ==========
for i := 0; i < workers; i++ {
wg.Go(func() {
wait.UntilWithContext(ctx, dc.worker, time.Second)
})
}
<-ctx.Done()
}
// ========== Worker 主循环(与 sample-controller 完全相同)==========
func (dc *DeploymentController) worker(ctx context.Context) {
for dc.processNextWorkItem(ctx) {}
}
func (dc *DeploymentController) processNextWorkItem(ctx context.Context) bool {
key, quit := dc.queue.Get()
defer dc.queue.Done(key)
err := dc.syncHandler(ctx, key) // syncHandler = dc.syncDeployment
dc.handleErr(ctx, err, key)
return true
}
func (dc *DeploymentController) handleErr(ctx context.Context, err error, key string) {
if err == nil || errors.HasStatusCause(err, v1.NamespaceTerminatingCause) {
dc.queue.Forget(key) // 成功或 namespace 正在删除:遗忘
return
}
// 最多重试 15 次(maxRetries = 15)
if dc.queue.NumRequeues(key) < maxRetries {
dc.queue.AddRateLimited(key) // 带限速重试
return
}
// 超过 15 次:放弃,记录日志
utilruntime.HandleError(err)
dc.queue.Forget(key)
}
小贴士
maxRetries = 15 意味着 Deployment Controller 最多重试 15 次。根据默认的指数退避限速器(base=5ms,max=1000s),第 15 次重试的等待时间约为 41 秒,总重试时间跨度约为 82 秒。这足以处理大多数临时性错误(网络抖动、API Server 临时不可用),但又不会无限重试。
思考记忆提示 — syncDeployment 是全篇最重要的方法——控制器的所有业务逻辑都从这里分发到各个分支
// pkg/controller/deployment/deployment_controller.go(行 589~679)
func (dc *DeploymentController) syncDeployment(ctx context.Context, key string) error {
logger := klog.FromContext(ctx)
// ========== 步骤1:从缓存获取 Deployment ==========
deployment, err := dc.dLister.Deployments(namespace).Get(name)
if errors.IsNotFound(err) { return nil } // Deployment 已被删除
if err != nil { return err }
d := deployment.DeepCopy() // 永远不修改缓存对象
// ========== 步骤2:获取 Deployment 管理的所有 ReplicaSet ==========
// getReplicaSetsForDeployment 通过 ControllerRefManager 认领/放弃 RS
rsList, err := dc.getReplicaSetsForDeployment(ctx, d)
if err != nil { return err }
// ========== 步骤3:获取所有 ReplicaSet 管理的 Pod ==========
// 按 RS UID 分组:map[RS_UID][]*Pod
podMap, err := dc.getPodMapForDeployment(d, rsList)
if err != nil { return err }
// ========== 第一层判断:Deployment 正在被删除 ==========
if d.DeletionTimestamp != nil {
return dc.syncStatusOnly(ctx, d, rsList) // 仅更新状态,不做其他操作
}
// ========== 第二层判断:Deployment 是否暂停 ==========
if err = dc.checkPausedConditions(ctx, d); err != nil { return err }
if d.Spec.Paused {
return dc.sync(ctx, d, rsList) // 暂停时只做扩缩容和清理,不做滚动更新
}
// ========== 第三层判断:是否有回滚请求 ==========
if getRollbackTo(d) != nil {
return dc.rollback(ctx, d, rsList) // 优先处理回滚
}
// ========== 第四层判断:是否是扩缩容事件 ==========
scalingEvent, err := dc.isScalingEvent(ctx, d, rsList)
if err != nil { return err }
if scalingEvent {
return dc.sync(ctx, d, rsList) // 扩缩容只调整副本数,不触发滚动更新
}
// ========== 第五层判断:选择策略类型 ==========
switch d.Spec.Strategy.Type {
case apps.RecreateDeploymentStrategyType:
return dc.rolloutRecreate(ctx, d, rsList, podMap)
case apps.RollingUpdateDeploymentStrategyType:
return dc.rolloutRolling(ctx, d, rsList)
}
return fmt.Errorf("unexpected deployment strategy type: %s", d.Spec.Strategy.Type)
}
我的理解的意思是说
syncDeployment 的五层分支决策可以用一个决策树来描述:
这个顺序是精心设计的:删除 → 暂停 → 回滚 → 扩缩容 → 滚动更新,从最紧急到最常规依次处理。
思考记忆提示 — 滚动更新是 Deployment 最核心的特性——理解 rolloutRolling 就理解了一半的 Deployment Controller
// pkg/controller/deployment/rolling.go(行 31~66)
// rolloutRolling:滚动更新的主入口,顺序是固定的:先扩容 newRS,再缩容 oldRSs
func (dc *DeploymentController) rolloutRolling(
ctx context.Context,
d *apps.Deployment,
rsList []*apps.ReplicaSet,
) error {
// 1. 获取/创建 newRS,同步 revision 编号
newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(ctx, d, rsList, true)
if err != nil { return err }
allRSs := append(oldRSs, newRS)
// ========== 2. 第一步:扩容 newRS ==========
scaledUp, err := dc.reconcileNewReplicaSet(ctx, allRSs, newRS, d)
if err != nil { return err }
if scaledUp {
return dc.syncRolloutStatus(ctx, allRSs, newRS, d) // 扩容后立即更新状态
}
// ========== 3. 第二步:缩容 oldRSs ==========
scaledDown, err := dc.reconcileOldReplicaSets(ctx, allRSs, oldRSs, newRS, d)
if err != nil { return err }
if scaledDown {
return dc.syncRolloutStatus(ctx, allRSs, newRS, d) // 缩容后立即更新状态
}
// ========== 4. 第三步:滚动完成,清理旧 RS ==========
if deploymentutil.DeploymentComplete(d, &d.Status) {
if err := dc.cleanupDeployment(ctx, oldRSs, d); err != nil {
return err
}
}
// 5. 同步 Deployment 状态
return dc.syncRolloutStatus(ctx, allRSs, newRS, d)
}
// reconcileNewReplicaSet:扩容 newRS 到期望值
func (dc *DeploymentController) reconcileNewReplicaSet(
ctx context.Context,
allRSs []*apps.ReplicaSet,
newRS *apps.ReplicaSet,
deployment *apps.Deployment,
) (bool, error) {
if *(newRS.Spec.Replicas) == *(deployment.Spec.Replicas) {
return false, nil // 已达到期望副本数,不需要扩容
}
// 计算 newRS 应该有的副本数(考虑 maxSurge)
newReplicasCount, err := deploymentutil.NewRSNewReplicas(deployment, allRSs, newRS)
if err != nil { return false, err }
// 执行扩容
scaled, _, err := dc.scaleReplicaSet(ctx, newRS, newReplicasCount, deployment, false)
return scaled, err
}
设计精髓
滚动更新必须先扩容 newRS,再缩容 oldRSs,这个顺序不能颠倒。原因:
思考记忆提示 — 比例缩放是滚动更新中最复杂的部分——maxUnavailable 和 maxSurge 如何协同工作
// pkg/controller/deployment/rolling.go(行 86~235)
// reconcileOldReplicaSets:缩容 oldRSs,处理三种场景
func (dc *DeploymentController) reconcileOldReplicaSets(
ctx context.Context,
allRSs []*apps.ReplicaSet,
oldRSs []*apps.ReplicaSet,
newRS *apps.ReplicaSet,
deployment *apps.Deployment,
) (bool, error) {
// 计算 maxUnavailable:最多可以删除多少个 Pod 而不破坏可用性
maxUnavailable := deploymentutil.MaxUnavailable(*deployment)
// ========== 场景1:清理 unhealthy replicas(优先)==========
// 先删除 oldRS 中的不健康 Pod(failed/pending),避免它们阻塞滚动更新
oldRSs, cleanupCount, err := dc.cleanupUnhealthyReplicas(ctx, oldRSs, deployment, maxCleanupCount)
if err != nil { return false, err }
// ========== 场景2:按 maxUnavailable 比例缩容 ==========
scaledDownCount, err := dc.scaleDownOldReplicaSetsForRollingUpdate(ctx, allRSs, oldRSs, deployment)
if err != nil { return false, err }
return (cleanupCount + scaledDownCount) > 0, nil
}
// scale:处理扩缩容的公共逻辑(用于 sync 方法中的 ScalingEvent)
func (dc *DeploymentController) scale(ctx context.Context,
deployment *apps.Deployment, newRS *apps.ReplicaSet, oldRSs []*apps.ReplicaSet) error {
// 如果只有一个活跃 RS(无滚动更新),直接扩缩到期望副本数
if activeOrLatest := deploymentutil.FindActiveOrLatest(newRS, oldRSs); activeOrLatest != nil {
if *(activeOrLatest.Spec.Replicas) == *(deployment.Spec.Replicas) { return nil }
_, _, err := dc.scaleReplicaSet(ctx, activeOrLatest, *(deployment.Spec.Replicas), deployment, false)
return err
}
// 如果 newRS 已饱和(maxSurge=0 或已扩容到最大),缩容所有 oldRS
if deploymentutil.IsSaturated(deployment, newRS) {
for _, old := range controller.FilterActiveReplicaSets(oldRSs) {
if _, _, err := dc.scaleReplicaSet(ctx, old, 0, deployment, false); err != nil { return err }
}
return nil
}
// 比例缩放:maxUnavailable 控制每次缩容的数量
if deploymentutil.IsRollingUpdate(deployment) {
// 计算可扩缩的总量
allowedSize := *(deployment.Spec.Replicas) + deploymentutil.MaxSurge(*deployment)
deploymentReplicasToAdd := allowedSize - allRSsReplicas
// 按比例分配到各个 RS(从大到小分配)
for _, rs := range allRSs {
proportion := deploymentutil.GetReplicaSetProportion(rs, *deployment, deploymentReplicasToAdd, deploymentReplicasAdded)
nameToSize[rs.Name] = *(rs.Spec.Replicas) + proportion
deploymentReplicasAdded += proportion
}
// 执行扩缩
for _, rs := range allRSs {
if _, _, err := dc.scaleReplicaSet(ctx, rs, nameToSize[rs.Name], deployment, true); err != nil {
return err
}
}
}
return nil
}
| 参数 | 类型 | 含义 | 示例(replicas=10) |
|---|---|---|---|
| maxUnavailable | int 或 % | 滚动过程中最多不可用的 Pod 数 | 2(最多 2 个不可用) |
| maxSurge | int 或 % | 滚动过程中最多超出期望副本数的 Pod 数 | 2(最多 12 个同时存在) |
| minAvailable | 计算得出 | 滚动过程中必须保持可用的 Pod 数 | 10 - 2 = 8 个 |
| allowedSize | 计算得出 | 滚动过程中 Pod 总数的上限 | 10 + 2 = 12 个 |
思考记忆提示 — 回滚机制的核心是"从旧 ReplicaSet 恢复到 Deployment.Spec.Template"
// pkg/controller/deployment/rollback.go(行 33~151)
// rollback:回滚的入口函数
func (dc *DeploymentController) rollback(
ctx context.Context,
d *apps.Deployment,
rsList []*apps.ReplicaSet,
) error {
newRS, allOldRSs, err := dc.getAllReplicaSetsAndSyncRevision(ctx, d, rsList, true)
allRSs := append(allOldRSs, newRS)
rollbackTo := getRollbackTo(d) // 从 annotation 中读取回滚目标版本
// 找到目标 revision 对应的 ReplicaSet
for _, rs := range allRSs {
v, _ := deploymentutil.Revision(rs)
if v == rollbackTo.Revision {
// 找到了:将 RS 的 PodTemplate 复制到 Deployment
performedRollback, _ := dc.rollbackToTemplate(ctx, d, rs)
return err
}
}
// 找不到目标版本:放弃回滚,清除 rollbackTo annotation
return dc.updateDeploymentAndClearRollbackTo(ctx, d)
}
// rollbackToTemplate:将 RS 的 template 恢复到 Deployment
func (dc *DeploymentController) rollbackToTemplate(
ctx context.Context,
d *apps.Deployment,
rs *apps.ReplicaSet,
) (bool, error) {
// 比较 Deployment 模板和 RS 模板(忽略 pod-template-hash)
if !deploymentutil.EqualIgnoreHash(&d.Spec.Template, &rs.Spec.Template) {
// 将 RS 的 PodTemplateSpec 复制到 Deployment.Spec.Template
deploymentutil.SetFromReplicaSetTemplate(d, rs.Spec.Template)
// 将 RS 的 annotations 也复制到 Deployment(保留历史信息)
deploymentutil.SetDeploymentAnnotationsTo(d, rs)
performedRollback = true
}
// 清除 rollbackTo annotation(一次性操作)
return performedRollback, dc.updateDeploymentAndClearRollbackTo(ctx, d)
}
我的理解的意思是说
回滚机制的关键在于:回滚不是"倒带",而是"重新播放"。具体来说:
这就是为什么回滚也需要时间——它是新的滚动更新,不是瞬间完成的。
思考记忆提示 — Recreate 是最简单的策略——先删所有 Pod,再创建 newRS
// pkg/controller/deployment/recreate.go(行 28~75)
// rolloutRecreate:Recreate 策略的主入口
func (dc *DeploymentController) rolloutRecreate(
ctx context.Context,
d *apps.Deployment,
rsList []*apps.ReplicaSet,
podMap map[types.UID][]*v1.Pod,
) error {
// 1. 获取 newRS(不创建,仅查找)
newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(ctx, d, rsList, false)
allRSs := append(oldRSs, newRS)
activeOldRSs := controller.FilterActiveReplicaSets(oldRSs)
// ========== 步骤1:缩容所有 old RS ==========
scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(ctx, activeOldRSs, d)
if err != nil { return err }
if scaledDown {
return dc.syncRolloutStatus(ctx, allRSs, newRS, d) // 缩容后更新状态
}
// ========== 步骤2:等待所有 old Pod 真正被删除 ==========
// oldPodsRunning 检查是否有旧 Pod 还在运行
// 如果有,deletePod 会在最后一个 Pod 删除后触发下一次调谐
if oldPodsRunning(newRS, oldRSs, podMap) {
return dc.syncRolloutStatus(ctx, allRSs, newRS, d)
}
// ========== 步骤3:old RS 都缩容到 0 且 Pod 都删完了,创建 newRS ==========
if newRS == nil {
newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(ctx, d, rsList, true)
allRSs = append(oldRSs, newRS)
}
// ========== 步骤4:扩容 newRS ==========
if _, err := dc.scaleUpNewReplicaSetForRecreate(ctx, newRS, d); err != nil {
return err
}
// 5. 滚动完成,清理旧 RS
if util.DeploymentComplete(d, &d.Status) {
dc.cleanupDeployment(ctx, oldRSs, d)
}
return dc.syncRolloutStatus(ctx, allRSs, newRS, d)
}
// scaleDownOldReplicaSetsForRecreate:将所有 old RS 缩容到 0
func (dc *DeploymentController) scaleDownOldReplicaSetsForRecreate(
ctx context.Context,
oldRSs []*apps.ReplicaSet,
deployment *apps.Deployment,
) (bool, error) {
scaled := false
for _, rs := range oldRSs {
if *(rs.Spec.Replicas) == 0 { continue }
scaledRS, updatedRS, err := dc.scaleReplicaSet(ctx, rs, 0, deployment, false)
if err != nil { return false, err }
if scaledRS { oldRSs[i] = updatedRS; scaled = true }
}
return scaled, nil
}
思考记忆提示 — 进度判断是 Deployment 的"超时保护"——防止滚动更新无限期卡住
// pkg/controller/deployment/progress.go(行 36~200)
// syncRolloutStatus:更新 Deployment 滚动过程中的状态
func (dc *DeploymentController) syncRolloutStatus(
ctx context.Context,
allRSs []*apps.ReplicaSet,
newRS *apps.ReplicaSet,
d *apps.Deployment,
) error {
newStatus := calculateStatus(allRSs, newRS, d)
// 检查进度是否完成
if deploymentutil.DeploymentComplete(d, &newStatus) {
// 标记 NewRSAvailable 条件
condition := util.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionTrue,
util.NewRSAvailableReason, msg)
util.SetDeploymentCondition(&newStatus, *condition)
} else if deploymentutil.DeploymentProgressing(d, &newStatus) {
// 仍在进行中:记录 Progressing 条件
condition := util.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionTrue,
util.ReplicaSetUpdatedReason, msg)
util.SetDeploymentCondition(&newStatus, *condition)
} else if deploymentutil.DeploymentTimedOut(ctx, d, &newStatus) {
// 进度超时:标记为失败
condition := util.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionFalse,
util.TimedOutReason, msg)
util.SetDeploymentCondition(&newStatus, *condition)
}
// 更新 Deployment 状态
if !reflect.DeepEqual(d.Status, newStatus) {
newDeployment := d; newDeployment.Status = newStatus
_, err := dc.client.AppsV1().Deployments(d.Namespace).UpdateStatus(ctx, newDeployment, metav1.UpdateOptions{})
return err
}
// 进度卡住:在 deadline 时间点重新入队检查
dc.requeueStuckDeployment(ctx, d, newStatus)
return nil
}
// requeueStuckDeployment:计算下次检查进度的时间
// 公式:lastUpdateTime + progressDeadlineSeconds - now
func (dc *DeploymentController) requeueStuckDeployment(
ctx context.Context,
d *apps.Deployment,
newStatus apps.DeploymentStatus,
) time.Duration {
currentCond := util.GetDeploymentCondition(d.Status, apps.DeploymentProgressing)
// 剩余时间 = 最后更新时间 + deadline - 现在时间
after := currentCond.LastUpdateTime.Time.Add(
time.Duration(*d.Spec.ProgressDeadlineSeconds) * time.Second).Sub(nowFn())
if after < time.Second {
dc.enqueueRateLimited(d) // 已超时,立即重新入队
return 0
}
dc.enqueueAfter(d, after+time.Second) // 等待到 deadline 后重新入队
return after
}
小贴士
DeploymentTimedOut 的判断逻辑是:在 progressDeadlineSeconds 时间内,如果没有新的 Pod 变成 Available 状态,就认为滚动更新超时。判断公式:now - lastTransitionTime > progressDeadlineSeconds。这个超时检测通过 requeueStuckDeployment 在精确的 deadline 时间点触发,而不是依赖控制器的周期性同步(那样可能延迟较大)。
思考记忆提示 — 这些特性完善了 Deployment 的工程化能力——Pause/Resume、Scale、RevisionHistory
// pkg/controller/deployment/sync.go(行 82~111)
// checkPausedConditions:检查暂停条件,更新 Deployment Conditions
func (dc *DeploymentController) checkPausedConditions(ctx context.Context, d *apps.Deployment) error {
// 暂停时添加 PausedDeployReason 条件
if d.Spec.Paused && !pausedCondExists {
condition := util.NewDeploymentCondition(
apps.DeploymentProgressing, v1.ConditionUnknown,
deploymentutil.PausedDeployReason, "Deployment is paused")
deploymentutil.SetDeploymentCondition(&d.Status, *condition)
needsUpdate = true
} else if !d.Spec.Paused && pausedCondExists {
// 恢复时添加 ResumedDeployReason 条件
condition := util.NewDeploymentCondition(
apps.DeploymentProgressing, v1.ConditionUnknown,
deploymentutil.ResumedDeployReason, "Deployment is resumed")
deploymentutil.SetDeploymentCondition(&d.Status, *condition)
needsUpdate = true
}
if needsUpdate {
dc.client.AppsV1().Deployments(d.Namespace).UpdateStatus(ctx, d, metav1.UpdateOptions{})
}
return err
}
// pkg/controller/deployment/sync.go(行 439~477)
// cleanupDeployment:保留最新的 N 个旧 ReplicaSet
func (dc *DeploymentController) cleanupDeployment(
ctx context.Context,
oldRSs []*apps.ReplicaSet,
deployment *apps.Deployment,
) error {
if !deploymentutil.HasRevisionHistoryLimit(deployment) { return nil }
diff := int32(len(cleanableRSes)) - *deployment.Spec.RevisionHistoryLimit
if diff <= 0 { return nil }
// 按 revision 从旧到新排序,删除最老的 N 个
sort.Sort(deploymentutil.ReplicaSetsByRevision(cleanableRSes))
for i := int32(0); i < diff; i++ {
rs := cleanableRSes[i]
// 只删除副本数为 0 的 RS(有 Pod 运行的不能删)
if rs.Status.Replicas != 0 || *(rs.Spec.Replicas) != 0 { continue }
if rs.Generation > rs.Status.ObservedGeneration { continue }
dc.client.AppsV1().ReplicaSets(rs.Namespace).Delete(ctx, rs.Name, metav1.DeleteOptions{})
}
}
思考记忆提示 — Deployment Controller 是 Kubernetes 控制器生态的核心,理解它有助于学习其他控制器
全篇必记总纲
DeploymentController 的核心链路是:三级级联(Deployment → ReplicaSet → Pod)+ 五层分支决策(删除/暂停/回滚/扩缩容/策略)+ RollingUpdate 三步循环(扩容 newRS → 缩容 oldRSs → 清理)+ 回滚机制(从旧 RS 的 template 恢复)。理解这条链路,就理解了 Kubernetes 应用管理的全部精髓。
思考记忆提示 — FAQ 是全篇的"临考前速背"模块,20 组覆盖全部关键知识点
通过 OwnerReference 建立三级所有权关系。Deployment 创建 ReplicaSet 时,在 RS 的 OwnerReferences 中写入自己的 UID;RS 创建 Pod 时,在 Pod 的 OwnerReferences 中写入自己的 UID。这种级联关系由 Kubernetes Garbage Collector 自动维护:删除 Deployment 时,GC 会自动删除其 ReplicaSet;删除 ReplicaSet 时,GC 自动删除其 Pod。DeploymentController 通过 OwnerReference 追溯:RS 事件 → GetControllerOf(rs) → resolveControllerRef → 找到 Deployment → 入队。
因为三个资源的任意变化都可能影响 Deployment 的期望状态收敛。Deployment 变化(用户修改镜像/副本数)→ 直接触发调谐;ReplicaSet 变化(RSController 修改了副本数)→ 通过 ControllerRef 追溯 Deployment 触发调谐;Pod 变化(Recreate 策略下所有旧 Pod 删除完成)→ 触发 Deployment 创建 newRS。只监听 Deployment 不够,因为 ReplicaSetController 也会改变 RS 副本数,Deployment Controller 需要感知这些变化并重新判断是否需要滚动更新。
pod-template-hash 是 Deployment 用于唯一标识每个 ReplicaSet 版本的标签,由 PodTemplateSpec 的哈希值计算得出。当 Deployment 的 PodTemplateSpec 发生变化时,哈希值改变,Deployment Controller 会创建一个带有新哈希值的新 ReplicaSet。新的 ReplicaSet 的 Selector 也包含这个哈希值,确保只有新的 Pod 才属于新的 RS。这个标签在 newReplicaSet 生成时添加:labelsutil.CloneAndAddLabel(d.Spec.Template.Labels, apps.DefaultDeploymentUniqueLabelKey, podTemplateSpecHash)。
因为 Kubernetes 允许多个对象有相同的 name(不同 namespace),但 UID 是全局唯一的。通过 name 查找后验证 UID,可以防止名称冲突导致的错误匹配。例如:deployment-A 和 deployment-B 在不同 namespace 可能有同名的情况,resolveControllerRef 先用 name 查找,再用 UID 匹配,确保精确找到目标 Deployment。
因为 DeploymentController 在 deleteReplicaSet 中通过 OwnerReference 判断,而 OwnerReference 在 RS 被 GC 删除时仍然保存在 Tombstone 中。当 RS 被删除时,Tombstone.Obj 包含 RS 的完整副本(包含 OwnerReferences),deleteReplicaSet 可以从中提取 controllerRef 并追溯到 Deployment。这与 sample-controller 的模式完全相同。
当一个 ReplicaSet 没有 OwnerReference 时,DeploymentController 会尝试"收养"它。getDeploymentsForReplicaSet 通过标签选择器判断哪些 Deployment 的 selector 能匹配这个 RS 的标签。如果有 Deployment 的 selector 能匹配(比如 selector 是 {"app":"nginx"},RS 也有 {"app":"nginx"} 标签),DeploymentController 会通过 ControllerRefManager 尝试收养(Adopt)这个 RS,给它加上 OwnerReference。
处理 RS 被从一个 Deployment 转移给另一个 Deployment 的场景。例如:RS 原来属于 Deployment-A,后来通过 kubectl edit 修改了其 labels,使其不再匹配 Deployment-A 的 selector,而是匹配 Deployment-B 的 selector。DeploymentController 检测到 ControllerRef 变化,会同时触发旧 Deployment 和新 Deployment 的调谐,确保旧 Deployment 放弃该 RS,新 Deployment 收养它。
因为 RollingUpdate 策略下 Pod 的副本数由 ReplicaSetController 管理,Deployment Controller 不需要监听。Recreate 策略要求"先下后上"——必须等所有旧 Pod 都删除后,才能创建 newRS。Pod 删除事件(deletePod)检测 numPods 是否为 0,如果是,触发 Deployment 调谐,创建 newRS 并扩容。这是 Recreate 策略实现"先下后上"的关键机制。
使用 Workqueue 的限速队列实现指数退避重试,第 N 次重试的等待时间为 5ms × 2^(N-1)。第 1 次失败后 5ms 重试,第 2 次 10ms,第 3 次 20ms,第 15 次约 41s。超过 15 次后,handleErr 调用 Forget,放弃重试。这处理了大多数临时性错误(网络抖动、API Server 临时不可用),同时防止无限重试。
因为扩缩容只改变副本数,不改变 PodTemplate,不需要滚动更新。如果扩缩容走了 RollingUpdate 流程,会错误地触发 rolloutRolling(创建 newRS 或 scale up newRS),这是不必要的开销。isScalingEvent 通过比较所有活跃 RS 的 DesiredReplicasAnnotation 和 Deployment.Spec.Replicas 判断是否是扩缩容事件:相同则不是,不同则是。
因为先扩容保证服务始终有 minAvailable 个可用 Pod,先缩容会导致暂时低于 minAvailable。例如 replicas=10, maxUnavailable=2,minAvailable=8。如果先缩容,集群中 Pod 数可能降到 8 个(已到最低可用数),这时再扩容存在时间窗口——期间服务可用性处于临界状态。先扩容到 allowedSize(10+maxSurge),Pod 数最多到 12,然后再缩容,可以始终保证服务可用。
不能。当两个都设置为百分比时,百分比之和必须小于等于 100%。这是 Kubernetes API server 的验证规则。例如 replicas=10,maxUnavailable=50%, maxSurge=50% 是不允许的(50%+50%=100%,没有余量给新 Pod)。常见配置是 maxUnavailable=25%,maxSurge=25%(总和=50%,有足够余量),或 maxUnavailable=0,maxSurge=25%(全部用 surge 模式,滚动期间最多 12.5 个 Pod)。
因为 unhealthy replicas(failed/pending/unkown)会阻塞滚动更新进度,导致 ProgressDeadlineExceeded 超时。如果 oldRS 中有 Pod 处于 CrashLoopBackOff 或 Pending 状态,这些 Pod 不提供可用服务,但它们占用着 replicas 计数。通过 maxUnavailable 允许删除它们的配额,先清理 unhealthy replicas,可以减少不可用 Pod 的数量,同时不增加总体的不可用 Pod 数。
因为 annotations 中包含关键的 revision 历史信息(change-cause 等),回滚时需要一并恢复。例如:Deployment 原版本 annotation 有 kubernetes.io/change-cause: "kubectl create",新版本有 kubernetes.io/change-cause: "kubectl edit image"。回滚到旧版本时,SetDeploymentAnnotationsTo 会把旧版本的 annotations 复制回 Deployment,确保回滚后历史信息一致。
因为回滚是"用旧模板重新做一次滚动更新",原有的 newRS 会被 oldRSs 列表接收,下一次调谐自动触发 rolloutRolling。rollbackToTemplate 将旧 RS 的 PodTemplateSpec 复制到 Deployment.Spec.Template 后,Deployment.Spec.Template 发生变化。syncDeployment 检测到这是 Template 变化(不是简单的扩缩容),会在 getNewReplicaSet 中找到旧 RS(作为"新的 newRS"),自动触发新的滚动更新流程。
因为 Pod 删除是异步的——删除请求发送到 API Server 后,Pod 对象被标记为 Deleting,但实际的容器进程还在运行。scaleDownOldReplicaSetsForRecreate 将 RS 缩容到 0 后,Kubernetes 开始终止 Pod。但 Pod 的 DELETE 事件需要经过 Informer 链路传递到 DeploymentController.deletePod。deletePod 检测到 numPods==0 时,才触发下一轮调谐创建 newRS。如果没有这个等待,newRS 可能在部分旧 Pod 还在运行时就创建了,违反了 Recreate 的"先下后上"保证。
通过 Progressing Condition 的 LastUpdateTime + progressDeadlineSeconds - now 计算。每次 Deployment 状态发生变化(Progressing condition 更新)时,LastUpdateTime 会被设置为当前时间。requeueStuckDeployment 计算 after = lastUpdateTime + deadline - now,如果 after < 1s,说明已经超时,立即重新入队检查;如果 after > 0,将任务 enqueueAfter(after) 延迟入队。这样即使控制器没有周期性同步,也能在精确的 deadline 时间点触发超时检测。
镜像变更会入队,但会被 syncDeployment 的 paused 判断拦住,不执行滚动更新。Deployment 更新触发 updateDeployment → enqueueDeployment(d),syncDeployment 被调用。但当 d.Spec.Paused=true 时,syncDeployment 直接 return dc.sync(ctx, d, rsList),只执行扩缩容和清理,不走 rolloutRolling。Pause 状态下只能调整副本数,不能改变模板。Resume 后,Deployment.Spec.Template 变化会触发正常的 rolloutRolling。
最多保留 2 个满足条件的旧 ReplicaSet(副本数为 0 且不在删除中的)。cleanupDeployment 只删除副本数为 0 的 RS——有 Pod 运行的 RS 不能删除(可能还在提供服务)。即使 RevisionHistoryLimit=2,如果旧 RS 中有一个还在运行副本,实际只会清理其他满足条件的 RS。cleanupDeployment 按 revision 从旧到新排序,优先删除最老的。
两个核心条件:Progressing(进度)和 Available(可用性)。Progressing 条件的 reason 包括:NewRSAvailable(新版本可用)、ReplicaSetUpdated(版本正在更新)、TimedOutReason(进度超时)、PausedDeployReason(已暂停)。Available 条件的 reason 包括:MinimumReplicasAvailable(满足最小可用数)、MinimumReplicasUnavailable(不满足最小可用数)。kubectl rollout status 和 kubectl describe deployment 的输出都来自这些 conditions。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。