CRI和多容器运行时

在CRI出现前,Kubelet通过内嵌的dockershim操作Docker API来操作容器,进而达到一个面向终态的效果。
CRI(Container Runtime Interface):对容器运行时的操作抽象出GRPC接口,将kubelet代码与具体的容器运行时的实现代码解耦开。
容器运行时只要实现了CRI,就能接入到Kubernetes的体系中。社区不必再为各种运行时做适配工作,也不用担心运行时和Kubernetes迭代周期不一致所带来的版本维护问题。
 
在引入CRI后,kubelet的架构如图所示:
Generic Runtime Manager:通用的运行时管理器。
dockershim还是存在于Kubelet的代码中,将于1.23后删除。
remote指的就是CRI接口,主要包含两个部分:
  • CRI Server,即通用(比如说创建、删除容器)的接口;
  • Streaming Server,即流式数据(比如exec、port-forward)的接口。
CNI(容器网络接口)也是在CRI进行操作的。因为创建Pod的时候需要同时创建网络资源然后注入到Pod中。
CRI实际描述了kubelet期望的容器运行时行为:
 
通过kubectl命令来运行一个Pod,kubelet会通过CRI执行以下操作:
(1)调用RunPodSandbox接口来创建一个pod容器,以持有相关资源(比如说网络空间、PID空间、进程空间等);
(2)调用CreatContainer接口在Pod容器的空间创建业务容器;
(3)调用StartContainer接口启动容器,相对应的销毁容器的接口为StopContainer与RemoveContainer。
 
通过kubectl命令来进入一个Pod,kubelet会通过CRI执行以下操作:
(1)exec操作会发送到apiserver,经过鉴权,apiserver将对Kubelet Server发起exec请求
(2)kubelet调用CRI的exec接口将具体的请求发至容器的运行时。
(3)容器运行时不是直接地在exec接口上来服务这次请求,而是通过streaming server异步地返回每一次执行的结果
也就是说,apiserver实际是跟streaming server交互来获取流式数据的。这样一来,CRI Server接口更轻量、更可靠。
 
目前CRI 的一些实现:
  • 红帽公司实现的cri-containerd
这套CRI接口是基于containerd实现的,以插件的形式实现到containerd中,在Meta services、Runtime service、Storage service等containerd提供的容器相关接口之上,包装了一个gRPC的服务
创建容器时就要创建具体的runtime和它的shim。它们和Container一起组成了一个Pod Sandbox。
优点:containerd还额外实现了更丰富的容器接口。可以用containerd提供的ctr工具来调用这些丰富的容器运行时接口,而不只是CRI接口。
  • cri-o
通过直接在OCI上包装容器接口来实现CRI服务。它对外提供的只有具体的 CRI 接口
主要包含两个部分,首先是对容器runtime的管理,另一个是对镜像的管理。
  • alibaba实现的CRI PouchContainer
该富容器技术与传统容器最明显的差异点:传统的容器将容器镜像中指定的CMD作为容器内pid=1的进程;而PouchContainer的富容器模式可以从三种init进程中选择:systemd、sbin/init、dumb-init
 
CRI 相关的工具:
  • crictl
一个类似docker的命令行工具,用来直接操作CRI接口调试容器问题。
  • critest
用于验证CRI接口行为是否是符合预期。
  • 测试接口性能的性能工具
 
containerd中的cri-plugin就实现了CRI,Kata、gVisor这样的容器运行时只需要对接containerd就可以了
为了解决多容器运行时的问题,Kubernetes内置了全局资源RuntimeClass,主要用来解决多个容器运行时混用的问题;
首先创建一个名字叫runv的RuntimeClass对象,Pod通过spec.runtimeClassName引用这个RuntimeClass。
  • handler
表示一个接收创建容器请求的程序,同时也对应一个容器运行时。
节点内会有一个或多个handler,每个handler对应一种容器运行时。根据.scheduling.nodeSelector,Pod最终会调度到某个节点,并最终由某个handler来创建 Pod
比如示例中的Pod 最终会调度到支持runv运行时的节点,被runv容器运行时创建容器。
  • scheduling
v1.16中被引入的,该 Scheduling 配置会被自动注入到 Pod 的 nodeSelector 中。
决定Pod最终会被调度到哪些Node上(需要用户提前为Node设置好label标识当前节点支持的容器运行时)。
Scheduling中包含了两个字段:
        NodeSelector代表的是支持该 RuntimeClass 的节点上应该有的 label 列表。一个 Pod 引用了该 RuntimeClass 后,RuntimeClass admission 会把该 label列表与Pod中的label 列表做一次合并。如果这两个label中有冲突(指key 相同,但value 不相同)的,会被 admission 拒绝。
        Tolerations表示RuntimeClass的容忍列表。一个 Pod引用该 RuntimeClass 之后,admission 也会把 toleration 列表与 Pod 中的 toleration 列表做一个合并。如果这两处的 Toleration 有相同的容忍配置,就会将其合并成一个。
  • Overhead
v1.16中才引入的一个新的字段,它表示Pod中的业务运行所需资源以外的额外开销。
引入原因:Docker Pod除了普通容器之外,还有一个pause容器,但我们在计算容器开销时会忽略它。对于其它运行时的Pod,有时这些开销都是没法忽略的。
Overhead只有一个字段PodFixed,代表了各种资源的占用量。每一对key是一个ResourceName,value 是一个 Quantity(代表该资源的使用量)。
引入Overhead后,只有namespace的资源剩余量大于Overhead+request时Pod才能被创建,只有节点的资源可用量大于等于Overhead+request时才能被调度上来。
Overhead会被统计到节点的已使用资源中,从而增加已使用资源的占比,最终会影响到Pod的驱逐。
Overhead还有一些使用限制和注意事项:
  1. Pod Overhead最终会永久注入到 Pod 内并且不可手动更改。即便是将RuntimeClass删除或者更新,Pod Overhead依然存在并且有效;
  2. Pod Overhead只能由RuntimeClass admission自动注入(至少目前是这样的),不可手动添加或更改;
  3. HPA和VPA是基于容器级别指标数据做聚合,Pod Overhead不会对它们造成影响。
 
多容器运行时示例:
创建pod的请求先到达kube-apiserver,kube-apiserver转发给 kubelet,最终kubelet将请求发至 cri-plugin。在containerd中可以配置多个容器运行时,cri-plugin在containerd的配置文件中查询运行时对应的Handler。例如runC是通过Shim API runtime v1请求containerd-shim,然后由它创建对应的容器。containerd 默认放在etc/containerd/config.toml]() 这个位置下。比较核心的配置是在 plugins.cri.containerd 目录下。    plugins.cri.containerd.runtimes.<runClassname>:runtimes的配置,都有相同的前缀,后面的<runClassname>要和前面RuntimeClass对象中Handler的名字对应    plugins.cri.containerd.runtimes.default_runtime:如果一个Pod没有指定RuntimeClass,但是被调度到当前节点的话,就默认使用该容器运行时。
 
常见容器运行时介绍
1、kata
Kata Containers是2017年KubeCon上对外发布的安全容器项目,这个项目有两个前身:runV以及Intel的Clear Container项目。
 
Kata Containers的思路是用虚拟机来做Kubernetes的PodSandbox。在 Kata里面被拿来做VM的先后有QEMU、Firecracker、ACRN、Cloud Hypervisor等。
 
下图是Kata Containers通过containerd(也可以通过CRI-O)和Kubernetes集成的原理:
CRI client(Kubelet)通过CRI接口找到containerd/CRI-O,由containerd/CRI-O来执行镜像相关操作。根据请求,它会把runtime部分的需求变成一个OCI spec,并交给OCI runtime执行。如图中上半部分的Kata-runtime,或者说下半部分精简过后的containerd-shim-kata-v2。
具体的过程如下:
(1)当containerd拿到请求时,它会首先创建一个shim-v2。这个shim-v2就是一个PodSandbox的代表,也是VM的代表;
(2)每一个Pod都会有一个shim-v2来为containerd/CRI-O执行各种操作。shim-v2会为这个Pod启动一个虚拟机,在里面运行着一个Linux Kernel(图中的Guest Kernel)。
如果这个里面用的是QEMU的话,会通过一些配置和补丁,让它变得小一些。同时这个里面也没有额外的Guest操作系统。
(3)容器的spec以及容器本身打包的存储,包括rootfs和文件系统,交给PodSandbox。PodSandbox会在虚机中由Kata-agent 把容器启动起来。
依照CRI语义和OCI规范,在Pod里可以启动多个相关联的容器。它们会被放到同一个虚拟机里面,并且可以根据需求共享某些 namespace。
此外,其它的一些外置的存储和卷也可以通过热插拔的方式来插到这个PodSandbox里。
(5)对于网络来说,目前使用tcfilter就可以无缝地接入几乎所有的Kubernetes的CNI插件。
而且还提供了一个enlightened 的模式,会有特制的CNI插件来提高容器的网络能力。
 
可以看到,在PodSandbox里,实际上只有一个Guest Kernel跑着一些容器本身的打包和容器应用,并不包含一个完整的操作系统。就是说,PodSandbox并不像是传统的虚拟机,并且通过少用不必要的内存、共享能共享的内存来进一步地降低内存的开销。
与传统的虚拟机比起来,PodSandbox开销更小、启动更轻快,对于大部分的场景来说,它可以做到“secure as VM”、“fast as container”。
同时,相比传统的虚机,它有更多的弹性。比如动态资源的插拔以及使用virtio-fs等技术,可以把host的基本文件系统的内容(比如说容器的rootfs)共享给虚拟机。
通过非易失存储、非易失内存来做的DAX的技术,能够在不同的PodSandbox间(也就是不同的Pod、不同的容器间)共享一些只读的内存部分。这样可以在不同的PodSandbox间节省很多的内存。
同时所有的Pod的管理都是通过Kubernetes从外部进行容器管理,并且从外部来获取metrics和Debug信息。
虽然从底层来看,它还是一个虚拟机,但是实际上它是一个面向云原生的虚拟化。
 
2、gVisor
gVisor是2018年KubeCon上google对外发布的安全容器项目,它是进程级的虚拟化。
相比Kata Containers通过对现有的隔离技术进行组合和改造来构建容器间的隔离层的话,gVisor的设计更加简洁,它是一个用Go语言重写的运行在用户台的操作系统内核sentry。它并不依赖于虚拟化和虚拟机技术,而是借助一个叫Platform的能力,让宿主机的操作系统把应用所有的期望对操作系统的操作都转交给sentry来进行。sentry做处理之后会把其中的一部分交给操作系统来帮它完成,大部分则由自己来完成。
 
gVisor是一个纯粹的面向应用的隔离层,它的安全性依据在于:
(1)攻击面变小,宿主机的操作系统将只为沙箱里的应用提供大约 20%的系统调用,不常用的Syscall的访问根本就到不了操作系统层面
(2)一些经常被攻击的系统调用被隔离到独立的进程里。一个独立的进程更融洽被限制和保护。
比如打开文件的open(),gVisor单独地把它放到了一个独立的进程Gofer里面去实现。Gofer 可以做更少的事情,可以用非 root 去执行。
(3)sentry和Gofer都是用Go语言来实现的,不是用传统的C语言实现的。
Go 语言本身是一个内存更安全的一个实现,因此整个gVisor更不容易被攻击,更不容易发生一些内存上的问题。
 
问题:
(1)重新实现内核很难,只有Google等公司可以实现
(2)sentry并不是Linux,在兼容性方面与kata比有一定差距。
(3)对于当前的系统调用的实现方式、CPU的指令系统,从应用去拦截 Syscall再送给sentry执行,有相当大的开销。
在一定场景之下,gVisor可以有更好的性能的。但是,在大部分的场景之下,gVisor的性能仍然是比不上Kata这样的解决方案。所以短时间之内,gVisor并不能成为一个终极的解决方案,不过它可以适应一些特定的场景。
 
kata和gVisor被称作安全容器。名字虽叫安全,但是它提供的其实是隔离性。
安全容器通过隔离层让应用的问题——不管是来自于外部的恶意攻击还是说意外的错误,都不至于影响主机,也不会在不同的Pod之间相互影响。所以,它的作用是不止于安全,它对于系统的调度、服务质量,还有应用信息的保护都有好处。
(1)传统的操作系统容器技术是内核进程管理的一个延伸,容器进程本身是一组相关联的进程,对于宿主机的调度系统来说,它是完全可见的。一个Pod 里的所有容器或进程,同时也都被宿主机调度和管理。
如果有一个大量容器的环境,宿主机本身内核的负担就会很重。在采纳安全容器后,在宿主机上就看不到这些容器内的进程,而是由隔离层承担对容器内进程的调度,主机只需要调度沙箱本身。
(2)提高调度效率的同时,它会把所有的应用彼此隔离起来,这样就避免了容器之间、容器和主机之间的干扰,提高了服务质量。
(3)容器需要使用资源,主机上不应能看到容器里的数据。安全容器可以把用户运行的东西完全封装在容器里,主机的运维管理操作并不能访问到应用的数据,从而把应用的数据保护在沙箱里。
 
可以看到,安全容器不仅仅是在做安全隔离,安全容器隔离层的内核相对于宿主机的内核是独立的,专门对应用服务,从这个角度来说,主机和应用的功能之间实际上是一个合理的功能分配与优化。它可以展现出很多的潜力,未来的安全容器,可能不仅仅是隔离性能开销的降低,同时也是在提高应用的性能。隔离技术会让云原生基础设施更加完美。
 
3、nvidia-container-runtime
nvidia-docker2默认使用的容器运行时
nvidia-container-runtime修改了runC,为所有容器添加了pre-start hook,会自动化做设备和驱动库的挂载、加载一些GPU相关的库。
posted @ 2021-03-29 14:47  扬羽流风  阅读(606)  评论(0编辑  收藏  举报