为Kubernetes选择合适的容器运行时


【编者的话】作为后台支撑,Kubernetes优势明显,具有自动化部署、服务伸缩、故障自我修复、负载均衡等特性。咪付的蓝牙过闸系统和全态识别AI系统的后台支撑采用了Kubernetes,经过线上的长期运行,其状态良好运行平稳。

蓝牙过闸系统和全态识别AI系统有着不同的数据特性,对数据的安全要求及运行效率也各不一样,因此如何选择容器的运行时成为了一个重点考虑的因素。

CRI的由来

CRI(Container Runtime Interface)是Kubernetes提出的容器运行时接口规范。
1.jpg

在Kubernetes体系中,是由Kubelet组件负责与容器运行时交互的。Kubelet调用容器运行时的流程如上图所示。CRI shim是实现CRI接口的gRPC server服务,负责连接Kubelet和Container runtime,Container runtime是容器运行时工具,它为用户进程隔离出一个独立的运行环境;具体的流程是Kubelet调用CRI shim接口,CRI shim响应请求,然后调用底层的Container runtime工具运行容器。Kubelet、CRI shim和Container runtime都部署在一个Kubernetes worker节点上,前两者是以独立的守护进程的方式启动的,而Container runtime不是守护进程,它通常是一个命令行工具。如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Kubernetes在v1.5版本之前是没有CRI接口的,那时Kubelet源码内部只集成了两个容器运行时(Docker和rkt)的相关代码。这两种容器运行时并不能满足所有用户的使用需求,在某些业务场景,用户对容器的安全隔离性有着更高的需求,用户希望Kubernetes能支持更多种类的容器运行时。因此,Kubernetes在1.5版本推出了CRI接口,各个容器运行时只要实现了CRI接口规范,就可以接入到Kubernetes平台为用户提供容器服务。

CRI接口带来的好处,首先它很好的将Kubernetes和容器运行时解耦,容器运行时的每次更新迭代,都不必对Kubelet工程源码进行编译发布;其次解放了容器运行时更新迭代的步伐,也能保证Kubernetes的代码质量和平台的稳定。

OCI是什么

OCI规范(Open Container Initiative 开放容器标准),该规范包含两部分内容:容器运行时标准(runtime spec)、容器镜像标准(image spec)。其具体内容的定义如下图:
2.jpg

容器运行时标准

runtime spec包含配置文件、运行环境、生命周期三部分内容。

配置文件
config.json,包含容器的配置信息(mounts、process、hostname、hooks等)。

运行环境

定义运行环境,是为了确保应用程序在多个运行时之间,能具有一致的环境 。

容器生命周期

定义了运行时的相关指令及其行为:state、create、start、kill、delete、prestart hooks、poststart hooks、poststop hooks。

容器镜像标准

通常我们根据Dockerfile定义的内容制作镜像,目的是构建一个符合OCI标准的镜像文件,那么OCI标准镜像文件的内容都有哪些呢?在OCI规范里的image spec对容器镜像格式做了定义,它主要包括以下几块内容:

文件系统

描述了如何以layer的方式叠加成一个完整的文件系统,以及如何用layer去表示对文件作出的改动(增加、删除、修改)。

config文件

保存了文件系统的层级信息(每个层级的 hash 值、历史信息),以及容器运行时需要的一些信息(比如环境变量、工作目录、命令参数、mount 列表等)。

manifest文件

记录镜像元信息,包括Image Config和Image Layers。

index文件

可选的文件,指向不同平台的manifest文件,相当于整个镜像的入口,从这个文件可以获取整个镜像依赖的所有文件信息。

OCI项目

下表是兼容OCI规范的容器运行时项目:
3.jpg

容器运行时对比

按照底层容器运行环境依托的技术分类,我们将容器运行时分为以下三类:
  • OSContainerRuntime(基于进程隔离技术)
  • HyperRuntime(基于Hypervisor技术)
  • UnikernelRuntime(基于unikernel)


通过下图对比它们之间的区别:
4.jpg

OSContainerRuntime下的Linux Container共享Linux内核,使用namespace、cgroup等技术隔离进程资源。namespace只包含了六项隔离(UTS、IPC、PID、Network、Mount、User),并非所有Linux资源都可以通过这些机制控制,比如时间和Keyring,另外,容器内的应用程序和常规应用程序使用相同的方式访问系统资源,直接对主机内核进行系统调用。因此即使有了很多限制,内核仍然向恶意程序暴露过多的攻击面。

HyperRuntime下的VM Container容器各自拥有独立Linux内核,资源隔离比Linux Container更彻底。但并不是说使用VM容器用户就可以高枕无忧,只是VM容器的攻击面比Linux容器小了很多,黑客要逃逸到宿主机就只剩下Hypervisor这个入口,所以说没有绝对的安全,相对来说VM容器更安全。

另一方面,VM容器的性能比不上Linux容器,因为Hypervisor这一层带来的性能损耗,在Linux容器这边是不存在的。

UnikernelRuntime下的容器同VM Container一样有着安全级别很高的运行环境,同样是使用Hypervisor技术进行容器隔离。

简单来说Unikernel是一个运行在Hypervisor之上的libOS系统,而libOS是由应用程序和libraries一起构建出的操作系统。

unikernel的特点如下:

性能好,应用程序和内核在同一地址空间,消除了用户态和内核态转换以及数据复制带来的开销。

更精简的内核,去掉了多余的驱动、依赖包、服务等,最终打包镜像更小,启动速度更快。

完全不可调试,在生产环境中如果遇到问题,只能依赖于收集到的日志进行排查,要不就是重启容器,原先熟悉的Linux排查方法和工具完全派不上用场。

Kubelet CRI架构

Kubernetes在引入CRI之后,Kubelet的架构如下图所示:
5.jpg

每一个容器引擎只需要自己实现一个CRI shim,对CRI请求进行处理,就可以接入Kubelet当中去。

我们所说的容器运行时,准确来说包含两部分,一部分是上层容器运行时CRI shim(即容器运行时管理程序,如Containerd、CRI-O),另一部分是下层容器运行时Container runtime(即容器运行时命令工具,如runc)。

CRI接口定义

CRI接口分为两部分,一个是容器运行时服务RuntimeService,负责管理pod和容器的生命周期;一个是镜像服务ImageService,负责管理镜像的生命周期。
6.jpg

当前CRI格局

目前实现了CRI的主流项目有:Docker、containerd、CRI-O、Frakti、Pouch,它们衔接Kubelet与运行时方式对比如下:
7.jpg

PS:由于rkt容器引擎目前未能完全兼容OCI规范,所以图中未将其包含进来。

Docker

Docker容器引擎安装后包含有这些组件:dockerd、Containerd、runC。

dockershim是内置在Kubelet的CRI gRPC server服务,它接收CRI请求后,通过unix本地套接字调用dockerd的API接口,再由dockerd请求下游的运行时组件Containerd和runc。调用的链路:dockershim => dockerd => Containerd => runc。

在当前CRI的请求链路上,dockerd只是简单接收CRI请求,在转换之后调用Containerd,dockerd还集成有其他功能,如network、swarm、volume等在Kubernetes平台下没有用武之地,可以说在CRI场景下docker显得笨重。

Containerd

Containerd项目是从早期的Docker源码中提炼出来的,它使用CRI插件来向kubelet提供CRI接口服务。

CRI插件是一个独立的项目,在Containerd编译时,如果go build命令没有显示设置参数-tags=no_cri,那么CRI插件将自动编译集成到Containerd的二进制文件中,然后在配置文件/etc/containerd/config.toml中声明启用CRI插件,就可以在Containerd中启动CRI shim服务了。

Containerd能支持多运行时,目前它内置了runc运行时,其他运行时如果要接入Containerd,则需要实现Containerd shim v2 gRPC接口,这样Containerd就可以通过shim v2调用其他运行时。他们的调用关系如下:Containerd --> shim v2 --> runtimes

CRI-O

CRI-O完整实现CRI接口功能,并且严格兼容OCI标准,CRI-O比Containerd更专注,它只服务于Kubernetes(而Containerd除支持Kubernetes CRI,还可用于Docker Swarm),从官网上我们可以了解到CRI-O项目的功能边界:
  • 支持多种image格式
  • 支持多种image下载方式
  • 容器镜像管理
  • 容器生命周期管理
  • 提供CRI要求的监控、日志功能
  • 提供CRI要求的资源隔离功能


CRI-O通过命令行调用默认运行时runC,所以runC二进制文件必须部署在目录/usr/bin/runc。CRI-O和Containerd调用runtime的方式不同,前者是通过Linux命令调用,后者是通过gRPC服务调用,所以只要符合OCI规范的runtime,都能直接接入CRI-O提供运行时服务,而除runC外的其他运行时要接入Containerd,只能走shim v2接口,因此我们看到像kata-runtime这样的运行时项目就是通过shim v2接口来适配Containerd的。

Frakti

Frakti是基于Hypervisor虚拟机管理程序的容器运行时,它相比其他的容器运行时具有如下这些功能特性:
8.jpg

PouchContainer

PouchContainer是阿里开源的容器引擎,它内部有一个CRI协议层和cri-manager模块,用于实现CRI shim功能。它的技术优势包括:
  1. 强隔离,包括的安全特性:基于Hypervisor的容器技术、lxcfs、目录磁盘配额、补丁Linux内核等。
  2. 基于P2P镜像分发,利用P2P技术在各节点间互传镜像,减小镜像仓库的下载压力,加快镜像下载速度。
  3. 富容器技术,PouchContainer的容器中除了运行业务应用本身之外,还有运维套件、系统服务、systemd进程管家等。


CRI命令工具

cri-tools是由kubernetes-sigs 开发维护的CRI命令工具(后简称crictl),它是为了在Kubernetes node节点上管理镜像、管理pod/container、与容器进行交互而设计的,crictl调用CRI接口获取相关信息、或者通过CRI的exec接口与容器交互。

crictl并不是docker cli的完全继任者,它仅提供了在Kubernetes node节点上常用的一些运维功能,具备一个较小的功能子集,而docker cli的命令更强大,事实上很多docker命令在生产环境并没有必要使用,所以crictl相对来说更安全,它避免运维人员在生产节点非法使用docker cli(rename、rm、rmi等子命令)造成一些人为的故障。所有实现了CRI接口的容器运行时,都可以通过crictl工具对其进行操作。

Kubernetes支持多运行时

为什么要支持多运行时呢?举个例子,有一个开放的云平台向外部用户提供容器服务,平台上运行有两种容器,一种是云平台管理用的容器(可信的),一种是用户部署的业务容器(不可信)。在这种场景下,我们希望使用runc运行可信容器(弱隔离但性能好),用runv运行不可信容器(强隔离安全性好)。面对这种需求,Kubernetes也给出了解决方案(使用API对象RuntimeClass支持多运行时)。

多运行时工作原理

9.jpg

Kubelet从apiserver接收到的Pod Spec,如果Pod Spec中使用runtimeClassName指定了容器运行时,则在调用CRI接口的RunPodSandbox()函数时,会将runtimeClassName信息传递给CRI shim,然后CRI shim根据runtimeClassName去调用对应的容器运行时,为Pod创建一个隔离的运行环境。

RuntimeClass配置

Kubernetes在v1.12中增加了RuntimeClass这个新API对象来支持多运行时(目的就是在一个woker节点上运行多种运行时)。

在Kubernetes中启用RuntimeClass时需要注意,尽量保持当前Kubernetes集群中的节点在容器运行时方面的配置都是同构的,如果是异构的,那么需要借助node Affinity等功能来调度Pod到已部署有匹配容器运行时的节点。

接下来只需要三步配置就可以为pod指定运行时:
  1. 在Kubernetes worker节点配置CRI shim;
  2. 创建RuntimeClass资源对象;
  3. 在pod中指定RuntimeClass。


配置CRI shim

例如CRI-O运行时的配置,需要在文件/etc/crio/crio.conf定义runtime的handler_name:
[crio.runtime.runtimes.${HANDLER_NAME}]
runtime_path = "${PATH_TO_BINARY}"


创建RuntimeClass

RuntimeClass yaml定义如下:
apiVersion: node.k8s.io/v1beta1  # RuntimeClass is defined in the node.k8s.io API group
kind: RuntimeClass
metadata:
name: myclass  # The name the RuntimeClass will be referenced by
# RuntimeClass is a non-namespaced resource
handler: myconfiguration  # The name of the corresponding CRI configuration

配置Pod
在pod yaml中指定RuntimeClassName:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
runtimeClassName: myclass
# ...

如何选择合适的容器运行时

在生产环境中,我们并不需要Docker的镜像打包、容器网络、文件挂载、swarm等这些能力,只需要部署Containerd + runC就可以在Node节点上运行Pod。因此在生产环境中我们可以不安装Docker,而是安装CRI shim组件和运行时工具来运行pod。在多个CRI shim和OCI工具之间,我们该如何选择呢?

首先对比Containerd和CRI-O调用runC的方式,runC代码内置在Containerd内部,通过函数调用;CRI-O是通过Linux命令方式调用runC二进制文件,显然前者属于进程内的函数调用,在性能上Containerd更具优势。其次对比runC和runV,这是两种完全不同的容器技术,runC创建的容器进程直接运行在宿主机内核上,而runV是运行在由Hypervisor虚拟出来的虚拟机上,后者占用的资源更多、启动速度慢,而且runV容器在调用底层硬件时(如CPU),中间多了一层虚拟硬件层,计算效率上不如runC容器。
10.jpg

因此建议结合自身业务特点、以及使用场景选择合适的容器运行时。在对用户的隔离没有很高诉求的情况下,可以优先考虑使用性能更好更轻量的Containerd + runc;在隔离性要求较高的业务场景下,推荐使用基于Hypervisor 的虚拟化容器运行时Frakti + runv。

原文链接:https://mp.weixin.qq.com/s/YooQQh7bXbM58kvSOMoQiw

0 个评论

要回复文章请先登录注册