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

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
AWS News Blog
AWS News Blog
Google DeepMind News
Google DeepMind News
U
Unit 42
博客园 - 叶小钗
博客园 - 聂微东
GbyAI
GbyAI
Stack Overflow Blog
Stack Overflow Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
D
DataBreaches.Net
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Jina AI
Jina AI
美团技术团队
The Cloudflare Blog
M
MIT News - Artificial intelligence
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
S
Schneier on Security
C
Check Point Blog
Project Zero
Project Zero
The Hacker News
The Hacker News
Scott Helme
Scott Helme
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Cisco Talos Blog
Cisco Talos Blog
P
Privacy International News Feed
SecWiki News
SecWiki News
Latest news
Latest news
MongoDB | Blog
MongoDB | Blog
S
Secure Thoughts
Google Online Security Blog
Google Online Security Blog
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
H
Help Net Security
TaoSecurity Blog
TaoSecurity Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Last Week in AI
Last Week in AI
P
Privacy & Cybersecurity Law Blog
Forbes - Security
Forbes - Security
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
L
Lohrmann on Cybersecurity
A
About on SuperTechFans
T
The Exploit Database - CXSecurity.com
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
大猫的无限游戏
大猫的无限游戏
T
Troy Hunt's Blog
H
Hacker News: Front Page
Vercel News
Vercel News

蛮荆

如何获取更多的免费服务器 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 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
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

扩展阅读