Pod 从创建到 Running 背后发生了什么

前言

简单记录一下 Pod 从创建到最终 Running 背后发生的事情, 以便对 k8s 的一些工作机制有一个更深入一点的了解。

本文内容所针对的 Kubernetes 版本为 v1.21.3

从发送创建 Pod 的请求到 Pod 信息存入 etcd

先讲一下从客户端发送创建 Pod 的请求到 apiserver 然后 apiserver 把数据存入 etcd 过程中发生的事情:

  1. 客户端向 apiserver 发送创建 Pod 的请求: POST /api/v1/namespaces/{namespace}/pods
  2. apiserver 收到请求后
    1. 首先会对请求做 认证(authentication) ,解析请求所携带的认证信息得到 User 信息,然后将 User 信息写入请求的 Context 中。 支持的认证方法详见 官方文档
    2. 认证通过后,再对 User 做 鉴权(Authorization) ,检查当前 User 对这个请求所操作的资源是否有相应的操作权限。 支持的鉴权方法详见 官方文档
    3. 认证和鉴权都通过后,请求的 body 将会被反序列化为 runtime.Object 对象。
    4. 存入 etcd 之前 , 反序列化后的对象会 先被 填充默认值 和进行 字段校验
    5. 然后这个请求和对象还会被 Admission Controllers 处理一遍。 Admission Controllers 即包括 kube-apiserver 内置的 admission controllers 也包括用户自行实现的 admission webhooks
      • Admission Controllers 既可以实现对请求做进一步的校验(比如按一定策略对请求校验,拦截未使用指定 docker registory 的 Pod)( validating admission )) 也可以实现修改请求创建/修改的对象的属性的需求(比如给 Pod 注入 sidecar 容器)( mutating admission ))。
      • 先处理 mutating admission 然后 再处理 validating admission )
      • 只要有一个 Admission Controller 返回失败,请求就会失败。
      • 多个 Admission Controller 串行执行 , 每个 Admission Controller 内部都有自己的逻辑,比如,
        • ValidatingAdmissionWebhook Controller 内部会 并发执行 定义的多个 validating admission webhook ,执行完成后只返回 第一个错误 (未被忽略的那个错误,因为 webhook 可以配置忽略错误)
        • MutatingAdmissionWebhook Controller 内部会 串行执行 定义的多个 mutating webhook ,出错(未被忽略的错误,因为 webhook 可以配置忽略错误)就 返回
    6. Admission controllers 处理完以后, 对象被存入到 etcd 中
    7. 最后根据执行结果方法相应的 Response。
  3. 此时 Pod 就创建成功了,但是还没有被调度到某个节点并且状态是 Pending。

Pod 调度

kube-scheduler 组件负责 Pod 的调度工作,具体过程如下:

  1. kube-scheduler 通过 Informer 机制 监控 Pod 等资源的变更事件并注册相应的回调函数
  2. 当上面的 Pod 创建成功后, 触发了 Pod 的变更事件 ,因为此时这个 Pod 满足 nodeName 的值为空并且 schedulerName 中指定的是已知的 Scheduler Framework Name,所以这个 Pod 对象会被放入到 SchedulingQueue 队列中等待处理。
  3. kube-scheduler 中 SchedulingQueue 中的待调度 Pod 会由 scheduleOne 函数进行处理,Pod 调度逻辑就在这个函数里:
    1. 根据 Pod 的 schedulerName 字段的值找到 Pod 指定要使用的 Scheduler Framework (fwk)
    2. 根据调度算法(内置的策略加 fwk 实现的策略)得出适合这个 Pod 的最佳节点(调度算法的详细说明以后再单独细说)
    3. 如果调度算法失败了:
      1. 执行 fwk.RunPostFilterPlugins 函数,获取可能的 nominatedNode
      2. 产生一个 FailedScheduling Event、 更新 Pod 的 status.conditions 字段增加一个 typePodScheduled statusFalse 的 PodCondition 以及更新 status.nominatedNodeName 字段的值为前面获取的 nominatedNode
    4. 如果调度算法成功返回了节点信息,首先执行 fwk.RunReservePluginsReserve 如果失败了执行 fwk.RunReservePluginsUnreserve 然后按上面 3.2 的操作记录调度失败
    5. 然后再执行 fwk.RunPermitPlugins `` 如果失败了执行 ``fwk.RunReservePluginsUnreserve 然后按上面 3.2 的操作记录调度失败
    6. 最后执行 binding 操作
      1. 执行 fwk.WaitOnPermit 如果失败了执行 fwk.RunReservePluginsUnreserve 然后按上面 3.2 的操作记录调度失败
      2. 执行 fwk.RunPreBindPlugins 如果失败了执行 fwk.RunReservePluginsUnreserve 然后按上面 3.2 的操作记录调度失败
      3. 执行真正的 binding 操作 sched.bind默认的 Bind 实现 会去 post 当前 Pod 的 binding 子资源 记录 Pod 被调度到哪个节点上了, 如果失败了执行 fwk.RunReservePluginsUnreserve 然后按上面 3.2 的操作记录调度失败
      4. 执行 fwk.RunPostBindPlugins

当 apiserver 收到对 Pod binding 子资源的 post 请求的时候,会触发 binding 的 create 逻辑, 更新 PodnodeName 字段为请求中包含的 NodeName 以及 更新 Pod 的 status.conditions 字段增加一个 type 为 PodScheduled status 为 True 的 pod condition 。

此时 Pod 就被调度到一个节点上了,但是 Pod 的还是 Pending 因为 Pod 内的容器还没有在被调度的节点上运行。

节点上运行 Pod 中的容器

kubelet 组件负责在节点上运行 Pod 中定义的容器,具体的过程如下:

  1. kubelet 组件启动后会 watch 所有 nodeName 字段的值是当前节点名称的 Pod 的 变更事件
  2. 当 Pod 经过调度后,它的 nodeName 字段会被设置为被选中的节点的名称,此时会触发 kubelet 中 pod ADD 事件(因为之前没在这个节点上处理过):
    1. 触发 Pod 更新处理 :
      1. 首先执行 canRunPod 检查( 检查 AppArmor 、 NoNewPrivs 以及 ProcMount 这三个特性),如果检查不通过的话,不会进行后续的操作
      2. 如果网络插件未就绪并且当前 Pod 未使用 Host 网络的话,返回 network is not ready 的错误以及产生一个 NetworkNotReady 的 Event
      3. 如果启用了 cgroups-per-qos 功能,将为 Pod 创建 Cgroups
      4. 创建存放 Pod 容器数据的目录:
        • Pod 目录,比如 /var/run/kubelet/pods/{PodUID}
        • PodVolumes 目录,比如 {PodDir}/volumes
        • PodPlugins 目录,比如 {PodDir}/plugins
      5. 通过 volumeManager.WaitForAttachAndMount 等待 Pod 中所有容器的 volumeMountsvolumeDevices 中使用的 volume 被成功 attatch 和 mount (关于 volumeManager 相关内容以后再单独细说)。 如果失败的话,返回 mount 失败的 event 和错误
      6. 获取 Pod 中指定的 imagePullSecrets 所使用的那些 secret 数据的内容。
      7. 容器运行时创建容器 :
        1. 执行 createPodSandbox 方法创建一个 pod sandbox
          • 内部 会通过 gRPC 调用不同 CRI(Container Runtime Interface) 所实现的 RunPodSandbox 接口
          • 不同 CRI 实现 RunPodSandbox 接口的方法可能会不尽相同。以 Docker 为例,dockershim 中实现的 RunPodSandbox 接口的 内部操作 如下:
            1. pull sandbox 容器(pause 容器)所用的镜像(默认是 k8s.gcr.io/pause:3.4.1 ,)
            2. 调用 docker client api 创建 sandbox 容器
            3. 创建 sandbox checkpoint
            4. 启动 sandbox 容器
            5. 更新容器内的 resolv.conf 文件的内容
            6. 如果 Pod 使用的是 Host 网络,直接返回, 如果不是用的 Host 网络的话,继续
            7. 通过 CNI 插件 配置容器网络 :
              • 实际上是调用 CNI 插件的二进制可执行文件, 执行 一个 ADD 指令
            8. 如果网络配置失败
              1. 清理网络资源:执行 CNI 插件的 DEL 指令
              2. 停止前面启动的容器
        2. 然后再通过调用 CRI 的 PodSandboxStatus 接口查询一下创建的 pod sandbox 的状态,确保创建的 pod sandbox 无异常,同时获取 status 中包含的 pod IP 信息。
        3. 启动 ephemeral 容器, 启动容器 的步骤如下:
          1. 使用前面 6 获取的 secret 数据 pull image
          2. 调用 CRI 的 CreateContainer 接口创建容器
          3. 调用 CRI 的 StartContainer 接口启动容器
          4. 执行 container 中定义的 lifecycle.postStart hook
        4. 启动 init 容器
        5. 启动剩下的容器
    2. 容器启动完成后,将当前 Pod 注册 probeManager 中。 probeManager 负责异步执行容器中定义的 startupProbereadinessProbe 以及 livenessProbe 操作。
      • 这些 probe 操作的结果会发送到 startupManagerreadinessManager 以及 livenessManager 中,从而触发响应的 事件响应逻辑
      • 比如 readinessProbe 执行成功了会触发更新 statusManager 中记录的 Pod 的 status 信息,更新 ContainersReady 和 Ready 信息,以及 触发 Pod 信息同步操作(这里会有更新 statusManager 把 statusManager 中的Pod 状态 更新 为 Running 的 逻辑 )。
      • statusManager 里有个 协程 会定期把待更新的 pod 状态通过 apiserver 进行 更新

经过 kubelet 中一些列的处理后,此时 Pod 的状态就变成 Running 了。

总结

简单记录了一下 Pod 从创建到最终 Running 背后发生的事情,其中有些细节没有展开, 后面再补充或者另写一些文章说一下那些没展开的内容。


Comments