k8s源码分析-kubelet
本文以k8s v1.10
为例,分析kubelet组件的工作原理。
入口
main函数定义在cmd/kubelet/kubelet.go,主要任务如下:
- 创建命令行对象,包括解析用户指定的参数,生成配置对象等
- 执行命令行对象,最后进入启动kubelet的逻辑,调用了kubelet的
RunKubelet()
函数
创建kubelet
以下是创建kubelet的关键函数,它创建了docker客户端、网络插件等重要组件:pkg/kubelet/kubelet.go:321
func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
初始化容器运行时服务端(CRI)
在1.5以前的版本中,k8s依赖于dokcer,为了支持不同的容器运行时,比如rkt、containerd,kubelet从1.5开始加入了CRI标准,CRI是一组rpc接口,只要是实现了这组接口都可以作为kubelet的运行时,而且在k8s内部将之前的Pod抽象为一种更为通用的SandBox。
调用过程如下:
kubelet -> remote -> CRI -> dockershim -> docker_client -> docker_daemon
提出了CRI标准以后,意味着在新的版本里需要使用新的连接方式与docker通信,为了兼容以前的版本,k8s提供了针对docker的CRI实现,也就是kubelet包下的dockershim包,dockershim是一个rpc服务,监听一个端口供kubelet连接,dockershim收到kubelet的请求后,将其转化为REST API请求,发送给物理机上的docker daemon,以下是创建和启动dockershim的代码:pkg/kubelet/kubelet.go:NewMainKubelet():622
// 创建dockershim
ds, err := dockershim.NewDockerService(kubeDeps.DockerClientConfig, crOptions.PodSandboxImage, streamingConfig,
...
// 启动rpc服务
if err := server.Start(); err != nil {
创建Docker客户端的逻辑是在创建dockershim的过程中,关键代码:pkg/kubelet/dockershim/libdocker/client.go:100
client, err := getDockerClient(dockerEndpoint)
client对象就是docker的客户端,包含了我们常用的docker run
,docker images
等所有操作,dockerEndpoint
就是--container-runtime-endpoint
选项的值,默认是unix:///var/run/docker.sock
。
初始化容器运行时客户端
dockershim是rpc的服务端,pkg/kubelet/remote
包是rpc客户端的实现,此包下的函数由kubelet组件调用,创建remote的代码:pkg/kubelet/kubelet.go:657
runtimeService, imageService, err := getRuntimeAndImageServices(remoteRuntimeEndpoint, remoteImageEndpoint, kubeCfg.RuntimeRequestTimeout)
初始化网络插件(CNI)
创建CNI实例的逻辑是在创建dockershim的过程中,用来在容器中创建和删除网络设备,关键代码:pkg/kubelet/dockershim/docker_service.go:NewDockerService():232
cniPlugins := cni.ProbeNetworkPlugins(pluginSettings.PluginConfDir, pluginSettings.PluginBinDirs)
初始化卷管理器
卷管理器的作用是检查容器需要的卷是否已挂载,需要卸载的卷是否已经卸载,关键代码:pkg/kubelet/kubelet.go:NewMainKubelet():817
klet.volumePluginMgr, err =
NewInitializedVolumePluginMgr(klet, secretManager, configMapManager, tokenManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber)
初始化Pod处理器
创建一个叫worker的对象,它持有处理Pod的入口函数,也是就klet.syncPod
:pkg/kubelet/kubelet.go:NewMainKubelet():854
klet.podWorkers = newPodWorkers(klet.syncPod, kubeDeps.Recorder, klet.workQueue, klet.resyncInterval, backOffPeriod, klet.podCache)
启动kubelet
启动kubelet的入口函数是:pkg/kubelet/kubelet.go:1370
func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
启动Pod处理器
kl.syncLoop(updates, kl)
其中syncLoop()
函数是处理Pod事件的入口函数,定义如下:pkg/kubelet/kubelet.go:1781
func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler)
处理Pod事件
当从kube-apiserver
中监听到需要处理的事件后,将事件交给相应的Pod处理器:pkg/kubelet/kubelet.go:1926
handler.HandlePodSyncs(podsToSync)
分配一个worker并开始处理事件的函数:pkg/kubelet/kubelet.go:1959
func (kl *Kubelet) dispatchWork(pod *v1.Pod, syncType kubetypes.SyncPodType, mirrorPod *v1.Pod, start time.Time)
执行worker中处理Pod事件的函数,也就是前面说到的入口函数:pkg/kubelet/pod_workers.go:170
err = p.syncPodFn(syncPodOptions{
mirrorPod: update.MirrorPod,
pod: update.Pod,
podStatus: status,
killPodOptions: update.KillPodOptions,
updateType: update.UpdateTy
...
同步Pod
入口函数定义:pkg/kubelet/kubelet.go:1450
func (kl *Kubelet) syncPod(o syncPodOptions) error
调用容器运行时的同步Pod接口:pkg/kubelet/kubelet.go:1649
result := kl.containerRuntime.SyncPod(pod, apiPodStatus, podStatus, pullSecrets, kl.backOff)
在1.11.x版本中该接口只有一个实现:pkg/kubelet/kuberuntime/kuberuntime_manager.go:567
func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, _ v1.PodStatus, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) {
与docker的衔接
这个SyncPod()函数函数是创建容器的关键逻辑,在这里可以一览创建Pod的全部流程,所有关于容器的操作都是调用remote模块最后到达docker daemon中,比如以下创建容器的代码:pkg/kubelet/kuberuntime/kuberuntime_container.go:SyncPod():118
containerID, err := m.runtimeService.CreateContainer(podSandboxID, containerConfig, podSandboxConfig)
其中runtimeService
字段就是在启动kubelet阶段创建的remote对象。
kubelet创建Pod的过程
这里列出SyncPod()函数的工作流程,也就是创建Pod的过程。
检查Pod信息是否有更改,如果是则杀死现有的Pod,包括Pod中的所有容器。
创建SandBox(沙箱),其实就是启动pause容器,这个过程中包含了一些重要操作:
创建SandBox的函数:pkg/kubelet/dockershim/docker_sandbox.go:79
func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
拉取pause容器的镜像。
启动pause容器。
写入resolv.conf文件,覆盖docker默认创建的resolv.conf文件:pkg/kubelet/dockershim/docker_sandbox.go:140
containerInfo, err := ds.client.InspectContainer(createResp.ID)
设置Pod网络,也就是调用CNI来为容器添加网卡:pkg/kubelet/dockershim/docker_sandbox.go:163
err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations)
启动init容器。
最后启动所有业务容器:pkg/kubelet/kuberuntime/kuberuntime_manager.go:712
for _, idx := range podContainerChanges.ContainersToStart { if msg, err := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP, kubecontainer.ContainerTypeRegular); err != nil { ... }
至此Pod就算创建完成了,如果创建了相应的service,那么proxy组件会为service在物理机上通过iptables
工具创建数据转发规则,将集群内的请求Pod的数据转发到相应的Pod中,如果Pod有多个副本,则以负载均衡的方式分别转发给这个Pod。