Docker(二)引擎

Docker(二)引擎

第一节 Docker引擎

1.1 简介

  Docker引擎是用来运行和管理容器的核心软件。如果有对VMware略知一二,可以将Docker引擎理解为ESXi的角色。基于开放容器计划(OCI)相关标准的要求,Docker引擎采用了模块化的设计原则,其组件是可替换的。可以把Docker引擎看作汽车引擎——二者都是模块化的,并且由许多可交换的部件组成。

  Docker引擎由如下主要的组件构成:Docker客户端 (Docker Client)、Docker守护进程(Docker daemon)、containerd以及runc,它们共同负责容器的创建和运行。总体逻辑如图所示。之后的整理中提到runc和containerd时,将一律使用小写的“r”和“c”。

Docker总体逻辑


1.2 详解

  Docker首次发布时,引擎由两个核心组件构成:LXC 和 Docker daemon。

  Docker daemon是单一的二进制文件,包含诸如Docker客户端、 Docker API、容器运行时、镜像构建等。

  LXC提供了对诸如命名空间(Namespace)和控制组(CGroup)等基础工具的操作能力,它们是基于Linux内核的容器虚拟化技术。

  图5.2阐释了在Docker旧版本中,Docker daemon、LXC和操作系统之间的交互关系。

先前的Docker架构

1.2.1 摆脱LXC

  对LXC的依赖自始至终都是个问题。首先,LXC是基于Linux的。这对于一个想要跨平台的项目来说是个问题。其次,如此核心的组件依赖于外部工具,这会给项目带来巨大风险,甚至影响其发展。因此,Docker公司开发了名为Libcontainer的自研工具,用于替代LXC。Libcontainer的目标是成为与平台无关的工具,可基于不同内核为Docker上层提供必要的容器交互功能。在Docker 0.9版本中,Libcontainer取代LXC成为默认的执行驱动。

1.2.2 摒弃大而全的Docker daemon

  随着时间的推移,Docker daemon的整体性带来了越来越多的问题。

  • 难于变更。
  • 运行越来越慢。
  • 这并非生态(或Docker公司)所期望的。

  Docker公司意识到了这些问题,开始努力着手拆解这个大而全的Docker daemon进程,并将其模块化。这项任务的目标是尽可能拆解出其中的功能特性,并用小而专的工具来实现它。这些小工具可以是可替换的,也可以被第三方拿去用于构建其他工具。这一计划遵循了在UNIX中得以实践并验证过的一种软件哲学:小而专的工具可以组装为大型工具。

  这项拆解和重构Docker引擎的工作仍在进行中。不过,所有容器执行和容器运行时的代码已经完全从daemon中移除,并重构为小而专的工具。

  目前Docker引擎的架构示意图如图5.3所示,图中有简要的描述。

Docker引擎的架构

1.2.3 开放容器计划(OCI)的影响

  当Docker公司正在进行Docker daemon进程的拆解和重构的时候,OCI也正在着手定义两个容器相关的规范(或者说标准)。

  • 镜像规范。
  • 容器运行时规范

  从Docker 1.11版本(2016年初)开始,Docker引擎尽可能实现了OCI的规范。如Docker daemon不再包含任何容器运行时的代码——所有的容器运行代码在一个单独的OCI兼容层中实现

  默认情况下,Docker使用runc来实现这一点。runc是OCI容器运行时标准的参考实现。如上面图5.3中的runc容器运行时层。runc项目的目标之一就是与OCI规范保持一致。目前OCI规范均为1.0版本,我们不希望它们频繁地迭代,毕竟稳定胜于一切。除此之外,Docker引擎中的containerd组件确保了Docker镜像能够以正确的OCI Bundle的格式传递给runc。

1.2.4 runc

  runc是OCI容器运行时规范的参考实现。去粗取精,会发现runc实质上是一个轻量级的、针对Libcontainer进行了包装的命令行交互工具(Libcontainer取代了早期Docker架构中的LXC)。

  runc生来只有一个作用——创建容器,这一点它非常拿手,速度很快!不过它是一个CLI包装器,实质上就是一个独立的容器运行时工具。因此直接下载它或基于源码编译二进制文件,即可拥有一个全功能的runc。但它只是一个基础工具,并不提供类似Docker引擎所拥有的丰富功能。有时也将runc所在的那一层称为“OCI层”,如图5.3所示。

1.2.5 containerd

  在对Docker daemon的功能进行拆解后,所有的容器执行逻辑被重构到一个新的名为containerd(发音为container-dee)的工具中。它的主要任务是容器的生命周期管理——start | stop | pause | rm….

  containerd在Linux和Windows中以daemon的方式运行,从1.11版本之后Docker就开始在Linux上使用它。Docker引擎技术栈中,containerd位于daemon和runc所在的OCI层之间。Kubernetes也可以通过cricontainerd使用containerd。

  如前所述,containerd最初被设计为轻量级的小型工具,仅用于容器的生命周期管理。然而,随着时间的推移,它被赋予了更多的功能, 比如镜像管理。
其原因之一在于,这样便于在其他项目中使用它。比如,在Kubernetes中,containerd就是一个很受欢迎的容器运行时。然而在Kubernetes这样的项目中,如果containerd能够完成一些诸如push和pull镜像这样的操作就更好了。因此,如今containerd还能够完成一些除容器生命周期管理之外的操作。不过,所有的额外功能都是模块化的、可选的,便于自行选择所需功能。所以,Kubernetes这样的项目在使用containerd时,可以仅包含所需的功能。

1.2.6 启动一个新的容器

  现在我们对Docker引擎已经有了一个总体认识,下面介绍一下创建新容器的过程。

  常用的启动容器的方法就是使用Docker命令行工具。下面的docker container run命令会基于alpine:latest镜像启动一个新容器。

1
$ docker container run --name ctr1 -it alpine:latest sh

  当使用Docker命令行工具执行如上命令时,Docker客户端会将其转换为合适的API格式,并发送到正确的API端点。API是在daemon中实现的。这套功能丰富、基于版本的REST API已经成为Docker的标志,并且被行业接受成为事实上的容器API。

  一旦daemon接收到创建新容器的命令,它就会向containerd发出调用。daemon已经不再包含任何创建容器的代码了!daemon使用一种CRUD风格的API,通过gRPC与containerd进行通信。虽然名叫containerd,但是它并不负责创建容器,而是指挥runc去做。containerd将Docker镜像转换为OCI bundle,并让runc基于此创建一个新的容器。

  然后,runc与操作系统内核接口进行通信,基于所有必要的工具(Namespace、CGroup等)来创建容器。容器进程作为runc的子进程启动,启动完毕后,runc将会退出。

  现在,容器启动完毕了。整个过程如图5.4所示。

启动新容器的过程

1.2.7 该模型的显著优势

  将所有的用于启动、管理容器的逻辑和代码从daemon中移除,意味着容器运行时与Docker daemon是解耦的,有时称之为“无守护进程的容器(daemonless container)”,如此,对Docker daemon的维护和升级工作不会影响到运行中的容器。

  在旧模型中,所有容器运行时的逻辑都在daemon中实现,启动和停止daemon会导致宿主机上所有运行中的容器被杀掉。这在生产环境中是一个大问题——想一想新版Docker的发布频次吧,每次daemon的升级都会杀掉宿主机上所有的容器。

1.2.8 shim

  shim是实现无daemon的容器(用于将运行中的容器与daemon解耦,以便进行daemon升级等操作)不可或缺的工具。

  前面提到,containerd指挥runc来创建新容器。事实上,每次创建容器时它都会fork一个新的runc实例。不过,一旦容器创建完毕,对应的runc进程就会退出。因此,即使运行上百个容器,也无须保持上百个运行中的runc实例。一旦容器进程的父进程runc退出,相关联的containerd-shim进程就会成为容器的父进程。作为容器的父进程,shim的部分职责如下。

  • 保持所有STDIN和STDOUT流是开启状态,从而当daemon重启的时候,容器不会因为管道(pipe)的关闭而终止。
  • 将容器的退出状态反馈给daemon。

1.2.9 在Linux中的实现

  在Linux系统中,前面谈到的组件由单独的二进制来实现,具体包括dockerd(Docker daemon)、docker-containerd(containerd)、docker
containerd-shim (shim)和docker-runc (runc)。通过在Docker宿主机的Linux系统中执行ps命令可以看到以上组件的进程。当然,有些进程只有在运行容器的时候才可见。

1.2.10 daemon的作用

  当所有的执行逻辑和运行时代码都从daemon中剥离出来之后,问题出现了——daemon中还剩什么?显然,随着越来越多的功能从daemon中拆解出来并被模块化,这一问题的答案也会发生变化。不过,当本书撰写时,daemon的主要功能包括镜像管理、镜像构建、REST API、身份验证、安全、核心网络以及编排。


参考博客和文章书籍等:

《深入浅出Docker》

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容