Kubernetes 项目目前的重点发展方向,是为开发者和使用者暴露更多的接口和可扩展机制,将更多的用户需求下放到社区来完成。其中,发展最为成熟也最为重要的一个接口就是 CRI。2018 年,由 containerd 社区主导的 shimv2 API 的出现,在 CRI 的基础上,为用户集成本身的容器运行时带来了更加成熟和方便的实践方法。算法
本次演讲分享了关于 Kubernetes 接口化设计、CRI、容器运行时、shimv二、RuntimeClass 等关键技术特性的设计与实现,并以 KataContainers 为例,为听众演示上述技术特性的使用方法。本文整理自张磊在 KubeCon + CloudNativeCon 2018 现场的演讲速记。api
今天,我给你们带来的分享是关于 Kubernetes CRI 和 containerd shimv2 的设计,这也是目前社区里比较重要的一个大方向。你们好,我是张磊,如今在阿里巴巴集团工做。既然今天我们会聊 Kubernetes 这个项目,那么首先咱们来简单看一下 Kubernetes 这个项目的工做原理。安全
Kubernetes 的工做原理工具
其实你们都知道 Kubernetes 这个项目它最上面是一层 Control Panel ,它也被不少人称之为 Master 节点。当你把 workload 就是你的应用提交给 Kubernetes 以后,首先为你作事情的是 API server,它会把你的 Application 存到 etcd 里,以 API 对象的方式存到 etcd 中去。性能
而 Kubernetes 中负责编排的是 Controller manager,一堆 controller 经过控制循环在 run。经过这个控制循环来作编排工做,帮你去建立出这些应用所须要的 Pod,注意不是容器,是 Pod。区块链
而一旦一个 Pod 出现以后,Scheduler 会 watch 新 Pod 的变化。若是他发现有一个新的 Pod 出现,Scheduler 会帮你去把全部调度算法都 run 一遍,把 run 到的结果:就是一个 Node 的名字,写在我这个 Pod 对象 NodeName 字段上面,就是一个所谓的 bind 的操做。而后把 bind 的结果写回到 etcd 里去,这就是所谓的 Scheduler 工做过程。因此 Control Panel 它忙活这么一圈下来,最后获得的结果是什么呢?你的一个 Pod 跟一个 Node 绑定(bind)在了一块儿,就是所谓 Schedule 了。优化
而 Kubelet 呢?它是运行在全部节点上。Kubelet 会 watch 全部 Pod 对象的变化,当它发现一个 Pod 与一个 Node 绑定在一块儿的时,而且它又发现这个被绑定的 Node 是它本身,那么 Kubelet 就会帮你去接管接下来的全部事情。加密
若是你看一下 Kubelet ,看看它在作什么呢?很简单,其实当 Kubelet 拿到这个信息以后,他是去 call 你运行在每一个机器上的 Containerd 进程,去 run 这个 Pod 里的每个容器。spa
这时候,Containerd 帮你去 call runC 因此最后实际上是 runC 帮你去 set up 起来这些 namespace、Cgroup 这些东西,是它去帮你 chroot ,“搭”出来所谓的一个应用和须要的容器。这就是整个 Kubernetes 工做的一个简单原理。插件
Linux Container
因此这个时候你可能会提出一个问题就是什么是容器?其实容器很是简单,咱们日常所说这个容器就是 Linux 容器,你能够把 Linux 容器分为两部分:第一个是 Container Runtime,第二个是 Container Image。
所谓的 Runtime 部分就是你所运行进程的动态视图和资源边界,因此它是由 Namespace 和 Cgroup 为你构建出来的。而对于 Image(镜像),你能够把它理解为是你想要运行的程序的静态视图,因此它实际上是你的程序+数据+全部的依赖+全部的目录文件组成一个压缩包而已。
而这些压缩包被以 union mount 的方式 mount 在一块儿的时候,咱们称之为 rootfs 。rootfs 就是你的整个 process 的静态视图,他们看到这个世界就这样子,因此这是 Linux Container。
KataContainer
可今天咱们还要聊另一种 Container,它与前面 Linux Container 大相径庭。他的 Container Runtime 是用 hypervisor 实现的,是用 hardware virtualization 实现的,像个虚拟机同样。因此每个像这样的 KataContainer 的 Pod,都是一个轻量级虚拟机,它是有完整的 Linux 内核。因此咱们常常说 KataContainer 与 VM 同样能提供强隔离性,但因为它的优化和性能设计,它拥有与容器项媲美的敏捷性。这个一点稍后会强调,而对于镜像部分, KataContainer 与 Docker 这些项目没有任何不一样,它使用的是标准 Linux Continer 容器,支持标准的 OCR Image 因此这一部分是彻底同样的。
容器安全
但是你可能会问为何咱们会有 KataContainer 这种项目? 其实很简单,由于咱们关心安全这个事,好比不少金融的场景、加密的场景,甚至如今区块链不少场景下,都须要一个安全的 Container Runtime,因此这是咱们强调 KataContainer 的一个缘由。
若是你如今正在使用 Docker, 我问一个问题就是你怎样才能安全地使用 Docker?你可能会有不少套路去作。好比说你会 drop 掉一些 Linux capibility,你能够去指定 Runtime 能够作什么,不能作什么。第二个你能够去 read-only mount points 。第三,你可使用 SELinux 或者 AppArmor 这些工具把容器给保护起来。还有一种方式是能够直接拒绝一些 syscalls,能够用到 SECCOMP。
可是我须要强调的是全部这些操做都会在你的 Container 和 Host 之间引入新的 layer,由于它要去作过滤,它要去拦截你的 syscalls,因此这个部分你搭的层越多,你容器性能越差,它必定是有额外的负面性能损耗的。
更重要的是,作这些事情以前你要想清楚到底应该干什么,到底应该 drop 掉哪些 syscalls,这个是须要具体问题具体分析的,那么这时候我应该怎么去跟个人用户去讲如何作这件事情?
因此,这些事情提及来很简单,但实际执行起来不多有人知道到底该怎么去作。因此在 99.99% 的状况下,大多数人都是把容器 run 到虚拟机里去的,尤为在公有云场景下。
而对于 KataContainer 这种项目来讲,它因为使用了与虚拟机同样的 hardware virualization,它是有独立内核的,因此这个时候它提供的 isolation 是彻底可信任的,就与你信任 VM 是同样的。
更重要的是,因为如今每个 Pod 里是有一个 Independent Kernel,跟个小虚拟机同样,因此这时候就容许你容器运行的 Kernel 版本跟 Host machine 适应是彻底不同。这是彻底 OK 的,就与你在在虚拟机中作这件事同样,因此这就是为何我会强调 KataContainers 的一个缘由,由于它提供了安全和多租户的能力。
Kubernetes + 安全容器
因此也就很天然会与有一个需求,就是咱们怎么去把 KataContainer run 在 Kubernetes 里?
那么这个时候咱们仍是先来看 Kubelet 在作什么事情,因此 Kubelet 要想办法像 call Containerd 同样去 call KataContainer,而后由 KataContainer 负责帮忙把 hypervisor 这些东西 set up 起来,帮我把这个小VM 运行起来。因此这个时候就要须要想怎么让 Kubernetes 能合理的操做 KataContainers。
Container Runtime Interface(CRI)
对于这个诉求,就关系到了咱们以前一直在社区推动的 Container Runtime Interface ,咱们叫它 CRI。CRI 的做用其实只有一个:就是它描述了,对于 Kubernetes 来讲,一个 Container 应该有哪些操做,每一个操做有哪些参数,这就是 CRI 的一个设计原理。但须要注意的是,CRI 是一个以容器为核心的 API,它里面没有 Pod 的这个概念。这个要记住。
为何这么说呢?咱们为何要这么设计呢?很简单,咱们不但愿像 Docker 这样的项目,必须得懂什么是 Pod,暴露出 Pod 的 API,这是不合理的诉求。Pod 永远都是一个 Kubernetes 的编排概念,这跟容器没有关系,因此这就是为何咱们要把这个 API 作成 Containerd -centric。
另一个缘由出于 maintain 的考虑,由于若是如今, CRI 里有 Pod 这个概念,那么接下来任何一个 Pod feature 的变动都有可能会引发 CRI 的变更,对于一个接口来讲,这样的维护代价是比较大的。因此若是你细看一下 CRI,你会发现它其实定了一些很是广泛的操做容器接口。
在这里,我能够把 CRI 大体它分为 Container 和 Sandbox。Sandbox 用来描述的是我经过什么样的机制来去实现 Pod ,因此它其实就是 Pod这个概念真正跟容器项目相关的字段。对于 Docker 或 Linux 容器来讲,它其实 match 到最后 run 起来的是一个叫 infra container 的容器,就是一个极小的容器,这个容器用来 hold 整个 Pod 的 Node 和 Namespace。
不过, Kubernetes 若是用 Linux Container Runtim, 好比 Docker 的话,它不会给你提供 Pod level 的 isolation,除了一层 Pod level cgroups 。这是一个不一样点。由于,若是你用 KataContainers 的话,KataContaniners 会在这一步为你建立一个轻量级的虚拟机。
接下来到下一阶段,到 Containers 这个 API 的时候,对于 Docker 来讲它就给你起在宿主机上启动用户容器,但对 Kata 来讲不是这样的,它会在前面的 Pod 对应的轻量级虚拟机里面,也就在前面建立的 Sandbox 里面 set up 这些用户容器所须要 Namespace ,而不会再跟你在一块儿新的容器。因此有了这样一个机制以后,当上面 Contol Panel 完成它的工做以后,它说我把 Pod 调度好了,这时候 Kubelet 这边启动或建立这个 Pod 的时候一路走下去,最后一步才会去 call 咱们这个所谓 CRI。在此以前,在 Kubelet 或者 Kubernetes 这是没有所谓 Containers runtime 这个概念的。
因此走到这一步以后,若是你用 Docker 的话,那么 Kubernetes 里负责响应这个 CRI 请求 是 Dockershim。但若是你用的不是 Docker 的话一概都要去走一个叫 remote 的模式,就是你须要写一个 CRI Shim,去 serve 这个 CRI 请求,这就是咱们今天所讨论下一个主题。
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,他们和 containerd 在本质上没有任何区别,跑到最后都是靠 runC 起容器,为何要这种东西?
咱们不知道,可是我做为 Kata maintainer,我须要给他们两个分别写两部分的 integration 把 Kata 集成进去。这就很麻烦,者就意味着我有 100 种这种 CRI 我就要写 100 个集成,并且他们的功能所有都是重复的。
Containerd ShimV2
因此在今天我给你们 propose 的这个东西叫作 Containerd 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项目的 maintainer 我就能够这么实现。我在 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,因此这个跟前面的时间就出现很大的不一样。
因此你如今去总结一下这个图的话,你发现咱们实现方式是变成这个样子:
首先,你仍是用原来的 CRI Containerd,只不过如今装的是 runC,你如今再装一个 katacontainer 放在那机器上面。接下来咱们 Kata 那边会给你写一个实现叫 kata-Containerd-Shimv2。因此前面要写一大坨 CRI 的东西,如今不用了。如今,咱们只 focus 在怎么去把 Containerd 对接在 kata container 上面,就是所谓的实现 Shimv2 API,这是咱们要作的工做。而具体到咱们这要作的事情上,其实它就是这样一系列与 run 一个容器相关的 API。
好比说我能够去 create、start,这些操做所有映射在我 Shimv2 上面去实现,而不是说我如今考虑怎么去映射,去实现 CRI,这个自由度因为以前太大,形成了咱们如今的一个局面,就有一堆 CRI Shim 能够用。这实际上是一个很差的事情。有不少政治缘由,有不少非技术缘由,这都不是咱们做为技术人员应该关心的事情,你如今只须要想我怎么去跟 Shimv2 对接就行了。
接下来,我为你演示一下经过 CRI + containerd shimv2调用 KataContainers 的一个 Demo(具体内容略)
总结
Kubernetes 如今的核心设计思想,就是经过接口化和插件化,将本来复杂的、对主干代码有侵入性的特性,逐一从核心库中剥离和解耦。而在这个过程当中,CRI 就是 Kubernetes 项目中最先完成了插件化的一个调用接口。而此次分享,主要为你介绍了在CRI基础上的另外一种集成容器运行时的思路,即:CRI + containerd shimv2 的方式。经过这种方式,你就不须要再为本身的容器运行时专门编写一个 CRI 实现(CRI shim),而是能够直接重用 containerd对 CRI 的支持能力,而后经过 containerd shimv2的方式来对接具体的容器运行时(好比 runc)。目前,这种集成方式已经成为了社区对接下层容器运行时的主流思路,像不少相似于 KataContainers,gVisor,Firecracker 等基于独立内核或者虚拟化的容器项目,也都开始经过 shimv2 ,进而借助 containerd项目无缝接入到 Kubernetes 当中。
而众所周知,在阿里内部,Sigma/Kubernetes 系统使用的容器运行时主要是 PouchContainer。事实上,PouchContainer 自己选择使用 containerd 做为其主要的容器运行时管理引擎,并自我实现了加强版的 CRI 接口,使其知足阿里巴巴强隔离、生产级别的容器需求。因此在 shimv2 API 在 containerd 社区发布以后,PouchContainer 项目就已经率先开始探索和尝试经过 containerd shimv2 来对接下层的容器运行时,进而更高效的完成对其余种类的容器运行时尤为是虚拟化容器的集成工做。咱们知道,自从开源以来,PouchContainer 团队一直都在积极地推进 containerd 上游社区的发展和演进工做,而在此次 CRI + containerd shimv2 的变革里, PouchContainer 再一次走到了各个容器项目的最前面。