type
status
date
slug
summary
tags
category
password
1、概述
Docker 底层的核心三大技术是:
- Namespaces:资源隔离,保证两个进程之间的独立性。
- Cgroups:资源控制,分配一个进程最大限度可以使用的硬件资源。
- Unionfs:合并多层文件,合并多层文件只读层和读写层。
正是这三项技术共同造就了容器的隔离、限制和分层这三大特性。
Docker 的本质还是一个 Linux 用户进程,在创建进程的过程中,三项技术互相协作,共同定义了“容器”这个概念。
docker run
命令执行时:- UnionFS 首先发挥作用:它基于镜像的各只读层,为容器创建一个可写层,组装成容器的根文件系统(
rootfs
)。
- Namespace 随后登场:Docker 在
clone()
调用时传入 Namespace 参数,然后调用 Linux 内核创建一系列 Namespace(PID、NET、MNT 等),为进程提供一个隔离的运行空间。
- Cgroup 最后配置:Docker 在
/sys/fs/cgroup/
下为这个容器创建子目录并设置资源限制参数(CPU、内存等),控制进程的资源使用。
2、Namespace
2.1 Namespace是什么
Namespace 是 Linux 提供的一种内核级别环境隔离技术,它将系统资源包装在一个抽象的隔离空间中,使得在 Namespace 中的进程看起来拥有自己独立的全局资源。
Linux namespace 实现了 6 项资源隔离,基本上涵盖了一个小型操作系统的运行要素,包括主机名、用户权限、文件系统、网络、进程号、进程间通信。
namespace | 系统调用参数 | 隔离内容 | 容器的表现 | 内核版本 |
UTS | CLONE_NEWUTS | 主机名和域名 | UTS namespace 让容器有自己的 hostname。默认情况下,容器的 hostname 是它的短ID,可以通过 -h 或 --hostname 参数设置。 | 2.6.19 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 | IPC namespace让容器拥有自己的共享内存和信号量(semaphore)来实现进程间通信,而不会与host和其他容器的IPC混在一起。 | 2.6.19 |
PID | CLONE_NEWPID | 进程编号 | 所有容器的进程都挂在 dockerd 进程下,同时也可以看到容器自己的子进程。如果我们进入到某个容器,ps就只能看到自己的进程了。
而且进程的 PID 不同于host中对应进程的 PID,容器中 PID=1 的进程当然也不是 host 的 init 进程。也就是说:容器拥有自己独立的一套 PID,这就是 PID namespace 提供的功能。 | 2.6.24 |
Network | CLONE_NEWNET | 网络设备、网络栈、端口等 | Network namespace让容器拥有自己独立的网卡、IP、路由等资源。 | 2.6.29 |
Mount | CLONE_NEWNS | 挂载点(文件系统) | Mount namespace让容器看上去拥有整个文件系统,让容器有自己的 / 目录,可以执行mount和umount命令 | 2.4.19 |
User | CLONE_NEWUSER | 用户和用户组 | User namespace 让容器能够管理自己的用户,host不能看到容器中创建的用户 | 3.8 |
User namespace的内核完成版本:3.8,这也是很多时候为什么我们说 Docker 只能在 centos7 中运行的原因。
2.2 深入Namespace
Namespace的实现主要是下面三个系统调用:
clone()
– 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
setns()
– 把某进程加入到某个namespace,在docker exec进入容器的时候使用。
unshare()
– 使某进程脱离某个namespace。
主要来看下 clone 调用:
- 参数child_func传入子进程运行的程序主函数
- 参数child_stack传入子进程使用的栈空间
- 参数flags表示使用哪些CLONE_*标志位
- 参数args则可用于传入用户参数
在调用 clone() 在 flags 参数时候传入以上系统调用参数,就可以控制进程的隔离内容。一个容器进程也可以再 clone() 出一个容器进程,这是容器的嵌套。
例如最简单的 clone() 调用:
这段代码的功能非常简单:在 main 函数里,我们通过 clone() 系统调用创建了一个新的子进程 container_main 执行了一个/bin/bash。clone() 第二个参数指定了子进程运行的栈空间大小,第三个参数即为创建不同 namespace 隔离的关键。
但是对于上面的程序,父子进程的进程空间是没有什么差别的,父进程能访问到的子进程也能。
使用了 Linux Namespace 隔离技术后:
上面的clone系统调用中,我们新增了2个参数:
CLONE_NEWUTS
、CLONE_NEWPID
;也就是新增了UTS和PID两种Namespace。为了能够看出容器内和容器外主机名的变化,我们子进程执行函数中加入:sethostname("test", 9);
运行以上程序,发现子进程的hostname变成了 container,同时查看当前的进程号变成了1:
这就是 Linux Namespace 技术的隔离效果。
事实上,作为一个普通用户,我们希望的情况是:每当创建一个新容器时,容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统,这里可以通过容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。
在 Linux 操作系统里,有一个名为
chroot
的命令可以帮助你在 shell 中方便地完成这个工作。它的作用就是帮你“change root file system”,即改变进程的根目录到你指定的位置。使用 Namespace + chroot,我们就可以创造出一个隔离的容器环境。3、Cgroups
3.1 Cgroups原理
Cgroups(Control Groups) 是 Linux 内核用于限制进程使用物理资源的技术。具体做法是统一将进程进行分组,并在分组的基础上对进程进行监控和资源控制管理等。
虽然通过 Namespace 创造出隔离的容器环境,但事实上仍是所有容器进程共享宿主机上的硬件资源。如果某个进程占用资源过高,就会影响宿主机上其他进程的稳定性,所以有必要限制单个进程可以占用的最大程度的硬件资源。
Cgroup 主要控制和限制的资源类型:
资源类型 | 功能 | 说明 |
CPU | 限制 CPU 使用 | 可以设置进程的 CPU 使用份额(相对权重)、绑定到特定的 CPU 核、或者设置硬性的 CPU 使用时间上限。 |
Memory | 限制内存使用 | 可以设置内存使用硬限( memory.limit_in_bytes ),超过则进程会被 OOM Killer 终止;还可以设置包括交换分区(swap)在内的限制。 |
blkio | 限制块设备 I/O | 控制进程对块设备(如磁盘)的读写带宽或 IOPS(每秒读写次数)。 |
pids | 限制进程数量 | 限制 Cgroup 内可以创建的最大进程数量。 |
net_cls | 网络流量控制 | 与 Linux 流量控制器(tc)配合,对容器的网络流量进行优先级排序和限制。 |
devices | 设备访问控制 | 允许或拒绝 Cgroup 中的进程访问特定设备。 |
Cgroup 的工作原理:Linux 内核通过一个虚拟文件系统(通常是
/sys/fs/cgroup/
)来暴露 Cgroup 的接口。Docker 引擎会为每个容器创建对应的 Cgroup 目录(例如在 /sys/fs/cgroup/memory/docker/<container_id>/
),并在此目录下的文件中写入限制参数(如 memory.limit_in_bytes
)。内核会根据这些参数来强制执行资源限制。例如我们启动一个 alpine 容器。
查看容器的 container id。
进入
/sys/fs/cgroup/cpu/docker
目录,发现其存在和 container id 相同的目录。在 /sys/fs/cgroup/cpu/docker
目录中,Linux 会为每个容器创建一个 cgroup 目录,以容器长 ID 命名。目录中包含所有与 cpu 相关的 cgroup 配置。文件 cpu.shares 保存的就是 --cpu-shares 的配置,值为 512。
同样的,
/sys/fs/cgroup/memory/docker
和 /sys/fs/cgroup/blkio/docker
中保存的是内存以及 Block IO 的 cgroup 配置。3.2 Dcoker对Cgroup的支持
docker run
命令中的 --cpu-shares
、 -m
、 --device-write-bps
等选项实际上就是在配置 Cgroup。例如我们希望某个容器最多使用 1.5 个 CPU 核和 512MB 内存,容器内最多运行 100 个进程,可以使用以下命令:Docker 资源限制主要选项:
资源类型 | 参数示例 | 作用描述 |
CPU | --cpus=1.5 | 限制容器最多使用 1.5 个 CPU 核心的计算能力 |
ㅤ | --cpu-shares=512 | 设置 CPU 相对权重(默认 1024),用于共享 CPU 时的优先级调度 |
ㅤ | --cpuset-cpus="0-3" | 将容器进程绑定到指定的 CPU 核心(例如 0-3 号核心)上执行 |
内存 | -m or --memory="512m" | 限制容器能使用的物理内存最大值(例如 512MB) |
ㅤ | --memory-swap="1g" | 限制容器能使用的内存+Swap交换分区总量 |
ㅤ | --oom-kill-disable | 禁止 OOM Killer 在容器超限时杀死容器进程,需与 -m 联用 |
磁盘 I/O | --blkio-weight=500 | 设置容器块设备 I/O 的相对权重(范围 100-1000,默认 500) |
ㅤ | --device-read-bps="/dev/sda:1mb" | 限制容器对指定块设备(如 /dev/sda )的读取速率(如 1MB/s) |
ㅤ | --device-write-bps="/dev/sda:1mb" | 限制容器对指定块设备(如 /dev/sda )的写入速率(如 1MB/s) |
进程数 | --pids-limit=100 | 限制容器内最大可创建的进程数 |
可以使用
docker stats
命令来实时查看容器的资源使用情况 。使用注意:
- 尽早设置:资源限制通常应在启动容器时 (
docker run
) 就设定好。虽然运行时调整有些许方式(如通过docker update
更新部分设置,或直接修改 cgroup 文件 ),但可能受限或需重启生效。
- 生产环境建议:对于生产环境的容器,务必设置内存和 CPU 限制 。建议预留一定的缓冲空间(例如实际使用量不超过限制值的80%),并结合监控(如
docker stats
,cadvisor
,Prometheus
)和警报机制。
- cgroup 驱动:确保 Docker 使用的 cgroup 驱动与系统其他组件(如 systemd)兼容。可通过
docker info
查看相关信息。若看到类似WARNING: No swap limit support
的警告,可能需要修改内核启动参数(如cgroup_enable=memory swapaccount=1
)并重启。
3.3 深入Cgroups
cgroups 是一种将进程按组进行管理的机制,在用户层看来,cgroups 技术就是把系统中的所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程,树的每个节点是一个进程组,而每颗树又和一个或者多个 subsystem 关联,树的作用是将进程分组,而 subsystem 的作用就是对这些组进行操作。cgroups 主要包括下面两部分:
- subsystem:一个 subsystem 就是一个内核模块,用来限制某一类资源的使用。他被关联到一颗 cgroup 树之后,就会在树的每个节点(进程组)上做具体的操作。目前 Linux 支持 12 种 subsystem,比如限制 CPU 的使用时间,限制使用的内存,统计CPU的使用情况,冻结和恢复一组进程等
- hierarchy:一个 hierarchy 可以理解为一棵 cgroup 树,系统中可以有很多颗 cgroup 树,每棵树可以和一到多个 subsystem 关联,但一个 subsystem 只能关联到一颗 cgroup 树,一旦关联并在这颗树上创建了子 cgroup,subsystems 和这棵 cgroup 树就成了一个整体。在一颗树里面,会包含 Linux 系统中的所有进程,树的每个节点就是一个进程组。一个进程可以属于多颗树,但一个进程不能同属于同一棵树下的多个节点。

比如上图表示两个 cgroups 层级结构,每一个层级结构中是一颗树形结构,树的每一个节点是一个 cgroup 结构体(比如cpu_cgrp, memory_cgrp)。第一个 cgroups 层级结构 attach 了 cpu 子系统和 cpuacct 子系统, 当前 cgroups 层级结构中的 cgroup 结构体就可以对 cpu 的资源进行限制,并且对进程的 cpu 使用情况进行统计。 第二个 cgroups 层级结构 attach 了 memory 子系统,当前 cgroups 层级结构中的 cgroup 结构体就可以对 memory 的资源进行限制。
在每一个 cgroups 层级结构中,每一个节点(cgroup 结构体)可以设置对资源不同的限制权重。比如上图中 cgrp1 组中的进程可以使用60%的 cpu 时间片,而 cgrp2 组中的进程可以使用20%的 cpu 时间片。
可以通过查看
/proc/cgroups
来查看当前系统支持哪些 subsystem。- blkio 对块设备的 IO 进行限制。
- cpu 限制 CPU 时间片的分配,与 cpuacct 挂载在同一目录。
- cpuacct 生成 cgroup 中的任务占用 CPU 资源的报告,与 cpu 挂载在同一目录。
- cpuset 给 cgroup 中的任务分配独立的 CPU(多处理器系统) 和内存节点。
- devices 允许或禁止 cgroup 中的任务访问设备。
- freezer 暂停/恢复 cgroup 中的任务。
- hugetlb 限制使用的内存页数量。
- memory 对 cgroup 中的任务的可用内存进行限制,并自动生成资源占用报告。
- net_cls 使用等级识别符(classid)标记网络数据包,这让 Linux 流量控制器(tc 指令)可以识别来自特定 cgroup 任务的数据包,并进行网络限制。
- net_prio 允许基于 cgroup 设置网络流量(netowork traffic)的优先级。
- perf_event 允许使用 perf 工具来监控 cgroup。
- pids 限制任务的数量。
3.4 Cgroups使用方法
Linux 中,用户可以使用 mount 命令挂载 cgroups 文件系统,格式为:
其中 subsystems 表示需要挂载的 cgroups 子系统, /cgroup/name 表示挂载点,这条命令同时在内核中创建了一个cgroups 层级结构。
使用示例:
1、挂载 cpuset、cpu、cpuacct、memory 4 个 subsystem 到 /cgroup/cpu_and_mem 目录下
2、查看系统上的所有 cgroup
4、Unionfs
UnionFS(Union File System)是一种联合挂载的文件系统技术,它允许将多个不同的目录(称为分支)透明地叠加在一起,最终形成一个单一连贯的文件系统视图。这些目录分为“只读层”和“可写层”。
Docker 镜像和容器的分层结构完全基于 UnionFS 技术:
- 镜像分层 (Image Layers):
- 一个 Docker 镜像由一系列只读层(read-only layers)组成。
- 每一层代表 Dockerfile 中的一条指令(例如:
FROM
,RUN
,COPY
)。 - 这些层是堆叠起来的,最底下的层是基础镜像(如 Ubuntu),之上的层是追加的修改。
- 容器层 (Container Layer):
- 当容器启动时,Docker 会在镜像的所有只读层之上,添加一个薄薄的可写层(writable layer),通常称为“容器层”。
- 所有对运行中容器的文件修改(如创建新文件、修改现有文件)都发生在这个可写层中。
- 写时复制 (Copy-on-Write, CoW):这是 UnionFS 带来的关键特性。
- 读操作:当容器需要读取一个文件时,它从最上层的镜像层开始查找,逐层向下,直到找到该文件。
- 写操作:当容器需要修改一个存在于下层只读镜像中的文件时,UnionFS 会先将这个文件复制到可写层,然后容器再修改这个副本。原始文件在只读层保持不变。这使得多个容器可以共享同一个基础镜像,只在需要写入时才复制自己的副本,极大地节省了磁盘空间和加速了容器启动。
可以使用
docker info
查看 Docker 使用的存储驱动(Storage Driver)。常用的存储驱动技术:- AUFS:Docker 使用的第一种存储驱动
- Overlay2:目前 Docker 的默认存储驱动
4.1 AUFS
AUFS 是 Docker 最先使用的 storage driver,它技术很成熟,社区支持也很好,它的特性使得它成为 storage driver 的一个好选择。但仍有一些Linux发行版不支持AUFS,主要是它没有被并入Linux内核。AUFS通过写时复制策略来实现镜像镜像的共享和最小化磁盘开销。
AUFS 的读写过程:第一次修改文件的时候,意味着它当前不在最顶层的读写层AUFS就会在下面的读写层中查找它,查找是自顶向下,逐层查找的。找到之后,就把整个文件拷贝到读写层,再对它进行修改。哪怕只是文件的一小部分被改变,也需要复制整个文件。
AUFS 的删除过程:AUFS 通过在最顶层(读写层)生成一个 whiteout 文件来删除文件。whiteout 文件会掩盖下面只读层相应文件的存在,但它事实上没有被删除。
4.2 Overlay2
Overlay2 是 Docker 目前默认的存储驱动。Overlay2 与 AUFS 相似,但分层命名有所不同。

overlay2 的分层:
- lowerdir:表示较为底层的目录,修改联合挂载点不会影响到 lowerdir。
- upperdir:表示较为上层的目录,修改联合挂载点会在 upperdir 同步修改。
- merged:是 lowerdir 和 upperdir 合并后的联合挂载点。
- workdir:用来存放挂载后的临时文件与间接文件。
在运行容器后,可以通过
mount
命令查看其具体挂载信息:- 联合挂载点:
/var/lib/docker/overlay2/16361198b12618b2234306c6998cd8eb1c55f577a02144913da60dba4ca0c6e5/merged
- lowerdir:
/var/lib/docker/overlay2/l/INEXQHCNWBDKYZLHC42SH33R43:/var/lib/docker/overlay2/l/H47VNXLFUBUVMHEAEGXMC6S3QJ
,冒号分隔多个lowerdir,从左到右层次越低。
- upperdir:
/var/lib/docker/overlay2/16361198b12618b2234306c6998cd8eb1c55f577a02144913da60dba4ca0c6e5/diff
- workdir:
/var/lib/docker/overlay2/16361198b12618b2234306c6998cd8eb1c55f577a02144913da60dba4ca0c6e5/work
overlay2 读文件的过程:
- 要读的文件不在container layer中,那就从lowerdir中读。
- 要读的文件之存在于container layer中:直接从upperdir中读。
- 要读的文件在container layer和image layer中都存在,从upperdir中读文件。
overlay2 写文件的过程:第一次修改时,文件不在container layer(upperdir)中,overlay driver调用 copy-up 操作将文件从 lowerdir 读到upperdir中,对文件的读写都直接在 upperdir 中进行。overlayfs 中仅有两层,这使得文件的查找效率很高(相对于aufs)。
overlay2 新建文件的过程:对 merged 目录新增文件,会同步到 upper 目录,底下 lower 目录不会受影响。
overlay2 删除文件的过程:删除文件和aufs一样,相应的whiteout文件被创建在upperdir。并不删除容器层(lowerdir)中的文件,whiteout文件屏蔽了它的存在。删除目录的时候,会建立一个 opaque 目录,作用同上。
可以通过
docker inspect CONTAINER_ID
查看容器的文件系统:- ID:容器ID,这里的容器ID是da23cxxx
- Image:镜像ID
- GraphDriver:overlay2分层信息,这里详细列出了四种目录指向的具体目录:
- LowerDir:指向 da23cxxx 的 Init 目录和 LowerDir 层的 diff 目录。
- MergedDir:指向 da23cxxx 的 merged 目录。
- UpperDir:指向 da23cxxx 的 diff 目录。
- WorkDir:指向 da23cxxx 的 work 目录。
- Author:mcbilla
- URL:http://mcbilla.com/article/0ff85c7d-7c1d-806c-b2fd-ce9460d1b131
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts