Kubernetes GC 设计与实现
2023-12-25 Cloud Native Kubernetes 读代码
概述
Kubernetes 和内置垃圾回收编程语言 (例如 Go, Java) 一样,内部也有垃圾回收机制,用于清理集群中的下列资源:
- 终止的 Pod
- 已完成的 Job
- 附属的主对象已经不存在的对象
- 未使用的容器和容器镜像
- StorageClass 回收策略为 Delete 的 PV 卷
- 过期的证书签名
- …
和编程语言中的 GC 运行机制一样,Kubernetes 中的垃圾回收周期自动执行,集群内每个运行 kubelet 的节点上都会有一个垃圾回收器在运行, 可以简单将其理解为一个独立运行的进程甚至一个 goroutine, 事实上,Kubernetes 的垃圾回收是以 控制器资源 的形式存在和运行的。
Owner, Dependent
Kubernetes 中被依赖的资源对象称之为 Owner (属主资源), 依赖其他资源的对象称之为 Dependent (依赖资源), 例如我们创建了一个副本数量为 3 的 Deployment。
# 官方示例 controllers/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3 # 副本数量,可以根据实际情况修改
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
那么会产生如下的依赖关系:
- Pod 作为 ReplicaSet 的 Dependent, 它的 Owner 为 ReplicaSet (Deployment 底层实现需要 ReplicaSet)
- ReplicaSet 作为 Owner 的同时也同样作为 Dependent, 它的 Owner 为 Deployment, Dependent 为 3 个依赖它的 Pod
- Deployment 作为 Owner, 它的 Dependent 为依赖的 ReplicaSet
回收机制
默认情况下,垃圾回收采用的是级联删除机制,例如删除 ReplicaSet 资源对象 R1 之后,会删除依赖 R1 资源对象的 Pod 资源对象, 级联删除有两种类型: 前台级联删除 和 后台级联删除。
前台级联删除: 当资源对象进入垃圾回收过程,垃圾回收控制器先删除其全部依赖 (Dependent) 对象,然后删除该资源 (Owner) 对象。
后台级联删除: API Server 立即删除该资源 (Owner) 对象,然后由垃圾回收控制器在后台清理其全部依赖 (Dependent) 对象,这是 Kubernetes 默认使用的级联删除方案。
孤儿资源对象
当 Kubernetes 删除某个资源 (Owner) 对象时,其全部依赖 (Dependent) 对象被称作被遗弃的 (Orphaned) 孤儿资源对象,该功能由下文中的 Finalizer 来实现。
Finalizer
Finalizer 用于防止误操作删除了集群依赖的正常运行资源,Finalizer 可以作为资源对象的属性和资源进行绑定,用于执行资源对象在删除之前的逻辑,
所有对象在删除之前,其 Finalizer 属性字段必须为 nil
, API Server 才会删除该对象,这样就可以防止级联删除了。
例如,现在试图删除一个正在被多个 Pod 使用的 PersistentVolume (持久卷) 资源,那么该 PersistentVolume 资源不会被理解删除, 因为 PersistentVolume 资源注册了 Finalizer 。
除了防止级联删除之外,还可以在资源对象删除之前执行指定的钩子函数,例如在一些场景中只想删除当前资源对象,而不想级联删除其依赖对象, 这时就可以在该资源对象上注册 OrphanFinalizer, 那么垃圾回收控制器在删除该资源对象之后,会忽略其依赖对象,这样给开发者自定义实现提供了更强的灵活性。
源码说明
本文着重从源代码的角度分析一下 ReplicaSet 的实现原理,ReplicaSet 功能对应的源代码位于 Kubernetes 项目的 pkg/controller/garbagecollector/
目录,本文以 Kubernetes v1.28
版本源代码进行分析。
流程图
下面我们跟着流程图一起看下源代码的具体实现。
GarbageCollector
GarbageCollector
对象表示垃圾回收控制器,是实现垃圾回收功能的核心对象。
通过监听 Informer 来获取资源变化并将结果构造为 DAG (有向无环图) 结构,DAG 对象存储了集群中不同资源对象之间的从属关系,当 DAG 发生变化时,
对应的资源对象可能会被垃圾回收加入到 attemptToDelete
队列,并将对象依赖的对象加入到 attemptToOrphan
队列。
type GarbageCollector struct {
...
// attemptToDelete 队列
// 存储垃圾回收尝试删除的资源对象
attemptToDelete workqueue.RateLimitingInterface
// attemptToOrphan 队列
// 存储垃圾回收尝试删除的资源对象所依赖的对象
attemptToOrphan workqueue.RateLimitingInterface
// 资源对象 DAG 构造器
dependencyGraphBuilder *GraphBuilder
}
初始化
NewGarbageCollector
方法通过参数对象构造一个新的 GarbageCollector
对象并返回。
func NewGarbageCollector(...) (*GarbageCollector, error) {
...
gc := &GarbageCollector{
...
}
gc.dependencyGraphBuilder = &GraphBuilder{
...
}
return gc, nil
}
启动入口
垃圾回收的启动入口位于 Kubernetes 项目的 /cmd/kube-controller-manager/app/core.go
文件中。
func startGarbageCollectorController(ctx context.Context, ...) (controller.Interface, bool, error) {
// 初始化 NewGarbageCollector 对象需要的各项参数
...
// 设置垃圾回收需要忽视的资源类型
ignoredResources := make(map[schema.GroupResource]struct{})
for _, r := range controllerContext.ComponentConfig.GarbageCollectorController.GCIgnoredResources {
ignoredResources[schema.GroupResource{Group: r.Group, Resource: r.Resource}] = struct{}{}
}
// 创建一个 NewGarbageCollector 对象
garbageCollector, err := garbagecollector.NewGarbageCollector(
...
)
// 获取垃圾回收并发的 goroutine 数量 (默认为 20 个)
workers := int(controllerContext.ComponentConfig.GarbageCollectorController.ConcurrentGCSyncs)
// 启动垃圾回收
go garbageCollector.Run(ctx, workers)
// 监听集群内的资源对象变化并同步需要被删除的资源对象
go garbageCollector.Sync(ctx, discoveryClient, 30*time.Second)
return garbageCollector, true, nil
}
从上面的源代码可以看到,启动垃圾回收器时,调用的核心方法为 GarbageCollector.Run
和 GarbageCollector.Sync
。
开始执行 GC
GarbageCollector.Run
方法作为垃圾回收器的入口方法,主要做两件事情:
- 单独启动一个 goroutine 执行资源对象的 DAG 构造和同步
- 根据配置启动相应数量的 goroutine 来处理存储将被回收的资源对象的
attemptToDelete
队列 - 根据配置启动相应数量的 goroutine 来处理存储将被回收的资源对象所依赖对象的
attemptToOrphan
队列
func (gc *GarbageCollector) Run(ctx context.Context, workers int) {
...
// 启动一个 goroutine 执行 DAG 的构建
go gc.dependencyGraphBuilder.Run(ctx)
// 等待所有资源对象的 DAG 构建完成
if !cache.WaitForNamedCacheSync("garbage collector", ctx.Done(), func() bool {
return gc.dependencyGraphBuilder.IsSynced(logger)
}) {
return
}
// 所有准备工作就绪之后,就可以执行垃圾回收了
// 根据配置启动多个 goroutine 来执行垃圾回收
for i := 0; i < workers; i++ {
go wait.UntilWithContext(ctx, gc.runAttemptToDeleteWorker, 1*time.Second)
go wait.Until(func() { gc.runAttemptToOrphanWorker(logger) }, 1*time.Second, ctx.Done())
}
<-ctx.Done()
}
回收对象队列
GarbageCollector.runAttemptToDeleteWorker
方法负责回收队列中要被删除的对象,内部是一个无限循环,通过调用 processAttemptToDeleteWorker
方法来决定退出的具体条件。
func (gc *GarbageCollector) runAttemptToDeleteWorker(ctx context.Context) {
for gc.processAttemptToDeleteWorker(ctx) {
}
}
func (gc *GarbageCollector) processAttemptToDeleteWorker(ctx context.Context) bool {
// 从队列中取出一个资源对象
item, quit := gc.attemptToDelete.Get()
...
// 调用 attemptToDeleteWorker 方法实现删除操作
// 限于篇幅,该方法的实现细节不做具体分析
action := gc.attemptToDeleteWorker(ctx, item)
// 根据返回值做具体的操作
switch action {
case forgetItem:
// 从队列中删除资源对象
gc.attemptToDelete.Forget(item)
case requeueItem:
// 将资源对象重新入队
gc.attemptToDelete.AddRateLimited(item)
}
return true
}
回收对象依赖队列
回收对象依赖队列 的处理方法和 回收对象队列 方法类似,不同之处只是操作的队列不同和调用的 资源删除方法 不同,这里跳过源代码分析。
func (gc *GarbageCollector) runAttemptToOrphanWorker(logger klog.Logger) {
for gc.processAttemptToOrphanWorker(logger) {
}
}
func (gc *GarbageCollector) processAttemptToOrphanWorker(logger klog.Logger) bool {
...
}
同步
GarbageCollector.Sync
方法是垃圾回收功能实现的另一个核心方法,主要负责定期同步监听的集群中的资源对象,并过滤出需要删除的资源对象。
func (gc *GarbageCollector) Sync(ctx context.Context, ...) {
// 获取可以被回收的资源对象
newResources, err := GetDeletableResources(logger, discoveryClient)
...
// 检测可回收对象是否发生变化
// 如果没有任何对象发生变化,意味着本轮垃圾回收无需执行
if reflect.DeepEqual(oldResources, newResources) {
return
}
// 代码执行到这里,说明需要进行一轮垃圾回收操作
// 因为垃圾回收过程中涉及到重建资源 DAG
// 所以需要加锁,暂停异步执行的 goroutine
gc.workerLock.Lock()
defer gc.workerLock.Unlock()
attempt := 0
wait.PollImmediateUntilWithContext(ctx, 100*time.Millisecond, func(ctx context.Context) (bool, error) {
attempt++
// 每一轮, 重新获取可以被回收的资源对象
if attempt > 1 {
newResources, err = GetDeletableResources(logger, discoveryClient)
...
}
// 同步资源对象
if err := gc.resyncMonitors(logger, newResources); err != nil {
return false, nil
}
// 等待所有资源对象的 DAG 构建完成
if !cache.WaitForNamedCacheSync("garbage collector", waitForStopOrTimeout(ctx.Done(), period), func() bool {
return gc.dependencyGraphBuilder.IsSynced(logger)
}) {
...
return false, nil
}
return true, nil
})
// 更换新旧资源对象
oldResources = newResources
}, period)
}