【QCon】精华内容上线85%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

CRI Shimv2:一种 Kubernetes 集成容器运行时的新思路

  • 2018-12-11
  • 本文字数:5911 字

    阅读完需:约 19 分钟

CRI Shimv2:一种 Kubernetes 集成容器运行时的新思路

Kubernetes 项目目前的重点发展方向,是为开发者和使用者暴露更多的接口和可扩展机制,将更多的用户需求下放到社区来完成。其中,发展最为成熟也最为重要的一个接口就是 CRI。在过去的一年里,已经有大量的容器项目比如 Docker,containerd,runC 等通过 CRI 成为了 Kubernetes 的标准容器运行时。


而在 2018 年,由 containerd 社区主导的 shimv2 API 的出现,在 CRI 的基础上,为用户集成自己的容器运行时带来了更加成熟和方便的实践方法。本文整理自张磊在 KubeCon + CloudNativeCon 2018 现场的演讲,将为你分享关于 Kubernetes 接口化设计、CRI、容器运行时、shimv2、RuntimeClass 等关键技术特性的设计与实现,并以 KataContainers 为例,为你演示上述技术特性的使用方法。

Kubernetes 的工作原理

其实大家都知道 Kubernetes 最上面是一层 Control Panel ,它也被很多人称之为 Master 节点。当把 workload, 也就是应用提交给 Kubernetes 之后,首先为你做事情的是 API server,它会把你的 应用存到 etcd 里,以 API 对象的方式存到 etcd 中去。


而 Kubernetes 中负责编排的是 Controller manager,一堆 controller 通过控制循环在运行。通过这个控制循环来做编排工作,帮你去创建出这些应用所需要的 Pod,注意不是容器,是 Pod。



而一旦一个 Pod 出现之后,Scheduler 会 watch 新 Pod 的变化。如果他发现有一个新的 Pod 出现,Schduler 会帮你去把所有调度算法都 run 一遍,把 run 到的结果:就是一个 Node 的名字,写在 Pod 对象 NodeName 字段上面,就是一个所谓的 bind 操作,然后把 bind 的结果写回到 etcd 里去,这就是所谓的 Schduler 工作过程。所以 Control Panel 忙活这么一圈下来,最后得到的结果是什么呢?你的一个 Pod 跟一个 Node 绑定(bind)在了一起,就是所谓 Schdule 了。


而 Kubelet 呢?它是运行在所有节点上。Kubelet 会 watch 所有 Pod 对象的变化,当它发现一个 Pod 与一个 Node 绑定在一起的时,并且它又发现这个被绑定的 Node 是它自己,那么 Kubelet 就会帮你去接管接下来的所有事情。


如果你看一下 Kubelet ,看看它在做什么呢?很简单,其实当 Kubelet 拿到这个信息之后,他是去 call 你运行在每个机器上的 Containerd 进程,去 run 这个 Pod 里的每一个容器。


这时候,Containerd 帮你去 call runC 所以最后其实是 runC 帮你去创建起来这些 namespace、Cgroup,是它去帮你 chroot ,“搭”出来所谓的应用和需要的容器。这就是整个 Kubernetes 工作的简单原理。


Linux Container

所以这个时候你可能会提出一个问题:什么是容器?其实容器非常简单,我们平常所说这个容器就是 Linux 容器,你可以把 Linux 容器分为两部分:第一个是 容器 Runtime,第二个是容器镜像。


所谓的 Runtime 部分就是你所运行进程的动态视图和资源边界,所以它是由 Namespace 和 Cgroup 为你构建出来的。而对于 Image(镜像),你可以把它理解为是你想要运行的程序的静态视图,所以它其实是你的程序+数据+所有的依赖+所有的目录文件组成一个压缩包而已。


而这些压缩包被以 union mount 的方式 mount 在一起的时候,我们称之为 rootfs 。rootfs 就是整个 process 的静态视图,他们看到这个世界就这样子,所以这是 Linux Container。


KataContainer

可今天我们还要聊另外一种容器,它与前面 Linux Container 截然不同。他的 容器 Runtime 是用 hypervisor 实现的,是用 hardware virtualization 实现的,像个虚拟机一样。所以每一个像这样的 KataContainer 的 Pod,都是一个轻量级虚拟机,它是有完整的 Linux 内核。


所以我们经常说 KataContainer 与 VM 一样能提供强隔离性,但由于它的优化和性能设计,它拥有与容器项媲美的敏捷性。这个一点稍后会强调,而对于镜像部分, KataContainer 与 Docker 这些项目没有任何不同,它使用的是标准 Linux Continer 容器,支持标准的 OCR Image 所以这一部分是完全一样的。


容器安全

可是你可能会问,为什么我们会有 KataContainer 这种项目? 其实很简单,因为我们关心安全这个事,比如很多金融的场景、加密的场景,甚至现在区块链很多场景下,都需要一个安全的容器 Runtime,所以这是我们强调 KataContainer 的一个原因。


如果你现在正在使用 Docker, 我问一个问题:你怎样才能安全地使用 Docker?你可能会有很多套路去做。比如说你会 drop 一些 Linux capibility,你可以去指定 Runtime 可以做什么,不能做什么。第二个你可以去 read-only mount points 。第三,你可以使用 SELinux 或者 AppArmor 这些工具把容器给保护起来。还有一种方式是可以直接拒绝一些 syscalls,可以用到 SECCOMP。



但是我需要强调的是所有这些操作都会在你的 容器和 Host 之间引入新的层,因为它要去做过滤,拦截 syscalls,所以这个部分搭的层越多,容器性能越差,它一定是有额外的负面性能损耗的。


更重要的是,做这些事情之前你要想清楚到底应该干什么,到底应该 drop 哪些 syscalls,这是需要具体问题具体分析的,那么这时候我应该怎么去跟我的用户讲如何做这件事情?


所以,这些事情说起来很简单,但实际执行起来很少有人知道到底该怎么去做。所以在 99.99% 的情况下,大多数人都是把容器运行到虚拟机里去的,尤其在公有云场景下。


而对于 KataContainer 这种项目来说,它由于使用了与虚拟机一样的 hardware virualization,是有独立内核的,所以这个时候它提供的 isolation 是完全可信任的,就与你信任 VM 是一样的。


更重要的是,由于现在每一个 Pod 里是有一个 Independent Kernel,跟个小虚拟机一样,所以这时候就允许容器运行的 Kernel 版本跟 Host machine 适应是完全不一样。这是完全 OK 的,就与你在在虚拟机中做这件事一样,所以这就是为什么我会强调 KataContainers 的一个原因,因为它提供了安全和多租户的能力。

Kubernetes+KataContainers

所以也就很自然会有一个需求:我们怎么把 KataContainer 运行在 Kubernetes 里? 这个时候我们还是先来看 Kubelet 在做什么事情。Kubelet 要想办法像 call Containerd 一样去 call KataContainer,然后由 KataContainer 负责帮忙把 hypervisor 这些东西搭建起来,把小 VM 运行起来。这个时候就要需要想怎么让 Kubernetes 能合理的操作 KataContainers。


Container Runtime Interface

对于这个诉求,就关系到了我们之前一直在社区推进的 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 大致它分为容器和 Sandbox。Sandbox 用来描述通过什么样的机制实现 Pod ,所以它是 Pod 这个概念真正跟容器项目相关的字段。对于 Docker 或 Linux 容器来说,它其实匹配到最后运行起来的是一个叫 infra container 的容器,就是一个极小的容器,这个容器用来 hold 整个 Pod 的 Node 和 Namespace。


不过, Kubernetes 如果用 Linux Container Runtim, 比如 Docker 的话,不会提供 Pod 级别的 isolation,除了一层 Pod level cgroups 。这是一个不同点。因为,如果你用 KataContainers 的话,KataContaniners 会在这一步创建一个轻量级的虚拟机。


接下来到容器的 API,对于 Docker 来说它能在宿主机上启动用户容器,但对 Kata 来说不是这样的,它会在前面的 Pod 对应的轻量级虚拟机里面,也就在前面创建的 Sandbox 里面创建这些用户容器所需要 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,这是不一样的。


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


而我们能够想到一个方式,我能不能重用现在的这些 CRI shim。重用在哪些?比如说 CRI containerd 这个项目它就是一个 containerd 的 CRI shim,它可以去响应 CRI 的请求过来,所以接下来我能不能把这些情况翻译成对 Kata 这些操作,所以这个是可以的,这也是我们将用一种方式,就是把 KataContainers 接到我的 Containerd 后面。


这时候它的工作原理大概这样这个样子,Containerd 它有一个独特设计,会为每一个容器起个 Contained shim。运行一下之后会看宿主机里面运行了一片 Containerd shim,一个一个对上去。



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


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


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


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


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

Containerd ShimV2

所以我给大家提出的这个东西叫做 Containerd ShimV2。前面我们说过 CRI 决定的是 Runtime 和 Kubernetes 之间的关系,那么我们现在能不能再有一层更细致的 API 来决定 CRI Shim 跟下面的 Runtime 之间真正的接口是什么样的?


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



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


而现在,我作为一个 Kata 维护者我就可以这么实现。在创建 Sandbox 的时候,我启动一个 Containerd Shim。但是当我下一步是 call API 的时候,我就不再起了,而是 reuse,我重用为你创建好的这个 Sandbox,这就为你的实现提供了很大的自由度。


所以这时候你会发现整个实现的方式变了,这时候 Containerd 用过来之后,它不再去关心每个容器起 Containerd ShimV,而是由你自己去实现。我的实现方式是我只在 Sandbox 时候,去创建 Containerd Shimv,而接下来整个后面的 容器级别的操作,全部走到 containerd shimV2 里,重用 Sandbox,所以这个跟前面的实践就出现很大的不同。



所以现在去总结一下这个图的话,你发现我们实验方式是这样的:


首先,你还是用原来的 CRI Containerd,只不过现在装的是 runC,你现在再装一个 katacontainer 放在机器上面。接下来 Kata 会写一个实现叫 kata-Containerd-Shimv2。所以前面要写一大坨 CRI 的东西,现在不用了。


现在,我们只关注怎么把 Containerd 对接在 kata container 上面,就是所谓的实现 Shimv2 API。而具体到我们这要做的事情上,其实就是这样一系列运行一个容器相关的 API。


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




作者简介


张磊,Kubernetes 社区资深成员与项目维护者。他是 Kubernetes 项目的成员和联合维护者,主要聚焦容器运行时接口(CRI)、调度、资源管理和基于管理程序的容器运行时间。他曾是 KataContainers/Hyper 团队成员,微软研究院(MSR)访问学者。现在阿里巴巴集团担任高级技术专家,共同负责 Kubernetes 上游和阿里巴巴大型集群管理系统的工程工作。张磊是 KubeCon 大会备受欢迎的演讲者,也曾在 LinuxCon 和 OpenStack Summit 发表过多次技术演讲。


2018-12-11 17:592463
用户头像

发布了 40 篇内容, 共 25.5 次阅读, 收获喜欢 142 次。

关注

评论 2 条评论

发布
用户头像
hi,"我们是从 CRI 到 Containerd 到 ShimV2,然后 ShimV2 再到 RunC 再到 KataContainer", 请问 containerd通过ShimV2向下接kata 还要到RunC吗?
2020-03-01 23:52
回复
没有更多了
发现更多内容

百分点助力常州科教城上线两大平台 打造国际智慧创新城

百分点认知智能实验室

AI 智慧城市

第十三周作业 (作业二)

Geek_83908e

架构师一期

架构师第十三周作业

_

极客大学架构师训练营 第十三周

架构师训练营第四周”系统架构“作业

随秋

极客大学架构师训练营

Week 13 作業

Judyyy

极客时间架构师训练营 1 期 - 第 13 周总结

Kaven

架构师训练营第九周作业2

韩儿

封装 axios 取消重复请求

360技术

Web 开发

《Python中的竞争性编程:128种提高编码技能的算法》PDF

计算机与AI

Python 算法

架构师第十三周总结

_

总结 架构师第十三周

极客大学 - 架构师训练营 第十三周作业

9527

架构师训练营 1 期第 13 周:数据应用(二)- 总结

piercebn

极客大学架构师训练营

架构师训练营第九周作业1

韩儿

Week9作业

lggl

第十三周作业 (作业一)

Geek_83908e

架构师一期

架构师训练营 1 期第 13 周:数据应用(二)- 作业

piercebn

极客大学架构师训练营

架构师训练营第四周”系统架构“总结

随秋

极客大学架构师训练营

架构师训练营第 1 期 - 第 13 周课后练习

Anyou Liu

极客大学架构师训练营

架构师训练营 第九周课程

文江

世界之书:《大国政治的悲剧》与美国独行

lidaobing

大国政治的悲剧 28天写作

生产环境全链路压测建设历程14:核心链路的改造

数列科技杨德华

全链路压测 七日更

极客时间架构师培训 1 期 - 第 13 周作业

Kaven

架构师训练营 - 第十三周总结

一个节点

极客大学架构师训练营

Week 13 學習總結

Judyyy

使用 Docker 部署 canal,并将消息推送到 RabbitMQ

AlwaysBeta

MySQL Docker RabbitMQ canal

架构师训练营第四周课后作业

万有引力

AWS 发布 180 项新服务与功能

亚马逊云科技 (Amazon Web Services)

云计算 AWS

NO.001-简说Java并发编程史

葛一凡

第十三周 数据应用 (二)

9527

Week9总结

lggl

作业

架构师训练营 - 第十三周作业

一个节点

极客大学架构师训练营

CRI Shimv2:一种 Kubernetes 集成容器运行时的新思路_容器_张磊_InfoQ精选文章