Kubernetes 基础体系 · 第 8/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes 调谐循环:Desired State、Watch、Queue、幂等和最终一致
Kubernetes 的核心控制方式不是“执行一次命令后结束”,而是持续比较两种状态:
- Desired State(期望状态):用户通过 API 对象声明系统应该是什么样。
- Actual State(实际状态):集群当前观察到的资源、节点和工作负载是什么样。
- 调谐(Reconcile):控制器根据两者的差异执行动作,使实际状态逐步接近期望状态。
Deployment、ReplicaSet、Job、Node 生命周期控制器以及大量 Operator,虽然职责不同,但都遵循这个基本模型。Watch 负责发现变化,Queue 负责安排处理,幂等逻辑负责允许重复处理,最终一致性则描述系统在故障和异步传播下如何收敛。
一、先建立系统模型:API 对象不是“命令”,而是状态记录
Kubernetes API 中的对象通常可以抽象为:
Object = metadata + spec + status
以 Deployment 为例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
这里的 spec.replicas: 3 不是“立即启动三个 Pod”的命令,而是一个持久化的声明:
对名为
web的 Deployment,我希望有三个符合模板和选择器约束的副本。
控制器随后会创建或调整 ReplicaSet,ReplicaSet 再创建或删除 Pod,Scheduler 为未绑定节点的 Pod 选择节点,kubelet 在节点上创建容器。用户写入的是一个 API 对象,但最终效果由多个异步组件共同完成。
1. Desired State
通常,用户可以直接控制对象的 spec。例如:
- Deployment 的副本数、Pod 模板;
- Service 的端口和选择器;
- Job 的并行度和完成次数;
- PersistentVolumeClaim 的容量和访问模式。
spec 并不总是由用户直接填写。某些对象可能由其他控制器或 admission webhook 修改,但从控制器的角度看,spec 仍然代表需要实现的期望输入。
2. Actual State
实际状态不是单一字段,而是控制器从 API 对象、子资源、节点和外部系统中观察到的事实。例如 Deployment 的实际状态可能包括:
- 当前 ReplicaSet 是哪个;
- 有多少 Pod 已创建;
- 有多少 Pod 已就绪;
- 滚动更新是否完成;
- 是否存在不可调度或启动失败的 Pod。
Kubernetes 通常把控制器观察到的摘要写入 status:
status:
replicas: 3
updatedReplicas: 3
readyReplicas: 2
availableReplicas: 2
conditions:
- type: Available
status: "False"
reason: MinimumReplicasUnavailable
status 是观察结果,不应被误认为用户新的期望输入。控制器不能因为 status.readyReplicas 为 2,就把期望副本数改成 2;它应该继续根据 spec.replicas: 3 进行调谐。
二、调谐循环的形式化定义
设:
- 表示期望状态;
- 表示某一时刻观察到的实际状态;
- 表示调谐函数;
- 表示调谐函数产生的动作;
- 表示执行动作后的新状态。
一次调谐过程可以写成:
理想情况下,当系统达到稳定点时:
这里的空动作并不意味着“控制器停止运行”,而是表示在当前观察结果下不需要进一步改变。
例如,期望副本数为 3:
| 调谐轮次 | 观察到的 Pod 数 | 动作 | 执行后的 Pod 数 |
|---|---|---|---|
| 1 | 1 | 创建 2 个 Pod | 3 |
| 2 | 2 | 创建 1 个 Pod | 3 |
| 3 | 3,但 1 个未就绪 | 不再创建;等待就绪状态变化 | 3 |
| 4 | 3,全部就绪 | 无结构变更 | 3 |
“Pod 数量为 3”和“3 个 Pod 全部就绪”是不同条件。控制器必须明确自己要收敛的状态维度,否则容易出现错误判断。
收敛需要哪些条件
最终一致性并不是“系统一定最终正确”的保证。一个典型控制器要收敛,至少需要以下条件:
-
期望状态最终稳定
如果用户不断修改spec,控制器没有固定目标可追赶。 -
实际状态变化最终停止或可被修复
例如节点故障、镜像不存在、外部数据库拒绝请求,可能让系统持续偏离目标。 -
变化最终能够被控制器观察到
Watch 断开后必须能够重新 List;队列中的失败项目必须能重试。 -
调谐动作不会持续制造反复变化
如果控制器每次都随机生成名称或无条件更新对象,就可能永远触发自己。 -
动作具有幂等性,或至少重复执行不会造成不可接受的副作用。
因此,调谐循环是一个反馈系统,而不是一次性的过程调用。
三、Kubernetes 中各组件如何参与这条反馈链
下面的流程以 Deployment 创建 Pod 为例:
flowchart LR
U[用户 kubectl apply] --> API[API Server]
API --> ETCD[(etcd)]
API --> DW[Deployment Controller]
DW --> RS[ReplicaSet API 对象]
RS --> RC[ReplicaSet Controller]
RC --> POD[Pod API 对象]
POD --> SCH[Scheduler]
SCH -->|绑定节点| API
POD --> K[Kubelet]
K --> C[容器运行时]
K -->|状态上报| API
API --> DW
关键路径如下:
kubectl apply向 API Server 提交 Deployment。- API Server 完成认证、授权、准入、校验,并将对象持久化到 etcd。
- Deployment Controller 通过 Watch 或重新 List 看到 Deployment。
- 控制器创建或更新 ReplicaSet。
- ReplicaSet Controller 根据 ReplicaSet 创建 Pod。
- Scheduler 观察没有
spec.nodeName的 Pod,选择节点并通过 API Server 写入绑定结果。 - kubelet 观察分配给本节点的 Pod,调用容器运行时启动容器。
- kubelet 将 Pod 状态写回 API Server。
- 各控制器再次观察状态变化,继续调谐或更新
status。
API Server 是统一的 API 入口和并发控制点;etcd 是 Kubernetes API 持久化数据的存储;Controller Manager 中运行多个控制器;Scheduler 主要负责 Pod 到节点的调度;Node 上的 kubelet 负责把 PodSpec 转化为节点上的运行实例。
这些组件之间通常没有一个全局事务。例如,Deployment 对象写入成功,不代表 Pod 已经启动;Pod 创建成功,也不代表镜像已拉取完成。异步阶段之间的延迟正是最终一致性的来源之一。
四、Watch:变化通知,不是可靠消息队列
控制器不能每秒完整扫描所有对象,否则 API Server 和控制器都会承受不必要的负载。因此 Kubernetes API 提供了 Watch:
GET /apis/apps/v1/namespaces/default/deployments?watch=true
逻辑上,Watch 会返回类似事件:
{
"type": "ADDED",
"object": {
"metadata": {
"name": "web",
"resourceVersion": "1024"
}
}
}
常见事件类型包括:
ADDED:观察范围内出现对象;MODIFIED:对象发生变化;DELETED:对象被删除;BOOKMARK:服务端报告一个进度位置,是否使用取决于客户端和服务端能力;ERROR:Watch 流本身报告错误。
1. ResourceVersion 的作用
对象的 metadata.resourceVersion 是 API 存储层用于表示观察位置的版本标识。控制器通常需要基于它重新建立 Watch,避免在断线后遗漏变化。
但 resourceVersion 不应被当作业务版本号,也不能简单理解为“该对象的第几次修改”。它主要服务于 API 的一致读取和 Watch 恢复机制。
2. Watch 会断开,也可能失效
Watch 不是永久连接,也不是由 Kubernetes 保证永不丢失的消息日志。网络断开、API Server 重启、代理超时或服务端历史事件不可用,都可能导致 Watch 结束。
如果客户端使用过期的资源版本重新 Watch,API Server 可能返回 410 Gone。正确处理方式通常是:
- 重新 List 当前对象;
- 用 List 结果重建本地缓存;
- 从新的资源版本继续 Watch。
这也是 Informer 采用 List-Watch 组合,而不是只使用 Watch 的原因。
3. Watch 事件只是提示
控制器不应该把一条 MODIFIED 事件直接解释为“只需要执行某个固定动作”。事件更适合表达:
某个对象可能需要重新调谐,请重新读取它以及相关对象的当前状态。
例如,Pod 被删除时,ReplicaSet Controller 不应只执行“创建一个 Pod”这一条命令,而应重新计算:
期望副本数 - 当前符合条件的副本数
如果期间用户已经修改了副本数,或者其他控制器已经创建了替代 Pod,重新读取状态才能避免过度创建。
五、Informer 和本地缓存:降低读取成本,但引入可见性延迟
client-go 中常见的控制器结构是:
API Server
│
├── List
└── Watch
│
Reflector
│
DeltaFIFO
│
Indexer / Informer Cache
│
Event Handler
│
WorkQueue
不同版本的 client-go 内部实现细节可能调整,但 List-Watch、缓存和队列这几个概念是稳定的控制器基础。
1. Reflector
Reflector 负责:
- 初始 List;
- 建立 Watch;
- 在 Watch 断开或资源版本失效后重新 List;
- 将对象变化放入本地处理管道。
2. Informer Cache
Informer Cache 保存对象的本地副本。控制器通常从缓存读取对象,而不是每次调谐都直接请求 API Server。
好处是:
- 大幅减少 API Server 读请求;
- 多个处理逻辑可以共享缓存;
- 通过索引快速查找关联对象,例如按 owner 或 label 查找。
代价是缓存不是强一致读取源。对象刚刚写入 API Server 时,控制器从缓存中暂时可能还看不到它。
因此,典型控制器要接受这种情况:
已经创建成功
↓
缓存尚未同步
↓
下一次调谐仍认为对象不存在
↓
尝试再次创建
这并不一定会造成重复资源,因为 API Server 会根据唯一名称返回 AlreadyExists;但控制器必须正确处理这一结果,或者使用稳定名称和关系查询避免无意义重试。
3. 删除事件中的 Tombstone
对象删除后,Informer 可能只剩下删除通知,而缓存中已经找不到对象。事件处理器因此可能收到 DeletedFinalStateUnknown 这样的 tombstone,而不是直接收到对象指针。
删除处理逻辑应能够从 tombstone 中取出对象键,或者至少可靠地得到:
namespace/name
不能假设删除事件中的对象一定仍然可以按正常对象类型直接断言。
六、Queue:把“需要处理的对象”变成可调度任务
事件处理器通常不会直接执行复杂业务逻辑,而是把对象的键放入工作队列:
namespace/name
这样做有三个目的:
- 事件接收和业务处理解耦;
- 可以控制并发 worker 数量;
- 可以对失败任务重试和限速。
1. Queue 中存的是键,不是最终状态
队列一般只存对象键,而不是事件发生时的完整对象。worker 取出 default/web 后,会从缓存重新读取当前对象。
假设短时间内发生以下事件:
t1: Deployment web 修改副本数为 3
t2: Deployment web 修改副本数为 5
t3: worker 开始处理 web
worker 不应处理 t1 的旧快照,而应从缓存读取当前对象,直接按副本数 5 调谐。
2. 队列通常是至少一次处理
client-go 的工作队列提供去重和重试语义,但控制器仍应按“至少一次处理”设计:
- 同一个 key 可能被多次加入;
- 同一个 key 可能因为多个事件被重复触发;
- worker 处理成功后才应
Forget; - 处理失败应
AddRateLimited; - 退出时需要正确
Done,否则队列可能认为项目仍在处理中。
队列不是 etcd,也不是跨进程持久化消息系统。控制器进程完全崩溃时,内存队列中的任务会丢失。不过只要 Informer 重启后能够重新 List,当前对象仍然会重新进入调谐流程。
3. 同一 key 的并发
常见实现会避免同一 key 被多个 worker 同时处理,但不同 key 可以并发调谐。控制器仍不能假设全局串行,因为:
- 多个资源之间可能存在关系;
- 不同控制器可能同时写同一个对象;
- 用户、Webhook 和其他控制器也可能修改对象。
如果确实存在跨对象的原子性要求,不能仅靠单个队列解决;Kubernetes API 通常也不提供跨资源事务。
七、幂等:重复调谐必须得到相同的正确结果
幂等不是“函数只执行一次”,而是:
在相同目标和相同有效状态下,重复执行调谐不会继续产生不必要变化,也不会造成额外不可接受的副作用。
形式上,如果第一次执行后得到状态 ,则再次执行应满足:
或者至少再次执行的结果等价于不执行。
1. 非幂等控制器的反例
以下伪代码存在严重问题:
func reconcile() {
child := &Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "worker-",
},
}
client.CoreV1().Pods(ns).Create(ctx, child)
}
如果父对象只要存在就执行这段代码,那么每次 Watch 事件、队列重试或进程重启都会创建一个新 Pod。实际状态会从:
0 个 Pod → 1 → 2 → 3 → 4 → ...
这不是收敛,而是控制器不断制造偏差。
2. 幂等的创建与更新
更安全的模式是:
- 使用稳定名称,或通过标签、OwnerReference 查找已有子资源;
- 获取当前子资源;
- 若不存在,创建;
- 若存在,比较需要由自己管理的字段;
- 只有确实不一致时才更新;
- 若已经符合目标,不执行写操作。
伪代码:
desired := buildChild(parent)
actual, err := childLister.Get(desired.Name)
switch {
case apierrors.IsNotFound(err):
_, err = childClient.Create(ctx, desired)
if apierrors.IsAlreadyExists(err) {
// 说明其他调谐或并发请求已经创建,下一轮重新读取
return nil
}
return err
case err != nil:
return err
}
if equalManagedFields(actual, desired) {
return nil
}
patched := actual.DeepCopy()
patchManagedFields(patched, desired)
_, err = childClient.Update(ctx, patched)
if apierrors.IsConflict(err) {
// 资源版本过期;重新读取并重试
return err
}
return err
这里的 AlreadyExists 和 Conflict 不是同一种错误:
AlreadyExists:创建目标已经存在,通常说明并发或缓存延迟;Conflict:更新时携带的resourceVersion已经过期,说明对象在读取后被其他写入者修改。
3. 不要覆盖不属于自己的字段
控制器如果拿到整个对象后无条件替换,可能覆盖:
- 用户字段;
- 其他控制器维护的字段;
- admission webhook 注入的字段;
- API Server 默认值。
更新时应只修改自己负责的字段。多个写入者需要协作时,可以使用 JSON Merge Patch、JSON Patch 或 Server-Side Apply,但字段管理关系和冲突策略必须明确。Server-Side Apply 是 API 能力,不意味着所有场景都应该无条件使用。
4. 外部副作用的幂等性更困难
如果调谐逻辑调用云厂商 API、支付系统或数据库,API Server 的幂等更新并不能自动保证外部调用幂等:
调用云 API 创建负载均衡器
↓
云 API 已创建成功
↓
网络超时,控制器没有收到响应
↓
控制器重试
如果第二次调用会再创建一个负载均衡器,就会产生泄漏。常见解决办法包括:
- 使用稳定的外部资源 ID;
- 使用外部 API 支持的幂等键;
- 保存外部资源标识到
status; - 在重试前查询外部系统;
- 明确“已提交但未确认”的状态。
status 可以帮助恢复,但不能单独解决外部系统不支持查询或幂等的问题。
八、一个完整的调谐算例:副本数从 1 变成 3
假设用户执行:
kubectl create deployment web --image=nginx:1.27
kubectl scale deployment web --replicas=3
kubectl get deployment web,pods -o wide
第一条命令创建的 Deployment 初始期望状态近似为:
D.replicas = 1
用户随后将其修改为:
D.replicas = 3
下面展示可能的状态变化。这里的 Pod 数和时序是示意,不是 Kubernetes 的时间保证。
第一次调谐
控制器读取:
期望副本数 = 3
当前 ReplicaSet 期望副本数 = 1
计算差异:
动作:
将 ReplicaSet.spec.replicas 更新为 3
这次调谐并不直接启动容器,它只改变下游控制器的期望状态。
第二次调谐
ReplicaSet Controller 观察到:
ReplicaSet.spec.replicas = 3
当前 Pod 数 = 1
于是创建两个 Pod:
pod-a
pod-b
pod-c
其中新 Pod 可能还没有节点:
pod-a: Pending,未绑定节点
pod-b: Pending,未绑定节点
pod-c: Running
第三次调谐
Scheduler 观察到未调度的 Pod,并分别写入绑定结果。kubelet 再根据绑定结果启动容器。
可能出现:
pod-a: Running / Ready
pod-b: ImagePullBackOff
pod-c: Running / Ready
此时:
当前 Pod 数 = 3
Ready Pod 数 = 2
ReplicaSet 的副本数量可能已经满足,但 Deployment 的可用性条件仍未满足。控制器会更新 Deployment 的 status.conditions,而不是继续创建 Pod。
镜像故障的反例
如果 pod-b 使用了不存在的镜像:
D.replicas = 3
当前 Pod 数 = 3
Ready Pod 数 = 2
控制器重复调谐也不会凭空修复错误镜像。因为结构上的副本数已经是 3,继续创建 Pod 只会导致过量副本。正确行为通常是:
- 维持当前期望副本;
- 在
status中反映不可用原因; - 由用户修改镜像或由其他机制修复故障;
- 状态变化后重新调谐。
这说明“调谐”不是保证任何条件都能成功,而是根据可观察事实持续执行正确动作。
九、事件如何进入 Queue:不要遗漏关联资源
一个 Deployment 的变化不仅可能由 Deployment 自身事件触发,也可能由以下事件触发:
- ReplicaSet 创建、删除或状态变化;
- Pod 创建、删除或就绪状态变化;
- 节点故障;
- 用户修改 Deployment;
- 控制器重启后的初始 List;
- 定期 resync。
因此控制器通常需要建立事件到父对象的映射:
Pod 事件
↓ ownerReferences / labels
找到所属 ReplicaSet
↓ ownerReferences
找到所属 Deployment
↓
将 Deployment 的 namespace/name 放入队列
如果只监听父对象,子资源异常时控制器可能没有及时机会重新调谐。反过来,如果监听了大量无关对象但没有过滤,队列会产生很多无效工作。
删除和 OwnerReference
Kubernetes 常用 metadata.ownerReferences 表示资源归属。控制器可以通过 owner reference:
- 找到父对象;
- 在父对象删除时进行级联清理;
- 通过 garbage collector 清理已无主资源。
但 OwnerReference 不是任意场景都能使用。例如跨命名空间的 owner 关系受到 API 规则限制,控制器不能仅凭标签就假设资源生命周期一定绑定成功。
Finalizer 则是另一种机制:对象删除请求到达后,如果仍有 finalizer,API Server 会先设置删除时间,控制器必须完成清理并移除 finalizer。控制器如果崩溃,删除对象可能长期停留在 terminating 状态。
十、一个基于 client-go 的核心 worker 结构
下面是典型的 worker 逻辑,展示队列的生命周期。具体 client-go 版本的导入路径和泛型 API 可能不同,生产代码应与项目所用 Kubernetes 依赖版本匹配。
func runWorker(ctx context.Context) {
for processNextItem(ctx) {
}
}
func processNextItem(ctx context.Context) bool {
item, shutdown := queue.Get()
if shutdown {
return false
}
defer queue.Done(item)
key, ok := item.(string)
if !ok {
queue.Forget(item)
return true
}
err := reconcile(ctx, key)
if err == nil {
queue.Forget(item)
return true
}
// 生产代码应使用带退避的重试,而不是立即无限循环。
queue.AddRateLimited(key)
return true
}
reconcile 通常执行以下步骤:
1. 解析 namespace/name
2. 从 Informer Cache 读取父对象
3. 如果父对象不存在:
- 可能是已删除对象
- 清理需要清理的外部资源(若仍可确定归属)
- 返回成功
4. 读取或索引查找子对象
5. 根据 spec 和实际状态计算动作
6. 创建、更新或删除子对象
7. 更新 status 或 conditions
8. 返回成功或可重试错误
几个重要的生命周期细节:
Get后必须调用Done;- 成功处理后调用
Forget; - 暂时性错误应进入限速重试;
- 永久性配置错误不能无上限高速重试;
- API Server 的
NotFound对已经删除的父对象通常不是失败; - 不能把缓存对象直接修改后写回,通常应先
DeepCopy,否则可能破坏 Informer Cache 中的对象。
调谐函数应尽量是一个基于当前状态的纯决策过程:
当前父对象 + 当前子对象 + 外部观察结果
↓
应该达到的子对象状态
↓
最小必要 API 动作
这样更容易测试“输入状态到输出动作”的关系。
十一、最终一致性到底意味着什么
最终一致性表示:在输入最终稳定、故障能够恢复、控制器持续运行且动作可完成的前提下,系统状态会逐步接近期望状态,而不是保证每次读取都立即看到最终结果。
可以把一次状态传播写成:
用户写入 spec
↓
API Server 接收并持久化
↓
Informer 收到 Watch 事件
↓
事件进入 Queue
↓
worker 执行 Reconcile
↓
子资源写入 API Server
↓
Scheduler / kubelet 继续处理
↓
status 回写
↓
用户读取到新状态
每一段都可能有延迟或失败,所以以下现象是正常的:
kubectl apply -f deployment.yaml
kubectl get pods
紧接着执行时,可能暂时看不到 Pod,或者看到 Pod 处于 Pending。这不表示 apply 失败;它只证明 API 对象已经提交,并不证明所有下游控制器已经完成工作。
API Server 对单次对象读写提供相应的一致性语义,但 Kubernetes 的多控制器链路没有跨组件的全局强一致事务。用户应区分:
- 提交成功:API Server 接受了对象;
- 控制器已观察:事件进入某个 Informer 或调谐流程;
- 子资源已创建:例如 Pod 对象存在;
- 节点已执行:kubelet 已启动容器;
- 业务已可用:应用通过探针并满足服务条件。
这些是不同层次的事实。
十二、故障路径:为什么系统仍能恢复
1. Watch 断开
Watch 断开
↓
Reflector 发现错误
↓
重新 List
↓
重建缓存
↓
建立新的 Watch
↓
相关对象再次进入调谐
控制器不需要依赖“每一条历史事件都必须被处理”。重新读取当前状态后,只要当前状态仍然偏离目标,就可以再次生成修复动作。
2. worker 在 API 写入后崩溃
假设控制器已经成功创建子资源,但在返回前进程崩溃。重启后可能再次处理同一个父对象:
第一次:Create → 成功 → 进程崩溃
第二次:Create → AlreadyExists
如果控制器把 AlreadyExists 视为可接受结果,并重新读取实际对象,就能恢复。若每次都使用随机名称,则无法判断第一次创建的对象是否就是自己需要的对象,容易产生泄漏。
3. 更新冲突
两个参与者同时修改同一个对象:
控制器读取 resourceVersion=10
用户修改对象,resourceVersion=11
控制器用 version=10 更新
API Server 返回 Conflict
控制器应丢弃旧对象,重新读取当前对象,再根据最新状态计算更新。强行覆盖可能删除其他参与者刚写入的字段。
4. 缓存延迟
控制器刚创建对象,但缓存还没有反映创建结果。下一次调谐可能暂时仍看到 NotFound。稳定名称、AlreadyExists 处理和后续 Watch 事件共同保证它最终恢复。
5. 外部系统不可用
如果调谐依赖云 API,而云 API 长时间不可用,队列会反复重试,但系统不会收敛。此时控制器应该:
- 把可诊断错误写入
status.conditions; - 使用指数退避限制压力;
- 区分可重试的网络错误和不可重试的参数错误;
- 在外部调用前后保持可恢复的状态记录。
十三、常见错误理解与实际表现
误解一:Watch 保证每个事件都被业务处理一次
Watch 只提供变化通知和恢复位置,不是带业务确认的消息系统。事件可能合并、连接可能断开,控制器应该依赖“重新读取当前状态”而不是依赖某个事件的具体内容。
误解二:队列保证任务只执行一次
队列通常提供去重和至少一次处理语义,不提供 exactly-once 执行。API 写操作和外部副作用都必须考虑重复执行。
误解三:调谐只在用户修改时运行
控制器还会被子资源变化、定期 resync、重启后的初始同步、删除事件和状态变化触发。仅处理用户修改会漏掉节点故障、Pod 被删除等关键路径。
误解四:返回 nil 就代表业务已经完成
reconcile 返回成功通常只表示本轮不需要立即重试。对于异步动作,例如 Pod 尚未就绪,控制器可能已经正确完成当前阶段,但最终目标仍未达到。后续状态事件会再次触发调谐。
误解五:无条件 Update 是安全的
无条件更新会带来:
- 不必要的
MODIFIED事件; - 自触发调谐循环;
- 与其他写入者产生冲突;
- 资源版本快速变化;
- 覆盖不属于自己的字段。
正确做法是比较自己管理的字段,只在实际不一致时写入。
十四、用 kubectl 观察一条调谐链路
以下命令使用稳定 API apps/v1 的 Deployment,适用于具备 Kubernetes 集群访问权限的环境。
创建资源:
kubectl create deployment web --image=nginx:1.27
kubectl scale deployment web --replicas=3
观察父资源和子资源:
kubectl get deployment web
kubectl get rs -l app=web
kubectl get pods -l app=web -o wide
查看 Deployment 的期望与观察状态:
kubectl get deployment web -o yaml
重点观察:
spec:
replicas: 3
status:
replicas: 3
readyReplicas: 3
spec.replicas 表示期望值;status.replicas 和 status.readyReplicas 表示控制器观察到的不同实际维度。它们暂时不相等是调谐进行中的正常表现。
观察事件:
kubectl describe deployment web
kubectl get events --sort-by=.lastTimestamp
查看 Pod 无法启动的原因:
kubectl describe pod <pod-name>
kubectl logs <pod-name> -c <container-name>
例如镜像拉取失败时,describe 中通常会看到 ErrImagePull 或 ImagePullBackOff 相关事件。此时如果 Deployment 的 Pod 数已达到 3,控制器不会通过继续创建 Pod 来修复镜像问题;应修改 Deployment 的 spec.template.spec.containers[].image,再让新的期望状态触发滚动更新。
实时观察对象变化:
kubectl get deployment web -w
-w 本身也是基于 Watch 的客户端行为。网络断开或对象历史版本不可用时,客户端会重新建立连接;它不是永久可靠的审计日志。需要审计完整变更历史时,应使用审计日志或专门的事件系统,而不是依赖 Watch 输出。
十五、生产实现中的取舍
控制器设计的关键不是让每次调谐都执行更多动作,而是让每次调谐都能根据当前事实做出可重复、可恢复的最小动作。
需要明确区分三类内容:
Kubernetes API 的规范保证
- API 对象由 API Server 统一管理;
- 对象具有
resourceVersion等并发控制信息; - Watch 可能结束,需要客户端重新同步;
spec和status表达期望与观察结果的不同角色;- 删除、OwnerReference、Finalizer 具有明确的 API 生命周期语义。
常见 client-go 实现
- Informer 使用 List-Watch 构建本地缓存;
- 事件处理器通常将 key 放入 WorkQueue;
- Worker 从缓存读取对象并执行 Reconcile;
- WorkQueue 通常支持去重、确认完成和限速重试。
这些是主流控制器开发方式,但具体类型、泛型接口和内部实现会随 client-go 版本变化,应以项目锁定的依赖版本为准。
工程上的经验选择
- 对缓存可接受的场景优先使用 Informer;
- 对必须确认最新写入结果的少数操作,谨慎使用 API Server 直接读取;
- 为每个外部副作用设计查询、幂等键或恢复记录;
- 通过指标观察队列深度、处理延迟、重试次数和 API 错误;
- 对状态条件写入做去重,避免控制器因自己更新
status形成高频自触发; - 使用超时、退避和并发限制保护 API Server 与外部系统。
十六、诊断调谐问题的正确顺序
当资源“没有按预期运行”时,可以按状态链路定位,而不是只看一条错误日志:
spec 是否正确?
↓
API Server 是否接受对象?
↓
父控制器是否创建了子资源?
↓
子资源的 spec 是否正确?
↓
Scheduler 是否完成调度?
↓
kubelet 是否创建容器?
↓
status 和 conditions 反映了什么?
对应命令可以是:
kubectl get <resource> -o yaml
kubectl describe <resource>
kubectl get events --sort-by=.lastTimestamp
kubectl get pods -o wide
kubectl describe pod <pod>
kubectl logs <pod>
如果子资源完全没有出现,重点检查控制器是否观察到父对象、RBAC 是否允许创建、队列是否持续重试、Webhook 是否拒绝请求。如果子资源存在但无法运行,则问题可能已经从控制器调谐转移到调度、镜像、存储、网络或节点运行时。
Kubernetes 调谐循环的本质,是让一组独立组件通过 API 对象不断交换状态,并在 Watch、缓存、队列和重试机制的帮助下逐步收敛。Watch 负责提供变化线索,Queue 负责安排工作,Reconcile 负责重新计算动作,幂等性保证重复处理不会破坏系统,最终一致性则允许这些过程在延迟、断线、冲突和重启后继续恢复。只要控制器始终基于当前状态做决策,而不是把事件当作一次性命令,系统就能在异步和不可靠环境中保持可恢复性。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes Namespace:作用域、资源配额、权限、多租户和删除
- 下一篇:Kubernetes etcd 数据层:Revision、Watch、事务、Compaction 和备份
- 延伸:Kubernetes 架构:API Server、Scheduler、Controller Manager、etcd 和 Node
- 延伸:Kubernetes Controller 开发:client-go、Cache、Queue、Reconcile 和幂等
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论