蛮荆

Kubernetes GC 设计与实现

2023-12-25

概述

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

Owner 和 Dependent 依赖关系图

回收机制

默认情况下,垃圾回收采用的是级联删除机制,例如删除 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 版本源代码进行分析。

GC 源代码目录

流程图

下面我们跟着流程图一起看下源代码的具体实现。

GC 流程图


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.RunGarbageCollector.Sync


开始执行 GC

GarbageCollector.Run 方法作为垃圾回收器的入口方法,主要做两件事情:

  1. 单独启动一个 goroutine 执行资源对象的 DAG 构造和同步
  2. 根据配置启动相应数量的 goroutine 来处理存储将被回收的资源对象的 attemptToDelete 队列
  3. 根据配置启动相应数量的 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)
}

小结

GC 流程图

Reference

扩展阅读

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。