本文开始于 2023年10月1日

学习环节

(一)Docker相关介绍

简介

Docker 个开源工具,它可以将你的应用打包成 个标准格式的镜像,并且以容器的方式运行。其保证了容器内应用程序运行环境的稳定性,不会被容器外的系统环境所影响

当然,由于每个容器共享一个kernel,因此同一个kernel可以运行各种发行版的linux

特点

  • 轻量级:在同一台上的容器共享系统 Kernel, 这使得它们可以迅速启动而且占用 内存极少,镜像是以分层文件系统构造的,这可以让它们共享相同的文件,使得磁盘使用率和镜像下载速度得到提高
  • 开放: docker 容器基于开放标准,这使得 Docker 容器可以运行在主流 Linux 发行版和 ndows 操作系统上
  • 安全:容器将各个应用程序隔离开来,这给所有的应用程序提供了层额外的安全防护

与虚拟机比较

容器更加便携和高效,而虚拟机包括了用户程序,操作系统和必要的函数库,通常会占用几个G的空间,消耗大

当然,容器之间是共享Kernel的,且容器在宿主机上相互隔离并在用户态运行

优势

  • 加速开发
  • 减少部署环境/工具冲突
  • 可以通过DockerHub等平台共享镜像,实现合作
  • 可以动态改变应用程序,做到秒级启动/停止,快速扩容

(二)Docker相关技术介绍

1. Namespace

这里的Namespace有别于C++的Namespace,他的作用是隔离系统资源,如Pid,UserId和NetWork等并且,其与chroot相似:Namespace也可以在比如进程树,网络接口或者挂载点等资源上将进程隔离

Namespace和chroot的区别
  • chroot相对简单操作,并且其常用于进行轻量级的应用必须依赖于一台现成的unix系统,且chroot仅仅在系统目录上进行了隔离,没有对进程和网络等层面进行隔离,并且它仍然依赖于操作系统的安全机制和权限管理,容易存在一些安全漏洞和逃逸的风险

  • 而Namespace虽然相对复杂,但是他能提供细粒度的隔离,并且运用更加灵活

Namespace用途
  • 通过UID级别的隔离实现用户资源分配,从而限制不同用户之间资源访问的权限
  • 虚拟化PID创建子进程,不同Namespace之间各属于不同的父进程,不同的Namespace内的进程相互隔离独立,如A和B空间能有PID相同的进程
    • Namespace有自己的init进程,并且子命名空间进程映射到父命名空间的进程上,也就是父命名空间可以知道每个子namespace的运行状态
    • 父进程在子namespace重会被默认为是PID为1的初始化进程,如图实例:image-20231001035017008

当前Linux实现了6种不同的NameSpaceimage-20231001035124870

Namespace类型介绍:
  1. UTS Namespace:

    用于隔离 nodename 和 domainname两个系统标识,也就是在该Namespace中,每个Namespace可以有自己的hostname

  2. IPC Namespace:

    其用于隔离 System V IPC 和POSIX message queques,其对应的每个Namespace都有自己的System V IPC 和POSIX message queques

    IPC 是 Inter-Process Communication(进程间通信)的缩写,它是指在操作系统中不同进程之间进行数据交换、共享资源和通信的机制和技术。

    当多个进程在同一系统中运行时,它们可能需要相互交换数据、进行协调和同步操作,以实现任务的分工和协作。这就需要使用 IPC 机制来实现进程间的通信。

    常见:

    1. 共享内存(Shared Memory):多个进程可以访问同一块共享内存区域,从而实现高效的数据共享和通信。
    2. 管道(Pipe):一种单向的、基于文件描述符的通信机制,用于在具有父子关系的进程之间进行通信。
    3. 消息队列(Message Queue):进程可以通过发送和接收消息来进行通信,消息队列提供了一种异步通信机制。
    4. 信号量(Semaphore):用于进程之间的同步和互斥,通过对共享资源的访问进行控制。
    5. 套接字(Socket):一种网络编程中常用的 IPC 机制,用于不同计算机之间的进程通信
  3. PID Namespace:

    用于隔离进程ID,也就是同一个进程在不同Namespace里拥有不同的PID,也就是你在Docker容器上的那个前台PID为1的进程和在宿主机上的PID不同

  4. Mount Namespace:

    其用于隔离各个进程看到的挂载点视图,也就是不同的进程中的文件系统层次不同,你在该namespace中调用mount()和umount()只会影响该namespace的文件系统

    其功能与chroot相似但却更安全

    Docker volume也是基于了这个Namespace的特性实现

  5. User Namespace:

    其主要用于隔离用户的用户组ID,也就是说可以保证UserId/GroupId在Namespace内外是可以不同的,也就是实现了权限隔离,宿主机内的非root用户也可以以root权限在Namespace中操作

  6. Network Namespace:

    其用于隔离网络设备和IP地址端口等网络的Namespace,其可以使容器内拥有独立的虚拟网络设备,并且可以绑定容器自己的端口,且各个容器可以通过网桥来通信

NameSpace调用:

Namespace的API通过以下三个调用:

  • clone() :创建新进程,并根据系统调参来判断哪些Namespace被创建,并且他们的子进程也被包含进这些Namespace中
  • unshare():将进程移出 Namespace
  • setns() :将进程加入Namespace
调用Namespace接口展示隔离:

这里对一些没怎么用到过的包简单说明(注:运行syscall一般需要你有root权限):

  • os/exec:

    用于执行外部命令和进程的管理

    作用:

    1. 启动外部命令,通过Command函数创建一个Cmd对象来执行命令
    2. 进程管理,可通过Cmd兑现的方法来等待命令完成或者发送信号给命令进程等操作

    当然基于上边的说明,os/exec包还有许多用法,通过 os/exec 包,你可以在 Go 程序中方便地执行外部命令、捕获命令输出、与命令进行交互,并管理命令的执行过程。这使得你可以方便地与外部程序进行集成和交互,实现更多复杂的功能和任务

  • syscall:

    用于调用底层系统调用接口的包,其允许你直接调用操作系统提供的系统调用并且进行交互

    作用:

    1. 系统调用接口,可以直接以更低级别的方式来和操作系统交互
    2. 跨平台兼容:其封装了不同系统的系统调用并提供相同的接口,使我们不用关心操作系统的差异
    3. 访问底层系统资源:其可以允许你访问,管理和操作诸如文件,进程,网络等底层系统资源
    4. syscall包允许你控制系统调用的行为和参数。你可以设置系统调用的输入参数、获取系统调用的返回值,并进行错误处理
  1. UTS Namespace

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main

    import (
    "os/exec"
    "syscall"
    "os"
    "log"
    )

    func main(){
    cmd := exec.Command("sh") // 执行一个新的shell进程
    cmd.SysProcAttr = &syscall.SysProcAttr{
    Cloneflags: syscall.CLONE_NEWUTS,
    }//设置参数,使用CLONE_NEWUTS这个标识符创建一个UTS,其封装了对clone()接口的调用
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err:=cmd.Run();err!=nil{
    log.Fatal(err)
    }
    }

    此时创建成功,我们通过pstree -pl来获取进程关系,如图image-20231002024124759

    然后我们用 readlink /proc/59468/ns/uts readlink /proc/59473/ns/uts查看二者的UTS是否相同

    我们在UTS命名空间中将hostname修改为test,同时我们开启一个新的进程,查看主机的hostname 是否被改变image-20231002024751399 说明二者的Hostname已经实现隔离独立

    readlink 命令的主要作用是用于查看符号链接(symbolic link)的目标或目标路径。符号链接是一种特殊类型的文件,它包含对另一个文件或目录的引用,而不是实际的文件内容

    使用 readlink 命令查看 UTS 命名空间符号链接的目的通常是为了验证两个进程是否在相同的 UTS 命名空间中,或者查看特定进程的 UTS 命名空间标识符

    如果这两个命令返回的路径相同,那么这两个进程在同一个 UTS 命名空间中,它们共享相同的主机名和域名。如果返回的路径不同,那么它们在不同的 UTS 命名空间中,拥有不同的主机名和域名

  2. IPC Namespace

    在1中代码的基础上修改一行:

    1
    2
    3
    cmd.SysProcAttr = &syscall.SysProcAttr{
    Cloneflags: syscall.CLONE_NEWUTS|syscall.CLONE_NEWIPC ,
    }

    image-20231002223007441我们先在Namespace上创建一个messagequeque,并确认,然后再在宿主机内查看image-20231002223143963宿主机内创建的message queque,说明IPC被隔离

  3. PID Namespace

    同2:

    1
    2
    3
    cmd.SysProcAttr = &syscall.SysProcAttr{
    Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
    }

    运行后,我们在宿主机执行pstree -pl查看Namespace所属进程的PID,可以发现shell所处进程为5337

    image-20231003013647054

    之后我们在Namespace内查看自己的PIDimage-20231003013725802

    可以发现二者并不相同

    echo $$ 是一个在 Bash Shell 或类似的命令行环境中使用的特殊命令。它的作用是显示当前正在执行的 Shell 进程的进程 ID(PID)

  4. Mount Namspace

同:

1
2
3
4
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS,
}

首先,我们查看/proc的文件内容image-20231003015125840

呈现类似的信息,由于这里的/proc属于宿主机,所以较乱。随后我们将/proc mount到自己的namespace中

输入mount -t proc proc /proc后再查看

image-20231003015302576

此时文件已经减少,我们这时候使用ps -ef查看系统的进程

image-20231003015351084

可以发现,ps -ef只读取到sh进程为1的进程,也就是mount和外部空间隔离,使/proc为Namespace内部的

/proc 是一个特殊的文件系统,它在许多类 UNIX 和 Linux 系统中存在。它提供了一种访问正在运行的进程和内核信息的接口,以文件和目录的形式呈现。

/proc 文件系统中,每个运行的进程都有一个对应的进程目录,以其进程 ID(PID)为名称。例如,进程 ID 123 的进程将在 /proc/123/ 目录下表示,在进程目录中,你可以访问有关该进程的各种信息。这些信息以文件和子目录的形式存在,可以用于查询和监视进程的状态、资源使用情况以及其他与进程相关的信息。

ps -ef本质上就是通过读取 /proc的消息来获取进程的信息

mount -t proc proc /proc 命令的作用是将Namespace的 proc 文件系统挂载到 /proc 目录,以便访问和查询正在运行的进程和系统信息

  1. User Namespace

    相较于前面的,我们配置了syscall.CLONE_NEWUSER作为标志:

    image-20231003022721822

    我们可以发现两者UID显然不同,说明做了隔离

  2. Network Namespace

    同上,添加标志syscall.CLONE_NEWNET

    查看宿主机内的网络配置信息:

    image-20231003023122782

​ 再查看一下Namespace内的:

image-20231003023159557

​ 发现此时Namespace内并没有配置任何网络设备,可以断定为隔离状态

2. Cgroups

介绍

1中我们介绍了Namespace的技术,其用于帮助进程隔离出单独的空间吗,但如何限制每个空间的大小保证每个进程之间不会相互争抢,这就要用到Cgroups

Cgroups提供了一组进程来对子进程进行资源限制,控制和统计,包括CPU,内春,存储,网络这一类的资源

组件

Cgroups主要由三个组件构成:

  1. cgroup用于对进程分组进行管理,一个cgroup包含一组进程,并且可以在Cgroup中增加subsystem各种参数配置,将该进程与下文的subsystem关联

  2. subsystem是一组资源控制模块,一般包含如下几项:

    • blkio对块设备输入输出的访问控制
    • cpu设置进程的CPU被调度的策略
    • cpuacct统计cgroup中的CPU占用
    • cpuset在多核机器中,可以设置cgroup可以使用的cpu和内存
    • devices控制进程对设备的访问
    • freezer用于挂起和恢复cgroup中的进程
    • memeory用于控制cgroup的内存占用
    • net_cls将进程产生的网络包分类,便于tc(traffic controller)可以区分出某个cgroup的包来限流/监控
    • net_prio指定进程产生网络流量的优先级
    • ns 可以使cgroup的进程在fork新进程时的创建出一个新的cgroup,并且该cgroup包含新的Namespace的进程
    • pref_event 对性能事件的监控和统计,用于性能分析和调试
    • hugetlb 用于管理和分配大页面的内存资源
    • pids 限制cgroup中进程数量的上限
    • rdma用于管理和远控直接内存访问资源(RDMA)和设备

    每个subsystem会关联到对应限制的cgroup上并对cgroup作出相应的限制和控制。这些subsystem可以通过安装cgroup的命令行工具cgrouop-bin后通过lssubsys -a可以看到当前kernel版本支持的subsystem

  3. hierarchy:其可以将一组cgroup整理为树状的结构,通过这样的树状结构,cgroups可以实现继承,比如某一cgroup1限制了cpu的使用率,现要求某一进程还要限制磁盘IO,为避免cgroup1中的其他受影响,可以创建cgroup2,其继承了cgroup1,在cpu使用率被限制的前提下可以限制磁盘IO

  4. cgroups通过三个组件相互协作实现的,其之间存在一定关系:

    • 系统创建了新的hierarchy后,所有的进程都会加入hierarchy的cgroup的根节点,该节点默认创建
    • 一个subsystem只能附加到一个hierarchy上
    • 一个hierarchy可以附加多个subsystem
    • 一个进程可以作为多个cgroup的成员,但是这些cgroup必须确保不在一个hierarchy
    • 当一个进程fork出子进程时,子和父是默认在一个cgroup中的,当让可以根据需要将他移到其他cgroup中
kernel接口

在前文我们知道,hierarchy是一种树状的组织结构,为了配置更加直观,kernel通过一个虚拟的树状文件系统来配置cgroups,通过层级目录模拟出cgroup树

通过sudo mount -t cgroup -o none ,name=cgroup-test cgroup-test .将hierarchy挂载到当前目录

查看image-20231004014337628

我们会发现此时添加了一些文件

挨个解读:

  • cgroup.clone_children cpuset的subsystem会读取这个配置文件,如果是子cgroup才会继承父cgroup的cpuset的配置
  • cgroup.procs是当前节点cgroup中的进程组ID,现在的位置是根节点,该文件有现在系统中所有进程组的ID
  • notify_on_releaserelease_agent一起使用,notify_on_release标识这个进程退出时是否执行了release_agentrelease_agent则时在进程退出后清理不再使用的cgroup
  • task 标识cgroup下面的进程ID,如果进程ID写道tasks中,其会将相应的进程添加到cgroup中

我们在挂载的目录下创建文件夹时,其会被标记为该cgroup的子group,其将继承父cgroup的属性,如创建cgroup-1和cgroup-2目录后,目录内自动生成对应文件:

image-20231004014608436

当然,由前文我们可以得知,一个进程在同一个hierarchy中只能在一个节点上存在,而进程默认在根节点上,如果要将进程移动,我们可以将进程ID写入cgroup节点中的task文件即可

例如,我们将当前进程移到cgroup1中

image-20231004015309600

可以发现cgroup已经被移入cgroup-1中

当然,我们也可以通过subsystem限制进程资源:

其实,系统默认为每个subsystem创建了默认的hierarhcy,比如memory:image-20231004015609655

可知该文件就是挂在了hierarchy上,那我们便可以做响应限制,如下为限制某一进程占用内存限制的参考:

image-20231004015735461

我们利用cgroup,将stree进程最大内存占用限制为100MB

Docker怎么使用Cgroups的
  1. docker 通过run的某个参数 -m来限制docker内存

  2. docker会为每个容器的hierarchy来创建cgrou

  3. docker利用cgroup进行资源的限制和监控

Go语言实现

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
)
// 挂在了memory subsystem 的hierarchy的根目录位置
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"

func main() {

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`)//在容器内创建一个stress进程来测试
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {//启动stress进程,并直接等待stress完成
fmt.Println(err)
os.Exit(1)
}
}
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 {

fmt.Println(cmd.Process.Pid)

os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes"), []byte("100m"), 0644)
}
cmd.Process.Wait()
}

查看对应:

image-20231005020816525

简单计算大致在100M左右,说明成功

3.Union File System

介绍

Union File System是一种将其他文件系统联合到一个挂载点的文件系统服务,他利用branch把不同文件系统的

文件和目录透明地覆盖形成了一个单一一致的文件系统。这些branch是read-only/read-write的。当我们对这个虚拟的联合文件系统进行写操作时,系统真正写到了一个新的文件中,且其没有改变原来的文件。其原因是UnifonFS用了一个名为写时复制的资源管理技术

写时复制(Copy-On-Write,COW):也被称为隐式共享,简单来说就是:当一个资源是重复的且没有任何修改,那么我们不需要创建一个新资源,而可以把这个资源作为新旧实例共享,新资源创建发生在对资源修改/写操作的时候,可以显著减少修改资源带来的消耗

当对联合文件系统进行读操作时,如果某个文件在文件系统A和B中都存在,那么只有一个文件会被返回,通常是按照优先级选择。这种方式实现了文件的透明覆盖,用户可以像访问单一文件系统一样访问这些文件。

而当对联合文件系统进行写操作时,写时复制技术发挥作用。当需要修改一个文件时,Union File System并不直接在原始文件所在的文件系统上进行修改,而是创建一个该文件的副本,并将修改操作应用于副本。这样,原始文件保持不变,只有副本被修改。

AUFS/Overlay2

AUFS是一种Union File System,其主要目的是为了可靠性和性能,并且引入了一些新的功能,比如可写分支的负载均衡。AUFS在使用上全兼容UnionFS,而且比之前的UnionFS在稳定性和性能上都要好很多,后来的UnionFS 2.x开始抄AUFS中的功能。

而Overlay与AUFS相像,但是他相较于AUFS设计更简单且处理效率更高,而且Overlay2已经被融入当然,现在主要的是OVerlay2了,且目前版本的Docker默认驱动都是Overlay2

从kernel 3.18进入主流Linux内核。设计简单,速度快,比AUFS和Device mapper速度快。在某些情况下,也比Btrfs速度快。是Docker存储方式选择的未来。因为OverlayFS只有两层,不是多层,所以OverlayFS “copy-up”操作快于AUFS。以此可以减少操作延时。

性能比较:
  •   Page Caching,页缓存。OverlayFS支持页缓存共享,也就是说如果多个容器访问同一个文件,可以共享一个或多个页缓存选项。这使得overlay/overlay2驱动高效地利用了内存,是PaaS平台或者其他高密度场景的一个很好地选项。
  •   copy_up。和AuFS一样,在容器第一次修改文件时,OverlayFS都需要执行copy-up操作,这会给写操作带来一些延迟——尤其这个要拷贝的文件很大时。不过,一旦文件已经执行了这个向上拷贝的操作后,所有后续对这个文件的操作都只针对这份容器层的新拷贝而已。 OverlayFS的copy_up操作比AuFS的copy_up操作要快。因为AUFS支持比OverlayFS支持更多的层数,如果需要在多层查找文件时,就可能导致比较大的延迟。
  •   Inode limits。使用overlay存储驱动可能导致过多的inode消耗,尤其是Docker host上镜像和容器的数目增长时。大量的镜像,或者很多容器启停,,会迅速消耗掉该Docker host上的inode。overlay2存储驱动不存在这个问题。

 

Overlay2在Docker的应用

OverlayFS使用两个目录,把一个目录置放于另一个之上,并且对外提供单个统一的视角。这两个目录通常被称作层,这个分层的技术被称作union mount。术语上,下层的目录叫做lowerdir,上层的叫做upperdir。对外展示的统一视图(也就是这两层联合挂载的结果)称作MergedDir。

参考:  

image-20231005201008443

img
  注意镜像层和容器层是如何处理相同的文件的:容器层(upperdir)的文件是显性的,会隐藏镜像层(lowerdir)相同文件的存在。容器映射(merged)显示出统一的视图。

上面这个图展示了Docker结构和OverlayFS结构的映射关系,镜像层对应着lowerdir,容器层对应着upperdir。我们的容器可写层、镜像层都一起被挂载到merged目录下。

overlay驱动只能工作在两层之上。也就是说多层镜像不能用多层OverlayFS实现。替代的,每个镜像层在/var/lib/docker/overlay2中用自己的目录来实现,使用硬链接这种有效利用空间的方法,来引用底层分享的数据。注意:Docker1.10之后,镜像层ID和/var/lib/docker中的目录名不再一一对应。
  创建一个容器,overlay驱动联合镜像层和一个新目录给容器。镜像顶层是overlay中的只读lowerdir,容器的新目录是可写的upperdir。

overlay读写:

有三种场景,容器会通过overlay只读访问文件。

  1. 容器层不存在的文件。如果容器只读打开一个文件,但该容器不在容器层(upperdir),就要从镜像层(lowerdir)中读取。这会引起很小的性能损耗。
  2. 只存在于容器层的文件。如果容器只读权限打开一个文件,并且容器只存在于容器层(upperdir)而不是镜像层(lowerdir),那么直接从镜像层读取文件,无额外性能损耗。
  3. 文件同时存在于容器层和镜像层。那么会读取容器层的文件,因为容器层(upperdir)隐藏了镜像层(lowerdir)的同名文件。因此,也没有额外的性能损耗。

 

有以下场景容器修改文件。

  1. 第一次写一个文件。容器第一次写一个已经存在的文件,容器层不存在这个文件。overlay/overlay2驱动执行copy-up操作,将文件从镜像层拷贝到容器层。然后容器修改容器层新拷贝的文件。    

    然而,OverlayFS工作在文件级别而不是块级别。也就是说所有的OverlayFS的copy-up操作都会拷贝整个文件,即使文件非常大但却只修改了一小部分,这在容器写性能上有着显著的影响。不过,有两个方面值得注意:     

    ▷ copy-up操作只发生在第一次写文件时。后续的对同一个文件的写操作都是直接针对拷贝到容器层的那个新文件。     

    ▷ OverlayFS只工作在两层中。这比AUFS要在多层镜像中查找时性能要好。

  2. 删除文件和目录。删除文件时,容器会在镜像层创建一个whiteout文件,而镜像层的文件并没有删除。但是,whiteout文件会隐藏它。    

    容器中删除一个目录,容器层会创建一个不透明目录。这和whiteout文件隐藏镜像层的文件类似。

  3. 重命名目录。只有在源路径和目的路径都在顶层容器层时,才允许执行rename操作。否则,会返回EXDEV。    因此,你的应用需要能够处理EXDEV,并且回滚操作,执行替代的“拷贝和删除”策略。

Docker测试
  1. 镜像存储

    以Nginx镜像为例:image-20231005204144214这里显示了镜像的存储路径:其中 MergedDir 代表当前镜像层在 overlay2 存储下的目录,LowerDir 代表当前镜像的父层关系,使用冒号分隔,冒号最后代表该镜像的最底层

    之后我们进入/var/lib/docker/overlay2中发现有数个目录,说明镜像被分成了数层

    随机进一个查看,可以发现该目录下有数个文件和子目录image-20231005204536560这里,镜像层的link文件内容为该镜像层的短 IDdiff文件夹为该镜像层的改动内容,lower文件内容为该层的所有父层镜像的短 ID,work层则为该镜像的工作目录

    我们也发现在镜像层有一个l目录,它的作用就是存放了软连接,根据这个短ID可以软连接到对应镜像层的diff文件夹下

    软链接和硬链接:

    在 Linux 文件系统中,文件通过 inode(节点索引编号)唯一区分。文件分为元数据(metadata)和数据域(data block),而 inode 唯一指向数据域。

    硬链接(Hard link)就是在同一个文件系统中,文件名不同,但 inode 一样的文件副本;软链接(Soft link, or symbolic link)则是另一个文件,inode 不同,但 inode 指向的数据域中存放的是所链接文件的路径。

    硬链接可以防止文件误删除,因为在 Linux 中一个文件的数据域被删除,当且仅当其 inode 的引用为 0,也就是必须删除该文件的所有硬链接,该文件才会被最终删除。

    软链接则不同,当所链接文件被删除时,链接也会失效。

​ 当我们查看父层的最底层,也就是LowerDir最后的镜像层,我们发现他没有lower文件,说明他是根镜像

​ 并且该层下diff文件夹的文件与Linux文件目录结构相同image-20231005214937805

​ 说明该镜像应该是基于一个Linux操作系统的镜像,然后在该镜像上安装nginx,DOkcerfile的每一个命令都 记录了一层diff文件

  1. 容器存储

    启动Nginx容器:image-20231006202830046 这里的LowerDir就是容器依赖的镜像层目录

    当我们查看overlay2目录时,发现新增了两个目录:image-20231006203145817

    这个带有init后缀的层是容器启动的一个临时层,从字面意思看它大概就是容器的初始化层。docker

    ``commit提交为镜像时是不会提交init`层的

    init层的作用:

    1. 容器在启动以后, 默认情况下lower层是不能够修改内容的, 但是用户有需求需要修改主 机名与域名地址, 那么就需要添加init层中的文件(hostname, resolv.conf), 用于解决此类问 题.
    2. 修改的内容只对当前的容器生效, 而在docker commit提交为镜像时候,并不会将init层提 交。

    进入容器层,其结构与镜像层大体相似,但是多出一个mergerd层:

    linklower文件与镜像层的功能一致,link文件内容为该容器层的短 IDlower 文件为该层的所有父层镜像的短 IDdiff目录为容器的读写层,容器内修改的文件都会在 diff中出现,merged 目录为分层文件联合挂载后的结果,也是容器内的工作目录。

    而当我们创建文件时,可以发现diff的目录下也有这些文件,而merged将lowerdirupperdir的文件合 并在了一起(也就是我们说的merged层)

    总体来说,overlay2 是这样储存文件的:overlay2将镜像层和容器层都放在单独的目录,并且有唯一ID,每一层仅存储发生变化的文件,最终使用联合挂载技术将容器层和镜像层的所有文件统一挂载到容器中,使得容器中看到完整的系统文件。

  2. COW的应用

    当容器启动时,一个新的可写层被加载到镜像的顶部。这一层被称之为“容器层”,容器层之下的都叫做“镜像层”。

    所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。只有容器层是可以写的,容器层下面的所有镜像层都是只读的。

    我们在容器中进行操作时:

    • 添加文件。在容器中创建文件时,新文件被添加到容器层中。
    • 读取文件。在容器中读取某个文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
    • 修改文件。在容器中修改已存在的文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改。
    • 删除文件。在容器中删除文件时,Docker也是从上往下依次在各镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。

    只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write。可见,容器层保存的镜像变化的部分,不会对镜像本身进行任何修改。

    写时复制不仅节省空间,而且还减少了容器启动时间。当你创建一个容器(或者来自同一个镜像的多个容器)时,Docker 只需要创建可写容器层即可。

    总结:当容器需要修改文件时,他会采用写时复制技术,将镜像的文件复制到自己容器的上层路径下,然后修改。如果下次要用这个文件,如果在容器层有就用容器层的,如果没有就看底层目录镜像层的。

实操overlay2

依据:https://blog.csdn.net/qq_45858169/article/details/115918469

先创建如下结构

image-20231007131308898

随后运行: sudo mount -t overlay overlay2 -olowerdir=lower1:lower2,upperdir=upper,workdir=work merged/

再查看:

image-20231007131636162

说明当前已经实现了UFS的功能

现在来尝试

当我们修改merged中的file 1.txt的文件时,lower1的文件并没有被相应修改,而upper中则出现了一个新的file1.txt文件

这表明

merged中的file1.txt确实被我们修改了,但lowerdir中的内容仍然不变,而是在upperdir中生成了一个file1.txt,这就是copy-on-write。这也验证了lowerdir是只读层这一点

而当我们试图删掉merged的file2.txt文件时,我们可以发现merged中的文件确实被删除了,但是我们在work中可以看到一个临时文件:

image-20231007135055575

意义暂时不明

且在upperdir内生成了一个file2.txt的特殊的字符设备文件:

image-20231007135124015

overlay2在联合挂载时,看到这个特殊的临时字符设备文件,会选择性的忽略lowerdir中对应的内容

也就是一种名叫做whitout文件

whiteout 概念存在于联合文件系统(UnionFS)中,代表某一类占位符形态的特殊文件,当用户文件夹与系统文件夹的共通部分联合到一个目录时(例如 bin 目录),用户可删除归属于自己的某些系统文件副本,但归属于系统级的原件仍存留于同一个联合目录中,此时系统将产生一份 whiteout 文件,表示该文件在当前用户目录中已删除,但系统目录中仍然保留。

后记

学习相关的技术原理学够了,该上实操了😋😋😋