Kubernetes架构和核心组件组成 Kubernetes节点“容器运行时”技术分析

来源: Chinaunix 作者:lvyilong316 2020-09-25 15:53:00

Kubernetes架构简介

Kubernetes架构如下图所示:

在这张系统架构图中,我们把服务分为运行在工作节点上的服务和组成集群级别控制板的服务。Kubernetes节点有运行应用容器必备的服务,而这些都是受Master的控制。

每次个节点上当然都要运行Docker。Docker来负责所有具体的映像下载和容器运行。

Kubernetes主要由以下几个核心组件组成:

1) etcd保存了整个集群的状态;

2) apiserver提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;

3) controller manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;

4) scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;

5) kubelet负责维护容器的生命周期,同时也负责Volume(CVI)和网络(CNI)的管理;

6) Container runtime负责镜像管理以及Pod和容器的真正运行(CRI);

7) kube-proxy负责为Service提供cluster内部的服务发现和负载均衡;

而和运行时紧密相关的就是kubelet。

kubelet架构

kubelet架构如下图所示:

kubelet 是运行在每个节点上的主要的“节点代理”,每个节点都会启动 kubelet进程,用来处理 Master 节点下发到本节点的任务,按照 PodSpec 描述来管理Pod 和其中的容器(PodSpec 是用来描述一个 pod 的 YAML 或者 JSON 对象)。kubelet 通过各种机制(主要通过 apiserver )获取一组 PodSpec 并保证在这些 PodSpec 中描述的容器健康运行。

容器运行时接口(CRI)

Kubernetes节点的底层由一个叫做“容器运行时”的软件进行支撑,它负责比如启停容器这样的事情。最广为人知的容器运行时当属Docker,但它不是唯一的。例如最近比较火热的安全容器KataContainer。所以也就很自然会与有一个需求,就是我们怎么去把 KataContainer run 在 Kubernetes 里? 

那么这个时候我们还是先来看 Kubelet 在做什么事情,所以 Kubelet 要想办法像 call docker 一样去 call KataContainer,然后由 KataContainer 负责帮忙把 hypervisor 这些东西 set up 起来,帮我把这个小VM 运行起来。所以这个时候就要需要想怎么让 Kubernetes 能合理的操作 KataContainers。

对于这个诉求,就关系到Container Runtime Interface ,我们叫它 CRI。CRI 的作用其实只有一个:就是它描述了对于 Kubernetes 来说,一个 Container 应该有哪些操作,每个操作有哪些参数,这就是 CRI 的一个设计原理(本质上是一堆ops)

Kubelet与容器运行时通信(或者是CRI插件填充了容器运行时)时,Kubelet就像是客户端,而CRI插件就像对应的服务器。它们之间可以通过Unix 套接字或者gRPC框架进行通信。

protocol buffers API包含了两个gRPC服务:ImageService和RuntimeService。ImageService提供了从镜像仓库拉取、查看、和移除镜像的RPC。RuntimeSerivce包含了Pods和容器生命周期管理的RPC,以及跟容器交互的调用(exec/attach/port-forward)。一个单块的容器运行时能够管理镜像和容器(例如:Docker和Rkt),并且通过同一个套接字同时提供这两种服务。这个套接字可以在Kubelet里通过标识–container-runtime-endpoint和–image-service-endpoint进行设置。

下图显示了ImageService和RuntimeService具体需要实现哪些接口。

CRI Shim

CRI Shim 可以做什么?它可以把 CRI 请求 翻译成 Runtime  API。我举个例子,比如说现在有个 Pod 里有一个 A 容器和有个 B 容器,这时候我们把这件事提交给 Kubernetes 之后,在 Kubelet 那一端发起的 CRI code 大概是这样的序列:首先它会 run Sandbox foo,如果是 Docker 它会起一个 infra 容器,就是一个很小的容器叫 foo,如果是 Kata 它会给你起一个虚拟机叫 foo,这是不一样的。

所以接下来你 creat start container A 和 B 的时候,在 Docker 里面是起两个容器,但在 Kata 里面是在我这个小虚拟机里面,在这 Sandbox 里面起两个小 NameSpace,这是不一样的。所以你把这一切东西总结一下,你会发现 OK,我现在要把 Kata run 在 Kubernetes 里头,所以我要做工作,在这一步要需要去做这个 CRI shim,我就想办法给 Kata 作一个 CRI shim。

而我们能够想到一个方式,我能不能重用现在的这些 CRI shim。重用现在哪些?比如说 CRI containerd 这个项目它就是一个 containerd 的 CRI shim,它可以去响应 CRI 的请求过来,所以接下来我能不能把这些情况翻译成对 Kata 这些操作,所以这个是可以的,这也是我们将用一种方式,就是把 KataContainers 接到我的 Containerd 后面。这时候它的工作原理大概这样这个样子,Containerd 它有一个独特设计,就是他会为每一个 Contaner 起个叫做 Contained shim。你 run 一下之后你会看他那个宿主机里面,会 run 一片这个 Containerd shim 一个一个对上去。

而这时候由于 Kata 是一个有 Sandbox 概念的这样一个 container runtime,所以 Kata 需要去 match 这些 Shim 与 Kata 之间的关系,所以 Kata 做一个 Katashim。把这些东西对起来,就把你的 Contained 的处理的方式翻译成对 kata 的 request,这是我们之前的一个方式。

但是你能看到这其实有些问题的,最明显的一个问题在于对 Kata 或 gVisor 来说,他们都是有实体的 Sandbox 概念的,而有了 Sandbox 概念后,它就不应该去再去给他的每一个 Container 启动有一个 shim match 起来,因为这给我们带来很大的额外性能损耗。我们不希望每一个容器都去 match 一个 shim,我们希望一个 Sandbox match 一个 shim

另外,就是你会发现 CRI 是服务于 Kubernetes 的,而且它呈现向上汇报的状态,它是帮助 Kubernetes 的,但是它不帮助 Container runtime。所以说当你去做这个集成时候,你会发现尤其对于 VM gVisor\KataContainer 来说,它与 CRI 的很多假设或者是 API 的写法上是不对应的。所以你的集成工作会比较费劲,这是一个不 match 的状态。

最后一个就是我们维护起来非常困难,因为由于有了 CRI 之后,比如 RedHat 拥有自己的 CRI 实现叫 cri-o(基于Open Container Initiative的Kubernetes Container Runtime Interface的实现),他们和 containerd 在本质上没有任何区别,跑到最后都是靠 runC 起容器,为什么要这种东西?

我们不知道,但是我作为 Kata maintainer,我需要给他们两个分别写两部分的 integration 把 Kata 集成进去。这就很麻烦,者就意味着我有 100 种这种 CRI 我就要写 100 个集成,而且他们的功能全部都是重复的。

Containerd ShimV2

为了解决以上的shim问题,引入了shimv2。前面我们说过 CRI,CRI 决定的是 Runtime 和 Kubernetes 之间的关系,那么我们现在能不能再有一层更细致的 API 来决定我的 CRI Shim 跟下面的 Runtime 之间真正的接口是什么样的?

这就是 ShimV2 出现的原因,它是一层 CRI shim 到 Containerd runtime 之间的标准接口,所以前面我直接从 CRI 到 Containerd 到 runC,现在不是。我们是从 CRI 到 Containerd 到 ShimV2,然后 ShimV2 再到 RunC 再到 KataContainer。这么做有什么好处?

最大的区别在于:在这种方式下,你可以为每一个 Pod 指定一个 Shim。因为在最开始的时候,Containerd 是直接启动了一个 Containerd Shim 来去做响应,但我们新的 API 是这样写的,是 Containerd Shim start 或者 stop。所以这个 start 和 stop 操作怎么去实现是你要做的事情。

例如KataContainers项目可以这么实现:在 created Sandbox 的时候 call 这个 start 的时候,我启动一个 Containerd Shim。但是当我下一步是 call API 的时候,就前面那个 CRI 里面, Container API 时候,我就不再起了,我是 reuse,我重用为你创建好的这个 Sandbox,这就位你的实现提供了很大的自由度。

所以这时候你会发现整个实现的方式变了,这时候 Containerd 用过来之后,它不再去 care 每个容器起 Containerd Shim,而是由你自己去实现。我的实现方式是我只在 Sandbox 时候,去创建 containerd-shim-v2,而接下来整个后面的 container level 操作,我会全部走到这个 containerd-shim-v2 里面,我去重用这个 Sandbox,所以这个跟前面的时间就出现很大的不同。如下图所示是Kata1.5中采用shim v2的实现。

首先,你还是用原来的 CRI Containerd,只不过现在装的是 runC,你现在再装一个 katacontainer 放在那机器上面。接下来我们 Kata 那边会给你写一个实现叫 kata-Containerd-Shimv2。所以前面要写一大坨 CRI 的东西,现在不用了。现在,我们只 focus 在怎么去把 Containerd 对接在 kata container 上面,就是所谓的实现 Shimv2 API,这是我们要做的工作。而具体到我们这要做的事情上,其实它就是这样一系列与 run 一个容器相关的 API。

比如说我可以去 create、start,这些操作全部映射在我 Shimv2 上面去实现,而不是说我现在考虑怎么去映射,去实现 CRI,这个自由度由于之前太大,造成了我们现在的一个局面,就有一堆 CRI Shim 可以用。这其实是一个不好的事情。有很多政治原因,有很多非技术原因,这都不是我们作为技术人员应该关心的事情,你现在只需要想我怎么去跟 Shimv2 对接就好了。

容器运行时总结

下图显示了当前主要的容器运行时和主要维护者。

0
收藏
0