Docker 指南 - 第一部分

2025-05-24

Docker 指南 - 第一部分

概括

我在 Github 上启动了一个名为“指南”的新项目。它是一个由我撰写的技术指南库。我创建这个项目的原因是,当我撰写关于某个主题的文章时,我能学到更多。它也有助于巩固或澄清概念。我会持续更新这些指南,并且永远不会更新。我的目标是使它们尽可能准确。但是,我并不总是能做到 100% 准确,我会依靠社区的反馈来改进或修正适用之处。

我开始写的第一篇指南是关于 Docker 的。我选择 Docker 作为我的第一篇指南,是因为我认为在 2018 年所有热门技术中,Docker 是所有参与软件开发/部署的人最感兴趣、最重要、最适用的技术之一。因此,无论您是编写软件(前端或后端),还是管理软件部署的运维方面,Docker 都能帮助您做得更好。

这篇文章是我Docker 指南的第一部分,概述。在第一部分中,我将讨论 Docker 的背景及其相关技术。

请在以下位置查找原始作品:

所有未来的更新都将发布到 github 项目存储库。


概述

了解 Docker 的背景以及最终导致其成功的因素至关重要。因此,在本指南中,我将阐述一些与应用程序生命周期挑战相关的主题,以及 Docker 如何帮助我们应对这些挑战。首先,我将讨论与应用程序生命周期相关的一些问题,以及为什么我们需要更好的应用程序运行方式。其次,我将讨论 Docker 及其相关技术,以及 Docker 如何帮助我们更好地运行和管理应用程序。

软件部署的挑战

在本节中,我将讨论与部署和运行应用程序相关的一些挑战。

计算机的本质在于能够运行应用程序。在虚拟化之前,通常会有一个物理计算机系统,例如服务器,所有应用程序都安装在其上。每个物理计算机系统都有自己的操作系统 (OS)。所有应用程序都需要与其所安装的操作系统兼容。操作系统需要通过拥有多个运行时、框架、库、配置和安全实现来支持多个应用程序。此外,操作系统还需要是所有应用程序都支持的特定版本。操作系统上的任何补丁、升级或安装都有可能破坏其他应用程序。尽管所有应用程序都在其自己的进程中运行,但与每个进程关联的默认隔离不足以确保操作系统及其底层应用程序的完整性和可靠性。操作系统不再是一个支持所有应用程序运行的可靠、干净的运行时,而是变成了一个大泥球。这基本上意味着操作系统的内聚性降低,并且与其他依赖项的耦合度越来越高,导致系统变得像“意大利面条”一样,缺乏明确的关注点分离。因此,当我们思考将应用程序投入生产环境的实际过程时,就会开始意识到它实际上有多么困难。当支持多个部署环境(开发、测试、预发布)时,挑战就更大了。因此,我们面临的挑战是如何以自动化、一致且无错误的方式将应用程序从开发环境一路迁移到各种部署环境。这一挑战可以进一步分解为更具体的挑战,即分发、安装和管理。我称之为“DIM(分发、安装、管理)”模型。在下文中,我将讨论DIM及其相关问题。

我想指出的是,以下挑战与规模成正比。对于一个拥有几个小型应用程序的小团队来说,以下挑战可能看起来根本不算什么。然而,随着部署架构的复杂性不断增长,与DIM相关的复杂性也会随之增长。复杂性要么是设计使然,要么是偶然造成的。如果是设计使然,那是因为你对问题的理解还不够透彻。通过寻找更好的DIM实现方法,我们可以通过更好的实践和设计来帮助降低整体复杂性。

分配

应用程序的分发既复杂又困难。需要确定如何将应用程序从开发阶段交付到测试阶段、预发布阶段和生产阶段的完整流程或工作流程。每个软件开发和运营环境的流程都有所不同。以下是一些应该思考的问题:

  • 应用程序将如何打包?
  • 应用程序是否需要可被发现?
  • 需要下载应用程序吗?
  • 分发是否只是简单地将文件复制到下一个目的地?
  • 分发会带来哪些安全隐患?例如,在应用程序分发过程中,是否有敏感数据需要分发?应用程序和/或配置是否应该加密?是否需要哈希码来确保分发应用程序的完整性?
  • 不同应用程序的更新和补丁是如何分发的?
  • 应用程序需要部署到多少个环境?
  • 可以使用持续集成 (CI) 和持续交付 (CD) 吗?如果可以,具体是什么样的?需要哪些技能?
  • 我们如何进行变更控制?如何让变更控制更简单、更容易?

综合考虑所有因素,将应用程序从开发人员的机器(实际上应该是从分布式版本控制系统)迁移到测试或生产环境,这一简单的任务可能会变得令人望而生畏。组织规模越大,应用程序数量越多,这个问题就越具有挑战性。

安装

即使作为一名开发人员,我也不得不在软件/应用程序安装的苦苦挣扎中度过相当长的时间。毋庸置疑,应用程序的安装可能是一项缓慢、容易出错、繁琐且令人沮丧的任务。在软件开发圈,有一个比喻,叫做“大猩猩叼着香蕉”,通常用来解释与 ORM(对象关系映射)相关的“n+1”问题。我在这里也使用这个比喻,因为我认为它也适用。把应用程序想象成香蕉。对于安装来说,我们想要的只是“香蕉”。但我们最终得到的不是香蕉。相反,我们得到的是香蕉、叼着香蕉、面目狰狞的大猩猩、整个大猩猩部落,以及它所栖息的整个丛林。我想你明白我的意思了。这本质上是一个“n+1”安装问题。大多数应用程序都带有大量的依赖项(每个应用程序都有自己的一组依赖项)。

以下是与安装相关的一些问题:

  • 并非所有应用程序都是跨平台的,这意味着并非所有应用程序都能在所有或多个操作系统上运行。这导致需要多个具有不同操作系统的物理计算机系统来运行相关的应用程序。

  • 应用程序经常需要在相似的端口上运行,从而导致端口冲突。这些冲突也只有在应用程序安装并启动后才会被检测到。

  • 部署应用程序后,通常需要进行一些配置以确保所安装应用程序的正常运行。此额外的配置步骤容易出错(例如缺少步骤或配置错误)并存在安全问题(例如纯文本密码)。

  • 所有应用程序在依赖项方面都有不同的要求。因此,为了使应用程序能够正确安装和运行,需要安装大量的库和框架。这通常会导致库和框架冲突,而唯一的解决方案往往是将应用程序安装在其自己的物理计算机系统上,并安装其自己的一套库和框架。

  • 这种模型下的安全性充满了漏洞(任何可能造成损害或损失的因素)。例如,环境变量所需的敏感信息通常以纯文本形式存储,任何能够访问计算机的人都可以看到这些信息。

  • 当部署新的应用程序或需要修补计算机系统(软件和安全更新)时,几乎总是会出现停机。

此外,安装面临的最大问题之一是所有应用程序都附带一些负担和要求,而这些负担和要求通常与部署目标不一致。这就是为什么我们总是听到“但它在我的机器上可以正常工作”这样的借口。此外,应用程序往往支离破碎,缺乏凝聚力。这也是为什么需要安装文档/指南来详细说明每个步骤的原因。但这些安装文档通常写得不好或不完整。文档也很少更新。

管理(运营)

至此,我们已经分发并安装了应用程序。但是,如何管理应用程序及其不断增长的资源需求呢?这方面的内容可以写成一本书。不过,我只想提一下我认为在管理应用程序时最常见的几个问题。虚拟化技术也可以帮助解决这些问题。

以下是在测试、暂存和生产等不同环境中管理应用程序时可能遇到的一些操作问题。

  • 应用程序的资源需求会随着时间的推移而增长。这意味着应用程序可能需要更高的处理能力、更大的内存 (RAM) 或更大的硬盘空间。因此,可以使用垂直可扩展性水平可扩展性可扩展性)来扩展系统。在大多数情况下,由于大多数应用程序无法以分布式方式运行,因此采用垂直可扩展性更为容易。结果可能是添加过多或过少的资源,最终导致资金和/或计算资源的浪费。

  • 水平扩展(不使用虚拟化)的问题在于,需要构建一台单独的物理机。这需要时间(通常是耗时的准备时间)和大量的资金来执行。而且,应用程序通常只在高峰时段(例如黑色星期五)才需要额外的计算能力。而当流量下降时,你只能依靠一台超级计算机,除了浪费资源和金钱之外什么也不做。

  • 启动、暂停、停止和重新启动应用程序等简单的应用程序生命周期操作要么非常困难,要么根本不可能。

  • 有时,应用程序可能会因为某个进程陷入无限循环而变得异常。如果不加以控制,异常应用程序可能会导致整个计算机系统瘫痪。

  • 冗余可以通过物理机实现,但成本高昂,而且同样容易导致利用率不足。即使使用虚拟机,启动新的虚拟机也需要很长时间。任何超过几秒钟的离线时间都是不可接受的,在某些情况下甚至会危及生命。

当需要搭建多个环境(例如开发、测试、预发布和生产)时,上述所有问题都会变得更加复杂。如果您拥有一个多租户环境,每个客户端都需要单独设置,情况会变得更加糟糕。再加上众多应用程序和各种不同的依赖关系,您就会陷入困境。

现在我们了解了与应用程序部署生命周期相关的一些挑战,在下一部分中我将讨论 Docker 以及它如何成为我们这个时代最热门的技术之一。


Docker

克服上述挑战的解决方案是使用虚拟化技术,例如虚拟机(通过虚拟机管理程序实现)。然而,虚拟机只是迈向部署乌托邦的第一步。容器才是部署演进的下一步。Docker 也正是在此时诞生,并彻底改变了我们使用容器的方式。

人们经常将Docker容器这两个术语混用,仿佛它们是同一个东西。然而,Docker 并非容器,容器的概念也不是 Docker 发明的。容器已经存在十多年了,而容器的概念则更早出现(容器的历史)。Docker 是“站在巨人的肩膀上”的经典案例。容器本身就难以使用和管理。因此,Docker应运而生,将这项难以理解(和使用)的技术带给了普通人。Docker 是一个容器管理系统,它让容器管理变得简单。

为了进一步了解 Docker 是什么,了解以下关键主题非常重要:

  • 虚拟机
  • 图像
  • 容器
  • 虚拟机/容器比较
  • 注册表
  • 存储库

虚拟机

在上一节中,我讨论了一些与应用程序生命周期相关的挑战。其中很大一部分问题在于必须在物理计算机的限制范围内运行。这些挑战催生了虚拟机管理程序 (Hypervisor) 技术,使我们能够创建虚拟机。由于容器经常被拿来与虚拟机进行比较,因此我想解释一下它们之间的区别,以帮助减少或避免两者之间的混淆。

如果可以创建物理机的某种“垂直切片”,会怎么样呢?每个切片都会分配一定比例的物理机资源。这样,就可以在每个切片上安装特定的操作系统,以满足应用程序的需求。这在理论上听起来很棒,但我们不能指望把物理机切成碎片,然后还能正常工作。至少在物理世界中是这样。虚拟化和虚拟机管理程序(hypervisor)应运而生。虚拟机管理程序帮助我们将物理机划分为虚拟计算“切片”。虽然虚拟机管理程序技术并非万能药,但它在解决上述一些挑战方面大有裨益。虚拟机管理程序帮助我们创建虚拟机。虚拟机是对物理计算机系统的模拟,它将 CPU、RAM、网络和磁盘等物理计算资源虚拟化。

我们先来看一张虚拟机技术的概念图。

虚拟机图
docker-guide-vm

从上图可以看出,物理机有 3 个“切片”,每个切片都是一个虚拟机。需要注意的是,虚拟机需要自己的客户操作系统。图中还展示了一个“Type-2 托管虚拟机管理程序”。这意味着该虚拟机管理程序运行在传统的操作系统上。另一种类型的虚拟机管理程序是“Type-1 裸机虚拟机管理程序”。这些虚拟机管理程序直接在主机硬件上运行,无需主机操作系统。

图像

镜像是一个文件(容器外观的二进制表示)。从概念上讲,镜像是用于创建容器的模板(蓝图)。事实上,很多时候提到容器时,实际上指的是镜像。然而,容器是“运行时”的,而镜像是“构建时”的。我喜欢用一个来自面向对象编程 (OOP) 的概念来类比。在 OOP 中,需要理解的两个最重要的概念是类和对象。类是一个模板(蓝图),它们用于在运行时创建对象。对象展现类的所有属性和行为,但它只存在于运行时。与类类似,镜像用于创建代表该镜像运行实例的容器。

因此,如果您希望在主机上运行 MongoDB 或 Elasticsearch 实例,为什么不使用官方 Docker 镜像仓库中提供的现成镜像呢(更多关于镜像仓库的信息将在后文介绍)。镜像位于以下位置:

记住,镜像是用来运行容器的。因此,例如, MongoDB镜像将用于运行MongoDB容器。

容器

想象一下,一个操作系统中运行着一大堆进程。这些进程几乎共享所有资源(CPU、内存、网络等),并且每个进程都相互了解。但是,如果您想在一个隔离的“沙盒”环境中运行一个或多个进程,该怎么办?沙盒可以提供隔离,并允许每个进程拥有自己的进程命名空间。此外,沙盒还可以控制和限制进程可以使用的资源量。

正是“沙盒”中隔离进程的概念催生了容器技术。我一直在讨论的“沙盒”就是我们今天所熟知的容器。容器是虚拟化操作系统的隔离进程。实现这一点的机制被称为“命名空间”“控制组 (cgroup)”

容器概述
docker-guide-container-overview

从上图可以看出,我们最初使用的是 Linux 操作系统。我之所以使用 Linux,是因为容器技术的起源完全基于 Linux 操作系统。每个进程都没有(或几乎没有)隔离。通过使用命名空间控制组,我们现在能够将每个进程作为独立的进程在其自己的容器中运行。

命名空间

命名空间的官方文​​档指出以下内容:

命名空间将全局系统资源包装在一个抽象层中,使命名空间内的进程感觉自己拥有该全局资源的独立实例。对全局资源的更改对于命名空间的其他成员进程可见,但对于其他进程则不可见。命名空间的用途之一是实现容器。

因此,命名空间通过允许每个进程在其自己的隔离工作区中运行来提供进程隔离。因此,当运行容器时,将为容器的各个方面创建以下命名空间。

  • IPC - 隔离和管理对 IPC 的访问
  • 网络——隔离和管理网络设备、堆栈、端口等。
  • Mount——隔离文件系统挂载点
  • PID 命名空间 - 提供进程隔离
  • 用户命名空间——隔离用户和组 ID
  • UTS 命名空间 - 隔离主机名和 NIS 域名
控制组(CGroups)

cgroups的官方文档说明如下:

控制 cgroup(通常简称为 cgroup)是 Linux 内核的一项功能,它允许将进程组织成层级组,从而限制和监控这些组对各种资源的使用情况。内核的 cgroup 接口通过一个名为 cgroups 的伪文件系统提供。分组功能在核心 cgroup 内核代码中实现,而资源跟踪和限制功能则在一组针对每种资源类型的子系统(内存、CPU 等)中实现。

因此,cgroup就是组织进程,以便可以向它们分配资源。

虚拟机/容器比较

容器不是微型虚拟机,它们与虚拟机也不同。这一点从我上面的讨论中应该已经很清楚了。然而,为了进一步阐明容器的含义,我将虚拟机与容器进行进一步的比较,如下所示。

虚拟机/容器比较
docker-guide-vm-vs-container

从上图可以看出虚拟机与容器的区别可以概括如下:

  • 虚拟机管理程序虚拟化物理计算系统(硬件),而容器虚拟化操作系统。  
  • 虚拟机管理程序将操作系统从硬件中抽象出来,而容器则将应用程序从操作系统中抽象出来。  
  • 虚拟机需要自己的客户操作系统,而容器则共享主机操作系统。这就是为什么要运行 Linux 容器,就需要一个 Linux 主机操作系统。而要运行 Windows 容器,就需要一个 Windows 操作系统。

此外,虚拟机和容器并非是对立的技术。事实上,它们可以作为互补技术使用。Windows 和 Apple 正是通过这种方式在其平台上提供 Linux 容器。Windows 和 Apple 操作系统都需要虚拟机管理程序来运行搭载 Linux 操作系统(Alpine)的虚拟机,以便使用 Docker 等技术托管 Linux 容器。不过,Windows 在容器技术方面已经取得了显著进展。截至撰写本文时,用户可以在 Windows(Windows 10 Pro)操作系统上选择使用 Windows 容器还是 Linux 容器。

容器也不能取代虚拟机。尽管在很多场景下使用容器会更好(例如微服务)。如上所述,这两种技术截然不同。记住,虚拟机是关于硬件虚拟化的,而容器是关于操作系统虚拟化的。

注册表

最基本的镜像仓库是存储和检索 Docker 镜像的地方。Docker 提供公共和私有镜像仓库,可在此处找到。如果您曾经使用过NPMNuget等软件开发包管理器,或者使用过DPKGRPM等 Linux 包管理器,那么使用 Docker 镜像仓库的概念应该会很熟悉。

Docker 并非唯一提供 Docker 注册表的平台。Microsoft Azure、Amazon Web Services、Google Cloud Platform 和 Digital Ocean 只是少数几个可供选择的 Docker 注册表。例如,您还可以使用Docker Trusted Registry托管本地注册表。

有关 Docker 注册表的更多信息,请在此处查看官方 Docker 文档

存储库

仓库是 Docker 注册表中特定镜像的实际存放位置。下图对此进行了进一步说明。

docker-repository


概括

我已经讨论了我认为与软件部署相关的主要挑战。从中得出的关键结论是,我们需要一种更好的方法来部署软件应用程序。我所说的部署指的是与软件应用程序的分发、安装和管理(操作)相关的一切。我们希望软件应用程序具有更高的可移植性、更强的内聚性、更强的独立性,并且与其他应用程序/进程隔离。这使我们能够降低与软件部署相关的复杂性和错误,最终实现更健壮、更安全、更易于扩展的软件应用程序。

虚拟机管理程序技术使我们能够创建虚拟机 (VM),从而隔离并独立管理应用程序。虚拟机的一个问题是它们会消耗大量主机资源。部分原因是每个虚拟机都需要拥有自己的客户操作系统。与启动(或准备)物理机相比,启动虚拟机相对较快。然而,与容器等的启动时间相比,容器的速度要快得多。虚拟机的另一个问题是它们的可移植性不如容器。

正如我之前在本指南中提到的,容器是应用程序部署演进的下一步。然而,容器的理解和使用都比较困难。因此,Docker 应运而生,旨在简化容器的管理。容器使我们能够将应用程序打包成单个内聚、隔离且可移植的软件包,从而可以轻松地在不同的部署环境之间迁移。

文章来源:https://dev.to/drminnaar/docker-guide---part-1--57c8
PREV
REST API 指南
NEXT
11 个 React 示例