


























在 Kubernetes(k8s)集群中,资源的生命周期管理是一个核心命题。当你删除一个 Deployment 时,它所管理的 ReplicaSet 和 Pod 是否应该随之消失?当一个 Pod 被意外删除时,它挂载的 PVC(PersistentVolumeClaim)要不要一起清理?这些问题的答案,都藏在 OwnerReference 这个看似简单的字段里。本文基于 k8s v1.36.1 源码,对 OwnerReference 的定义、垃圾回收器(Garbage Collector)的工作原理、以及生产环境中常见的使用场景进行源码级别的深度解析。
Kubernetes 1.36.1 Go 1.26 Garbage Collection OwnerReference Cascading Deletion Controller
学习重点
⭐ 核心理解:OwnerReference 是 k8s 声明资源父子关系、实现自动级联删除的基石字段。
⭐ 源码能力:掌握 OwnerReference 五个字段的精确含义,理解 GarbageCollector 如何根据它构建依赖图并决定删除策略。
⭐ 生产避坑:了解 BlockOwnerDeletion 与 Finalizer 的配合机制,避免删除 owner 时子资源残留导致的"孤儿资源"问题。
目录
OwnerReference 是 Kubernetes 对象元数据(ObjectMeta)中一个可选字段,它用来声明"当前资源归属于哪个资源"。当资源 A 的 metadata.ownerReferences 中包含资源 B 的信息时,我们就称 A 是 B 的"被管理对象"(dependent/owned),称 B 是 A 的"拥有者"(owner)。
这个关系不是普通的标签(Label)或注解(Annotation),而是 k8s 垃圾回收系统的核心输入。APIServer 会根据这个字段自动构建资源之间的依赖图,当 owner 被删除时,垃圾回收器会自动删除其所有 dependents,而无需人工介入。
OwnerReference 的完整结构如下(来自 staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go,k8s v1.36.1):
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (k8s v1.36.1)
type OwnerReference struct {
// API 版本,格式为 "group/version",例如 "apps/v1"
APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"`
// 资源类型,如 Deployment、ReplicaSet、StatefulSet
Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"`
// owner 的名称
Name string `json:"name" protobuf:"bytes,3,opt,name=name"`
// owner 的 UID(唯一标识,防止名称冲突欺骗)
UID types.UID `json:"uid" protobuf:"bytes,4,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"`
// 可选,是否为管理控制器(管理关系中只能有一个 controller=true)
Controller *bool `json:"controller,omitempty" protobuf:"varint,6,opt,name=controller"`
// 可选,是否阻止 owner 被删除(配合 foregroundDeletion finalizer 使用)
BlockOwnerDeletion *bool `json:"blockOwnerDeletion,omitempty" protobuf:"varint,7,opt,name=blockOwnerDeletion"`
}
可以看到,OwnerReference 一共只有五个字段,但每个字段都在垃圾回收流程中扮演不可替代的角色。下面逐一拆解。
APIVersion:标识 owner 资源所在的 API 组和版本。格式为 group/version,例如 apps/v1。垃圾回收器需要根据这个字段找到对应的 REST 路径,才能发起删除请求。
Kind:标识 owner 的资源类型(如 Deployment、Job、CronJob)。与 APIVersion 组合,垃圾回收器可以唯一定位 owner 资源。
Name:owner 资源的名称。注意,仅有名称不足以唯一确定一个资源,因为不同命名空间可以有同名资源。所以 Name 必须配合 UID 使用。
UID:owner 资源的唯一标识符(UID)。这是最关键的防欺骗字段——如果有人创建了一个同名资源来"劫持"你的资源,仅凭 Name 是无法通过校验的,必须 UID 也完全匹配。垃圾回收器在判断 owner 是否存在时,会同时检查 Name 和 UID。
Controller:布尔指针,标记这个 OwnerReference 是否指向"管理控制器"。在一组 OwnerReferences 中,只能有一个 Controller=true 的引用(k8s 源码中的 ValidateOwnerReferences 函数会强制校验这个约束)。这个字段的作用是区分"管理关系"和"所有权关系"——Deployment 管理 ReplicaSet 时,ReplicaSet 的 OwnerReference 中 Controller=true;但如果某个 ConfigMap 被多个资源引用,它们可以各自有一个 Controller=false 的 OwnerReference。
BlockOwnerDeletion:布尔指针,标记是否阻止 owner 被删除。当它为 true 且 owner 拥有 foregroundDeletion 这个 finalizer 时,APIServer 不会允许 owner 被从 key-value store 中删除,直到所有具有 blockOwnerDeletion=true 的 dependents 从 owner 的 OwnerReferences 列表中移除。这个机制是"前台删除"(Foreground Deletion)的核心实现。
要理解 OwnerReference 的价值,先看看在没有这个机制的时代,资源清理面临什么问题:
| 对比维度 | 传统方案(无 OwnerReference) | k8s 方案(有 OwnerReference) |
|---|---|---|
| 父子资源关系 | 手动记录,或通过命名约定(如 prefix)隐式关联,容易丢失 | 显式声明在元数据中,APIServer 永久持有 |
| 删除 owner 时的行为 | 需手动逐个删除子资源,容易遗漏 | GC 自动感知并级联删除,无需人工介入 |
| owner 不存在时 | 孤立子资源无法自动发现,积累成为孤儿资源 | GC 检测到 dangling ownerRef,自动删除 dependents |
| 误删除保护 | 依赖外部脚本或手动备份 | 通过 Controller/BlockOwnerDeletion 字段精细控制 |
| 跨类型资源管理 | 很难表达 Deployment→Pod 这种跨类型关系 | APIVersion+Kind+Name+UID 精确表达任意类型关系 |
在 k8s 架构中,OwnerReference 被广泛用于表达控制关系:
Deployment 管控 ReplicaSet 管控 Pod 挂载 PVC
在 k8s 内置控制器中,几乎所有资源都依赖 OwnerReference 来建立控制关系:Deployment 管理 ReplicaSet,ReplicaSet 管理 Pod,StatefulSet 管理 PVC 和 Pod,DaemonSet 管理 Pod,Job/CronJob 管理 Pod,HorizontalPodAutoscaler 管理 Pod(通过 subresource)。此外,Operator 框架下开发的自定义控制器也会大量使用这个字段来管理 CR(Custom Resource)所对应的底层资源。
OwnerReference 和 Finalizer 是两个独立但经常配合使用的机制。Finalizer 是资源元数据中的一个字符串数组,表示"在删除前必须完成的清理任务";OwnerReference 则表示"谁拥有这个资源"。
两者配合最典型的场景是 ForegroundDeletion:当删除一个带有 foregroundDeletion finalizer 的 owner 时,k8s 会执行"前台删除"——owner 的 DeletionTimestamp 被设置,但 owner 不会被真正从 etcd 中移除,直到所有 Controller=true 的 dependents 都被删除。此时,BlockOwnerDeletion=true 的 OwnerReference 起到"门卫"作用:即使 dependents 还未被 GC 删除,它们会阻塞 owner 的真正消失,确保删除顺序正确。
在使用 k8s 内置控制器(Deployment、StatefulSet、DaemonSet、Job 等)时,OwnerReference 是由控制器自动设置在子资源上的,无需人工干预。以下是 Deployment 创建后自动生成的 ReplicaSet 和 Pod 的 OwnerReference 示例:
# 查看 Deployment 管理的 ReplicaSet(ReplicaSet 会自动持有 Deployment 的 OwnerReference)
$ kubectl get replicaset myapp-abc123 -o yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: myapp-abc123
namespace: default
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: Deployment
name: myapp
uid: 3d5f8a12-1234-5678-abcd-ef0123456789
# ... 其他字段
spec:
replicas: 3
selector:
matchLabels:
app: myapp
# ...
# 查看 ReplicaSet 管理的 Pod(Pod 会自动持有 ReplicaSet 的 OwnerReference)
$ kubectl get pod myapp-abc123-xyz789 -o yaml
apiVersion: v1
kind: Pod
metadata:
name: myapp-abc123-xyz789
namespace: default
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: myapp-abc123
uid: 7e9f1b34-5678-90ab-cdef-0123456789ab
# ... 其他字段
spec:
containers:
- name: myapp
image: myapp:latest
# ...
可以看到,所有内置控制器都自动设置了 controller: true 和 blockOwnerDeletion: true。这意味着:当 Deployment 被删除时,GC 会先等待 ReplicaSet 删除完毕,再等待 Pod 删除完毕,最后才真正让 Deployment 从 etcd 中消失。
在开发自定义 Operator 时,通常需要手动在 CR 创建的底层资源上设置 OwnerReference。k8s 提供了 controller_ref_manager.go 中的工具函数来简化这一操作。以下是 Go 代码中设置 OwnerReference 的标准方式(k8s v1.36.1):
// pkg/controller/controller_ref_manager.go (k8s v1.36.1)
// ownerRefControllerPatch 函数:构造向 dependent 添加 OwnerReference 的 JSON Patch
func ownerRefControllerPatch(
controller metav1.Object,
controllerKind schema.GroupVersionKind,
uid types.UID,
finalizers ...string,
) ([]byte, error) {
blockOwnerDeletion := true
isController := true
addControllerPatch := objectForAddOwnerRefPatch{
Metadata: objectMetaForPatch{
UID: uid,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: controllerKind.GroupVersion().String(), // "apps/v1"
Kind: controllerKind.Kind, // "Deployment"
Name: controller.GetName(),
UID: controller.GetUID(),
Controller: &isController, // true:声明管理关系
BlockOwnerDeletion: &blockOwnerDeletion, // true:阻塞 owner 删除
},
},
Finalizers: finalizers,
},
}
patchBytes, err := json.Marshal(&addControllerPatch)
if err != nil {
return nil, err
}
return patchBytes, nil
}
这段代码的逻辑很清晰:通过 JSON Merge Patch 向 dependent(被管理的资源)的 metadata.ownerReferences 字段添加一个 OwnerReference 条目。设置 Controller=true 表示这是管理关系,BlockOwnerDeletion=true 表示在"前台删除"场景下,这个依赖关系会阻止 owner 的最终消失。
当你手动删除一个资源时,可以通过 propagationPolicy 参数控制 GC 的行为:
# 前台删除(Foreground):等待所有 dependents 全部删除后,owner 才真正消失 kubectl delete deployment myapp --grace-period=30 -- propagation-policy=Foreground # 后台删除(Background)(默认):owner 立即消失,GC 在后台异步删除 dependents kubectl delete deployment myapp -- propagation-policy=Background # 孤立删除(Orphan):owner 消失,但 dependents 被保留(解除关联关系) kubectl delete deployment myapp -- propagation-policy=Orphan
三种策略的含义在 k8s v1.36.1 源码中定义如下:
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (k8s v1.36.1)
// DeletionPropagation 枚举定义了三种删除策略
type DeletionPropagation string
const (
// Orphans the dependents.(孤立删除:保留 dependents,但解除 OwnerReference 关系)
DeletePropagationOrphan DeletionPropagation = "Orphan"
// Deletes the object from the key-value store, the garbage collector will
// delete the dependents in the background.(后台删除:owner 立即消失,GC 异步删除 dependents)
DeletePropagationBackground DeletionPropagation = "Background"
// The object exists in the key-value store until the garbage collector
// deletes all the dependents whose ownerReference.blockOwnerDeletion=true
// from the key-value store. API sever will put the "foregroundDeletion"
// finalizer on the object, and sets its deletionTimestamp.
// This policy is cascading, i.e., the dependents will be deleted with Foreground.
//(前台删除:owner 被阻塞,直到所有 blockOwnerDeletion=true 的 dependents 消失)
DeletePropagationForeground DeletionPropagation = "Foreground"
)
有时我们不想删除 dependent,只想解除 OwnerReference 关系(让 dependent 独立存活)。可以通过 JSON Strategic Merge Patch 实现:
// pkg/controller/controller_ref_manager.go (k8s v1.36.1)
// GenerateDeleteOwnerRefStrategicMergeBytes:构造从 dependent 上移除 OwnerReference 的 Patch
func GenerateDeleteOwnerRefStrategicMergeBytes(
dependentUID types.UID,
ownerUIDs []types.UID,
finalizers ...string,
) ([]byte, error) {
var ownerReferences []map[string]string
for _, ownerUID := range ownerUIDs {
// $patch=delete 表示从数组中移除 uid 匹配的那条 OwnerReference
ownerReferences = append(ownerReferences, map[string]string{
"$patch": "delete",
"uid": string(ownerUID),
})
}
patch := objectForDeleteOwnerRefStrategicMergePatch{
Metadata: objectMetaForMergePatch{
UID: dependentUID,
OwnerReferences: ownerReferences,
DeleteFinalizers: finalizers,
},
}
patchBytes, err := json.Marshal(&patch)
if err != nil {
return nil, err
}
return patchBytes, nil
}
这里用到的关键技巧是 "$patch": "delete"。在 JSON Strategic Merge Patch 中,$patch: delete 表示"从数组中删除匹配 uid 的元素"。这样可以在不删除 dependent 资源本身的情况下,解除它与 owner 之间的 OwnerReference 关系,实现"解除收养"(orphan)的效果。
OwnerReference 定义在 k8s apimachinery 的核心类型文件中。理解它的字段注解(struct tag)是掌握其行为的关键:
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (k8s v1.36.1)
// 第 311-340 行
// OwnerReference contains enough information to let you identify an owning
// object. An owning object must be in the same namespace as the dependent, or
// be cluster-scoped, so there is no namespace field.
// +structType=atomic
type OwnerReference struct {
// API version of the referent.
APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"`
// Kind of the referent.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"`
// Name of the referent.
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names
Name string `json:"name" protobuf:"bytes,3,opt,name=name"`
// UID of the referent.
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids
UID types.UID `json:"uid" protobuf:"bytes,4,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"`
// If true, this reference points to the managing controller.
// +optional
Controller *bool `json:"controller,omitempty" protobuf:"varint,6,opt,name=controller"`
// If true, AND if the owner has the "foregroundDeletion" finalizer, then
// the owner cannot be deleted from the key-value store until this
// reference is removed.
// See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion
// for how the garbage collector interacts with this field and enforces the foreground deletion.
// Defaults to false.
// To set this field, a user needs "delete" permission of the owner,
// otherwise 422 (Unprocessable Entity) will be returned.
// +optional
BlockOwnerDeletion *bool `json:"blockOwnerDeletion,omitempty" protobuf:"varint,7,opt,name=blockOwnerDeletion"`
}
关键源码解读:
structType=atomic:这个代码生成注释表示 OwnerReference 是一个"原子类型"的 struct,即它内部没有需要单独管理的子字段。在并发场景下,k8s 对包含 OwnerReference 的 ObjectMeta 的读写可以整体进行,无需对 OwnerReference 内部的字段单独加锁。
patchStrategy="merge" 和 patchMergeKey="uid":OwnerReference 数组在 ObjectMeta 中的 patch strategy 是 merge(即 JSON Merge Patch),而合并的 key 是 uid。这意味着当你对包含多个 OwnerReference 的资源打 patch 时,k8s 会根据 uid 来匹配数组元素——如果 uid 相同则覆盖,如果 uid 不同则追加。这正是 GenerateDeleteOwnerRefStrategicMergeBytes 中使用 "$patch": "delete" + "uid": "xxx" 能精确删除某一条 OwnerReference 的底层依据。
casttype 注解:UID 字段的 casttype=k8s.io/apimachinery/pkg/types.UID 注解用于代码生成工具,告诉 deepcopy-gen 在类型转换时使用特定的类型转换函数,而非默认的浅拷贝。
OwnerReference 数组在 ObjectMeta 中的定义包含丰富的 patch 策略注解:
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (k8s v1.36.1) // 第 261 行(ObjectMeta 结构体中) OwnerReferences []OwnerReference `json:"ownerReferences,omitempty" patchStrategy:"merge" patchMergeKey:"uid" protobuf:"bytes,13,rep,name=ownerReferences"`
解读:patchStrategy:"merge" 表示对 OwnerReferences 数组使用 JSON Merge Patch(RFC 7386),而不是 JSON Strategic Merge Patch 的默认行为(后者是针对 Kubernetes 扩展的 patch 策略)。patchMergeKey:"uid" 表示数组元素的合并/删除匹配 key 是 uid 字段。
了解了 OwnerReference 的类型定义后,我们需要理解 k8s 的垃圾回收器(GarbageCollector)是如何利用这些信息来完成级联删除的。GC 的核心实现在 pkg/controller/garbagecollector/garbagecollector.go(k8s v1.36.1)中。
// pkg/controller/garbagecollector/garbagecollector.go (k8s v1.36.1)
// 第 64-77 行:GarbageCollector 的核心结构体
// Note that having the dependencyGraphBuilder notify the garbage collector
// ensures that the garbage collector operates with a graph that is at least as
// up to date as the notification is sent.
type GarbageCollector struct {
restMapper meta.ResettableRESTMapper
metadataClient metadata.Interface
// garbage collector attempts to delete the items in attemptToDelete queue when the time is ripe.
attemptToDelete workqueue.TypedRateLimitingInterface[*node]
// garbage collector attempts to orphan the dependents of the items in the attemptToOrphan queue,
// then deletes the items.
attemptToOrphan workqueue.TypedRateLimitingInterface[*node]
dependencyGraphBuilder *GraphBuilder
// GC caches the owners that do not exist according to the API server.
absentOwnerCache *ReferenceCache
kubeClient clientset.Interface
eventBroadcaster record.EventBroadcaster
}
GC 的架构由两个核心工作队列和依赖图构建器组成:
attemptToDelete 队列:存放"等待被删除"的资源节点。GC worker 从这个队列中取出节点,判断是否满足删除条件,然后执行删除操作。
attemptToOrphan 队列:存放"等待孤立化 dependents"的资源节点。当一个资源被删除但希望保留其 dependents 时(propagationPolicy=Orphan),对应的 dependents 会被移到这个队列,GC 会从 dependents 的 OwnerReferences 中移除指向该 owner 的引用。
dependencyGraphBuilder:负责监听集群中所有资源的变更事件(Add/Update/Delete),并构建和维护一个依赖图(directed graph)。图的节点是资源,图的边是 OwnerReference 关系。当 owner 被删除时,这个图决定了哪些 dependents 需要被处理。
absentOwnerCache:一个缓存,存放"已确认不存在的 owner UID"。这是一个性能优化——如果某个 owner UID 已经确认不存在,GC 不需要每次都去 APIServer 查询,可以直接从缓存中判断 dangling references。
GC 在处理每个资源节点时,会先对其所有 OwnerReferences 进行分类,这是决定是否删除该资源的关键步骤。这个逻辑由 classifyReferences 函数实现(k8s v1.36.1):
// pkg/controller/garbagecollector/garbagecollector.go (k8s v1.36.1)
// 第 458-486 行
// classify the latestReferences to three categories:
// solid: the owner exists, and is not "waitingForDependentsDeletion"
// dangling: the owner does not exist
// waitingForDependentsDeletion: the owner exists, its deletionTimestamp is non-nil,
// and it has FinalizerDeletingDependents
// This function communicates with the server.
func (gc *GarbageCollector) classifyReferences(
ctx context.Context,
item *node,
latestReferences []metav1.OwnerReference,
) (
solid, dangling, waitingForDependentsDeletion []metav1.OwnerReference,
err error,
) {
for _, reference := range latestReferences {
isDangling, owner, err := gc.isDangling(ctx, reference, item)
if err != nil {
return nil, nil, nil, err
}
if isDangling {
// owner 不存在:标记为 dangling(悬挂引用)
dangling = append(dangling, reference)
continue
}
ownerAccessor, err := meta.Accessor(owner)
if err != nil {
return nil, nil, nil, err
}
// owner 存在,但正在删除 dependents
if ownerAccessor.GetDeletionTimestamp() != nil && hasDeleteDependentsFinalizer(ownerAccessor) {
waitingForDependentsDeletion = append(waitingForDependentsDeletion, reference)
} else {
// owner 存在且健康:标记为 solid(稳固引用)
solid = append(solid, reference)
}
}
return solid, dangling, waitingForDependentsDeletion, nil
}
三类引用的含义:
solid(稳固引用):owner 存在且健康,不处于删除状态。这意味着 dependent 被一个正常运行的资源所拥有,不应该被 GC 删除。
dangling(悬挂引用):owner 不存在了(NotFound 或 UID 不匹配)。此时 dependent 成为一个"孤儿资源",如果没有其他 solid 引用,GC 会将其删除。
waitingForDependentsDeletion(等待依赖删除):owner 存在,但已经被标记为删除(deletionTimestamp 非空),且拥有 FinalizerDeleteDependents finalizer。这意味着 owner 正在执行前台删除,dependents 正在被逐个删除。在所有 dependents 删除完毕之前,owner 不会真正从 etcd 中消失。
当一个节点进入 GC 的删除流程时,attemptToDeleteItem 函数会根据引用分类结果做出最终决策:
// pkg/controller/garbagecollector/garbagecollector.go (k8s v1.36.1)
// 第 503-660 行(attemptToDeleteItem 核心逻辑)
// 1. 获取最新资源对象(防止误删旧版本)
latest, err := gc.getObject(item.identity)
// 2. 如果资源不存在(NotFound)或 UID 不匹配,生成虚拟删除事件
// (因为 GraphBuilder 可能为尚未创建的 owner 创建了"虚拟节点")
case errors.IsNotFound(err):
gc.dependencyGraphBuilder.enqueueVirtualDeleteEvent(item.identity)
return enqueuedVirtualDeleteEventErr
// 3. 如果资源正在"等待 dependents 删除",进入前台删除处理流程
if item.isDeletingDependents() {
return gc.processDeletingDependentsItem(logger, item)
}
// 4. 对所有 OwnerReferences 进行分类
ownerReferences := latest.GetOwnerReferences()
solid, dangling, waitingForDependentsDeletion, err := gc.classifyReferences(ctx, item, ownerReferences)
// 5. 决策分支
switch {
case len(solid) != 0:
// solid 不为空:至少有一个存活的 owner,不删除
// 但需要清理 dangling 和 waitingForDependentsDeletion 引用
// (因为 dangling 引用会阻塞等待它们的 dependents 被删除)
ownerUIDs := append(ownerRefsToUIDs(dangling), ownerRefsToUIDs(waitingForDependentsDeletion)...)
patch, _ := gc.deleteOwnerRefJSONMergePatch(n, ownerUIDs...)
// 通过 $patch=delete 移除 dangling/waiting 引用
case len(waitingForDependentsDeletion) != 0 && item.dependentsLength() != 0:
// 有 owner 正在等待 dependents 删除,且自身有 dependents
// 以 Foreground 策略删除(等所有下层 dependents 先消失)
policy := metav1.DeletePropagationForeground
gc.deleteObject(item.identity, latest.ResourceVersion, latest.OwnerReferences, &policy)
default:
// 所有 owner 都不存在(dangling)或无 solid 引用
// 根据 finalizer 类型决定传播策略
var policy metav1.DeletionPropagation
switch {
case hasOrphanFinalizer(latest):
policy = metav1.DeletePropagationOrphan
case hasDeleteDependentsFinalizer(latest):
policy = metav1.DeletePropagationForeground
default:
policy = metav1.DeletePropagationBackground
}
gc.deleteObject(item.identity, latest.ResourceVersion, latest.OwnerReferences, &policy)
}
这个决策流程的精妙之处在于:即使一个资源的所有 owner 都不存在了,GC 也不会立即删除它——而是先检查该资源本身是否有特殊的 finalizer(OrphanDependents 或 DeletingDependents)。这允许用户在删除 owner 后,通过给 dependent 打 finalizer 来控制其最终命运。
GC 依赖一个专门的 GraphBuilder 来持续监听集群中的资源变更并更新依赖图。这个图是以 UID 为 key 的节点映射,节点之间的关系由 OwnerReference 构建。GraphBuilder 通过 informer 监听所有资源的 Add/Update/Delete 事件,当事件到达时:
这个图使得 GC 可以在内存中快速查询"哪些资源依赖 X"和"资源 Y 依赖哪些资源",而不需要每次都去 APIServer 做昂贵的 list 操作。
k8s 在 pkg/controller/controller_utils.go 中提供了一组与 OwnerReference 相关的辅助函数,方便控制器在创建子资源时设置 OwnerReference。其中最核心的是 NewControllerRef 函数:
// pkg/controller/controller_utils.go (k8s v1.36.1)
// NewControllerRef:为一个对象创建指向其 controller 的 OwnerReference
// NewControllerRef creates an OwnerReference pointing to the given owner.
func NewControllerRef(owner metav1.Object, gvk schema.GroupVersionKind) *metav1.OwnerReference {
blockOwnerDeletion := true
isController := true
return &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: owner.GetName(),
UID: owner.GetUID(),
Controller: &isController,
BlockOwnerDeletion: &blockOwnerDeletion,
}
}
这个函数是所有 k8s 内置控制器在创建子资源时设置 OwnerReference 的标准入口。它同时设置了 Controller=true 和 BlockOwnerDeletion=true,这符合大多数控制器的需求——一个 controller 应该对其管理的资源拥有完整的生命周期控制权。
下面这张图展示了从 Deployment 到 Pod 的完整 OwnerReference 链路,以及 GC 如何感知这条链路:
ObjectMeta(Pod 的 metadata)
└── OwnerReferences: []OwnerReference
└── [0]: OwnerReference
├── APIVersion: "apps/v1"
├── Kind: "ReplicaSet"
├── Name: "myapp-abc123"
├── UID: "7e9f1b34-..." ←── GC 通过 UID 唯一确定 owner
├── Controller: true ←── 声明这是管理关系
└── BlockOwnerDeletion: true ←── 前台删除时阻塞 owner 消失
GC 依赖图中的边:
Deployment ──(管理)──▶ ReplicaSet
ReplicaSet ──(管理)──▶ Pod
现象
执行 kubectl delete deployment myapp 后,Deployment 已删除,但 ReplicaSet 仍然存在。Pod 仍在运行,kubectl get rs 显示 ReplicaSet 的 AGE 显示创建时间较早而非刚刚被删除的时间。
根因分析
这种情况通常不是 GC 的问题,而是用户使用了 --cascade=false 或旧版 kubectl 的默认行为。在 k8s v1.36.1 中,kubectl delete 的默认 propagationPolicy 是 Background,即 owner 会立即消失,GC 在后台异步删除 dependents。但如果在删除过程中 APIServer 重启,或者 GC worker 数量不足以处理积压,dependent 的删除可能会有延迟。
另一个可能的原因是:ReplicaSet 的 OwnerReference 中的 UID 与实际 Deployment 的 UID 不匹配。这通常发生在手动删除了 Deployment(而非通过 kubectl)后,有其他进程创建了同名 Deployment,导致 UID 发生变化。此时 ReplicaSet 的 OwnerReference 指向了一个已经不存在的 UID,GC 会将其视为 dangling 并最终删除,但如果 GC 的同步延迟较长,ReplicaSet 会短暂存在。
预防建议:在生产环境中删除重要资源前,先用 kubectl get <owner-type> <name> -o yaml | grep -A 20 ownerReferences 确认 OwnerReference 关系正常。对于核心 Deployment,建议使用 kubectl delete -- propagation-policy=Foreground 确保级联删除完成后再返回。
现象
删除一个命名空间后,某些 PVC(PersistentVolumeClaim)并未被自动删除。kubectl get pvc -A 仍显示这些 PVC 处于 Terminating 或 Pending 状态,且无法通过 kubectl delete pvc 正常删除。
根因分析
这是一个经典的"命名空间删除与 Finalizer 死锁"问题。当 PVC 被 Pod 使用时,Pod 的 Volume 挂载会持有对 PVC 的引用,同时 PVC 上可能绑定了 kubernetes.io/pv-protection 或 kubernetes.io/storage-protection 这类保护 finalizer。在删除 Namespace 时,Namespace Controller 需要删除该命名空间内的所有资源,但如果 PVC 卡在 Terminating 状态(因为 finalizer 未移除),Namespace Controller 就无法完成命名空间的删除。
另一个常见原因是:PVC 的 OwnerReference 未正确指向 StatefulSet(或 Deployment)。在 k8s v1.36.1 中,PVC 默认没有 OwnerReference——它们是由 Pod 的 Volume 模板间接关联的。如果 PVC 不是由 StatefulSet 管理的,那么删除 Namespace 时,PVC 不会通过 GC 级联删除,而是依赖 Namespace Controller 直接清理。Namespace Controller 与 GC 的处理逻辑不同,如果两者协调不当,PVC 可能残留。
高危操作:手动移除 finalizer 会绕过正常的资源保护机制,可能导致数据丢失。在执行 patch 命令前,必须确认所有依赖该 PVC 的应用已停止且数据已备份。
现象
删除一个 Deployment 时,kubectl 长时间卡住不动,describe 显示 Deployment 处于 Terminating 状态。最终删除超时,Deployment 仍然存在,Events 中无错误信息。
根因分析
这是 BlockOwnerDeletion 与 GC 时序冲突导致的经典问题。当一个 owner 的所有 OwnerReference 中存在 BlockOwnerDeletion=true 的 dependent 时,如果 dependent 因为某种原因(比如 GC worker 崩溃、APIServer 重启导致 GC 同步延迟)没有被及时删除,那么 owner 就会被永久阻塞在删除过程中。
在 k8s v1.36.1 的 GC 实现中,attemptToDeleteItem 函数会在有 solid owner 时提前返回(不删除 dependent),但如果 dependent 已经处于 isDeletingDependents 状态,GC 会等待其 dependents 先消失。这里存在一个潜在的竞态条件:如果 dependent 的 OwnerReference 中有指向其他资源的 dangling 引用,GC 在清理 dangling 引用时会触发对 dependent 的 patch,这个 patch 可能与 dependent 的删除操作产生竞争,导致 dependent 既无法被删除也无法完全解除 OwnerReference。
预防建议:在使用 Operator 框架时,不要在所有 dependent 上都设置 BlockOwnerDeletion=true。对于那些生命周期很短或经常重建的资源(如短生命周期 Job 的 Pod),应将 BlockOwnerDeletion 设为 false,以避免 owner 删除被阻塞。
现象
删除一个 CronJob 后,其创建的 Job 资源没有被删除,而是保留在集群中,但这些 Job 的 ownerReferences 字段显示仍然指向已不存在的 CronJob。
根因分析
CronJob Controller 在创建 Job 时,会将 Job 的 ownerReferences 设置为指向 CronJob(Controller=true, BlockOwnerDeletion=true)。但 k8s 的垃圾回收器只能处理其监控范围内的资源类型。如果在 GC 的配置中,CronJob 和 Job 没有被同时监控(即 --gc-ignored-resources 参数中包含了 Job 或 CronJob),GC 就不会为它们构建依赖图,导致删除 CronJob 时,Job 的 OwnerReference 变为 dangling 但 GC 不会主动删除这些 Job。
在 k8s v1.36.1 中,CronJob→Job 的 OwnerReference 管理依赖 GC,但 GC 的监控列表取决于 kube-controller-manager 的启动参数。默认情况下 GC 会监控所有标准 API 资源,但某些自定义资源(CRD)如果未正确注册,GC 就无法处理其 OwnerReference。
现象
一个 ConfigMap 被两个 Deployment 引用(两个 Deployment 的 Pod 都使用这个 ConfigMap)。删除其中一个 Deployment 后,Pod 没有被删除(因为另一个 Deployment 仍然管理着这些 Pod),但预期行为是:删除 owner 时,如果 dependent 有多个 OwnerReference,只要有一个 solid owner 存在,dependent 就不应该被 GC 删除——这实际上是正确行为。真正的问题是:当两个 Deployment 都被删除后,ConfigMap 的 OwnerReferences 中两个引用都被标记为 dangling,但由于 GC 分类逻辑的 bug,某些场景下 ConfigMap 会被意外删除。
根因分析
在 k8s v1.36.1 的 classifyReferences 函数中,当一个资源有多个 OwnerReference 时,每个引用被独立分类。如果所有 solid owner 都被删除,剩余的都是 dangling 引用,那么 GC 会尝试删除这个 ConfigMap。但在删除 ConfigMap 之前,GC 会先执行"清理 dangling 引用"的操作——从 ConfigMap 的 OwnerReferences 中移除 dangling 条目。如果此时 ConfigMap 上有 FinalizerDeleteDependents finalizer,且 GC 的 patch 操作顺序不当,可能导致 ConfigMap 卡在 Terminating 状态。
另一个问题出现在 attemptToDeleteItem 的 switch 分支中:当 solid 非空时,函数会清理 dangling 引用,但如果 dangling 和 waitingForDependentsDeletion 的引用数量很多,GC 会分批 patch,每批 patch 都会触发一次 APIServer 请求。如果在这个过程中 APIServer 发生错误,patch 可能部分成功,导致 OwnerReferences 处于不一致状态。
v1.36.1 版本的差异:在 k8s v1.26 之前的版本中,GC 对 dangling OwnerReference 的清理使用了不同的 patch 策略。在 v1.36.1 中,GenerateDeleteOwnerRefStrategicMergeBytes 使用了更精确的 $patch=delete + uid 匹配方式,避免了之前版本中可能出现的"误删同 UID 不同名引用"的问题。如果你在使用较旧版本的 k8s 集群,强烈建议升级到 v1.26+ 以获得这个修复。
现象
使用 kubebuilder 或 operator-sdk 开发的自定义 Operator,在删除 CR(Custom Resource)后,CR 创建的底层资源(如 Deployment、Service、ConfigMap)没有被自动删除。集群中积累了大量"孤儿资源"。
根因分析
这是 Operator 开发中最常见的问题之一,根本原因是 CR 在创建底层资源时没有正确设置 OwnerReference。在 kubebuilder v3.x / v4.x 中,需要在 controller-runtime 中显式调用 controllerutil.SetControllerReference(owner, dependent, scheme) 或 controllerutil.SetOwnerReference(owner, dependent, scheme) 来设置 OwnerReference。如果没有调用这个函数,底层资源就不知道自己归谁管理,GC 自然不会级联删除它们。
另一个常见错误是:OwnerReference 的 APIVersion 写错了(比如写成了 mygroup/v1alpha1 而实际 CR 是 mygroup/v1beta1),导致 GC 无法正确解析 GVK,从而无法找到 owner 资源。此时 GC 会把 dependent 视为"没有被任何 owner 拥有"的资源,理论上应该删除,但如果 GC 的 metadata client 没有 watch 到这个 dependent 类型,就完全不会处理它。
最佳实践:在使用 controller-runtime 时,推荐同时设置 BlockOwnerDeletion=true 和 Controller=true,确保 owner 的删除行为与 GC 的预期一致。如果你的 CR 有较长的清理逻辑(如等待后台任务完成),可以在 CR 上添加一个自定义 finalizer,并在 finalizer 处理逻辑中负责删除底层资源,这样可以不完全依赖 GC 的级联删除机制。
Q1:OwnerReference 和 Label Selector 有什么区别?
A:这是两个完全不同的机制。Label Selector 是控制器(如 ReplicaSet、Deployment)用来匹配"哪些 Pod 属于我"的声明式查询工具——它通过标签选择器动态发现 Pod,即使 Pod 的创建顺序与控制器无关。OwnerReference 则是静态的元数据引用——每个 dependent 资源在创建时就明确声明了"我属于谁",这个关系由 APIServer 持久化,不需要运行时查询。GC 只能感知 OwnerReference,无法通过 Label Selector 来判断资源间的父子关系。
Q2:同一个资源可以有多个 OwnerReference 吗?
A:可以。OwnerReferences 是一个数组,可以包含多条 OwnerReference。但需要遵守以下约束:① 同一时刻最多只能有一个 Controller=true 的引用(k8s 的 ValidateOwnerReferences 函数强制校验),防止多个 controller 争夺同一资源的控制权;② 每个 OwnerReference 的组合(APIVersion+Kind+Name+UID)必须唯一,数组中不能有两条指向同一个 owner 的引用。多个 OwnerReference 的典型场景是:一个 ConfigMap 被多个 Deployment 的 Pod 使用,此时 ConfigMap 的 OwnerReferences 中可以包含多条来自不同 Deployment/StatefulSet 的引用。
Q3:Cluster-Scoped 资源可以作为 Namespace-Scoped 资源的 owner 吗?
A:可以,但需要满足命名空间兼容性规则。owner 和 dependent 之间的关系受到以下约束:Namespace-Scoped 资源的 owner 必须是同一个 namespace 中的资源,或者是 Cluster-Scoped 资源(Cluster-Scoped 资源没有命名空间,所以无法限制在特定 namespace 中)。例如,一个 Node(Cluster-Scoped)可以作为 PV(也是 Cluster-Scoped)的 owner;但一个 Deployment(Namespace-Scoped)不能成为另一个 namespace 中资源的 owner——因为 k8s 强制要求 dependent 必须在 owner 的同一 namespace 中(如果有 namespace 的话)。OwnerReference 结构体中没有 namespace 字段,正是因为 namespace 关系由上述规则隐式确定。
Q4:BlockOwnerDeletion 和 Controller 字段同时为 true 是什么意思?
A:在 k8s 内置控制器中,这两个字段几乎总是同时设为 true,但含义不同。Controller=true 表示"这是一个管理关系,此 owner 是这个 dependent 的管理控制器"。在具有多个 OwnerReference 的资源中,Controller=true 的那一条标识了"真正的管理者"——其他 controller 不能同时设置 Controller=true。BlockOwnerDeletion=true 表示"在 owner 执行前台删除(foregroundDeletion)时,GC 必须先删除我,然后才能完成 owner 的最终消失"。两者结合的效果是:owner 删除了,dependent 一定先被 GC 删除了,owner 才会真正从 etcd 中消失。
Q5:为什么 GC 删除资源是异步的,而不是同步的?
A:GC 被设计为异步处理有多个原因:① 依赖图可能非常庞大,删除一个有 10000 个 Pod 的 Deployment 时,如果 GC 同步等待所有 Pod 删除完成,会阻塞 API 请求处理,影响集群的可用性;② GC 需要对每个 dependent 发起独立的 DELETE 请求,这些请求之间没有依赖关系,可以并行处理;③ GC worker 数量(默认 5 个 concurrent-gc-syncs)可以通过配置调整,以适应不同规模的集群。异步设计使得 GC 可以在后台持续处理大量删除请求,而不影响 APIServer 的正常请求处理。
Q6:如何查看 GC 的工作日志?
A:GC 的日志输出到 kube-controller-manager 的标准输出(或日志文件中)。查看日志的命令取决于你的集群日志方案:① 如果使用 kubectl logs(pod 模式):kubectl logs -n kube-system -l app=garbage-collector --tail=200;② 如果使用 journald:journalctl -u kube-controller-manager | grep garbage-collector;③ 日志级别可以通过在 kube-controller-manager 启动参数中添加 --v=5 来提高详细程度(默认是 v=2)。GC 的关键日志前缀包括:Processing item(正在处理一个节点)、classify item's references(正在分类 OwnerReferences)、Deleting item(正在删除一个资源)。
Q7:CustomResourceDefinition(CRD)资源会被 GC 自动处理 OwnerReference 吗?
A:这取决于多个因素。首先,CRD 的资源类型必须被 GC 监控——GC 通过监听 API discovery 来动态发现所有可用的资源类型,如果 CRD 资源没有在 discovery 中正确注册,GC 就不会为它们构建依赖图。其次,OwnerReference 中的 APIVersion 必须能被 GC 的 REST mapper 正确解析为 GVK(即 CRD 的 version 和 group 必须正确)。第三,Operator 开发者需要在创建 CR 的底层资源时显式调用 controllerutil.SetControllerReference 来设置 OwnerReference——CRD 本身不会自动设置。在满足以上条件的情况下,GC 会像处理内置资源一样处理 CRD 资源。
Q8:删除一个带有 FinalizerDeleteDependents finalizer 的 owner 时,会发生什么?
A:这是 k8s 前台删除(Foreground Deletion)的完整流程:① APIServer 接收到删除请求,将 owner 的 deletionTimestamp 设置为当前时间,并添加 foregroundDeletion finalizer(如果还没有);② GC 检测到 owner 处于删除状态且有 FinalizerDeleteDependents finalizer,查询依赖图找到所有 Controller=true 的 dependents;③ GC 逐个删除 dependents,在这个过程中 owner 始终存在于 etcd 中(deletionTimestamp 已设置,但未被真正删除);④ 所有 dependents 删除完毕后,GC 移除 owner 上的 FinalizerDeleteDependents finalizer;⑤ APIServer 检测到 finalizer 已清空,owner 真正从 etcd 中消失。这个流程保证了在 owner 彻底消失之前,GC 有足够的时间完成所有 dependents 的清理。
Q9:propagationPolicy=Orphan 和 propagationPolicy=Background 哪个更常用?
A:这取决于使用场景。Background(默认)是大多数场景的最佳选择——owner 立即返回删除成功,GC 在后台异步清理 dependents,既保证了快速的 API 响应,又确保了资源的最终清理。Orphan 则用于"保留 dependents,让它们独立存活"的场景——典型场景包括:① 升级过程中临时删除一个 Deployment,希望保留现有的 Pod 以便快速回滚;② 调试时临时停止控制器,但保留已创建的资源;③ 数据迁移场景,先解除关系再重新关联。在日常运维中,Background 远比 Orphan 常用,因为级联删除是 k8s 默认的安全行为——保留孤儿资源通常是一个需要主动决策的操作。
Q10:GC 会对哪些资源进行处理?哪些资源会被忽略?
A:GC 默认会处理所有通过 API discovery 发现的可删除资源类型。可以通过 --gc-ignored-resources 启动参数来排除特定资源类型。例如,如果你在删除某个 CRD 时不希望 GC 处理对应的 CR,可以将这个 CRD 资源类型加入忽略列表。但注意:GC 不会处理 Finalizer 本身、Events、Leases(这些资源的删除频率太高,不适合 GC 处理)。此外,在 k8s v1.36.1 中,GC 的 ignoredResources 配置在 GarbageCollectorControllerOptions 中设置,可以在 kube-controller-manager 的配置文件中通过 GarbageCollectorController.GCIgnoredResources 字段进行细粒度配置。
Q11:一个资源的 OwnerReference 中,UID 字段的作用具体是什么?
A:UID 是防止"名称欺骗"的安全机制。考虑这个场景:用户 A 创建了一个 Deployment "web",用户 B 创建了另一个 Deployment "web"(同名但不同 UID)。用户 A 的 Deployment 管理着 ReplicaSet,ReplicaSet 的 OwnerReference 中 Name="web"、UID=UserA 的 UID。如果 GC 只检查 Name,那么用户 B 的 "web" 就会被错误地识别为 ReplicaSet 的 owner,导致误删。通过 Name+UID 的双重校验,GC 只会匹配 UID 完全一致的资源。即使有人在另一个 namespace 创建了同名资源,由于 namespace 不同(k8s 强制 owner 和 dependent 同 namespace 或同为 cluster-scoped),也不会产生混淆。
Q12:k8s 1.36.1 中 GC 有哪些性能优化?
A:k8s v1.36.1 的 GC 包含多个性能优化机制:① AbsentOwnerCache(absentOwnerCache)——缓存已确认不存在的 owner UID,避免对 APIServer 的重复查询。② Virtual Delete Event——当 GC 发现一个节点已不存在(NotFound)时,不会触发完整的 graph 更新流程,而是生成一个"虚拟删除事件"直接清理图中的虚拟节点,避免不必要的 API 调用。③ DependencyGraphBuilder 的增量更新——GraphBuilder 监听的是 informer 事件(Add/Update/Delete),而不是定期全量扫描,这使得依赖图的更新是增量的,复杂度与实际变更量成正比。④ 多 worker 并行处理——GC 默认有 5 个 attemptToDelete worker 和 5 个 attemptToOrphan worker,可以并行处理大量删除请求。
Q13:StatefulSet 的 OwnerReference 和 Deployment 有什么不同?
A:最核心的区别在于数据安全敏感度。Deployment 的 ReplicaSet 和 Pod 在删除后数据无法恢复(Pod 的本地数据会丢失),所以可以放心使用 Background 删除。StatefulSet 管理的 Pod 通常挂载了 PVC(PersistentVolume),而 PVC 中可能存储了重要数据。StatefulSet Controller 在设置 OwnerReference 时,会将 blockOwnerDeletion 设为 true,确保在删除 StatefulSet 时不会意外丢失有状态的 Pod。此外,StatefulSet 还会在 PVC 上设置指向 StatefulSet 自身的 OwnerReference(通过 PVC 的 volumeClaimTemplates 机制),这使得当 StatefulSet 被删除时,GC 会级联删除 PVC——这个行为由 volumeProtectionController 和 GC 共同控制。
Q14:如何手动清理积累了大量的孤儿资源?
A:孤儿资源(OwnerReference 指向不存在的 owner)最终会被 GC 自动删除,但如果需要立即清理,可以使用以下方法:① 识别孤儿资源:编写脚本遍历所有资源,对每个资源的 OwnerReferences 中的 UID,向 APIServer 查询该 UID 对应的资源是否存在(404 NotFound 即为孤儿)。② 批量清理 dangling 引用:对于 OwnerReference 已设置为 dangling 的资源,GC 会在下次处理周期中删除它们,可以等待 GC 自动处理,或者手动执行删除。③ 批量删除特定类型的孤儿资源:kubectl get <resource-type> -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.metadata.ownerReferences[0].uid}{"\n"}{end}' | while read ns name uid; do kubectl get <resource> "$name" -n "$ns" &>/dev/null || echo "Orphan: $ns/$name (uid=$uid)"; done。
Q15:GC 和 Finalizer 的执行顺序是怎样的?
A:当一个带有 FinalizerDeleteDependents finalizer 的 owner 被删除时,GC 的执行顺序是:① GC 读取 owner 的 OwnerReferences,找到所有 Controller=true 的 dependents;② 对每个 dependent,如果它也有 FinalizerDeleteDependents finalizer,GC 先等待它的 dependents 被删除(递归);③ 确认 dependent 没有更多 dependents 后,GC 移除 dependent 上的 FinalizerDeleteDependents finalizer,然后 dependent 被真正删除;④ 所有 dependents 删除完毕后,GC 移除 owner 上的 FinalizerDeleteDependents finalizer,owner 被真正删除。这个顺序保证了"自底向上"的删除——最底层的资源(没有其他 dependents 的)先被删除,然后逐层向上。
Q16:Operator 中可以在不删除 dependent 的情况下解除 OwnerReference 关系吗?
A:可以。在 pkg/controller/controller_ref_manager.go(k8s v1.36.1)中,GenerateDeleteOwnerRefStrategicMergeBytes 函数正是用于此目的——它生成一个 JSON Strategic Merge Patch,向 dependent 的 OwnerReferences 数组中注入 "$patch": "delete" 条目,从而精确移除特定 owner 的 OwnerReference,而保留其他 OwnerReference 和 dependent 资源本身。这个操作实现了"解除收养"的效果:dependent 不再受任何 owner 管理,变成了一个孤儿资源(但仍在集群中存在)。在实际应用中,这常用于"接管"资源——新的 controller 通过 patch 先移除旧的 OwnerReference,再添加自己的 OwnerReference,实现资源的平滑迁移。
Q17:为什么删除 HPA(HorizontalPodAutoscaler)不会自动删除它管理的 Pod?
A:这是因为 HPA 与 Pod 之间没有直接的 OwnerReference 关系。HPA 通过修改 Deployment/ReplicaSet 的副本数(replicas 字段)来间接控制 Pod 数量。HPA 的 OwnerReference 指向 Deployment(Controller=true),但 Deployment/ReplicaSet 的 OwnerReference 指向 HPA 是不存在的(因为 Deployment 不知道自己被谁 scale)。所以删除 HPA 时,HPA 本身会被删除,但 Deployment 和 Pod 不会受到影响——Deployment 失去了 HPA 的自动扩缩容控制,但会保持最后一次 scale 操作的副本数继续运行。这是有意为之的设计:如果 HPA 删除会导致 Pod 消失,那将是非常危险的行为,因为扩缩容操作不应该影响应用的高可用性。
Q18:在 v1.36.1 中,GC 的 concurrent-gc-syncs 参数设置为多少合适?
A:默认值是 5。在生产环境中,这个参数可以根据集群规模和 GC 工作负载进行调整:① 小型集群(节点数 < 50):默认值 5 已足够,过高的并发数会增加 APIServer 的压力。② 中大型集群(节点数 50-500):建议设为 10-20。③ 超大规模集群(节点数 500+):建议设为 20-50,并配合监控观察 GC 的处理延迟和队列积压。需要注意,这个参数设置的是 GC worker 的数量,每个 worker 都会独立向 APIServer 发起请求,过高的并发数可能导致 APIServer 负载过高。可以通过 kubectl get pods -n kube-system -l app=garbage-collector 查看 GC pod 的配置,并通过监控 GC 的队列长度来评估是否需要调整。
Q19:GC 会不会处理节点的 OwnerReference?比如 Pod 和 PV 的关系。
A:对于 Pod 和 PV(PersistentVolume)的关系,k8s 使用了一个特殊的"节点授权图"(Node Authorizer Graph),而不是通用的 GC 机制。在 plugin/pkg/auth/authorizer/node/graph_populator.go(k8s v1.36.1)中,graphPopulator 组件通过 informer 监听 Pod、PersistentVolume、VolumeAttachment 等资源的变更,维护一个专门的节点级依赖图。这个图用于节点授权(Node Authorization)——kubelet 只能操作属于自己节点的资源。当 Pod 被删除时,这个图会相应更新,但具体的 PV 删除逻辑不经过通用 GC,而是由 PV Controller(pkg/controller/volume/persistentvolume/pv_controller.go)处理——PV Controller 会根据 StorageClass 的 reclaimPolicy(Retain/Delete)决定是否删除 PV。
Q20:升级到 k8s v1.36.1 后,OwnerReference 的行为有变化吗?
A:在 k8s v1.36.1 中,OwnerReference 的核心行为与 v1.25-v1.30 系列保持一致,没有破坏性变更。值得关注的改进包括:① GC 的 isDangling 判断逻辑在 v1.30+ 中得到了优化,减少了因 APIServer 延迟导致的误判(当一个 owner 刚被删除但 informer cache 还未同步时,GC 会通过 absentOwnerCache 缓存结果,避免重复查询)。② MergePreservingRelativeOrder 函数在 staging/src/k8s.io/apimachinery/pkg/util/sort/sort.go(k8s v1.36.1)中引入了 Kahn's Algorithm 拓扑排序的确定性优化,确保当多个 OwnerReference 需要被批量处理时,删除顺序是确定性的(不受 map 遍历顺序影响)。这些改进提升了 GC 在大规模集群中的正确性和稳定性。
Q21:如何理解"前台删除"(Foreground Deletion)和"后台删除"(Background Deletion)的性能差异?
A:核心差异在于 owner 资源何时从 etcd 中消失。Foreground Deletion 中,owner 的 deletionTimestamp 被设置,但 owner 仍然存在于 etcd 中,直到所有 dependents 删除完毕。在这个过程中,owner 的 /status 端点仍然可以访问(供监控系统查询删除进度),但所有对该资源的修改请求都会被拒绝(因为处于删除状态)。Foreground Deletion 的代价是:owner 在整个 dependents 删除期间占用 etcd 的存储空间和 API 对象。Background Deletion 则立即将 owner 从 etcd 中移除(deletionTimestamp 也设置),dependents 的删除完全异步进行,owner 的 API 对象不再存在,但 dependents 仍在运行,直到 GC 在后台逐个清理。从用户体验角度看,Foreground Deletion 让用户感知到删除的"进度"(kubectl get 时 owner 处于 Terminating),而 Background Deletion 让用户感知到"立即消失"。
Q22:在多租户环境中,GC 的 OwnerReference 机制会带来哪些安全考量?
A:在多租户环境中,OwnerReference 的设置权限需要谨慎控制:① 用户 A 创建了一个 ConfigMap 并将其 OwnerReference 设置为指向用户 B 的 Deployment——这在 RBAC 正常配置下是不可能的,因为设置 BlockOwnerDeletion=true 的 OwnerReference 需要对 owner 资源有 "delete" 权限,用户 A 没有用户 B 的 Deployment 的 delete 权限,APIServer 会返回 422(Unprocessable Entity)。② 如果恶意用户绕过了 RBAC 限制,在自己的资源上设置了指向其他用户资源的 OwnerReference,GC 在删除被指向的 owner 时,会错误地删除恶意用户的资源,导致"连带删除"攻击。k8s v1.36.1 通过在设置 BlockOwnerDeletion=true 时要求对 owner 有 delete 权限来缓解这个问题,但 Controller=true 的 OwnerReference 设置不受此限制(任何人可以为任意资源设置 Controller=false 的 OwnerReference)。建议在多租户环境中通过 RBAC 严格限制用户创建/修改 resources with OwnerReference 的能力。
Q23:kubebuilder/operator-sdk 开发中,SetControllerReference 和 SetOwnerReference 有什么区别?
A:在 controller-runtime 库中:SetControllerReference(owner, dependent, scheme) 设置 Controller=true 的 OwnerReference,同时做完整的 GVK 校验(确保 owner 的 scheme 中注册了 dependent 的类型),并在 dependent 上添加 owner 的 OwnerReference;SetOwnerReference(owner, dependent, scheme) 设置 Controller=false 的 OwnerReference,同样做 GVK 校验,但允许设置 BlockOwnerDeletion 参数。从业务语义上,SetControllerReference 用于声明"主管理关系"(如 Deployment 管理 Pod),SetOwnerReference 用于声明"所有权关系"但不是管理关系(如 Deployment 和 Service 都引用同一个 ConfigMap)。需要注意的是,这两个函数都会验证 owner 和 dependent 的类型是否兼容(same namespace 或同为 cluster-scoped)。
Q24:当 GC 和控制器同时操作同一个资源的 OwnerReferences 时会发生什么?
A:这是一个竞态条件(race condition)的典型场景。例如:控制器正在 patch 一个 Pod 添加新的 OwnerReference,同时 GC 正在 patch 同一个 Pod 删除 dangling OwnerReference。由于 patch 操作是独立的,如果 GC 的 patch 在控制器的 patch 之后到达 APIServer,但 APIServer 处理顺序不同,最终的 OwnerReferences 数组可能处于不一致状态。k8s v1.36.1 的 GC 实现中,attemptToDeleteItem 在处理 dangling 引用时,会先从 APIServer 获取最新的资源对象(gc.getObject(item.identity)),再基于最新的 resourceVersion 打 patch。如果 patch 失败(409 Conflict),GC 会重试。整个过程中,k8s 的 resourceVersion 机制保证了乐观并发控制——如果两个 patch 同时到达,第二个会收到 409 Conflict 并重试。这种机制并不能完全消除竞态,但在大多数情况下,重试逻辑能够收敛到正确的状态。
Q25:GC 的依赖图存储在哪里?重启后会丢失吗?
A:GC 的依赖图存储在内存中(GraphBuilder.uidToNode 字段是一个 map),而不是持久化到存储中。因此,kube-controller-manager 重启后,依赖图需要重新构建。GraphBuilder 的重建过程是增量进行的:它通过 informer 的 List 操作获取所有已存在资源的 OwnerReferences,快速重建初始图,然后继续通过 informer 的 Watch 事件进行增量更新。这个 List 操作的代价取决于集群中资源的总量——在超大规模集群中(节点数 1000+),GC 的初始同步可能需要几十秒到几分钟。在这个窗口期内,如果有人删除了一个 owner,GC 可能无法及时感知,导致 dependents 不会被级联删除。GC 的 Sync 方法会定期重新从 discovery 获取可用的资源类型列表,确保新注册的 CRD 也能被 GC 处理。
来源说明
本文中所有源码引用均基于 Kubernetes v1.36.1 官方仓库(github.com/kubernetes/kubernetes at v1.36.1)。源码路径包括:staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go(OwnerReference 类型定义)、pkg/controller/garbagecollector/garbagecollector.go(GC 核心逻辑)、pkg/controller/controller_ref_manager.go(OwnerReference 补丁生成)、staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go(Object 接口定义)。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。