Kubernetes 弃用Docker

Kubernetes 在其最新的 Changelog 中宣布,自 Kubernetes 1.20 之后将弃用 Docker 作为容器运行时。那么这到底是怎么回事?开发者和企业会受到什么样到影响?

近几年,Kubernetes 已经成为自有机房、云上广泛使用的容器编排方案,最广泛的使用方式是 Kubernetes+Docker。从 DevOps 人员的角度,一面用 kubctl 命令、k8s API 来操作集群,一面在单机用 Docker 命令来管理镜像、运行镜像。

单独用 Docker 的情况,在一些公司的场景里面也是有的。一种场景是“只分不合”,把一台机器用 Docker 做资源隔离,但是不需要将多容器“编排”。单独用 Kubernetes,下层不是 Docker 的情况,并不算很多。

Kubernetes 和 Docker 的关系,简单来说,有互补,也有竞争。在一般的认知中,Kubernetes 和 Docker 是互补关系:

        Dockers 属于下层——容器引擎
        Kubernetes 属于上层——编排调度层。

Docker 源于 Linux Container,可以将一台机器的资源分成 N 份容器,做到资源的隔离,并将可运行的程序定义为标准的 docker image;Kubernetes 则可以把不同机器的每份容器进行编排、调度,组成分布式系统。
Kubernetes 和 Docker 并不完全是“泾渭分明”的互补关系,它之间有重叠部分,也可以说成是竞争,主要在于几个点:

  • 系统三大移植资源是计算、存储、网络,从 Kubernetes 角度 Docker 属于“Runtime(运行时)”,也就是计算资源;但是 Docker 技术体系里面,本身也包括存储层、网络层。上下层职责的重叠,也可以看作竞争。

  • Docker 原本有个原生的调度引擎——Swarm,几年前在调度编排领域,还是 Kubernetes、Mesos、Swarm 三者并存,Kubernetes 最终胜出,但 Docker 仍有“继续向上做一层的意愿”。

avatar

Kubernetes 在如何使用 Docker 方面,存在争议和变数。kubernetes 1.20 ChangeLog 中所谓要废弃 Docker 的传言,也是无风不起浪。换句话说,即便 Kubernetes 一直用 Docker,也不是用 Docker 的全部,多少是不一样的。

avatar

而且,“弃用 Docker”这个词本身有多重的含义,Docker 并非一个单层软件,Kubernetes 1.20 弃用 dockershim 并不代表弃用了 Docker 的全部,仍有 containerd 可以对接 docker。

Kubernetes 有 CRI、OCI 两个容器标准

在目前广泛使用 kubernetes 与 Runtime 的桥接方案,CRI(Container Runtime Interface)与 OCI(Open Container Initiative)是“套娃“关系。Kubernetes 的 kubelet 调用 CRI,OCI 的实现者然后再调用 OCI。

下图也说明了 CRI 与 OCI 的关系:

avatar

从 Kubernetes 的角度,CRI 是与 CNI(网络)、CSI(存储)相同层级的接口。

  • OCI 是个自下而上的标准,也就是从实现抽象出接口,它是 Linux Foundation 主导的。Docker 实现的核心 RunC,也就是 OCI 的典型实现、标准实现。
  • CRI 是个自上而下的标准,源于 Kubernetes 对移植层(运行时)的要求。

容器引擎层自下而上定义 OCI,容器编排层自上而下定义 CRI,这也让它们出现了“套娃“运行情况。

在 Kubernetes 的 dockershim、cri-containerd、cri-o 三种实现中,RedHat 推崇的 cri-o 已经比较主流,它虽然仍是“套娃“,但已经比较精简。

avatar

下面是从 kubernetes 集群运行的全景图看 cri-o 的位置:

avatar

Docker 本源于 Linux Container

Docker 作为容器引擎,其实现的基础是 Linux Container——从内核到用户空间的机制。Linux Container 可以分成两个部分,内核里面的 cgroup,用户空间的 lxc。Docker 最初实现的时候,也是完全基于 Linux Container 的,基于 lxc 做更上层的东西。这张很多人认为“与事实不符“的图,其实代表了过去:

avatar

在 Docker 的发展过程中,最终弃用了 C 语言写成 lxc,换成了 go 语言写成的 libcontainer。下面的图也不是很新,但它更能表示 Docker 后续典型的架构,这里面已经没有了 lxc。

avatar

然而,万变不离其宗,Docker 实现的本源,还是 Linux Container。即便不用 lxc,当仍要用内核的 cgroup,并且模式也是类似的。

Kubernetes 最终如何桥接容器

从纯技术的角度,与其讨 Kubernetes 与 Docker 关系,不如讨论 Kubernetes 与最终容器实现层的关系。因为 Docker 这个名词,在不同的时候,有着不同的内涵、外延。

下面是 Docker 的简图:

avatar

从软件模块的角度,图中的 docker Engine、cri-containd、containd-shim、runC 都属于 Docker 体系的软件。

下图中的紫、橙、红三种颜色,代表了 dockershim、cri-containerd、cri-o 三种 CRI 的典型方式——流程在逐渐缩短,这也是 CRI 实现的一个演进过程。

avatar

1、 如果是 kubelet 的 dockershim 模式(紫色),流程是这样的:

  • 1.kubelet 从 CRI 的 gRPC 调用 dockershim,二者在同一个进程
  • 2.dockershim 调用 docker 守护进程
  • 3.docker 守护进程调用 containerd;containerd 调用 containerd-shim(有时名为 docker-containerd-shim 守护进程)完成创建容器等操作
  • 4.containerd-shim 访问 OCI 的实现 runC(命令行可执行程序)

2、 如果是 kubelet 的 cri-containerd 模式(橙色),流程是这样的:

  • 1.kubelet 从 CRI 的 gRPC 调用 cri-containerd;
  • 2.cri-containerd 调用 containerd;containerd 调用 containerd-shim(同上)
  • 3.containerd-shim 调用 RucnC (同上)

在很多人的印象中,如果不用 docker 守护进程,就相当于“弃用 docker“,这其实也就是从 dockershim 到 containerd 的变化。从另一个角度来说,containerd 这个守护进程,也是 docker 组织做的。

3、 如果是 kubelet 的 cri-o 模式(红色),则更加简练:

  • 1.kubelet 从 CRI 的 gRPC 调用 cri-o;
  • 2.cri-o 调用 OCI 的实现 runC

如果以 kubelet 调用 CRI 为起点,OCI 的 runC 调用为终点,三种模式经历的可执行程序分别是:

  • dockershim 模式:dockershim(*)->dockd->containerd->containerd-shim
  • cri-containerd 模式:cri-containerd(*)-> containerd->containerd-shim
  • cri-o 模式:cri-o

    dockershim 模式有 3 个可执行程序,dockershim 一般与 kubelet 同进程;cri-containerd 模式有 2-3 个可执行程序,cri-containerd 可与 containerd 同进程;cri-o 模式只有 1 个可执行程序。

显然在这种 Red Hat 推崇的 cri-o 模式下,Docker 体系的 containerd 也用不着了,只剩 runC 这个命令行的程序。runC 也是用 go 写成的,里面有调用 libcontainer。

当 Docker 萎缩到这个地步,其实也只剩 Linux 内核里面 cgroup、namespace 功能的封装了。

总结来说,由于老技术实现的惯性,在生产环境大量使用的经典 Kubernetes+ Docker 方案依然运行,且运维已经成熟,不会很快升级。对于开发人员、企业,对于 K8S API 的使用频率、变数,远远大于 Docker API,至于 Kubernetes 和 Docker 的桥接,更不用关心。因此,即便“彻底弃用 Docker”,对开发者与企业的影响也非常有限。