Docker 容器如何实现隔离?_docker隔离机制
文章目录
- 前言
- 一、Namespace(命名空间)
-
-
- PID Namespace
- Network Namespace
- Mount Namespace
- UTS Namespace
- IPC Namespace
- User Namespace
-
- 二、Cgroups(控制组)
- 总结
前言
关于联合文件系统可以看之前写的docker和联合文件系统那些事
Docker 容器的隔离性主要通过 Linux 内核的 Namespace(命名空间)、Cgroups(控制组) 和 UnionFS(联合文件系统) 等技术实现。
一、Namespace(命名空间)
Namespace 是 Linux 内核自 2.6 版本开始引入的进程隔离技术,通过 clone() 系统调用的标志位(如 CLONE_NEWPID、CLONE_NEWNET 等)实现。
Docker 直接利用内核的 Namespace 功能,为每个容器创建独立的命名空间,实现进程、网络、文件系统等资源的隔离。Docker 的 Namespace 与内核 Namespace 完全一致,容器的隔离性直接由内核保证。
PID Namespace
原理 :PID(进程 ID)命名空间为每个容器创建独立的进程树,容器内的进程拥有自己的 PID 编号。这意味着在容器内部,进程的 PID 从 1 开始计数,与宿主机或其他容器中的进程 PID 相互独立。
实现细节:当启动一个容器时,Docker 会调用 Linux 内核的 clone 系统调用,并指定 CLONE_NEWPID 标志,从而创建一个新的 PID 命名空间。容器内的第一个进程(通常是 init 进程)在该命名空间内的 PID 为 1。
影响:容器内的进程无法看到宿主机或其他容器中的进程,提高了进程的隔离性。例如,在容器内使用 ps -ef 命令只能看到本容器内的进程。
Network Namespace
原理:网络命名空间为每个容器提供独立的网络栈,包括网络接口、IP 地址、路由表、防火墙规则等。每个容器可以有自己的网络配置,就像一台独立的物理机器一样。
实现细节:Docker 使用虚拟网络设备(如 veth pair)将容器连接到宿主机的网络中。当创建一个新的网络命名空间时,Docker 会创建一对 veth 设备,一端连接到容器的网络命名空间,另一端连接到宿主机的网桥(如 docker0)。
影响:不同容器之间的网络默认是隔离的,它们可以使用相同的端口而不会发生冲突。 例如,两个容器都可以监听 80 端口,而不会相互干扰。
Mount Namespace
原理:挂载命名空间允许每个容器有自己独立的文件系统挂载点。容器内的进程只能看到和操作自己挂载的文件系统,无法访问宿主机或其他容器的文件系统。
实现细节:当启动一个容器时,Docker 会创建一个新的挂载命名空间,并将容器的根文件系统挂载到该命名空间中。容器内的文件系统操作不会影响到宿主机或其他容器。
影响:容器可以有自己独立的文件系统布局,即使宿主机和容器使用相同的文件路径,它们也是相互独立的。 例如,容器内的 /var/log 目录与宿主机的 /var/log 目录是不同的。
UTS Namespace
原理:UTS(Unix Time - Sharing System)命名空间允许每个容器有自己独立的主机名和域名。容器内的进程可以使用自己的主机名进行网络通信,而不会与宿主机或其他容器的主机名冲突。
实现细节:在创建容器时,Docker 会为容器分配一个新的 UTS 命名空间,并允许用户通过 --hostname 选项指定容器的主机名。
影响:容器可以模拟不同的主机环境,方便进行测试和部署。 例如,在一个容器中可以将主机名设置为 test-server,而不会影响宿主机的主机名。
IPC Namespace
原理:进程间通信(IPC)命名空间为每个容器提供独立的 IPC 资源,如共享内存、消息队列和信号量等。容器内的进程只能与同一容器内的其他进程进行 IPC 通信,无法与宿主机或其他容器中的进程进行通信。
实现细节:当创建一个新的 IPC 命名空间时,Docker 会隔离该命名空间内的 IPC 资源,使得不同容器之间的 IPC 资源相互独立。
影响:容器内的进程可以安全地使用 IPC 机制,而不用担心与其他容器或宿主机的进程发生冲突。 例如,一个容器内的进程可以创建一个共享内存段,而不会影响到其他容器。
User Namespace
原理:用户命名空间允许将容器内的用户和组 ID 映射到宿主机上的不同用户和组 ID。这可以提高容器的安全性,避免容器内的进程以宿主机的 root 用户身份运行。
实现细节:在 Docker 中,可以通过 --userns 选项来启用用户命名空间。当启用用户命名空间时,Docker 会将容器内的用户和组 ID 映射到宿主机上的非特权用户和组。
影响:即使容器内的进程以 root 用户身份运行,在宿主机上也会以非特权用户的身份运行,从而降低了容器被攻击时对宿主机的影响。
二、Cgroups(控制组)
Cgroups 是 Linux 内核用于资源管理的核心子系统,通过文件系统接口(/sys/fs/cgroup)控制进程组的资源使用。
Docker 通过 Cgroups 限制容器的 CPU、内存、磁盘 I/O 等资源,例如设置 --memory 或 --cpu-shares 参数。
Docker 的资源限制完全基于内核 Cgroups 的实现,与手动通过 cgexec 命令控制进程组的逻辑相同。
CPU 限制
原理:Cgroups 通过 cpu.shares 和 cpu.cfs_quota_us 等参数来控制容器对 CPU 资源的使用。cpu.shares 表示容器在竞争 CPU 资源时的相对权重,cpu.cfs_quota_us 表示容器在一定时间内可以使用的 CPU 时间配额。
实现细节:当创建一个容器时,Docker 会为该容器创建一个对应的 Cgroup 子系统,并设置相应的 CPU 限制参数。例如,通过 docker run --cpu-shares 512 可以设置容器的 CPU 权重为 512。
影响:在多个容器竞争 CPU 资源时,具有较高 cpu.shares 值的容器将获得更多的 CPU 时间。而 cpu.cfs_quota_us 可以确保容器不会超过指定的 CPU 时间配额,避免某个容器占用过多的 CPU 资源。
内存限制
原理:Cgroups 通过 memory.limit_in_bytes 参数来限制容器可以使用的内存上限。当容器使用的内存超过该限制时,内核会采取相应的措施,如终止容器内的进程。
实现细节:在创建容器时,可以使用 docker run --memory 1g 来设置容器的内存上限为 1GB。Docker 会在 Cgroup 子系统中设置相应的 memory.limit_in_bytes 参数。
影响:内存限制可以防止容器过度使用宿主机的内存资源,确保其他容器和宿主机本身有足够的内存可用。
磁盘 I/O 限制
原理:Cgroups 通过 blkio.weight 和 blkio.throttle 等参数来控制容器对磁盘 I/O 资源的使用。blkio.weight 表示容器在竞争磁盘 I/O 资源时的相对权重,blkio.throttle 可以设置具体的磁盘读写速度限制。
实现细节:例如,通过 docker run --blkio-weight 500 可以设置容器的磁盘 I/O 权重为 500。还可以使用 --device-read-bps 和 --device-write-bps 等选项来设置具体的磁盘读写速度限制。
影响:磁盘 I/O 限制可以避免某个容器占用过多的磁盘 I/O 资源,确保其他容器和宿主机的磁盘 I/O 性能不受影响。
Pids 限制
原理:Cgroups 通过 pids.max 参数来限制容器内可以创建的最大进程数。这可以防止容器内的进程无限创建,导致宿主机资源耗尽。
实现细节:在创建容器时,可以使用 docker run --pids-limit 100 来设置容器内最多可以创建 100 个进程。Docker 会在 Cgroup 子系统中设置相应的 pids.max 参数。
影响:Pids 限制可以提高容器的稳定性和安全性,避免因容器内进程失控而影响宿主机的性能。
三、UnionFS(联合文件系统)
镜像分层
原理:Docker 镜像由多个只读的层(Layer)组成,每个层代表一个文件系统的变更。这些层可以被多个镜像共享,从而节省存储空间。
实现细节:当构建一个 Docker 镜像时,每执行一条 RUN、COPY 或 ADD 指令,都会在镜像中创建一个新的层。这些层按照顺序叠加在一起,形成最终的镜像。
影响:镜像分层使得镜像的构建和传输更加高效。 例如,如果多个镜像共享同一个基础层,那么在传输这些镜像时,只需要传输不同的层,而不需要重复传输相同的基础层。
容器读写层
原理:当启动一个容器时,Docker 会在镜像的顶层添加一个可读写层(Write Layer)。所有对容器文件系统的修改都只会保存在这个可读写层中,而不会影响到底层的镜像层。
实现细节:容器内的进程对文件系统的修改(如创建、删除或修改文件)都会被记录在可读写层中。当容器被删除时,可读写层中的数据也会被删除。
影响:容器读写层实现了容器文件系统的隔离,不同容器的修改相互独立。 例如,两个基于同一个镜像启动的容器可以对各自的可读写层进行不同的修改,而不会相互影响。
四、Network Namespace扩展(在不同网络场景中的体现)
Network Namespace 属于 Linux 内核命名空间机制的一部分,其功能是对系统的网络资源(像网络设备、IP 地址、路由表、防火墙规则等)进行隔离。每个 Network Namespace 都有一套独立的网络栈,这就保证了不同命名空间内的网络环境相互独立,互不干扰。
默认网桥(docker0)
隔离机制: 当启动 Docker 容器时,Docker 会为容器创建一个新的 Network Namespace。在这个新的命名空间里,容器拥有自己独立的网络设备、IP 地址和路由表。
连接方式: 容器的虚拟网络接口(通常以 veth 开头)会通过 veth pair 连接到宿主机上的 docker0 网桥。veth pair 是一种虚拟网络设备,它就像一根管道,两端分别处于不同的 Network Namespace 中,从而实现了容器和宿主机之间的网络通信。
网络隔离: 每个容器的 Network Namespace 相互隔离,容器只能看到自己命名空间内的网络资源,无法直接访问其他容器的网络资源,除非通过 docker0 网桥进行通信。
自定义网络
创建独立网络环境: 使用 docker network create 命令创建自定义网络时,Docker 会为这个网络创建相应的 Network Namespace 或者利用已有的机制来实现网络隔离。
灵活控制: 自定义网络允许用户将不同的容器划分到不同的 Network Namespace 中,进而实现更精细的网络隔离和控制。例如,创建多个网桥网络,把不同类型的容器分别放置在不同的网络中,这样可以限制容器之间的通信,提高网络安全性。
端口映射
网络暴露: 端口映射主要是为了让容器内的服务能够被外部网络访问。虽然端口映射本身并不直接依赖 Network Namespace 来实现隔离,但它是在 Network Namespace 提供的隔离网络环境基础上进行的。
访问控制: 通过端口映射,外部网络可以通过宿主机的 IP 地址和映射的端口访问容器内的服务。在这个过程中,Network Namespace 确保了容器的网络环境与宿主机和其他容器相互隔离,只有经过映射的端口才会暴露给外部网络。
与手动创建的 Network Namespace 对比
使用 ip netns 命令手动创建的 Network Namespace 与 Docker 容器的网络环境本质上是相同的,都是基于 Linux 内核的 Network Namespace 机制实现网络隔离。不过,Docker 对 Network Namespace 进行了封装和管理,让用户可以更方便地创建、配置和管理容器的网络环境。
五、文件系统隔离扩展( Mount Namespace 与 UnionFS共同完成)
Rootfs 隔离与 UnionFS
原理:
Docker 镜像由多个 只读层 叠加组成 (通过 UnionFS),容器启动时会在这些只读层上添加一个 可写层(/var/lib/docker/overlay2)。
Mount Namespace 将容器的文件系统挂载点隔离,使得容器只能看到自己的 Rootfs(由 UnionFS 合并后的只读层 + 可写层),而无法访问宿主机或其他容器的文件系统。
实现细节:
UnionFS 将镜像的只读层和容器的可写层合并为一个统一的文件系统视图。
写时复制(CoW): 当容器修改文件时,UnionFS 会将文件从只读层复制到可写层进行修改,避免影响原始镜像层。
Mount Namespace 确保容器的挂载点(如 /)与宿主机隔离,容器无法看到宿主机的文件系统结构。
影响:
UnionFS 是 Rootfs 隔离的核心,它通过分层和写时复制实现高效的镜像管理和隔离;Mount Namespace 则确保容器的文件系统挂载点独立。
Volume 卷与 Mount Namespace
原理:
Volume 卷的隔离性由 Mount Namespace 保证,而非 UnionFS。当将宿主机目录或 Volume 挂载到容器时,Mount Namespace 会将该挂载点与宿主机或其他容器隔离。
在 Docker 中,虽然直接挂载宿主机目录或在 Dockerfile 中定义绑定挂载可以实现数据共享,但 Volume 卷 仍然是更推荐的方案。
Volume 卷实现细节:
Volume 卷绕过了 UnionFS 的分层机制,直接挂载到容器的文件系统中。
创建 Volume 卷
# 创建名为 my_volume 的 Volumedocker volume create my_volume
与宿主机目录的区别:Volume 数据默认存储在宿主机的 /var/lib/docker/volumes/my_volume/_data,而宿主机需要使用 docker run -v /host/dir:/container/dir表示容器内的 /container/dir 是宿主机 /host/dir 的映射
运行容器时挂载 Volume
# 语法:docker run -v <volume_name>:/container/path ...docker run -d -v my_volume:/app/data --name my_container nginx//my_volume:指定已创建的 Volume 名称。
容器内的 /app/data 是 Volume 的挂载点,但其他容器(如容器 B)无法看到该挂载点。
宿主机的 /var/lib/docker/volumes/my_volume 目录与容器内的 /app/data 是同一物理存储的不同挂载点,但容器无法直接访问宿主机的其他目录(如 /etc、/home)。
容器内写入数据
docker exec my_container sh -c \"echo \'Hello Volume\' > /app/data/test.txt\"
宿主机验证数据
# 查看 Volume 存储路径(宿主机)docker volume inspect my_volume | grep Mountpoint# 输出类似:# \"Mountpoint\": \"/var/lib/docker/volumes/my_volume/_data\"# 检查文件是否存在cat /var/lib/docker/volumes/my_volume/_data/test.txt# 输出:Hello Volume
清理 Volume 卷
# 停止并删除容器docker rm -f my_container# 删除 Volume(数据将被永久删除)docker volume rm my_volume
Volume 卷的优势
数据持久化: 容器删除后数据仍保留,无需依赖宿主机目录结构。
跨平台兼容性: Docker 自动处理不同操作系统的路径差异。
集中管理: 通过 docker volume 命令统一管理,支持备份、迁移和清理。
安全性: 默认存储路径受 Docker 保护,减少容器访问宿主机敏感文件的风险。
通过以上步骤,用户可以更高效、安全地使用 Volume 卷替代宿主机目录挂载。
与宿主机的隔离性
容器无法访问宿主机其他目录: 即使 Volume 存储在宿主机的 /var/lib/docker,容器内的 /app/data 也只能访问该目录下的文件,无法访问宿主机的其他目录(如 /etc、/proc),是因为Mount Namespace 限制了容器的文件系统可见范围。
宿主机的修改不影响容器: 宿主机直接修改 /var/lib/docker/volumes/my_volume/_data 的文件后,容器内的 /app/data 会同步变更,但容器内的其他文件(如镜像的只读层)不会被宿主机影响。
原因:UnionFS 保证了镜像只读层的隔离性,而 Volume 挂载绕过了 UnionFS。
容器与容器的隔离性
无法互相访问对方的挂载点(即使挂载同一 Volume)
通过 Volume 共享物理存储
Mount Namespace 限制挂载点的可见范围
UnionFS 不参与 Volume 的隔离(仅处理镜像分层)
UnionFS vs. Mount Namespace
Rootfs 的隔离:主要由 UnionFS 实现,通过分层和写时复制确保容器文件系统独立。
Volume 卷的隔离: 由 Mount Namespace 实现,确保挂载点独立,但数据直接与宿主机或外部存储交互。
两者结合:UnionFS 和 Mount Namespace 共同构成 Docker 文件系统隔离的基础,前者负责镜像管理和高效复制,后者负责挂载点的隔离。
总结
Docker 容器的隔离性完全依赖于 Linux 内核的 Namespace、Cgroups、虚拟网络设备等功能。
两者的核心区别在于:
Linux 内核: 提供底层隔离机制,但需要用户通过系统调用或命令行工具(如 ip netns、cgexec)手动配置。
Docker: 通过封装内核接口,提供声明式的容器管理 API(如 docker run),简化了多容器环境的部署和维护。
本质上,Docker 是 Linux 内核容器化功能的用户空间增强工具,其设计哲学是 “复用内核能力,避免重复造轮子”。这也是 Docker 能在性能和易用性之间取得平衡的关键原因。