前言
深入学习!手撕docker!
我们常听说docker是一个基于linux namespace和linux cgroups的虚拟化工具,下文将介绍docker依赖的linux namespace 和 linux cgroups是什么技术
Linux Namespace
namespace 是Linux kernel(内核)提供的可以隔离一系列系统资源的功能,这为docker的隔离特性提供了基础。
Namespace类型 | 系统调用参数 | 内核版本 | 备注 |
Mount Namespace | CLONE_NEWNS | 2.4.19 | 用于隔离各个进程看到的挂载点视图 |
UTS(Unix Time-haring System) Namespace | CLONE_NEWUTS | 2.6.19 | 隔离hostname及域名 |
IPC Namespace | CLONE_NEWIPC | 2.6.19 | 隔离通讯接口 |
PID Namespace | CLONE_NEWPID | 2.6.24 | 隔离进程id |
Network Namespace | CLONE_NEWNET | 2.6.29 | 隔离网络设备 |
User Namespace | CLONE_NEWUSER | 3.8 | 隔离用户组ID |
表1
Tip 常用系统调用有:clone()(创建新进程并根据参数有判断哪些namespace被创建,新进程的子进程也会被包含进这些namespace)、unshare()(将进程移除namespace)、setns()(将进程加入namespace)
Mount Namespace
通过Mount Namespace,docker得以隔离出自己的文件系统。
Mount Namespace可以隔离出具有独立挂载点信息的环境,而内核会去维护挂载点列表。在不同的namespace中,看到的文件系统的层次是不一样的。具有独立的挂载点信息也就意味着,每个Mount namespace可以拥有自己独立的目录层级,对于docker来说这让他得以拥有只属于自己的文件系统。
使用mount等命令,在Mount Namespace中调用mount(),umount(),仅仅会影响当前Namespace中的内容。可以说各个挂载点之间是独立的,docker volume(卷)也就是利用了这个特性。
举例:可以建立一个mount namespace ,在linux中对比挂载前后的/proc(proc是linux内核的虚拟文件系统,可以通过内核和内核模块与进程通讯:比如对其中虚拟文件的读写与内核实体进行通讯,甚至改变内核的运行状态)。对比后可以发现在mount namespace有一个不同与宿主机的proc。
UTS Namespace
UTS Namespace,全称Unix Time-sharing System,他主要隔离了hostname和domainname(这个系统标识主要与主机的NIS域有关)两个系统标识。
他使得namespace空间如同一台独立的linux系统,有自己的hostname,自己的域名。
IPC Namespace
IPC即进程间通讯,这个namespace显然完成的是通讯上的隔离,比如:message queue
拓展:
“
进程间通讯的机制称为 IPC(Inter-Process Communication)。Linux 下有多种 IPC 机制:管道(PIPE)、命名管道(FIFO)、信号(Signal)、消息队列(Message queues)、信号量(Semaphore)、共享内存(Share Memory)、内存映射(Memory Map)、套接字(Socket)。
其中的三种消息队列(Message queues)、信号量(Semaphore)、共享内存(Share Memory)被称为 XSI IPC,他们源自于 UNIX System V IPC。
”
Linux提供了POSIX和 System V两种不同标准的接口,IPC Namespace也即完成了对这两种不同标准通讯接口的隔离。
PID Namespace
这个namespace就比较显然,他隔离了同一进程在不同namespace中的ID。
比如,他可以让子命名空间的进程映射到父命名空间,而父命名空间可以知道子命名空间的运行状态,毕竟子命名空间的进程正是父命名空间某些进程虚拟化的结果。
以子命名空间1为例,他拥有PID为1的进程(初始化进程),在子命名空间的视角里他拥有独立的初始化进程,可以认为其相当于独立的linux机器。
但其实从host的视角来看,这只不过是父命名空间1001号进程虚拟化的结果。
Network namespace
Network namespace 是实现网络虚拟化的重要功能,它能创建多个隔离的网络空间,它们有独自的网络设备,而且空间内的应用可以绑定自己独立的端口,多个空间的端口是彼此隔离的。
这使得docker如同拥有自己独立的网络了一样
拓展:
namespace间的通讯
network namespace下的双向通讯:linux 提供了
veth pair
。可以把veth pair
当做是双向的 pipe(管道),从一个方向发送的网络数据,可以直接被另外一端接收到;或者也可以想象成两个 namespace 直接通过一个特殊的虚拟网卡连接起来,可以直接通信。network namespace下实现多命名空间相互通讯——linux 桥接:
可参考文章
浅析docker容器网桥的实现原理以及docker的四种网络模式和bridge模式的具体原理 - 古兰精 - 博客园 (cnblogs.com)
User Namespace
他隔离了用户的用户组ID。
这意味着一个进程的uid和group id在namespace 内外可以是不同的,在不同的namespace里可以被映射成不同的用户。
比如,有一种常用的做法是将宿主机内的非root用户映射到namespace中作为root用户。实现了对某些资源权限的隔离
Linux Cgroups
简介
前文写道,Linux namespace完成了一系列系统资源的隔离,这个说法实际上是不严谨的,我们可以看到namespace主要是隔离了很多系统标识符以及做了一些虚拟化的工作。
那cpu的分配呢?内存的分配呢?如果每个隔离的容器都独立运作,cpu怎么调度的呢?
这里,Linux Cgroups就完成了对资源的限制和监控。包括:CPU、内存、存储、网络等等,而且Linux Cgroups也方便了实时监控进程和统计信息。
Cgroups的组成及运作
Cgroups提供了对一组进程以及其将来子进程的资源限制、控制、统计的能力,有三个重要组件组成:cgroup、subsystem、hierarchy
- cgroup是对进程分组管理的机制,一个cgroup可以关联subsystem来对组内进程的资源使用进行限制。
- subsystem是资源控制模块,每个subsystem会被关联到定义了相应限制的cgroup上,并对这个cgroup中的进程做限制和控制。常用参数有:cpu(设置cgroup中进程的CPU被调度的策略)、cpuset(多核机器上设置cgroup中进程可以使用的cpu和内存)、memory(控制cgroup中进程的内存占用)、blkio(设置对块设备输入输出的访问控制)......
- hierarchy是负责把cgroup串成一个树状的结构,一颗cgroup串成的树被称为hierarchy。通过hierarchy,Cgroups就可以支持继承了。比如,一个cgroup被限制了cpu的使用,现在需要启动一个额外的定时任务,这个任务还需要被限制磁盘,那么就可以创建一个cgroup2继承自cgroup,并且添加额外的磁盘使用限制。cgroup2会继承cgroup的cpu使用限制,但cgroup2的磁盘使用限制不会影响到父节点cgroup。
这三个组件的相互协作其实是有一些规则的,比如:
- 当创建新的hierarchy时,会默认自动创建一个cgroup作为根节点,而当前系统的所有进程都会加入这个cgroup。在这个hierarchy创建的任意cgroup都会是这个默认节点的子节点。
- 一个subsystem只能绑定一个hierarchy,但一个hierarchy可以绑定多个subsystem。
- 一个进程可以同时属于多个cgroup,但这种情况不会在同一个hierarchy中发生。
- 一个cgroup中的进程fork了一个子进程,子进程依然属于这个cgroup,当然也可以主动移出。
Linux Namespace和Linux Cgroups进行隔离的代码示例(GO)
下面展示一个设置namespace,并且用Cgroups的机制对资源进行限制的GO演示程序。
利用sh启动一个容器进程
if os.Args[0] == "/proc/self/exe" {
fmt.Printf("current pid %d",syscall.Getpid())
fmt.Println()
cmd := exec.Command("sh","-c",'stress --vm-bytes 200m --vm-keep -m 1')
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err:=cmd.Run();err!=nil{
fmt.Println(err)
}
}
通过系统调用创建命名空间,进行系统资源的隔离
cmd := exec.Command("/proc/self/exe")
// 这里进行系统调用创建namespace,具体接口见文章表1
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err:=cmd.Start();err!=nil{
fmt.Println(err)
os.Exit(1)
}
fork一个新进程,并限制其资源使用。
// 挂载了subsystem member的hierarchy
const cgroupMemoryHierarchyMount = "...."
if err := cmd.Start(); err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
} else {
// 得到 fork 出来进程映射在外部命名空间的 pid
fmt.Printf("v", cmd.Process.Pid)
// 在系统默认创建挂载了 memory subsystem 的 Hierarchy 上创建
cgroupos.Mkdir(path.Join(cgroupMemoryHierarchyMount,"testmemorylimit"),0755)
// 将容器进程加入到这个 cgroup 中
ioutil,WriteFile(path.Join(cgroupMemoryHierarchyMount,"testmemorylimit""tasks")
[]byte(strconv.Itoa(cmd.Process.Pid)), 0644)
// 限制 cgroup 进程使用
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount,"testmemorylimit","memory.limit_in_bytes") ,[]byte("100m"),0644)
}
cmd.ProcessWait()
我们通过对Cgroups中虚拟文件的配置,完成了对stress进程内存资源使用的限制。
分层文件系统
Union File System 与AUFS
Union File System可以把不同文件系统用同一个联合挂载点联合成同一个文件系统服务。
他使用branch将不同的文件和目录“透明地”覆盖,形成一个统一的文件系统,这些branch都是read-only或者read-write的。
虽然看起来在这个虚拟出来的联合文件系统里,我们似乎可以随意的对这些文件进行操作,但其实这些操作并不会改变原本文件系统里的文件,这里利用了写时复制(Copy-on-Write)的技术,这也是docker能够进行分层构建镜像的技术基础(不过其实只是相似,docker主要靠layer)。
那么,写时复制做了什么呢?
写时复制也叫隐式共享,在一个虚拟的联合文件系统里,如果我们对某个文件进行写入操作,这个操作实际上会发生在一个新文件上面。在CoW机制下,他会将我们写入的文件复制一份再在复制文件中进行写入,原文件不会发生任何变动。
docker的镜像分层机制
“
简单来说,一个 Image 是通过一个 DockerFile 定义的,然后使用 docker build 命令构建它。DockerFile 中的每一条命令的执行结果都会成为 Image 中的一个 Layer。Docker Image 是有一个层级结构的,最底层的 Layer 为 BaseImage(一般为一个操作系统的 ISO 镜像),然后顺序执行每一条指令,生成的 Layer 按照入栈的顺序逐渐累加,最终形成一个 Image。如果 DockerFile 中的内容没有变动,那么相应的镜像在 build 的时候会复用之前的 layer,以便提升构建效率。并且,即使文件内容有修改,那也只会重新 build 修改的 layer,其他未修改的也仍然会复用。
通过了解了 Docker Image 的分层机制,我们多多少少能够感觉到,Layer 和 Image 的关系与 AUFS 中的联合目录和挂载点的关系比较相似。而 Docker 也正是通过 AUFS 来管理 Images 的。
”
引自文章:Docker 镜像分层机制与 AUFS(Advanced Union File System) - 知乎 (zhihu.com)
简述docker对AUFS的使用
Docker使用了AUFS的CoW技术来实现image layer的共享。对于每个docker container来说,它至多将image layer复制一次,后续的拷贝都在第一次拷贝的container layer上发生。
docker有关的aufs相关内容都在如下目录里
/var/lib/docker/aufs/
├── diff
├── layers
└── mnt
diff 存放的是每一个 Image 中 Layers 的内容, 而 layer 目录则是保存的每一个镜像的 layer 的结构信息。mnt 目录下的内容,其实就是一个镜像或者容器运行起来之后,使用的 AUFS 的挂载点。可以认为它就是你在容器内部所看到的文件系统。通过这三个子目录,docker 就能实现镜像的分层存储、容器的 Copy-On-Write 启动。
AUFS工作的例子
准备工作:先创建五个文件夹,container-layer,image-layer${n}(1<=n<=4),再在文件夹内部创建与文件夹命名相同的.txt文件。接着创建mnt目录用于后续挂载。
我们用一个mount aufs命令来把这些文件系统联合挂载到mnt上
sudo mount -t aufs -o dirs=./container-layer:./image-layer4:./image-layer3:./image-layer2:./image-layer1 none ./mnt
tip:在mount aufs 的命令中,没有指定待挂载的5个文件夹的权限,默认的行为是,dirs 指定的左边起第一个目录是 read-write 权限,后续的都是read-only权限。
此时mnt tree的结构是
mnt
├──container-layer.txt
├──image-layer1.txt
├──image-layer2.txt
├──image-layer3.txt
└──image-layer4.txt
接着开始试验,我们向其中一个image-layer中追加一些内容
echo -e "\ntest test test">> ./mnt/image-layer4.txt
用cat命令查看mnt文件系统下的image-layer4.txt发现内容改变。
但是mnt下的文件只是一个虚拟的挂载点,肯定有真实的文件产生了这些内容。
这里直接给结论:去查看image-layer4目录下的image-layer4.txt会发现内容没有发生改变,而作为读写层(拥有read-write权限)的container-layer文件夹下会多出一个image-layer4.txt文件,内容与虚拟文件系统里mnt/image-layer4.txt中的内容完全一致。
至此,docker运用的三种比较重要的技术就介绍完毕了。