kubernetes-kubelet进程源码分析(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hahachenchen789/article/details/87807076

kubelet关键代码分析

在上篇博文,我们分析了kubelet进程的启动流程,大致明白了kubelet的核心个哦你工作流程就是不断从Pod Source中获取与本节点相关的Pod,然后开始加工处理,所以我们先来分析Pod source部分代码。前面我们提到,kubelet可是同时支持三类Pod source,为了能够将不同的Pod source汇聚到一起统一处理,谷歌特地设计了Podconfig这个对象,其代码如下:

其中,source属性包括了当前加载的所有Pod source类型,sourceLock是source的排他锁,在新增Pod source的方法里使用它来避免共享冲突。

在Pod发生变动时,例如Pod创建、删除或更新,相关的Pod source就会产生对应的PodUpdate事件并推送到channel,为了能够统一处理来自多个source的channel,谷歌设计了config.mux这个聚合器,它负责监听多路channel,当接收到channel发来的事件后,交给merger对象进行统一处理,merger对象最终把多路channel发来的时间合并写入updates这个汇聚channel等待处理。

下面是config.mux的结构体定义,其属性sources为一个channel Map,key是对应的Pod source的类型:

我们继续深入分析config.Mux的工作过程,前面提到,kubelet在启动过程中在makePodSourceConfig方法里创建了一个PodConfig对象,并且根据启动参数来决定要加载哪些类型的Pod Source,在这个过程中调用了下述方法来创建一个对应的channel:

而channel具体的创建过程则在config.Mux里,channel创建完成后被加入config.mux的sources并启动一个协程开始监听消息,代码如下:

config.Mux的上述listen方法很简单,就是监听新创建的channel,一旦发现channel上有数据就交给merger进行处理:

我们先来看看Pod source是如何发送PodUpdate事件到自己所在的channel上的,在上篇博文,我们所见到的下面这段代码创建了一个config file类型的Pod source:

在NewSourceFile方法里启动了一个协程,每隔指定时间(kc.FileCheckFrequency)就执行一次sourceFile的run方法,在run方法里所调用的主体逻辑就是下面的函数:

看一下上面的代码,我们就大致明白了config file类型的Pod source是如何工作的:它从指定的目录中加载多了Pod定义文件并转换为Pod列表或者加载单个Pod定义文件并转化为单个Pod,然后生成对应的全量类型的PodUpdate事件并写入channel中去。

接下来我们分析merger对象,Podconfig里的merger对象其实是一个config.podStorage实例,它同时是PodConfig的pods属性的一个引用。podStorage的源码位于pkg/kubelet.config/config.go里,其定义如下:

我们看到podstorage的关键属性解释如下。

1.pods:类型是Map,存放每个Pod source上拉过来的pod数据,是podstorage当前保存全量Pod的地方

2.updates:它就是PodConfig里的updates属性的一个引用

3.mode:表明podstorage的pod事件通知模式,有以下几种:

1)PodConfigNotificationSnapshot:全量快照通知模式

2)PodConfigNotificationSnapshotAndUpdates:全量快照+更新Pod通知模式(代码中创建podstorage实例时采用的模式)

3)PodConfigNotificationIncremental:增量通知模式

podStorage实现的merge接口的源码如下:

在上述merge过程中,先调用内部函数merge,将Pod source的channel上发来的PodUpdate事件分解为对应的新增、更新及删除等三类PodUpdate事件,然后判断是否有更新事件,如果有,则直接写入汇总的channel里(podstorage.updates),然后调用mergestate函数复制一份podstorage的当前全量Pod列表,以此产生一个全量的PodUpdate事件并写入汇总的channel中,从而实现了多Pod Source channel的汇聚逻辑。

分析完merger过程以后,我们接下来看看是什么对象,以及如何消费这个汇总的channel。在上篇博文提到,在kubelet进程启动的过程中调用了startKubelet方法,此方法首先启动一个协程,让kubelet处理来自PodSource的Pod update消息,即下面这行代码:

‘’

其中,PodConfig的updates()方法返回了前面我们所说的汇总channel变量的一个引用,下面是kubelet的Run(updates < -chan PodUpdate)方法的代码:

上述代码首先启动了一个HTTP file server来远程获取本节点的系统日志,接下来根据启动参数的设置来决定是否在指定的docker容器中启动kubelet进程,然后分别启动Image manager(负责Image GC)、CAdvisor(Docker性能监控)、Container Manger(Container GC)、OOM Watcher(OOM监测)、Status Manager(负责同步本节点上Pod的状态到API Server上)等组件,最后进入syncLoop方法中,无线循环调用下面的syncLoopIteration方法:

在上述代码中,如果从Channel中拉取到了PodUpdate事件,则先调用podManager的UpdatePods方法来确定此PodUpdate的同步类型,并将结果放入podSyncTypes这个Map中,同时为了提升处理效率,在代码中增加了持续循环拉取PodUpdate数据直到channel为空为止的一段逻辑,在方法的最后,调用syncHandler接口来完成Pod同步的具体逻辑,从而实现了PodUpdate事件的高效批处理模式。

syncHandler在这里就是kubelet实例,它的syncPods方法比较长,其主要逻辑如下:

1.将传入的全量Pod,与Statusmanager中当前保存的Pod集合进行对比,删除Statusmanager中当前已经不存在的Pod(孤儿Pod)。

2.调用kubelet的admitPods方法以过滤掉不适合本节点创建的Pod。此方法首先过滤掉状态为failed或者succeeded的pod,接着过滤掉不适合本节点的Pod,比如Host Port冲突、Node Label的约束不匹配及Node的可用资源不足等情况;最后检查磁盘的使用情况,如果磁盘的可用空间不足,则过滤掉所有Pod。

3.对上述过滤后的Pod集合中的每一个Pod调用podWorkers的UpdatePod方法,而此方法内部创建了一个Pod的workUpdate事件并发布到该Pod对应的一个Work Channel上。

4.对于已经删除或者不存在的Pod,通知podWorkers删除相关联的work channel。

5.对比Node当前运行中的Pod及目标Pod列表,杀掉多余的Pod,并且调用docker runtime API,重新获取当前运行中的Pod列表信息。

6.清理孤儿Pod所遗留的PV和磁盘目录。

要真正理解Pod是怎么在Node上落地的,还要继续深入分析上述第三步的代码,首先我们看看对workUpdatec这个结构体的定义:

其中的属性Pod是当前要操作的Pod对象,mirrorPod则是对应的镜像Pod,下面是对它的解释:

对于每个来自非API Server Pod source上的Pod,kubelet都在API Sever上注册一个几乎一模一样的Pod,这个Pod被称为mirrorPod,这样一来,就将不同的Pod source上的Pod都统一到了kebelet的注册表上,从而统一了Pod生命周期的管理流程。

workUpdate的updateCompleteFn属性是一个回调函数,work完成后会执行此回调函数,在上述第三步中,此函数用来计算该work的调度时延指标。

对于每个要同步的Pod,podWorkers会用一个长度为1的channel来存放其对应workUpdate,而属性lastUndeliveredWorkUpdate则存放最后一个待安排执行的workUpdate,这是因为一个Pod的前一个workUpdate正在执行的时候,可能会有一个新的PodUpdate事件需要处理,理解了这个过程后,再来看podWorkers的定义,就不难了:

下面这个函数就是第3步里产生workUpdate事件并放入podWorkers的对应channel的方法的源码:

上述代码会调用podWorkers的managePodLoop方法来处理podUpdates队列,这里主要是获取必要的参数,最终处理又转手交给syncPodFn方法去处理,下面是managePodLoop的源码:

追踪podWorkers的构造函数调用过程,可以发现syncPodFn函数其实就是kubelet的syncPod方法,这个方法的代码量很多,主要逻辑如下:

1.根据系统配置中的权限控制,检查Pod是否有权在本节点运行,这些权限包括Pod是否有权使用HostNetwork(由Pod source类决定)、Pod中的容器是否被授权以特权模式启动(privileged mode)等,如果没有被授权,则删除当前运行中的旧版本的Pod实例并返回错误信息。

2.创建Pod相关的工作目录、PV存放目录、Plugin插件目录,这些目录都以Pod的UID为上一级目录。

3.如果Pod有PV定义,则针对每个PV执行目录的mount操作。

4.如果是syncPodUpdate类型的Pod,则从Docker runtime的API接口查询获取Pod及相关容器的最新状态信息。

5.如果Pod有imagePullSecrets属性,则在API Server上获取对应的Secret。

6.调度Container Runtime的API接口方法SyncPod,实现Pod真正同步的逻辑。

7.如果Pod source不来自API Server,则继续处理其关联的mirrorPod。

1)如果mirrorPod跟当前Pod的定义不匹配,则它会被删除。

2)如果mirrorPod还不存在(比如新创建的Pod),则会在API Server上新建一个。

kubernetes中container runtime的默认实现是dockers,对应类是dockertools.DockerManager,其源码位于pkg/kublelet/dockertools/manager.go里,在上述kubelet.syncPod方法中所调用的DockerManager的syncPod方法实现了下面的逻辑:

1.判断一个Pod实例的哪些组成部分需要重启:包括Pod的infra容器是否发生变化(如网络模式、Pod里运行的各个容器的端口是否发生变化);Pod里运行的容器是否发生变化;用Probe检测容器的状态以确定容器是否异常等。

2.根据Pod实例重启结果的判断,如果需要重启Pod的infra容器,则先kill Pod然后启动Pod的infra容器,设定好网络,最后启动Pod里的所有container;否则就先kill那些需要重启的container,然后重新启动它们。注意,如果是新创建的Pod,则因为找不到Node上对应的Pod的infra容器,所以会被当做重启Pod的infra容器的逻辑来实现创建过程。

dockermanager创建Pod的infra容器的逻辑在createPodInfracontainer方法里,大致逻辑如下:

1.如果Pod的网络不是HostNetwork模式,则搜集Pod所有容器的Port作为infra容器所要暴露的Port列表。

2.如果infra容器的Image目前不存在,则尝试拉取Image。

3.创建infra的container对象并且启动run ContainerInPod方法。

4.如果容器定义有Lifecycle,并且PostStart回调方法被设置了,就会触发此方法的调用,如果调用失败则kill容器并返回。

5.创建一个软连接文件指向容器的日志问加你,此软连接文件名包括Pod的名称、容器的名称及容器的ID,这样的目的是让ElasticSearch这样的搜索技术容易索引和定位Pod日志。

6.如果此容器是Pod infra容器,则设置其OOM参数低于标准值,使得它比其他容器具备更强的抗灾能力

7.修改docker生成的容器的resolv.conf文件,增加ndots参数并默认设置为5,这是因为kubernetes默认假设的域名分割长度是5.

上述逻辑中所调用的runContainerInPod是Dock人Manager的核心方法之一,不管是创建Pod的infra容器还是Pod里的其他容器,都会通过此方法使得容器被创建和运行,以下是其主要逻辑。

1.生成container必要的环境变量和参数,比如ENV环境变量,Volume Mounts信息、端口映射信息、DNS服务器信息、容器的日志目录、parent cgroup等。

2.调用runContainer方法完成Docker container实例的创建过程,简单地说,就是完成docker create container命令行所需的各种参数的构造过程,并通过程序来调用执行。

3.构造HostConfig对象,主要参数有目录映射、端口映射等、Cgroup设定等,简单地说,就是完成了docker start container命令行所需的必要参数的构造过程,并通过程序来调用执行。

在上述逻辑中,runContainer与startContainer的具体实现都是靠DockerManger中的dockerClient对象完成的,它实现了DockerInterface接口,dockerClient的创建过程在pkg/kubelet/dockertools/docker.go里,下面是这段代码:

这里的dockerEndpoint是本节点上的docker deamon进程的访问地址,默认是unix:///var/run/docker/sock,在上述代码中使用了来自开源项目http://github.com/fsouza/go-dockerclient提供的docker client,它也是Go语言实现的一个用HTTP访问Docker Deamon提供的标准API的客户端框架。

我们来看看dockerclient创建容器的具体代码(createcontainer):

上述代码其实就是通过调用标准的docker Rest API来实现功能的,我们进入docker.client的do方法可以看到更多详情,例如输入参数转化为JSON格式的数据、DockerAPI版本检查及异常处理等逻辑,最有趣的是:在dockerEndpoint是Unix套接字的情况下,会先建立套接字连接,然后在这个连接上创建HTTP连接。

至此,我们分析了kubelet创建和同步Pod实例的整个流程,简单总结如下:

1.汇总,先将多个Pod source上过来的PodUpdate事件汇总到一个总的channel上去。

2.初审:分析并过滤掉不符合本节点的PodUpdate时间,对满足条件的PodUpdate则生成一个workupdate时间,交给podworkers处理。

3.接待:podworkers对每个pod的workUpdate事件排队,并且负责更新cache中的Pod状态,而把具体的任务转给kubelet去处理(syncPod方法)。

4.终审:kubelet对符合条件的Pod进一步审查,例如检查Pod是否有权在本节点运行,对符合审查的Pod开始着手准备工作,包括目录创建、PV创建、image获取、处理Mirror Pod问题等,然后把皮球踢给了dockermanager。

5.落地:任务抵达dockermanager之后,dockermanager尽心尽责分析每个pod的情况,以决定这个Pod究竟是新建、完全重启还是部分更新的,给出分析结果后,剩下的就是dockerclient的工作了。

猜你喜欢

转载自blog.csdn.net/hahachenchen789/article/details/87807076