手写docker—核心概念(一)
一、Namespace、Cgroups、Rootfs
进程和容器有什么区别?
- 进程作为计算机程序运行起来后资源管理的总和,内部包含了程序计数器、堆栈、各种变量指令等等;
- 容器就是对进程做一些限制和约束,从而形成一个边界。Cgroups 技术是用来制造约束的主要手段,Namespace 技术用来修改进程视图的主要方法。
.assets/20240107172245.png)
Namespace
处于不同 namespace 的进程拥有独立的全局系统资源,修改某一个 namespace 的系统资源只会影响到当前 namespace 的进程,对其它 namespace 的进程没有影响。
Linux 下根据隔离的属性分为不同的 Namespace:
- PID Namespace; 隔离 Process ID,系统调用参数为 CLONE_NEWPID;
- Mount Namespace;隔离挂载点,系统调用参数为 CLONE_NEWNS;
- UTS Namespace;隔离主机名和域名,系统调用参数为 CLONE_NEWUTS;
- IPC Namespace;隔离系统消息队列,系统调用参数为 CLONE_NEWIPC;
- Network Namespace;隔离网络、端口、网桥等等,系统调用参数为 CLONE_NEWNET;
- User Namespace;隔离用户和用户组ID,系统调用参数为 CLONE_NEWUSER;
例如 docker run -it xxx 进入容器内部后 ip a 命令只能看到容器内网络情况,就是利用了 Network Namesapce。
可以通过 docker inspect -f {{.State.Pid}} 容器ID 查询到容器对应进程 Pid,然后执行 nsenter pid 进入到指定进程空间内,执行 ip a 查看容器内 IP 地址。
[root@localhost ~]# clear
[root@localhost ~]# docker inspect -f "{{.State.Pid}}" 1e0352aa667f
1636
[root@localhost ~]# nsenter --target 1636 --net
[root@localhost ~]# ip a show eth0
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
1.1 API操作
-
clone:创建一个新进程,并把它放到新的 namespace 中:int clone(int (*fn)(void *), void *stack, int flags, void *arg, ... /* pid_t *parent_tid, void *tls, pid_t *child_tid */ ); -
setns:将当前进程加入到已有的 namespace 中:int setns(int fd, int nstype);- fd:指向 /proc/[pid]/ns/ 目录下的 namespace 对应的文件;
- nstype: namespace 的类型
- 1.如果当前进程不能根据 fd 得到它的类型,就需要通过 nstype来指定;
- 2.如果进程能够根据fd得到 namespace 类型,那么 nstype 设置为 0
-
unshare:使当前进程退出指定类型的 namespace,并加入到新创建的 namespace(相当于创建并加入新的 namespace)int unshare(int flags);- flags:上面指定的 CLONE_NEW*
-
ioctl_ns:查询 namespace 信息new_fd = ioctl(fd, request);- fd: 指向/proc/[pid]/ns/目录里相应namespace对应的文件
- request:
NS_GET_USERNS: 返回指向拥有用户的文件描述符namespace fd引用的命名空间
NS_GET_PARENT: 返回引用父级的文件描述符由fd引用的命名空间的命名空间。
1.2 Namespace 实例
- UTC Namespace 隔离 hostname
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
// 生成 cmd 命令
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS, //设置clone函数标志位 CLONE_NEWUTS
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
执行后进入新的 shell 客户端,在控制台大于查看父子进程是否处于同一个 UTS Namespace 中:
.assets/20240107172815.png)
- IPC Namespace 隔离消息队列、PID Namespace 隔离进程
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
IPC Namespace 用来隔离不同进程的消息队列;PID Namespace 用来隔离不同进程 ID,同一个进程在不同的 PID Namespace 可以拥有不同的 PID,在 docker container 里面通过 ps -ef 可以发现同样的进程有不同的 PID,这就是 PID Namespace 所做的事情。
- Mount Namespace
Mount Namespace 用来隔离各个进程看到的挂载点视图,在不同的 Namespace 进程中看到的文件系统层次是不一样的。Mount Namespace 和 chroot() 函数类似,都是将一个子目录变成根节点,但是 Mount Namespace 实现起来更方便和安全。
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
利用 mount -t proc proc /proc 命令将 /proc 挂载到我们自己的 Namespace 下来,可以发现文件少了很多,同时通过 ps -ef 命令查看当前环境的进程列表。
Sh 进程是 PID 为1 的进程,说明当前 Mount Namespace 中的 mount 和外部空间是隔离的,并不会影响外部。
sh-4.2# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 12:11 pts/0 00:00:00 sh
root 8 1 0 12:12 pts/0 00:00:00 ps -ef
- User Namespace
User Namespace 主要是隔离用户的用户组ID,一个进程的 UserID 和 GroupID 在 User Namespace 内外可以是不同的。在宿主机上以一个非 root 用户运行 User Namespace,然后在 Namespace 中映射为 root 用户。这意味着进程在 Namespace 中具有 root 权限,在原宿主机却没有,做到了权限隔离。 - Network Namespace
Network Namespace 是用来隔离网络设备、IP地址端口等网络栈,让每个容器拥有自己独立的网络设备。且每个Namespace 内的端口都不会互相冲突,在宿主机上搭建网桥就很容易实现容器间的通信。
Cgroups
Linux Cgroups 提供了对一组进程及将来的子进程的资源限制、控制和统计的能力,包括CPU、内存、网络、存储。
Linux Cgroups 技术将系统中所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程。树的每个节点对应一个进程组,每棵树又和一个或多个 subsystem 关联,Cgroups 树的作用就是进程分组,而 subsystem 的作用是对这些组进行操作。
1.1 核心组件
Cgroups 包括如下三个组件:
- cgroup:对进程分组管理的一种机制,一个 cgroup 对应一组进程,并可以在这个 cgroup 上增加 Linux subsystem 的各种参数配置,将一组进程和一组 subsystem 的系统参数关联起来。
- subsystem:一组资源控制的内核模块,当它被关联到一棵 cgroup 树之后就会在树的每个节点上做具体限制操作。
subsystem可以实现如下资源限制:- net_cls:将 cgroup 中进程产生的网络包分类,以便 Linux 的 tc(traffic controller) 可以根据分类区分出来自某个 cgroup 的包并做限流或监控。这个子系统使用等级识别符(classid)标记网络数据包,可允许 Linux 流量控制程序 (tc)识别从具体 cgroup 中生成的数据包。
- net_prio:设置 cgroup 中进程产生的网络流量的优先级。
- memory:控制 cgroup 中进程的内存占用。
- cpuset:在多核机器上设置 cgroup 中进程可以使用的 cpu 和内存。这个子系统为 cgroup 中的任务分配独立 CPU(在多核系统)和内存节点。
- freezer:挂起(suspend)和恢复(resume) cgroup 中的进程。这个子系统挂起或者恢复 cgroup 中的任务。
- blkio:设置对块设备(如硬盘)输入输出的访问控制。这个子系统为块设备设定输入/输出限制,比如物理设备(磁盘,固态硬盘,USB 等等)。
- cpu:设置 cgroup 中进程的 CPU 占用。这个子系统使用调度程序提供对 CPU 的 cgroup 任务访问。
- cpuacct:统计 cgroup 中进程的 CPU 占用。这个子系统自动生成 cgroup 中任务所使用的 CPU 报告。
- devices:控制 cgroup 中进程对设备的访问 16 这个子系统可允许或者拒绝 cgroup 中的任务访问设备。
- hierarchy:把一组 cgroup 串成一个树状的结构,简单理解就是一个 hierarchy 对应一棵 cgroup 树,树的每个节点就是一个进程组,这样 cgroups 就可以做到继承。一棵 cgroup 树中包含 Linux 系统的所有进程,但是每个进程只能属于一个 Cgroup 节点(进程组)。例如系统对一组定时的任务进程通过 cgroup1 限制了CPU的使用率,然后其中有一个定时 dump 日志进程需要额外限制磁盘IO,为了避免限制了磁盘IO之后影响到其它进程,可以创建 cgroup2,使其继承自 cgroup1并限制其磁盘IO。
.assets/20240203130227.png)
.assets/20240203141726.png)
1.2 linux 如何配置 Cgroups
上面我们知道 Cgroups 中的 hierarchy 是一种树状的组织结构,我们需要模拟出 cgroup 在 hierarchy 上的层级结构,需要按以下步骤进行:
- 创建并挂载一个 hierarchy;
# 创建 hierarchy(Cgroup树) 的目录
mkdir cgroup-test
# 挂载一个 cgroup树到该目录,但是不关联子系统
mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test
完成挂载后可以发现挂载目录下包含文件:
cgroup.clone_children:值为1 或者 0,如果为1表示子 cgroup 会继承父 cgroup 的资源配置;cgroup.procs:记录当前节点 cgroup 中的所有进程ID;notify_on_release:和 release_agent 一起使用,标识 cgroup 最后一个进程退出时是否执行 release_agent ;release_agent:记录可执行程序的路径,用作进程退出后自动清理掉不再使用的 cgroup;tasks:当前 cgroup 中的所有线程ID,不保证是顺序排列的。- 在 hierarchy基础上扩展出多个子 cgroup:
mkdir cgroup-1
mkdir cgroup-2
tree
.
├── cgroup-1
│ ├── cgroup.clone_children // 继承父 cgroup 的属性
│ ├── cgroup.event_control
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup-2
│ ├── cgroup.clone_children
│ ├── cgroup.event_control
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
- 在 cgroup 中添加和移动进程,也就是将进程ID 写入或移除 tasks 文件:
cd ./cgroup-test/cgroup-1
# 将所在终端移入 cgroup-1 中
sudo sh -c "echo $$ >> tasks"
- 通过 subsystem 限制 cgroup 中进程的资源
上述操作只是创建了 hierarchy ,并没有添加对资源限制的 subsystem 组件。系统默认为每个 subsystem 创建一个默认的 hierarchy,例如关于 memory 的 cgroup 树默认创建在 /sys/fs/cgroup/memory,关于 cpu 的 cgroup 树默认创建在 /sys/fs/cgroup/cpu。
这里以 stress 命令启动一个进程为例来演示 subsystem 对资源限制的作用:
# 查看 memory 挂载目录
mount | grep memory
# 切换到系统内存对应的 cgroup树目录
cd /sys/fs/cgroup/memory
# 启动一个不做约束的 stress进程,进程占用200MB内存
stress --vm-bytes 200m --vm-keep -m 1
# 创建cgroup
sudo mkdir test-limit-memory && cd test-limit-memory
# 设置cgroup 最大内存占用 100MB
sudo sh -c "echo "100m" > memory.limit_in_bytes"
# 将当前进程移动到该 cgroup中
sudo sh -c "echo $$ > tasks"
# 再次运行stress进程
stress --vm-bytes 200m --vm-keep -m 1
加入 subsystem 约束前,stress 进程占用 CPU、内存百分比:
.assets/20240123210857.png)
加入后占用 CPU、内存 百分比:
.assets/20240123211133.png)
通过上述操作可以发现,Cgroups 对资源的限制操作就是子系统目录加上一组资源限制文件。
如果想对某一进程的内存限制,可以按如下步骤执行:
- 首先切换到
/sys/fs/cgroup/memory目录,这是 linux 系统对内存管理的挂载目录; - 创建子系统目录
test-limit-memory,此时系统会在该目录下自定义创建一些资源管理相关的子文件; - 修改其中的
memory.limit_in_bytes文件可以起到限制进程内存的作用; - 将进程 Pid 加入到
tasks文件中,实现对进程内存控制;
如果想对进程的 CPU 限制,可以按如下操作执行:
- 切换到
/sys/fs/cgroup/cpu目录; - 创建子系统目录
test-limit-cpu,系统默认初始化相关子文件,其中cpu.cfs_quota_us文件代表每个控制周期内进程可以使用的 cpu 时间;cpu.cfs_period_us代表每个进程的控制周期; - 修改其中
cpu.cfs_quota_us实现对进程的 CPU 的限制; - 将进程 Pid 加入到
tasks文件中,实现对进程 CPU 的控制。
1.3 docker 如何使用 Cgroups
# docker启动容器,返回 cgroup 文件夹名
docker run -itd -m 128m ubuntu
957459145e9092618837cf94alcb356e206f2f0da560b40cb31035e442d3dfll
# docker为每个容器在系统的hierarchy中创建 cgroup
cd /sys/fs/cgroup/memory/docker/957459145e9092618837cf94alcb356e206f2f0da560b40cb310 35e442d3dfl1
# 查看内存限制
cat memory.limit_in_bytes
134217728
docker 使用 Cgroup 管理容器的资源配置:
- 为每一个容器创建一个 子 cgroup;
- 根据
docker run提供的参数调整 cgroup 中的配置; - 容器被删除的同时删除对应的子 cgroup;
1.4 通过 golang 操作 Cgroups 限制容器资源
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
"time"
)
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
func main() {
if os.Args[0] == "/proc/self/exe" {
// 容器进程触发
fmt.Printf("current pid %d", syscall.Getpid())
fmt.Println()
// 启动stress进程,指定内存占用200MB
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)
os.Exit(1)
}
}
// 创建 Namespace 容器
cmd := exec.Command("/proc/self/exe")
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("ERROR", err)
os.Exit(1)
} else {
time.Sleep(3 * time.Second)
// 得到fork出子进程映射在外部命名空间的 pid
fmt.Println("exec first")
fmt.Printf("%v", cmd.Process.Pid)
// 系统默认创建挂载了 memory subsystem 的 hierarchy 上创建 cgroup (目录 testmemorylimit)
os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
// 将容器进程pid 加入到 cgroup tasks 管理
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.Process.Wait()
fmt.Println("Process end....")
}
}
1.5 Cgroup 核心组件间关系
这里参考 https://tech.meituan.com/2015/03/31/cgroups.html 对 Cgroup 进一步讲解:
.assets/20240125175910.png)
如上图代表两个 hierarchy,每个 hierarchy 是一棵树形结构,树的每一个节点是一个 cgroup(cpu_cgrp、memory_cgrp)。
- 第一个
hierarchy关联了 cpu子系统和 cpuacct子系统,因此当前hierarchy的 cgroup 可以对 CPU 的资源进行限制,并且对进程的 cpu 使用情况进行统计; - 第二个
hierarchy关联了 memory子系统,可以对进程的 memory 使用情况进行统计和限制;
每一个 hierarchy 承担了对 cgroup 中进程资源的管理和限制,比如 cgrp1 进程使用 80% CPU,cgrp2 中使用 20%时间片。同时每个 hierarchy 关联一个或多个 subsystem,实现对资源管理限制。
- 每一个进程的描述符中有一个指针指向辅助数据结构
css_set(cgroup subsystem set)。指向某一个css_set结构的进程会加入到css_set的进程链表中。一个进程只能属于一个css_set,一个css_set可以包含多个进程,属于同一个css_set的进程受到同一个css_set所关联的资源限制; css_set和 cgroups 节点间是多对多的关联。但是cgroups不允许css_set同时关联同一个 cgroups 层级中多个节点,这样会导致对同一资源的多种限制;- 一个
cgroups节点关联多个css_set时,表明多个css_set下的进程列表受到同一份资源的相同限制。
1.6 Cgroup 相关操作命令
1.6.1 hierarchy 创建
hierarchy 的创建是 cgroup 树的创建,其过程就是将 cgroup 类型文件系统挂载到指定的目录。
- 语法为
mount -t cgroup -o subsystems name /cgroup/name。 subsystems表示需要挂载的 cgroups 子系统;/cgroup/name表示挂载点(文件目录);
上述命令的作用是在内核中创建一个 hierarchy 以及一个默认的 root cgroup,比如下面的例子就是挂载了一个 hierarchy 到 cg1 目录,如果对应的 hierarchy 不存在就会创建。
mkdir cg1
mount -t cgroup -o cpuset cg1 ./cg1
1.6.2 hierarchy 卸载
语法为 umount /cgroup/name,其中 /cgroup/name 为具体的挂载目录,卸载后相关的 cgroup 都会被删除。
1.6.3 release_agent 触发
在创建完 hierarchy 后会默认创建资源相关的配置文件,包括 cgrouo.clone_children、cgroup.procs、cgroup.sane_behavior、notify_on_release、release_agent、tasks。
其中 notify_on_release=1 时在 cgroup 退出时将调用 release_agent 中的命令。
1.6.4 cgroup 创建
cgroup 创建就是在 hierarchy 下新建一个目录。
1.6.5 cgroup 删除
Union File System
Docker 的存储驱动是基于 Union FileSystem 来实现的,它是一种将其它文件系统联合到一个联合挂载点的文件系统服务,简单来说就是将多个物理位置不同的文件目录联合起来,挂载到某一个目录下形成一个抽象的文件系统。它使用了写时复制的管理技术,在不修改资源时不需要重复创建资源,可以复用资源,docker 正式利用了这一点来创建镜像和容器。
AUFS
AUFS 为 Docker 选取的存储驱动,它具有快速启动容器、高效利用存储和内存的优点。这里举个例子来介绍 AUFS
- 首先建立
company、home两个目录及其下的文件:
root@home:~/root/aufs# tree .
|-- company
| |-- code
| `-- meeting
`-- home
|-- eat
`-- sleep
- 通过
mount命令将两个目录挂载到mnt目录下:
root@home:~/root/aufs# mkdir mnt
root@home:~/root/aufs# ll
total 20
drwxr-xr-x 5 root root 4096 Oct 25 16:10 ./
drwxr-xr-x 5 root root 4096 Oct 25 16:06 ../
drwxr-xr-x 4 root root 4096 Oct 25 16:06 company/
drwxr-xr-x 4 root root 4096 Oct 25 16:05 home/
drwxr-xr-x 2 root root 4096 Oct 25 16:10 mnt/
root@home:~/root/aufs# mount -t aufs -o dirs=./home:./company none ./mnt
root@home:~/root/aufs# ll
total 20
drwxr-xr-x 5 root root 4096 Oct 25 16:10 ./
drwxr-xr-x 5 root root 4096 Oct 25 16:06 ../
drwxr-xr-x 4 root root 4096 Oct 25 16:06 company/
drwxr-xr-x 6 root root 4096 Oct 25 16:10 home/
drwxr-xr-x 8 root root 4096 Oct 25 16:10 mnt/
root@home:~/root/aufs# tree ./mnt/
./mnt/
|-- code
|-- eat
|-- meeting
通过 mnt 目录下结果我们发现原来的两个目录合并挂载到同一个目录下,同时注意文件读写权限,默认情况下内核将根据从左到右的顺序将第一个目录指定为可读写,其它都为只读。
如果向只读目录执行一些修改操作,实际修改内容不会保存到原始目录中,而是在可读写目录下创建对应的修改目录,然后记录修改内容。
所以我们可以将服务的源代码和修改代码分别放到两个目录,然后利用 mount 将其联合挂载到同一个目录,前者设置只读权限,后者设置读写权限。那么针对源代码的一切修改都只会记录到修改目录下。
AUFS 和 Docker image 的关系
image 是 Docker 部署的基本单位,镜像运行在容器中,镜像包含了我们的程序文件,以及这个程序依赖的资源的环境,docker image 对外是以一个文件的形式展示的(实际看到的是文件目录挂载点)。
一个镜像是通过 dockerfile 定义的,然后利用 docker build 命令构建,dockerfile 中每一条命令的执行结果都会成为镜像中的一个镜像层。Docker 镜像是层级结构,最底层为 baseImage(一般为操作系统ISO镜像),然后顺序执行每一条指令生成 layer 并按照入栈的顺序逐渐累加,最终形成一个镜像。如果 dockerfile 中的内容没有变动,那么 build 过程会复用之前的镜像层;同时即使文件内容有修改,也只会重新 build 对应的镜像层。
每一个 Docker 镜像都是由一系列的 read-only layer 组成的,镜像层内容都是存储在 /var/lib/docker/aufs/diff 目录下;/var/lib/docker/aufs/layer 路径下存储了如何堆栈这些镜像层的元数据信息。
.assets/20240125000653.png)
如果我们执行以下命令构建新容器,会发现它对历史镜像的复用:
# 以 ubuntu 为基础镜像构建 changed-ubuntu 镜像
# dockerfile 文件内容
FROM ubuntu:15.04
RUN echo "Hello World" > /tmp/newfile
# 构建自定义镜像 changed-ubuntu
docker build -t changed-ubuntu .
# Sending build context to Docker daemon 10.75 kB
# Step 1 : FROM ubuntu:lS. 04 ---> dlb55fd07600
# Step 2 : RUN echo ” Hello world" > /tmp/newfile ---> Running in c72100f8lddl ---> 9d8602c9aeel
# Removing intermediate container c72100f8lddl
# Successfully built 9d8602c9aeel
执行完上述步骤后查看 /var/lib/docker/aufs/diff、/var/lib/docker/aufs/mnt 目录,会发现只是新增了一个文件夹,说明对历史镜像复用。
AUFS 和 container layer
Docker Container也使用AUFS作为文件系统,容器中环境等配置信息也以层级的形式存储在/var/lib/docker/aufs/xxx目录下。- Docker 在启动一个容器时,会为其创建一个只读的初始化层,用来存储这个容器内环境的相关内容;同时 docker 还会为其创建一个可读写层来执行读写操作。
container layer的 mount 目录也是/var/lib/docker/aufs/mnt,容器的元数据和配置文件存放在/var/lib/docker/container/<container-id>目录下。即使容器停止,这个读写层依然存在,所有重启重启不会丢失容器数据,只有当容器被删除时,可读写层才会一起删除。
container layer 和 image layer 有一些联系:
- 查找某个文件时先从
container layer下查找,不存在就逐层向下搜索直到找到该文件; - 对
container layer中已存在的文件修改会直接在该文件中操作;对image-layer中文件修改会将该文件完整复制到container layer中,因为镜像层只是可读的; - 删除
container layer文件直接删除,删除image layer文件会在容器层的可读写层创建对应的whitefile文件来隐藏对应文件。
实现自己的 AUFS
- 首先创建一个 aufs 目录并在该目录下创建如下文件夹和文件:
root@hecs-349201:/home/root/software/aufs# tree .
.
├── container-layer
│?? └── container-layer.txt
├── image-layer1
│?? └── image-layer1.txt
├── image-layer2
│?? └── image-layer2.txt
├── image-layer3
│?? └── image-layer3.txt
├── image-layer4
│?? └── image-layer4.txt
└── mnt
- mount 联合挂载各目录:
mount -t aufs -o dirs=./container-layer:./image-layer1:./image-layer2:./image-layer3:./image-layer4 none ./mnt
- 查看各文件夹权限:
ls
/home/xjx/aufs/container-layer=rw
/home/xjx/aufs/image-layer4=ro
/home/xjx/aufs/image-layer3=ro
/home/xjx/aufs/image-layer2=ro
/home/xjx/aufs/image-layerl=ro
- 默认第一个文件夹
container-layer可读写,其余均只可读,这里向image-layer4.txt文件写入数据:
echo -e "hosdfosndgs" >> ./mnt/image-layer4.txt
会发现修改对原始目录 image-layer4/image-layer4.txt 下不可见,反而会在可读写文件夹 ./container-layer 下创建 image-layer4.txt 文件并写入数据。
Rootfs
修改容器中的文件并不会影响宿主机,正是因为 Mount Namespace 的作用。
容器中的文件系统经过 Mount Namespace 的隔离是独立的,它修改了容器进程对文件系统 “挂载点” 的认知。只有在挂载操作发生后,进程的视图才会改变,在此之前新创建的容器会直接继承宿主机的各个挂载点。
Linux 的 chroot 命令可以实现修改挂载点的功能。
.assets/20240107173420.png)
挂载点在容器中有个更专业的名字,rootfs(根文件系统),rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这二者是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
而同一台机器上的所有容器都共享了宿主机的系统内核。这区别于虚拟机,虚拟机会模拟机器硬件。
容器中的文件系统在构建镜像的时候就被打包进去,在容器启动时挂载到根目录下。
镜像层 Layer
Docker 在镜像设计中采用了层的概念,用户制作镜像的每一步操作都会生成一个层,也就是一个增量 rootfs。层的引入实现了对 rootfs 的复用,不必每次生成一个新的 rootfs,只需要增量修改即可。
Docker 镜像层使用了一种叫做联合文件系统(Union File System) 的能力。它会将多个不同位置的目录联合挂载到同一个目录下。例如现在有 A目录、B目录、C目录,通过 UnionFS 将 A、B挂载到C目录,由于看不到 A、B目录的存在,C目录看来好像就拥有这些文件一样。
UnionFS 在不同系统中有不同实现,常见的有 aufs(ubuntu)、overlay2(centos)。
.assets/20240107173521.png)
如上图,Union mount 提供了统一视图,用户看上去好像整个系统只有一层,实际下面包含了很多层。因为镜像只包含了静态文件,但是容器会产生实时数据,所以容器的 rootfs 在镜像基础上新增了可读写层和 init 层。
容器 rootfs 包括:只读层(镜像rootfs) + init层(容器启动时初始化修改的部分数据) + 可读写层(容器中实时数据)
- 只读层:只读层是 rootfs 最下面几层,即镜像中所有层的总和,它们的挂载方式是只读(ro+wh readonly+whiteout)。
- 可读写层:可读写层为容器 rootfs 中最上面一层,它的挂载方式为 rw(read+write)。如果是删除操作,并不会实际删除文件,而是类似标记删除,比如要删除 foo 的文件,实际操作是在可读写层创建一个名为 .wh.foo 文件进行遮挡,实现对删除文件的隐藏。
- init 层:init 层位于只读层和读写层之间,用来存放容器启动时初始化修改的部分配置信息等数据。
资料
https://juejin.cn/post/7134631844103847973?searchId=20240106134245C7E62D9528092A0F9DC5
https://github.com/lixd/mydocker
https://juejin.cn/post/6971335828060504094
http://mcyou.cc/archives/xie-yi-ge-jian-dan-de-docker

浙公网安备 33010602011771号