kube-controller-manager walk-around(3): garbage collection

·  阅读 632

garbage collection in kubernetes

kubernetes controller manager中有两个GC的组件

  • garbage collector
  • pod garbage collector

pod-garbage-collector

PGC的逻辑比较简单,初始化的函数在cmd/kube-controller-manager/app/core.go的startPodGCController

func startPodGCController(ctx ControllerContext) (http.Handler, bool, error) {
	go podgc.NewPodGC(
		ctx.ClientBuilder.ClientOrDie("pod-garbage-collector"),
		ctx.InformerFactory.Core().V1().Pods(),
		int(ctx.ComponentConfig.PodGCController.TerminatedPodGCThreshold),
	).Run(ctx.Stop)
	return nil, true, nil
}
复制代码

PodGCController的结构体:其中最后一个terminatedPodThreshold简单来说就是允许集群上存在多少succeeded或者failed的pod,超过这个门限的终止的pod都将被删除。

type PodGCController struct {
	kubeClient clientset.Interface

	podLister       corelisters.PodLister
	podListerSynced cache.InformerSynced

	deletePod              func(namespace, name string) error
	terminatedPodThreshold int
}
复制代码

NewPodGC的三个参数分别为clientset,podInformer,terminatedPodThreshold,而deletePod则注册为

		deletePod: func(namespace, name string) error {
			klog.Infof("PodGC is force deleting Pod: %v/%v", namespace, name)
			return kubeClient.CoreV1().Pods(namespace).Delete(name, metav1.NewDeleteOptions(0))
		},
复制代码

Run()函数中wait.Until(gcc.gc, gcCheckPeriod, stop),其中gcCheckPeriod为20s,gc这个函数做了三件事情

  • 回收超过阈值数目的terminated pods
  • 回收孤儿pod,这个孤儿的定义与我们平时的认知不同
  • 回收terminating的未被调度的pod

回收terminated pod

回收terminated pod的时候首先从cache里获取所有pod。

这段代码有点啰嗦,在于首先判断了gcc.terminatedPodThreshold,大于0的情况下才触发gcc.gcTerminated(pods),之后又判断了一次deleteCound与terminatedPodCount的大小,然而由于terminatedPodThreshold肯定大于0,这个判断是冗余的

	deleteCount := terminatedPodCount - gcc.terminatedPodThreshold

	if deleteCount > terminatedPodCount {
		deleteCount = terminatedPodCount
	}
复制代码

deleteCount就是要删除的pod的数量,删除的时候是按照创建时间顺序排序的,排序函数中有个有趣的用法func (o byCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] }。golang中sort.Sort()调用了quickSort(),对快排做了优化

func quickSort(data Interface, a, b, maxDepth int) {
	for b-a > 12 { // Use ShellSort for slices <= 12 elements
		if maxDepth == 0 {
			heapSort(data, a, b)
			return
		}
		maxDepth--
		mlo, mhi := doPivot(data, a, b)
		// Avoiding recursion on the larger subproblem guarantees
		// a stack depth of at most lg(b-a).
		if mlo-a < b-mhi {
			quickSort(data, a, mlo, maxDepth)
			a = mhi // i.e., quickSort(data, mhi, b)
		} else {
			quickSort(data, mhi, b, maxDepth)
			b = mlo // i.e., quickSort(data, a, mlo)
		}
	}
	if b-a > 1 {
		// Do ShellSort pass with gap 6
		// It could be written in this simplified form cause b-a <= 12
		for i := a + 6; i < b; i++ {
			if data.Less(i, i-6) {
				data.Swap(i, i-6)
			}
		}
		insertionSort(data, a, b)
	}
}
复制代码

maxDepth是2*ceil(lg(n+1)),n为数组的长度,maxDepth的作用是当分治的深度超过maxDepth的时候将剩余的问题转为堆排序,防止极端情况下快排的复杂度退化为O(N^2)。当数组长度小于12时,直接做插入排序。在快排的位置选择上,用了doPivot()计算。在计算位置时,总体思路是计算data[low], data[medium], data[high]的中位数,在此之前对data[low], data[medium], data[high]的每个元素都取了额外的随机值先进行一次medianOfThree(),之后还用了3-way partition解决重复数字的问题。

并发删除pod的时候,用了waitGroup的wait.Wait()等待要删除的都删除完毕再退出。

回收孤儿pod

kubernetes官方对孤儿的定义是:

如果删除对象时,不自动删除它的 Dependent,这些 Dependent 被称作是原对象的孤儿。

然而实际使用时,由于节点重启或crash,可能导致pod成为孤儿进程,本地有挂载卷残留,使得kubelet没法自动回收,这样的pod我们也称为孤儿pod。

在controller中的孤儿pod的定义比较偏门,专指绑定到不存在的node上的pod。起到的作用比较有限。且它要从apiserver中获取一次所有的节点,带来了额外的开销。

回收unscheduled terminating pod

删除所有pod.DeletionTimestamp不为空且pod.Spec.NodeName为空的pod。由于没有调度,kubelet尚未创建实例,pod就只是一个etcd中的数据,所以可以直接删除。

整体来看,pod-garbage-collector的功能还是比较鸡肋的。

garbage-collector

garbage-collector的初始化在cmd/kube-controller-manager/app/core.go的startGarbageCollectorController中。该函数首先初始化了两个client

  • discoveryClient:只用做初始化
  • dynamicClient:之前的文章已经分析过了

由这些clientset得到deletableResourcesignoredResources。这些resource都不是真正的pod等资源,而是schema.GroupVersionResource的列表,即垃圾回收有能力回收哪些种类的资源,哪些是用户配置不希望被回收的。

之后进入NewGarbageCollector(),返回一个GarbageCollector实例,它包含以下内容

  • restMapper:从discovery中重置自己
  • dynamicClient
  • attemptToDelete:workqueue
  • attemptToOrphan:workqueue
  • dependencyGraphBuilder:DAG图,维护各资源的从属关系
  • absentOwnerCache:把apiserver中不存在的owners加入到这个cache中
  • sharedInformers
  • workerLock:读写锁

New之后开始Run(),之后再关注。Run之后还要进行garbageCollector.Sync(gcClientset.Discovery(), 30*time.Second, ctx.Stop),它将每30s运行一次GetDeletableResources,重新获取可操作的资源类型,同样稍后再关注细节,所以在GetDeletableResources的注释中写到

All discovery errors are considered temporary. Upon encountering any error, GetDeletableResources will log and return any discovered resources it was able to process (which may be none).

workqueue

workqueue的基本原理是:处理queue中的object失败后,会增加其retry计数,并延迟一段时间后重新入队。如果retry次数超过限制,则丢弃该object。

GC中的attemptToDeleteattemptToOrphan都调用NewNamedRateLimitingQueue()生成一个workqueue,这个函数将返回rateLimitingType,包含一个delaying queue和一个rate limiter。

ratelimiter

先看ratelimiter,它由DefaultControllerRateLimiter()生成,实现了三个方法

  • when:该object将多久后重新入队
  • forget:处理成功或者重试超过限制后丢弃这个object
  • numRequeues:就是之前说的retry次数

DefaultControllerRateLimiter()将生成一个NewMaxOfRateLimiter,它实际上是一个[]RateLimiter,注释中说的比较明白

MaxOfRateLimiter calls every RateLimiter and returns the worst case response. When used with a token bucket limiter, the burst could be apparently exceeded in cases where particular items were separately delayed a longer time.

在默认实现中,它实际上包含两个rate limiter

  • NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second)
  • &BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)}

先看第一个,5ms是初始时延,1000s是最大时延,看一下它的When()方法,每次时延都是之前的2倍,直到达到最大时延,所以它的名字叫Exponential。

exp := r.failures[item]
backoff := float64(r.baseDelay.Nanoseconds()) * math.Pow(2, float64(exp))
if backoff > math.MaxInt64 {
	return r.maxDelay
}
calculated := time.Duration(backoff)
if calculated > r.maxDelay {
	return r.maxDelay
}
复制代码

再看第二个,BucketRateLimiter包含一个rate.NewLimiter(rate.Limit(10), 100),这是一个令牌池,根据注释

NewLimiter returns a new Limiter that allows events up to rate r and permits bursts of at most b tokens.

A Limiter controls how frequently events are allowed to happen. It implements a "token bucket" of size b, initially full and refilled at rate r tokens per second

即一共b个桶,初始装满token,每次事件消耗一个桶中的token,每秒填入r个token,所以平均来看事件发生的速率就是r,而每次最高的并发量是b。所以这里平均每秒10个事件,最高并发量为100。所谓的rate.Limit()就是float64()

ratelimiter提供三种限速方式

  • wait: 参数可以带context,比如可以传withTimeOut设置超时,或者在外部调用cancel(),它将阻塞,直到超时或外部cancel了
  • allow: 最终调用lim.reserveN(now, n, 0).ok,马上返回true或者false
  • reserve: 返回一个reservation,告知还要等多久才能满足条件

ratelimiter负责整体workqueue接收事件的频率。

graphBuilder

graphBuilder的结构体中包含一些不容易理解的内容

  • restMapper:我也没有完全理解,等看完apiserver再补充吧……
  • monitors: 每个monitor list/watch一类资源,结合sharedInformers controller.InformerFactory大概就能猜到实现原理了

monitor

graphBuilder维护一个DAG图,在NewGarbageCollector中额外开了一个routine运行gb.syncMonitors(deletableResources),先看一下syncMonitors,它移除了所有现有的monitor,根据传入的deletableResources重建monitors。这个函数的源码比较简单就补贴了,它的关键函数是c, s, err := gb.controllerFor(resource, kind),而一个monitor就由controller和store组成。

看源码很好理解,informer收到资源变化的事件之后直接把事件交给graphChanges这个workqueue,通过sharedInformerFactory生成对应资源的generic informer。而monitor其实是informer的controller和store的重新封装。

func (gb *GraphBuilder) controllerFor(resource schema.GroupVersionResource, kind schema.GroupVersionKind) (cache.Controller, cache.Store, error) {
	handlers := cache.ResourceEventHandlerFuncs{
		// add the event to the dependencyGraphBuilder's graphChanges.
		AddFunc: func(obj interface{}) {
			event := &event{
				eventType: addEvent,
				obj:       obj,
				gvk:       kind,
			}
			gb.graphChanges.Add(event)
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
			// TODO: check if there are differences in the ownerRefs,
			// finalizers, and DeletionTimestamp; if not, ignore the update.
			event := &event{
				eventType: updateEvent,
				obj:       newObj,
				oldObj:    oldObj,
				gvk:       kind,
			}
			gb.graphChanges.Add(event)
		},
		DeleteFunc: func(obj interface{}) {
			// delta fifo may wrap the object in a cache.DeletedFinalStateUnknown, unwrap it
			if deletedFinalStateUnknown, ok := obj.(cache.DeletedFinalStateUnknown); ok {
				obj = deletedFinalStateUnknown.Obj
			}
			event := &event{
				eventType: deleteEvent,
				obj:       obj,
				gvk:       kind,
			}
			gb.graphChanges.Add(event)
		},
	}
	shared, err := gb.sharedInformers.ForResource(resource)
	if err != nil {
		klog.V(4).Infof("unable to use a shared informer for resource %q, kind %q: %v", resource.String(), kind.String(), err)
		return nil, nil, err
	}
	klog.V(4).Infof("using a shared informer for resource %q, kind %q", resource.String(), kind.String())
	// need to clone because it's from a shared cache
	shared.Informer().AddEventHandlerWithResyncPeriod(handlers, ResourceResyncTime)
	return shared.Informer().GetController(), shared.Informer().GetStore(), nil
}
复制代码

garbageCollector的运行

New之后进行了两个动作:run和sync。

run调用了go gc.dependencyGraphBuilder.Run(stopCh),其中调用了gb.startMonitors(),之后进行wait.Until(gb.runProcessGraphChanges, 1*time.Second, stopCh)。然后缓存同步之后,开始垃圾回收

	// gc workers
	for i := 0; i < workers; i++ {
		go wait.Until(gc.runAttemptToDeleteWorker, 1*time.Second, stopCh)
		go wait.Until(gc.runAttemptToOrphanWorker, 1*time.Second, stopCh)
	}
复制代码

go garbageCollector.Sync(gcClientset.Discovery(), 30*time.Second, ctx.Stop),这个函数每30s调用一次GetDeletableResources,获取到最新的资源之后,调用gc.restMapper.Reset()重置了resetMapper中的内容,之后运行resyncMonitors,其中调用了syncMonitors重建所有的Monitor,并调用startMonitors。sync函数其实就是重置了所有的缓存,刷新了可回收的资源类型,并重启了相应的monitor。

接下来我们看一下什么样的对象会被回收。回到之前Monitor注册的回调函数gb.graphChanges.Add(event)中,所有的对象变化都将加入到graphChanges这个队列中,那么处理的关键就在runProcessGraphChanges这个函数中,它将进入死循环,调用processGraphChanges()

先看这一段:将item转为events类型,event包括

  • eventType
  • obj:interface{}
  • oldObj
  • gvk:groupVersionKind

从obj转换成metav1.Ojbect通过accessor, err := meta.Accessor(obj)完成。

	item, quit := gb.graphChanges.Get()
	if quit {
		return false
	}
	defer gb.graphChanges.Done(item)
	event, ok := item.(*event)
	if !ok {
		utilruntime.HandleError(fmt.Errorf("expect a *event, got %v", item))
		return true
	}
	obj := event.obj
	accessor, err := meta.Accessor(obj)
	if err != nil {
		utilruntime.HandleError(fmt.Errorf("cannot access obj: %v", err))
		return true
	}
复制代码

之后再uidToNode中查看UID对应的对象是否存在。uidToNode维护map[types.UID]*node,这里的node不是corev1.Node,是DAG图中的一个节点,后面会看到具体的用法。如果能找到,说明这个节点是真实存在的。

	existingNode, found := gb.uidToNode.Read(accessor.GetUID())
	if found {
		// this marks the node as having been observed via an informer event
		// 1. this depends on graphChanges only containing add/update events from the actual informer
		// 2. this allows things tracking virtual nodes' existence to stop polling and rely on informer events
		existingNode.markObserved()
	}
复制代码

之后开始一个switch case,有三种情况

  • event type是add或者update,UID对应的节点没有找到:
    • 新建一个节点
    • gb.insertNode(newNode)
    • gb.processTransitions(event.oldObj, accessor, newNode)
  • event type是add或者update,UID对应的节点可以找到:
    • added, removed, changed := referencesDiffs(existingNode.owners, accessor.GetOwnerReferences())
    • gb.processTransitions(event.oldObj, accessor, existingNode)
  • event type是delete:
    • 级联的删除所有相关资源

attemptToOrphan队列

garbage collector attempts to orphan the dependents of the items in the attemptToOrphan queue, then deletes the items.

与attemptToDelete队列队列的区别在于,多了一步orphan。在runAttemptToOrphanWorker()中处理该队列,主要依赖orphanDependentsremoveFinalizer两个步骤。

  • orphanDependents:找到所有的dependents项,从dependents的ownerRefernece中删掉该object
  • 在object中移除orphan finalizer

期间任何的错误都将根据workqueue的机制重新入队。

attemptToDelete队列

garbage collector attempts to delete the items in attemptToDelete queue when the time is ripe.

runAttemptToDeleteWorker()中处理该队列,主要逻辑是attemptToDeleteItem,它将尝试从apiserver中获取node对应的对象,如果在apiserver中不存在,则说明这是一个virtual node,enqueue一个virtual delete操作,将该虚拟的节点删除。

如果node处于deletingDependents状态,如果没有blockingDependents,移除foregroundDeletion的finalizer。如果有blockingDependents,尝试将所有的blockingDependents加入attemptToDelete队列。

node对象

node结构体记录了节点包含的对象的属性、对象是否真实存在、对象的父对象owners和子对象dependents,以及一些锁。对象是否真实存在其实并不是说我们可以虚构一个对象加入到队列中,而是指可能该object的子对象加入时,在map中无法找到父对象,可能这时父对象尚未入队,并不是程序错误。这种情况下,我们应该创建虚拟对象。

type node struct {
	identity objectReference
	// dependents will be read by the orphan() routine, we need to protect it with a lock.
	dependentsLock sync.RWMutex
	// dependents are the nodes that have node.identity as a
	// metadata.ownerReference.
	dependents map[*node]struct{}
	// this is set by processGraphChanges() if the object has non-nil DeletionTimestamp
	// and has the FinalizerDeleteDependents.
	deletingDependents     bool
	deletingDependentsLock sync.RWMutex
	// this records if the object's deletionTimestamp is non-nil.
	beingDeleted     bool
	beingDeletedLock sync.RWMutex
	// this records if the object was constructed virtually and never observed via informer event
	virtual     bool
	virtualLock sync.RWMutex
	// when processing an Update event, we need to compare the updated
	// ownerReferences with the owners recorded in the graph.
	owners []metav1.OwnerReference
}
复制代码

情况一:event type是add或者update,UID对应的节点没有找到

接收到事件后,需要在uidToNodes中记录该事件。首先要新建一个node结构体。node中的identity是一个objectReference,包含ownerReference和namespace,其实我并不喜欢这个命名,有点混淆。ownerReference其实记录的是object自己的UID和name,即identity记录着event的object的信息。

之后把node插入到uidToNodes中。insertNode()中一方面要在map中记录node信息,之后还要把node信息添加到ownerReference对象所在的node中。如果ownerNode不存在,不得已创建一个虚拟的对象,还要把它额外加入attemptToDelete队列中。看一下创建的node

		newNode := &node{
			identity: objectReference{
				OwnerReference: metav1.OwnerReference{
					APIVersion: event.gvk.GroupVersion().String(),
					Kind:       event.gvk.Kind,
					UID:        accessor.GetUID(),
					Name:       accessor.GetName(),
				},
				Namespace: accessor.GetNamespace(),
			},
			dependents:         make(map[*node]struct{}),
			owners:             accessor.GetOwnerReferences(),
			deletingDependents: beingDeleted(accessor) && hasDeleteDependentsFinalizer(accessor),
			beingDeleted:       beingDeleted(accessor),
		}
复制代码

identity比较混淆的用了ownerReference和Namespace两个属性,其实它们都在讲这个object自身,这个ownerReference和下面的owners是两回事。dependents初始化为空,owners是它的ownerReference指向的对象。deletingDependents意味着是否要删除子对象,当且仅当该对象正在删除且删除模式为foregroundDeletion时才为true。关于级联删除的说明可以在级联删除页面查看。

之后执行processTransitions(),执行了两个判断。第一个判断startsWaitingForDependentsOrphaned(),如果为真,则将节点放入attemptToOrphan队列中;第二个判断startsWaitingForDependentsDeleted()如果为真,将节点的deletingDependents设为真,将节点的子对象与节点放入attemptToDelete队列。

第一个判断的条件为deletionStarts(oldObj, newAccessor) && hasOrphanFinalizer(newAccessor)

  • deletionStarts判断对象是否正在删除。如果oldObj为空,即可能对象刚刚创建出来,informer把create和update事件合并了,就只要判断newObj是否正在删除;否则只有老对象正在删除,老对象没有删除,才返回真。
  • hasOrphanFinalizer判断删除模式是否为orphan。

第二个判断的条件为deletionStarts(oldObj, newAccessor) && hasDeleteDependentsFinalizer(newAccessor)

  • hasDeleteDependentsFinalizer判断删除模式是否为foregroundDeletion

情况二:event type是add或者update,UID对应的节点可以找到

要比较其owners是否有变化,一旦有变化,对于移除与改变的owners来说

if an blocking ownerReference points to an object gets removed, or gets set to "BlockOwnerDeletion=false", add the object to the attemptToDelete queue.

之后在existingNode上加入新增的owners,删除旧的owners。之后执行processTransitions()

情况三:event type是delete

移除object对应的node,如果node有dependents,将object的UID加入absentOwnerCache。之后将dependents加入attemptToDelete队列。之后遍历其owners,如果有owner的deletingDependents为真,将owner加入attemptToDelete队列。

garbageCollector行为总结

删除object时,总是尝试删除所有的dependents,不同的是:

  • 如果删除模式是foreground,则给object打上deletingDependents的标签,此时用户可以从apiserver中看到该资源,它将一直阻塞,知道所有的blockingDependents都被删除。
  • 如果删除模式是orphan,其原理是在所有的dependents的ownerReference中删除该object,然后移除orphan这个finalizer
  • 普通的background模式将直接删除该object,同时删除所有的dependents。

添加或者更新object时,可能owners此时在cache中尚未存在,则将创建一个假的资源,之后尝试删掉这个假的资源。

garbage collector并不是真正的删除etcd的信息,而是管理DAG图,控制各对象的finalizer,真正的删除还是交给kubelet完成的。

分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改