在 Kubernetes 中运行了一系列控制器来确保集群的当前状态与期望状态保持一致,它们就是 Kubernetes 集群内部的管理控制中心,或者说是"中心大脑"。例如,ReplicaSet 控制器负责维护集群中运行的 Pod 数量;Node 控制器负责监控节点的状态,并在节点出现故障时,执行自动化修复流程,确保集群始终处于期望的工作状态。
前言
上一篇文章我们详细拆解了 Pod 的概念与结构——它是 Kubernetes 的最小调度单元。但单靠 Pod 本身,集群是无法自愈的:Pod 挂了不会自己爬起来,缩容了不会自动补齐,更新版本也要人工介入。
真正让 Kubernetes 实现"期望状态(Desired State)“自动化的,是一整套运行在后台的控制器(Controller)。它们就像一个不知疲倦的监工,时刻观察集群的实际情况,一旦发现实际状态偏离了期望状态,就立刻动手把偏差纠正过来。
本文是控制器系列的开篇,将从宏观的视角梳理:控制器到底是什么、核心思想是什么、它是怎么运作的,以及 Kubernetes 里到底有哪些控制器。
一、什么是控制器
1.1 中心大脑
通俗地说,控制器就是负责让集群"保持期望状态"的进程。Kubernetes 官方给出了一个经典比喻:
控制器可以被理解为一个"空调温控器”——你设定目标温度(期望状态),温控器持续采集当前室温(实际状态),温度低了就加热,高了就制冷,最终让室温稳定在你设定的温度附近。
Kubernetes 集群中运行着一大群控制器,每个控制器只关心一类资源,各司其职:
| 控制器 | 负责"调温"的对象 |
|---|---|
| ReplicaSet / Deployment | 维护 Pod 副本数 |
| StatefulSet | 有状态应用的稳定身份与存储 |
| DaemonSet | 确保每个 Node 上恰好跑一个副本 |
| Job / CronJob | 一次性任务 / 定时任务 |
| Node | 节点健康监控与自动化修复 |
| Namespace / Service / Endpoints | 命名空间、服务发现等全局资源 |
这些控制器共同构成了集群的"中心大脑",把人类运维从"盯着、手修"中解放出来,让集群具备自我调整、自我修复的能力。
核心认知:控制器管理的不是"物理仓体的某一个具体 Pod",而是"集群应该处于的状态"。它不修复死掉的 Pod,而是直接创建新 Pod 来补齐副本数量。
2. 控制器的核心思想:期望状态 vs 实际状态
控制器的整套逻辑,可以浓缩成一句话:
不断拉近"实际状态"与"期望状态"之间的差。
2.1 期望状态(Desired State)
期望状态是用户声明的目标,它被写在资源的 spec 字段里,是一种声明式的意图。
- Deployment 里声明
replicas: 3,期望就是"有 3 个副本"; - 资源清单(YAML)就是我们向集群"许愿"的方式,k8s-资源清单 里有详细说明。
2.2 实际状态(Actual / Current State)
实际状态是集群当下真实呈现出来的结果,由控制器持续从集群中观察得到:
- 现在集群里
app=webapp标签的 Pod 到底有几个; - 某个 Node 的统计量是否异常;
- 某 Service 后面到底绑定了几个可用副本。
2.3 偏差触发补救
两者一旦出现偏差,控制器就要出手:
Desired State(用户声明:3 副本)
│ ─────────── 比对 ───────────
▼ │
Reconcile Loop ◄───────── 观察 Actual State(实际 2 副本)
│
▼
动作:再创建一个 Pod,凑齐 3 个
不论偏差是因为 Pod 崩溃、Node 故障、还是人为误删,控制器的动作逻辑都是一样的——把实际状态拉回期望状态。
关键认知:这正是"声明式"与"命令式"的本质区别。命令式是"你给我做这件事";声明式是"我要这个结果,你不我来"——至于"怎么做",交给控制器去解决。
3. 控制器的运作机制:Reconcile Loop
控制器不是"打了就忘",而是无时无刻盯着集群,这套运转机制在 Kubernetes 里叫 Reconcile Loop(调谐循环)。
一个典型的控制器内部通常由这几部分组成:
3.1 组件拆解
| 组件 | 职责 |
|---|---|
| Watch(监听) | 通过 API Server 的 Informer 监听目标对象,并维护本地缓存 |
| WorkQueue(工作队列) | 收到变化事件后,投递到内部的工作队列 |
| Reconcile(调谐) | 从队列取出对象,比对实际与期望状态,执行相应的创建、更新、删除动作 |
┌───────────────────────────────────────────────┐
│ kube-controller-manager │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │ Informer │────▶│ WorkQueue│────▶│Reconcile│ │
│ └──────────┘ └──────────┘ └───────┘ │
│ ▲ │ │
└────────┼─────────────────────────────────┼─────┘
(监听 etcd / API) (写入 API 进行补救)
- 监听——Informer 从 API Server 同步资源的增删改,并把变化缓存、推送到工作队列;
- 工作队列——把关注的对象(如某个 Deployment 的 key 或副本数的 key)依次排好队;
- 调谐——控制器取出对象,读取期望状态、查询实际状态、计算差距、执行的动作。
因为循环是持续进行的,即使没有新事件发生,控制器也会定期"自检"一遍,防止漏掉任何状态漂移。
3.2 为什么用"调谐"而不是"事件驱动"
早期系统往往用"收到事件才反应"的事件驱动设计,但事件可能丢失、重复或乱序。
而 Reconcile Loop 采用"反复核对“的方式:不管基础形式是什么,只要实际状态和目标不同,就要补一口。这种设计简单、健壮、自愈,这正是 Kubernetes 控制器的灵魂。
4. 集群里到底有哪些控制器
Kubernetes 的控制器分层次、分类型,下面我们从最贴近 Pod 的开始到管理全局的,逐一梳理。
4.1 管理 Pod 副本的控制器(本系列的"主角”)
| 控制器 | 一句话 | 典型场景 |
|---|---|---|
| ReplicaSet | 只保证固定数量的 Pod 运行 | 基础副本管理(被 Deployment 包装使用) |
| Deployment | 无状态应用,支持滚动更新、回滚 | Web 服务、API 服务 |
| StatefulSet | 有状态应用,Pod 有稳定身份和顺序 | 数据库、ZooKeeper、etcd |
| DaemonSet | 每个 Node 恰好运行一个副本 | 日志采集、监控 Agent、节点网络插件 |
| Job | 完成任务后即终止 | 一次性批处理任务 |
| CronJob | 按 cron 表达式定时触发 Job | 定时备份、定时清理 |
这一大类是本文系列的主角,后面的博文会逐个深入:ReplicaSet(如何维护副本数)→ Deployment(如何做滚动更新)→ StatefulSet / DaemonSet / Job / CronJob。
4.2 控制节点与集群资源的控制器
| 控制器 | 职责 |
|---|---|
| Node | 监控节点状态,节点故障时执行自动修复(如驱逐 Pod) |
| Namespace | 维护命名空间的删除回收 |
| Service / Endpoints | 维护 Service 与后端 Endpoints 的绑定关系 |
| EndpointSlice | 生成和维护 EndpointSlice(新版 Endpoints 实现机制) |
| ServiceAccount / Token | PV/存储、服务账号、密钥等附加资源 |
这篇文章只是全景扫描。接下来系列会按"从单体到复杂"的顺序,把管理 Pod 副本的控制器一个个讲透。这一篇,我们把"控制器是什么"先立起来。
5. 控制器的居住地:控制平面
上面说了这么多控制器,它们都住在哪里?答案:kube-controller-manager(控制器管理器),它是 Kubernetes 控制平面(Control Plane)的核心组件之一。
┌────────────────────────────── 控制平面 ──────────────────────────────┐
│ │
│ etcd ───────── kube-apiserver ───────── scheduler │
│ │ │
│ kube-controller-manager ◄────── kube-scheduler │
│ (各类控制器的汇聚地) │
│ │
└────────────────────────────────────────────────────────────────────┘
- kube-apiserver:所有组件的通信中枢,控制器通过它读写集群状态;
- kube-controller-manager:把集群运行中所有内置控制器打包进同一个进程统一运行、统一精简;
- kube-scheduler:决定新 Pod 放到哪个 Node(在 Pod 篇说过,调度以 Pod 为单位)。
默认情况下,kube-controller-manager 会把内置控制器聚合在一起运行,每个控制器本质上都是一个 Reconcile Loop,通过 API 与集群交互。
二、ReplicationController 和 ReplicaSet
实验环境:本文(以及整个 Pod 控制器系列)的所有实验文件都会统一放在 master 节点的
/root/5目录下,与之前「资源清单」实验用的/root/4目录同级。先创建它:
mkdir -p /root/5
cd /root/5
ReplicationController(RC)用来确保容器应用的副本数始终保持在用户定义的副本数,即如果有容器异常退出,会自动创建新的 Pod 来替代;而如果异常多出来的容器也会自动回收。
在新版本的 Kubernetes 中建议使用 ReplicaSet 来取代 ReplicationController。ReplicaSet 跟 ReplicationController 没有本质的不同,只是名字不一样,并且 ReplicaSet 支持集合式的 selector。
实验一:ReplicationController 副本保持
目标:验证 RC 会维持副本数不变——Pod 被删后自动重建,多出来的 Pod 会被回收。
Step 1:创建 RC
# 1.rc.yaml
apiVersion: v1
kind: ReplicationController
metadata:
name: rc-demo
spec:
replicas: 3
selector:
app: rc-demo
template:
metadata:
labels:
app: rc-demo
spec:
containers:
- name: rc-demo-container
image: wangyanglinux/myapp:v1.0
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
Step 2:部署并观察
kubectl apply -f 1.rc.yaml
kubectl get rc
kubectl get pods -o wide
NAME DESIRED CURRENT READY AGE
rc-demo 3 3 3 10s
NAME READY STATUS RESTARTS AGE
rc-demo-9x7kl 1/1 Running 0 10s
rc-demo-b3ndz 1/1 Running 0 10s
rc-demo-m4tqz 1/1 Running 0 10s
RC 根据 template 创建了 3 个 Pod,Pod 名以 rc-demo- 为前缀,后面是随机字符串。
Step 3:手动删除一个 Pod,观察自动重建
kubectl delete pod rc-demo-9x7kl
kubectl get pods -o wide
删除后 RC 立刻创建一个新 Pod 补齐到 3 个,新 Pod 的名字和 IP 都变了:
NAME READY STATUS RESTARTS AGE
rc-demo-b3ndz 1/1 Running 0 2m
rc-demo-m4tqz 1/1 Running 0 2m
rc-demo-k8jpq 1/1 Running 0 3s ← 新建的
Step 4:扩容 / 缩容
# 扩容到 5 个
kubectl scale rc rc-demo --replicas=5
kubectl get pods -o wide
# 观察:新增 2 个 Pod
# 缩容到 2 个
kubectl scale rc rc-demo --replicas=2
kubectl get pods -o wide
# 观察:多余的 Pod 被回收
缩容时谁先被删? 缩容时 Kubernetes 并不是随机挑 Pod 删除,而是按一套优先级排序,大致顺序为:未调度的 Pod → Pending 状态的 Pod → 所在节点上同组 Pod 数量更多的(尽量打散)→ 创建时间越晚(越"年轻")的 Pod → 重启次数更多的。
所以在其他条件相同的情况下,缩容会优先删除较年轻的 Pod,保留老 Pod。这样设计是因为老 Pod 通常已稳定运行、缓存已热,优先保留它们有助于减少缩容对服务的抖动。
Step 5:验证「子集匹配」——改标签让 Pod 脱离/回归 RC 管理
RC 的 selector 是等值匹配,它只认自己 template 里带 app=rc-demo 标签的 Pod。我们通过 kubectl label --overwrite 修改 Pod 的标签,观察 RC 如何根据标签归属来调整副本:
# 当前 3 个 Pod 都带 app=rc-demo 标签
kubectl get pods -L app
先把其中一个 Pod 的标签从 app=rc-demo 改成 app=other:
kubectl label pod rc-demo-9x7kl app=other --overwrite
kubectl get pods -L app
Pod rc-demo-9x7kl 不再匹配 RC 的 selector,被 RC"踢出"它的管理范围。RC 发现实际匹配数(2)小于期望数(3),立刻新建一个带 app=rc-demo 标签的 Pod 补齐,Pod 总数保持 3 不变:
NAME READY STATUS RESTARTS AGE APP
rc-demo-b3ndz 1/1 Running 0 2m rc-demo
rc-demo-m4tqz 1/1 Running 0 2m rc-demo
rc-demo-9x7kl 1/1 Running 0 2m other ← 标签被改了,脱离了 RC
rc-demo-k8jpq 1/1 Running 0 3s rc-demo ← RC 新建补齐
再把标签改回来:
kubectl label pod rc-demo-9x7kl app=rc-demo --overwrite
kubectl get pods -L app
标签改回 app=rc-demo 后,rc-demo-9x7kl 重新回归 RC 的标签子集,此时实际数(4)超过期望数(3),RC 会回收掉其中一个,让副本数回到 3。
结论:RC 只认标签子集,不认"谁是它创建的"。标签匹配就纳入管理,不匹配就被踢出。所以通过改标签,就能让同一个 Pod 进进出出,RC 永远把副本数拉回期望值。
清理:kubectl delete rc rc-demo
ReplicationController 与 ReplicaSet 的区别
| 特性 | ReplicationController | ReplicaSet |
|---|---|---|
| 镜像路径 | apiVersion: v1,kind: ReplicationController | apiVersion: apps/v1,kind: ReplicaSet |
| Selector 匹配 | 只支持等值匹配 | 支持等值 + 集合匹配 |
| label 不一致 | selector 与 template 必须一致,否则报错 | 允许 selector 与 template 不同 |
| 状态 | 已废弃,但仍可用 | 当前推荐(被 Deployment 包装使用) |
关键差异:RC 的
selector可以省略(默认取 template 的 labels),而 ReplicaSet 不强制要求 selector 与 template 的 labels 完全一致。这是 RC 被 RS 取代的核心理由。
实验二:ReplicaSet 副本保持
目标:演示 ReplicaSet 可以实现与 ReplicationController 基本一致的效果——同样能维持副本数、Pod 被删自动重建、支持 kubectl scale 扩缩容。整套实验流程与实验一(RC)几乎一一对应,唯一的差别在于:RS 的 selector 更强大,支持 matchLabels(多标签精确匹配)和 matchExpressions(集合式表达式)。本实验通过一个多标签 matchLabels 的例子来体现这一点。
Step 1:创建 ReplicaSet
# 2.rs.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: rs-ml-demo
spec:
replicas: 3
selector:
matchLabels:
app: rs-ml-demo
domain: xinxianghf
template:
metadata:
labels:
app: rs-ml-demo
domain: xinxianghf
version: v1
spec:
containers:
- name: rs-ml-demo-container
image: wangyanglinux/myapp:v1.0
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
和 RC 对比一下:
- 结构几乎一样,只是
kind从ReplicationController换成了ReplicaSet,apiVersion从v1换成了apps/v1;- RC 的 selector 只能是单键值等值匹配,且可以省略;RS 必须显式声明 selector,可以写成
matchLabels(本例)或matchExpressions;- 本例
matchLabels用了两个标签(app+domain),template 里还多了一个version: v1——RS 允许 selector 是 template labels 的子集。
Step 2:部署并观察(对应实验一 Step 2)
kubectl apply -f 2.rs.yaml
kubectl get rs
kubectl get pods -o wide --show-labels
# 观察:副本数为 3,每个 Pod 都带着 app / domain / version 三个标签
Step 3:删除 Pod,观察自动重建(对应实验一 Step 3)
kubectl delete pod <pod-name>
kubectl get pods -o wide
# 观察:新的 Pod 立即被 RS 补齐 —— 与 RC 表现完全一致
Step 4:扩容 / 缩容(对应实验一 Step 4)
kubectl scale rs rs-ml-demo --replicas=5
kubectl get pods -o wide
# 观察:新增 2 个 Pod
kubectl scale rs rs-ml-demo --replicas=2
kubectl get pods -o wide
# 观察:多余的 Pod 被回收(同样优先删除较年轻的 Pod)
Step 5:体验 RS 独有的集合式 selector
这一步是 RS 相对于 RC 的增量能力:可以用 in / notin / exists 这样的集合表达式来筛选 Pod。
kubectl get pods -l 'app in (rs-ml-demo), domain in (xinxianghf)'
# 观察:所有由该 RS 创建的 Pod 都能被集合式表达式选中
清理:kubectl delete rs rs-ml-demo
小结:从 Step 2 到 Step 4,你会看到 RS 和 RC 的使用体验几乎没有差别,这就是"RS 用来取代 RC"的底气;而 Step 5 展示的集合式 selector,就是 RS 相较于 RC 的关键优势。
selector.matchExpressions
RS 在标签选择器上,除了可以定义键值对的选择形式(matchLabels),还支持 matchExpressions 字段,可以提供多种选择。目前支持的操作包括:
- In:label 的值在某个列表中
- NotIn:label 的值不在某个列表中
- Exists:某个 label 存在
- DoesNotExist:某个 label 不存在
RS 控制器功能与 RC 控制器类似,但是多了标签选择的运算方式。
实验三:matchExpressions 集合式 selector
目标:演示 matchExpressions 的 Exists 操作符——只要 Pod 带有某个 key 的标签就会被选中,不限定具体 value。
Step 1:创建 ReplicaSet
# 3.rs.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: rs-me-exists-demo
spec:
selector:
matchExpressions:
- key: app
operator: Exists
template:
metadata:
labels:
app: spring-k8s
spec:
containers:
- name: rs-me-exists-demo-container
image: wangyanglinux/myapp:v1.0
ports:
- containerPort: 80
解读:selector 使用
matchExpressions,含义是"只要 Pod 存在app这个标签 key 就算匹配"——这就是 RS 相比 RC 的集合式 selector 能力。注意这里没有指定replicas,默认为 1。
Step 2:部署并观察
kubectl apply -f 3.rs.yaml
kubectl get rs
kubectl get pods --show-labels
⚠️ 关于"接管 Pod"的准确规则:
控制器创建后确实会尝试根据 selector 认领(adopt)标签匹配的 Pod,把它们当作自己管理的副本——但前提是这些 Pod 是"无主"的,即它们的
metadata.ownerReferences里没有任何controller: true的条目。也就是说:
- 如果集群里存在带
app=xxx标签、且没有控制器归属的孤儿 Pod(例如手动kubectl run或 YAML 直接创建的裸 Pod),这个rs-me-exists-demo会把它们"认领"过来,写入自己的ownerReferences,并把它们算作副本。- 如果这些 Pod 已经被其它控制器(比如上一节没清理的
rs-ml-demo)接管,那么无论标签是否匹配,新控制器都不会抢走它们。一个 Pod 在任一时刻只能有一个控制器 owner。因此使用
matchExpressions这种范围较宽的 selector 时,真正需要提防的是集群里的裸 Pod(尤其是共享标签 key 的裸 Pod),而不是别的控制器管理的 Pod。
Step 3:验证 Exists 只看 key、不看 value
先记下当前 Pod 名字,然后把它的 app 标签值改掉(key 保留):
POD=$(kubectl get pods -l app -o jsonpath='{.items[0].metadata.name}')
# 把 app 的值从 spring-k8s 改成任意其它值
kubectl label pod $POD app=whatever-value --overwrite
kubectl get pods --show-labels
kubectl get rs rs-me-exists-demo
# 观察:Pod 数量仍为 1,RS 没有新建 Pod,被改标签的 Pod 依然归 RS 管理
确认它的 owner 还在:
kubectl get pod $POD -o jsonpath='{.metadata.ownerReferences}'
# 输出中仍能看到 kind:ReplicaSet name:rs-me-exists-demo controller:true
结论:value 变了但 Pod 没有脱离管理,证明
Exists只判断app这个 key 是否存在,与具体取值无关。
Step 4:验证 key 消失后 Pod 脱离管理
# 彻底删掉 app 这个标签(注意末尾的减号)
kubectl label pod $POD app-
kubectl get pods --show-labels
# 观察:
# 1. 被删标签的 Pod 仍然存在,但 ownerReferences 已被清空,变成孤儿 Pod
# 2. RS 发现可用副本不足,立即新建了一个带 app=spring-k8s 的 Pod
kubectl get pod $POD -o jsonpath='{.metadata.ownerReferences}'
# 输出为空 —— 已被 RS 释放
结论:key 不存在即不匹配,Pod 被释放。这与 Step 3 形成对照,完整验证了
Exists的语义。
清理:kubectl delete rs rs-me-exists-demo (别忘了顺手删掉那个脱离管理的孤儿 Pod)
实验四:matchExpressions 的 In 操作符
目标:演示 matchExpressions 的 In 操作符——标签 value 只要在给定列表中就算匹配。
Step 1:创建 ReplicaSet
# 4.rs.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: rs-me-in-demo
spec:
selector:
matchExpressions:
- key: app
operator: In
values:
- spring-k8s
- hahahah
template:
metadata:
labels:
app: spring-k8s
spec:
containers:
- name: rs-me-in-demo-container
image: wangyanglinux/myapp:v1.0
ports:
- containerPort: 80
解读:
In操作符表示"app 标签的值只要是spring-k8s或hahahah之一就匹配"。这里 template 的标签是app: spring-k8s,满足 selector 条件。如果集群中有其它无主 Pod 的app值恰好也是这两个之一,同样会被该 RS 认领。对比上一个实验的
Exists(只看 key 存不存在,不关心 value),In既检查 key 也检查 value 是否在白名单内,控制粒度更细。
Step 2:部署并观察
kubectl apply -f 4.rs.yaml
kubectl get rs
kubectl get pods --show-labels
Step 3:验证 In 的值白名单匹配
把 Pod 的 app 标签改成 values 列表里的另一个值(hahahah),验证 Pod 不会脱离管理:
POD=$(kubectl get pods -l app=spring-k8s -o jsonpath='{.items[0].metadata.name}')
# 改成 values 列表中的 hahahah
kubectl label pod $POD app=hahahah --overwrite
kubectl get pods --show-labels
kubectl get rs rs-me-in-demo
# 观察:Pod 仍归 RS 管理,副本数没变,RS 没有创建新 Pod
# 因为 hahahah 在 In 的 values 列表里,依然满足 selector
Step 4:验证值不在列表中时 Pod 脱离管理
# 把 app 改成一个不在 values 列表中的值
kubectl label pod $POD app=not-in-list --overwrite
kubectl get pods --show-labels
kubectl get rs rs-me-in-demo
# 观察:
# 1. 被改标签的 Pod 脱离了 RS 管理(ownerReferences 被清空)
# 2. RS 发现副本不足,立即新建了一个带 app=spring-k8s 的 Pod
结论:
In操作符只认 values 列表里的值——改成列表内的hahahah依然匹配,改成列表外的值就不匹配了。对比实验三的Exists(只看 key 存不存在),In对 value 有明确的白名单约束。
清理:kubectl delete rs rs-me-in-demo(顺手清理孤儿 Pod)
三、Deployment
Deployment 为 Pod 和 ReplicaSet 提供了一个声明式定义(declarative)方法,用来替代以前的 ReplicationController 来方便地管理应用。典型的应用场景包括:
- 定义 Deployment 来创建 Pod 和 ReplicaSet
- 滚动升级和回滚应用
- 扩容和缩容
- 暂停和继续 Deployment
Deployment 本身不直接创建 Pod,而是通过管理 ReplicaSet 来间接管理 Pod。下图展示了 Deployment 与 ReplicaSet、Pod 之间的层级关系——在滚动更新过程中,Deployment 会同时持有 NewReplicaSet(新版本,正在扩容)和 OldReplicaSet(旧版本,正在缩容),直到旧版本 Pod 全部下线、新版本 Pod 全部就绪,更新才算完成。

声明式与命令式
声明式(Declarative)是对最终结果的陈述,只表明意图,不关心实现过程。在 Kubernetes 里,声明式就是说:“集群里应该有一个包含三个 Pod 的 ReplicaSet”——至于当前是 0 个还是 5 个、怎么创建、怎么删多的,都交给 Kubernetes 自己想办法拉齐。
命令式(Imperative)则是直接下达命令,主动且明确:"创建一个包含三个 Pod 的 ReplicaSet"、"删除这个 Deployment"、"把副本改成 10"。命令式关注动作,声明式关注结果。
Kubernetes 里最常见的这几条命令,正好覆盖了两种风格:
| 风格 | 命令 | 语义 | 资源已存在时的表现 | 资源不存在时的表现 |
|---|---|---|---|---|
| 命令式 | kubectl create -f xxx.yaml | “立刻创建这个资源” | ❌ 报错 AlreadyExists | ✅ 创建 |
| 命令式 | kubectl replace -f xxx.yaml | “用这份 YAML 完整替换现有资源” | ✅ 整体替换(YAML 里没写的字段会丢) | ❌ 报错 NotFound |
| 命令式 | kubectl delete -f xxx.yaml | “把这个资源删掉” | ✅ 删除 | ❌ 报错(可加 --ignore-not-found) |
| 声明式 | kubectl apply -f xxx.yaml | “确保集群状态与这个文件一致” | ✅ 三方合并更新 | ✅ 自动创建 |
| 辅助 | kubectl diff -f xxx.yaml | “预览 apply 会改哪些字段” | ✅ 只读预览 | ✅ 只读预览 |
几个关键差别单独拎出来说:
create只管创建,不管更新。同一份 YAML 已经 apply 过一次,再执行kubectl create -f会直接报Error from server (AlreadyExists)——它不会帮你更新,也不会幂等地跳过,就是简单粗暴地失败。所以脚本、CI/CD 里几乎没人再用create -f,都用apply -f:不存在就创建,存在就合并更新,天然幂等。replace只管更新,不管创建。而且是整体替换,YAML 里没写的字段会按默认值重建(比如没写replicas就会掉回 1,实验五 Step 5 已经踩过这个坑)。资源不存在时replace会报NotFound,除非加--force(内部实现是先 delete 再 create,会短暂中断服务,慎用)。delete是命令式删除。可以按文件删(kubectl delete -f xxx.yaml),也可以按名字删(kubectl delete deployment myapp)。默认删不存在的资源会报错,加--ignore-not-found可以变得幂等;对 Deployment 这类带子资源的对象,删除时会级联删掉它管理的 ReplicaSet 和 Pod。apply是声明式的一把梭。它会在资源上写一个kubectl.kubernetes.io/last-applied-configuration注解,下次 apply 时用「上一次 YAML + 当前集群状态 + 这次 YAML」做三方合并(three-way merge),从而准确算出哪些字段该改、哪些该保留,避免误伤运维手动加的 annotation 或 HPA 改过的 replicas。
一句话结论:命令式适合一次性的手工操作和排错(
create、delete、scale、edit),声明式适合把 YAML 纳入版本控制、反复部署(apply)。日常首选apply——它幂等、可重复、能创建也能更新,正是 GitOps 的基石。create/replace/delete更多是补充手段,遇到"资源已存在"这种尴尬场景时,apply会不动声色地帮你合并,而create只会甩你一脸AlreadyExists。
replace 与 apply 对比
虽然 replace 和 apply 都能"用一个 YAML 文件更新现有资源",但两者的更新语义完全不同,理解差异对生产环境很重要。
1. 替换方式
kubectl replace:使用新的配置完全替换掉现有资源的配置。这意味着新配置将覆盖现有资源的所有字段和属性,包括未指定的字段,会导致整个资源的替换。kubectl apply:使用新的配置部分地更新现有资源的配置。它会根据提供的配置文件或参数,只更新与新配置中不同的部分,而不会覆盖整个资源的配置。
2. 字段级别的更新
kubectl replace:由于是完全替换,所以会覆盖所有字段和属性,无论是否在新配置中指定。kubectl apply:只更新与新配置中不同的字段和属性,保留未指定的字段不受影响。
3. 部分更新
kubectl replace:不支持部分更新,它会替换整个资源的配置。kubectl apply:支持部分更新,只会更新新配置中发生变化的部分,保留未指定的部分不受影响。
4. 与其他配置的影响
kubectl replace:不考虑其他资源配置的状态,直接替换资源的配置。kubectl apply:可以结合使用-f或-k参数,从文件或目录中读取多个资源配置,并根据当前集群中的资源状态进行更新。
一句话总结:
| 维度 | kubectl replace | kubectl apply |
|---|---|---|
| 更新粒度 | 整体替换 | 字段级合并 |
| 未指定字段 | 会被清空/丢失 | 保留不动 |
| 是否需要资源已存在 | 需要 | 不需要(不存在则创建) |
| 是否记录历史 | 不记录 | 记录 last-applied-configuration |
| 典型场景 | 完全接管、明确知道所有字段 | 日常声明式管理、GitOps |
⚠️ 踩坑提示:如果用
kubectl edit或 kubectl 别的命令给某个资源额外加了字段(比如运维手动加了 annotation),此后再用kubectl replace -f old.yaml会把这些字段丢失;而kubectl apply借助三方合并机制,通常不会误伤这些"文件里没写"的字段。
实验五:声明式与命令式
目标:通过 scale(命令式)、diff(变更预览)、apply(声明式)、replace(整体替换)四种方式操作同一个 Deployment,直观感受它们在副本数、镜像更新、字段保留上的差异。
Step 1:创建 Deployment
# 5.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myapp-deploy
name: myapp-deploy
spec:
selector:
matchLabels:
app: myapp-deploy
template:
metadata:
labels:
app: myapp-deploy
spec:
containers:
- image: wangyanglinux/myapp:v1.0
name: myapp
YAML 里没有写
replicas,默认就是 1 个副本。
kubectl apply -f 5.deployment.yaml
kubectl get deploy myapp-deploy
kubectl get rs
kubectl get pods -o wide -l app=myapp-deploy
NAME READY UP-TO-DATE AVAILABLE AGE
myapp-deploy 1/1 1 1 10s
观察:Deployment 自动创建了 1 个 ReplicaSet,下面挂着 1 个 Pod。
Step 2:命令式扩容——scale 到 10 个
kubectl scale deployment myapp-deploy --replicas=10
kubectl get deploy myapp-deploy
kubectl get pods -l app=myapp-deploy
NAME READY UP-TO-DATE AVAILABLE AGE
myapp-deploy 10/10 10 10 1m
观察:副本数立刻变成 10,但
5.deployment.yaml文件内容并没有变。这就是典型的「命令式」——直接告诉集群「现在要 10 个」,而不是改 YAML 再提交。
此时 YAML 文件仍是 v1.0、也没写 replicas: 10,用 diff 预览一下 apply 会改什么:
kubectl diff -f 5.deployment.yaml
观察:没有任何输出(命令 exit code 为 0)。说明 diff 认为「按当前 YAML 做 apply」不会改变集群状态;scale 改过的
replicas: 10不会被 apply 碰,diff 也不会提示要把副本改回去。
Step 3:声明式更新——改镜像 v1.0 → v2.0,先 diff 再 apply
把 5.deployment.yaml 里的镜像改成 wangyanglinux/myapp:v2.0,先预览、再提交:
kubectl diff -f 5.deployment.yaml
diff -u -N /tmp/LIVE-... /tmp/MERGED-...
--- /tmp/LIVE-...deployment.apps/myapp-deploy
+++ /tmp/MERGED-...deployment.apps/myapp-deploy
@@ ... @@
containers:
- - image: wangyanglinux/myapp:v1.0
+ - image: wangyanglinux/myapp:v2.0
name: myapp
观察:
- diff 用
+/-标出 apply 后会变的字段,这里只有镜像版本;- 没有出现 replicas 相关 diff —— 再次证明 apply/diff 都不会动 scale 过的副本数;
- diff 无输出 = 无变更;有输出 = apply 会改这些字段(但还没真正执行)。
确认无误后再 apply:
kubectl apply -f 5.deployment.yaml
kubectl rollout status deployment/myapp-deploy
kubectl get rs -l app=myapp-deploy
kubectl get pods -l app=myapp-deploy -o wide
观察:
- apply 触发了滚动更新,Pod 镜像逐步从 v1.0 切到 v2.0;
- 会出现新的 ReplicaSet(v2.0),旧的 ReplicaSet(v1.0)被缩到 0;
- 副本数仍然是 10 —— 因为 YAML 里没写 replicas,apply 不会把 scale 改过的值覆盖回去。
Step 4:查看 Pod 并 curl 验证版本
kubectl get pods -l app=myapp-deploy -o wide
任选一个 Running 状态的 Pod IP,在集群内 curl(master 或任意 Node 上均可):
curl <Pod-IP>
观察:v2.0 的 myapp 会返回对应版本的页面内容。多 curl 几个 Pod,确认都已经切到 v2.0。
也可以直接看镜像版本:
kubectl get pods -l app=myapp-deploy -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'
Step 5:整体替换——改镜像 v2.0 → v3.0,先看 diff 再用 replace
把 5.deployment.yaml 里的镜像改成 wangyanglinux/myapp:v3.0,同样先跑 diff:
kubectl diff -f 5.deployment.yaml
diff -u -N /tmp/LIVE-... /tmp/MERGED-...
--- /tmp/LIVE-...deployment.apps/myapp-deploy
+++ /tmp/MERGED-...deployment.apps/myapp-deploy
@@ ... @@
containers:
- - image: wangyanglinux/myapp:v2.0
+ - image: wangyanglinux/myapp:v3.0
name: myapp
观察:diff 仍然只显示镜像变化,完全没有提示 replicas 会从 10 变成 1。这是因为
kubectl diff预览的是 apply 的合并语义,不是 replace 的整体替换语义。
再用 replace 真正执行:
kubectl replace -f 5.deployment.yaml
kubectl rollout status deployment/myapp-deploy
kubectl get deploy myapp-deploy
kubectl get pods -l app=myapp-deploy
观察:
- 镜像同样会滚动更新到 v3.0;
- 但 replicas 很可能从 10 掉回 1 —— 因为 replace 是用 YAML「整体替换」spec,YAML 里没写 replicas 就等于声明
replicas: 1(默认值);- 多余的 Pod 会被回收,只剩 1 个。
Step 6:对比 apply、diff 与 replace 的区别
把几次操作后的关键差异汇总:
| 对比项 | Step 3 kubectl diff + apply(v1.0→v2.0) | Step 5 kubectl diff + replace(v2.0→v3.0) |
|---|---|---|
| 预览方式 | diff 只显示镜像变更 | diff 同样只显示镜像变更 |
| 实际执行 | apply 字段级合并 | replace 整体替换 |
| 副本数 | 保持 10(与 diff 预览一致) | 回到 1(diff 没预告,replace 才触发) |
| 镜像更新 | 触发滚动更新 ✅ | 触发滚动更新 ✅ |
| 未写在 YAML 里的字段 | 保留不动 | 按 YAML 重建,未写的字段用默认值或丢失 |
| 命令 | 作用 | 是否改集群 | 语义 |
|---|---|---|---|
kubectl diff -f | 预览 apply 会改什么 | ❌ 只读 | 与 apply 一致的三方合并 |
kubectl apply -f | 声明式提交变更 | ✅ | 字段级合并 |
kubectl replace -f | 整体替换资源 | ✅ | 完整替换,diff 预览不了 |
再用命令验证一下当前状态:
# 看 Deployment 当前副本数和镜像
kubectl get deploy myapp-deploy -o yaml | grep -E 'replicas:|image:'
# 看 ReplicaSet 历史(apply 留下的旧 RS vs replace 后的新 RS)
kubectl get rs -l app=myapp-deploy
# 看 apply 留下的 last-applied-configuration 注解
kubectl get deploy myapp-deploy -o yaml | grep -A2 last-applied-configuration
结论:
kubectl scale是命令式改副本,快但不改 YAML,集群状态和文件容易不一致;kubectl diff是 apply 的「干跑」预览,只读、不改集群,适合提交前确认变更范围;但它遵循 apply 语义,预览不了 replace 的副作用(比如 replicas 被重置);kubectl apply是声明式合并,改 YAML 后提交,不会误伤 YAML 里没写的字段(比如 replicas);kubectl replace是整体替换,YAML 里没写的字段会被默认值覆盖,scale 过的副本数很容易丢。推荐工作流:改 YAML →
kubectl diff确认 →kubectl apply提交;只有在你明确知道 YAML 是资源的「完整真相」时才用replace。
清理:kubectl delete deployment myapp-deploy
Deployment 常用命令
上面实验五中我们已经用过 apply、scale、diff、replace 这些命令。除此之外,Deployment 在日常运维中还有一批高频命令,掌握它们能覆盖 90% 的场景:
# 创建 Deployment,并记录本次变更的命令到 revision 历史中
kubectl create -f deployment.yaml --record
--record参数会把本次执行的命令写入 Deployment 的kubernetes.io/change-cause注解,方便后续通过kubectl rollout history查看每次 revision 的变化来源。新版本 kubectl 中该参数已被标记为 deprecated,可以改用kubectl annotate deployment <name> kubernetes.io/change-cause="..."手动打注解。
# 手动扩缩容:把 deployment-demo 副本数设置为 10
kubectl scale deployment deployment-demo --replicas=10
# 自动扩缩容(HPA):CPU 使用率超过 80% 时扩容,副本数在 10~15 之间浮动
kubectl autoscale deployment deployment-demo --min=10 --max=15 --cpu-percent=80
autoscale会自动创建一个 HorizontalPodAutoscaler(HPA)对象。前提是集群中已部署 metrics-server,否则 HPA 拿不到 CPU 指标,无法工作。
# 更新镜像:把 deployment-demo 中名为 deployment-demo-container 的容器切到 v2.0
kubectl set image deployment/deployment-demo deployment-demo-container=wangyanglinux/myapp:v2.0
set image会触发滚动更新,效果等价于改 YAML 里的image字段后kubectl apply,但更适合脚本化/流水线场景。命令中的第一个deployment-demo-container是容器名(对应spec.template.spec.containers[*].name),不是 Deployment 名。
# 回滚:撤销上一次变更,回到前一个 revision
kubectl rollout undo deployment/deployment-demo
# 回滚到指定 revision
kubectl rollout undo deployment/deployment-demo --to-revision=2
# 查看历史 revision
kubectl rollout history deployment/deployment-demo
# 查看滚动更新的实时进度
kubectl rollout status deployment/deployment-demo
# 暂停 / 恢复滚动更新(用于批量改多个字段但只想触发一次滚动)
kubectl rollout pause deployment/deployment-demo
kubectl rollout resume deployment/deployment-demo
一句话速查表:
| 命令 | 作用 |
|---|---|
kubectl create -f xxx.yaml --record | 创建资源并记录变更来源 |
kubectl scale deployment/xxx --replicas=N | 手动扩缩容 |
kubectl autoscale deployment/xxx --min --max --cpu-percent | 基于 CPU 自动扩缩容(HPA) |
kubectl set image deployment/xxx container=image:tag | 更新镜像触发滚动升级 |
kubectl rollout undo deployment/xxx | 回滚到上一版本 |
kubectl rollout history deployment/xxx | 查看版本历史 |
kubectl rollout status deployment/xxx | 查看滚动更新进度 |
kubectl rollout pause/resume deployment/xxx | 暂停 / 恢复滚动更新 |
Deployment 更新策略
Deployment 支持两种更新策略,通过 spec.strategy.type 字段设置:
| 策略 | 说明 |
|---|---|
| RollingUpdate(默认) | 滚动更新——逐步用新版 Pod 替换旧版 Pod,保证服务不中断 |
| Recreate | 重建——先把所有旧 Pod 全部杀掉,再一次性创建新 Pod(会有短暂停服) |
RollingUpdate 的核心参数
滚动更新的节奏由两个参数控制:maxUnavailable(最大不可用)和 maxSurge(最大超量)。
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25% # 更新过程中允许不可用的 Pod 占期望数的比例(或绝对数)
maxSurge: 25% # 更新过程中允许超出期望数的 Pod 占期望数的比例(或绝对数)
通俗解释:
- maxUnavailable:升级过程中,允许最多有多少个 Pod 处于不可用状态(down)。默认 25%,表示任何时刻至少有 75% 的 Pod 在正常提供服务。
- maxSurge:升级过程中,允许最多临时多创建多少个 Pod(超出期望副本数)。默认 25%,表示滚动期间 Pod 总数最多比期望值多 25%。
两个参数可以写百分比(如
25%),也可以写绝对数(如1)。百分比会按replicas向上取整。
举例:假设 replicas: 4,使用默认的 maxUnavailable: 25% + maxSurge: 25%:
- maxUnavailable = ceil(4 × 25%) = 1 → 更新中最多允许 1 个 Pod 不可用,即最少有 3 个可用;
- maxSurge = ceil(4 × 25%) = 1 → 更新中最多允许 Pod 总数达到 5 个。
所以滚动过程中,集群里同时存活的 Pod 数量在 3 ~ 5 之间波动:先创建 1 个新版 Pod(总数 5),待其 Ready 后删 1 个旧版(总数 4,可用 3+1),以此滚动推进,直到全部换成新版。
更新策略的演进
早期版本的 Kubernetes 中,maxUnavailable 和 maxSurge 的默认值都是 1(绝对值),即滚动时一次只多一个、少一个,更新速度较慢。后来的版本把默认值改成了 25%(百分比),对于大副本数的 Deployment 滚动速度明显更快——比如 100 个副本,一次可以同时滚动 25 个,而不是一个一个来。当前版本可以用下面的命令确认实际生效的默认值:
kubectl get deploy <name> -o jsonpath='{.spec.strategy}' && echo
# 输出示例:{"rollingUpdate":{"maxSurge":"25%","maxUnavailable":"25%"},"type":"RollingUpdate"}
Recreate 策略
如果业务不允许新旧版本同时运行(比如数据库 schema 不兼容、或应用自身不支持多版本共存),可以使用 Recreate:
spec:
strategy:
type: Recreate
Recreate 的行为非常简单:先把所有旧版 Pod 全部 Terminate → 等待全部退出 → 一次性创建所有新版 Pod。代价是更新期间服务完全不可用,适合开发/测试环境或对可用性要求不高的批处理任务。
滚动更新的完整流程
以 replicas: 3、maxSurge: 1、maxUnavailable: 1 为例,把镜像从 v1 更新到 v2 时,Deployment 控制器的内部动作:
时间线 →
Step 1:创建新 RS(v2),scale up 新 RS 到 1
旧 RS(v1): 3 pods 新 RS(v2): 1 pod 总数=4(允许 surge 1)
Step 2:新 Pod Ready 后,scale down 旧 RS 到 2
旧 RS(v1): 2 pods 新 RS(v2): 1 pod 总数=3
Step 3:scale up 新 RS 到 2
旧 RS(v1): 2 pods 新 RS(v2): 2 pods 总数=4
Step 4:新 Pod Ready 后,scale down 旧 RS 到 1
旧 RS(v1): 1 pod 新 RS(v2): 2 pods 总数=3
Step 5:scale up 新 RS 到 3
旧 RS(v1): 1 pod 新 RS(v2): 3 pods 总数=4
Step 6:新 Pod Ready 后,scale down 旧 RS 到 0
旧 RS(v1): 0 pods 新 RS(v2): 3 pods 总数=3 ✅ 完成
整个过程中,可用 Pod 数始终 ≥ 2(
replicas - maxUnavailable = 3 - 1 = 2),Pod 总数始终 ≤ 4(replicas + maxSurge = 3 + 1 = 4),在不中断服务的前提下完成了版本切换。
金丝雀部署(Canary Deployment)
名字的由来
金丝雀部署的名字灵感来源于 17 世纪英国矿井工人使用金丝雀检测瓦斯的传统方法。金丝雀对瓦斯这种气体十分敏感,空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然人类毫无察觉,金丝雀却早已毒发身亡。在采矿设备相对简陋的年代,工人们每次下井都会带上一只金丝雀作为"瓦斯检测指标",一旦金丝雀出现异常,就立刻紧急撤离。
把这个隐喻搬到软件发布上:新版本就是那只"金丝雀"——先让它去接触一小部分真实流量,如果它"死了"(出错、指标异常),受影响的只是极少数用户,我们立刻回滚即可;如果它"活得好好的",再逐步扩大新版本的比例,最终全量。
核心思想
金丝雀部署的核心思想是:在真实运行环境中,先用一小部分用户或流量去测试新版本,而大部分流量仍然使用旧版本。 通过对新版本进行有限范围的实时测试和监控,可以:
- 及早发现潜在问题(新版本的 bug、性能退化、内存泄漏等);
- 把故障影响面控制在极小范围内,减少对整个系统的冲击;
- 用真实流量验证新版本,比任何测试环境都更接近生产实况。
它和前面滚动更新(RollingUpdate)的区别在于:滚动更新是无条件、自动地把所有 Pod 逐步换成新版;而金丝雀部署会先停在"新旧共存"的中间状态,等人工观察确认新版没问题后,再决定是继续放量还是回滚。
100% 流量
│
▼
┌───────────────────────────────┐
│ Service(selector: app=xxx) │
│ 同一组 label 同时选中新旧 Pod │
└───────────────┬───────────────┘
┌─────────┴─────────┐
~90% │ │ ~10%
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 旧版 v1 │ │ 新版 v2 │ ← 金丝雀
│ 9 pods │ │ 1 pod │
└───────────────┘ └───────────────┘
Kubernetes 原生的 Service 是按 Pod 数量做近似的负载均衡(并不能精确按百分比切流量)。要实现精确的流量比例,需要配合 Ingress、Service Mesh(如 Istio)等更上层的组件。仅用 Deployment + Service 时,金丝雀的"流量占比"约等于"金丝雀 Pod 数 ÷ 总 Pod 数"。
用 patch 命令实现金丝雀部署
kubectl patch 可以直接修改运行中资源的部分字段,而不需要重新提交整个 YAML。它的核心用法是:
kubectl patch <资源类型> <资源名> -p '<JSON 片段>'
-p(--patch)后面跟一段 JSON,Kubernetes 会把它合并到资源现有的定义中,只覆盖你写到的字段,其余保持不变。金丝雀部署正是借助 patch 来"更新镜像 + 立刻暂停",把发布卡在新旧共存的中间状态。
准备工作:基础 Deployment
先准备一个 10 副本的 Deployment,把它作为演示金丝雀的基础清单:
# 6.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: deployment-demo
name: deployment-demo
spec:
replicas: 10
selector:
matchLabels:
app: deployment-demo
template:
metadata:
labels:
app: deployment-demo
spec:
containers:
- image: wangyanglinux/myapp:v1.0
name: deployment-demo-container
kubectl apply -f 6.deployment.yaml
Step 0:用 patch 收紧滚动更新策略
清单里没有显式声明 strategy,Deployment 会走默认的 RollingUpdate:maxSurge=25%、maxUnavailable=25%。在 replicas: 10 下,这意味着一次可以多出 3 个新 Pod、同时允许 2 个旧 Pod 不可用——金丝雀数量不可控。
用 kubectl patch 直接给运行中的 Deployment “打个补丁”,把策略改成 maxSurge=1, maxUnavailable=0:
kubectl patch deployment deployment-demo -p \
'{"spec":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0}}}}'
含义:
maxSurge: 1——一次最多多出 1 个 新版 Pod,金丝雀数量精确为 1;maxUnavailable: 0——不允许任何旧 Pod 提前被删除,服务容量始终保持 10。
打完补丁后,集群将来的滚动行为就是"每次 +1 个新 Pod、等 Ready 后再 -1 个旧 Pod",正好对应示意图里 9 个 v1 + 1 个 v2 的经典金丝雀状态。
Step 1:更新镜像并立即暂停滚动更新
kubectl patch deployment deployment-demo \
--patch '{"spec": {"template": {"spec": {"containers": [{"name": "deployment-demo-container", "image": "wangyanglinux/myapp:v2.0"}]}}}}' \
&& kubectl rollout pause deploy deployment-demo
这条命令做了两件事:
patch把 Pod 模板里容器的镜像改成v2.0,触发一次滚动更新;- 紧接着
rollout pause把更新暂停,让控制器只滚出一个新版 Pod 就停下来,形成金丝雀。
注意时间竞争:
&&是"前一条成功后立刻执行后一条",但 patch 触发的滚动更新是异步的。如果副本数少、拉镜像快,可能在pause生效前就多滚出几个新 Pod。生产环境建议先把maxSurge=1, maxUnavailable=0设好(保证一次只多一个 Pod),再执行上面的命令,把金丝雀数量控制到最小。
Step 2:观察金丝雀 Pod 的运行情况
# 查看 Pod,新旧版本会同时存在
kubectl get pods -l app=deployment-demo -o wide
# 查看滚动更新状态(此时应显示 paused)
kubectl rollout status deploy deployment-demo
此时集群处于"新旧共存"状态:大部分流量走旧版,少量流量打到新版金丝雀。可以结合日志、监控指标观察新版是否正常。
Step 3-A:确认没问题,恢复更新,全量上线
kubectl rollout resume deploy deployment-demo
resume 会解除暂停,控制器继续把剩余的旧版 Pod 全部替换成新版,完成全量发布。
Step 3-B:发现异常,回滚到上一个版本
# 先 resume 让 rollout 恢复可操作状态,再回滚
kubectl rollout resume deploy deployment-demo
kubectl rollout undo deploy deployment-demo
回滚后受影响的只有那一个金丝雀 Pod 承接的少量流量,把故障影响面控制到了最小。
patch 的三种合并策略:
kubectl patch默认使用strategic(策略性合并,K8s 原生资源专用,能智能处理数组);此外还有--type=merge(标准 JSON Merge Patch,RFC 7386)和--type=json(JSON Patch,RFC 6902,可精确指定 add/replace/remove 操作)。上面更新容器镜像用默认的策略性合并即可。
回滚命令(rollout)
Kubernetes 提供了一套完整的 kubectl rollout 子命令来管理 Deployment 的发布生命周期:
| 命令 | 作用 |
|---|---|
kubectl rollout status | 查看滚动更新的进度与状态 |
kubectl rollout history | 查看历史 revision 列表 |
kubectl rollout undo | 回滚到上一个(或指定)版本 |
kubectl rollout pause | 暂停滚动更新 |
kubectl rollout resume | 恢复已暂停的滚动更新 |
rollout status:查看更新状态
kubectl rollout status deployment/deployment-demo
该命令会实时跟踪滚动更新的进度,直到更新完成或失败才退出。配合 echo $? 可以在脚本中判断更新是否成功:
kubectl rollout status deployment/deployment-demo
echo $?
# 返回 0 表示成功完成,非 0 表示失败或超时
rollout history:查看发布历史
kubectl rollout history deployment/deployment-demo
输出示例:
REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 <none>
每次修改 Pod 模板(镜像、环境变量、资源配置等)都会产生一个新的 revision。CHANGE-CAUSE 列对应 Deployment 的 kubernetes.io/change-cause 注解,如果发布时没有记录变更原因,这一列就是 <none>——历史记录只剩下光秃秃的编号,回滚时根本分不清哪个 revision 对应哪个版本。
小实验:让 CHANGE-CAUSE 显示出来
Step 1:删掉之前的 Deployment,用 --record 重新创建,让 kubectl 把执行的命令记进注解:
kubectl delete deployment deployment-demo
kubectl create -f 6.deployment.yaml --record
Step 2:连续做两次镜像升级,同样带上 --record:
# v1.0 → v2.0
kubectl set image deployment/deployment-demo \
deployment-demo-container=wangyanglinux/myapp:v2.0 --record
# v2.0 → v3.0
kubectl set image deployment/deployment-demo \
deployment-demo-container=wangyanglinux/myapp:v3.0 --record
Step 3:再看历史,CHANGE-CAUSE 列就有内容了:
kubectl rollout history deployment/deployment-demo
REVISION CHANGE-CAUSE
1 kubectl create --filename=6.deployment.yaml --record=true
2 kubectl set image deployment/deployment-demo deployment-demo-container=wangyanglinux/myapp:v2.0 --record=true
3 kubectl set image deployment/deployment-demo deployment-demo-container=wangyanglinux/myapp:v3.0 --record=true
现在每个 revision 对应哪次操作、切到了哪个镜像,一目了然,回滚时就能准确挑出目标版本。
Step 4:还可以查看某个 revision 的详细 Pod 模板:
kubectl rollout history deployment/deployment-demo --revision=2
输出会包含该 revision 完整的 Pod 模板(镜像、标签、端口等),方便回滚前确认内容:
deployment.apps/deployment-demo with revision #2
Pod Template:
Labels: app=deployment-demo
pod-template-hash=6b5d9c8f7d
Annotations: kubernetes.io/change-cause: kubectl set image ... --record=true
Containers:
deployment-demo-container:
Image: wangyanglinux/myapp:v2.0
...
--record 已废弃,用注解代替
从 kubectl v1.18 起,--record 被标记为 deprecated,执行时会打印警告:
Flag --record has been deprecated, --record will be removed in the future
推荐的做法是手动打注解,效果完全一样,而且描述可以写得更有业务含义:
kubectl set image deployment/deployment-demo \
deployment-demo-container=wangyanglinux/myapp:v3.0
kubectl annotate deployment deployment-demo \
kubernetes.io/change-cause="升级到 v3.0:修复登录超时问题" --overwrite
再看历史:
REVISION CHANGE-CAUSE
1 kubectl create --filename=6.deployment.yaml --record=true
2 kubectl set image ... myapp:v2.0 --record=true
3 升级到 v3.0:修复登录超时问题
注意注解的时机:
change-cause是打在 Deployment 上的注解,它会被记录到"当前"这个 revision 上。所以要先改镜像、再打注解(或者反过来先打注解再改镜像也可以,只要两步在同一个 revision 周期内)。如果先打注解后又触发了新的模板变更,注解会跟着落到新 revision 上。另外--overwrite是必须的,否则注解已存在时会报错。
rollout undo:回滚版本
基础用法——回滚到上一个版本:
kubectl rollout undo deployment/deployment-demo
指定版本回滚——--to-revision:
# 精确回滚到 revision 2
kubectl rollout undo deployment/deployment-demo --to-revision=2
undo 的回滚机制详解
很多人以为 rollout undo 是"栈"式的逐层回退——执行多少次就退多少级。但实际上,undo 不是退栈,而是在"当前版本"和"上一个版本"之间来回切换。
以 v1 → v2 → v3 的发布历史为例:
初始状态(revision 历史):
rev1: v1
rev2: v2
rev3: v3 ← 当前运行
第一次 undo:回滚到上一个 revision(rev2 = v2)。undo 会把旧模板作为"新版本"重新应用,产生一个新的最高 revision 号,同时原来的 rev2 条目被去重移除:
rev1: v1
rev3: v3
rev4: v2 ← 当前运行(内容是 v2,revision 号变成了 4)
第二次 undo:再次回滚到"上一个 revision"。此时当前是 rev4,前一个是 rev3(v3):
rev1: v1
rev4: v2
rev5: v3 ← 当前运行(又回到了 v3)
结论:
v3 --undo--> v2 --undo--> v3(回到原点)
连续执行两次 undo,实际上是在 v2 和 v3 之间来回横跳,并不会退到 v1。这是 rollout undo 最常见的误区。想跨版本回滚,必须用 --to-revision 指定目标 revision 号。
rollout pause / resume:暂停与恢复
# 暂停滚动更新
kubectl rollout pause deployment/deployment-demo
# 恢复滚动更新
kubectl rollout resume deployment/deployment-demo
暂停后可以对 Deployment 做多次修改(改镜像、改资源限制、改环境变量),这些修改不会触发新的滚动更新。等所有修改完成后,执行 resume 一次性触发一轮滚动更新,避免频繁修改导致多次无意义的滚动。
关于 revisionHistoryLimit:Deployment 的
.spec.revisionHistoryLimit字段控制保留多少条历史 ReplicaSet 记录(默认 10)。超出限制的旧 revision 会被自动清理,被清理掉的版本将无法再回滚。如果你的发布频率较高,可以适当调大这个值。
四、DaemonSet
DaemonSet 确保全部(或者一部分) Node 上运行一个 Pod 的副本。当有 Node 加入集群时,也会自动为它们新增一个 Pod;当有 Node 从集群移除时,这些 Pod 也会被回收。删除 DaemonSet 将会删除它创建的所有 Pod。
与 Deployment / ReplicaSet 的核心区别在于:Deployment 关心的是"副本总数"——我需要 N 个 Pod 跑在哪里无所谓;而 DaemonSet 关心的是"每个节点恰好一个"——集群有多少个 Node,就有多少个 Pod,自动跟随节点增减。
典型用法
使用 DaemonSet 的一些典型场景:
- 运行集群存储 daemon——例如在每个 Node 上运行
glusterd、ceph,为集群提供分布式存储能力; - 在每个 Node 上运行日志收集 daemon——例如
fluentd、logstash、filebeat,把节点上的容器日志统一收集转发到集中式日志系统; - 在每个 Node 上运行监控 daemon——例如 Prometheus Node Exporter、
collectd、Datadog 代理、New Relic 代理,或 Gangliagmond,负责采集每台机器的 CPU、内存、磁盘、网络等指标。
简单概括:只要你有一类 Pod 需要"每台机器跑一个、跟着节点走“的需求,就用 DaemonSet。
实验六:DaemonSet 每节点一个 Pod
目标:演示 DaemonSet 不需要声明 replicas,副本数完全由集群中可调度的 Node 数量决定;并验证删除 Pod 后会自动重建、删除 DaemonSet 会清理掉它创建的全部 Pod。
Step 1:编写 DaemonSet 清单
# 8.ds.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: deamonset-demo
labels:
app: daemonset-demo
spec:
selector:
matchLabels:
name: deamonset-demo
template:
metadata:
labels:
name: deamonset-demo
spec:
containers:
- name: daemonset-demo-container
image: wangyanglinux/myapp:v1.0
和 Deployment / ReplicaSet 对比一下:
- 没有
replicas字段——这是 DaemonSet 最直观的特征。副本数不由你指定,而是"有多少个可调度的 Node,就有多少个 Pod”;selector.matchLabels仍然是必填的,且必须能匹配上template.metadata.labels,否则 API Server 会直接拒绝创建;- 本例 selector 用的标签键是
name(不是app),只要 template 里的 labels 对得上就没问题——metadata.labels里的app: daemonset-demo只是给 DaemonSet 对象本身打的标签,不参与 Pod 选择。
Step 2:部署并观察
kubectl apply -f 8.ds.yaml
kubectl get ds
# 观察 DESIRED / CURRENT / READY 三列,数值等于集群中可调度的 Node 数
kubectl get pods -o wide
# 观察:每个 worker 节点上恰好一个 Pod,NODE 列各不相同
**为什么 master 节点上没有 Pod?**控制平面节点默认带着
node-role.kubernetes.io/control-plane:NoSchedule污点,DaemonSet 同样受污点约束。如果确实需要覆盖 master,得在spec.template.spec里加上对应的tolerations。
Step 3:删除 Pod,观察自动重建
kubectl delete pod <pod-name>
kubectl get pods -o wide
# 观察:该节点上立刻起了一个新 Pod —— 控制器要保证"每个节点都有"
Step 4:清理
kubectl delete -f 8.ds.yaml
kubectl get pods
# 观察:它创建的所有 Pod 一并被回收
五、Job
前面讲的 RC / RS / Deployment / DaemonSet 都是"长期运行“型的控制器——它们的期望状态是"始终有 N 个 Pod 在跑”,一旦 Pod 退出就会被立刻重建。但现实中还有另一类需求:跑一次就结束,例如数据库迁移脚本、离线数据处理、批量图片压缩、机器学习训练任务等。这正是 Job 控制器要解决的问题。
一句话概括:Job 负责批处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束(
Completed)。
5.1 Job 的特性说明
spec.template格式同 Pod——Pod 模板部分和其它控制器完全一致;RestartPolicy仅支持Never或OnFailure——这是 Job 独有的强制约束。Deployment 里默认的Always在 Job 场景下没有意义(任务本来就是"跑完就退",Always会导致 Pod 无限重启);- 单个 Pod 时:默认 Pod 成功运行(退出码 0)后 Job 即结束;
.spec.completions:标志 Job 结束需要成功运行的 Pod 个数,默认为1;.spec.parallelism:标志并行运行的 Pod 个数,默认为1;.spec.activeDeadlineSeconds:标志失败 Pod 的重试最大时间,超过这个时间不会继续重试。
NevervsOnFailure怎么选?
OnFailure:Pod 内的容器在原地重启(Pod 本身不重建),Job 的失败计数不增加——适合能够自愈的任务;Never:容器失败时不重启,Job 会创建一个新的 Pod 来重试,.status.failed每失败一次 +1,直到达到.spec.backoffLimit(默认 6)后 Job 被标记为Failed。
5.2 completions 与 parallelism 的三种典型组合
completions 和 parallelism 是 Job 最容易混淆的两个字段。三种常见组合可以覆盖大部分批处理场景:
| 场景 | completions | parallelism | 语义 |
|---|---|---|---|
| 一次性任务 | 1(默认) | 1(默认) | 跑一个 Pod,成功一次就结束 |
| 固定次数、串行执行 | N | 1 | 一次只跑一个,跑够 N 次成功才结束 |
| 固定次数、并行执行 | N | M | 最多同时跑 M 个 Pod,累计 N 次成功后结束 |
实验七:用 Job 跑一次性任务
目标:用一个计算 π 的经典例子,演示 Job 的完整生命周期——创建 → 运行 → 成功退出 → Job 完成。
Step 1:编写 Job 清单
# 9.job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl:5.34
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4
和其它控制器对比一下:
apiVersion是batch/v1,不再是apps/v1;- 没有
selector——Job 会自动为 Pod 生成一个基于controller-uid的 selector,不要手动指定,否则容易冲突;restartPolicy只能是Never或OnFailure,写Always会被 API Server 直接拒绝;backoffLimit: 4表示最多允许失败 4 次,超过后 Job 状态变成Failed。
Step 2:部署并观察
kubectl apply -f 9.job.yaml
kubectl get job
# NAME COMPLETIONS DURATION AGE
# pi 0/1 5s 5s —— 正在跑
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# pi-xxxxx 1/1 Running 0 5s
Step 3:等待任务成功结束
kubectl get job -w
# COMPLETIONS 从 0/1 变成 1/1,DURATION 定格
kubectl get pods
# STATUS 变成 Completed —— 注意这个状态是 Job 独有的正常终态
Step 4:查看计算结果
kubectl logs <pod-name>
# 3.1415926535897932384626433832795028841971693993751058209749445923...(2000 位 π)
Step 5:清理
kubectl delete -f 9.job.yaml
# 删除 Job 会一并回收它创建的 Pod(包括已经 Completed 的)
**为什么 Completed 的 Pod 默认不会自动消失?**Job 保留成功的 Pod 是为了让你能事后
kubectl logs查看结果。如果不想手动清理,可以配合.spec.ttlSecondsAfterFinished字段让 Job 在结束后自动过期。
