Docker容器的演变
1. 简介
在本教程中,我们将探索容器技术的基本概念并了解它们是如何演变的。重点将主要放在探索 Linux 容器上。
这将引导我们了解 Docker 是如何诞生的,以及它是如何继承 Linux 容器以及与 Linux 容器不同的。
2. 什么是容器?
容器通过提供逻辑打包机制将应用程序从它们运行的环境中抽象出来。但是,这种抽象有什么好处呢?好吧,容器允许我们在任何环境中轻松一致地部署应用程序。
我们可以在本地桌面上开发应用程序,将其容器化,然后满怀信心地将其部署在公共云上。
这个概念与虚拟机没有太大区别,但容器实现它的方式却大不相同。虚拟机的存在时间远远超过容器,至少在流行领域是如此。
如果我们回想一下,虚拟机允许我们在虚拟机监视器(如管理程序)的帮助下在主机操作系统之上运行多个客户操作系统。
虚拟机和容器都虚拟化了对 CPU、内存、存储和网络等底层硬件的访问。但是,如果我们将虚拟机与容器进行比较,那么创建和维护虚拟机的成本就会很高:
正如我们在上图中看到的,容器在操作系统级别进行虚拟化,而不是虚拟化硬件堆栈。多个容器共享同一个操作系统内核。
与虚拟机相比,这使得容器更轻量级。因此,容器启动速度更快,使用的硬件资源也少得多。
3. 容器的早期演变
既然我们了解了容器是什么,那么了解它们如何演变以正确看待事物将会很有帮助。尽管开发人员对容器的大众吸引力是相当新的,但某种形状和形式的容器的概念已经存在了几十年。
容器的主要概念是为在同一主机上运行的多个进程提供隔离。我们可以追溯到几十年前提供某种程度的进程隔离的工具的历史。工具chroot 于 1979 年推出,可以将进程及其子进程的根目录更改到文件系统中的新位置。
当然,在进程隔离方面, chroot没有提供任何其他功能。几十年后,FreeBSD 扩展了这个概念,在 2000 年引入了jails ,通过操作系统级虚拟化对进程隔离提供了高级支持。FreeBSD 监狱使用自己的网络接口和 IP 地址提供更明确的隔离。
紧随其后的是 2001 年的Linux-VServer ,它采用了类似的机制来对文件系统、网络地址和内存等资源进行分区。Linux 社区在 2005 年进一步提出了提供操作系统级虚拟化的OpenVZ 。
也有其他尝试,但都没有一个全面到接近虚拟机的程度。
4. 了解 Linux 容器
Linux Containers ,通常称为 LXC,可能是完整容器管理器的第一个实现。它是操作系统级别的虚拟化,它提供了一种机制来限制和优先考虑多个应用程序之间的 CPU 和内存等资源。此外,它允许完全隔离应用程序的进程树、网络和文件系统。
LXC 的好处是它可以与 vanilla Linux 内核一起使用,无需任何额外的补丁。这与其前身如 Linux-VServer 和 OpenVZ 形成鲜明对比。LXC 的第一个版本有它自己的问题,包括安全性,但在后来的版本中这些问题都被克服了。 此外,还有其他替代品,如LXD 和 LXCFS 。
4.1.引入控制组 (cgroups)
LXC 现在是每个 Linux 发行版的一部分,它是在 2008 年创建的,主要基于 Google 的努力。在 LXC 用于包含进程和提供隔离的其他内核特性中,*cgroups *是一个非常重要的资源限制内核特性。
cgroups功能早在 2007年就由 Google 以进程容器的名称启动,并在不久之后并入 Linux 内核主线。基本上,cgroups为 Linux 内核中的进程隔离提供了一个统一的接口。
让我们看看我们可以定义的规则来限制进程的资源使用:
正如我们在这里看到的,cgroup通过关联代表单个内核资源(如 CPU 时间或内存)的子系统来工作。它们是分层组织的,很像 Linux 中的进程。因此,子cgroup从其父 cgroup 继承了一些属性。但与进程不同的是,cgroup 作为多个单独的层次结构存在。
我们可以将每个层次结构附加到一个或多个子系统。但是,一个进程只能属于单个层次结构中的单个cgroup。
4.2. 介绍命名空间
另一个对 LXC 提供进程隔离至关重要的 Linux 内核特性是*namespaces *——它 允许我们对内核资源进行分区,以便一组进程能够看到其他进程不可见的资源。这些资源包括进程树、主机名、用户挂载和文件名等。
这实际上是chroot的演变,它允许我们将任何目录分配为进程的系统根目录。但是,chroot存在问题,不同namespaces中的应用程序仍然可能会干扰。Linux namespaces为不同资源提供更安全的隔离,因此成为 Linux 容器的基础。
让我们看看进程命名空间是如何工作的。众所周知,Linux 中的进程模型作为单一层次结构工作,根进程在系统启动期间启动。从技术上讲,这个层次结构中的任何进程都可以检查其他进程——当然,有一定的限制。这是进程命名空间允许我们拥有多个嵌套进程树的地方:
在这里,一个进程树中的进程与兄弟或父进程树中的进程保持完全隔离。但是,父 namespaces中的进程仍然可以拥有子 namespaces中进程的完整视图。
此外,一个进程现在可以属于多个namespaces,因此可以有多个 PID。
4.3. LXC 架构
我们现在已经看到cgroup和namespaces是 Linux 容器的基础。然而,LXC通过自动化流程消除了配置cgroup和namespaces的复杂性。
此外,LXC 还使用了一些其他内核特性,例如 Apparmor 和 SELinux 配置文件,以及 Seccomp 策略。现在,让我们了解一下 LXC 的总体架构:
在这里,正如我们所见,LXC 为多个 Linux 内核包含特性(如namespaces和cgroups )提供了用户空间接口。因此,LXC 可以更轻松地相互沙箱处理进程并控制它们在 Linux 中的资源分配。
请注意,所有进程共享相同的内核空间,这使得容器与虚拟机相比非常轻量级。
5. Docker 的到来
尽管 LXC 在用户空间级别提供了简洁而强大的界面,但它仍然不是那么容易使用,也没有产生大众吸引力。这就是Docker 改变游戏规则的地方。在抽象处理内核功能的大部分复杂性的同时,它提供了一种简单的格式,用于将应用程序及其依赖项捆绑到容器中。
此外,它还支持自动构建、版本控制和重用容器。我们将在本节后面讨论其中的一些。
5.1. 与LXC的关系
Docker 项目由 Solomon Hykes 作为平台即服务公司 dotCloud 的一部分启动。它后来在 2013 年作为开源项目发布。
启动时,Docker 使用 LXC 作为其默认执行环境。然而,这是短暂的,将近一年后,*LXC 被一个内部执行环境libcontainer ***取代,该环境用 Go 编程语言编写。
请注意,虽然 Docker 已停止使用 LXC 作为其默认执行环境,但它仍然与 LXC 兼容,事实上,它还与libvert 和*systemd-nspawn *等其他隔离工具兼容。这可以通过使用执行驱动程序 API来实现,这也使 Docker 能够在非 Linux 系统上运行:
切换到libcontainer允许 Docker 自由地操作namespaces、cgroups、AppArmor 配置文件、网络接口和防火墙规则——所有这些都以可控和可预测的方式——而不依赖于 LXC 等外部包。
这使 Docker 免受 LXC 不同版本和发行版的副作用。
5.2. 与LXC相比的优势
基本上,Docker 和 LXC 都为进程隔离提供了操作系统级别的虚拟化。那么,Docker 比 LXC 好在哪里呢?让我们看看 Docker 相对于 LXC 的一些关键优势:
- 以应用为中心:LXC 专注于提供像轻量级机器一样的容器。但是,Docker 针对应用程序的部署进行了优化。事实上,Docker 容器旨在支持单个应用程序,从而导致松散耦合的应用程序。
- 可移植性:LXC 提供了一个有用的抽象来利用内核特性进行进程沙箱。但是,它不保证容器的可移植性。另一方面,Docker 定义了一种简单的格式,用于将应用程序及其依赖项捆绑到易于移植的容器中。
- 分层容器:虽然 LXC 在很大程度上对文件系统是中立的,但 Docker 使用文件系统的只读层构建容器。这些层形成了我们所说的中间图像,代表了对其他图像的变化。这使我们能够重用任何父图像来创建更专业的图像。
请注意,Docker 不仅仅是 LXC 等内核隔离特性的接口,它还具有其他几个特性,使其作为一个完整的容器管理器变得强大:
- 共享:Docker 带有一个名为 Docker Hub 的公共镜像注册表。那里有数千个有用的、社区贡献的图像,我们可以使用它们来构建我们自己的图像。
- 版本控制:Docker 使我们能够对容器进行版本控制,并允许我们在不同版本之间进行差异化、提交新版本以及回滚到以前的版本,所有这些都使用简单的命令。此外,我们可以完全追溯容器是如何组装的以及由谁组装的。
- 工具生态系统:有越来越多的工具支持与 Docker 集成以扩展其功能。其中包括像Chef 和Puppet这样的配置管理工具,像 Jenkins 和Travis 这样的持续集成工具等等。
6. 了解 Docker 容器
因此,我们已经看到 Docker 如何从 LXC 演变为提供更好的灵活性和易用性的容器管理器。我们还了解了 Docker 与 LXC 的不同之处以及它的定义特性是什么。
在本节中,我们将更详细地了解 Docker 的核心架构以及其中一些定义特性。
6.1. 架构
虽然 Docker 推出时间不长,但其核心架构已经发展了几次。例如,我们之前看到 LXC 被替换为libcontainer作为默认执行环境。还有其他变化,但我们只关注 Docker 的当前架构。
Docker 具有模块化架构,并依赖于一些关键组件来提供其服务:
这种架构允许核心组件独立发展和标准化。让我们更详细地了解这些核心组件:
- dockerd :这是 Docker 的核心部分——我们也知道它是 Docker 引擎。它包含监听 API 请求和管理 Docker 对象的 Docker 守护进程。它还提供了一个 API 接口和一个命令行接口来与 Docker 守护进程交互。
- containerd :为了管理 Docker 对象, dockerd使用containerd,它是另一个守护进程服务,可以帮助执行诸如下载图像并将它们作为容器运行之类的任务。最重要的是,它遵循标准 API 供dockerd等客户端连接。
- runc :最后, containerd需要一个组件来与内核功能交互,该组件称为runc。它提供了创建名称空间和控制组的标准机制。它基本上是重新打包libcontainer以符合 OCI 规范。
6.2. 典型的 Docker 工作流程
除了我们在上一节中看到的核心组件之外,Docker 中还有其他组件可以实现典型的工作流程。典型的工作流程可以将应用程序打包为映像,将其发布到注册表,然后将其作为容器运行,可能具有持久性。
让我们看看如何使用 Docker 实现这一点:
上面的工作流程并不完整,因为它不包括从应用程序创建映像并将其发布到注册表。但是,它非常直观,涵盖了理解典型过程所需的部分。
让我们了解一些在这里发挥作用的重要组件:
- Docker 客户端:一种通过命令行界面或 REST API 与 Docker 守护进程交互的方式。这启用了客户端-服务器架构,并允许客户端位于与守护程序不同的主机上。
- 容器和镜像:Docker 守护进程使用镜像创建容器,这是 Docker 可移植性的基本方面。映像是封装应用程序、库和其他依赖项的只读文件系统层。因此,容器是图像的实例。
- 卷:Docker 容器创建的数据是短暂的,并与容器一起消亡。在 Docker 中持久化甚至共享数据的标准机制是使用卷。还有其他可用的替代方案:“ bind mount ”和“ tmpfs mount ”。
- Docker Registry:这就是使图像可移植的原因。它是守护进程从中提取的图像存储库。我们可以创建一个公共或私有存储库,或者使用称为Docker Hub 的默认公共注册表。Docker 注册表提供了类似 git 的语义来使用。
6.3. 了解 Docker 映像
我们已经看到,镜像是 Docker 用来实例化容器的只读文件系统的层。在本节中,我们将详细探讨我们如何创建这些层以及它们实际代表什么。
创建 Docker 镜像最常见和最方便的方法是使用 Dockerfile,这是一种描述构建镜像指令的简单文本格式。
假设我们要将 Spring Boot 应用程序打包到 Docker 容器中:
正如我们在上面看到的,我们通常从像8-jdk-alpine 这样的父图像开始,并在其之上构建更多层。Dockerfile 中的每条指令都会在父镜像之上生成一层。然而,许多指令只创建临时的中间层。
我们也可以从一个空图像开始,也称为临时图像。我们经常使用临时镜像来构建其他父镜像。
请注意,最终图像中的所有图层都是只读的。当 Docker 守护进程从这个镜像实例化一个容器时,它会在顶部添加一个可写层以供正在运行的容器使用:
当然,要从这种分层的镜像结构中受益,我们应该遵循 Dockerfile 中的某些最佳实践。我们应该小心保持构建上下文尽可能少以获得更小的图像。还有一些技术,例如在*.dockerignore*中定义排除模式,使用多阶段构建来进一步减小图像大小。
6.4. 使用 Docker 进行存储
所以,我们现在知道文件系统的层在 Docker 容器中是一层一层堆叠的。但是,它们存储在哪里以及它们如何相互交互?这就是存储驱动程序出现的地方。
Docker使用存储驱动来管理镜像层和可写容器层的内容。有几个可用的存储驱动程序,如aufs、overlay、overlay2、btrfs和zfs。虽然每个存储驱动程序的实现方式各不相同,但它们都使用可堆叠的图像层和写时复制 (CoW) 策略。CoW 基本上是一种共享和复制文件以实现最大效率的策略。
存储驱动程序的选择取决于操作系统、Docker 的分布以及所需的整体性能和稳定性。对于大多数 Linux 发行版,overlay2是默认和推荐的存储驱动器r。
运行容器创建的数据在存储驱动程序的帮助下进入可写容器层。但是这种管理数据的方式效果并不好,而且容器中的数据也不再存在。Docker提供了多种选项来以更高效和持久的方式管理数据,例如volumes、bind mounts和tmpfs:
bind mounts是 Docker 中用于管理数据的最简单和最古老的方式。它们允许我们将主机上的任意文件或目录挂载到容器中。volumes也以类似的方式工作,但 Docker 在文件系统的特定部分完全管理它们。
由于非 Docker 进程无法修改文件系统的这一部分,因此volumes是管理 Docker 中持久数据的更好方法。tmpfs挂载仅保留在主机的内存中,永远不会保留在文件系统中。
6.5.与 Docker 联网
Docker 容器可以相互连接,也可以与非 Docker 工作负载连接,而无需明确了解其他工作负载的类型。这是可能的,因为Docker 的网络子系统是完全可插拔的。
我们可以使用几个网络驱动程序来提供此网络功能:
- bridge:我们可以使用桥驱动程序在独立容器中运行的应用程序之间进行通信
- host:host驱动在我们想去掉容器和宿主之间的网络隔离,直接使用宿主网络的时候很有用
- overlay:我们可以使用overlay驱动将多个 Docker 守护进程连接在一起——例如,让 swarm 服务相互通信
- macvlan:macvlan驱动程序使我们能够为容器分配 MAC 地址,以便 Docker 守护进程可以通过它们的 MAC 地址将流量路由到容器
除此之外,我们可以使用none驱动程序禁用容器的网络。此外,我们还可以安装和使用第三方网络插件或编写网络驱动插件来创建我们自己的自定义驱动程序。
如果我们不指定驱动,bridge驱动就是 Docker 中默认的网络驱动。默认情况下有一个可用的桥接网络,这是 Docker 启动容器时使用的,除非另有说明。
我们还可以创建自己的桥接网络并使用它启动容器。
让我们看看桥接网络是如何工作的:
在这里,我们可以看到,Docker 默认创建了一个桥接网络“docker0”,并通过“vethxx”将所有容器网络链接到它。连接到这个默认桥接网络的容器可以通过 IP 地址相互通信。此外,桥接网络“docker0”连接到主机网络“eth0”,从而提供到外部网络的连接。
7. 标准化工作
我们已经看到进程隔离的概念是如何从chroot发展到现代 Docker 的。如今,除了 LXC 和 Docker 之外,还有其他几种替代方案可供选择。来自 Apache Foundation 的 Mesos Containerizer 和来自 CoreOS 的 rkt 是最受欢迎的。
这些容器技术之间的互操作性至关重要的是一些核心组件的标准化。我们已经看到 Docker 如何将自己转变为模块化架构,从而受益于containerd和runc等标准化。
在本节中,我们将讨论一些推动容器技术标准化的行业合作。
7.1. 开放容器倡议
Open Container Initiative (OCI) 由 Docker 和其他行业领导者于 2015 年作为 Linux 基金会项目成立。它推动了围绕容器格式和运行时的行业标准。他们目前维护两个规范:一个镜像规范(image-spec)和一个运行时规范(runtime-spec)。
基本上,OCI 实现会下载 OCI 映像,然后将该映像解压缩到 OCI 运行时文件系统包中。然后,该 OCI 运行时包可以由 OCI 运行时运行。有一个OCI 运行时的标准参考实现,称为runc,最初由 Docker 开发。还有其他几个符合 OCI 的运行时,例如 Kata Containers。
7.2. 云原生计算基金会
云原生计算基金会 (CNCF) 也成立于 2015 年,是一个 Linux 基金会项目,旨在推进容器技术并使行业围绕其发展进行调整。它维护着许多托管子项目,包括containerd 、Kubernetes 、Prometheus 、Envoy 、CNI 和CoreDNS ,仅举几例。
在这里,containerd是一个毕业的 CNCF 项目,旨在提供一个行业标准的核心容器运行时,可用作 Linux 和 Windows 的守护进程。Container Network Interface (CNI) 是一个孵化中的 CNCF 项目,最初由 CoreOS 开发,由用于编写插件以配置网络接口的规范和库组成。
8. 容器编排及其他
容器提供了一种以平台中立的方式打包应用程序并在任何地方放心运行的便捷方式。然而,随着容器数量的增加,管理它们成为另一个挑战。管理数百个容器的部署、扩展、通信和监控并非易事。
这就是Kubernetes 等容器编排技术开始流行的地方。Kubernetes 是另一个毕业的 CNCF 项目,最初是在 Google 开始的。Kubernetes 为容器的部署、管理和扩展提供自动化。它通过容器运行时接口 (CRI) 支持多个符合 OCI 的容器运行时,例如Docker 或CRI-O 。
另一个挑战与连接、保护、控制和观察容器中运行的许多应用程序有关。虽然可以设置单独的工具来解决这些问题,但这绝对不是微不足道的。这就是像**Istio 这样的服务网格出现的地方——它是一个专用的基础设施层,控制应用程序彼此共享数据的方式**。