架构概要

这里我将会从上到下分为 5 层来说明 docker 运行的原理

​​

image-20240828154433-2qlwwdc

整体的架构就是 C/S 架构,也就是 client/server 架构

第一层: Docker Daemon

Docker Daemon 的作用:它是 Docker 的守护进程,负责接收来自 Docker 客户端的命令并执行相应的操作。可以通过修改 /etc/docker/daemon.json​ 文件来对 Docker 进行配置。以下是推荐的配置示例:

{
  // 配置镜像的代理仓库,加速从 Docker Hub 拉取镜像
  "registry-mirrors": ["https://6web2eic.mirror.aliyuncs.com"],

  // 设置容器日志的存储方式
  "log-driver": "json-file",

  // 日志选项配置:限制日志文件的大小和数量
  "log-opts": {
    "max-size": "100m",  // 单个日志文件最大 100MB
    "max-file": "3"      // 最多保留 3 个日志文件
  },

  // 实验性特性配置
  "features": {
    "containerd-snapshotter": true  // 启用 containerd snapshotter,提高镜像和容器的存储性能
  },

  // 配置 Docker 的 cgroup 驱动为 systemd,以便与 kubelet 的 cgroup 驱动 systemd 兼容
  "exec-opts": ["native.cgroupdriver=systemd"]
}

解释说明:

  1. registry-mirrors​: 通过配置镜像加速器,提高从 Docker Hub 拉取镜像的速度,特别适用于网络环境受限的场景。
  2. log-driver​: 指定 Docker 容器的日志驱动为 json-file​,日志文件存储在 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log​。
  3. log-opts​: 通过 max-size​ 和 max-file​ 选项限制日志文件的大小和数量,避免日志占用过多磁盘空间。
  4. features​: 启用实验性特性 containerd-snapshotter​,该特性优化了镜像和容器的存储操作,有助于提高 Docker 的性能。
  5. exec-opts​: 将 Docker 的 cgroup 驱动配置为 systemd​,这有助于与 Kubernetes 中的 kubelet​ 的 cgroup 驱动保持一致,避免潜在的兼容性问题。

这个配置文件对 Docker 的使用进行了优化,适用于需要高效管理容器和镜像的生产环境。

第二层:containerd

随着 Docker 的发展,尤其是在 Docker 需要支持大规模的编排工具(如 Kubernetes)时,将容器管理功能从 Docker Daemon 中分离出来变得非常有必要。这种分离有助于更好地管理和扩展容器生命周期,增加了灵活性和可移植性。因此,Docker 将 containerd 独立出来,并捐赠给 CNCF,成为云原生基础设施的核心组件之一。

作用:containerd 是一个负责管理容器生命周期的守护进程。它处理镜像管理、容器执行、存储和网络配置等功能。containerd 是 Docker Daemon 和更底层的容器运行时之间的中间层,提供了一套 API,供上层调用来创建和管理容器。

关系:containerd 是 Docker Daemon 和更底层容器运行时(如 runc)之间的抽象层。它接收来自 Docker Daemon 的命令并将其转发给更底层的运行时进行实际的容器操作。

命令展示:

一台只装了 docker 的虚拟机

// 使用docker 命令行去启动了一个容器,容器id
(base) root@ubuntu:~# docker run -d --rm  busybox /bin/sh -c "sleep 1000"
c20a49ff8b2e69f13f3d4ce63073ffd64b5320820ade9c557adb5d520a82f81f

// 查看一下 容器运行
(base) root@ubuntu:~# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS     NAMES
c20a49ff8b2e   busybox   "/bin/sh -c 'sleep 1…"   5 seconds ago   Up 4 seconds             fervent_wright

// containerd的命令行工具是ctr, 也是可以看到容器的启动的.ctr是需要分命名空间的,这里为什么是moby呢?因为github上docker的开源库改为为了moby,而原先的docker库指向了自家的
// 付费产品,因为docker的流量大嘛,大家打开github想看docker,肯定第一个搜索的就是docker,你会发现没有源码,因为换成了另一个库moby.
(base) root@ubuntu:~# ctr -n moby containers ls
CONTAINER                                                           IMAGE    RUNTIME              
c20a49ff8b2e69f13f3d4ce63073ffd64b5320820ade9c557adb5d520a82f81f    -        io.containerd.runc.v2  

docker daemon 是面向 client 的命令行工具,而 containerd 更像是面向底层的容器运行时.

第三层:containerd shim

containerd shim 的出现是为了避免 containerd 直接与容器进行长时间绑定。这样,containerd 可以管理更多的容器而不被单个容器的运行所牵制。shim 使得容器运行时的结构更加松耦合,提高了系统的弹性和容错能力。而且这样做的好处就是即使 containerd 运行失败了,也不会让下层的 runc 全军覆没,有 containerd shim 就行.

作用:containerd shim 是一个小型的守护进程,用于在 containerd 和更底层的容器运行时(如 runc)之间提供抽象。它的主要功能是在容器启动后保持容器的运行,确保 containerd 可以在不直接管理容器的情况下退出或重启,同时保持容器的状态。

关系:containerd shim 位于 containerd 和 runc 之间。当 containerd 启动容器时,它会通过 containerd shim 调用 runc,runc 启动容器后,shim 会接管 runc 的任务,继续管理容器的生命周期,而 runc 则退出。

第四层:runc

runc 是为了标准化容器的创建和运行过程。它实现了 OCI 的运行时规范,确保容器可以在不同的环境中以一致的方式运行。runc 的出现,使得容器技术具有更强的可移植性和兼容性,促进了容器生态系统的发展。

作用:runc 是一个 CLI 工具,用于根据 OCI(Open Container Initiative)标准创建和运行容器。它直接与 Linux 内核交互,负责设置容器的 cgroup、namespace 和文件系统等隔离环境,并启动容器中的进程。

关系:runc 是容器执行的最底层工具。它被 containerd shim 调用,用于实际创建和启动容器。runc 完成其任务后即退出,将容器的管理交给 shim。

第五层: 资源隔离和限制.

runc 在对一个容器,或者说是一个进程进行资源隔离和限制所用到的技术主要是下面几点(下面都会说成进程,容器本身就是一种进程):

  1. rootfs1: 进程访问根目录的限制.
  2. linux namespace2: linux namespace 并不是一种 namespace,它是多种 namespace 的组合,如 pid namespace、user naespace、net namespace、mnt namespace,分别对进程实现的 pid、network、mnt、user 等的隔离
  3. cgroup3: 对进程使用 cpu 和 memory 进行的限制.
  4. aufs4: 对相同文件进行合并,这在镜像分层中起到了重要作用.但是现在 linux 系统基本默认支持的是 overlayfs,而且 docker 高一点的版本也都是用的 overlayfs 去做的镜像的分层,aufs 基本都不用了.

  1. rootfs

    1. rootfs

    rootfs​: 进程运行后的根目录,又叫根文件系统。

    操作系统 rootfs 包含了操作系统运行所需要的文件和目录。

    对于镜像而言,应用以及它运行所需的所有依赖都被打包到了 rootfs。

    容器进程运行后的根目录,就叫做 rootfs。这个根文件系统,一般来说包括下面这些文件/bin,/etc,/proc 等
    image

    运行容器时执行的/bin/bash 其实是容器的 rootfs 中/bin 目录下的 bash 可执行文件,和宿主机的 rootfs 中/bin 目录下的 bash 文件没关系。

    2. chroot

    chroot(change root)​: 改变进程的根目录,使它不能访问该目录之外的其他文件。而容器实际上就是一个特殊的进程,所以我们就可以使用 chroot 限制容器的文件访问,使得每个容器都有一个自己的文件系统。

    chroot [OPTION] NEWROOT [COMMAND [ARG]...]

    • NEWROOT:表示切换到的新的 root 目录

    • COMMAND:表示切换 root 目录后执行的命令

    现在写一个 go 项目输出/目录下的所有文件

    func main(){
    	fds,err := os.ReadDir("/")
    	if err != nil{
    		panic(err)
    	}
    	for _,fd:=range fds{
    		if fd.IsDir(){
    			fmt.Print(fd.Name()+" ")
    		}
    	}
    	fmt.Println()
    }
    // 打包
    go build -o server main.go
    //将server复制到/opt/chroot下
    cp server /opt/chroot
    
    (base) root@ubuntu:/opt/chroot# ls -la
    total 2040
    drwxr-xr-x 2 root root    4096 Aug  3 15:43 .
    drwxr-xr-x 6 root root    4096 Aug  3 15:43 ..
    -rwxr-xr-x 1 root root 2080126 Aug  3 15:37 server
    
    //执行server,将显示系统本身根目录下的目录
    (base) root@ubuntu:/opt/chroot# ./server 
    app boot dev etc home lost+found media mnt opt proc project root run snap srv sys tmp usr var 
    

    创建一个目录 go-server,作为 server 命令的新的 root 目录,并在 go-server 目录下创建几个目录以供 server 命令使用。

    (base) root@ubuntu:/opt/chroot# mkdir -p go-server/{test1,test2,test3,test4}
    (base) root@ubuntu:/opt/chroot# ls go-server/
    test1  test2  test3  test4
    

    将 go-server 目录作为 server 命令来执行。

    // 将server命令移到go-server的目录或者子目录下
    (base) root@ubuntu:/opt/chroot# mv server go-server/
    
    // 查看目录结构
    (base) root@ubuntu:/opt/chroot# tree go-server/
    go-server/
    ├── server
    ├── test1
    ├── test2
    ├── test3
    └── test4
    
    // 先单独执行一下 server 命令,会输出系统根目录下的目录
    (base) root@ubuntu:/opt/chroot# ./go-server/server 
    app boot dev etc home lost+found media mnt opt proc project root run snap srv sys tmp usr var 
    
    // 添加新的根目录/go-server,执行
    (base) root@ubuntu:/opt/chroot# chroot go-server/ /server
    test1 test2 test3 test4
    
    //更换根目录,再次执行
    (base) root@ubuntu:/opt# ls -la chroot/
    total 16
    drwxr-xr-x 4 root root 4096 Aug  3 16:06 .
    drwxr-xr-x 6 root root 4096 Aug  3 15:43 ..
    drwxr-xr-x 6 root root 4096 Aug  3 15:53 go-server
    drwxr-xr-x 5 root root 4096 Aug  3 16:06 ls_test
    (base) root@ubuntu:/opt# chroot chroot/ /go-server/server
    go-server ls_test 
    

    其实我们还可以给系统本身的命令新的 root 目录,这里以 ls 命令为例

    // 创建所需的目录
    (base) root@ubuntu:/opt/chroot# mkdir -p ls_test/{bin,lib,lib64}
    
    // 将ls命令复制到ls_test/bin目录中
    (base) root@ubuntu:/opt/chroot# cp /bin/ls ls_test/bin/
    
    // 查看ls的所有依赖
    (base) root@ubuntu:/opt/chroot# ldd /bin/ls
            linux-vdso.so.1 (0x00007ffe396bc000)
            libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fc398b87000)
            libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc398995000)
            libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fc398904000)
            libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fc3988fe000)
            /lib64/ld-linux-x86-64.so.2 (0x00007fc398be1000)
            libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc3988db000)
    
    // 将所有依赖也放入到对应的目录中
    (base) root@ubuntu:/opt/chroot# cp /lib/x86_64-linux-gnu/libselinux.so.1 ls_test/lib
    (base) root@ubuntu:/opt/chroot# cp /lib/x86_64-linux-gnu/libc.so.6 ls_test/lib
    (base) root@ubuntu:/opt/chroot# cp /lib/x86_64-linux-gnu/libpcre2-8.so.0 ls_test/lib
    (base) root@ubuntu:/opt/chroot# cp /lib/x86_64-linux-gnu/libdl.so.2 ls_test/lib
    (base) root@ubuntu:/opt/chroot# cp /lib64/ld-linux-x86-64.so.2 ls_test/lib64/
    (base) root@ubuntu:/opt/chroot# cp /lib/x86_64-linux-gnu/libpthread.so.0 ls_test/lib
    

    给 ls 命令添加新的 root 目录

    // 先单独执行一下命令,显示的是我们系统根目录下的文件
    (base) root@ubuntu:/opt/chroot# ./ls_test/bin/ls /
    app   lib         mnt      sbin      usr          wget-log.12  wget-log.18  wget-log.5
    bin   lib32       opt      snap      var          wget-log.13  wget-log.19  wget-log.6
    boot  lib64       proc     srv       wget-log     wget-log.14  wget-log.2   wget-log.7
    dev   libx32      project  swap.img  wget-log.1   wget-log.15  wget-log.20  wget-log.8
    etc   lost+found  root     sys       wget-log.10  wget-log.16  wget-log.3   wget-log.9
    home  media       run      tmp       wget-log.11  wget-log.17  wget-log.4
    
    //给ls命令添加新的root目录
    (base) root@ubuntu:/opt/chroot# chroot ./ls_test/ /bin/ls /
    bin  lib  lib64
    

    现在我们使用一下 docker 执行容器时,经常用到的/bin/bash 命令

    // 创建一个新的root目录供bash命令使用
    (base) root@ubuntu:/opt/chroot# mkdir new_rootfs
    
    // 将上面的ls命令都复制到新的目录下
    (base) root@ubuntu:/opt/chroot# cp -R ls_test/* new_rootfs/
    
    */ //  bash 命令复制到 new_rootfs/bin/目录下
    (base) root@ubuntu:/opt/chroot# cp /bin/bash new_rootfs/bin/
    
    // 查看bash命令依赖的库
    (base) root@ubuntu:/opt/chroot# ldd /bin/bash
            linux-vdso.so.1 (0x00007ffc9d9b8000)
            libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f62405ed000)
            libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f62405e7000)
            libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f62403f5000)
            /lib64/ld-linux-x86-64.so.2 (0x00007f6240753000)
    
    // 将依赖库都复制到目录下
    (base) root@ubuntu:/opt/chroot# cp /lib/x86_64-linux-gnu/libtinfo.so.6 /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libc.so.6 new_rootfs/lib/
    (base) root@ubuntu:/opt/chroot# cp /lib64/ld-linux-x86-64.so.2 new_rootfs/lib64/
    
    // 目录结构
    (base) root@ubuntu:/opt/chroot# tree new_rootfs/
    new_rootfs/
    ├── bin
       ├── bash
       └── ls
    ├── lib
       ├── libc.so.6
       ├── libdl.so.2
       ├── libpcre2-8.so.0
       ├── libpthread.so.0
       ├── libselinux.so.1
       └── libtinfo.so.6
    └── lib64
        └── ld-linux-x86-64.so.2
    
    3 directories, 9 files
    

    给 bash 命令新的 root 目录

    (base) root@ubuntu:/opt/chroot# chroot new_rootfs/ /bin/bash
    bash-5.0# ls -la ./*  
    */ //目录结构
    ./bin:
    total 1304
    drwxr-xr-x 2 0 0    4096 Aug  3 16:44 .
    drwxr-xr-x 5 0 0    4096 Aug  3 16:44 ..
    -rwxr-xr-x 1 0 0 1183448 Aug  3 16:44 bash
    -rwxr-xr-x 1 0 0  142144 Aug  3 16:44 ls
    
    ./lib:
    total 3092
    drwxr-xr-x 2 0 0    4096 Aug  3 16:46 .
    drwxr-xr-x 5 0 0    4096 Aug  3 16:44 ..
    -rwxr-xr-x 1 0 0 2029592 Aug  3 16:46 libc.so.6
    -rw-r--r-- 1 0 0   18848 Aug  3 16:46 libdl.so.2
    -rw-r--r-- 1 0 0  588488 Aug  3 16:44 libpcre2-8.so.0
    -rwxr-xr-x 1 0 0  157224 Aug  3 16:44 libpthread.so.0
    -rw-r--r-- 1 0 0  163200 Aug  3 16:44 libselinux.so.1
    -rw-r--r-- 1 0 0  192032 Aug  3 16:46 libtinfo.so.6
    
    ./lib64:
    total 196
    drwxr-xr-x 2 0 0   4096 Aug  3 16:44 .
    drwxr-xr-x 5 0 0   4096 Aug  3 16:44 ..
    -rwxr-xr-x 1 0 0 191504 Aug  3 16:47 ld-linux-x86-64.so.2
    
    // 使用 cat 命令试一下
    bash-5.0# cat bin/ls 
    bash: cat: command not found
    //发现并不行,这是因为我们在这个新的rootfs中只添加了ls命令
    
    // 但是我们还可以使用bash的内置命令,`cd`、`echo`、`export` 等
    bash-5.0# cd lib
    bash-5.0# ls
    libc.so.6  libdl.so.2  libpcre2-8.so.0  libpthread.so.0  libselinux.so.1  libtinfo.so.6
    bash-5.0# echo helloworld
    helloworld
    // 当使用cd / 时也会到我们新的root目录下
    

    其实到这里我们就大概明白 docker 是怎么调用 /bin/bash 命令了,应该就是 docker exec .. /bin/bash 时会使用 chroot 去为 docker 的进程中的/bin/bash 开一个新的 root 目录。

    3. busybox

    busybox​: 是一个集成了一百多个最常用 linux 命令和工具的软件,甚至还集成了一个 http 服务器和一个 telnet 服务器,但是占用储存很小。

    像上面出现的问题,因为没有加入 cat 命令,然后 cat 就无法使用,那我们需要将所有的基础命令都一个一个加进来的话,一是很麻烦,二是占用内存很大,所以可以使用 busybox 实现 linux 基础命令的使用

    获取 busybox 的方法:

    1. 官网下载 busybox 的包,然后手动进行编译。
    2. docker 运行 busybox 容器,并将 busybox 中的根目录复制出来。

    我这里使用第二种:

    // 创建busybox目录
    (base) root@ubuntu:/opt/chroot# mkdir busybox/
    // 启动busybox容器
    (base) root@ubuntu:/opt/chroot# docker run -d --rm busybox sh -c "sleep 1000"
    a3b7bb10695d3263fdc919745949537c02e62b1e5b4dfd91306869acecbd05d1
    // 将容器中的根目录复制出来
    (base) root@ubuntu:/opt/chroot# docker cp sad_maxwell:/bin /opt/chroot/busybox/
    Successfully copied 1.47MB to /opt/chroot/busybox/
    

    我们想在对比一下所占储存的大小

    // 系统目录:bin目录占用393M,lib目录占用1.8G
    (base) root@ubuntu:/opt/chroot/busybox# du -sh /usr/*
    */
    393M    /usr/bin
    4.0K    /usr/games
    23M     /usr/include
    1.8G    /usr/lib
    4.0K    /usr/lib32
    4.0K    /usr/lib64
    140M    /usr/libexec
    4.0K    /usr/libx32
    254M    /usr/local
    33M     /usr/sbin
    225M    /usr/share
    264M    /usr/src
    
    // busybox目录:就只占用了1.2M
    (base) root@ubuntu:/opt/chroot# du -sh busybox/*
    */
    1.2M    busybox/bin
    
    // 当然系统中肯定有很多其他的命令,但是我们使用容器时,用到的命令,一般
    // 就是最基础的那些linux命令,所以这也是为什么容器中都会使用busybox中的命令
    

    使用 busybox 添加一个创建一个新的 rootfs。

    (base) root@ubuntu:/opt/chroot# chroot busybox/ /bin/sh
    / # ls -la
    total 20
    drwxr-xr-x    3 0        0             4096 Aug  3 19:25 .
    drwxr-xr-x    3 0        0             4096 Aug  3 19:25 ..
    drwxr-xr-x    2 0        0            12288 Dec 29  2021 bin
    / # veee
    /bin/sh: eee: not found
    / # echo 'hello world' > hello
    / # ls -la
    total 24
    drwxr-xr-x    3 0        0             4096 Aug  3 19:34 .
    drwxr-xr-x    3 0        0             4096 Aug  3 19:34 ..
    drwxr-xr-x    2 0        0            12288 Dec 29  2021 bin
    -rw-r--r--    1 0        0               12 Aug  3 19:34 hello
    / # cat hello 
    hello world
    / # vi hello 
    / # cat hello 
    hello world
    hello vi
    / # exit
    

    在这里我们就可以完整的使用 linux 最基本的命令了。

    4. 检测隔离性

    将宿主机中的进程挂载到新的 rootfs 中

    // 创建一个proc目录
    / # mkdir proc
    
    // 目录中没有任何文件
    / # ls -la proc/
    total 8
    drwxr-xr-x    2 0        0             4096 Aug  4 08:25 .
    drwxr-xr-x    4 0        0             4096 Aug  4 08:25 ..
    
    // 无法查看任何进程
    / # ps -ef
    PID   USER     TIME  COMMAND
    
    // 将系统的进程挂载到新的rootfs下
    / # mount -t proc proc /proc/
    
    // 查看 /proc文件夹 是有进程文件的
    / # ls /proc/ | wc -l
    347
    
    //查看进程
    / # ps -ef | tail
    479750 0         0:00 tail
    1339388 0         0:05 /lib/systemd/systemd --user
    1339397 0         0:00 (sd-pam)
    1370493 0         0:00 /root/.vscode-server/code-f1b07bd25dfad64b0167beb15359ae573aecd2cc command-shell --cli-data-dir /root/.vscode-server/cli --on-port --require-token 87f4bc72fac6
    1370528 0         0:00 /bin/sh
    1497443 0         0:00 /root/.vscode-server/code-f1b07bd25dfad64b0167beb15359ae573aecd2cc command-shell --cli-data-dir /root/.vscode-server/cli --on-port --require-token 9e0a5eb6aac7
    1497478 0         0:00 /bin/sh
    2246221 0         0:02 kubectl exec -it -n jenkins godemo-131-806d4-h13fx-nz0lq -c docker sh
    2326527 111      10:54 /usr/bin/fwupdmgr refresh
    3931508 0        16:16 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
    

    现在检查一下隔离性

    // 在宿主机中启动一个进程
    (base) root@ubuntu:/opt/chroot# sleep 1000 &
    [1] 480012
    
    // 查看
    (base) root@ubuntu:/opt/chroot# ps -ef | grep sleep
    root      479780  476214  0 08:28 ?        00:00:00 sleep 180
    root      480012  479830  0 08:31 pts/1    00:00:00 sleep 1000
    root      480088  479830  0 08:31 pts/1    00:00:00 grep --color=auto sleep
    
    // 进入新的rootfs下,可以看到是有这个进程的
    / # ps -ef | grep sleep
    480012 0         0:00 sleep 1000
    480111 0         0:00 sleep 180
    480169 0         0:00 grep sleep
    
    //现在将进程kill掉
    / # kill -9 480012
    
    // 回到宿主机总查看,sleep 1000 的进程消失
    (base) root@ubuntu:/opt/chroot# ps -ef | grep sleep
    [1]+  Killed                  sleep 1000
    root      480111  476214  0 08:31 ?        00:00:00 sleep 180
    root      480277  480273  0 08:33 ?        00:00:00 sleep 1
    root      480280  479830  0 08:33 pts/1    00:00:00 grep --color=auto sleep
    
    

    所以我们看到进程并没有做到隔离,这在容器中是不一样的。

    5. 小结

    chroot​: 改变进程的根目录,是他不能访问该目录之外的其他目录。

    rootfs​: 操作系统或者镜像运行的根目录,可以由 chroot 进行切换。

    busybox​: 可以做小的 rootfs。

    上面也只是实现了最基本的文件隔离,但是进程这些并没有进行隔离。

  2. namespace

    1. 简介

    Linux namespace​的主要作用是做了一层资源隔离,使得在 namespace 中的进程或者进程组可以看起来拥有自己的独立资源。

    这种隔离机制和 chroot 很类似,chroot 是为某个进程创建新的 root 目录,使得进程无法访问 root 目录外的其他的目录。而 Linux namespace 在此基础之上,提供了对 UTS、IPC、Mount、PID、Network、User 等隔离机制。

    比如拥有独立的 PID 编号,独立的网络(独立 ip 和端口)、独立的 hostname。

    image

    • cgroup​:控制组命名空间,隔离进程的资源控制设置。
    • ipc​:进程间通信命名空间,隔离信号量、消息队列和共享内存。
    • mnt​:挂载命名空间,隔离挂载点。
    • net​:网络命名空间,隔离网络设备、IP 地址、防火墙规则等。
    • pid​:PID 命名空间,隔离进程 ID。
    • user​:用户命名空间,隔离用户和组 ID。
    • uts​:UTS 命名空间,隔离主机名和域名。

    2. API

    clone()​:传递特定的 flag(CLONE_NEW*)标志给 clone(),则会根据每个标志创建对应的新的 namespace 并且将子进程添加为其成员。

    setns()​:允许进程加入一个已存在的 namespace 中。

    unshare()​:允许进程取消其执行上下文,可以利用此系统调用来让当前的进程移动至一个新的 namespace 中。

    3. unshare

    sudo unshare --pid --fork --mount-proc /bin/sh
    
    • sudo​:以超级用户权限运行命令,因为创建新的命名空间通常需要超级用户权限。
    • unshare​:这个命令用于创建新的命名空间并在其中运行指定的命令。
    • --pid​:创建一个新的 PID 命名空间。在这个新的命名空间中,进程将具有独立的 PID 视图。例如,新的 shell 会看到自己作为 PID 1。
    • --fork​:在新的命名空间中 fork 一个新的进程来运行指定的命令。这样做的目的是确保命令在新的命名空间中运行,而不是在当前命名空间中。
    • --mount-proc​:在新的命名空间中挂载一个新的 /proc​ 文件系统。这是为了确保新的命名空间中的进程能够正确访问 /proc​ 文件系统中的信息。这个选项通常与 --pid​ 一起使用,因为 PID 命名空间中的进程需要一个独立的 /proc​ 文件系统。
    • /bin/sh​:这是要在新的命名空间中运行的命令。在这个例子中,它是一个新的 shell(/bin/sh​)。
    (base) root@ubuntu:/opt/namespace# sudo unshare --pid --fork --mount-proc /bin/sh
    # sleep 1000 &
    # ps -ef
    UID          PID    PPID  C STIME TTY          TIME CMD
    root           1       0  0 15:30 pts/2    00:00:00 /bin/sh
    root           2       1  0 15:30 pts/2    00:00:00 sleep 1000
    root           3       1  0 15:30 pts/2    00:00:00 ps -ef
    # ls -l /proc/2/ns
    total 0
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 cgroup -> 'cgroup:[4026531835]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 ipc -> 'ipc:[4026531839]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 mnt -> 'mnt:[4026532743]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 net -> 'net:[4026531992]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 pid -> 'pid:[4026532744]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 pid_for_children -> 'pid:[4026532744]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 user -> 'user:[4026531837]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:31 uts -> 'uts:[4026531838]'
    # exit
    /bin/sh: 5: Cannot set tty process group (No such process)
    
    // 切换到 宿主机 中
    (base) root@ubuntu:/opt/namespace# sleep 1000 &
    [1] 533796
    (base) root@ubuntu:/opt/namespace# ps -ef | grep sleep
    root      533255  528046  0 15:29 ?        00:00:00 sleep 180
    root      533796  531208  0 15:31 pts/2    00:00:00 sleep 1000
    root      533817  531208  0 15:31 pts/2    00:00:00 grep --color=auto sleep
    (base) root@ubuntu:/opt/namespace# ls -l /proc/533796/ns/
    total 0
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 cgroup -> 'cgroup:[4026531835]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 ipc -> 'ipc:[4026531839]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 mnt -> 'mnt:[4026531840]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 net -> 'net:[4026531992]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 pid -> 'pid:[4026531836]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 pid_for_children -> 'pid:[4026531836]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 user -> 'user:[4026531837]'
    lrwxrwxrwx 1 root root 0 Aug  5 15:32 uts -> 'uts:[4026531838]'
    

    在上面操作中我们可以看到两个进程的 ns 中 mnt 和 pid 不同,对应前面的 --pid​ 和 --mount-proc​,给 namespace 中进程提供了新的 pid namespace 和 mnt namespace。

    4. clone

    clone 命令在 linux 中的源码

    int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);
    
    • fn​:子进程将执行的函数的指针。
    • child_stack​:子进程的栈指针。需要为子进程分配一个新的栈,并将其地址传递给 child_stack​。
    • flags​:控制子进程行为的标志。可以使用多个标志组合来指定子进程与父进程共享或隔离的资源。
    • arg​:传递给子进程函数 fn​ 的参数。
    • 其他可选参数:用于传递子进程的 PID、TLS 等。
    常用标志

    flags​ 参数是一个位掩码,可以由以下常用标志组合而成:

    • CLONE_VM​:父进程和子进程共享同一个内存空间。
    • CLONE_FS​:父进程和子进程共享文件系统信息(当前工作目录、根目录等)。
    • CLONE_FILES​:父进程和子进程共享文件描述符表。
    • CLONE_SIGHAND​:父进程和子进程共享信号处理器。
    • CLONE_PARENT​:子进程的父进程为调用 clone​ 的进程的父进程,而不是调用 clone​ 的进程本身。
    • CLONE_THREAD​:子进程作为调用 clone​ 的进程的线程,属于同一个线程组。
    • CLONE_NEWNS​:创建新的挂载命名空间。
    • CLONE_NEWPID​:创建新的 PID 命名空间。
    • CLONE_NEWNET​:创建新的网络命名空间。
    • CLONE_NEWUSER​:创建新的用户命名空间。
    • CLONE_NEWUTS​:创建新的 UTS 命名空间。
    • CLONE_NEWIPC​:创建新的 IPC 命名空间

    使用 clone 创建一个完全隔离的子进程,这里我使用 go 语言写的(因为不会 c...)

    package main
    
    import (
    	"fmt"
    	"os"
    	"os/exec"
    	"syscall"
    )
    
    const stackSize = 1024 * 1024 // 定义子进程栈大小
    
    // 子进程执行的函数
    func child() {
    	fmt.Println("Inside child process:")
    	fmt.Printf("PID: %d\n", syscall.Getpid())
    	fmt.Printf("PPID: %d\n", syscall.Getppid())
    
    	// 执行一个简单的命令
    	cmd := exec.Command("ls", "-l", "/proc/self/ns")
    	cmd.Stdout = os.Stdout
    	cmd.Stderr = os.Stderr
    	if err := cmd.Run(); err != nil {
    		fmt.Println("Error:", err)
    	}
    
    	// 进入一个新的 shell
    	cmd = exec.Command("/bin/sh")
    	cmd.Stdout = os.Stdout
    	cmd.Stderr = os.Stderr
    	cmd.Stdin = os.Stdin
    	if err := cmd.Run(); err != nil {
    		fmt.Println("Error:", err)
    	}
    }
    
    func main() {
    	// 为子进程分配栈
    	stack := make([]byte, stackSize)
    
    	// 使用 clone 创建子进程,并指定命名空间标志
    	flags := syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWUSER | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.SIGCHLD
    	pid, _, errno := syscall.RawSyscall(syscall.SYS_CLONE, uintptr(flags), uintptr(unsafe.Pointer(&stack[len(stack)-1])), 0)
    	if errno != 0 {
    		fmt.Println("Error:", errno)
    		os.Exit(1)
    	}
    
    	if pid == 0 {
    		// 子进程执行
    		child()
    		os.Exit(0)
    	} else {
    		// 父进程等待子进程结束
    		var ws syscall.WaitStatus
    		_, err := syscall.Wait4(int(pid), &ws, 0, nil)
    		if err != nil {
    			fmt.Println("Error:", err)
    			os.Exit(1)
    		}
    
    		fmt.Println("Child process has terminated.")
    	}
    }
    

    5. 小结

    在 namespace 阶段就可以实现进程、用户、网络等的隔离。

  3. CGroup

    1. 简介

    CGroup 的全称是 Control Group,主要作用是限制进程使用的资源,是容器实现环境隔离的两种关键技术之一。

    CGroup 的所有操作都是基于 cgroup virtual filesystem,这个文件系统一般挂载在 /sys/fs/cgroup​目录下,可以查看能够限制哪些资源。

    (base) root@ubuntu:/opt/namespace# ls -l /sys/fs/cgroup/
    total 0
    dr-xr-xr-x 6 root root  0 May 30 07:54 blkio
    lrwxrwxrwx 1 root root 11 May 30 07:54 cpu -> cpu,cpuacct
    lrwxrwxrwx 1 root root 11 May 30 07:54 cpuacct -> cpu,cpuacct
    dr-xr-xr-x 6 root root  0 May 30 07:54 cpu,cpuacct
    dr-xr-xr-x 3 root root  0 May 30 07:54 cpuset
    dr-xr-xr-x 6 root root  0 May 30 07:54 devices
    dr-xr-xr-x 4 root root  0 May 30 07:54 freezer
    dr-xr-xr-x 3 root root  0 May 30 07:54 hugetlb
    dr-xr-xr-x 6 root root  0 May 30 07:54 memory
    lrwxrwxrwx 1 root root 16 May 30 07:54 net_cls -> net_cls,net_prio
    dr-xr-xr-x 3 root root  0 May 30 07:54 net_cls,net_prio
    lrwxrwxrwx 1 root root 16 May 30 07:54 net_prio -> net_cls,net_prio
    dr-xr-xr-x 3 root root  0 May 30 07:54 perf_event
    dr-xr-xr-x 6 root root  0 May 30 07:54 pids
    dr-xr-xr-x 3 root root  0 May 30 07:54 rdma
    dr-xr-xr-x 6 root root  0 May 30 07:54 systemd
    dr-xr-xr-x 6 root root  0 Jun  5 03:28 unified
    

    2. 对单个进程进行限制

    限制进程的 cpu 使用。

    (base) root@ubuntu:/sys/fs/cgroup/cpu# cd /sys/fs/cgroup/cpu
    
    // 创建一个目录用来对进程做限制
    (base) root@ubuntu:/sys/fs/cgroup/cpu# mkdir go_test
    
    // 系统会自动挂载相关的参数文件
    (base) root@ubuntu:/sys/fs/cgroup/cpu# ls -la go_test/
    total 0
    drwxr-xr-x 2 root root 0 Aug  6 14:58 .
    dr-xr-xr-x 8 root root 0 May 30 07:54 ..
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cgroup.clone_children
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cgroup.procs
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpuacct.stat
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cpuacct.usage
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpuacct.usage_all
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpuacct.usage_percpu
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpuacct.usage_percpu_sys
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpuacct.usage_percpu_user
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpuacct.usage_sys
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpuacct.usage_user
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cpu.cfs_period_us
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cpu.cfs_quota_us
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cpu.shares
    -r--r--r-- 1 root root 0 Aug  6 14:58 cpu.stat
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cpu.uclamp.max
    -rw-r--r-- 1 root root 0 Aug  6 14:58 cpu.uclamp.min
    -rw-r--r-- 1 root root 0 Aug  6 14:58 notify_on_release
    -rw-r--r-- 1 root root 0 Aug  6 14:58 tasks
    

    编写一个 cpu 高占用的程序

    package main
    
    import (
    	"runtime"
    )
    
    func busyLoop() {
    	for {
    		// 这是一个无限循环,占用CPU时间
    	}
    }
    
    func main() {
    	// 获取CPU核心数量
    	numCPU := runtime.NumCPU()
    
    	// 设置最大并发线程数为CPU核心数量
    	runtime.GOMAXPROCS(numCPU/2)
    
    	// 启动与CPU核心数量相同的Goroutine
    	for i := 0; i < numCPU; i++ {
    		go busyLoop()
    	}
    
    	// 阻止主函数退出
    	select {}
    }
    
    

    打包,运行。

    (base) root@ubuntu:/opt/cgroup# go build -o high_cpu main.go 
    (base) root@ubuntu:/opt/cgroup# ./high_cpu &
    [1] 585806
    
    //top 查看high_cpu占用cpu的一半左右
    top - 15:09:35 up 68 days,  7:15,  1 user,  load average: 1.79, 1.20, 1.09
    Tasks: 287 total,   2 running, 284 sleeping,   1 stopped,   0 zombie
    %Cpu(s): 50.1 us,  0.2 sy,  0.0 ni, 49.5 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
    MiB Mem :  16008.3 total,   5212.1 free,   1243.8 used,   9552.4 buff/cache
    MiB Swap:   4096.0 total,   4092.7 free,      3.2 used.  14424.9 avail Mem 
    
        PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND           
     585806 root      20   0 1225164   1376    856 R 400.3   0.0   1:46.62 high_cpu          
     571083 root      20   0 1347388 125840  48816 S   1.0   0.8   1:45.27 node              
     571110 root      20   0 1275320  75244  43192 S   0.3   0.5   1:57.25 node              
     579167 root      20   0   13820   9056   7528 S   0.3   0.1   0:01.87 sshd              
     579272 root      20   0   44792  16580  11876 S   0.3   0.1   0:03.55 code-b1c0a14de1   
     579309 root      20   0   23.3g 583352  56208 S   0.3   3.6   0:27.26 node              
     584861 root      20   0    9376   4008   3252 R   0.3   0.0   0:01.93 top               
          1 root      20   0  171560  12324   7672 S   0.0   0.1  25:40.95 systemd           
          2 root      20   0       0      0      0 S   0.0   0.0   0:02.78 kthreadd          
          3 root       0 -20       0      0      0 I   0.0   0.0   0:00.00 rcu_gp   
    

    将进程写入到新建的 cgroup 中。

    (base) root@ubuntu:/sys/fs/cgroup/cpu# cd /sys/fs/cgroup/cpu
    // 将进程写入到cgroup.procs文件中
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# cat cgroup.procs 
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# echo 585806 > cgroup.procs 
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# cat cgroup.procs 
    585806
    // tasks会自动写入和585806有关的协程,查看可通过 ps -eLf | grep <pid>
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# cat tasks 
    585806
    585808
    585809
    585810
    585811
    585812
    

    限制进程 cpu 的使用,主要通过两个参数:

    • cpu.cfs_period_us​:定义分配周期的时间长度(以微秒为单位)。
    • cpu.cfs_quota_us​:定义在这个周期中该 cgroup 可以使用的 CPU 时间(以微秒为单位)。
    // 先看一下,限制前占用率。
    // 开始为 -1,不做任何限制。
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# cat cpu.cfs_quota_us 
    -1
    // 400% 左右,就是cpu的一半
        PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND           
     585806 root      20   0 1225164   1376    856 R 401.3   0.0  91:13.16 high_cpu 
    
    //做限制,限制50ms
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# echo 50000 > cpu.cfs_quota_us 
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# cat cpu.cfs_quota_us 
    50000
    // 使用率降到50%左右
        PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND           
     585806 root      20   0 1225164   1376    856 R  49.5   0.0  96:06.61 high_cpu   
    

    删除进程

    (base) root@ubuntu:/opt/cgroup# kill -9 585806
    // cgroup中对应的文件也会删除
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# cat cgroup.procs 
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# cat tasks 
    (base) root@ubuntu:/sys/fs/cgroup/cpu/go_test# 
    

    3. cgroup 驱动

    cgroup 驱动分为两种:

    1. cgroupfs: 这个是直接去 cgroup 文件进行更改,和上面的操作一样.

    2. systemd: 一种命令行工具,可以用本身的命令去更加便捷的更改我们的 cgroup,底层也是调用 cgroupfs.

    当你使用 k8s 去做容器编排工具和使用 docker 去做容器运行时的时候,你应该注意一下 cgroup 的选择.kubelet 默认使用的是 systemd,而 docker 默认使用的是 cgroupfs,所以你应该更改一下 docker 的驱动,来更好的适配 kubelet.

  4. overlayFS

    1. 概述

    1.1 Union File System

    Union File System ​:简称 UnionFS 是一种为 Linux FreeBSD NetBSD 操作系统设计的,把其他文件系统联合到一个联合挂载点的文件系统服务。

    它使用 branch 不同文件系统的文件和目录“透明地​”覆盖,形成一个单一一致的文件系统。

    这些 branches 或者是 read-only 或者是 read-write 的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为 unionfs 用到了一个重要的资管管理技术叫写时复制。

    写时复制(copy-on-write,下文简称 CoW) ,也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。

    它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源,这个资源可以被新旧实例共享。

    创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增减小部分的开销。

    UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。

    比如,我现在有两个目录 A 和 B,它们分别有两个文件:

    $ tree
    .
    ├── A
    │  ├── a
    │  └── x
    └── B
      ├── b
      └── x
    

    然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

    $ mkdir C
    $ mount -t aufs -o dirs=./A:./B none ./C
    

    这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

    $ tree ./C
    ./C
    ├── a
    ├── b
    └── x
    

    可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。

    这就是联合文件系统,目的就是将多个文件联合在一起成为一个统一的视图

    1.2 AUFS

    AuFS​:的全称是 Another UnionFS,后改名为 Alternative UnionFS,再后来干脆改名叫作 Advance UnionFS。

    AUFS 完全重写了早期的 UnionFS 1.x,其主要目的是为了可靠性和性能,并且引入了一些新的功能,比如可写分支的负载均衡。

    AUFS 的一些实现已经被纳入 UnionFS 2.x 版本。

    AUFS 只是 Docker 使用的存储驱动的一种,除了 AUFS 之外,Docker 还支持了不同的存储驱动,包括 aufs​、devicemapper​、overlay2​、zfs​ 和 vfs​ 等等,在最新的 Docker 中,overlay2​ 取代了 aufs​ 成为了推荐的存储驱动,但是在没有 overlay2​ 驱动的机器上仍然会使用 aufs​ 作为 Docker 的默认驱动。

    1.3 overlayfs

    Overlayfs 是一种类似 aufs 的一种堆叠文件系统,于 2014 年正式合入 Linux-3.18 主线内核,目前其功能已经基本稳定(虽然还存在一些特性尚未实现)且被逐渐推广,特别在容器技术中更是势头难挡。Overlayfs 是一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如 ext4fs 和 xfs 等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现。

    overlayfs​: 一般分为 lower、upper、merged 和 work 4 个目录。

    • lower​:只读层,该层数据不会被修改。
    • upper​:可读写层,所有修改都会发生在这一层,即使是修改的 lower 中的数据。
    • merged​:视图层,可以看到 lower、upper 中的所有内容。
    • work​:overlayfs 自己去使用的,对我们用户来说就是无感的。

    假设我们有 dir1 和 dir2 两个目录:

      dir1                    dir2
        /                       /
          a                       a
          b                       c
    

    然后我们可以把 dir1 和 dir2 挂载到 dir3 上,就像这样:

     dir3
        /
          a
          b
          c
    

    2. overlayFS 演示

    2.1 进行合并

    创建目录

    mkdir ./{merged,work,upper,lower}
    ls -la
    echo "common in lower" > lower/common
    echo "common in upper" > upper/common
    echo "xxo in lower" > lower/xxo
    echo "foo in upper" > upper/foo
    

    目录结构

    (base) root@ubuntu:/opt/overlayfs# tree 
    .
    ├── lower
       ├── common
       └── xxo
    ├── merged
    ├── upper
       ├── common
       └── foo
    └── work
        └── work
    

    使用 overlayfs 将 lower​ 和 upper​ 的目录合并到 merged​目录

    mount -t overlay  overlay -o lowerdir=./lower/,upperdir=./upper/,workdir=./work/ ./merged/
    

    查看目录结构

    (base) root@ubuntu:/opt/overlayfs# tree 
    .
    ├── lower
       ├── common
       └── xxo
    ├── merged
       ├── common
       ├── foo
       └── xxo
    ├── upper
       ├── common
       └── foo
    └── work
        └── work
    
    5 directories, 7 files
    

    merged 目录下挂载了三个集成文件,common foo xxo

    分别查看文件内容
    (base) root@ubuntu:/opt/overlayfs# cat merged/common 
    common in upper
    (base) root@ubuntu:/opt/overlayfs# cat merged/foo 
    foo in upper
    (base) root@ubuntu:/opt/overlayfs# cat merged/xxo
    xxo in lower
    

    上面可以看到被合并的 common 文件显示的是 upper 的文件内容。

    所以可推理当合并时出现 upper 和 lower 层一样的文件,那么 upper 层的会将 lower 的文件覆盖掉。

    image

    2.2 修改文件

    修改 common 文件

    (base) root@ubuntu:/opt/overlayfs# cat merged/common 
    common in upper
    
    // 修改merged目录下的common文件,upper改变,lower不变
    (base) root@ubuntu:/opt/overlayfs# cat merged/common 
    common in upper
    (base) root@ubuntu:/opt/overlayfs# echo "common in merged" > merged/common 
    (base) root@ubuntu:/opt/overlayfs# cat merged/common 
    common in merged
    (base) root@ubuntu:/opt/overlayfs# cat upper/common 
    common in merged
    (base) root@ubuntu:/opt/overlayfs# cat lower/common 
    common in lower
    
    // 修改upper目录下的common文件,merged改变,lower不变
    (base) root@ubuntu:/opt/overlayfs# echo "common in upper" >  upper/common 
    (base) root@ubuntu:/opt/overlayfs# cat merged/common 
    common in upper
    (base) root@ubuntu:/opt/overlayfs# cat upper/common 
    common in upper
    (base) root@ubuntu:/opt/overlayfs# cat lower/common 
    common in lower
    
    // 修改lower目录下的common文件,merged不变,upper不变
    (base) root@ubuntu:/opt/overlayfs# echo "common in lower2" > lower/common 
    (base) root@ubuntu:/opt/overlayfs# cat merged/common 
    common in upper
    (base) root@ubuntu:/opt/overlayfs# cat upper/common 
    common in upper
    (base) root@ubuntu:/opt/overlayfs# cat lower/common 
    common in lower2
    

    修改 xxo 文件

    修改 lower/xxo 文件,merged/xxo 也会跟着改变。虽然 lower 层是只读的的,但是也是对于客户端只读,在底层还是可以更改的。

    (base) root@ubuntu:/opt/overlayfs# cat lower/xxo 
    xxo in lower
    (base) root@ubuntu:/opt/overlayfs# echo "xxo in lower2" > lower/xxo 
    (base) root@ubuntu:/opt/overlayfs# cat lower/xxo 
    xxo in lower2
    (base) root@ubuntu:/opt/overlayfs# cat merged/xxo 
    xxo in lower2
    

    修改 merged/xxo 文件,但是查看的时候发现 merged/xxo 改变了!

    xxo 文件时来自 lower 层的,而 lower 层是只读的,那么为什么可以从上层去更改文件,难道结论错误的?

    (base) root@ubuntu:/opt/overlayfs# echo "xxo in merge" > merged/xxo 
    (base) root@ubuntu:/opt/overlayfs# cat merged/xxo 
    xxo in merge
    

    查看 lower 目录下的 xxo 文件,发现依然没有改变,是上一步骤中的值啊,但是 merged 中有不同的 xxo,他不会凭空出现的,所以真相应该就在 upper 目录中。

    (base) root@ubuntu:/opt/overlayfs# cat lower/xxo 
    xxo in lower2
    

    我门会发现 upper 目录下出现一个 xxo 文件,文件内容和 merged 目录中的文件相同。所以推理一下可得,当修改合并到 merged 层的 lower 层文件时,由于 lower 是无法执行写操作的,所以 overlayfs 会在 upper 层创建一个同名的文件去实现文件的修改。而 upper 层是会覆盖掉 lower 层相同名字的文件的,所以现在 merged 现在的 xxo 其实就是挂载 upper 目录下的 xxo 文件的。

    (base) root@ubuntu:/opt/overlayfs# ll upper/
    total 20
    drwxr-xr-x 2 root root 4096 Aug  9 08:10 ./
    drwxr-xr-x 6 root root 4096 Aug  9 08:08 ../
    -rw-r--r-- 1 root root   16 Aug  9 08:44 common
    -rw-r--r-- 1 root root   13 Aug  9 08:10 foo
    -rw-r--r-- 1 root root   13 Aug  9 09:12 xxo
    (base) root@ubuntu:/opt/overlayfs# cat upper/xxo 
    xxo in merge
    

    image

    2.3 删除文件
    // 先在lower层加入,merged层出现文件lol
    (base) root@ubuntu:/opt/overlayfs# echo "lol in lower" > lower/lol
    (base) root@ubuntu:/opt/overlayfs# tree
    .
    ├── lower
       ├── common
       ├── lol
       └── xxo
    ├── merged
       ├── common
       ├── foo
       ├── lol
       └── xxo
    ├── upper
       ├── common
       ├── foo
       └── xxo
    └── work
        └── work
    
    // 删除merged/lol文件
    (base) root@ubuntu:/opt/overlayfs# rm merged/lol 
    
    // 发现lower目录下面lol文件并没有删除,merged目录下的lol文件已经删除,upper目录下多了一个lol的文件
    (base) root@ubuntu:/opt/overlayfs# tree 
    .
    ├── lower
       ├── common
       ├── lol
       └── xxo
    ├── merged
       ├── common
       ├── foo
       └── xxo
    ├── upper
       ├── common
       ├── foo
       ├── lol
       └── xxo
    └── work
        └── work
    
    5 directories, 10 files
    
    // 可以看到lol是一个c开头的文件。
    (base) root@ubuntu:/opt/overlayfs# ll upper/
    total 20
    drwxr-xr-x 2 root root 4096 Aug  9 09:53 ./
    drwxr-xr-x 6 root root 4096 Aug  9 08:08 ../
    -rw-r--r-- 1 root root   16 Aug  9 08:44 common
    -rw-r--r-- 1 root root   13 Aug  9 08:10 foo
    c--------- 1 root root 0, 0 Aug  9 09:53 lol
    -rw-r--r-- 1 root root   13 Aug  9 09:12 xxo
    
    // 删除upper/lol发现merged目录下的lol又出现了。
    (base) root@ubuntu:/opt/overlayfs# rm -rf upper/lol
    

    通过上面推理,在 merged 层删除 lower 层的文件时,进行的是软删除,会在 upper 层创建一个同名的 c 类型文件,告诉 merged 层该文件已经删除了。

    image

    2.4 添加文件

    没有什么特点,在 merge 层添加就是添加到 upper 层。

    3. docker 中如何使用 overlayfs

  • 待分类

    用户发帖时如果不填标签,则默认加上“待分类”。这样做是为了减少用户发帖的负担,同时也减少运营维护的工作量。具有帖子更新权限的用户可以帮助社区进行帖子整理,让大家可以更方便地找到所需内容。这里是关于这样设计的一些思考,欢迎讨论。

    3 引用 • -274 回帖 • 4 关注

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...