kubelet 源码剖析(二):创建一个Pod的整体视角

2024年 3月 8日 98.1k 0

这篇文章,我们来说说创建一个 Pod 的整体流程,这里不讲具体的细节,在后面的文章中我们会对重要的步骤展开详细的分析。

下面这张图是从组件层面来看创建 Pod 的主要流程

pF0bUVs.md.png

kubelet 需要能够实时感知到有新 pod 需要创建,并且要知道 pod 的定义是什么,kubelet 有如下三种方式数据来源

  • kube-apiserver

    kube-apiserver 可以说是最常见的 pod 数据源,用户的应用基本都是通过 kube-apiserver 创建 deployment 等资源,kube-control-manager 基于 deployment 等资源调用 kube-apiserver 接口创建 pod。kubelet 启动时先从 kube-apiserver list 所有 pod,然后判断有哪些 pod 是分配到本节点上的,并把这些 pod 创建出来。后续 kubelet 也会持续 watch kube-apiserver 感知 pod 的变化,不错过任何分配到本节点的 pod。

  • node 文件系统

    这种方式是 kubelet 通过监测本地文件系统特定目录的方式来获取需要创建的 pod,这个目录通过启动参数 --pod-manifest-path="" 或者配置文件参数 staticPodPath 来配置,如果是通过 kubeadm 部署的话默认为 /etc/kubernetes/manifests。这种 pod 叫 static pod。

    如果你想通过这种方式部署一个 pod,只需要在上述目录中放入 pod 的 manifest 文件即可,kubelet 会自动监测到该文件,并创建 pod。 这种 pod 你无法通过 Kubectl 命令行工具管理,因为他的生命周期不归控制面组件管理,只能通过在节点上编辑文件来变更 pod。

    通常,管理面组件如 apiserver、controller-manager、scheduler 会通过这种方式部署。因为想要通过监测 kube-apiserver 的方式部署 pod,那么 kube-apiserver 必须是正常工作的,现在 kube-apiserver 还没有部署,所以需要通过 static pod 的方式部署

  • 外部 http 服务

    这种方式是通过定期访问外部的 http 提供的服务,监测是否有需要创建的 pod,这种方式创建的 pod 也是 static pod,本质上和第二种方式区别不大,平时我们也用的不多,这里就不多赘述。

我们来看下这个循环监测的整体代码框架

// pkg/kubelet/kubelet.go
func (kl *Kubelet) syncLoop(...) {
	...
	for {
		...
		if !kl.syncLoopIteration(updates, handler, syncTicker.C, housekeepingTicker.C, plegCh) {
			break
		}
		...
	}
}

// pkg/kubelet/kubelet.go
func (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate,...) {
	select {
		case u, open := <-configCh:
			if !open {
				klog.Errorf("Update channel is closed. Exiting the sync loop.")
				return false
			}
			switch u.Op {
			case kubetypes.ADD:
				klog.V(2).Infof("SyncLoop (ADD, %q): %q", u.Source, format.Pods(u.Pods))
				handler.HandlePodAdditions(u.Pods)
			...
			}
	}
}

syncLoop 中会无限循环的调用 syncLoopIteration ,在 syncLoopIteration 中会检测 configCh 通道,如果能够从通道中读取到数据且判断为是新建 pod,那么就调用 HandlePodAdditions 开始一个 pod的创建流程。

我们在上面说了 kubelet 会监测三种数据源,实际上是通过三个协程去处理的,当有新 Pod 创建时,对应的协程就会发需要创建的 pod 配置发送到 configCh 这个通道,这样 syncLoopIteration 就能获取到数据做处理。

上述流程,我们可以用下图示意:

pFB00zT.md.png

syncLoopIteration 拿到需要新建的 pod 配置后,于是调用 HandlePodAdditions 开始创建 pod。因为这篇文章,我们只讲整体框架,不讲细节,所以具体的详细流程我们后面讲。

我们知道,一个 pod 中可以有多个容器,这些容器共享这个 pod 的 Linux 命名空间,从而进行资源隔离,常见的有网络命名空间、PID命名空间等。在创建 Pod 时,首先会创建一个 pause 容器,然后用这个容器中的进程去 "hold" 住网络命名空间,后续 pod 中所有的容器启动后容器内的进程后会加入到该网络命名空间内。

下图为 pod 网络命名空间示意图

pFsFh8K.md.png

为了创建 sandbox(一个独立的网络栈),kubelet 开始调用 cri(容器运行时,如containerd),cri 会先启动一个 pause 容器,然后调用 cni(容器网络接口,如calico/flannel等都实现了cni)为该 pause 容器(其实也就是一个进程)设置网络栈,cni所做的事就是为 pause 容器设置网络栈,如创建网卡、设置路由、获取一个 IP,为网卡配置IP,该IP就是容器IP。不同的 cni 实现有不同的 IP 管理方式。该过程可以通过下面图示表示:kubelet 源码剖析(二):创建一个Pod的整体视角-1

pFsGYNV.md.png

cri 创建完了 sandbox后,那么 pause 容器就启动了,这个容器有自己的网络命名空间,返回 sandbox id 给 kubelet。

有了 sandbox后,就获得了一个隔离环境,进而可以后续的其他容器创建,一共有三种类型的容器:

  • 临时容器

  • init 容器

  • 业务容器(承载用户业务)

在创建这些容器时,kubelet 会传给 cri 之前返回的 sandbox id,cri收到创建任务后,就会拉起容器,并且把容器进程加入到 sandbox id 所代表的的网络命名空间中,这样这个 pod 所看到的网络栈就是一致的,所有容器共享一个 pod ip。

我们可以看到这个过程中,主要处理创建 pod 任务的组件是 cri 和 cni,现在有如下几个问题:

  • kubelet 是怎么确定 cri 的地址的?
  • 如果一个节点上有多个 cri,kubelet又是怎么确定使用哪个的?
  • cri 又是怎么确定调用哪个 cni 的?

除了网络命名空间这个隔离外,如 pid 命名空间、IPC 命名空间,也都是类似的,我们这里就不多说了。

相关文章

KubeSphere 部署向量数据库 Milvus 实战指南
探索 Kubernetes 持久化存储之 Longhorn 初窥门径
征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
那些年在 Terraform 上吃到的糖和踩过的坑
无需 Kubernetes 测试 Kubernetes 网络实现
Kubernetes v1.31 中的移除和主要变更

发布评论