5.4 kubelet运行机制解析

在Kubernetes集群中,在每个Node(又称Minion)上都会启动一个kubelet服务进程。该进程用于处理Master下发到本节点的任务,管理Pod及Pod中的容器。每个kubelet进程都会在API Server上注册节点自身的信息,定期向Master汇报节点资源的使用情况,并通过cAdvisor监控容器和节点资源。

5.4.1 节点管理

节点通过设置kubelet的启动参数“--register-node”,来决定是否向API Server注册自己。如果该参数的值为true,那么kubelet将试着通过API Server注册自己。在自注册时,kubelet启动时还包含下列参数。

◎ --api-servers:API Server的位置。

◎ --kubeconfig:kubeconfig文件,用于访问API Server的安全配置文件。

◎ --cloud-provider:云服务商(IaaS)地址,仅用于公有云环境中。

一开始,每个kubelet进程都被授予创建和修改任何节点的权限,后来这个安全漏洞被修复。Kubernetes限制了kubelet的权限,仅允许它修改和创建其所在节点的权限。如果在集群运行过程中遇到集群资源不足的情况,用户就很容易通过添加机器及运用kubelet的自注册模式来实现扩容。在某些情况下,Kubernetes集群中的某些kubelet没有选择自注册模式,用户需要自己去配置Node的资源信息,同时告知Node上kubelet API Server的位置。集群管理者能够创建和修改节点信息,如果其希望手动创建节点信息,则通过设置kubelet的启动参数“--register-node=false”即可完成。

kubelet在启动时通过API Server注册节点信息,并定时向API Server发送节点的新消息,API Server在接收到这些信息后,会将其写入etcd中。通过kubelet的启动参数--node-status-update-frequency可设置kubelet每隔多长时间向API Server报告节点的状态,默认为10s。

5.4.2 Pod管理

kubelet通过以下方式获取在自身Node上要运行的Pod清单。

(1)静态Pod配置文件:kubelet通过启动参数--config指定目录下的Pod YAML文件(默认目录为/etc/kubernetes/manifests/),kubelet会持续监控指定目录下的文件变化,以创建或删除Pod。这种类型的Pod没有通过kube-controller-manager进行管理,被称为“静态Pod”。另外,可以通过启动参数--file-check-frequency设置检查该目录的时间间隔,默认为20s。

(2)HTTP端点(URL):通过--manifest-url参数设置,通过--http-check-frequency设置检查该HTTP端点数据的时间间隔,默认为20s。

(3)API Server:kubelet通过API Server监听etcd目录,同步Pod列表。

所有以非API Server方式创建的Pod都叫作Static Pod。kubelet将Static Pod的状态汇报给API Server,API Server为该Static Pod创建一个Mirror Pod与其匹配。Mirror Pod的状态将真实反映Static Pod的状态。当Static Pod被删除时,与之相对应的Mirror Pod也会被删除。在本章中只讨论通过API Server获得Pod清单的方式。kubelet通过API Server Client使用Watch加List的方式监听/registry/nodes/$当前节点的名称和/registry/pods目录,将获取的信息同步到本地缓存中。

kubelet监听etcd,所有针对Pod的操作都会被kubelet监听。如果发现有新的绑定到本节点的Pod,则按照Pod清单的要求创建该Pod。

如果发现本地的Pod需要被修改,则kubelet会做出相应的修改,比如在删除Pod中的某个容器时,会通过Docker Client删除该容器。如果发现本地的Pod需要被删除,则kubelet会删除相应的Pod,并通过Docker Client删除Pod中的容器。

kubelet读取监听到的信息,如果是创建和修改Pod任务,则做如下处理。

(1)为该Pod创建一个数据目录。

(2)从API Server中读取该Pod清单。

(3)为该Pod挂载外部卷(External Volume)。

(4)下载Pod用到的Secret。

(5)检查已经运行在节点上的Pod,如果该Pod没有容器或Pause容器(kubernetes/pause镜像创建的容器)没有启动,则先停止Pod里所有容器的进程。如果在Pod中有需要删除的容器,则删除这些容器。

(6)用kubernetes/pause镜像为每个Pod都创建一个容器。该Pause容器用于接管Pod中所有其他容器的网络。每创建一个新的Pod,kubelet都会先创建一个Pause容器,然后创建其他容器。kubernetes/pause镜像大概有200KB,是个非常小的容器镜像。

(7)为Pod中的每个容器都做如下处理。

◎ 为容器计算一个哈希值,然后用容器的名称去查询对应Docker容器的哈希值。若查找到容器,且二者的哈希值不同,则停止Docker中容器的进程,并停止与之关联的Pause容器的进程;若二者相同,则不做任何处理。

◎ 如果容器被终止,且容器没有指定的restartPolicy(重启策略),则不做任何处理。

◎ 调用Docker Client下载容器镜像,调用Docker Client运行容器。

5.4.3 容器健康检查

Pod通过两类探针来检查容器的健康状态。一类是LivenessProbe探针,用于判断容器是否健康并反馈给kubelet,如果LivenessProbe探针探测到容器不健康,则kubelet将删除该容器,并根据容器的重启策略做相应的处理;如果一个容器不包含LivenessProbe探针,则kubelet会认为该容器的LivenessProbe探针返回的值永远是Success。另一类是ReadinessProbe探针,用于判断容器是否启动完成,且准备接收请求。如果ReadinessProbe探针检测到容器启动失败,则Pod的状态将被修改,Endpoint Controller将从Service的Endpoint中删除包含该容器所在Pod的IP地址的Endpoint条目。

kubelet定期调用容器中的LivenessProbe探针来诊断容器的健康状况。LivenessProbe包含以下3种实现方式。

(1)ExecAction:在容器内部运行一个命令,如果该命令的退出状态码为0,则表明容器健康。

(2)TCPSocketAction:通过容器的IP地址和端口号执行TCP检查,如果端口能被访问,则表明容器健康。

(3)HTTPGetAction:通过容器的IP地址和端口号及路径调用HTTP Get方法,如果响应的状态码大于或等于200且小于或等于400,则认为容器状态健康。

LivenessProbe探针被包含在Pod定义的spec.containers.{某个容器}中。下面的示例展示了两种Pod中的容器健康检查方式:HTTP检查和容器命令执行检查。

(1)本示例实现了通过容器命令执行检查:

kubelet在容器中运行“cat/tmp/health”命令,如果该命令返回的值为0,则表明容器处于健康状态,否则表明容器处于不健康状态。

(2)本示例实现了对容器的HTTP检查:

kubelet发送一个HTTP请求到本地主机、端口及指定的路径,来检查容器的健康状况。

5.4.4 cAdvisor资源监控

在Kubernetes集群中,应用程序的执行情况可以在不同的级别监测到,这些级别包括容器、Pod、Service和整个集群。作为Kubernetes集群的一部分,Kubernetes希望提供给用户详细的各个级别的资源使用信息,这将使用户深入地了解应用的执行情况,并找到应用中可能的瓶颈。

cAdvisor是一个开源的分析容器资源使用率和性能特性的代理工具,它是因为容器而产生的,因此自然支持Docker容器。在Kubernetes项目中,cAdvisor被集成到Kubernetes代码中,kubelet则通过cAdvisor获取其所在节点及容器上的数据。cAdvisor自动查找其所在Node上的所有容器,自动采集CPU、内存、文件系统和网络使用的统计信息。在大部分Kubernetes集群中,cAdvisor都通过它所在Node的4194端口暴露一个简单的UI。

如图5.14所示是cAdvisor的一个截图。kubelet作为连接Kubernetes Master和各Node的桥梁,管理运行在Node上的Pod和容器。kubelet将每个Pod都转换成它的成员容器,同时从cAdvisor上获取单独的容器使用统计信息,然后通过该REST API暴露这些聚合后的Pod资源使用的统计信息。

图5.14 cAdvisor的一个截图

cAdvisor只能提供2~3min的监控数据,对性能数据也没有持久化,因此在Kubernetes的早期版本中需要依靠Heapster来实现集群范围内全部容器性能指标的采集和查询功能。从Kubernetes 1.8版本开始,性能指标数据的查询接口升级为标准的Metrics API,后端服务则升级为全新的Metrics Server。因此,cAdvisor在4194端口提供的UI和API服务从Kubernetes 1.10版本开始进入弃用流程,并于 1.12版本时完全关闭。如果还希望使用cAdvisor的这个特性,则从1.13版本开始可以通过部署一个DaemonSet在每个Node上都启动一个cAdvisor来提供UI和API,请参考cAdvisor在GitHub上的说明。

在新的Kubernetes监控体系中,Metrics Server用于提供Core Metrics(核心指标),包括Node和Pod的CPU和内存使用数据。其他Custom Metrics(自定义指标)则由第三方组件(如Prometheus)采集和存储。

5.4.5 容器运行时

kubelet负责本节点上所有Pod的全生命周期管理,其中就包括相关容器的创建和销毁这种基本操作。容器的创建和销毁等操作的代码不属于Kubernetes的代码范畴,比如目前流行的Docker容器引擎就属于Docker公司的产品,所以kubelet需要通过某种进程间的调用方式如gRPC来实现与Docker容器引擎之间的调用控制功能。在说明其原理和工作机制之前,我们首先要理解一个重要的概念——Container Runtime(容器运行时)。

“容器”这个概念是早于Docker出现的,容器技术最早来自Linux,所以又被称为Linux Container。LXC项目是一个Linux容器的工具集,也是真正意义上的一个Container Runtime,它的作用就是将用户的进程包装成一个Linux容器并启动运行。Docker一开始时就使用了LXC项目代码作为Container Runtime来运行容器,但从0.9版本开始被Docker公司自研的新一代容器运行时Libcontainer所取代,再后来,Libcontainer的代码被改名为runc,被Docker公司捐赠给了OCI组织,成为OCI容器运行时规范的第1个标准参考实现。所以,LXC与runC其实都可被看作开源的Container Runtime,但它们都属于低级别的容器运行时(low-level container runtimes),因为它们不涉及容器运行时所依赖的镜像操作功能,比如拉取镜像,也没有对外提远程供编程接口以方便其他应用集成,所以又有了后来的高级别容器运行时(high-level container runtimes),其中最知名的就是Docker公司开源的containerd。containerd被设计成嵌入一个更大的系统如Kubernetes中使用,而不是直接由开发人员或终端用户使用,containerd底层驱动runc来实现底层的容器运行时,对外则提供了镜像拉取及基于gRPC接口的容器CRUD封装接口。发展至今,containerd已经从Docker里的一个内部组件,变成一个流行的、工业级的开源容器运行时,已经支持容器镜像的获取和存储、容器的执行和管理、存储和网络等相关功能。在containerd和runC成为标准化容器服务的基石后,上层应用就可以直接建立在containerd和runC之上了。如果我们只希望用一个纯粹的、稳定性更好、性能更优的容器运行时,就可以直接使用containerd而无须再依赖Docker了。

除了containerd,还有类似的其他一些高层容器运行时也都在runC的基础上发展而来,目前比较流行的有红帽开源的CRI-O、openEuler社区开源的iSula等。这些Container Runtime还有另外一个共同特点,即都实现了Kubernetes提出的CRI接口规范(Container Runtime Interface),可以直接接入Kubernetes中。CRI顾名思义,就是容器运行时接口规范,这个规范也是Kubernetes顺应容器技术标准化发展潮流的一个重要历史产物,早在Kubernetes 1.5版本中就引入了CRI接口规范。如图5.15所示,引入了CRI接口规范后,kubelet就可以通过CRI插件来实现容器的全生命周期控制了,不同厂家的Container Runtime只需实现对应的CRI插件代码即可,Kubernetes无须重新编译就可以使用更多的容器运行时。

图5.15 CRI接口规范示意图

如图5.16所示,CRI接口规范主要定义了两个gRPC接口服务:ImageService和RuntimeService。其中,ImageService提供了从仓库拉取镜像、查看和移除镜像的功能;RuntimeService则负责实现Pod和容器的生命周期管理,以及与容器的交互(exec/attach/port-forward)。我们知道,Pod由一组应用容器组成,其中包含共有的环境和资源约束,这个环境在CRI里被称为Pod Sandbox。Container Runtime可以根据自己的内部实现来解释和实现自己的Pod Sandbox,比如对于Hypervisor这种容器运行时引擎,会把PodSandbox具体实现为一个虚拟机。所以,RuntimeService服务接口除了提供了针对Container的相关操作,也提供了针对Pod Sandbox的相关操作以供kubelet调用。在启动Pod之前,kubelet调用RuntimeService.RunPodSandbox来创建Pod环境,这一过程也包括为Pod设置网络资源(分配IP等操作),Pod Sandbox在被激活之后,就可以独立地创建、启动、停止和删除用户业务相关的Container了,当Pod销毁时,kubelet会在停止和删除Pod Sandbox之前首先停止和删除其中的Container。

图5.16 CRI接口规范的工作原理

本节最后说说容器运行时相关的另外一个重要概念——RuntimeClass。

随着CRI机制的成熟及第三方Container Runtime的不断涌现,用户有了新的需求:在一个Kubernetes集群中配置并启用多种Container Runtime,不同类型的Pod可以选择不同特性的Container Runtime来运行,以实现资源占用或者性能、稳定性等方面的优化,这就是RuntimeClass出现的背景和动力。Kubernetes从1.12版本开始引入RuntimeClass,用于在启动容器时选择特定的容器运行时,目前为Beta阶段。以下面的RuntimeClass例子为例:

其中,handler参数是对应的CRI配置名称,指定Container Runtime的类型,一旦创建好RuntimeClass资源,我们就可以通过Pod中的spec.runtimeClassName字段与它进行关联了。当目标Pod被调度到某个具体的kubelet时,kubelet就会通过CRI接口调用指定的Container Runtime来运行该Pod,如果指定的RuntimeClass不存在,无法运行相应的Container Runtime,那么Pod会进入Failed状态。