在前面几篇关于 controller 源码分析的文章中多次提到了当删除一个对象时,其对应的 controller 并不会执行删除对象的操作,在 kubernetes 中对象的回收操作是由 GarbageCollectorController 负责的,其作用就是当删除一个对象时,会根据指定的删除策略回收该对象及其依赖对象,本文会深入分析垃圾收集背后的实现。
kubernetes 中的删除策略
kubernetes 中有三种删除策略:Orphan
、Foreground
和 Background
,三种删除策略的意义分别为:
Orphan
策略:非级联删除,删除对象时,不会自动删除它的依赖或者是子对象,这些依赖被称作是原对象的孤儿对象,例如当执行以下命令时会使用Orphan
策略进行删除,此时 ds 的依赖对象controllerrevision
不会被删除;
1 | $ kubectl delete ds/nginx-ds --cascade=false |
Background
策略:在该模式下,kubernetes 会立即删除该对象,然后垃圾收集器会在后台删除这些该对象的依赖对象;Foreground
策略:在该模式下,对象首先进入“删除中”状态,即会设置对象的deletionTimestamp
字段并且对象的metadata.finalizers
字段包含了值 “foregroundDeletion”,此时该对象依然存在,然后垃圾收集器会删除该对象的所有依赖对象,垃圾收集器在删除了所有“Blocking” 状态的依赖对象(指其子对象中ownerReference.blockOwnerDeletion=true
的对象)之后,然后才会删除对象本身;
在 v1.9 以前的版本中,大部分 controller 默认的删除策略为 Orphan
,从 v1.9 开始,对于 apps/v1 下的资源默认使用 Background
模式。以上三种删除策略都可以在删除对象时通过设置 deleteOptions.propagationPolicy
字段进行指定,如下所示:
1 | $ curl -k -v -XDELETE -H "Accept: application/json" -H "Content-Type: application/json" -d '{"propagationPolicy":"Foreground"}' 'https://192.168.99.108:8443/apis/apps/v1/namespaces/default/daemonsets/nginx-ds' |
finalizer 机制
finalizer 是在删除对象时设置的一个 hook,其目的是为了让对象在删除前确认其子对象已经被完全删除,k8s 中默认有两种 finalizer:OrphanFinalizer
和 ForegroundFinalizer
,finalizer 存在于对象的 ObjectMeta 中,当一个对象的依赖对象被删除后其对应的 finalizers 字段也会被移除,只有 finalizers 字段为空时,apiserver 才会删除该对象。
1 | { |
此外,finalizer 不仅仅支持以上两种字段,在使用自定义 controller 时也可以在 CR 中设置自定义的 finalizer 标识。
GarbageCollectorController 源码分析
kubernetes 版本:v1.16
GarbageCollectorController 负责回收 kubernetes 中的资源,要回收 kubernetes 中所有资源首先得监控所有资源,GarbageCollectorController 会监听集群中所有可删除资源产生的所有事件,这些事件会被放入到一个队列中,然后 controller 会启动多个 goroutine 处理队列中的事件,若为删除事件会根据对象的删除策略删除关联的对象,对于非删除事件会更新对象之间的依赖关系。
startGarbageCollectorController
首先还是看 GarbageCollectorController 的启动方法 startGarbageCollectorController
,其主要逻辑为:
- 1、初始化 discoveryClient,discoveryClient 主要用来获取集群中的所有资源;
- 2、调用
garbagecollector.GetDeletableResources
获取集群内所有可删除的资源对象,支持 “delete”, “list”, “watch” 三种操作的 resource 称为deletableResource
; - 3、调用
garbagecollector.NewGarbageCollector
初始化 garbageCollector 对象; - 4、调用
garbageCollector.Run
启动 garbageCollector; - 5、调用
garbageCollector.Sync
监听集群中的DeletableResources
,当出现新的DeletableResources
时同步到 monitors 中,确保监控集群中的所有资源; - 6、调用
garbagecollector.NewDebugHandler
注册 debug 接口,用来提供集群内所有对象的关联关系;
k8s.io/kubernetes/cmd/kube-controller-manager/app/core.go:443
1 | func startGarbageCollectorController(ctx ControllerContext) (http.Handler, bool, error) { |
在 startGarbageCollectorController
中主要调用了四种方法garbagecollector.NewGarbageCollector
、garbageCollector.Run
、garbageCollector.Sync
和 garbagecollector.NewDebugHandler
来完成核心功能,下面主要针对这四种方法进行说明。
garbagecollector.NewGarbageCollector
NewGarbageCollector
的主要功能是初始化 GarbageCollector 和 GraphBuilder 对象,并调用 gb.syncMonitors
方法初始化 deletableResources 中所有 resource controller 的 informer。GarbageCollector 的主要作用是启动 GraphBuilder 以及启动所有的消费者,GraphBuilder 的主要作用是启动所有的生产者。
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:74
1 | func NewGarbageCollector(......) (*GarbageCollector, error) { |
gb.syncMonitors
syncMonitors
的主要作用是初始化各个资源对象的 informer,并调用 gb.controllerFor
为每种资源注册 eventHandler,此处每种资源被称为 monitors,因为为每种资源注册 eventHandler 时,对于 AddFunc、UpdateFunc 和 DeleteFunc 都会将对应的 event push 到 graphChanges 队列中,每种资源对象的 informer 都作为生产者。
k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:179
1 | func (gb *GraphBuilder) syncMonitors(resources map[schema.GroupVersionResource]struct{}) error { |
gb.controllerFor
在 gb.controllerFor
中主要是为每个 deletableResources 的 informer 注册 eventHandler,此处就可以看到真正的生产者了。
k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:127
1 | func (gb *GraphBuilder) controllerFor(resource schema.GroupVersionResource, kind schema.GroupVersionKind) (cache.Controller, cache.Store, error) { |
至此 NewGarbageCollector
的功能已经分析完了,在 NewGarbageCollector
中初始化了两个对象 GarbageCollector 和 GraphBuilder,然后在 gb.syncMonitors
中初始化了所有 deletableResources 的 informer,为每个 informer 添加 eventHandler 并将监听到的所有 event push 到 graphChanges 队列中,此处每个 informer 都被称为 monitor,所有 informer 都被称为生产者。graphChanges 是 GraphBuilder 中的一个对象,GraphBuilder 的主要功能是作为一个生产者,其会处理 graphChanges 中的所有事件并进行分类,将事件放入到 attemptToDelete 和 attemptToOrphan 两个队列中,具体处理逻辑下文讲述。
NewGarbageCollector
中的调用逻辑如下所示:
1 | |--> ctx.ClientBuilder. |
garbageCollector.Run
上文已经详述了 NewGarbageCollector
的主要功能,然后继续分析 startGarbageCollectorController
中的第二个核心方法 garbageCollector.Run
,garbageCollector.Run
的主要作用是启动所有的生产者和消费者,其首先会调用 gc.dependencyGraphBuilder.Run
启动所有的生产者,即 monitors,然后再启动一个 goroutine 处理 graphChanges 队列中的事件并分别放到 attemptToDelete 和 attemptToOrphan 两个队列中,dependencyGraphBuilder 即上文提到的 GraphBuilder,run
方法会调用 gc.runAttemptToDeleteWorker
和 gc.runAttemptToOrphanWorker
启动多个 goroutine 处理 attemptToDelete 和 attemptToOrphan 两个队列中的事件。
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:124
1 | func (gc *GarbageCollector) Run(workers int, stopCh <-chan struct{}) { |
Run
方法中调用了 gc.dependencyGraphBuilder.Run
来完成 GraphBuilder 的启动。
gc.dependencyGraphBuilder.Run
GraphBuilder 在 garbageCollector 整个环节中起到承上启下的作用,首先看一下 GraphBuilder 对象的结构:
1 | type GraphBuilder struct { |
uidToNode
此处有必要先说明一下 uidToNode 的功能,uidToNode 数据结构中维护着所有对象的依赖关系,此处的依赖关系是指比如当创建一个 deployment 时会创建对应的 rs 以及 pod,pod 的 owner 就是 rs,rs 的 owner 是 deployment,rs 的 dependents 是其关联的所有 pod,deployment 的 dependents 是其关联的所有 rs。
uidToNode 中的 node 不是指 k8s 中的 node 节点,而是将 graphChanges 中的 event 转换为 node 对象,k8s 中所有 object 之间的级联关系是通过 node 的概念来维护的,garbageCollector 在后续的处理中会直接使用 node 对象,node 对象定义如下:
1 | type concurrentUIDToNode struct { |
GraphBuilder 主要有三个功能:
- 1、监控集群中所有的可删除资源;
- 2、基于 informers 中的资源在 uidToNode 数据结构中维护着所有对象的依赖关系;
- 3、处理 graphChanges 中的事件并放到 attemptToDelete 和 attemptToOrphan 两个队列中;
上文已经说了 gc.dependencyGraphBuilder.Run
的功能,启动所有的 informers 然后再启动一个 goroutine 处理 graphChanges 队列中的事件并分别放到 attemptToDelete 和 attemptToOrphan 两个队列中,代码如下所示:
k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:281
1 | func (gb *GraphBuilder) Run(stopCh <-chan struct{}) { |
gc.dependencyGraphBuilder.Run
的核心是调用了 gb.startMonitors
和 gb.runProcessGraphChanges
两个方法来完成主要功能,继续看这两个方法的主要逻辑。
gb.startMonitors
startMonitors
的功能很简单就是启动所有的 informers,代码如下所示:
k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:232
1 | func (gb *GraphBuilder) startMonitors() { |
gb.runProcessGraphChanges
runProcessGraphChanges
方法的主要功能是处理 graphChanges 中的事件将其分别放到 GraphBuilder 的 attemptToDelete 和 attemptToOrphan 两个队列中,代码主要逻辑为:
- 1、从 graphChanges 队列中取出一个 item 即 event;
- 2、获取 event 的 accessor,accessor 是一个 object 的 meta.Interface,里面包含访问 object meta 中所有字段的方法;
- 3、通过 accessor 获取 UID 判断 uidToNode 中是否存在该 object;
- 4、若 uidToNode 中不存在该 node 且该事件是 addEvent 或 updateEvent,则为该 object 创建对应的 node,并调用
gb.insertNode
将该 node 加到 uidToNode 中,然后将该 node 添加到其 owner 的 dependents 中,执行完gb.insertNode
中的操作后再调用gb.processTransitions
方法判断该对象是否处于删除状态,若处于删除状态会判断该对象是以orphan
模式删除还是以foreground
模式删除,若以orphan
模式删除,则将该 node 加入到 attemptToOrphan 队列中,若以foreground
模式删除则将该对象以及其所有 dependents 都加入到 attemptToDelete 队列中; 5、若 uidToNode 中存在该 node 且该事件是 addEvent 或 updateEvent 时,此时可能是一个 update 操作,调用
referencesDiffs
方法检查该对象的OwnerReferences
字段是否有变化,若有变化(1)调用gb.addUnblockedOwnersToDeleteQueue
将被删除以及更新的 owner 对应的 node 加入到 attemptToDelete 中,因为此时该 node 中已被删除或更新的 owner 可能处于删除状态且阻塞在该 node 处,此时有三种方式避免该 node 的 owner 处于删除阻塞状态,一是等待该 node 被删除,二是将该 node 自身对应 owner 的OwnerReferences
字段删除,三是将该 nodeOwnerReferences
字段中对应 owner 的BlockOwnerDeletion
设置为 false;(2)更新该 node 的 owners 列表;(3)若有新增的 owner,将该 node 加入到新 owner 的 dependents 中;(4) 若有被删除的 owner,将该 node 从已删除 owner 的 dependents 中删除;以上操作完成后,检查该 node 是否处于删除状态并进行标记,最后调用gb.processTransitions
方法检查该 node 是否要被删除;举个例子,若以
foreground
模式删除 deployment 时,deployment 的 dependents 列表中有对应的 rs,那么 deployment 的删除会阻塞住等待其依赖 rs 的删除,此时 rs 有三种方法不阻塞 deployment 的删除操作,一是 rs 对象被删除,二是删除 rs 对象OwnerReferences
字段中对应的 deployment,三是将 rs 对象OwnerReferences
字段中对应的 deployment 配置BlockOwnerDeletion
设置为 false,文末会有示例演示该操作。6、若该事件为 deleteEvent,首先从 uidToNode 中删除该对象,然后从该 node 所有 owners 的 dependents 中删除该对象,将该 node 所有的 dependents 加入到 attemptToDelete 队列中,最后检查该 node 的所有 owners,若有处于删除状态的 owner,此时该 owner 可能处于删除阻塞状态正在等待该 node 的删除,将该 owner 加入到 attemptToDelete 中;
总结一下,当从 graphChanges 中取出 event 时,不管是什么 event,主要完成三件时,首先都会将 event 转化为 uidToNode 中的 node 对象,其次一是更新 uidToNode 中维护的依赖关系,二是更新该 node 的 owners 以及 owners 的 dependents,三是检查该 node 的 owners 是否要被删除以及该 node 的 dependents 是否要被删除,若需要删除则根据 node 的删除策略将其添加到 attemptToOrphan 或者 attemptToDelete 队列中;
k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:526
1 | func (gb *GraphBuilder) runProcessGraphChanges() { |
processTransitions
上述在处理 add 或 update event 时最后都调用了 processTransitions
方法检查 node 是否处于删除状态,若处于删除状态会通过其删除策略将 node 放到 attemptToOrphan 或 attemptToDelete 队列中。
k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:509
1 | func (gb *GraphBuilder) processTransitions(oldObj interface{}, newAccessor metav1.Object, n *node) { |
gc.runAttemptToDeleteWorker
runAttemptToDeleteWorker
是执行删除 attemptToDelete 中 node 的方法,其主要逻辑为:
- 1、调用
gc.attemptToDeleteItem
删除 node; - 2、若删除失败则重新加入到 attemptToDelete 队列中进行重试;
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:280
1 | func (gc *GarbageCollector) runAttemptToDeleteWorker() { |
gc.runAttemptToDeleteWorker
中调用了 gc.attemptToDeleteItem
执行实际的删除操作。
gc.attemptToDeleteItem
gc.attemptToDeleteItem
的主要逻辑为:
- 1、判断 node 是否处于删除状态;
- 2、从 apiserver 获取该 node 最新的状态,该 node 可能为 virtual node,若为 virtual node 则从 apiserver 中获取不到该 node 的对象,此时会将该 node 重新加入到 graphChanges 队列中,再次处理该 node 时会将其从 uidToNode 中删除;
- 3、判断该 node 最新状态的 uid 是否等于本地缓存中的 uid,若不匹配说明该 node 已更新过此时将其设置为 virtual node 并重新加入到 graphChanges 队列中,再次处理该 node 时会将其从 uidToNode 中删除;
4、通过 node 的
deletingDependents
字段判断该 node 当前是否处于删除 dependents 的状态,若该 node 处于删除 dependents 的状态则调用processDeletingDependentsItem
方法检查 node 的blockingDependents
是否被完全删除,若blockingDependents
已完全被删除则删除该 node 对应的 finalizer,若blockingDependents
还未删除完,将未删除的blockingDependents
加入到 attemptToDelete 中;上文中在 GraphBuilder 处理 graphChanges 中的事件时,若发现 node 处于删除状态,会将 node 的 dependents 加入到 attemptToDelete 中并标记 node 的
deletingDependents
为 true;- 5、调用
gc.classifyReferences
将 node 的ownerReferences
分类为solid
,dangling
,waitingForDependentsDeletion
三类:dangling
(owner 不存在)、waitingForDependentsDeletion
(owner 存在,owner 处于删除状态且正在等待其 dependents 被删除)、solid
(至少有一个 owner 存在且不处于删除状态); - 6、对以上分类进行不同的处理,若
solid
不为 0 即当前 node 至少存在一个 owner,该对象还不能被回收,此时需要将dangling
和waitingForDependentsDeletion
列表中的 owner 从 node 的ownerReferences
删除,即已经被删除或等待删除的引用从对象中删掉; - 7、第二种情况是该 node 的 owner 处于
waitingForDependentsDeletion
状态并且 node 的 dependents 未被完全删除,该 node 需要等待删除完所有的 dependents 后才能被删除; - 8、第三种情况就是该 node 已经没有任何 dependents 了,此时按照 node 中声明的删除策略调用 apiserver 的接口删除即可;
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:404
1 | func (gc *GarbageCollector) attemptToDeleteItem(item *node) error { |
gc.runAttemptToOrphanWorker
runAttemptToOrphanWorker
是处理以 orphan
模式删除的 node,主要逻辑为:
- 1、调用
gc.orphanDependents
删除 owner 所有 dependentsOwnerReferences
中的 owner 字段; - 2、调用
gc.removeFinalizer
删除 owner 的orphan
Finalizer; - 3、以上两步中若有失败的会进行重试;
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:574
1 | func (gc *GarbageCollector) runAttemptToOrphanWorker() { |
garbageCollector.Sync
garbageCollector.Sync
是 startGarbageCollectorController
中的第三个核心方法,主要功能是周期性的查询集群中所有的资源,过滤出 deletableResources
,然后对比已经监控的 deletableResources
和当前获取到的 deletableResources
是否一致,若不一致则更新 GraphBuilder 的 monitors 并重新启动 monitors 监控所有的 deletableResources
,该方法的主要逻辑为:
- 1、通过调用
GetDeletableResources
获取集群内所有的deletableResources
作为 newResources,deletableResources
指支持 “delete”, “list”, “watch” 三种操作的 resource,包括 CR; - 2、检查 oldResources, newResources 是否一致,不一致则需要同步;
- 3、调用
gc.resyncMonitors
同步 newResources,在gc.resyncMonitors
中会重新调用 GraphBuilder 的syncMonitors
和startMonitors
两个方法完成 monitors 的刷新; - 4、等待 newResources informer 中的 cache 同步完成;
- 5、将 newResources 作为 oldResources,继续进行下一轮的同步;
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:164
1 | func (gc *GarbageCollector) Sync(discoveryClient discovery.ServerResourcesInterface, period time.Duration, stopCh <-chan struct{}) { |
garbageCollector.Sync
中主要调用了两个方法,一是调用 GetDeletableResources
获取集群中所有的可删除资源,二是调用 gc.resyncMonitors
更新 GraphBuilder 中 monitors。
GetDeletableResources
在 GetDeletableResources
中首先通过调用 discoveryClient.ServerPreferredResources
方法获取集群内所有的 resource 信息,然后通过调用 discovery.FilteredBy
过滤出支持 “delete”, “list”, “watch” 三种方法的 resource 作为 deletableResources
。
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:636
1 | func GetDeletableResources(discoveryClient discovery.ServerResourcesInterface) map[schema.GroupVersionResource]struct{} { |
ServerPreferredResources
ServerPreferredResources
的主要功能是获取集群内所有的 resource 以及其 group、version、verbs 信息,该方法的主要逻辑为:
- 1、调用
ServerGroups
方法获取集群内所有的 GroupList,ServerGroups
方法首先从 apiserver 通过/api
URL 获取当前版本下所有可用的APIVersions
,再通过/apis
URL 获取 所有可用的APIVersions
以及其下的所有APIGroupList
; - 2、调用
fetchGroupVersionResources
通过 serverGroupList 再获取到对应的 resource; - 3、将获取到的 version、group、resource 构建成标准格式添加到
metav1.APIResourceList
中;
k8s.io/kubernetes/staging/src/k8s.io/client-go/discovery/discovery_client.go:285
1 | func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) { |
GetDeletableResources
方法中的调用流程为:
1 | |--> d.ServerGroups |
gc.resyncMonitors
gc.resyncMonitors
的主要功能是更新 GraphBuilder 的 monitors 并重新启动 monitors 监控所有的 deletableResources,GraphBuilder 的 syncMonitors
和 startMonitors
方法在前面的流程中已经分析过,此处不再详细说明。
k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:116
1 | func (gc *GarbageCollector) resyncMonitors(deletableResources map[schema. GroupVersionResource]struct{}) error { |
garbagecollector.NewDebugHandler
garbagecollector.NewDebugHandler
主要功能是对外提供一个接口供用户查询当前集群中所有资源的依赖关系,依赖关系可以以图表的形式展示。
1 | func startGarbageCollectorController(ctx ControllerContext) (http.Handler, bool, error) { |
具体使用方法如下所示:
1 | $ curl http://192.168.99.108:10252/debug/controllers/garbagecollector/graph > tmp.dot |
依赖关系图如下所示:
示例
在此处会有一个小示例验证一下源码中的删除阻塞逻辑,当以 Foreground
策略删除一个对象时,该对象会处于阻塞状态等待其依依赖被删除,此时有三种方式避免该对象处于删除阻塞状态,一是将依赖对象直接删除,二是将依赖对象自身的 OwnerReferences
中 owner 字段删除,三是将该依赖对象 OwnerReferences
字段中对应 owner 的 BlockOwnerDeletion
设置为 false,下面会验证下这三种方式,首先创建一个 deployment,deployment 创建出的 rs 默认不会有 foregroundDeletion finalizers
,此时使用 kubectl edit 手动加上 foregroundDeletion finalizers
,当 deployment 正常运行时,如下所示:
1 | $ kubectl get deployment nginx-deployment |
当 deployment、rs、pod 都处于正常运行状态且 deployment 关联的 rs 使用 Foreground
删除策略时,然后验证源码中提到的三种方法,验证时需要模拟一个依赖对象无法删除的场景,当然这个也很好模拟,三种场景如下所示:
- 1、当 pod 所在的 node 处于 Ready 状态时,以
Foreground
策略删除 deploment,因为 rs 关联的 pod 会直接被删除,rs 也会被正常删除,此时 deployment 也会直接被删除; - 2、当 pod 所在的 node 处于 NotReady 状态时,以
Foreground
策略删除 deploment,此时因 rs 关联的 pod 无法被删除,rs 会一直处于删除阻塞状态,deployment 由于 rs 无法被删除也会处于删除阻塞状态,此时更新 rs 去掉其ownerReferences
中对应的 deployment 部分,deployment 会因无依赖对象被成功删除; - 3、和 2 同样的场景,node 处于 NotReady 状态时,以
Foreground
策略删除 deploment,deployment 和 rs 将处于删除阻塞状态,此时将 rsownerReferences
中关联 deployment 的blockOwnerDeletion
字段设置为 false,可以看到 deployment 会因无 block 依赖对象被成功删除;
1 | $ systemctl stop kubelet |
总结
GarbageCollectorController 是一种典型的生产者消费者模型,所有 deletableResources
的 informer 都是生产者,每种资源的 informer 监听到变化后都会将对应的事件 push 到 graphChanges 中,graphChanges 是 GraphBuilder 对象中的一个数据结构,GraphBuilder 会启动另外的 goroutine 对 graphChanges 中的事件进行分类并放在其 attemptToDelete 和 attemptToOrphan 两个队列中,garbageCollector 会启动多个 goroutine 对 attemptToDelete 和 attemptToOrphan 两个队列中的事件进行处理,处理的结果就是回收一些需要被删除的对象。最后,再用一个流程图总结一下 GarbageCollectorController 的主要流程:
1 | monitors (producer) |