设想这么一个场景:我们在 K8s 上创建了一个对象,它根据需要生成副本集和 Pod。在检查时,我们遗漏了容器某个属性的设置,因此又重新编辑了 Deployment。新的 Deployment 就产生了新的副本集对象和新的 Pod。这里就出现了一个问题,旧的副本集和 Pop 去哪了?另外,如果直接删除 Deployment,那副本集和 Pod 又会如何?事实就是,在删除 Deployment 后,副本集和 Pod 也会一起被删除,要不然集群早就乱套了。
在这个场景之下,我们可以深入思考几个问题:在 K8s 中该如何实现级联删除?有几种级联删除策略?在 K8s 中有没有可能存在孤儿对象(orphan object)?这些问题其实就是典型的垃圾回收(garbage collection,GC)问题。本文将介绍 K8s 中垃圾回收的概念以及实现方法。
什么是垃圾回收?
一般来说,垃圾回收(GC)就是从系统中删除未使用的对象,并释放分配给它们的计算资源。GC 存在于所有的高级编程语言中,较低级的编程语言通过系统库实现 GC。
GC 最常见的算法之一是 mark-and-sweep,这个算法会标记将删除的对象,再进行删除,如下图所示:
OwnerRefernce
在面向对象的语言中,一些对象会引用其他对象或者直接由其他对象组成,k8s 也有类似形式,例如副本集管理一组 Pod,而 Deployment 又管理着副本集。
但与面向对象语言不同的是,在 K8s 对象的定义中,没有明确所有者之间的关系,那么系统要如何确定它们的关系呢?其实,在 K8s 中,每个从属对象都具有 唯一数据字段名称 metadata.ownerReferences
用于确定关系。
从 Kubernetes v1.8 开始,K8s 对于 ReplicaSet、StatefulSet、DaemonSet、Deployment、Job、 CronJob 等创建或管理的对象,会自动为其设置 ownerReferences 的值。如果有需要,我们还可以手动设置 ownerReferences。
以下内容显示了 core-dns Deployment 上 metadata.ownerReferences
的值。
k get deployment -n kube-system -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES
coredns 2/2 2 2 44d coredns k8s.gcr.io/coredns:1.6.7
k get rs -n kube-system -o json | jq ".items[0].metadata.name, .items[0].metadata.ownerReferences"
"coredns-66bff467f8"
[
{
"apiVersion": "apps/v1",
"blockOwnerDeletion": true,
"controller": true,
"kind": "Deployment",
"name": "coredns",
"uid": "d8f29b78-439c-497e-9a45-7c33bd626a9f"
}
]
k get pods coredns-66bff467f8-rsnmg -n kube-system -o json | jq ".metadata.name, .metadata.ownerReferences"
"coredns-66bff467f8-rsnmg"
[
{
"apiVersion": "apps/v1",
"blockOwnerDeletion": true,
"controller": true,
"kind": "ReplicaSet",
"name": "coredns-66bff467f8",
"uid": "085d5398-1358-43e2-918e-2e03da18c7bd"
}
]
认真观察上述命令的输出,其实它和其他对象 GC 之间是有些许差别的。对象关联参考金字塔是颠倒的:
K8s 的垃圾回收策略
如前面所讲,在 Kubernetes v1.8 之前,依赖对象逻辑删除的实现是在客户端,对于某些资源而言则是在控制器端。有时,客户端会中途失败,导致集群状态混乱,需要手动清理。后来为了解决这个问题,K8s 社区引入并实现了 Garbage Collector Controller(垃圾回收器),用更好用且更简单的方式实现 GC。在 K8s 中,有两大类 GC:
- 级联(Cascading):在级联删除中,所有者被删除,那集群中的从属对象也会被删除。
- 孤儿(Orphan):这种情况下,对所有者的进行删除只会将其从集群中删除,并使所有对象处于“孤儿”状态。
级联删除
在级联删除(cascading deletion strategy)中,从属对象(dependent object)与所有者对象(owner object)会被一起删除。在级联删除中,又有两种模式:前台(foreground)和后台(background)。
前台级联删除(Foreground Cascading Deletion):在这种删除策略中,所有者对象的删除将会持续到其所有从属对象都被删除为止。当所有者被删除时,会进入“正在删除”(deletion in progress)状态,此时:
- 对象仍然可以通过 REST API 查询到(可通过 kubectl 或 kuboard 查询到)
- 对象的 deletionTimestamp 字段被设置
- 对象的 metadata.finalizers 包含值 foregroundDeletion
一旦对象被设置为 “正在删除” 状态,垃圾回收器将删除其从属对象。当垃圾回收器已经删除了所有的“blocking”从属对象(ownerReference.blockOwnerDeletion=true 的对象)以后,将删除所有者对象。
后台级联删除(Background Cascading Deletion):这种删除策略会简单很多,它会立即删除所有者的对象,并由垃圾回收器在后台删除其从属对象。这种方式比前台级联删除快的多,因为不用等待时间来删除从属对象。
孤儿删除
在孤儿删除策略(orphan deletion strategy)中,会直接删除所有者对象,并将从属对象中的 ownerReference 元数据设置为默认值。之后垃圾回收器会确定孤儿对象并将其删除。
垃圾回收器如何工作?
如果对象的 OwnerReferences 元数据中没有任何所有者对象,那么垃圾回收器会删除该对象。垃圾回收器由 Scanner、Garbage Processor 和 Propagator 组成:
Scanner:它会检测 K8s 集群中支持的所有资源,并通过控制循环周期性地检测。它会扫描系统中的所有资源,并将每个对象添加到"脏队列"(dirty queue)中。
Garbage Processor:它由在"脏队列"上工作的 worker 组成。每个 worker 都会从"脏队列"中取出对象,并检查该对象里的 OwnerReference 字段是否为空。如果为空,那就从“脏队列”中取出下一个对象进行处理;如果不为空,它会检测 OwnerReference 字段内的 owner resoure object 是否存在,如果不存在,会请求 API 服务器删除该对象。
Propagator :用于优化垃圾回收器,它包含以下三个组件:
- EventQueue:负责存储 k8s 中资源对象的事件
- DAG(有向无环图):负责存储 k8s 中所有资源对象的 owner-dependent 关系
- Worker:从 EventQueue 中取出资源对象的事件,并根据事件的类型会采取操作
在有了 Propagator 的加入之后,我们完全可以仅在 GC 开始运行的时候,让 Scanner 扫描系统中所有的对象,然后将这些信息传递给 Propagator 和“脏队列”。只要 DAG 一建立起来之后,那么 Scanner 其实就没有再工作的必要了。
总体而言,K8s 中 GC 的实现是非常通用且非常有效,希望这篇文章可以帮助大家更加了解 K8s 中的 GC。