删除的时候到底发生了什么
一句话总结
Kubernetes 中并没有统计所有权关系的账簿。只有子对象指向父对象的一行 ownerReferences,而垃圾回收器就相信这一行来行动。所以如果把这一行写错,故障会悄无声息地发生。
为什么需要这种设计
Operator 接收一个自定义资源,创建出多个实际资源。例如一个 Site 对应两个 ConfigMap、一个 Service、一个 Secret。那么删除 Site 时,这四个对象由谁来删除?
让控制器直接删除,看起来很简单,但有问题。如果控制器停止运行期间父对象被删除,子对象就会永远留下来。如果把控制器整个撤掉,这些资源就会变成没人知道的垃圾。所以 Kubernetes 把清理工作交给了API 服务器一侧的垃圾回收器,而不是控制器,并让子对象的元数据中写入该回收器要读取的信息。
metadata:
ownerReferences:
- apiVersion: own.labhub.io/v1
kind: Site
name: alpha
uid: 8f1c... # 진짜 열쇠는 이것
controller: true # 이 참조가 '관리자' 인가
blockOwnerDeletion: true
四个字段是必填的——apiVersion、kind、name、uid。其中真正用来查找父对象的是 uid。名称是给人读的值,垃圾回收器是用 uid 来确认父对象是否存在的。
工作原理
uid 写错,子对象就会被删除。从回收器的角度看,“没有拥有那个 uid 的对象”等同于“父对象已被删除”。所以它会在几秒之内清理子对象。这个性质在现场引发事故的典型路径是这样的——控制器缓存了父对象的 uid,而在此期间父对象被删除,又以相同的名称重新创建。新父对象的 uid 不同。以旧 uid 创建的子对象,刚创建就消失了。
不存在跨命名空间的所有权。命名空间范围的子对象只能有同一个命名空间中的父对象,只有集群范围的对象是例外,可以拥有任意命名空间的子对象。违反规则的引用不会在 apply 时被拦下。取而代之的是,回收器之后清理那个子对象时,会留下名为 OwnerRefInvalidNamespace 的警告事件。事件默认一小时就会消失,所以如果过了这段时间才开始调查,几乎没有办法知道子对象为什么不见了。
删除传播有三种。
--cascade |
父对象 | 子对象 | 何时使用 |
|---|---|---|---|
background(默认) |
立即消失 | 回收器随后删除 | 普通的删除 |
foreground |
在子对象全部消失之后才消失 | 先被删除 | 清理顺序很重要时 |
orphan |
立即消失 | 保留,只是引用被解除 | 只想撤掉 Operator 时 |
foreground 为了表达等待,会给父对象加上名为 foregroundDeletion 的终结器(finalizer)。父对象会带着 deletionTimestamp 保留下来,blockOwnerDeletion: true 的子对象全部消失之后,它才会消失。orphan 不删除子对象,但会把子对象上的所有者引用摘掉。如果不摘掉,没有父对象的引用会留下来,下次清理时子对象就会消失。
终结器不是阻止删除的机制,而是延迟删除的机制。只要有一个终结器存在,删除请求就只会打上 deletionTimestamp 然后结束。对象仍然可以被查询和修改,但不能以相同的名称重新创建。控制器会在这段时间里清理外部世界(释放云资源、保存备份、断开连接),完成后把自己的终结器从列表中去掉。列表变空的那一刻,对象才真正消失。
在现场相遇的样子
第一,卡在 Terminating 无法解除的对象。最常见的原因是没有能去掉终结器的控制器——先撤掉 Operator,再删除 CR,就会恰好变成这样。这时人们最常做的,是手动摘掉终结器,但那等于跳过了清理,云资源会原样留下,费用还会继续产生。顺序必须相反——先清理 CR,再撤掉 Operator。
第二,命名空间卡在 Terminating。原因通常是其中某个对象带着终结器。如果强行删除命名空间,其中的对象没有得到清理,只是记录消失了。
第三,预先建立诊断流程。遇到卡住的对象时,要问的有三件事——deletionTimestamp 是什么时候打上的,还剩下哪些终结器,负责去掉这些终结器的主体现在是否还活着。如果还剩 foregroundDeletion,还要再加一个问题——blockOwnerDeletion 为 true 的子对象中,还有哪个没有消失。预先做一个一次性提取这四项的脚本,出事故时就不用从头思考了。
本实验环境的局限
kwok 集群中的 Pod 是假的,所以容器不会真正运行。因此,在终结器中清理外部系统的真实工作(调用云 API、释放卷)只是模拟。反过来,垃圾回收器和删除传播本身是由真实的 kube-controller-manager 处理的,所以 uid 错误的子对象会消失,以及 foreground 删除留下终结器而被卡住,都与实际行为完全一致。
下一项实验要做什么
创建名为 Site 的父类型,并排创建 uid 正确和错误的子对象,观察垃圾回收器会做什么。确认跨命名空间的引用所留下的警告事件,分别运行三种 --cascade,用对象来证明结果。最后,针对用 foreground 停住的对象,编写一个一次性提取被卡住的父对象和阻塞它的子对象的诊断脚本。