这篇文章,我们来说说创建一个 Pod 的整体流程,这里不讲具体的细节,在后面的文章中我们会对重要的步骤展开详细的分析。
下面这张图是从组件层面来看创建 Pod 的主要流程
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 就能获取到数据做处理。
上述流程,我们可以用下图示意:
syncLoopIteration 拿到需要新建的 pod 配置后,于是调用 HandlePodAdditions 开始创建 pod。因为这篇文章,我们只讲整体框架,不讲细节,所以具体的详细流程我们后面讲。
我们知道,一个 pod 中可以有多个容器,这些容器共享这个 pod 的 Linux 命名空间,从而进行资源隔离,常见的有网络命名空间、PID命名空间等。在创建 Pod 时,首先会创建一个 pause 容器,然后用这个容器中的进程去 "hold" 住网络命名空间,后续 pod 中所有的容器启动后容器内的进程后会加入到该网络命名空间内。
下图为 pod 网络命名空间示意图
为了创建 sandbox(一个独立的网络栈),kubelet 开始调用 cri(容器运行时,如containerd),cri 会先启动一个 pause 容器,然后调用 cni(容器网络接口,如calico/flannel等都实现了cni)为该 pause 容器(其实也就是一个进程)设置网络栈,cni所做的事就是为 pause 容器设置网络栈,如创建网卡、设置路由、获取一个 IP,为网卡配置IP,该IP就是容器IP。不同的 cni 实现有不同的 IP 管理方式。该过程可以通过下面图示表示:
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 命名空间,也都是类似的,我们这里就不多说了。