Docker 揭秘
介绍
基本架构
容器隔离
Docker的机遇与挑战
常见问题
结论
介绍
自 2013 年开源以来,Docker 已成为最受欢迎的技术之一。众多公司为其做出贡献,大量用户正在使用和采用它。但它为何如此受欢迎?它提供了哪些前所未有的功能?在这篇博文中,我们将深入探究 Docker 的内部机制,了解它的工作原理。
本文第一部分将简要概述 Docker 容器的基本架构概念。第二部分将介绍构成 Docker 容器隔离基础的四个主要功能:1) cgroups,2) 命名空间,3) 可堆叠镜像层和写时复制,以及 4) 虚拟网桥。第三部分将讨论使用容器和 Docker 时面临的机遇和挑战。最后,我们将解答一些关于 Docker 的常见问题。
基本架构
“Docker 是一个开源项目,可以自动在软件容器内部署应用程序。” -维基百科
人们在谈论操作系统级虚拟化时通常会提到容器。操作系统级虚拟化是指操作系统内核允许多个隔离的应用程序实例共存的一种方法。容器有很多实现方式,其中之一就是 Docker。
Docker 基于镜像启动容器。镜像就像一张蓝图,定义了容器创建时应该包含的内容。定义镜像的常用方法是通过Dockerfile。Dockerfile 包含如何逐步构建镜像的说明(不用担心,稍后您将更深入地了解其内部工作原理)。例如,以下 Dockerfile 将从包含 OpenJDK 的镜像开始,在其中安装 Python 3,将 复制到requirements.txt
镜像内部,然后从需求文件中安装所有 Python 包。
FROM openjdk:8u212-jdk-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
Python3=3.5.3-1 \
Python3-pip=9.0.1-2+deb9u1 \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt requirements.txt
RUN pip3 install --upgrade -r requirements.txt
镜像通常存储在名为 Docker registries的镜像仓库中。Dockerhub 是一个公共的 Docker registries。为了下载镜像并启动容器,您需要一个 Docker主机。Docker 主机是一台运行 Docker守护进程(守护进程是一个始终运行的后台进程,等待执行任务)的Linux 机器。
要启动容器,您可以使用 Docker 客户端,它会向 Docker 守护进程提交必要的指令。如果 Docker 守护进程在本地找不到请求的镜像,它还会与 Docker 镜像仓库通信。下图展示了Docker 的基本架构:
需要注意的是,Docker 本身并不提供实际的容器化功能,而只是利用了 Linux 中现有的功能。让我们深入了解一下技术细节。
容器隔离
Docker 通过四个主要概念的组合来实现不同容器的隔离:1)cgroups,2)命名空间,3)可堆叠镜像层和写时复制,以及 4)虚拟网桥。在接下来的小节中,我们将详细解释这些概念。
控制组(cgroups)
Linux 操作系统管理可用的硬件资源(内存、CPU、磁盘 I/O、网络 I/O 等),并为进程提供便捷的访问和使用方式。例如,Linux 的 CPU 调度程序会确保每个线程最终都能在 CPU 核心上获得一些时间,这样就不会有应用程序陷入等待 CPU 时间的困境。
控制组 (cgroups) 是一种将资源子集分配给特定进程组的方法。例如,它可以用于确保即使您的 CPU 因 Python 脚本而异常繁忙,您的 PostgreSQL 数据库仍然能够获得专用的 CPU 和内存。下图以 4 个 CPU 核心和 16 GB 内存为例说明了这一点。
在 zeppelin-grp 中启动的所有 Zeppelin 笔记本将仅使用核心 1 和 2,而 PostgreSQL 进程共享核心 3 和 4。内存也是如此。Cgroup 是容器隔离的重要组成部分,因为它们可以实现硬件资源隔离。
命名空间
cgroup 隔离硬件资源,而命名空间则隔离并虚拟化系统资源。可以虚拟化的系统资源示例包括进程 ID、主机名、用户 ID、网络访问、进程间通信和文件系统。为了更清楚地理解这一点,我们首先深入研究一个进程 ID (PID) 命名空间的示例,然后再简要讨论其他命名空间。
PID 命名空间
Linux 操作系统将进程组织成所谓的进程树。树根是操作系统启动后第一个启动的进程,其 PID 为 1。由于只能存在一个进程树,所有其他进程(例如 Firefox、终端仿真器、SSH 服务器)都需要由该进程(直接或间接)启动。由于该进程初始化所有其他进程,因此通常被称为init进程。
下图说明了典型进程树的各个部分,其中 init 进程启动了日志服务 ( syslogd
)、调度程序 ( cron
) 和登录 shell ( bash
):
1 /sbin/init
+-- 196 /usr/sbin/syslogd -s
+-- 354 /usr/sbin/cron -s
+-- 391 login
+-- 400 bash
+-- 701 /usr/local/bin/pstree
在这棵树中,每个进程都可以看到其他所有进程,并根据需要发送信号(例如,请求进程停止)。使用 PID 命名空间可以虚拟化特定进程及其所有子进程的 PID,使其认为自己拥有 PID 1。这样一来,除了自己的子进程之外,它也无法看到任何其他进程。下图说明了不同的 PID 命名空间如何隔离两个 Zeppelin 进程的进程子树。
1 /sbin/init
|
+ ...
|
+-- 506 /usr/local/zeppelin
1 /usr/local/zeppelin
+-- 2 interpreter.sh
+-- 3 interpreter.sh
+-- 511 /usr/local/zeppelin
1 /usr/local/zeppelin
+-- 2 java
文件系统命名空间
命名空间的另一个用例是 Linux 文件系统。与 PID 命名空间类似,文件系统命名空间虚拟化并隔离树的各个部分——在本例中是文件系统树。Linux 文件系统以树的形式组织,并且有一个根,通常称为/
。
为了实现文件系统级别的隔离,命名空间会将文件系统树中的节点映射到该命名空间内的虚拟根目录。在浏览该命名空间内的文件系统时,Linux 不允许超出虚拟根目录的范围。下图展示了一个文件系统的一部分,该文件系统的文件夹中包含多个“虚拟”文件系统根目录/drives/xx
,每个根目录包含不同的数据。
其他命名空间
除了 PID 和文件系统命名空间之外,还有其他类型的命名空间。Docker 允许您利用它们来实现所需的隔离程度。例如,用户命名空间允许您将容器内的用户映射到容器外部的其他用户。这可以用来将容器内的 root 用户映射到容器外部的非 root 用户,这样容器内的进程在容器内部就像管理员一样,但在容器外部没有任何特殊权限。
可堆叠图像层和写入时复制
现在,我们已经更详细地了解了硬件和系统资源隔离如何帮助我们构建容器,接下来我们将研究 Docker 存储镜像的方式。如前所述,Docker 镜像就像容器的蓝图。它包含启动其所包含应用程序所需的所有依赖项。但是这些依赖项是如何存储的呢?
Docker 将镜像持久化到可堆叠的层中。每层包含对前一层的更改。例如,如果您先安装 Python,然后复制 Python 脚本,则您的镜像将包含两个额外的层:一个包含 Python 可执行文件,另一个包含脚本。下图展示了基于 Ubuntu 的 Zeppelin、Spring 和 PHP 镜像。
为了避免 Ubuntu 存储三次,层是不可变且共享的。Docker 使用写时复制技术,仅在文件发生更改时才进行复制。
当基于镜像启动容器时,Docker 守护进程会提供该镜像中包含的所有层,并将其放置在一个与该容器隔离的文件系统命名空间中。可堆叠层、写时复制和文件系统命名空间的组合,使您可以完全独立于 Docker 主机上“安装”的内容运行容器,而不会浪费大量空间。这也是容器比虚拟机更轻量级的原因之一。
虚拟网桥
现在我们知道了如何隔离硬件资源(cgroup)和系统资源(命名空间),以及如何为每个容器提供一组预定义的依赖项,使其独立于主机系统(镜像层)。最后一个构建块,虚拟网桥,帮助我们隔离容器内的网络堆栈。
网桥是一种计算机网络设备,它能将多个通信网络或网段创建成一个聚合网络。我们来看看连接两个网段(LAN 1 和 LAN 2)的物理网桥的典型设置:
通常,Docker 主机上只有有限数量的网络接口(例如物理网卡),并且所有进程都需要以某种方式共享对这些接口的访问权限。为了隔离容器之间的网络,Docker 允许为每个容器创建一个虚拟网络接口。然后,它会将所有虚拟网络接口连接到主机网络适配器,如下图所示:
本例中的两个容器eth0
在其网络命名空间内拥有各自的网络接口。它们被映射到 Docker 主机上相应的虚拟网络接口veth0
。veth1
虚拟网桥docker0
将主机网络接口连接eth0
到所有容器网络接口。
Docker 在配置桥接器方面为您提供了很大的自由,以便您可以仅向外界公开特定端口,或者直接将两个容器连接在一起(例如数据库容器和需要访问它的应用程序),而无需向外部公开任何内容。
连接点
利用前面几节中描述的技术和特性,我们现在能够“容器化”我们的应用程序。虽然可以使用 cgroup、命名空间、虚拟网络适配器等手动创建容器,但 Docker 是一个让创建容器变得便捷且几乎零开销的工具。它处理所有手动、配置密集型任务,使软件开发人员(而不仅仅是 Linux 专家)能够轻松使用容器。
事实上,一位 Docker 工程师做了一个精彩的演讲,演示了如何手动创建容器,并解释了我们在本小节中介绍的细节。
Docker的机遇与挑战
现在,很多人每天都在使用 Docker。容器带来了哪些好处?Docker 提供了哪些以前没有的功能?毕竟,容器化应用程序所需的一切功能在 Linux 中早就有了,不是吗?
让我们来看看迁移到基于容器的设置时会遇到的一些机遇(当然,这不是详尽的清单)。当然,采用 Docker 不仅有机遇,也有一些挑战可能会给你带来困难。我们也将在本节中列举一些。
机会
Docker 支持 DevOps。DevOps理念致力于连接开发和运维活动,使开发人员能够自行部署应用程序。您构建,您运行。通过基于 Docker 的部署,开发人员可以直接将他们的工件与所需的依赖项一起交付,而无需担心依赖项冲突。此外,它还允许开发人员编写更复杂的测试并更快地执行它们,例如,在另一个容器中创建一个真实的数据库,并在几秒钟内将其链接到笔记本电脑上的应用程序(参见Testcontainers)。
容器提高了部署的可预测性。不再需要“在我的机器上运行”。不再需要因为一台机器安装了不同版本的 Java 而导致应用程序部署失败。只需构建一次镜像,即可在任何地方运行(前提是安装了 Linux 内核和 Docker)。
采用率高,并与众多知名集群管理器良好集成。使用 Docker 的一大优势在于其周边的软件生态系统。如果您计划进行大规模运营,那么您将不得不使用其中任何一个集群管理器。无论您是决定让其他人管理您的部署(例如 Google Cloud、Docker Cloud、Heroku、AWS 等),还是想要维护自己的集群管理器(例如 Kubernetes、Nomad、Mesos),都有大量的解决方案可供选择。
轻量级容器支持快速故障恢复或自动扩展。想象一下,你正在运营一家网店。圣诞节期间,人们会开始访问你的 Web 服务器,而你当前的配置可能在容量方面不够用。假设你拥有足够的空闲硬件资源,启动几个容器来托管你的 Web 应用只需几秒钟。此外,只需将容器迁移到新机器即可恢复故障机器。
挑战
容器给人一种虚假的安全感。在保护应用程序安全方面存在许多陷阱。认为将应用程序放入容器中就能确保安全的想法是错误的。容器本身并不能保证任何东西的安全。如果有人入侵了你的容器化 Web 应用程序,他可能会被锁定在命名空间中,但根据设置,有几种方法可以摆脱这种锁定。请注意这一点,并像不使用 Docker 一样,在安全方面投入尽可能多的精力。
Docker 让人们可以轻松部署半成品解决方案。选择你最喜欢的软件,在 Google 搜索栏中输入它的名称,并加上“Docker”。你可能会在 Dockerhub 上找到至少一个甚至几十个包含你软件的公开镜像。那么,为什么不直接运行它并尝试一下呢?会出什么问题呢?很多事情都可能出错。当软件被放入容器中时,看起来光鲜亮丽,人们甚至不再关注里面的实际软件和配置。
胖容器反模式会导致部署文件庞大且难以管理。我见过一些 Docker 镜像,要求你在容器中为不同的应用程序公开超过 20 个端口。Docker 的理念是,一个容器应该只做一件事,你应该将它们组合起来,而不是让它们变得更重。如果你最终把所有工具都放在一个容器里,你就失去了所有优势,里面可能包含不同版本的 Java 或 Python,最终会得到一个 20 GB 大小、难以管理的镜像。
调试某些情况可能仍然需要深厚的 Linux 知识。你可能听过同事说 XXX 不能与 Docker 一起使用。发生这种情况的原因有很多。如果应用程序不能正确区分它们绑定的网络接口和它们通告的网络接口,则某些应用程序在桥接网络命名空间内运行时会出现问题。另一个问题可能与 cgroup 和命名空间有关,其中共享内存的默认设置与你最喜欢的 Linux 发行版上的不同,从而导致在容器内运行时出现 OOM 错误。然而,大多数问题实际上与 Docker 无关,而是因为应用程序设计不当,而且这种情况并不常见。尽管如此,它们仍然需要对 Linux 和 Docker 的工作原理有更深入的了解,而并非每个 Docker 用户都具备这种了解。
常见问题
问:容器和虚拟机有什么区别?
我们先不深入探讨虚拟机 (VM) 的架构细节,先从概念层面来看一下两者之间的主要区别。容器在操作系统内部运行,使用内核特性来隔离应用程序。而虚拟机则需要在操作系统内部运行的虚拟机管理程序 (hypervisor)。虚拟机管理程序会创建虚拟硬件,供其他操作系统访问。下图比较了基于虚拟机的应用程序设置和基于容器的设置。
可以看出,基于容器的设置开销较小,因为它不需要为每个应用程序额外部署操作系统。这是因为容器管理器(例如 Docker)直接使用操作系统功能,以更轻量级的方式隔离应用程序。
这是否意味着容器优于虚拟机?这得看情况。两种技术都有各自的用例,有时甚至可以将它们结合起来,在虚拟机中运行容器管理器。目前有很多博客文章讨论了这两种解决方案的优缺点,所以我们现在就不详细讨论了。重要的是要理解它们之间的区别,不要将容器视为某种“轻量级虚拟机”,因为它们在内部是不同的。
问:容器里有东西吗?
回顾容器的定义以及我们目前所学的知识,我们可以肯定地说,使用 Docker 部署隔离的应用程序是可行的。通过将控制组和命名空间与可堆叠的镜像层、虚拟网络接口以及虚拟网桥相结合,我们拥有了完全隔离应用程序所需的所有工具,甚至可能将进程锁定在容器中。然而,现实表明这并非易事。首先,需要正确配置;其次,您会发现,大多数情况下,完全隔离的容器并没有多大意义。
最终,您的应用程序需要以某种方式产生一些副作用(将数据持久化到磁盘、通过网络发送数据包等等)。因此,您最终会通过转发网络流量或将主机卷挂载到文件系统命名空间来破坏隔离。此外,您不需要使用所有可用的命名空间功能。虽然网络、PID 和文件系统命名空间功能默认启用,但使用用户 ID 命名空间需要您添加额外的配置选项。
因此,仅仅将某些东西放入容器中就能保证其安全的想法是错误的。例如,AWS 使用名为Firecracker的轻量级虚拟机引擎来安全地执行多租户短期工作负载。
问:容器能让我的生产环境更加稳定吗?
有些人认为容器能够提高稳定性,因为它们可以隔离错误。虽然这在某种程度上是正确的,因为正确配置的命名空间和 cgroup 可以限制单个进程异常造成的副作用,但在实践中,有一些事情需要注意。
如前所述,容器只有在正确配置的情况下才能发挥作用,并且大多数情况下你希望它们与系统的其他部分进行交互。因此,可以说容器有助于提高部署的稳定性,但你应该始终记住,它并不能保护你的应用程序免于故障。
结论
Docker 是一项非常棒的技术,它能够以可重复且隔离的方式独立部署应用程序。与以往一样,Docker 并没有万能的解决方案,在选择 Docker 作为工具之前,您应该充分了解自身在安全性、性能、可部署性、可观察性等方面的需求。
幸运的是,Docker 周围已经有一个强大的工具生态系统。可以根据需要添加服务发现、容器编排、日志转发、加密和其他用例的解决方案。我想引用我最喜欢的一条推文来结束这篇文章:
“将有问题的软件放入 Docker 容器并不会使其问题变得更好。” - @sadserver
如果您喜欢这篇文章,您可以在 ko-fi 上支持我。
文章来源:https://dev.to/frosnerd/docker-demystified-27kl