Appearance
状态管理与 Status 子资源
本篇聚焦 Operator 的「状态报告」能力——Status。前面几章我们用了一个简单的 phase 字段表示集群阶段,但生产级 Operator 需要更精细的状态表达。本篇会讲清楚 Status 子资源的作用、Conditions 模式、Phase vs Condition 的取舍、ObservedGeneration 的含义,以及如何避免 Status 更新引起的无限循环。最后给出一个生产级的 RedisCluster 状态管理实现。
一、Status 子资源的作用
1. 为什么要把 Spec 和 Status 分开
K8s 资源对象天然分成两部分:
- Spec:用户声明的「期望状态」,由用户写入。
- Status:控制器报告的「实际状态」,由控制器写入。
这种分离有深刻的设计意义:
- 权限隔离:可以给用户写 Spec 的权限,但不给写 Status 的权限(防止用户伪造状态)。RBAC 里
redisclusters和redisclusters/status是两个独立资源,可分别授权。 - 避免无限循环:Status 是控制器写的,如果 Status 变化也触发 Reconcile,控制器写 Status → 触发 Reconcile → 又写 Status → 又触发……死循环。子资源机制让 Status 变化不触发本资源的 Reconcile。
- 审计清晰:Spec 是「意图」,Status 是「事实」,分开后审计日志一目了然。
2. 启用 Status 子资源
在 CRD 类型上加 +kubebuilder:subresource:status 标记:
go
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type RedisCluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec RedisClusterSpec `json:"spec,omitempty"`
Status RedisClusterStatus `json:"status,omitempty"`
}make manifests 后,CRD YAML 末尾会多出:
yaml
versions:
- name: v1
...
subresources:
status: {}3. Status 子资源的行为
启用后,API Server 对该资源的行为改变:
| 操作 | 不开子资源 | 开了 status 子资源 |
|---|---|---|
PUT /redisclusters/x(含 status) | 可改 status | 忽略 status,只能改 spec/metadata |
PUT /redisclusters/x/status | 不存在该路径 | 专门改 status 的端点 |
status 变化触发 Reconcile | 会 | 不会 |
也就是说,开了子资源后:
- 普通的
Update(客户端r.Update)不能改 status,API Server 会静默忽略 status 字段。 - 必须用
r.Status().Update()或r.Status().Patch()才能改 status。 - Status 变化不会触发该 CR 自己的 Reconcile(但仍可能触发别的 Controller 的 Watch)。
4. Status 更新不触发 Reconcile 的意义
这是子资源最重要的好处。考虑这个场景:
- Operator 把
status.phase从Pending改成Running。 - 如果 status 变化触发 Reconcile,Controller 立刻再次进入 Reconcile。
- Reconcile 发现状态没变(还是 Running),不写 status,返回。
- 没有新事件,循环停止。
看起来没问题?但如果你的 Reconcile 里有「乐观更新」逻辑(每次都写 status 即使没变),就会无限循环。子资源机制从根上避免了这一点——status 变化根本不进队列。
不过要注意:status 变化虽然不触发本 CR 的 Reconcile,但如果别的 Controller Watch 了这个 CR,它仍然会收到 status 变化事件。所以你的 Reconcile 写 status 时仍要判断「是否真的变了」,避免给别的 Controller 制造噪声。
二、Conditions 模式
1. 为什么 Phase 不够用
phase 是一个单一字符串,比如 Pending / Running / Failed。它简单直观,但表达能力有限:
- 一个集群可能同时处于多个状态:Deployment 就绪了,但备份还没完成。Phase 只能选一个,丢失了「备份中」这个信息。
- Phase 变化是「覆盖式」的,历史信息丢失。从
Running变Failed再变回Running,你不知道中间发生过什么。 - Phase 的语义靠人为约定,不同 Operator 的
Running含义可能不同,缺乏标准化。
Conditions 模式就是为了解决这些问题——用一个 Condition 列表,每个 Condition 描述一个独立维度的状态。
2. Condition 的结构
K8s 社区约定 Condition 结构(来自 metav1.Condition):
go
type Condition struct {
// Type 是条件类型,如 Ready、Progressing、Available
Type string `json:"type"`
// Status 是该条件的状态:True / False / Unknown
Status metav1.ConditionStatus `json:"status"`
// ObservedGeneration 是控制器观察到的 spec.generation
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// LastTransitionTime 是上次 status 变化的时间
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
// Reason 是机器可读的简短原因码,驼峰大写
Reason string `json:"reason"`
// Message 是人类可读的详细说明
Message string `json:"message,omitempty"`
}一个 CR 的 status 里维护一个 Conditions 列表:
go
type RedisClusterStatus struct {
Phase string `json:"phase,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
Nodes []string `json:"nodes,omitempty"`
}3. Condition 类型设计
Type 是 Condition 的标识,用驼峰大写。常见的 Condition Type:
| Type | 含义 | True 时机 |
|---|---|---|
| Ready | 整体就绪,可对外服务 | 所有关键条件都满足 |
| Available | 服务可用(有足够就绪副本) | ReadyReplicas >= 1 |
| Progressing | 正在变更中(创建、扩缩、升级) | 有未完成的滚动操作 |
| Degraded | 降级(部分功能不可用但整体仍运行) | 某些副本不健康 |
| BackupRunning | 业务特定:备份进行中 | 备份任务运行中 |
设计原则:
- 正交:每个 Type 描述一个独立维度,不要重叠。
- 可观测:选那些用户/监控系统关心的状态。
- 稳定:Type 名字一旦发布就不要改,否则破坏兼容性。
4. Condition 状态:True/False/Unknown
- True:该条件成立(如
Ready=True表示就绪)。 - False:该条件不成立(如
Ready=False表示未就绪)。 - Unknown:控制器还没观察到(如刚启动、或与 API Server 失联)。
注意语义:Ready=False 不等于「出错」,它只表示「当前未就绪」,可能是正常的启动过程。错误信息靠 Reason 和 Message 表达。
5. Condition 历史记录
Condition 自身只记录「最后一次状态变化时间」(LastTransitionTime),不记录历史。如果要保留历史,需要自己在 Status 里维护一个 events 列表,或依赖 K8s Event 资源。
K8s 内置的 Event 资源(kubectl get events)天然适合记录状态变化的「日志」,建议把「发生了什么」记进 Event,把「现在是什么状态」记进 Condition。
go
import "k8s.io/client-go/tools/record"
type RedisClusterReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
// 在状态变化时记录事件
r.Recorder.Eventf(rc, corev1.EventTypeNormal, "PhaseChanged",
"Phase changed from %s to %s", oldPhase, newPhase)三、Phase 模式 vs Condition 模式
1. 各自的定位
| 维度 | Phase | Conditions |
|---|---|---|
| 表达能力 | 单一维度 | 多维度,正交 |
| 历史信息 | 无 | 仅最后变化时间 |
| 易用性 | 高(kubectl 看 phase 列) | 中(需解析 conditions 数组) |
| 标准化 | 弱(各家自定义) | 较强(社区约定 Type/Status) |
| 监控集成 | 简单(按 phase 标签告警) | 灵活(按 condition 查询) |
2. 实践建议
两者并用,Phase 做高层摘要,Conditions 做细粒度诊断:
- Phase 给「一眼看懂」的场景:
kubectl get rc直接看 phase 列。 - Conditions 给「深入诊断」的场景:某个集群 phase 是 Failed,看 Conditions 知道是
Available=False(副本不足)还是BackupFailed=True(备份失败)。
Phase 由 Conditions 推导出来,而不是独立维护,避免不一致:
go
func computePhase(conditions []metav1.Condition) string {
ready := getCondition(conditions, "Ready")
progressing := getCondition(conditions, "Progressing")
degraded := getCondition(conditions, "Degraded")
if ready.Status == metav1.ConditionTrue {
if degraded.Status == metav1.ConditionTrue {
return "Degraded"
}
return "Running"
}
if progressing.Status == metav1.ConditionTrue {
return "Progressing"
}
return "Pending"
}四、ObservedGeneration
1. 什么是 Generation
metadata.generation 是 K8s 维护的整数计数器,仅在 spec 变化时递增(metadata/label/annotation/status 变化不递增)。它本质上是「spec 的版本号」。
2. ObservedGeneration 的作用
status.observedGeneration 是控制器写入的,表示「我处理到的 spec.generation 是多少」。用户对比这两个值就能知道控制器是否处理了最新的 spec:
metadata.generation == status.observedGeneration:控制器已处理最新 spec。metadata.generation > status.observedGeneration:控制器还没处理最新 spec(spec 改了但还没 Reconcile)。
3. 为什么重要
考虑这个场景:
- 用户把
spec.size从 3 改成 5,generation变成 2。 - Operator 正在 Reconcile 别的 CR,还没轮到这个。
- 用户
kubectl get rc看到status.readyReplicas=3, phase=Running。 - 用户误以为「已经扩到 5 了,且 3 个就绪」——其实 Operator 根本没开始处理。
如果 status 里有 observedGeneration=1,用户就能发现「generation=2 但 observedGeneration=1,控制器还没处理最新 spec」,从而正确等待。
4. 如何设置
在更新 status 时,把当前的 metadata.generation 写进 observedGeneration:
go
func (r *RedisClusterReconciler) updateStatus(ctx context.Context, rc *cachev1.RedisCluster) error {
base := rc.DeepCopy()
// 设置 observedGeneration
rc.Status.ObservedGeneration = rc.Generation
// ... 计算 conditions、phase ...
return r.Status().Patch(ctx, rc, client.MergeFrom(base))
}更好的做法是用 +kubebuilder:default 标记让 CRD schema 要求这个字段,并在每个 Condition 里也带上 ObservedGeneration(表示「这个 condition 是基于哪个 spec 版本评估的」)。
五、Status 更新最佳实践
1. 使用 Status().Patch()
第四章讲过,Patch 优于 Update。对 Status 更是如此——Status 更新频繁,Patch 只发 diff,冲突少。
go
base := rc.DeepCopy()
// 修改 status
rc.Status.Phase = "Running"
rc.Status.ObservedGeneration = rc.Generation
// 用 base 做 merge 基准
return r.Status().Patch(ctx, rc, client.MergeFrom(base))MergeFrom(base) 会自动算出 base 到当前对象的 diff,只 patch 变化的字段。
2. 避免无限循环
虽然开了 status 子资源后 status 变化不触发本 CR Reconcile,但仍要遵循「变了才写」原则,原因:
- 减少 API 调用(每次 Patch 都是 API Server 负载)。
- 避免给别的 Watch 这个 CR 的 Controller 制造噪声。
- 减少 etcd 写入(etcd 写入有性能上限)。
实现方式:维护一个「期望 status」结构,与当前 status 比对,不同才 Patch。
go
func (r *RedisClusterReconciler) updateStatus(ctx context.Context, rc *cachev1.RedisCluster, desired cachev1.RedisClusterStatus) error {
if statusEqual(rc.Status, desired) {
return nil // 没变,不写
}
base := rc.DeepCopy()
rc.Status = desired
return r.Status().Patch(ctx, rc, client.MergeFrom(base))
}3. 状态更新与资源调谐分开
不要在「创建 Deployment」和「更新 Status」之间穿插,否则容易因为 Status 更新失败导致部分资源已变更但状态没记录。推荐结构:
1. 调谐所有资源(Deployment/Service/...)
2. 收集实际状态
3. 一次性更新 Status4. 乐观并发与重试
Status 更新可能因冲突失败(别人也改了)。用 RetryOnConflict 重试:
go
import "k8s.io/client-go/util/retry"
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
// 重新 Get 最新版本
var latest cachev1.RedisCluster
if err := r.Get(ctx, req.NamespacedName, &latest); err != nil {
return err
}
base := latest.DeepCopy()
// 计算新的 status
latest.Status = computeDesiredStatus(&latest)
// 只在确实变化时 patch
if statusEqual(base.Status, latest.Status) {
return nil
}
return r.Status().Patch(ctx, &latest, client.MergeFrom(base))
})六、完整的状态管理实现
下面给出一个生产级的 Status 管理工具集,适用于任何 Operator。
1. 定义 Conditions 类型
直接复用 metav1.Condition,并写一组辅助函数:
go
package controller
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Condition 类型常量
const (
ConditionReady = "Ready"
ConditionAvailable = "Available"
ConditionProgressing = "Progressing"
ConditionDegraded = "Degraded"
)
// Condition 原因常量
const (
ReasonReconciling = "Reconciling"
ReasonReady = "AllReplicasReady"
ReasonPartiallyReady = "PartialReplicasReady"
ReasonNoReplicas = "NoReplicasReady"
ReasonScaling = "Scaling"
ReasonUpdating = "UpdatingImage"
)
// setCondition 设置或更新一个 condition
// 如果同 Type 的 condition 状态没变,只更新时间戳以外的字段
func setCondition(conditions []metav1.Condition, newCond metav1.Condition) []metav1.Condition {
for i, c := range conditions {
if c.Type == newCond.Type {
if c.Status == newCond.Status {
// 状态没变,保留 LastTransitionTime
newCond.LastTransitionTime = c.LastTransitionTime
}
conditions[i] = newCond
return conditions
}
}
// 不存在该 Type,追加
if newCond.LastTransitionTime.IsZero() {
newCond.LastTransitionTime = metav1.NewTime(time.Now())
}
return append(conditions, newCond)
}
// getCondition 取指定 Type 的 condition
func getCondition(conditions []metav1.Condition, condType string) metav1.Condition {
for _, c := range conditions {
if c.Type == condType {
return c
}
}
return metav1.Condition{
Type: condType,
Status: metav1.ConditionUnknown,
Reason: "NotObserved",
}
}
// removeCondition 删除指定 Type 的 condition
func removeCondition(conditions []metav1.Condition, condType string) []metav1.Condition {
for i, c := range conditions {
if c.Type == condType {
return append(conditions[:i], conditions[i+1:]...)
}
}
return conditions
}2. 计算 Conditions
针对 RedisCluster 的状态计算逻辑:
go
// computeStatus 根据实际资源情况计算期望的 Status
func (r *RedisClusterReconciler) computeStatus(ctx context.Context, rc *cachev1.RedisCluster) cachev1.RedisClusterStatus {
// 查询关联 Deployment
var dep appsv1.Deployment
err := r.Get(ctx, types.NamespacedName{Name: rc.Name, Namespace: rc.Namespace}, &dep)
status := cachev1.RedisClusterStatus{
ObservedGeneration: rc.Generation,
}
if err != nil && errors.IsNotFound(err) {
// Deployment 不存在,所有条件未就绪
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionReady,
Status: metav1.ConditionFalse,
Reason: ReasonNoReplicas,
Message: "Deployment not created yet",
ObservedGeneration: rc.Generation,
})
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionProgressing,
Status: metav1.ConditionTrue,
Reason: ReasonReconciling,
Message: "Creating deployment",
ObservedGeneration: rc.Generation,
})
status.Phase = "Pending"
return status
}
if err != nil {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionReady,
Status: metav1.ConditionUnknown,
Reason: "GetFailed",
Message: err.Error(),
})
status.Phase = "Unknown"
return status
}
// 收集 Pod 名
var pods corev1.PodList
r.List(ctx, &pods, client.InNamespace(rc.Namespace),
client.MatchingLabels{"app.kubernetes.io/instance": rc.Name})
for _, p := range pods.Items {
status.Nodes = append(status.Nodes, p.Name)
}
desired := rc.Spec.Size
ready := dep.Status.ReadyReplicas
status.ReadyReplicas = ready
// Available: 至少 1 个就绪
if ready >= 1 {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionAvailable,
Status: metav1.ConditionTrue,
Reason: ReasonPartiallyReady,
Message: fmt.Sprintf("%d/%d replicas ready", ready, desired),
ObservedGeneration: rc.Generation,
})
} else {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionAvailable,
Status: metav1.ConditionFalse,
Reason: ReasonNoReplicas,
Message: "No ready replicas",
ObservedGeneration: rc.Generation,
})
}
// Progressing: 还在扩缩/更新
if ready != desired || dep.Status.UpdatedReplicas != desired {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionProgressing,
Status: metav1.ConditionTrue,
Reason: ReasonScaling,
Message: fmt.Sprintf("Scaling to %d, %d ready", desired, ready),
ObservedGeneration: rc.Generation,
})
} else {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionProgressing,
Status: metav1.ConditionFalse,
Reason: "Reconciled",
Message: "All replicas ready and up-to-date",
ObservedGeneration: rc.Generation,
})
}
// Ready: 所有副本就绪且无更新进行中
if ready == desired && ready > 0 {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionReady,
Status: metav1.ConditionTrue,
Reason: ReasonReady,
Message: fmt.Sprintf("%d replicas ready", ready),
ObservedGeneration: rc.Generation,
})
status.Phase = "Running"
} else if ready > 0 {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionReady,
Status: metav1.ConditionFalse,
Reason: ReasonPartiallyReady,
Message: fmt.Sprintf("%d/%d ready", ready, desired),
ObservedGeneration: rc.Generation,
})
status.Phase = "Progressing"
} else {
status.Conditions = setCondition(status.Conditions, metav1.Condition{
Type: ConditionReady,
Status: metav1.ConditionFalse,
Reason: ReasonNoReplicas,
Message: "No ready replicas",
ObservedGeneration: rc.Generation,
})
status.Phase = "Pending"
}
return status
}3. 应用 Status
go
func (r *RedisClusterReconciler) reconcileStatus(ctx context.Context, rc *cachev1.RedisCluster) error {
desired := r.computeStatus(ctx, rc)
if statusEqual(rc.Status, desired) {
return nil
}
base := rc.DeepCopy()
rc.Status = desired
if err := r.Status().Patch(ctx, rc, client.MergeFrom(base)); err != nil {
return client.IgnoreNotFound(err)
}
return nil
}
// statusEqual 比较两个 Status 是否一致(忽略 LastTransitionTime 的细微差异)
func statusEqual(a, b cachev1.RedisClusterStatus) bool {
if a.Phase != b.Phase {
return false
}
if a.ObservedGeneration != b.ObservedGeneration {
return false
}
if a.ReadyReplicas != b.ReadyReplicas {
return false
}
if !sameStringSlice(a.Nodes, b.Nodes) {
return false
}
if len(a.Conditions) != len(b.Conditions) {
return false
}
// 比较 conditions(忽略 LastTransitionTime)
for i := range a.Conditions {
if a.Conditions[i].Type != b.Conditions[i].Type ||
a.Conditions[i].Status != b.Conditions[i].Status ||
a.Conditions[i].Reason != b.Conditions[i].Reason {
return false
}
}
return true
}4. 整合进 Reconcile
go
func (r *RedisClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var rc cachev1.RedisCluster
if err := r.Get(ctx, req.NamespacedName, &rc); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 调谐资源
needRequeue, err := r.reconcileDeployment(ctx, &rc)
if err != nil {
return ctrl.Result{}, err
}
if err := r.reconcileService(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
// 更新 status
if err := r.reconcileStatus(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
if needRequeue {
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
return ctrl.Result{}, nil
}七、完整示例:RedisCluster 的状态管理
把上面的工具集用到第三章的 RedisCluster 上。先扩展 Status 类型:
go
type RedisClusterStatus struct {
// ObservedGeneration 表示控制器已处理的 spec.generation
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Phase 是高层状态摘要:Pending / Progressing / Running / Degraded / Failed
Phase string `json:"phase,omitempty"`
// Conditions 是细粒度状态列表
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Nodes 是实际 Pod 名列表
Nodes []string `json:"nodes,omitempty"`
// ReadyReplicas 是就绪副本数
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
}部署后查询:
bash
$ kubectl get rc my-redis -o jsonpath='{.status}'
{
"observedGeneration": 2,
"phase": "Running",
"conditions": [
{"type":"Ready","status":"True","reason":"AllReplicasReady","message":"3 replicas ready","observedGeneration":2},
{"type":"Available","status":"True","reason":"PartiallyReady","observedGeneration":2},
{"type":"Progressing","status":"False","reason":"Reconciled","observedGeneration":2}
],
"nodes": ["my-redis-xxx-aaa","my-redis-xxx-bbb","my-redis-xxx-ccc"],
"readyReplicas": 3
}用 jq 看特定 condition:
bash
$ kubectl get rc my-redis -o json | jq '.status.conditions[] | select(.type=="Ready")'
{
"type": "Ready",
"status": "True",
"reason": "AllReplicasReady",
"message": "3 replicas ready",
"observedGeneration": 2
}监控告警可以基于 condition:
# Prometheus 告警规则示例
rediscluster_status{condition="Ready",status="False"} == 1八、小结
本篇系统讲解了 Operator 的状态管理。要点回顾:
- Status 子资源用
+kubebuilder:subresource:status开启。开启后普通 Update 不能改 status,必须用Status().Update/Patch;且 status 变化不触发本 CR 的 Reconcile,从根上避免循环。 - Conditions 模式用多个正交的 Condition 描述多维状态,比单一 phase 表达力强。每个 Condition 有 Type/Status(True/False/Unknown)/Reason/Message/ObservedGeneration/LastTransitionTime。
- Phase 与 Conditions 并用:Phase 做高层摘要(kubectl 一眼看),Conditions 做细粒度诊断。Phase 由 Conditions 推导,不独立维护。
- ObservedGeneration 表示控制器已处理的 spec 版本,让用户能判断「status 是否反映最新 spec」。每次更新 status 都要设。
- 更新最佳实践:用
Status().Patch+MergeFrom(base);更新前比较,变了才写;资源调谐与 status 更新分离;冲突用RetryOnConflict重试。 - setCondition 工具函数:维护 condition 列表时,状态没变保留 LastTransitionTime,状态变了更新时间戳。这是社区标准做法。
下一篇讲 Finalizer——当级联删除不够用、需要清理外部资源时,Finalizer 是唯一的正确机制。