第一章: Docker 架构与核心概念解析

第一章: Docker 架构与核心概念解析

摘要: 在本章中,我们将彻底拆解 Docker 的“黑盒”。你将不再仅仅是命令的执行者,而是深入理解其内部工作原理的架构师。我们将从 Docker 引擎的客户端-服务端 (C/S) 架构入手,理清镜像、容器与仓库三大核心组件的交互关系。最重要的是,我们会将 Docker 的隔离机制与你熟知的 Linux 知识——命名空间 (Namespaces) 和控制组 (Cgroups) 进行深度关联,让你明白所谓的“容器魔法”其实源于坚实的 Linux 内核技术。最后,我们将聚焦于我们的 WSL2 环境,揭示 Docker 在其中资源管理的奥秘。


在本章中,我们将像剥洋葱一样,层层深入 Docker 的核心:

  1. 首先,我们将揭示 Docker 引擎的 C/S 架构,让你明白 docker 命令是如何与后台守护进程通信的。
  2. 接着,我们将精准定义 Docker 世界的三大基本元素:镜像、容器和仓库
  3. 然后,我们将深入底层,借助你的 Linux 知识,理解实现资源隔离的两大基石:命名空间 (Namespaces)
  4. 紧接着,我们将探索实现资源限制的另一大基石:控制组 (Cgroups)
  5. 最后,我们将把理论与实践结合,探讨 Docker 在 WSL2 中的资源管理 模式。

1.1. Docker 引擎:客户端-服务端 (C/S) 架构详解

在我们成功安装并运行 hello-world 之后,你可能认为 docker 是一个单一的可执行文件。然而,这只是冰山一角。Docker 实际上是一个标准的 客户端-服务端 (Client/Server) 应用

本小节核心知识点:

  • Docker 引擎 (Docker Engine): 这是 Docker 的核心,一个 C/S 架构的应用,主要由 Docker 守护进程 (Daemon)、REST API 和 Docker CLI 三部分组成。
  • 守护进程 (Daemon): 名为 dockerd 的后台进程,它负责处理所有核心工作,如创建和管理镜像、容器、网络和存储卷。守护进程
  • Docker CLI: 命令行工具,也就是我们常用的 docker 命令。它扮演客户端的角色,将我们的指令通过 REST API 发送给守护进程。
  • REST API: 客户端与守护进程之间的桥梁,允许它们通过一个标准的接口进行通信。默认情况下,在 Linux 系统中,它们通过一个 UNIX 套接字 (socket) /var/run/docker.sock 进行通信。

痛点背景: 当我们在终端输入 docker run nginx 时,这个命令是如何让一个 Nginx 服务器运行起来的?如果 docker 只是一个简单的命令,它关闭后容器为什么还能继续运行?

解决方案: 理解 C/S 架构就能豁然开朗。

  1. 我们在 WSL2 终端中输入的 docker 命令,启动了 Docker 客户端
  2. 客户端将 run nginx 这个请求,打包成一个标准的 API 请求,发送给在本机后台持续运行的 Docker 守护进程 `dockerd`
  3. 守护进程接收到请求后,执行所有繁重的工作:检查本地是否存在 nginx 镜像,如果不存在就从远程仓库拉取,然后基于该镜像创建一个新的容器,并启动它。
  4. 守护进程将执行结果返回给客户端,客户端在我们的终端上显示容器 ID 等信息,然后退出。
  5. 即使客户端退出了,守护进程和它所创建的容器依然在后台运行,这就是为什么关闭终端窗口,我们的 Nginx 服务不会中断的原因。

我们可以在 WSL2 中亲眼验证守护进程的存在:

1
2
# 在 WSL2 终端中执行
ps aux | grep dockerd

1.2. 核心组件交互:镜像 (Image)、容器 (Container) 与仓库 (Registry)

理解了 C/S 通信模型后,我们再来明确通信内容中的三个核心“名词”:镜像、容器和仓库。对于有编程经验的你来说,一个恰当的类比能让你瞬间理解它们的关系。

本小节核心知识点:

  • 镜像 (Image): 一个只读的模板,包含了运行应用所需的所有文件系统内容和配置。它采用分层存储结构,可以被看作是软件交付的“集装箱”本身。
  • 容器 (Container): 镜像的一个可运行实例。容器与镜像的关系,就像是面向对象编程中 对象 (Object)类 (Class) 的关系。镜像是静态的定义,容器是动态的运行实体。容器在镜像的只读层之上增加了一个可写层。
  • 仓库 (Registry): 集中存储和分发镜像的服务。仓库与镜像的关系,就像是 代码仓库 (如 GitHub)代码 (Code) 的关系。最著名的公共仓库是 Docker Hub。

核心关系链:
开发者在本地构建一个 Image -> 将 Image 推送到远程的 Registry -> 其他开发者或服务器从 Registry 拉取该 Image -> 在本地运行该 Image,创建出一个或多个 Container

这个流程完美地解决了“在我电脑上明明是好的”这一经典难题,因为它确保了整个团队和所有环境(开发、测试、生产)都使用完全相同的只读模板(镜像)来创建运行环境(容器)。


1.3. 底层技术揭秘 (上):命名空间 (Namespaces) 如何实现资源隔离

承上启下: 我们已经知道容器是镜像的实例,并且容器之间是相互隔离的。那么,这种“隔离”的魔法究竟是如何实现的?这正是你的 Linux 知识大显身手的时刻。Docker 的隔离能力,主要依赖于 Linux 内核的两大特性,首先是 命名空间 (Namespaces)

痛点背景:

  • 为什么我在容器 A 中启动了一个 Web 服务监听 80 端口,还可以在容器 B 中再次启动一个服务监听 80 端口,而不会产生端口冲突?
  • 为什么在容器内部执行 ps aux 只能看到容器自己的进程,而看不到宿主机或其他容器的进程?

解决方案: 命名空间 (Namespaces) 是 Linux 内核提供的一种资源隔离方案。它能让一个进程(以及它的子进程)看起来像是拥有自己独立的全局资源。Docker 正是为每个容器创建了一系列专属的命名空间,从而实现了“欺骗”容器内进程的效果,让它以为自己独占了整个操作系统。

Docker 主要使用了以下几种命名空间:

命名空间 (Namespace)隔离的资源解决的痛点
PID Namespace进程 ID在容器内,进程可以拥有独立的 PID,例如 PID = 1 的初始进程,与宿主机的 PID 体系完全隔离。
NET Namespace网络设备、端口、路由表每个容器拥有独立的网络栈,包括自己的 IP 地址、端口空间和路由规则,解决了端口冲突问题。
MNT Namespace文件系统挂载点容器拥有独立的文件系统视图,看不到宿主机或其他容器的文件。
IPC Namespace进程间通信隔离了 System V IPC 和 POSIX message queues,防止不同容器间进程的意外通信。
UTS Namespace主机名和域名每个容器可以拥有独立的主机名 (hostname)。
User Namespace用户和用户组 ID实现容器内的 root 用户映射为宿主机上的一个普通用户,提升安全性。

当你执行 docker run 时,Docker 在后台为你做的关键工作之一,就是创建好这些命名空间,然后将容器的初始进程放入其中。这就像是为容器内的进程戴上了一副“VR 眼镜”,让它看到的世界是经过内核精心“伪造”的。


1.4. 底层技术揭秘 (下):控制组 (Cgroups) 如何实现资源限制

承上启下: 命名空间为容器提供了隔离的“视野”,解决了“能看到什么”的问题。但这还不够,如果一个容器发生内存泄漏,它可能会耗尽宿主机的所有内存,导致整个系统崩溃。如何限制容器“能用多少”资源?这就是 Linux 内核的第二个法宝——控制组 (Cgroups) 的用武之地。

痛点背景:

  • 如何确保一个容器最多只能使用 2 核 CPU 和 1GB 内存?
  • 如何防止某个“坏邻居”容器抢占所有资源,影响到同一台宿主机上的其他重要服务?

解决方案: 控制组 (Cgroups) 是 Linux 内核的另一个核心特性,其主要作用是 限制、记录和隔离进程组所使用的物理资源,包括 CPU、内存、磁盘 I/O 等。

当 Docker 创建一个容器时,它不仅会为其创建命名空间,还会为其在 Cgroups 的层级体系中创建一个对应的控制组。所有容器内的进程都会在这个控制组的管辖之下。

我们可以通过 docker run 命令的参数来轻松地利用 Cgroups 的能力:

  • 限制内存: docker run --memory=1g ... 这条命令告诉 Docker,创建一个容器,并配置其所属的 Cgroup,确保该容器使用的内存总量不会超过 1GB。
  • 限制 CPU: docker run --cpus=2 ... 这条命令则限制容器最多可以使用两个 CPU 核心的计算能力。

总结: Namespaces 负责隔离,让容器“看不见”彼此和宿主机。Cgroups 负责限额,让容器“用不超”分配给它的资源。两者结合,构成了现代容器技术的基石。

🤔 思考一下
我们刚刚剖析了容器依赖的两大 Linux 内核技术。现在,请结合这些知识,思考一下:容器 (Container) 与我们熟知的传统虚拟机 (Virtual Machine) 之间,最本质的区别是什么?

最本质的区别在于隔离的层级和资源的开销:

  • 虚拟机 (VM): 虚拟机通过 Hypervisor (虚拟机监控程序) 在物理硬件之上虚拟出一整套完整的硬件(CPU、内存、磁盘),然后在这套虚拟硬件上运行一个完整的客户操作系统 (Guest OS)。每个 VM 都有自己独立的内核。这种隔离是 硬件级别 的,非常彻底,但资源开销巨大,启动慢。

  • 容器 (Container): 容器内的所有进程都直接运行在宿主机的内核之上,它们共享同一个内核。容器之间通过 Namespaces 进行资源视图的隔离,通过 Cgroups 进行资源使用的限制。这种隔离是 操作系统内核级别 的,非常轻量级,资源开销极小,启动速度可以达到秒级甚至毫秒级。

简单来说:VM 是“房子里又盖了一栋带地基的小房子”,而容器是“大房子里用隔板隔出的一个个独立房间”。


1.5. WSL2 集成模式下的资源管理与性能监控

承上启下: 理解了 Namespaces 和 Cgroups 这两大通用 Linux 原理后,我们把目光拉回到具体的开发环境:Windows + WSL2。Docker Desktop 在 WSL2 上的运行方式非常巧妙,了解它有助于我们更好地管理资源。

痛点背景:

  • 开发者普遍担心 Docker Desktop for Windows 会占用大量系统资源,拖慢电脑。
  • 在 WSL2 模式下,我们如何精确地控制 Docker 能使用的最大内存和 CPU 数量?

解决方案: Docker Desktop 并没有直接在你的 Windows 系统上运行 dockerd。相反,它在 WSL2 内部启动了一个专用的、轻量级的 Linux 发行版(名为 docker-desktop),Docker 守护进程和所有容器都运行在这个专门的 WSL2 “虚拟机” 中。

这种方式的优势在于性能,因为它利用了 WSL2 提供的完整 Linux 内核,让 Docker 可以原生运行。但这也意味着,Docker 的资源消耗被计入了整个 WSL2 的资源池中。

要精确控制 Docker (以及所有 WSL2 发行版) 的资源上限,我们可以在 Windows 用户目录下创建一个名为 .wslconfig 的文件。

文件路径: C:\Users\<你的用户名>\.wslconfig

在这个文件中,我们可以这样配置:

1
2
3
4
5
6
7
8
9
10
11
# .wslconfig

[wsl2]
# 限制所有 WSL2 "虚拟机"加起来最多能使用 4GB 内存
memory=4GB

# 限制所有 WSL2 "虚拟机"加起来最多能使用 2 个 CPU 核心
processors=2

# 将 Windows 上的 D:\docker-data 目录挂载到 WSL2 中,可用于存放 Docker 数据
# swap=0 # 如果不需要交换空间,可以设置为0来节省磁盘空间

重要提示: 修改 .wslconfig 文件后,必须在 PowerShell 或 CMD 中执行 wsl --shutdown 命令来彻底关闭所有 WSL2 实例,然后重新启动 Docker Desktop 或你的 WSL2 终端,配置才会生效。你可以通过 wsl -l -v 命令来查看所有正在运行的 WSL2 实例。

通过这种方式,我们就为 Docker 设置了一个清晰的资源“天花板”,再也不用担心它会失控并耗尽整个 Windows 系统的资源了。


1.6. 本章核心速查总结

本章我们深入了 Docker 的内部架构与核心概念,为你后续的实战打下了坚实的理论基础。

分类关键项核心描述
核心架构C/S 架构Docker Engine 由客户端 (CLI)、服务端 (Daemon) 和 REST API 组成。我们操作的是客户端,真正工作的是守护进程。
核心组件镜像 (Image)静态的、只读的模板,应用打包的交付物。相当于面向对象中的“类”。
核心组件容器 (Container)动态的、可运行的实例,由镜像创建。相当于面向对象中的“对象”。
核心组件仓库 (Registry)集中存储和分发镜像 的服务,如 Docker Hub。相当于代码领域的“GitHub”。
底层技术命名空间 (Namespaces)实现资源隔离。让容器感觉自己独占了操作系统(独立的进程树、网络、文件系统等)。
底层技术控制组 (Cgroups)实现资源限制。控制容器能使用的 CPU、内存、I/O 等物理资源上限。
环境配置.wslconfig在 Windows 用户目录下,用于 配置 WSL2 全局资源限制(内存、CPU 等),从而间接控制 Docker 的资源上限。

总结要点:
Docker 并非魔法,它巧妙地运用了成熟的 Linux 内核技术(Namespaces 和 Cgroups)来提供轻量级的应用隔离与资源限制。理解其 C/S 架构和三大核心组件(镜像、容器、仓库)的交互关系,是掌握 Docker 的关键第一步。


1.7. 高频面试题与陷阱

面试官深度追问
2025-09-18

你好,看你简历上写了熟悉 Docker。那你能用自己的话,深入地讲讲容器和传统虚拟机最本质的区别是什么吗?

当然可以。最本质的区别在于它们的隔离层级和因此带来的资源开销差异。

虚拟机是通过 Hypervisor 在硬件层之上虚拟出一整套硬件,再安装一个完整的客户机操作系统,所以它有独立的内核。这是硬件级别的隔离,非常彻底,但启动慢、资源消耗大。

而容器是直接运行在宿主机的内核之上的,它和宿主机共享同一个内核。容器的隔离是通过 Linux 内核的命名空间(Namespaces)和控制组(Cgroups)技术实现的,这是一种操作系统级别的隔离。

很好,那你能具体说说命名空间和控制组分别解决了什么问题吗?

命名空间解决了“资源可见性”的问题,比如 PID 命名空间让容器内的进程看不到宿主机的进程,网络命名空间让容器有自己独立的 IP 和端口。它就像是给容器进程造了一个“信息茧房”。

控制组则解决了“资源使用量”的问题,它可以限制一个容器最多能用多少 CPU、多少内存。它就像是给这个容器的资源使用量设置了一个“天花板”,防止它影响到其他容器或宿主机。

非常清晰。总结一下,就是虚拟机是模拟硬件,容器是隔离进程,对吗?

是的,这个总结非常精辟。虚拟机更“重”,像一个完整的房子;容器更“轻”,像是房子里的一个独立房间。