惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
博客园_首页
T
Tailwind CSS Blog
美团技术团队
博客园 - 叶小钗
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
Microsoft Azure Blog
Microsoft Azure Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
InfoQ
MongoDB | Blog
MongoDB | Blog
The Cloudflare Blog
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Y
Y Combinator Blog

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述
Kubernetes GC 设计与实现
2023-12-25 · via 蛮荆

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

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

扩展阅读