深入浅出Docker 读书笔记(二)

                                                                             第五章:Docker引擎

Docker引擎是用来运行和管理容器的核心软件。通常人们会简单地将其代指为 Docker 或 Docker 平台。基于开放容器计划(OCI)相关标准的要求,Docker 引擎采用了模块化的设计原则,其组件是可替换的。从多个角度来看,Docker 引擎就像汽车引擎—二者都是模块化的,并且由许多可交换的部件组成。汽车引擎由许多专用的部件协同工作,从而使汽车可以行驶,例如进气管、节气门、气缸、排气管等。Docker 引擎由许多专用的工具协同工作,从而可以创建和运行容器,例如 API、执行驱动、运行时、shim 进程等。Docker 引擎由如下主要的组件构成:Docker 客户端(Docker Client)、Docker 守护进程(Docker daemon)、containerd 以及 runc。它们共同负责容器的创建和运行。总体逻辑如下图所示。
 

Docker总体逻辑

Docker 首次发布时,Docker 引擎由两个核心组件构成:LXC ((LinuXContainers Linux容器)和 Docker daemon。Docker daemon 是单一的二进制文件,包含诸如 Docker 客户端、Docker API、容器运行时、镜像构建等。LXC 提供了对诸如命名空间(Namespace)和控制组(CGroup)等基础工具的操作能力,它们是基于Linux内核的容器虚拟化技术。下图阐释了在 Docker 旧版本中,Docker daemon、LXC 和操作系统之间的交互关系。

先前的Docker架构

对 LXC 的依赖自始至终都是个问题,首先,LXC 是基于 Linux 的。这对于一个立志于跨平台的项目来说是个问题。其次,如此核心的组件依赖于外部工具,这会给项目带来巨大风险,甚至影响其发展。因此,Docker 公司开发了名为 Libcontainer 的自研工具,用于替代 LXC。Libcontainer 的目标是成为与平台无关的工具,可基于不同内核为 Docker 上层提供必要的容器交互功能。随着时间的推移,Docker daemon 的整体性带来了越来越多的问题。难于变更、运行越来越慢。这并非生态(或Docker公司)所期望的。拆解这个大而全的 Docker daemon 进程,并将其模块化。这项任务的目标是尽可能拆解出其中的功能特性,并用小而专的工具来实现它。这些小工具可以是可替换的,也可以被第三方拿去用于构建其他工具。这一计划遵循了在 UNIX 中得以实践并验证过的一种软件哲学:小而专的工具可以组装为大型工具。目前 Docker 引擎的架构示意图如下图所示,图中有简要的描述。

Docker引擎的架构

开放容器计划(OCI)的影响定义两个容器相关的规范(或者说标准)。例如Docker daemon 不再包含任何容器运行时的代码—所有的容器运行代码在一个单独的 OCI 兼容层中实现。默认情况下,Docker 使用 runc 来实现这一点。runc 是 OCI 容器运行时标准的参考实现。如上图中的 runc 容器运行时层。runc 项目的目标之一就是与 OCI 规范保持一致。除此之外,Docker 引擎中的 containerd 组件确保了 Docker 镜像能够以正确的 OCI Bundle 的格式传递给 runc。关于runc如前所述,runc 是 OCI 容器运行时规范的参考实现。Docker 公司参与了规范的制定以及 runc 的开发。runc 实质上是一个轻量级的、针对 Libcontainer 进行了包装的命令行交互工具(Libcontainer 取代了早期 Docker 架构中的 LXC)。runc 生来只有一个作用——创建容器,这一点它非常拿手,速度很快!不过它是一个 CLI 包装器,实质上就是一个独立的容器运行时工具。因此直接下载它或基于源码编译二进制文件,即可拥有一个全功能的 runc。但它只是一个基础工具,并不提供类似 Docker 引擎所拥有的丰富功能。有时也将 runc 所在的那一层称为“OCI 层”,如上图所示。关于 runc 的发布信息见 GitHub 中 opencontainers/runc 库的 release。

      在对 Docker daemon 的功能进行拆解后,所有的容器执行逻辑被重构到一个新的名为 containerd的工具中。它的主要任务是容器的生命周期管理——start | stop | pause | rm....。containerd 在 Linux 和 Windows 中以 daemon 的方式运行,Docker 引擎技术栈中,containerd 位于 daemon 和 runc 所在的 OCI 层之间。Kubernetes 也可以通过 cri-containerd 使用 containerd ,它最初被设计为轻量级的小型工具,仅用于容器的生命周期管理。然而随着时间的推移,它被赋予了更多的功能,比如镜像管理。

       启动一个容器:常用的启动容器的方法就是使用 Docker 命令行工具。下面的docker container run命令会基于 alpine:latest 镜像启动一个新容器。     $ docker container run --name ctr1 -it alpine:latest sh      当使用 Docker 命令行工具执行如上命令时,Docker 客户端会将其转换为合适的 API 格式,并发送到正确的 API 端点。API 是在 daemon 中实现的。一旦 daemon 接收到创建新容器的命令,它就会向 containerd 发出调用。daemon 已经不再包含任何创建容器的代码了!daemon 使用一种 CRUD 风格的 API,通过 gRPC 与 containerd 进行通信。虽然名叫 containerd,但是它并不负责创建容器,而是指挥 runc 去做。containerd 将 Docker 镜像转换为 OCI bundle,并让 runc 基于此创建一个新的容器。然后,runc 与操作系统内核接口进行通信,基于所有必要的工具(Namespace、CGroup等)来创建容器。容器进程作为 runc 的子进程启动,启动完毕后,runc 将会退出。至此,容器启动完毕。整个过程如下图所示。

启动新容器的过程

扫描二维码关注公众号,回复: 10746036 查看本文章

该模型的优势在于将所有的用于启动、管理容器的逻辑和代码从 daemon 中移除,意味着容器运行时与 Docker daemon 是解耦的,有时称之为“无守护进程的容器(daemonless container)”,如此,对 Docker daemon 的维护和升级工作不会影响到运行中的容器。在旧模型中,所有容器运行时的逻辑都在 daemon 中实现,启动和停止 daemon 会导致宿主机上所有运行中的容器被杀掉。

     shim 是实现无 daemon 的容器(用于将运行中的容器与 daemon 解耦,以便进行 daemon 升级等操作)不可或缺的工具。
containerd 指挥 runc 来创建新容器。事实上,每次创建容器时它都会 fork 一个新的 runc 实例。一旦容器创建完毕,对应的 runc 进程就会退出。因此即使运行上百个容器,也无须保持上百个运行中的 runc 实例。一旦容器进程的父进程 runc 退出,相关联的 containerd-shim 进程就会成为容器的父进程。作为容器的父进程,shim 的部分职责如下。

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

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

                                                                             第6章:Docker镜像

       Docker镜像可以理解为 VM里的模板,VM 模板就像停止运行的 VM,而 Docker 镜像就像停止运行的容器;而作为一名研发人员,则可以将镜像理解为类(Class)。拉取镜像下载到本地 Docker 主机,可以使用该镜像启动一个或者多个容器。镜像由多个层组成,每层叠加之后,从外部看来就如一个独立的对象。镜像内部是一个精简的操作系统(OS),同时还包含应用运行所必须的文件和依赖包。因为容器的设计初衷就是快速和小巧,所以镜像通常都比较小。在该前提下,镜像可以理解为一种构建时(build-time)结构,而容器可以理解为一种运行时(run-time)结构,如下图所示。
 

镜像与容器

      镜像和容器:上图从顶层设计层面展示了镜像和容器间的关系。通常使用docker container run和docker service create命令从某个镜像启动一个或多个容器。一旦容器从镜像启动后,二者之间就变成了互相依赖的关系,并且在镜像上启动的容器全部停止之前,镜像是无法被删除的。尝试删除镜像而不停止或销毁使用它的容器,会导致出错。容器目的就是运行应用或者服务,这意味着容器的镜像中必须包含应用/服务运行所必需的操作系统和应用文件。容器追求快速和小巧,这意味着构建镜像的时候通常需要裁剪掉不必要的部分,保持较小的体积。例如,Docker 镜像通常不会包含 6 个不同的 Shell 让读者选择——通常 Docker 镜像中只有一个精简的Shell,甚至没有 Shell。镜像中还不包含内核——容器都是共享所在 Docker 主机的内核。所以有时会说容器仅包含必要的操作系统(通常只有操作系统文件和文件系统对象)。

       镜像拉取:Docker 主机安装之后,本地并没有镜像。docker image pull 是下载镜像的命令。镜像从远程镜像仓库服务的仓库中下载。默认情况下,镜像会从 Docker Hub 的仓库中拉取。docker image pull alpine:latest 命令会从 Docker Hub 的 alpine 仓库中拉取标签为 latest 的镜像。Linux Docker 主机本地镜像仓库通常位于 /var/lib/docker/<storage-driver>,Windows Docker 主机则是 C:\ProgramData\docker\windowsfilter。可以使用以下命令检查 Docker 主机的本地仓库中是否包含镜像。$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE

Windows示例如下:> docker image pull microsoft/powershell:nanoserver

镜像仓库:Docker 镜像存储在镜像仓库服务(Image Registry)当中。Docker 客户端的镜像仓库服务是可配置的,默认使用 Docker Hub。镜像仓库服务包含多个镜像仓库(Image Repository)。同样,一个镜像仓库中可以包含多个镜像。所以下图展示了包含 3 个镜像仓库的镜像仓库服务,其中每个镜像仓库都包含一个或多个镜像。
 

包含3个镜像仓库的镜像仓库服务

官方和非官方镜像仓库:Docker Hub 也分为官方仓库(Official Repository)和非官方仓库(Unofficial Repository)。官方仓库中的镜像是由 Docker 公司审查的。这意味着其中的镜像会及时更新,由高质量的代码构成,这些代码是安全的,有完善的文档和最佳实践。非官方仓库更像江湖侠客,其中的镜像不一定具备官方仓库的优点,但这并不意味着所有非官方仓库都是不好的!非官方仓库中也有一些很优秀的镜像。

       镜像命名和标签:只需要给出镜像的名字和标签,就能在官方仓库中定位一个镜像(采用“:”分隔)。从官方仓库拉取镜像时,docker image pull 命令的格式如下。docker image pull <repository>:<tag>  如:$ docker image pull mongo:3.3.11  //该命令会从官方Mongo库拉取标签为3.3.11的镜像。如果没有在仓库名称后指定具体的镜像标签,则 Docker 会假设用户希望拉取标签为 latest 的镜像。标有 latest 标签的镜像不保证这是仓库中最新的镜像!所以使用 latest 标签时需要谨慎!镜像也以根据需求设置多个标签。这是因为标签是存放在镜像元数据中的任意数字或字符串。

过滤 docker image ls 的输出内容:Docker 提供 --filter 参数来过滤 docker image ls 命令返回的镜像列表内容。
下面的示例只会返回悬虚(dangling)镜像。那些没有标签的镜像被称为悬虚镜像,在列表中展示为<none>:<none>。$ docker image ls --filter dangling=true   通常出现这种情况,是因为构建了一个新镜像,然后为该镜像打了一个已经存在的标签。
当此情况出现,Docker 会构建新的镜像,然后发现已经有镜像包含相同的标签,接着 Docker 会移除旧镜像上面的标签,将该标签标在新的镜像之上。可以通过 docker image prune 命令移除全部的悬虚镜像。如果添加了 -a 参数,Docker 会额外移除没有被使用的镜像(那些没有被任何容器使用的镜像)。
Docker 目前支持如下的过滤器。

  • dangling:可以指定 true 或者 false,仅返回悬虚镜像(true),或者非悬虚镜像(false)。
  • before:需要镜像名称或者 ID 作为参数,返回在之前被创建的全部镜像。
  • since:与 before 类似,不过返回的是指定镜像之后创建的全部镜像。
  • label:根据标注(label)的名称或者值,对镜像进行过滤。docker image ls命令输出中不显示标注内容。

docker search 命令允许通过 CLI 的方式搜索 Docker Hub。可以通过“NAME”字段的内容进行匹配,并且基于返回内容中任意列的值进行过滤。$ docker search nigelpoulton, “NAME”字段是仓库名称,包含了 Docker ID,或者非官方仓库的组织名称。Docker 镜像由一些松耦合的只读镜像层组成。Docker 负责堆叠这些镜像层,并且将它们表示为单个统一的对象。查看镜像分层的方式可以通过 docker image inspect 命令。所有的 Docker 镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上,创建新的镜像层。举一个简单的例子,假如基于 Ubuntu Linux 16.04 创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加python包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。该镜像当前已经包含 3 个镜像层。在添加额外的镜像层的同时,镜像始终保持是当前所有镜像的组合,理解这一点非常重要。下图中举了一个简单的例子,每个镜像层包含 3 个文件,而镜像包含了来自两个镜像层的 6 个文件。
 

添加额外的镜像层后的镜像

Docker 通过存储引擎(新版本采用快照机制)的方式来实现镜像层堆栈,并保证多镜像层对外展示为统一的文件系统。Linux 上可用的存储引擎有 AUFS、Overlay2、Device Mapper、Btrfs 以及 ZFS。顾名思义,每种存储引擎都基于 Linux 中对应的文件系统或者块设备技术,并且每种存储引擎都有其独有的性能特点。Docker 在 Windows 上仅支持 windowsfilter 一种存储引擎,该引擎基于 NTFS 文件系统之上实现了分层和 CoW[1]。多个镜像之间可以并且确实会共享镜像层。这样可以有效节省空间并提升性能。Docker拉取镜像时它可以识别出要拉取的镜像中,哪几层已经在本地存在 就会以Already exists 结尾。Docker 在 Linux 上支持很多存储引擎(Snapshotter)。每个存储引擎都有自己的镜像分层、镜像层共享以及写时复制(CoW)技术的具体实现。
不仅可以通过标签拉取镜像还可以通过摘要(镜像散列值)(而不是标签)来再次拉取该镜像。镜像本身就是一个配置对象,其中包含了镜像层的列表以及一些元数据信息。镜像层才是实际数据存储的地方(比如文件等,镜像层之间是完全独立的,并没有从属于某个镜像集合的概念)。镜像的唯一标识是一个加密 ID,即配置对象本身的散列值。每个镜像层也由一个加密 ID 区分,其值为镜像层本身内容的散列值。这意味着修改镜像的内容或其中任意的镜像层,都会导致加密散列值的变化。所以,镜像和其镜像层都是不可变的,任何改动都能很轻松地被辨别。这就是所谓的内容散列(Content Hash)。每个镜像层同时会包含一个分发散列值(Distribution Hash)。这是一个压缩版镜像的散列值,当从镜像仓库服务拉取或者推送镜像的时候,其中就包含了分发散列值,该散列值会用于校验拉取的镜像是否被篡改过。这个内容寻址存储模型极大地提升了镜像的安全性,因为在拉取和推送操作后提供了一种方式来确保镜像和镜像层数据是一致的。该模型也解决了随机生成镜像和镜像层 ID 这种方式可能导致的 ID 冲突问题。

在拉取镜像并运行之前,需要考虑镜像是否与当前运行环境的架构匹配,这破坏了 Docker 的流畅体验。多架构镜像(Multi-architecture Image)的出现解决了这个问题!Docker(镜像和镜像仓库服务)规范目前支持多架构镜像。这意味着某个镜像仓库标签(repository:tag)下的镜像可以同时支持 64 位 Linux、PowerPC Linux、64 位 Windows 和 ARM 等多种架构。简单地说,就是一个镜像标签之下可以支持多个平台和架构。下面通过实操演示该特性。为了实现这个特性,镜像仓库服务 API 支持两种重要的结构:Manifest 列表(新)和 Manifest。Manifest 列表是指某个镜像标签支持的架构列表。其支持的每种架构,都有自己的 Mainfest 定义,其中列举了该镜像的构成。

删除镜像:可以通过 docker image rm 命令从 Docker 主机删除该镜像。其中,rm 是 remove 的缩写。删除操作会在当前主机上删除该镜像以及相关的镜像层。这意味着无法通过 docker image ls 命令看到删除后的镜像,并且对应的包含镜像层数据的目录会被删除。但是,如果某个镜像层被多个镜像共享,那只有当全部依赖该镜像层的镜像都被删除后,该镜像层才会被删除。
如通过镜像 ID 来删除镜像,可能跟读者机器上镜像 ID 有所不同。$ docker image rm 02674b9cb179   如果被删除的镜像上存在运行状态的容器,那么删除操作不会被允许。再次执行删除镜像命令之前,需要停止并删除该镜像相关的全部容器。一种删除某 Docker 主机上全部镜像的快捷方式是在 docker image rm 命令中传入当前系统的全部镜像 ID,可以通过 docker image ls 获取全部镜像 ID(使用 -q 参数)。如果是在 Windows 环境中,那么只有在 PowerShell 终端中执行才会生效。在 CMD 中执行并不会生效。$ docker image rm $(docker image ls -q) -f

为了理解具体工作原理,首先下载一组镜像,然后通过运行 docker image ls -q

$ docker image rm $(docker image ls -q) -f

发布了105 篇原创文章 · 获赞 86 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/dingyahui123/article/details/104250793