在同一操作系统中运行的不同微服务存在需要不同的、可能存在冲突的动态链接库版本或具有不同的环境要求。

当一个系统由少量应用程序组成时,可以为每个应用程序分配一个专用的虚拟机并在各自的操作系统中运行每个应用程序。但是随着微服务变得更小并且它们的数量开始增长,如果你想保持低硬件成本并且不浪费资源,你可能无法为每个微服务提供自己的虚拟机。

这不仅仅是浪费硬件资源的问题——每个虚拟机通常都需要单独配置和管理,这意味着运行更多数量的虚拟机也会导致更高的人员配备,更复杂的自动化系统的需求。由于转向微服务架构,其中系统由数百个部署的应用程序实例组成,因此需要虚拟机的替代方案。容器就是这样的选择。

容器和虚拟机的对比

大多数开发和运营团队现在更喜欢使用容器,而不是使用虚拟机来隔离单个微服务(或一般的软件流程)的环境。它们允许您在同一台主机上运行多个服务,同时保持它们彼此隔离。与虚拟机类似,但开销要少得多。

与每个运行具有多个系统进程的单独操作系统的 VM 不同,在容器中运行的进程在现有主机操作系统中运行。因为只有一个操作系统,所以不存在重复的系统进程。尽管所有应用程序进程都在同一个操作系统中运行,但它们的环境是隔离的,尽管不如在单独的 VM 中运行它们时那么好。对于容器中的进程,这种隔离使得计算机上看起来没有其他进程存在。您将在接下来的几节中了解这是如何实现的,但首先让我们更深入地了解容器和虚拟机之间的区别。

比较容器和虚拟机的开销

与虚拟机相比,容器要轻得多,因为它们不需要单独的资源池或任何额外的操作系统级进程。虽然每个虚拟机通常运行自己的一组系统进程,除了用户应用程序自己的进程消耗的资源之外,这还需要额外的计算资源,而容器只不过是运行在现有主机操作系统中的一个孤立进程,只消耗资源应用程序消耗,几乎没有其他开销。

下图显示了两台裸机计算机,一台运行两台虚拟机,另一台运行容器。后者有额外容器的空间,因为它只运行一个操作系统,而第一个运行三个——一个主机和两个客户操作系统。
image.png
由于 VM 的资源开销,您经常将多个应用程序分组到每个 VM 中。您无法为每个应用程序专门使用一个虚拟机。但是容器不会引入任何开销,这意味着您可以为每个应用程序创建一个单独的容器。事实上,你永远不应该在同一个容器中运行多个应用程序,因为这使得管理容器中的进程变得更加困难。此外,所有现有的处理容器的软件,包括 Kubernetes 本身,都是在容器中只有一个应用程序的前提下设计的。

比较容器和虚拟机的启动时间

除了较低的运行时开销外,容器还可以更快地启动应用程序,因为只需要启动应用程序进程本身。不需要像启动新虚拟机时那样首先启动其他系统进程。

容器和虚拟机的隔离对比

你会同意容器在资源使用方面显然更好,但也有缺点。当您在虚拟机中运行应用程序时,每个虚拟机都运行自己的操作系统和内核。在这些虚拟机下面是虚拟机管理程序(可能还有一个额外的操作系统),它将物理硬件资源分成更小的虚拟资源集,每个虚拟机中的操作系统可以使用这些虚拟资源。如下图所示,在这些虚拟机中运行的应用程序对虚拟机中的客户操作系统内核进行系统调用(sys-calls),然后内核在虚拟 CPU 上执行的机器指令通过虚拟机管理程序。
image.png
另一方面,容器都在主机操作系统中运行的单个内核上进行系统调用。这个单一内核是唯一在主机 CPU 上执行指令的内核。 CPU 不需要像处理虚拟机那样处理任何类型的虚拟化。

从下图了解在裸机上运行三个应用程序、在两个单独的虚拟机中运行它们或在三个容器中运行它们之间的区别。
image.png在第一种情况下,所有三个应用程序都使用相同的内核,并且根本不是孤立的。在第二种情况下,应用程序 A 和 B 在同一个 VM 中运行,因此共享内核,而应用程序 C 与其他两个完全隔离,因为它使用自己的内核。它只与前两个共享硬件。

第三种情况显示了在容器中运行的相同的三个应用程序。虽然它们都使用相同的内核,但它们彼此隔离,完全不知道其他人的存在。隔离是由内核本身提供的。每个应用程序只看到物理硬件的一部分,并将自己视为操作系统中运行的唯一进程,尽管它们都运行在同一个操作系统中。

了解容器隔离的安全含义

使用虚拟机而不是容器的主要优势是它们提供的完全隔离,因为每个虚拟机都有自己的 Linux 内核,而容器都使用相同的内核。这显然会带来安全风险。如果内核中存在错误,则一个容器中的应用程序可能会使用它来读取其他容器中的应用程序的内存。如果应用程序在不同的虚拟机中运行,因此只共享硬件,则此类攻击的可能性要低得多。当然,完全隔离只能通过在单独的物理机上运行应用程序来实现。

此外,容器共享内存空间,而每个 VM 使用自己的内存块。因此,如果您不限制容器可以使用的内存量,这可能会导致其他容器内存不足或导致它们的数据被换出到磁盘。

了解什么支持容器以及什么支持虚拟机

虽然虚拟机是通过 CPU 中的虚拟化支持和主机上的虚拟化软件启用的,但容器是由 Linux 内核本身启用的。稍后您可以自己尝试容器技术时,您将了解它们。您需要为此安装 Docker,所以让我们了解它如何融入容器故事。

介绍 Docker 容器平台

虽然容器技术已经存在了很长时间,但它们只是随着 Docker 的兴起而广为人知。 Docker 是第一个让它们在不同计算机之间轻松移植的容器系统。它简化了将应用程序及其所有库和其他依赖项(甚至整个 OS 文件系统)打包成一个简单、可移植的包的过程,该包可用于在任何运行 Docker 的计算机上部署应用程序。

介绍 containers、images 和 registries

Docker 是一个用于打包、分发和运行应用程序的平台。如前所述,它允许您将应用程序与其整个环境一起打包。这可以只是应用程序所需的几个动态链接库,也可以是操作系统通常附带的所有文件。 Docker 允许您通过公共存储库将此包分发到任何其他支持 Docker 的计算机。
image.png

  • Images ——容器镜像是您将应用程序及其环境打包到其中的东西。就像一个 zip 文件或一个 tarball。它包含应用程序将使用的整个文件系统和其他元数据,例如执行镜像时要运行的可执行文件的路径、应用程序监听的端口以及有关镜像的其他信息。
  • Registries——仓库是容器镜像的存储库,可以在不同的人和计算机之间交换镜像。构建镜像后,您可以在同一台计算机上运行它,也可以将镜像推送(上传)到仓库,然后将其拉取(下载)到另一台计算机。某些仓库是公开的,允许任何人从中提取镜像,而其他仓库是私有的,只有具有所需身份验证凭据的个人、组织或计算机才能访问。
  • Containers——容器是从容器镜像中实例化的。运行中的容器是在宿主机操作系统中运行的正常进程,但它的环境与宿主机和其他进程的环境是隔离的。容器的文件系统源自容器镜像,但也可以将其他文件系统挂载到容器中。容器通常是资源受限的,这意味着它只能访问和使用已分配给它的 CPU 和内存等资源量。

    构建、分发和运行容器镜像

    要了解容器、镜像和仓库如何相互关联,让我们看看如何构建容器镜像、通过仓库分发它并从镜像创建运行容器。这三个过程如下图所示:
    image.png
    image.png
    image.png
    由于应用程序环境与主机环境解耦,因此可以在任何计算机上运行应用程序。

    了解应用程序看到的环境

    当您在容器中运行应用程序时,它会准确地看到您捆绑到容器镜像中的文件系统内容,以及您挂载到容器中的任何其他文件系统。应用程序无论是在您的笔记本电脑上还是在成熟的生产服务器上运行,都会看到相同的文件,即使生产服务器使用完全不同的 Linux 发行版。应用程序通常无法访问主机操作系统中的文件,因此服务器是否拥有与您的开发计算机完全不同的一组库已经无关紧要了。

例如,如果您使用整个 Red Hat Enterprise Linux (RHEL) 操作系统的文件打包您的应用程序然后运行它,该应用程序将认为它在 RHEL 中运行,无论您是在基于 Fedora 还是在 Debian 上运行它。安装在主机上的 Linux 发行版无关紧要。唯一可能重要的是内核版本和它加载的内核模块。稍后,我会解释原因。

这类似于通过创建一个新 VM、在其中安装操作系统和您的应用程序来创建 VM 镜像,然后分发整个 VM 镜像,以便其他人可以在不同的主机上运行它。 Docker 实现了相同的效果,但它不是使用虚拟机进行应用程序隔离,而是使用 Linux 容器技术来实现(几乎)相同级别的隔离。

了解镜像层

与虚拟机镜像不同,虚拟机镜像是安装在 VM 中的操作系统所需的整个文件系统的大块,容器镜像由通常小得多的层组成。这些层可以在多个镜像之间共享和重用。这意味着如果镜像的某些层已经作为包含相同层的另一个镜像的一部分下载到主机,则只需要下载镜像的中另外那些不存在的层。

镜像层使镜像分发非常有效,也有助于减少镜像的存储空间。 Docker 只存储每一层一次。如图所示,从两个包含相同层的镜像创建的两个容器使用相同的文件。
image.png
图中显示容器 A 和 B 共享一个镜像层,这意味着应用程序 A 和 B 读取了一些相同的文件。此外,它们还与容器 C 共享底层。但是如果所有三个容器都可以访问相同的文件,那么它们之间如何完全隔离呢?应用程序 A 对存储在共享层中的文件所做的更改对应用程序 B 不可见吗?确实不可见,这就是为什么?

文件系统由写时复制 (CoW) 机制隔离。容器的文件系统由容器镜像中的只读层和堆叠在顶部的附加读/写层组成。当容器 A 中运行的应用程序更改其中一个只读层中的文件时,整个文件将被复制到容器的读/写层中,并且文件内容在那里被更改。由于每个容器都有自己的可写层,因此对共享文件的更改在任何其他容器中都不可见。

当您删除一个文件时,它仅在读/写层中被标记为已删除,但它仍然存在于下面的一个或多个层中。接下来是删除文件永远不会减小镜像的大小。

即使是看似无害的操作(例如更改文件的权限或所有权)也会导致在读/写层中创建整个文件的新副本。如果对大文件或许多文件执行此类操作,图像大小可能会显着增大。

了解容器镜像的可移植性限制

理论上,基于 Docker 的容器镜像可以在任何运行 Docker 的 Linux 计算机上运行,但有一点需要注意,因为容器没有自己的内核。如果容器化应用程序需要特定的内核版本,它可能无法在每台计算机上运行。如果计算机运行的是不同版本的 Linux 内核或未加载所需的内核模块,则应用程序无法在其上运行。下图说明了这种情况:
image.png
容器 B 需要特定的内核模块才能正常运行。该模块在第一台计算机的内核中加载,但在第二台计算机中不加载。您可以在第二台计算机上运行容器镜像,但它会在尝试使用缺少的模块时中断。

这不仅仅是关于内核及其模块。还应该清楚的是,为特定硬件架构构建的容器化应用程序只能在具有相同架构的计算机上运行。您不能将针对 x86 CPU 架构编译的应用程序放入容器中,并期望在基于 ARM 的计算机上运行它,因为那里有 Docker。为此,您需要一个虚拟机来模拟 x86 架构。

介绍 Docker 替代方案和开放容器倡议(Container Initiative)

Docker 是第一个使容器成为主流的容器平台。我希望我已经明确表示 Docker 本身不提供进程隔离。容器的实际隔离发生在 Linux 内核级别,使用它提供的机制。 Docker 只是让这些机制的使用变得容易,并允许您将容器镜像分发到不同的主机。

介绍开放容器倡议 (OCI)

在 Docker 成功之后,Open Container Initiative (OCI) 应运而生,旨在围绕容器格式和运行时创建开放的行业标准。 Docker 是该计划的一部分,其他容器运行时和许多对容器技术感兴趣的组织也是如此。

OCI 成员创建了 OCI 镜像格式规范,它规定了容器镜像的标准格式,以及 OCI 运行时规范,它定义了容器运行时的标准接口,旨在标准化容器的创建、配置和执行。

介绍容器运行时接口 (CRI) 及其实现 (CRI-O)

本书重点介绍使用 Docker 作为 Kubernetes 的容器运行时,因为它最初是 Kubernetes 唯一支持的,并且仍然是最广泛使用的。但是 Kubernetes 现在通过容器运行时接口 (CRI) 支持许多其他容器运行时。

CRI 的一种实现是 CRI-O,它是 Docker 的轻量级替代方案,允许您将任何符合 OCI 的容器运行时与 Kubernetes 结合使用。符合 OCI 的运行时示例包括 rkt(发音为 Rocket)、runC 和 Kata Containers。