59057298b44c655c0b8ef2556914b915.png

文章来源:腾讯云容器团队 / 原文链接

作者蔡靖,腾讯高级后台开发工程师,拥有多年大规模 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

7de3d540572f9e36df078eade68e715b.png

  • kubelet log

46b19abba91068a8f26561b354ab71ba.png
  • kubectl get node xxx -oyaml 的volumesAttachedvolumesInUse

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 策略。

f984584ed2c681e54815a12cbc293833.png

控制器模式是 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 个数据结构:

  1. actualStateOfWorld —— 表征实际状态(后面简称 asw)

  2. 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 的协同工作。所以这个问题的定位首先要明确:

  1. volume manager 为什么认为 volume 没有按照 node 状态挂载,ad controller 却认为 volume attch 成功了?

  2. volumesAttachedvolumesInUse 在 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(volumesToMountpodsToMount );
    • 然后遍历,验证每个volumeToMount是否已经 attach 了。这个volumeToMount是由podManager中的podInformer加入到相应内存中,然后desiredStateOfWorldPopulator周期性同步到 dsw 中的;
    • 验证逻辑中,在GenerateVerifyControllerAttachedVolumeFunc中会去遍历本节点的node.Status.VolumesAttached,如果没有找到就报错(Volume not attached according to node status);
  • 所以可以看出来,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 种:

  1. 如果真正执行了volumePlugin的具体实现DetachVolume失败,会把 volume add back 到nodesToUpdateStatusFor(之后在 attach 逻辑结束后,会再次 patch node);

  2. 如果是 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 重试。

  1. 由于 detach 失败,该 volume 也不会从 asw 的attachedVolumes中删除

由于 detach 时,

  1. 先从node.status.VolumesAttached中删除 volume,之后才去执行 detach

  2. detach时返回backoffError不会把该 volumeadd back node.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和以下两个逻辑:

  1. detach 时 backoffError,不会 add back;

  2. detach 是先删除,失败再 add back。

所以只要想办法能在任何情况下 add back 就不会有问题了。根据以上两个逻辑就对应有以下 2 种解决方案, 推荐使用方案2:
  1. 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 已经有该方案的稳定运行版本,在灰度中。

END
Kubernetes  CKA线下班 40a7f50a8d4baa873ca311433a4c44de.png

d6ec9fdc23e50a222f95a74d196d9579.gif

1db6a0ce6fa504f1191fe9aa59b5595d.png

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐