type
status
date
slug
summary
tags
category
icon
password
1、实现容器的底层技术
Docker 的本质还是一个 Linux 用户进程,在创建进程的过程中:
- 在 clone 调用时传入 namespace 参数,实现资源隔离。
- 对进程配置 cgroups 参数,控制资源使用。
- 使用 chroot/pivot_root 切换进程的根目录。
- 使用 unionfs 技术合并文件读写层和只读层。
所以,Docker 底层的核心三大技术是:
Namespaces
:资源隔离,保证两个进程之间的独立性。
Cgroups
:资源控制,分配一个进程最大限度可以使用的硬件资源。
Unionfs
:合并多层文件,合并多层文件只读层和读写层。
2、Namespace
2.1 Namespace是什么
Linux Namespace 是 Linux 提供的一种内核级别环境隔离的方法。使用 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 创造出隔离的容器环境,但事实上仍是所有容器进程共享宿主机上的硬件资源。如果某个进程占用资源过高,就会影响宿主机上其他进程的稳定性,所以有必要限制单个进程可以占用的最大程度的硬件资源。 docker run 命令中的 --cpu-shares、-m、--device-write-bps 等选项实际上就是在配置 cgroup。
cgroup 到底长什么样子呢?我们可以在
/sys/fs/cgroup
中找到它。例如我们启动一个 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 深入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.3 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)是把其他文件系统联合到一个联合挂载点的文件系统服务。它使用 branch 把不同文件系统的文件和目录覆盖,形成一个一致的文件系统。
UnionFS 使用
COW
(copy-on-write,写时复制)的技术:如果一个资源是重复的,并且没有任何修改,这时并不需要立即创建一个新的资源,这个资源可以被新旧实例共享;当对资源进行修改后的时候,写到一个新的文件中,并没有改变原来的文件。通过这种资源共享的方式可以显著地减少未修改资源复制带来的消耗。AUFS
是 Docker 使用的第一种存储驱动,目前 Docker 默认的存储驱动已经变为了 overlay2
。可以使用 docker info
查看 docker 使用的 storage driver。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,upperdir 和 merged 三个层次:
- 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