深究k8s弃用docker的背后

背景

​ 近日,Kubernetes官方发布公告,宣布自 v1.20 起放弃对 Docker 的支持,届时用户将收到 Docker 弃用警告,并需要改用其他容器运行时。但 Docker 作为容器镜像构建工具的作用将不受影响,用其构建的容器镜像将一如既往地在集群中与所有容器运行时正常运转。

​ 在这里,日常使用docker和k8s的大家,是不是会有这样的疑问:现有打包的景象要怎么迁移?会不会出现不兼容?对以后开发和工作会有怎么样的影响?

在这里,小编可以很肯定的回答大家,不会有什么影响,该用docker的地方还是可以用docker,现有的镜像也不需要迁移,日后的开发工作流也不会发生很大的改变

Kubernetes调度容器的架构

​ 按照1.20的changelog原文

Docker support in the kubelet is now deprecated and will be removed in a future release. The kubelet uses a module called "dockershim" which implements CRI support for Docker and it has seen maintenance issues in the Kubernetes community

​ 原文说的是kubelet是通过一个叫做dockershim的模块实现对docker的支持,然后以后版本会移除这个dockershim模块。

典型的k8s runtime架构(dockershim)

​ 到底这个dockershim是何方神圣,以至于k8s需要把它弃用呢?

​ 故事还得从k8s 发布之初和docker的关系说起。k8s发布之初,容器实现在当时占用垄断地位的是docker,并且大家最熟悉以及用的最多的也是docker,为了迎合主流,kubernetes官方也是率先使用了docker 作为底层容器的实现。

​ 于是乎,kubelet调度底层容器的架构是这样的

image-20201210094732911

​ 当kubelet调度容器的时候,会经历以下几步

  1. Kubelet 通过CRI接口调用dockershim请求创建容器
  2. dockershim把创建容器的请求转换成docker daemon的请求往docker创建一个容器
  3. docker daemon这时候把容器创建请求请求到使用CRI实现的containerd
  4. containerd通过OCI调用containerd-shim然后进而使用操作系统底层实现容器的创建

​ 看到这里,相信大家已经明白了官方为啥要弃用dockershim了,因为这个调用合着需要经过两个划水的dockershim和docker daemon。kubelet直接调用containerd就完事了

移除dockershim后的世界

​ 当dockershim从kubelet源码中消失后,整个架构会变成下图这样

image-20201210095845659

​ 原本的创建步骤会简化成了:

  1. kubelet 通过CRI接口调用CRI-containerd插件请求创建容器
  2. CRI-containerd和containerd通信,然后创建容器

​ 链路看起来是没什么问题的,确实绕过了docker ,然后直接调起了containerd。可能这时候大家担心的问题是:

要是这个dockershim在之前的架构中提供了更多的功能怎么办?

​ 要回答这个问题,首先我们得看一下dockershim 调用的功能在docker 架构图中的部分

img

​ 在整个庞大的docker架构中,kubelet仅仅需要红框部分,也就是符合CRI标准的的containerd。

​ 像其他部分,网络以及持久存储,在k8s本身架构中已经有解决方案,从图中看,kubelet因为只使用了containerd部分,所以无论通过dockershim+docker daemon做中介还是直接调用 containerd 都是一样效果的。还是直接调用成本最低,也最快。

这里说的移除了dockershim后要使用containerd 不是说仅仅只有containerd,而是官方的一个典型采用CRI运行时例子。实际上任何满足CRI运行时的都可以接入到kubelet。

CRI运行时

CRI运行时中定义了容器镜像的服务的接口,用于容器编排工具和容器底层的接口的一个中介

CRI一般会有以下的功能

  1. 针对容器操作的接口,例如容器创建,启动,停止容器等等
  2. 针对镜像操作的接口,包括拉取镜像删除镜像等;
  3. 一套针对 PodSandbox(容器沙箱环境)的操作接口

​ 目前市面上,CRI运行时主要有两种实现,containerdCRI-O

Containerd

​ docker架构的一个组成部分,用来提供CRI接口

​ 如果大家打算从docker迁移出来,那么containerd本身就是docker的核心,并且它提供给kubelet的CRI完全就是由docker提供的。如果弃用了dockershim后,迁移到containerd应该是最平滑的选择

​ 并且这个CRI运行时还是全开源的 https:/ /github.com/containerd/containerd/

CRI-O

​ 一个由 Red Hat 员工开发的 CRI 运行时。它的最大区别在于并不依赖于 Docker。

​ 不同于作为 Docker 组成部分的 containerd,CRI-O 在本质上属于纯 CRI 运行时、因此不包含除 CRI 之外的任何其他内容。

​ 因为它和docker的交集太小,因此从docker迁移到CRI-O往往是很困难的

​ https://github.com/cri-o/cri-o

OCI运行时

​ OCI 运行时负责接收CRI运行时传递过来的创建容器请求,然后使用 Linux 内核系统调用(例如 cgroups 与命名空间)生成容器。

​ 一般,我们常见的OCI运行时有runC和gVisor

runC

​ CRI会调用runC直接在Linux内核执行系统调用,然后在操作系统内核启动容器

image-20201210102942624

gVisor

​ 在gVisor模式中,容器是把二进制文件启动在一个访客内核中,而不能直接访问到操作系统的内核,因此是更安全的实现

image-20201210103110914

k8s和CRI和OCI的协同

image-20201210104326587

CRI充当一个中间人

  • 此kubelet 获取容器创建的请求
  • 根据OCI的规范,把创建容器请求转换成OCI能看懂的OCI json请求

扩展延伸

弃用的docker和我们日常的docker

​ 总结起来,k8s弃用docker其实是弃用了dockershim,直接对接了CRI接口,并且默认支持的是containerd,一个docker自带的组件,所以是可以和docker本身做到100%兼容。对于以前打包的镜像,应用程序,也不需要发生其他的变更,即可适应新的环境。

​ 顺便说一下,弃用了docker,也不代表着docker要凉了。docker带给我们的更是一种新的工作流和开发方式,一种容器化的思维。在k8s以外的其他场景,也还是会有数以万计的容器在docker里面运行。在我们日常开发中,docker在临时搭建一些环境同样还是可以继续发挥巨大的作用。

​ k8s 去除dockershim是作为自身发展考虑,为的是支持更通用的CRI标准,更高的抽象化意味着更好的兼容性。

​ 因为有了CRI标准的存在,当以后不管用docker或者其他的容器编排工具,只要工具本身提供了CRI接口,就可以被k8s所支持,并且其中的资源也是可以互相复用的。

updatedupdated2021-02-092021-02-09