type
status
date
slug
summary
tags
category
password

1、概述

Docker 底层的核心三大技术是:
  • Namespaces:资源隔离,保证两个进程之间的独立性。
  • Cgroups:资源控制,分配一个进程最大限度可以使用的硬件资源。
  • Unionfs:合并多层文件,合并多层文件只读层和读写层。
正是这三项技术共同造就了容器的隔离限制分层这三大特性。
Docker 的本质还是一个 Linux 用户进程,在创建进程的过程中,三项技术互相协作,共同定义了“容器”这个概念。docker run 命令执行时:
  1. UnionFS 首先发挥作用:它基于镜像的各只读层,为容器创建一个可写层,组装成容器的根文件系统(rootfs)。
  1. Namespace 随后登场:Docker 在 clone() 调用时传入 Namespace 参数,然后调用 Linux 内核创建一系列 Namespace(PID、NET、MNT 等),为进程提供一个隔离的运行空间。
  1. 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_NEWUTSCLONE_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。
notion image
同样的,/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 系统中的所有进程,树的每个节点就是一个进程组。一个进程可以属于多颗树,但一个进程不能同属于同一棵树下的多个节点。
notion image
比如上图表示两个 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 技术:
  1. 镜像分层 (Image Layers):
      • 一个 Docker 镜像由一系列只读层(read-only layers)组成。
      • 每一层代表 Dockerfile 中的一条指令(例如:FROMRUNCOPY)。
      • 这些层是堆叠起来的,最底下的层是基础镜像(如 Ubuntu),之上的层是追加的修改。
  1. 容器层 (Container Layer):
      • 当容器启动时,Docker 会在镜像的所有只读层之上,添加一个薄薄的可写层(writable layer),通常称为“容器层”。
      • 所有对运行中容器的文件修改(如创建新文件、修改现有文件)都发生在这个可写层中。
  1. 写时复制 (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 相似,但分层命名有所不同。
notion image
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 目录。
Docker系列:Docker网络Docker系列:Docker镜像
Loading...