k8s删除pod_揭秘 Kubernetes 漏洞致使 pod 启动失败问题
文章来源:腾讯云容器团队 /原文链接作者蔡靖,腾讯高级后台开发工程师,拥有多年大规模 Kubernetes 集群开发运维经验。目前负责腾讯云 TKE 存储组件的功能特性实现,以及稳定性与性能的提升。本次分享以 controller manager 未能正常挂载 volume 致使 pod 启动失败的案例展开,通过问题根因分析过程以及如何制定解决方案等内容,帮助大家深入理解 k8s at...
作者蔡靖,腾讯高级后台开发工程师,拥有多年大规模 Kubernetes 集群开发运维经验。目前负责腾讯云 TKE 存储组件的功能特性实现,以及稳定性与性能的提升。本次分享以 controller manager 未能正常挂载 volume 致使 pod 启动失败的案例展开,通过问题根因分析过程以及如何制定解决方案等内容,帮助大家深入理解 k8s attach/detach controller。
1. 前言
本文主要通过深入学习 k8s attach/detach controller 源码,挖掘出 controller manager 未能正常挂载 volume 致使 pod 启动失败这一案例发生 attach/detach controller bug 的根因,进而给出解决方案。
看完本文你将学习到:
attach/detach controller 的主要数据结构有哪些,保存什么数据,数据从哪来,到哪去等等;
k8s attach/detach volume 的详细流程,如何判断 volume 是否需要 attach/detach,attach/detach controller 和 kubelet (volume manager) 如何协同工作等等。
2. 案例背景
本节我们首先了解下案例的问题和现象;然后去深入理解 ad controller 维护的数据结构;之后根据数据结构与 ad controller 的代码逻辑,再来详细分析案例出现的原因和解决方案,从而深入理解整个 ad controller。
2.1 问题描述
一个 statefulsets(sts) 引用了多个 pvc cbs,我们更新 sts 时,删除旧 pod,创建新 pod,此时如果删除旧 pod 时 cbs detach 失败,且创建的新 pod 调度到和旧 pod 相同的节点,就可能会让这些 pod 一直处于
ContainerCreating
。
2.2 现象
kubectl describe pod
kubelet log
kubectl get node xxx -oyaml
的volumesAttached
和volumesInUse
volumesAttached:
- devicePath: /dev/disk/by-id/virtio-disk-6w87j3wv
name: kubernetes.io/qcloud-cbs/disk-6w87j3wv
volumesInUse:
- kubernetes.io/qcloud-cbs/disk-6w87j3wv
- kubernetes.io/qcloud-cbs/disk-7bfqsft5
3. k8s 存储简述
k8s 中 attach/detach controller 负责存储插件的 attach/detach。本文结合一个具体案例来分析 ad controller 的源码逻辑,该案例是因 k8s 的 ad controller bug 导致的 pod 创建失败。
k8s 中涉及存储的组件主要有:attach/detach controller、pv controller、volume manager、volume plugins、scheduler。每个组件分工明确:
attach/detach controller:负责对 volume 进行 attach/detach。
pv controller:负责处理 pv/pvc 对象,包括 pv 的 provision/delete(cbs intree的provisioner设计成了external provisioner,独立的 cbs-provisioner 来负责 cbs pv 的 provision/delete)。
volume manager:主要负责对 volume 进行 mount/unmount。
volume plugins:包含 k8s 原生的和各厂商的的存储插件。
原生的包括:emptydir、hostpath、flexvolume、csi 等。
各厂商的包括:aws-ebs、azure、我们的 cbs 等。
scheduler:涉及到 volume 的调度。比如对 ebs、csi 等的单 node 最大可 attach 磁盘数量的 predicate 策略。
控制器模式是 k8s 非常重要的概念,一般一个 controller 会去管理一个或多个 API 对象,以让对象从实际状态/当前状态趋近于期望状态。
所以 attach/detach controller 的作用其实就是去 attach 期望被 attach 的 volume,detach 期望被 detach 的 volume。
后续 attach/detach controller 简称 ad controller。
4 ad controller 深度分析
4.1 ad controller数据结构
对于 ad controller 来说,理解了其内部的数据结构,再去理解逻辑就事半功倍。ad controller 在内存中维护 2 个数据结构:
actualStateOfWorld
—— 表征实际状态(后面简称 asw)desiredStateOfWorld
—— 表征期望状态(后面简称 dsw)
很明显,对于声明式 API 来说,是需要随时比对实际状态和期望状态的,所以 ad controller 中就用了 2 个数据结构来分别表征实际状态和期望状态。
actualStateOfWorld
actualStateOfWorld
包含2个 map:
attachedVolumes
:包含了那些 ad controller 认为被成功 attach 到 nodes 上的 volumes。nodesToUpdateStatusFor
:包含要更新node.Status.VolumesAttached
的 nodes。
attachedVolumes
如何填充数据?
1、在启动 ad controller 时,会 populate asw,此时会 list 集群内所有 node 对象,然后用这些 node 对象的node.Status.VolumesAttached
去填充attachedVolumes
。
2、之后只要有需要 attach 的 volume 被成功 attach 了,就会调用MarkVolumeAsAttached
(GenerateAttachVolumeFunc
中)来填充到attachedVolumes中
。
如何删除数据?
只有在 volume 被 detach 成功后,才会把相关的 volume 从attachedVolumes
中删掉。(GenerateDetachVolumeFunc
中调用MarkVolumeDetached
)。
nodesToUpdateStatusFor
如何填充数据?
detach volume 失败后,将 volume add back 到nodesToUpdateStatusFor
- GenerateDetachVolumeFunc
中调用AddVolumeToReportAsAttached
。
如何删除数据?
在 detach volume 之前会先调用RemoveVolumeFromReportAsAttached
从nodesToUpdateStatusFor
中先删除该 volume 相关信息。
desiredStateOfWorld
desiredStateOfWorld
中维护了一个 map:
nodesManaged
:包含被 ad controller 管理的 nodes,以及期望 attach 到这些 node 上的 volumes。
nodesManaged
如何填充数据?
1、在启动 ad controller 时,会 populate asw,list 集群内所有 node 对象,然后把由 ad controller 管理的 node 填充到nodesManaged
。
2、ad controller 的nodeInformer
watch 到 node 有更新也会把 node 填充到nodesManaged
。
3、另外在 populate dsw 和podInformer
watch 到 pod 有变化(add, update)时,往nodesManaged
中填充 volume 和 pod 的信息。
4、desiredStateOfWorldPopulator
中也会周期性地去找出需要被 add 的 pod,此时也会把相应的 volume 和 pod 填充到nodesManaged
。
如何删除数据?
1、当删除 node 时,ad controller 中的nodeInformer
watch 到变化会从 dsw 的nodesManaged
中删除相应的node。
2、当 ad controller 中的podInformer
watch 到 pod 的删除时,会从nodesManaged
中删除相应的 volume 和 pod。
3、desiredStateOfWorldPopulator
中也会周期性地去找出需要被删除的 pod,此时也会从nodesManaged
中删除相应的 volume 和 pod。
4.2 ad controller 流程简述
ad controller 的逻辑比较简单:
1、首先,list 集群内所有的 node 和 pod,来populate actualStateOfWorld
(attachedVolumes
)和desiredStateOfWorld
(nodesManaged
)。
2、然后,单独开个 goroutine 运行reconciler
,通过触发 attach, detach 操作周期性地去 reconcile asw(实际状态)和 dws(期望状态)。
触发 attach,detach 操作也就是,detach 该被 detach 的 volume,attach 该被 attach 的 volume。
3、之后,又单独开个 goroutine 运行DesiredStateOfWorldPopulator
,定期去验证 dsw 中的 pods 是否依然存在,如果不存在就从 dsw 中删除。
5 案例分析
接下来结合上面所说的案例,来详细看看reconciler
的逻辑。
5.1 案例初步分析
从 pod 的事件可以看出来:ad controller认为 cbs attach 成功了,然后 kubelet 没有 mount 成功。
但是从 kubelet 日志却发现
Volume not attached according to node status
,也就是说 kubelet 认为 cbs 没有按照 node 的状态去挂载。这个从 node info 也可以得到证实:volumesAttached
中的确没有这个 cbs 盘(disk-7bfqsft5)。node info 中还有个现象:
volumesInUse
中还有这个 cbs。说明没有 unmount 成功
很明显,cbs 要能被 pod 成功使用,需要 ad controller 和 volume manager 的协同工作。所以这个问题的定位首先要明确:
volume manager 为什么认为 volume 没有按照 node 状态挂载,ad controller 却认为 volume attch 成功了?
volumesAttached
和volumesInUse
在 ad controller 和 kubelet 之间充当什么角色?
这里只对分析 volume manager 做简要分析。
- 根据
Volume not attached according to node status
在代码中找到对应的位置,发现在GenerateVerifyControllerAttachedVolumeFunc
中。仔细看代码逻辑会发现,volume manager 的 reconciler 会先确认该被 unmount 的 volume 被 unmount 掉,然后确认该被 mount 的 volume 被 mount。- 此时会先从 volume manager 的 dsw 缓存中获取要被 mount 的 volumes(
volumesToMount
的podsToMount
); - 然后遍历,验证每个
volumeToMount
是否已经 attach 了。这个volumeToMount
是由podManager
中的podInformer
加入到相应内存中,然后desiredStateOfWorldPopulator
周期性同步到 dsw 中的; - 验证逻辑中,在
GenerateVerifyControllerAttachedVolumeFunc
中会去遍历本节点的node.Status.VolumesAttached
,如果没有找到就报错(Volume not attached according to node status
);
- 此时会先从 volume manager 的 dsw 缓存中获取要被 mount 的 volumes(
- 所以可以看出来,volume manager 就是根据 volume 是否存在于
node.Status.VolumesAttached
中来判断volume 有无被 attach 成功。 - 那谁去填充
node.Status.VolumesAttached
?ad controller 的数据结构nodesToUpdateStatusFor
就是用来存储要更新到node.Status.VolumesAttached
上的数据的。 - 所以,如果 ad controller 那边没有更新
node.Status.VolumesAttached
,而又新建了 pod,desiredStateOfWorldPopulator
从podManager中的内存把新建 pod 引用的 volume 同步到了volumesToMount
中,在验证 volume 是否 attach 时,就会报错(Volume not attached according to node status)。当然,之后由于 kublet 的 syncLoop 里面会调用
WaitForAttachAndMount
去等待 volumeattach 和 mount 成功,由于前面一直无法成功,等待超时,才会有会面timeout expired
的报错。
所以接下来主要需要看为什么 ad controller 那边没有更新node.Status.VolumesAttached
。
5.2 ad controller 的reconciler
详解
接下来详细分析下 ad controller 的逻辑,看看为什么会没有更新node.Status.VolumesAttached
,但从事件看 ad controller 却又认为 volume 已经挂载成功。
从流程简述中表述可见,ad controller 主要逻辑是在reconciler
中。
一、reconciler
定时去运行reconciliationLoopFunc
,周期为 100ms。
二、reconciliationLoopFunc
的主要逻辑在reconcile()
中:
1. 首先,确保该被 detach 的 volume 被 detach 掉
遍历 asw 中的
attachedVolumes
,对于每个 volume,判断其是否存在于 dsw 中:根据 nodeName 去 dsw.nodesManaged 中判断 node 是否存在;存在的话,再根据 volumeName 判断 volume是否存在。如果 volume 存在于 asw,且不存在于 dsw,则意味着需要进行 detach;
之后,根据
node.Status.VolumesInUse
来判断 volume 是否已经 unmount 完成,unmount 完成或者等待 6min timeout 时间到后,会继续 detach 逻辑;在执行 detach volume 之前,会先调用
RemoveVolumeFromReportAsAttached
从 asw 的nodesToUpdateStatusFor
中去删除要 detach 的 volume;然后 patch node,也就等于从
node.status.VolumesAttached
删除这个 volume;之后进行 detach,detach 失败主要分 2 种:
如果真正执行了volumePlugin的具体实现DetachVolume失败,会把 volume add back 到nodesToUpdateStatusFor(之后在 attach 逻辑结束后,会再次 patch node);
如果是 operator_excutor 判断还没到 backoff 周期,就会返回backoffError,直接跳过DetachVolume;
backoff 周期起始为 500ms,之后指数递增至 2min2s。已经 detach 失败了的 volume,在每个周期期间进入 detach 逻辑都会直接返回
backoffError
。
2. 之后,确保该被 attach 的 volume 被 attach 成功
遍历dsw的
nodesManaged
,判断 volume 是否已经被 attach 到该 node,如果已经被 attach 到该 node,则跳过 attach 操作;去 asw.attachedVolumes 中判断是否存在,若不存在就认为没有 attach 到 node;若存在,再判断 node,node 也匹配就返回
attachedConfirmed
;而
attachedConfirmed
是由 asw 中AddVolumeNode
去设置的,MarkVolumeAsAttached
设置为 true(true 即代表该 volume 已经被 attach 到该 node 了);之后判断是否禁止多挂载,再由 operator_excutor 去执行 attach。
3. 最后,UpdateNodeStatuses
去更新 node status。
5.3 案例详细分析
前提
volume detach 失败
sts+cbs(pvc),pod recreate 前后调度到相同的 node
涉及 k8s 组件
ad controller
kubelet(volume namager)
ad controller 和 kubelet (volume namager) 通过字段
node.status.VolumesAttached
交互。ad controller 为
node.status.VolumesAttached
新增或删除 volume,新增表明已挂载,删除表明已删除kubelet(volume manager)需要验证新建pod中的(pvc 的) volume 是否挂载成功,存在于
node.status.VolumesAttached
中,则表明验证 volume 已挂载成功;不存在,则表明还未挂载成功。
以下是整个过程:
首先,删除 pod 时,由于某种原因 cbs detach 失败,失败后就会 backoff 重试。
由于 detach 失败,该 volume 也不会从 asw 的
attachedVolumes
中删除
由于 detach 时,
先从
node.status.VolumesAttached
中删除 volume,之后才去执行 detachdetach时返回
backoffError
不会把该 volumeadd backnode.status.VolumesAttached
之后,我们在 backoff 周期中(假如就为第一个周期的 500ms 中间)再次创建 sts,pod 被调度到之前的 node
而 pod 一旦被创建,就会被添加到 dsw 的nodesManaged
(nodeName 和 volumeName 都没变)
reconcile()中的第 2 步,会去判断 volume 是否被 attach,此时发现该 volume 同时存在于 asw 和 dws 中,并且由于 detach 失败,也会在检测时发现还是 attach,从而设置attachedConfirmed
为 true
ad controller 就认为该 volume 被 attach 成功了
reconcile() 中第 1 步的 detach 逻辑进行判断时,发现要 detach 的 volume 已经存在于dsw.nodesManaged
了(由于 nodeName 和 volumeName 都没变),这样 volume 同时存在于 asw 和 dsw 中了,实际状态和期望状态一致,被认为就不需要进行 detach 了。
这样,该 volume 之后就再也不会被 add back 到node.status.VolumesAttached
。所以就出现了现象中的 node info 中没有该 volume,而 ad controller 又认为该 volume 被 attach 成功了
由于kubelet(volume manager)与 controller manager 是异步的,而它们之间交互是依据node.status.VolumesAttached
,所以volume manager在验证volume是否attach成功,发现node.status.VolumesAttached
中没有这个 voume,也就认为没有 attach 成功,所以就有了现象中的报错Volume not attached according to node status
之后 kubelet 的syncPod
在等待 pod 所有的 volume attach 和 mount 成功时,就超时了(现象中的另一个报错timeout expired wating...
)。
所以 pod 一直处于ContainerCreating
。
5.4 小结
所以,该案例出现的原因是:
sts+cbs,pod recreate 时间被调度到相同的 node;
由于 detach 失败,backoff 期间创建 sts/pod,致使 ad controller 中的 dsw 和 asw 数据一致(此时该 volume 由于没有被 detach 成功而确实处于 attach 状态),从而导致 ad controller 认为不再需要去 detach 该 volume;
又由于 detach 时,是先从
node.status.VolumesAttached
中删除该 volume,再去执行真正的DetachVolume
。backoff 期间直接返回backoffError
,跳过DetachVolume
,不会 add back;之后,ad controller 因 volume 已经处于 attach 状态,认为不再需要被 attach,就不会再向
node.status.VolumesAttached
中添加该 volume;最后,kubelet 与 ad controller 交互就通过
node.status.VolumesAttached
,所以 kubelet 认为没有 attach 成功,新创建的 pod 就一直处于ContianerCreating
了。
据此,我们可以发现关键点在于
node.status.VolumesAttached
和以下两个逻辑:
detach 时 backoffError,不会 add back;
detach 是先删除,失败再 add back。
- backoffError 时,也 add back
但这种方式有个缺点:patch node的请求数增加了10+次/(s * volume)
pr #72914——https://github.com/kubernetes/kubernetes/pull/72914
一进入 detach 逻辑就判断是否 backoffError(处于 backoff 周期中),是就跳过之后所有 detach 逻辑,不删除就不需要 add back 了。
这个方案能避免方案 1 的问题,且会进一步减少请求 apiserver 的次数,且改动也不多
pr #88572——https://github.com/kubernetes/kubernetes/pull/88572
总结
AD Controller 负责存储的 Attach、Detach。通过比较 asw 和 dsw 来判断是否需要 attach/detach。最终 attach 和 detach 结果会体现在node.status.VolumesAttached
。以上案例出现的现象,是 k8s ad controller 的 bug 导致,目前社区并未修复。
现象出现的原因主要是:
先删除旧 pod 过程中 detach 失败,而在 detach 失败的 backoff 周期中创建新 pod,此时由于 ad controller 逻辑 bug,导致 volume 被从
node.status.VolumesAttached
中删除,从而导致创建新 pod 时,kubelet 检查时认为该 volume 没有 attach 成功,致使 pod 就一直处于ContianerCreating
。
而现象的解决方案,推荐使用 pr #88572——https://github.com/kubernetes/kubernetes/pull/88572
目前 TKE 已经有该方案的稳定运行版本,在灰度中。
Kubernetes CKA线下班
更多推荐
所有评论(0)