runc有两种,一种docker,一种containerd,上面是以containerd来说明,更加轻量。
用户去创建pod,那么pod的请求会发给apiserver,apiserver接收到请求之后,会把对象存储到etcd里面,存储完成之后,调度器就去watch到pod的创建时间,并且完成调度,绑定pod。
绑定之后,也就是更新,继续去etcd里面做持久化,持久化完成之后,kubelet就watch到已经绑定的pod。
kubelet在启动pod的时候,它启动的不是一个容器进程,而是多个容器进程,不仅仅启动了应用进程,同时还启动了pause容器镜像的实例,容器进程启动了之后可以把它和某个网络namespace产生关联关系,把它放在某个网络namespace下面,在这个独立的网络namesapce下面,它可以有独立的网络配置,如果直接将容器进程放到网络namesapce里面会出现什么样的问题呢?
你的容器进程可靠吗?会不会有错误,会不会异常退出,oom,空指针,等等,当出现这些问题的时候,容器进程就退出了,退出之后容器进程就和这个网络namespace之间的绑定关系就消失了,如果不做一些措施的话,那是不是每次容器进程退出都需要重新为这个容器进程配置网络,那么这样的效率不会很高,如果容器频繁重启,那么就会导致节点上面多了很多不必要的系统操作。
有什么办法让容器进程退出的时候,网络,存储这些东西都不发生变化,这就是为什么需要pause容器。
在容器启动之前,网络就需要就绪,网络是要在容器之前就绪的,这就需要额外的容器进程先启动起来。这个也是pause的功能。
pause容器里面entrypoint其实就是pause,它永远sleep,它是一个不会退出,不消耗资源的一个稳定的进程,那么所有的网络就可以挂载在这个pause上面,当容器应用进程启动的时候,我只需要将应用进程的网络namespace挂载在pause上面就行了。
即使容器出现各种问题重启,没关系,因为网络是挂载在其下面的。所以它就提供了非常稳定的基座。
再回过头来看pod启动,pod启动的时候最先启动pause容器,启动之后containerd就会去调用cni的插件去setup pod,其实它会将ADD的命令告诉cni插件帮我setup网络,cni这边setup网络之后会去将pod信息返回给runtime,然后这个信息会返回给kubelet,那么这个时候其实pod就有IP了,这个时候才会去进行下面的用户应用容器的启动。
可以看到在启动容器的时候,网络是已经就绪的,所以有网络需求就可以满足了,整个启动完之后就会将状态回写到Apiserver里面,那么整个的启动过程就完成了。
上面是更加细的流程框图,如果读代码可以按照上面的思路作为指导去梳理代码的走读流程。
可以看到最左边是kubelet 中间是CRI 右边是CNI
CheckAdmit:kubelet在sync pod也就是做pod同步的时候,首先也会去做准入,比如它要去启动pod,如果节点资源不够,你非要将nodename设置为该节点,因为和节点产生绑定关系,那么就需要启动这个容器了,启动的时候我要去check一下,也需要去做准入的,要去看你的cpu的需求满足不满足,如果不满足就直接报错了,out of cpu的error,写回到这个pod状态里面。
Check network plugin:然后会去监听当前节点上网络插件的情况,如果网络插件不就绪,那么pod是启动不起来的,所以这里也会直接报错。
Update cgroup:namspace cgroup这些技术要去启动容器进程的时候,要将容器对应的cgroup文件配置好。
Makepoddir:pod需要存储日志,容器的日志需要在主机上有个目录,它会去将数据目录创建起来。
WaitForattachandmount:你的pod是需要一些存储,比如configmap轻量级存储只需要将文件下载下来,然后mount到容器当中就行了,如果是更高要求的存储,比如网络存储,那么需要去创建volume,然后和这个节点产生attach关系,然后再mount到容器里面,其实就是等待存储就绪。
面试的时候:CRI CNI CSI,它们之间关系是怎么样的呢?在启动pod的时候谁先启动,谁后启动,上面可以很清晰看到CSI就在kubelet这部分运行,就是在pod后续加载还没有启动的时候,我就得先去将存储挂载好,并且mount进来。
如果这一步不过的话,它会一直卡在这里,接下来才会去做syncpod。
syncpod里面就是来计算sandbox和容器变化,如果sandox发生变化了,就是已经在运行的容器和我pod本身不匹配了,那就是重建了,这里面其实就是computerpodaction的动作,如果这个pod已经不存在了,你的容器还在,那么就需要将容器kill掉。
然后你要启动一个新的容器,那么会去经历启动的动作。之前讲了第一步就是createpodsandbox,它会去生成这些sandbox的config,它有一些mainfest,要将这些配置文件生成出来。然后pod的日志目录要创建好,然后要去调用runtime的sandbox。
以containerd为例,它的cri本身又是一个grpc的服务,kubelet会去调用grpc服务,这个时候整个请求就转到containerd进程里面,kubelet就要暂时等待它的返回结果了。
然后就是createsandboxcontainer,然后一步一步的往下走,最后由cri去调用cni的接口。
针对这种创建网络的请求,其实就是setup网络的过程,在cni里面是addnetwork这种方法去实现的,上面就是整个pod清单的获取,一直到pod容器进程启动的这样一个过程。至于用户容器启动就不多说了。