Docker 实现原理
Docker 的实现依赖于三大核心技术:Namespace、Cgroup 和 AUFS。这些技术共同支撑了 Docker 的隔离性、资源管理和镜像分层机制。
- Namespace:用于实现资源的隔离,使每个容器拥有独立的进程、网络、文件系统等视图,从而在操作系统层面实现容器之间的隔离。
- Cgroup:用于限制和分配容器的资源(如 CPU、内存、磁盘 I/O 等),确保容器不会过度占用主机资源。
- 联合文件系统:用于实现容器的分层镜像和写时复制(Copy-on-Write)机制,使容器可以高效地共享基础镜像并独立修改文件。
Namespace
Namespace 用于实现资源的隔离,使每个容器拥有独立的进程、网络、文件系统等视图,从而在操作系统层面实现容器之间的隔离。
1. PID Namespace(进程命名空间)
- 作用:隔离进程 ID(PID),使得每个容器内的进程拥有独立的 PID 空间。
- 特点:
- 每个容器内的进程 PID 从 1 开始,类似于主机上的
/sbin/init
进程。 - 容器内的进程只能看到同一命名空间或子命名空间中的进程。
/proc
文件系统只显示当前命名空间中的进程。- 父命名空间可以看到子命名空间中的进程,但 PID 可能不同。
- 每个容器内的进程 PID 从 1 开始,类似于主机上的
- 应用场景:实现进程隔离,避免容器内的进程影响主机或其他容器。
2. Net Namespace(网络命名空间)
- 作用:隔离网络设备、IP 地址、路由表等网络资源。
- 特点:
- 每个容器拥有独立的网络设备(如虚拟网卡)、IP 地址和路由表。
/proc/net
目录只显示当前命名空间的网络信息。- Docker 默认使用
veth
方式将容器的虚拟网卡连接到主机的docker bridge
。
- 应用场景:实现网络隔离,使每个容器拥有独立的网络栈。
3. IPC Namespace(进程间通信命名空间)
- 作用:隔离进程间通信(IPC)资源,如信号量、消息队列和共享内存。
- 特点:
- 每个 IPC 资源有一个唯一的 32 位 ID。
- 容器内的进程只能访问同一命名空间中的 IPC 资源。
- 应用场景:避免容器间的进程间通信干扰。
4. Mnt Namespace(挂载命名空间)
- 作用:隔离文件系统挂载点,使每个容器拥有独立的文件系统视图。
- 特点:
- 类似于
chroot
,但更强大。 - 每个容器只能看到自己命名空间中的挂载点。
/proc/mounts
只显示当前命名空间的挂载信息。
- 类似于
- 应用场景:实现文件系统隔离,使容器拥有独立的文件目录结构。
5. UTS Namespace(主机名和域名命名空间)
- 作用:隔离主机名和域名。
- 特点:
- 每个容器可以拥有独立的主机名和域名。
- 使容器在网络上被视为独立节点。
- 应用场景:实现主机名和域名的隔离,使容器在网络中独立标识。
6. User Namespace(用户命名空间)
- 作用:隔离用户和用户组 ID。
- 特点:
- 容器内的用户和用户组 ID 可以与主机不同。
- 容器内的进程可以以容器内部的用户身份运行,而非主机用户。
- 应用场景:实现用户权限隔离,增强安全性。
7. Cgroup Namespace(Cgroup 命名空间)
- 作用:隔离 cgroup 视图。
- 特点:
- 它让容器内的进程只能看到自己的 cgroup 层次结构,而看不到其他容器或宿主机的 cgroup 信息。
- 应用场景:
- 防止信息泄露:容器进程无法看到其他容器或宿主机的 cgroup 信息。
- 简化容器迁移:容器可以在不同宿主机间迁移,而不需要担心 cgroup 配置的冲突。
- 限制资源使用:容器进程无法获取宿主机的 cgroup 访问权限,只能管理自己的 cgroup。
Cgroup
Cgroup(Control Groups)是 Linux 内核的一个特性,用于对进程组的物理资源(如 CPU、内存、磁盘 I/O 等)进行细粒度的控制和监控。
Core(核心)
- 定义:Core 是 cgroup 的核心功能,即分层组织进程。
- 特点:
- 每个 cgroup 可以包含子 cgroup,形成一个层次结构(树)。
- 这种层次结构允许对进程进行分组管理。
- 作用:
- 提供了一种灵活的方式来组织和管理进程。
- 比如,你可以将所有数据库进程放在一个 cgroup 中,而将所有 Web 服务器进程放在另一个 cgroup 中。
Hierarchy(层级结构)
- 定义:hierarchy 是 cgroup 的组织形式,可以理解为一棵树,树的每个节点是一个 cgroup(进程组)。
- 特点:
- 每棵树可以关联零到多个子系统(subsystem)。
- 一棵树中包含系统中的所有进程,但每个进程只能属于树中的一个节点(cgroup)。
- 系统中可以有多棵树,每棵树关联不同的子系统。
- 一个进程可以属于多棵树,但每棵树关联的子系统不同。
- 作用:
- 将进程分组,并对这些组进行资源管理。
- 通过关联不同的子系统,实现对不同资源的控制(如 CPU、内存等)。
Controller(控制器)或 SubSystem(子系统)
- 定义:控制器(Controller)和子系统(Subsystem)在 cgroup 的上下文中其实是同一个概念,只是叫法不同。它们都指的是 cgroup 中负责管理特定类型资源的模块。
- 特点:
- 控制器负责管理特定类型的系统资源,不同的控制器可以控制不同的资源。
- 例如:
cpu
控制器:控制进程的 CPU 时间分配。memory
控制器:控制进程的内存使用。blkio
控制器:控制块设备 I/O(如硬盘)的带宽和 IOPS。net_cls
控制器:控制网络流量的分类和质量服务。freezer
控制器:暂停和恢复 cgroup 任务。device
控制器:允许或拒绝 cgroup 任务对设备的访问。
- 作用:
- 每个 cgroup 都有一个
cgroup.controllers
文件,其中列出了所有可供该 cgroup 启用的控制器。 - 这意味着你可以为不同的 cgroup 启用不同的控制器组合,以实现对不同资源的控制。
- 每个 cgroup 都有一个
联合文件系统 UnionFS
1. 什么是联合文件系统 UnionFS?
联合文件系统可以将多个目录(称为分支)挂载到同一个虚拟文件系统下。每个分支可以有不同的权限,比如:
- 只读(readonly):不能修改。
- 读写(readwrite):可以修改。
- 白名单(whiteout-able):可以删除或隐藏文件。
核心思想是 分层存储,通过将多个只读层和一个可写层叠加在一起,形成一个完整的文件系统视图。
2. 联合文件系统在 Docker 中的作用
Docker 使用联合文件系统来实现容器的 镜像分层 和 写时复制(Copy-on-Write) 机制。
(1) 镜像分层
- Docker 镜像是由多个 只读层(readonly layers) 组成的,每一层包含文件系统的一部分。
- 例如:
- 基础镜像(Base Image)包含操作系统的核心文件(如
/bin
、/lib
等)。 - 上层镜像包含应用程序及其依赖。
- 基础镜像(Base Image)包含操作系统的核心文件(如
- 这些只读层通过联合文件系统叠加在一起,形成一个完整的文件系统视图。
(2) 容器层
- 当 Docker 启动一个容器时,会在镜像的最上层添加一个 可写层(writable layer)。
- 所有对文件系统的修改(如创建、修改、删除文件)都发生在可写层中,而不会影响底层的只读镜像。
- 这种机制称为 写时复制(Copy-on-Write)。
(3) 镜像共享
- 多个容器可以共享同一个基础镜像,但每个容器有自己的可写层。
- 这样可以节省存储空间,因为基础镜像只需要存储一次。
3. 联合文件系统的优势
- 节省存储空间:多个容器可以共享同一个基础镜像,避免了重复存储。
- 快速部署:由于镜像分层,启动容器时只需要加载可写层,而不需要复制整个文件系统。
- 内存更省:多个容器共享同一个基础镜像,操作系统可以将这些文件缓存到内存中。
- 升级更方便:如果需要更新基础镜像,只需要更新底层的只读层,而不需要修改每个容器。
- 灵活的文件修改:所有对文件系统的修改都发生在可写层中,不会影响底层的只读镜像。
Docker 容器的启动过程
1. 典型的 Linux 启动过程
- BootFS(Boot File System):
- 包含 bootloader 和内核(kernel)。
- 系统启动时,BIOS/UEFI 加载 bootloader(如 GRUB)。
- bootloader 加载内核到内存中。
- 内核初始化硬件并加载必要的驱动。
- 内核加载完成后,BootFS 会被卸载,内存使用权交给内核。
- RootFS(Root File System):
- 包含操作系统的核心文件,如
/bin
、/lib
、/etc
、/dev
等。 - 内核挂载 RootFS 为只读(readonly),并进行文件系统检查。
- 检查完成后,RootFS 被切换为读写(readwrite),供用户使用。
- 系统启动初始化进程(如 systemd 或 init),加载服务并进入用户空间。
- 包含操作系统的核心文件,如
2. Docker 容器的启动过程
- BootFS:
- Docker 镜像的最底层是 BootFS,包含 bootloader 和内核。
- Docker 容器启动时,会加载 BootFS 中的内核。
- 内核初始化后,BootFS 会被卸载。
- 注意:Docker 容器通常共享宿主机的内核,因此 BootFS 在容器中并不常用。
- RootFS:
- RootFS 包含操作系统的核心文件,如
/bin
、/lib
、/etc
、/dev
等。 - Docker 镜像的 RootFS 是只读(readonly)的,由多个镜像层(layers)组成。
- 容器启动时,Docker 会在 RootFS 之上加载一个可写层(容器层)。
- 容器的文件系统视图是 RootFS(只读)和容器层(可写)的联合挂载。
- RootFS 包含操作系统的核心文件,如
- 容器层:
- 容器层是容器运行时唯一的可写层,所有对文件系统的修改都发生在这里。
- 容器启动后,用户可以在容器层中创建、修改或删除文件。
- 这些修改不会影响底层的只读镜像,从而实现容器的隔离性和可移植性。
- 后续操作:
- 容器启动后,Docker 会执行容器中的入口点(Entrypoint)或命令(CMD)。
- 容器进程运行在容器层中,用户可以通过命令行或应用程序与容器交互。
3. Docker 容器与典型 Linux 启动过程的区别
特性 | 典型 Linux 启动过程 | Docker 容器启动过程 |
---|---|---|
BootFS | 包含 bootloader 和内核,启动后卸载。 | 通常共享宿主机的内核,BootFS 不常用。 |
RootFS | 挂载为只读,检查后切换为读写。 | 由多个只读镜像层组成,容器层为可写。 |
文件系统 | 完整的文件系统,包含所有目录和文件。 | 联合文件系统,只读镜像层 + 可写容器层。 |
内核 | 使用自己的内核。 | 通常共享宿主机的内核。 |
隔离性 | 无隔离,直接运行在物理机或虚拟机上。 | 通过命名空间和控制组实现隔离。 |
Docker 使用的不同的存储驱动
Docker 在不同的时期和环境下使用了不同的存储驱动(Storage Driver),包括 AUFS、OverlayFS、Device Mapper 等。具体使用哪个存储驱动取决于以下因素:
- 操作系统:不同的 Linux 发行版支持不同的存储驱动。
- 内核版本:某些存储驱动需要特定的内核版本支持。
- 用户配置:用户可以通过 Docker 配置文件手动指定存储驱动。
1. AUFS
- 使用时期:Docker 早期版本(如 Docker 1.0 到 Docker 1.12)。
- 支持环境:Ubuntu 和 Debian 等支持 AUFS 的 Linux 发行版。
- 特点:
- 性能较好,尤其是在处理大量小文件时。
- 未合并到 Linux 内核主线,需要额外补丁支持。
- 现状:由于未合并到内核主线,AUFS 逐渐被 OverlayFS 取代。
2. OverlayFS
- 使用时期:Docker 1.4 及以后版本。
- 支持环境:Linux 内核 3.18 及以上版本。
- 特点:
- 高性能,稳定性高。
- 已合并到 Linux 内核主线,无需额外补丁。
- 现状:OverlayFS 是 Docker 的默认存储驱动,广泛应用于现代 Linux 发行版。
3. Device Mapper
- 使用时期:Docker 早期版本(如 Docker 1.0 到 Docker 1.12)。
- 支持环境:CentOS、RHEL 等不支持 AUFS 的 Linux 发行版。
- 特点:
- 基于块设备的存储驱动,适合企业级环境。
- 配置复杂,性能较 AUFS 和 OverlayFS 稍差。
- 现状:逐渐被 OverlayFS 取代,但在某些特定场景下仍在使用。