所有文章

k8s源码分析-kubelet

本文以k8s v1.10为例,分析kubelet组件的工作原理。

入口

main函数定义在cmd/kubelet/kubelet.go,主要任务如下:

  1. 创建命令行对象,包括解析用户指定的参数,生成配置对象等
  2. 执行命令行对象,最后进入启动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 rundocker 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.syncPodpkg/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处理器

pkg/kubelet/kubelet.go:1413

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的过程。

  1. 检查Pod信息是否有更改,如果是则杀死现有的Pod,包括Pod中的所有容器。

  2. 创建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)
      
  3. 启动init容器。

  4. 最后启动所有业务容器: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。


编写日期:2018-03-06