惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Blog — PlanetScale
Blog — PlanetScale
Jina AI
Jina AI
MyScale Blog
MyScale Blog
N
Netflix TechBlog - Medium
月光博客
月光博客
云风的 BLOG
云风的 BLOG
T
The Blog of Author Tim Ferriss
博客园_首页
GbyAI
GbyAI
The Cloudflare Blog
博客园 - 叶小钗
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
MongoDB | Blog
MongoDB | Blog
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
量子位
博客园 - Franky
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
人人都是产品经理
人人都是产品经理
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
M
MIT News - Artificial intelligence
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
InfoQ
Google DeepMind News
Google DeepMind News
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
Apple Machine Learning Research
Apple Machine Learning Research
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Stack Overflow Blog
Stack Overflow Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Last Week in AI
Last Week in AI
J
Java Code Geeks
腾讯CDC
aimingoo的专栏
aimingoo的专栏
C
Check Point Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
S
Schneier on Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
Lohrmann on Cybersecurity
S
Securelist
F
Full Disclosure
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
The GitHub Blog
The GitHub Blog

博客园 - 左扬

VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— VictoriaLogs 协同:Metrics 到 Logs 的一体化监控 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 源码阅读路线图:如何高效阅读 VM 源码 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 开源生态:VM 在 CNCF 生态中的位置 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 与其他 TSDB 对比:Prometheus/InfluxDB/Thanos/VM VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 写入吞吐/查询延迟/内存占用的数学模型 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 模块依赖图——从 import 语句看组件关系 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Goroutine 池/atomic/零拷贝/sync.Pool VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 多租户架构——accountID/projectID 与 tenant 隔离 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 版本演进:1.146.0 LTS 重大更新解析 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 整体数据流:一条监控数据的完整生命周期 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 架构演进:从 TSDB 到 MergeSet 的设计取舍 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Single-Node vs Cluster 模式本质区别 VictoriaMetrics 1.146.0 源码【左扬精讲】—— 开篇总览 Rust 专题【左扬精讲】—— 从语法到灵魂:Ownership、Borrowing 与多语言对比 kubernetes 源码【左扬精讲】—— kube-scheduler 启动流程源码分析 Rust 专题【左扬精讲】—— 选择控制语句、运算符与格式化输出 Rust 专题【左扬精讲】—— 所有权详解 Rust 专题【左扬精讲】—— 作用域详解 Rust 专题【左扬精讲】—— 变量、常量与标量数据类型 kubernetes 源码 / Operator 专题【左扬精讲】—— Operator 开发中的 Webhook:从准入控制到生产部署 Kubernetes源码 / Operator 专题【左扬精讲】—— 实现 Application Controller:从零构建生产级控制器 Kubernetes 编程 / Operator 专题【左扬精讲】—— 定义 Application 资源 + 添加自定义新 API 完整指南 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 八):内部架构与核心组件 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 八): —— 从入口到调度的全链路源码剖析(k8s v1.36.1) DeepSeek-R1 多模态 R1 / VLM-GRPO【左扬精讲】—— Qwen2-VL 微调与视觉推理强化学习实战 DeepSeek-R1 工业 RAG + 微调混合系统【左扬精讲】—— R1 系列收官之作:从 Prompt → RAG → 微调 选型决策树 DeepSeek-R1 推理时扩展【左扬精讲】—— o1 / R1 慢思考机制:Self-Consistency + ToT + PRM 详解 DeepSeek-R1 端侧 LLM 工程【左扬精讲】—— llama.cpp 调参与 Apple Silicon / 国产 NPU / Android 端侧落地全攻略 DeepSeek-R1 vLLM + k8s 生产部署【左扬精讲】—— 从单卡 7B 到 100 卡 671B MoE 集群的工业化部署实战 DeepSeek-R1 评估与系统(Evaluation & Systems)【左扬精讲】—— 从 GSM8K/MMLU 到 LLM-as-Judge 的工业级评估方法论 DeepSeek-R1 模型训练与算法【左扬精讲】—— GRPO 进阶算法:DAPO / PRIME / RLVR / PRM 四大 2025 前沿改进 DeepSeek-R1 模型训练与算法【左扬精讲】—— 数据蒸馏:用 DeepSeek-R1-671B 生成 800K 高质量 CoT 样本的完整流水线 DeepSeek-R1 优化与微调实战【左扬精讲】—— 从 R1 强化学习新范式到 GRPO 微调一站式入门 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 七):自定义插件开发实战 —— 手写一个 Score 插件并注册到集群 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 六):Scheduler Profile 与多调度器 —— 如何配置多个 profile 实现多租户、Coordinated LeaderElection Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 五):SchedulingQueue 与 QueueingHint —— 三段队列的细节、v1.36 新引入的 QueueingHint 工作机制 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 四):抢占(Preemption)算法剖析 —— DefaultPreemption 如何选 victim、PodDisruptionBudget 如何约束 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 二):内置插件逐个精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件 逐个精读 k8s 源码级精讲(二十六):调度器内置插件逐个精读 Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):Scheduling Framework 扩展点逐个源码拆解 Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):初识调度模型、内部架构与事件驱动机制 Kubernetes 编程 / client-go 专题【左扬精讲】—— 四种客户端:为什么、怎么选、怎么用 Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime、kubebuilder、operator-sdk 三大框架深度对比 Kubernetes 编程 / Operator 专题【左扬精讲】—— 深入理解 ManagedFields 字段冲突协调机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Finalizers 深度解析:对象的生命周期与删除控制 Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference 字段与级联删除机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— 深入学习 Server-Side Apply:managedFields 替代 last-applied-configuration 的演进方向 Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Annotations 与元数据体系(Operator 专题) Kubernetes 编程 / Operator 专题【左扬精讲】—— RESTMapper:把 Group / Version / Kind / Resource 四元组翻译成 REST 路径的"查字典"大师 Kubernetes 编程 / Operator 专题【左扬精讲】—— Converter 资源版本转换器 Kubernetes 编程 / Operator 专题【左扬精讲】—— Application 业务扩展:从单 Deployment 到多 Workload 的复合 Operator 演进 Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制:k8s 资源生命周期的三大支柱 Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime 框架内幕:从 Manager 到 Reconcile 的全栈拆解 Kubernetes 编程 / Operator 专题【左扬精讲】—— 生产级 Operator 最佳实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— application-operator Reconcile 循环源码精讲:从 client-go Informer 到 workqueue 的全链路解剖 Kubernetes 编程 / Operator 专题【左扬精讲】—— 从零搭建一个 application-operator 新项目:脚手架、API 设计与基于原生 DeploymentStatus/ServiceStatus 的状态建模 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:生产级 Controller 实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析: Controller 调试与诊断工具:从日志分析到问题定位 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DynamicClient 操作 CRD:无需代码生成的动态操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:控制器与 APIServer 完整交互流程:从 Watch 到缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:错误处理与重试机制:WorkQueue 限速器详解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Leader 选举机制:高可用控制器的必备技能 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Controller 开发模式完整实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:SharedInformerFactory 与等待缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:从认证配置到 Deployment 操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:版本对应、架构组件与组件关系 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Informer 源码深度解析:从底层原理到实战应用 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Reflector 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:ListWatcher 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Indexer 与 ThreadSafeStore 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DeltaFIFO 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:workqueue 核心原理与实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码:serializer 与 codec 差异、编解码数据结构、codec 核心调用链路 Kubernetes 编程 / Operator 专题【左扬精讲】—— Scheme 资源注册机制全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 自定义资源的内部版本与外部版本:从源码看版本定义机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 1.36.1 核心 API 数据结构全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 构建过程 【AIOPS】一文读懂LLM【左扬精讲】:从诞生到普及,解锁大语言模型的核心密码 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Hooks 源码精讲:Hooks 可观测性的无侵入式实现 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 配置解析源码精讲 ——SRE 自定义 Agent 核心技巧 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 并发模型解析 ——SRE 应对高并发采集的调优思路 【AIOPS】AI Agent 专题【左扬精讲】基础架构篇:MCP-VictoriaMetrics Golang 源码整体架构拆解 ——SRE 必懂的核心模块与数据流 OpenTelemetry 开发实战【左扬精讲】—— 云原生可观测体系构建与分布式追踪二次开发 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(下篇)—— 预测式弹性扩缩容 Operator 落地实现) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(上篇)—— 时序预测模型构建与离线训练) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 6 —— 基于运维专家知识库的智能故障诊断与排查 Operator 实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 5 —— 基于大语言模型(LLM)的实时日志流智能监测 Operator 实现 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 4 —— 基于 Operator 实现大模型私有化部署与管理 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 3(上篇)—— 面向 AI / 算力调度场景:GPU 竞价实例资源池统一调度管理 Operator 开发 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目 2 —— 面向零售 / 电商潮汐流量难题:多云多集群数据中心级全链路弹性伸缩 DataCenter Scaler Operator 从 0 到 1 全链路开发 Kubernetes编程 / Operator专题【左扬精讲】—— 深入理解Kubebuilder注解:为什么Operator开发离不开这些特殊注释 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目1 —— Applicaion Operator(通用应用生命周期管理 Operator 实战) Pod 镜像拉取失败?kubectl edit pods修改镜像地址的底层原理与实操 (该方法仅为临时应急方案,并非长期解决方案) Kubernetes编程/Operator专题精讲—— 理解控制器模式 —— 控制器模式的核心原理与实现逻辑(从原理到实践) 【AIOPS】AI Agent 专题【左扬精讲】模型微调实战:一站式平台 LLaMA-Factory 【AIOPS】AI Agent 专题【左扬精讲】基于 k8s+vLLM+Ray 分布式部署全指南:架构设计、资源调度与性能优化 【AIOPS】AI Agent专题【左扬精讲】非量化版DeepSeek分布式部署全指南:精度保障、显存规划与Ollama/vLLM选型 【AIOPS】AI Agent 专题【左扬精讲】零开发框架实现 ReAct Agent(Go SRE友好)
kubernetes 源码 / Operator 专题【左扬精讲】—— Deployment Controller 源码分析:从对象创建到滚动更新
左扬 · 2026-06-22 · via 博客园 - 左扬

kubernetes 源码 / Operator 专题【左扬精讲】—— Deployment Controller 源码分析:从对象创建到滚动更新

读完本篇,你应该能回答:

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 的超时计算公式

文章目录

  1. 一、Deployment 基础:为什么需要 Deployment?—— 声明式 Pod 管理
  2. 二、DeploymentController 架构总览:三级级联体系
  3. 三、DeploymentController 结构体设计
  4. 四、NewDeploymentController:三个 Informer + 三组事件处理器
  5. 五、事件处理链路:Deployment → ReplicaSet → Pod 事件如何驱动调谐
  6. 六、DeploymentController.Run:启动 Workers 与缓存同步
  7. 七、syncDeployment:主调谐循环的五层分支决策树
  8. 八、滚动更新:rolloutRolling 完整流程
  9. 九、滚动更新:比例缩放与 maxUnavailable / maxSurge
  10. 十、回滚机制:rollback 的核心原理
  11. 十一、Recreate 策略:先下后上
  12. 十二、进度判断:ProgressDeadlineExceeded 与 syncRolloutStatus
  13. 十三、其他特性:Pause、Scale、RevisionHistory
  14. 十四、Roadmap:后续预告

一、Deployment 基础:为什么需要 Deployment?—— 声明式 Pod 管理

思考记忆提示本节是全篇的"入口"——理解 Deployment 解决什么问题,才能理解 Controller 的设计决策

  • ReplicaSet 已经能管理 Pod,Deployment 在 ReplicaSet 之上增加了一层"声明式滚动更新"
  • Deployment 提供:滚动更新、回滚、暂停、比例缩放等高级特性
  • Deployment 通过 OwnerReference 管理 ReplicaSet,ReplicaSet 管理 Pod
  • 面试高频提问:Deployment 和 ReplicaSet 的本质区别是什么?

Kubernetes 的 Deployment 是运行在 ReplicaSet 之上的一个"声明式管理层"。ReplicaSet 能确保指定数量的 Pod 副本始终运行,但它是"无状态"的——如果你想更新这些 Pod 的镜像版本,ReplicaSet 本身并不提供任何机制。

Deployment 在 ReplicaSet 之上增加了一个关键能力:声明式地描述 Pod 模板的变更,并自动将变更以滚动更新的方式应用到集群中。当你修改 Deployment 的 .spec.template 时,Deployment Controller 会自动创建一个新的 ReplicaSet(newRS),然后逐步将流量从旧 ReplicaSet(oldRS)迁移到 newRS。

我的理解的意思是说

Deployment 的架构可以用出版社-期刊社-印刷厂来类比:

  • Deployment = 出版社总编:声明"我要发行第三版杂志"(更新 PodTemplate)
  • ReplicaSet = 期刊社:管理当前版本的印刷任务(newRS = 第三版,oldRS = 第二版)
  • Pod = 印刷厂:实际印刷的每一本书
  • OwnerReference = 出版授权:每一本书(Pod)属于某个期刊社(RS),期刊社(RS)属于某个出版社(Deployment)
  • 滚动更新:总编决定"每版保留 2 本旧刊、再加印 1 本新刊,逐步替换"
  • 回滚:总编决定"回到第二版",期刊社重新启动第二版的印刷

这样每一层都有自己的职责,层层级联,形成了 Kubernetes 应用管理的完整闭环。

二、DeploymentController 架构总览:三级级联体系

思考记忆提示本节是全篇的"地图"——DeploymentController 同时监听三个资源,理解它们的关系是关键

  • Deployment → ReplicaSet → Pod 是三级级联,OwnerReference 贯穿每一级
  • DeploymentController 同时持有这三个资源的 Informer 和 Lister
  • Deployment 变化 → ReplicaSet 变化 → Pod 变化,三路事件都可以触发调谐
  • 面试高频提问:为什么 DeploymentController 需要监听 Pod 的 Delete 事件?
┌──────────────────────────────────────────────────────────────────────────────────────┐
│                        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 建立明确的所有权关系。这种设计的核心优势是:

  • 职责分离:Deployment 只管"版本管理",ReplicaSet 只管"副本数管理",Pod 是实际执行单元
  • GC 驱动清理:删除 Deployment 时,GC 自动删除其 ReplicaSet;删除 ReplicaSet 时,GC 自动删除其 Pod
  • 事件驱动调谐:三个资源的任意变化都可以触发 Deployment 的重新调谐,确保最终状态收敛

三、DeploymentController 结构体设计

思考记忆提示Controller 结构体是全篇的核心骨架,每个字段都有明确的职责

  • 三个 Lister + 三个 Synced:分别对应 Deployment、ReplicaSet、Pod 的本地缓存
  • rsControl:RSControlInterface,用于 Adoption/Orphaning(认领/放弃 ReplicaSet)
  • podIndexer:Pod 的 Indexer,通过 ControllerRef UID 索引查找 Pod
  • 面试高频提问:为什么需要 podIndexer 而不是直接用 podLister?
// 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]
}

小贴士

podIndexerpodLister 的关系是:Lister 是对外的高层接口,Indexer 是底层的索引存储。Lister 底层使用 Indexer。podIndexer 特别重要是因为 controller.FilterPodsByOwner 需要通过 ControllerRef UID 索引快速查找属于某个 ReplicaSet 的所有 Pod,这在 getPodMapForDeployment 中被大量使用。

四、NewDeploymentController:三个 Informer + 三组事件处理器

思考记忆提示NewDeploymentController 是整个控制器的"组装工厂"——三个 Informer 的事件处理器是核心

  • Deployment Informer:处理 Deployment 的增删改事件
  • ReplicaSet Informer:处理 ReplicaSet 的增删改事件(通过 ControllerRef 追溯到 Deployment)
  • Pod Informer:仅处理 Pod 的删除事件(Recreate 策略需要)
  • 面试高频提问:为什么 Pod Informer 只注册了 DeleteFunc,没有 AddFunc/UpdateFunc?
// 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 的核心逻辑。

五、事件处理链路:Deployment → ReplicaSet → Pod 事件如何驱动调谐

思考记忆提示这是全篇最复杂的部分——理解三条事件链路的区别和联系

  • Deployment 事件:直接触发调谐(最简单的路径)
  • ReplicaSet 事件:通过 OwnerReference 追溯 Deployment,再触发调谐
  • Pod 事件:仅 Recreate 策略下所有 Pod 删完后触发
  • 面试高频提问:ReplicaSet 变化时,如何判断它是否属于某个 Deployment?

5.1 Deployment 事件链路

// 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 会处理"已删除"的情况
}

5.2 ReplicaSet 事件链路(核心)

// 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) }
    }
}

5.3 Pod 事件链路(Recreate 专用)

// 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)
        }
    }
}

我的理解的意思是说

三条事件链路的触发条件各不相同,理解它们的区别是关键:

  • Deployment 事件:最直接。用户修改 Deployment(改变副本数、改变镜像)→ 直接触发调谐。
  • ReplicaSet 事件:最复杂。RS 被更新(比如 ReplicaSetController 修改了副本数)→ 通过 OwnerReference 找到 Deployment → 触发调谐。RS ControllerRef 变化(被不同 Deployment 收养)→ 旧 Deployment 和新 Deployment 都要调谐。
  • Pod 事件:最特殊。只有 Recreate 策略需要。当所有旧 Pod 都被删除后,deletePod 检测到 numPods==0,触发 Deployment 调谐创建 newRS(先下后上的关键节点)。

六、DeploymentController.Run:启动 Workers 与缓存同步

思考记忆提示Run 是控制器的启动入口——与 sample-controller 相同的模式,但多了 PodIndexer

  • 必须等待三个缓存全部同步才能启动 Workers
  • Workers 是独立的 goroutine,通过 WaitGroup 管理生命周期
  • handleErr 使用 maxRetries=15 防止无限重试
  • 面试高频提问:为什么 DeploymentController 要等待 Pod 缓存同步?
// 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:主调谐循环的五层分支决策树

思考记忆提示syncDeployment 是全篇最重要的方法——控制器的所有业务逻辑都从这里分发到各个分支

  • 第一层:Deployment 是否被删除(Deleting)
  • 第二层:Deployment 是否暂停(Paused)
  • 第三层:是否有回滚请求(Rollback)
  • 第四层:是否是扩缩容事件(ScalingEvent)
  • 第五层:选择策略类型(Recreate / RollingUpdate)
  • 面试高频提问:为什么 ScalingEvent 要单独处理,不走 RollingUpdate 流程?
// 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 被删除? → syncStatusOnly(仅更新状态)
  • Deployment 被暂停? → sync(只扩缩容,不更新模板)
  • 有回滚请求? → rollback(优先处理回滚)
  • 只是扩缩容? → sync(只调副本数,不换模板)
  • 策略是 Recreate? → rolloutRecreate(先下后上)
  • 策略是 RollingUpdate? → rolloutRolling(滚动更新)

这个顺序是精心设计的:删除 → 暂停 → 回滚 → 扩缩容 → 滚动更新,从最紧急到最常规依次处理。

八、滚动更新:rolloutRolling 完整流程

思考记忆提示滚动更新是 Deployment 最核心的特性——理解 rolloutRolling 就理解了一半的 Deployment Controller

  • rolloutRolling 的三步循环:scale up newRS → scale down oldRSs → cleanup oldRSs
  • reconcileNewReplicaSet:确保 newRS 副本数达到期望值
  • reconcileOldReplicaSets:确保 oldRS 按 maxUnavailable 比例缩容
  • 面试高频提问:为什么先 scale up 再 scale down?反过来会怎样?
// 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,这个顺序不能颠倒。原因:

  • 如果先缩容 oldRSs,集群中的可用 Pod 数会暂时低于 minAvailable(replicas - maxUnavailable),导致服务中断
  • 先扩容 newRS(使用 maxSurge 超出期望副本数),再缩容 oldRSs,可以保证服务始终有 minAvailable 个 Pod 可用
  • 这个顺序是由 Kubernetes 滚动更新的"零停机"保证所要求的

九、滚动更新:比例缩放与 maxUnavailable / maxSurge

思考记忆提示比例缩放是滚动更新中最复杂的部分——maxUnavailable 和 maxSurge 如何协同工作

  • maxUnavailable:滚动过程中最多可以有多少个 Pod 不可用
  • maxSurge:滚动过程中最多可以比期望副本数多出多少个 Pod
  • 两者不能同时为 0,也不能同时为百分比时超过 100%
  • 面试高频提问:maxUnavailable=2, maxSurge=2, replicas=10 的滚动更新中,最多同时有多少个 Pod?
// 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 个

十、回滚机制:rollback 的核心原理

思考记忆提示回滚机制的核心是"从旧 ReplicaSet 恢复到 Deployment.Spec.Template"

  • 回滚不创建新的 ReplicaSet,而是改变 Deployment 的 PodTemplate
  • 变更后的 PodTemplate 会触发正常的滚动更新流程
  • RevisionHistoryLimit 控制保留多少个旧 ReplicaSet 用于回滚
  • 面试高频提问:kubectl rollout undo 的底层原理是什么?
// 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)
}

我的理解的意思是说

回滚机制的关键在于:回滚不是"倒带",而是"重新播放"。具体来说:

  • kubectl rollout undo 设置 Deployment 的 deployment.kubernetes.io/revision annotation 指向旧版本的 revision
  • Deployment Controller 检测到 rollbackTo 不为空,调用 rollback()
  • rollback() 找到目标版本的 ReplicaSet,将它的 PodTemplateSpec 复制到 Deployment.Spec.Template
  • Deployment.Spec.Template 变化后,下一次 syncDeployment 会检测到这是滚动更新,自动触发正常的 rolloutRolling 流程
  • 这意味着:回滚实际上是一个"用旧模板做一次新的滚动更新"

这就是为什么回滚也需要时间——它是新的滚动更新,不是瞬间完成的。

十一、Recreate 策略:先下后上

思考记忆提示Recreate 是最简单的策略——先删所有 Pod,再创建 newRS

  • Recreate 保证新旧 Pod 不会同时运行(适合有状态应用)
  • 关键等待点:必须等所有 oldRS 的 Pod 都删完后,才创建 newRS
  • Pod 删除事件通过 deletePod 触发下一次调谐
  • 面试高频提问:为什么 Recreate 需要监听 Pod 删除事件?
// 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
}

十二、进度判断:ProgressDeadlineExceeded 与 syncRolloutStatus

思考记忆提示进度判断是 Deployment 的"超时保护"——防止滚动更新无限期卡住

  • ProgressDeadlineExceeded:超过指定时间没有进度则标记为失败
  • requeueStuckDeployment:在指定时间后重新入队检查进度
  • syncRolloutStatus 更新 Deployment 的 Conditions(Progressing / Available)
  • 面试高频提问:ProgressDeadlineExceeded 是如何判断超时的?
// 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 时间点触发,而不是依赖控制器的周期性同步(那样可能延迟较大)。

十四、其他特性:Pause、Scale、RevisionHistory

思考记忆提示这些特性完善了 Deployment 的工程化能力——Pause/Resume、Scale、RevisionHistory

  • Pause:通过 .spec.paused=true 暂停滚动更新,可以安全地修改后恢复
  • RevisionHistoryLimit:保留多少个旧 ReplicaSet 用于回滚
  • cleanupDeployment:删除超出限制的旧 ReplicaSet
  • 面试高频提问:Paused 状态下,Deployment 还响应 Spec 变更吗?

13.1 Pause 机制

// 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
}

13.2 RevisionHistoryLimit 与清理

// 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{})
    }
}

十四、Roadmap:后续预告

思考记忆提示Deployment Controller 是 Kubernetes 控制器生态的核心,理解它有助于学习其他控制器

  • ReplicaSetController:Deployment 和 ReplicaSet 的下一层,管理 Pod 副本数
  • StatefulSet Controller:有状态应用的层级管理
  • DaemonSet Controller:每个节点一个 Pod 的管理
  • ReplicaSetController 详解:Deployment 的下一层,管理 Pod 的副本数保证,是 Deployment 滚动更新的实际执行者。重点讲解 ReplicaSetController 如何与 DeploymentController 协同工作。
  • StatefulSet Controller:有状态应用的管理,PersistentVolumeClaim 的自动绑定、StatefulSet 的有序部署/删除/扩展。
  • HPA(HorizontalPodAutoscaler)Controller:基于指标自动扩缩容,与 Deployment 的副本数联动。

全篇必记总纲

DeploymentController 的核心链路是:三级级联(Deployment → ReplicaSet → Pod)+ 五层分支决策(删除/暂停/回滚/扩缩容/策略)+ RollingUpdate 三步循环(扩容 newRS → 缩容 oldRSs → 清理)+ 回滚机制(从旧 RS 的 template 恢复)。理解这条链路,就理解了 Kubernetes 应用管理的全部精髓。


FAQ:常见疑问(20 组)

思考记忆提示FAQ 是全篇的"临考前速背"模块,20 组覆盖全部关键知识点

  • Q1-Q5 围绕三级级联架构:OwnerReference、ControllerRefManager、PodIndexer
  • Q6-Q10 围绕事件处理链路:Deployment/RS/Pod 三路事件处理逻辑
  • Q11-Q15 围绕滚动更新:maxUnavailable/maxSurge、比例缩放、scale up/down 顺序
  • Q16-Q20 围绕回滚、进度判断、Pause、RevisionHistory、其他策略

Q1. Deployment → ReplicaSet → Pod 的三级级联是如何建立的?

通过 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 → 入队。

Q2. DeploymentController 为什么需要同时监听 Deployment、ReplicaSet、Pod 三个资源?

因为三个资源的任意变化都可能影响 Deployment 的期望状态收敛。Deployment 变化(用户修改镜像/副本数)→ 直接触发调谐;ReplicaSet 变化(RSController 修改了副本数)→ 通过 ControllerRef 追溯 Deployment 触发调谐;Pod 变化(Recreate 策略下所有旧 Pod 删除完成)→ 触发 Deployment 创建 newRS。只监听 Deployment 不够,因为 ReplicaSetController 也会改变 RS 副本数,Deployment Controller 需要感知这些变化并重新判断是否需要滚动更新。

Q3. pod-template-hash 是什么?为什么每个 ReplicaSet 都有这个标签?

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)

Q4. resolveControllerRef 为什么用 name+UID 双重验证?

因为 Kubernetes 允许多个对象有相同的 name(不同 namespace),但 UID 是全局唯一的。通过 name 查找后验证 UID,可以防止名称冲突导致的错误匹配。例如:deployment-A 和 deployment-B 在不同 namespace 可能有同名的情况,resolveControllerRef 先用 name 查找,再用 UID 匹配,确保精确找到目标 Deployment。

Q5. ReplicaSet 的 DeleteFunc 为什么不需要 Tombstone 处理(像 sample-controller 那样)?

因为 DeploymentController 在 deleteReplicaSet 中通过 OwnerReference 判断,而 OwnerReference 在 RS 被 GC 删除时仍然保存在 Tombstone 中。当 RS 被删除时,Tombstone.Obj 包含 RS 的完整副本(包含 OwnerReferences),deleteReplicaSet 可以从中提取 controllerRef 并追溯到 Deployment。这与 sample-controller 的模式完全相同。

Q6. addReplicaSet 中处理 Orphan RS 的逻辑是什么?

当一个 ReplicaSet 没有 OwnerReference 时,DeploymentController 会尝试"收养"它。getDeploymentsForReplicaSet 通过标签选择器判断哪些 Deployment 的 selector 能匹配这个 RS 的标签。如果有 Deployment 的 selector 能匹配(比如 selector 是 {"app":"nginx"},RS 也有 {"app":"nginx"} 标签),DeploymentController 会通过 ControllerRefManager 尝试收养(Adopt)这个 RS,给它加上 OwnerReference。

Q7. ReplicaSet UpdateFunc 中的 controllerRefChanged 判断是处理什么场景?

处理 RS 被从一个 Deployment 转移给另一个 Deployment 的场景。例如:RS 原来属于 Deployment-A,后来通过 kubectl edit 修改了其 labels,使其不再匹配 Deployment-A 的 selector,而是匹配 Deployment-B 的 selector。DeploymentController 检测到 ControllerRef 变化,会同时触发旧 Deployment 和新 Deployment 的调谐,确保旧 Deployment 放弃该 RS,新 Deployment 收养它。

Q8. Pod DeleteFunc 为什么只在 Recreate 策略下触发调谐?

因为 RollingUpdate 策略下 Pod 的副本数由 ReplicaSetController 管理,Deployment Controller 不需要监听。Recreate 策略要求"先下后上"——必须等所有旧 Pod 都删除后,才能创建 newRS。Pod 删除事件(deletePod)检测 numPods 是否为 0,如果是,触发 Deployment 调谐,创建 newRS 并扩容。这是 Recreate 策略实现"先下后上"的关键机制。

Q9. maxRetries=15 的重试机制是如何工作的?

使用 Workqueue 的限速队列实现指数退避重试,第 N 次重试的等待时间为 5ms × 2^(N-1)。第 1 次失败后 5ms 重试,第 2 次 10ms,第 3 次 20ms,第 15 次约 41s。超过 15 次后,handleErr 调用 Forget,放弃重试。这处理了大多数临时性错误(网络抖动、API Server 临时不可用),同时防止无限重试。

Q10. syncDeployment 中为什么 ScalingEvent 要单独判断,不能走 RollingUpdate 流程?

因为扩缩容只改变副本数,不改变 PodTemplate,不需要滚动更新。如果扩缩容走了 RollingUpdate 流程,会错误地触发 rolloutRolling(创建 newRS 或 scale up newRS),这是不必要的开销。isScalingEvent 通过比较所有活跃 RS 的 DesiredReplicasAnnotation 和 Deployment.Spec.Replicas 判断是否是扩缩容事件:相同则不是,不同则是。

Q11. RollingUpdate 为什么必须先扩容 newRS 再缩容 oldRSs?

因为先扩容保证服务始终有 minAvailable 个可用 Pod,先缩容会导致暂时低于 minAvailable。例如 replicas=10, maxUnavailable=2,minAvailable=8。如果先缩容,集群中 Pod 数可能降到 8 个(已到最低可用数),这时再扩容存在时间窗口——期间服务可用性处于临界状态。先扩容到 allowedSize(10+maxSurge),Pod 数最多到 12,然后再缩容,可以始终保证服务可用。

Q12. maxUnavailable 和 maxSurge 可以同时设置为百分比吗?

不能。当两个都设置为百分比时,百分比之和必须小于等于 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)。

Q13. cleanupUnhealthyReplicas 的优先级为什么高于 scaleDownOldReplicaSets?

因为 unhealthy replicas(failed/pending/unkown)会阻塞滚动更新进度,导致 ProgressDeadlineExceeded 超时。如果 oldRS 中有 Pod 处于 CrashLoopBackOff 或 Pending 状态,这些 Pod 不提供可用服务,但它们占用着 replicas 计数。通过 maxUnavailable 允许删除它们的配额,先清理 unhealthy replicas,可以减少不可用 Pod 的数量,同时不增加总体的不可用 Pod 数。

Q14. rolloutToTemplate 中为什么要复制 RS 的 annotations 到 Deployment?

因为 annotations 中包含关键的 revision 历史信息(change-cause 等),回滚时需要一并恢复。例如:Deployment 原版本 annotation 有 kubernetes.io/change-cause: "kubectl create",新版本有 kubernetes.io/change-cause: "kubectl edit image"。回滚到旧版本时,SetDeploymentAnnotationsTo 会把旧版本的 annotations 复制回 Deployment,确保回滚后历史信息一致。

Q15. 回滚为什么不需要创建新的 ReplicaSet?

因为回滚是"用旧模板重新做一次滚动更新",原有的 newRS 会被 oldRSs 列表接收,下一次调谐自动触发 rolloutRolling。rollbackToTemplate 将旧 RS 的 PodTemplateSpec 复制到 Deployment.Spec.Template 后,Deployment.Spec.Template 发生变化。syncDeployment 检测到这是 Template 变化(不是简单的扩缩容),会在 getNewReplicaSet 中找到旧 RS(作为"新的 newRS"),自动触发新的滚动更新流程。

Q16. Recreate 策略中,为什么必须等待 oldPodsRunning 返回 false?

因为 Pod 删除是异步的——删除请求发送到 API Server 后,Pod 对象被标记为 Deleting,但实际的容器进程还在运行。scaleDownOldReplicaSetsForRecreate 将 RS 缩容到 0 后,Kubernetes 开始终止 Pod。但 Pod 的 DELETE 事件需要经过 Informer 链路传递到 DeploymentController.deletePod。deletePod 检测到 numPods==0 时,才触发下一轮调谐创建 newRS。如果没有这个等待,newRS 可能在部分旧 Pod 还在运行时就创建了,违反了 Recreate 的"先下后上"保证。

Q17. ProgressDeadlineExceeded 的超时是如何精确计算的?

通过 Progressing Condition 的 LastUpdateTime + progressDeadlineSeconds - now 计算。每次 Deployment 状态发生变化(Progressing condition 更新)时,LastUpdateTime 会被设置为当前时间。requeueStuckDeployment 计算 after = lastUpdateTime + deadline - now,如果 after < 1s,说明已经超时,立即重新入队检查;如果 after > 0,将任务 enqueueAfter(after) 延迟入队。这样即使控制器没有周期性同步,也能在精确的 deadline 时间点触发超时检测。

Q18. Paused 状态下修改 Deployment 的镜像会发生什么?

镜像变更会入队,但会被 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。

Q19. RevisionHistoryLimit=2 时,实际保留多少个旧 ReplicaSet?

最多保留 2 个满足条件的旧 ReplicaSet(副本数为 0 且不在删除中的)。cleanupDeployment 只删除副本数为 0 的 RS——有 Pod 运行的 RS 不能删除(可能还在提供服务)。即使 RevisionHistoryLimit=2,如果旧 RS 中有一个还在运行副本,实际只会清理其他满足条件的 RS。cleanupDeployment 按 revision 从旧到新排序,优先删除最老的。

Q20. Deployment 的 status.conditions 中有哪些关键条件?它们的意义是什么?

两个核心条件:Progressing(进度)和 Available(可用性)。Progressing 条件的 reason 包括:NewRSAvailable(新版本可用)、ReplicaSetUpdated(版本正在更新)、TimedOutReason(进度超时)、PausedDeployReason(已暂停)。Available 条件的 reason 包括:MinimumReplicasAvailable(满足最小可用数)、MinimumReplicasUnavailable(不满足最小可用数)。kubectl rollout status 和 kubectl describe deployment 的输出都来自这些 conditions。