kubelet 原理解析三:runtime

本文转自:https://feisky.xyz/posts/kubernetes-container-runtime/

架构

Kubelet 架构图

  • Generic Runtime Manager:这是容器运行时的管理者,负责于 CRI 交互,完成容器和镜像的管理
  • 在 CRI 之下,包括两种容器运行时的实现
* 一个是内置的 dockershim,实现了 docker 容器引擎的支持以及 CNI 网络插件(包括 kubenet)的支持
* 另一个就是外部的容器运行时,用来支持 runc、containerd、gvisor 等外部容器运行时

Kubelet 通过 CRI 接口跟外部容器运行时交互,它包括

  • CRI Server: 这是 CRI gRPC server,监听在 unix socket 上面
  • Streaming Server: 提供 streaming API,包括 Exec、Attach、Port Forward
  • 容器和镜像的管理:比如拉取镜像、创建和启动容器等
  • CNI 网络插件的支持: 用于给容器配置网络
  • 容器引擎的管理: 比如支持 runc 、containerd 或者支持多个容器引擎

这样,Kubernetes 中的容器运行时按照不同的功能就可以分为三个部分:

  • 第一个是 Kubelet 中容器运行时的管理,它通过 CRI 管理容器和镜像
  • 第二个是容器运行时接口,是 Kubelet 与外部容器运行时的通信接口
  • 第三个是具体的容器运行时实现,包括 Kubelet 内置的 dockershim 以及外部的容器运行时(如 cri-o、cri-containerd、frakti等)

这样的话,我们就可以基于这三个不同部分来看一看容器运行时的演进过程

演进过程


容器运行时的演进可以分为三个阶段:

  • 第一阶段: 在 Kubernetes v1.5 之前,Kubelet 内置了 Docker 和 rkt 的支持,并且通过 CNI 网络插件给它们配置容器网络。这个阶段的用户如果需要自定义运行时的功能是比较痛苦的,需要修改 Kubelet 的代码,并且很有可能这些修改无法推到上游社区。这样,还需要维护一个自己的 fork 分支,维护和升级都非常麻烦。

  • 第二阶段: 不同用户实现的容器运行时各有所长,许多用户都希望Kubernetes支持更多的运行时。于是,从v1.5 开始增加了 CRI 接口,通过容器运行时的抽象层消除了这些障碍,使得无需修改 Kubelet 就可以支持运行多种容器运行时。CRI 接口包括了一组 Protocol Buffer、gRPC API 、用于 streaming 接口的库以及用于调试和验证的一系列工具等。在此阶段,内置的 Docker 实现也逐步迁移到了 CRI 的接口之下。但此时 rkt 还未完全迁移,这是因为 rkt 迁移 CRI 的过程将在独立的 repository 完成,方便其维护和管理。

  • 第三阶段: 从 v1.11 开始,Kubelet 内置的 rkt 代码删除,CNI 的实现迁移到 dockershim 之内。这样,除了 docker 之外,其他的容器运行时都通过 CRI 接入。外部的容器运行时一般称为 CRI Shim,它除了实现 CRI 接口外,也要负责为容器配置网络。一般推荐使用 CNI,因为这样可以支持社区内的众多网络插件,不过这不是必需的,网络插件只需要满足 Kubernetes 网络的基本假设即可,即 IP-per-Pod、所有 Pod 和 Node 都可以直接通过 IP 相互访问。

容器运行时接口(CRI)

容器运行时接口(CRI)是一个用来扩展容器运行时的接口,它基于 gPRC,用户不需要关心内部通信逻辑,而只需要实现定义的接口就可以,包括 RuntimeService 和 ImageService。

  • RuntimeService负责管理Pod和容器的生命周期
  • ImageService负责镜像的生命周期管理

除了 gRPC API,CRI 还包括用于实现 streaming server 的库(用于 Exec、Attach、PortForward 等接口)和 CRI Tools。

基于 CRI 接口的容器运行时通常称为 CRI shim, 这是一个 gRPC Server,监听在本地的unix socket上;而kubelet作为gRPC的客户端来调用CRI接口。另外,外部容器运行时需要自己负责管理容器的网络,推荐使用CNI,这样跟Kubernetes的网络模型保持一致。

CRI 的推出为容器社区带来了新的繁荣,cri-o、frakti、cri-containerd 等一些列的容器运行时为不同场景而生:

  • cri-containerd——基于 containerd 的容器运行时
  • cri-o——基于 OCI 的容器运行时
  • frakti——基于虚拟化的容器运行时

而基于这些容器运行时,还可以轻易联结新型的容器引擎,比如可以通过 clear container、gVisor 等新的容器引擎配合 cri-o 或 cri-containerd 等轻易接入 Kubernetes,将 Kubernetes 的应用场景扩展到了传统 IaaS 才能实现的强隔离和多租户场景。

当使用CRI运行时,需要配置kubelet的–container-runtime参数为remote,并设置–container-runtime-endpoint为监听的unix socket位置(Windows上面为 tcp 端口)。

CRI 接口

CRI 接口包括 RuntimeService 和 ImageService 两个服务,这两个服务可以在一个 gRPC server 里面实现,当然也可以分开成两个独立服务。目前社区的很多运行时都是将其在一个 gRPC server 里面实现。

分类

管理镜像的 ImageService 提供了 5 个接口,分别是查询镜像列表、拉取镜像到本地、查询镜像状态、删除本地镜像以及查询镜像占用空间等。这些都很容易映射到 docker API 或者 CLI 上面。

而 RuntimeService 则提供了更多的接口,按照功能可以划分为四组:

  • PodSandbox 的管理接口:PodSandbox 是对 Kubernete Pod 的抽象,用来给容器提供一个隔离的环境(比如挂载到相同的 cgroup 下面),并提供网络等共享的命名空间。PodSandbox 通常对应到一个 Pause 容器或者一台虚拟机。
  • Container 的管理接口:在指定的 PodSandbox 中创建、启动、停止和删除容器。
  • Streaming API 接口:包括 Exec、Attach 和 PortForward 等三个和容器进行数据交互的接口,这三个接口返回的是运行时 Streaming Server 的 URL,而不是直接跟容器交互。
  • 状态接口,包括查询 API 版本和查询运行时状态。

Streaming API

Streaming API 用于客户端与容器进行交互,包括Exec、PortForward 和 Attach 等三个接口。Kubelet 内置的 docker 通过 nsenter、socat 等方法来支持这些特性,但它们不一定适用于其他的运行时,也不支持 Linux 之外的其他平台。因而,CRI 也显式定义了这些 API,并且要求容器运行时返回一个 streaming server 的 URL 以便 Kubelet 重定向 API Server 发送过来的流式请求。

这是因为所有容器的流式请求都会经过 Kubelet,这有可能会给节点的网络流量带来瓶颈,因而 CRI 要求容器运行时启动一个对应请求的单独的流服务器,将地址返回给Kubelet。Kubelet然后将这个信息再返回给Kubernetes API Server,它会打开直接与运行时提供的服务器相连的流连接,并通过它跟客户端连通。

这样一个完整的 Exec 流程就如上图所示,分为多个阶段:

  • 客户端 kubectl exec -i -t …
  • kube-apiserver 向 Kubelet 发送流式请求 /exec/
  • Kubelet 通过 CRI 接口向 CRI Shim 请求 Exec 的 URL
  • CRI Shim 向 Kubelet 返回 Exec URL
  • Kubelet 向 kube-apiserver 返回重定向的响应
  • kube-apiserver 重定向流式请求到 Exec URL,接着就是 CRI Shim 内部的 Streaming Server 跟 kube-apiserver 进行数据交互,完成 Exec 的请求和响应

在 v1.10 及更早版本中,容器运行时必需返回一个 API Server 可直接访问的 URL(通常跟 Kubelet 使用相同的监听地址);而从 v1.11 开始,Kubelet 新增了–redirect-container-streaming选项(默认为 false),支持不转发而是代理 Streaming 请求,这样运行时可以返回一个 localhost 的 URL(当然也不再需要配置 TLS)。

运行时分类

以下是几个常见容器运行时的例子,它们各有所长,并且也支持不同的容器引擎:

隔离性

在多租户场景下,强隔离(特别是虚拟化级别的隔离)是一个最基本的需求。

以前使用 Kubernetes 时,由于只支持Docker 容器,而它只提供了内核命名空间(namespace)的隔离,虽然也支持 SELinux、AppArmor 等基本的安全控制,但还是无法满足多租户的需求。所以曾经社区有人提出节点独占的方式实现租户隔离,即每个容器或租户独占一台虚拟机,资源的浪费是很明显的。

有了 CRI 之后,就可以接入 Kata Container、Clear Container 等基于虚拟化的容器引擎。这样通过虚拟化实现了容器的强隔离,不同租户的容器也可以运行在相同的 Node 上,大大提高了资源的利用率。

当然了,多租户不仅需要容器自身的强隔离,还需要众多其他的功能一起配合,比如

网络隔离,比如可以使用 CNI 构建新的网络插件,把不同租户的 Pod 接入到相互隔离的虚拟网络中。
资源管理,比如基于 CRD 构建租户 API 和租户控制器,管理租户和租户的资源。
认证、授权、配额管理等等也都可以在 Kubernetes API 之上构建。

工具

CRI Tools 是社区 Node 组针对 CRI 接口开发的辅助工具,它包括两个工具:crictl 和 critest。

crictl 是一个容器运行时命令行接口,它对系统和应用的排错来说是个很有用的工具。当使用 Docker 运行时,调试系统信息的时候我们可能使用 docker ps 和 docker inspect 等命令检查应用的进程情况。但是对于其他基于 CRI 的容器运行时来说,它们可能没有自己的命令行工具;或者即便有,它们的操作界面也不一定与 Kubernetes 中的概念一致。更不用说,很有很多的命令对 Kubernetes 没什么用,甚至会损害系统(比如 docker rename)。因而,我们推荐使用 crictl 作为 Docker CLI 的继任者,用于 Kubernetes 节点上 pod、容器以及镜像的除错工具。

crictl 提供了类似 Docker CLI 的使用体验, 并且支持所有 CRI 兼容的容器运行时。并且,crictl 提供了一个对 Kubernetes 来说更加友好的容器视角:它就是为 Kubernetes 而设计的,有不同的命令分别与Pod 和容器进行交互。例如 crictl pods 会列出 Pod 信息,而 crictl ps 只会列出应用容器的信息。

而 critest 则是一个容器运行时的验证测试工具,用于验证容器运行时是否符合 Kubelet CRI 的要求。除了验证测试,critest 还提供了 CRI 接口的性能测试,比如 critest -benchmark。

推荐将 critest 集成到容器运行时开发的 Devops 流程中,保证每个变更都不会破坏 CRI 的基本功能。另外,还可以选择将 critest 的测试结果与 Kubernetes Node E2E 的结果提交到 Sig-node 的 TestGrid,向社区和用户展示。

多容器运行时

多容器运行时用于不同的目的,比如使用虚拟化容器引擎式运行不可信应用和多租户应用,而使用 Docker 运行系统组件或者无法虚拟化的容器(比如需要 HostNetwork 的容器)。比如典型的用例为:

  • Kata Containers/gVisor + runc
  • Windows Process isolation + Hyper-V isolation containers

以前,多容器运行时通常以注解(Annotation)的形式支持,比如 cri-o、frakti 等都是这么支持了多容器运行时。但这一点也不优雅,并且也无法实现基于容器运行时来调度容器。因而,Kubernetes 在 v1.12 中将开始增加 RuntimeClass 这个新的 API 对象,用来支持多容器运行时。

GPU 支持

Nvidia提供了k8s pod使用GPU的一整套解决方案。运行时方面,nvidia提供了特定的运行时,主要的功能是为了让container访问从node节点上分配的GPU资源。

如上图所示,libnvidia-container被整合进docker的runc中。通过在runc的prestart hook 中调用nvidia-container-runtime-hook来控制GPU。启动容器时,prestart hook会校验环境变量GPU-enabled来校验该容器是否需要启用GPU,一旦确认需要启用,就调用nvidia定制的运行时来启动容器,从而为容器分配limit指定个数的GPU。

在k8s的调度方面,nvidia基于k8s的device plugin来实现了kubelet层GPU资源的上报,通过在pod的spec中指定对应的limit来声明对GPU个数的申请情况。在spec中必须指定limit的值(且必须为整数),reqire的值要么不设置,要么等于limit的值。

NVIDIA_VISIBLE_DEVICES : controls which GPUs will be accessible inside the container. By default, all GPUs are accessible to the container.
NVIDIA_DRIVER_CAPABILITIES : controls which driver features (e.g. compute, graphics) are exposed to the container.
NVIDIA_REQUIRE_* : a logical expression to define the constraints (e.g. minimum CUDA, driver or compute capability) on the configurations supported by the container.

serverless

无服务器(Serverless)现在是一个很热门的方向,各个云平台也提供很多种类的无服务器计算服务,比如 Azure Container Instance、AWS Farget 等。它们的好处是用户不需要去管理容器底层的基础设施,而只需要管理容器即可,并且容器通常按实际的运行时间收费。这样,对用户来说,不仅省去了管理基础设施的繁琐步骤,还更节省成本。

那么 CRI 在这里有什么应用场景呢?假如你是一个云平台的管理者,想要构建一个无服务器容器服务,那么使用 CRI 配合多容器运行时就是一个很好的思路。这样的话,

  • Kubernetes 可以用来给整个平台提供调度和编排
  • 基于 Kubernetes API 可以搭建租户管理功能
  • 基于 CRI 可以实现多租户容器运行的强隔离
  • 基于 CNI 可以实现多租户的网络强隔离

那么,对云平台的用户呢?这些无服务器容器服务提供的功能通常都比较简单,并不具备编排的功能。但可以借助 Virtual Kubelet 项目,使用 Kubernetes 为这些平台的容器提供编排功能。

Virtual Kubelet 是针对 Serverless 容器平台设计的虚拟 Kubernetes 节点,它模拟了 Kubelet 的功能,并将 Serverless 容器平台抽象为一个虚拟的无限资源的 Node。这样就可以通过 Kubernetes API 来管理其上的容器。

目前 Virtual Kubelet 已经支持了众多的云平台,包括

  • Azure Container Instance
  • AWS Farget
  • Service Fabric
  • Hyper.sh
  • IoT Edge

Written by

说点什么

欢迎讨论

avatar

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  Subscribe  
提醒