docker namespace

一文读懂容器三大核心技术——Namespace,Cgroup和UnionFS-CSDN博客

linux Namespace

linux Namepsace是Linux提供的一种资源隔离方案。Namespace之间资源相互独立。目前Linux中提供7种Namepsace

NamespaceFlag说明
CgroupCLONE_NEWCGROUP隔离cgroup
IPCCLONE_NEWIPC隔离进程间通信
NetworkCLONE_NEWN隔离网络资源
MountCLONE_NEWNS隔离挂载点
PIDCLONE_NEWPID隔离进程的ID
UserCLONE_NEWUSER隔离用户和用户组的ID
UTSCLONE_NEWUTS隔离主机名和域名信息

向clone系统调用传入上述表格中对应的Flag参数,可以为新创建的进程创建相应的Namespace。也可以使用setns系统调用将进程加入到一个已经存在的namespace中。通过namespace技术实现资源隔离。

实际上,linux内核实现namespace的主要目的,就是为了实现轻量级虚拟化技术服务。在同一个namespace下的进程合一感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,仿佛自己置身一个独立的系统环境中,以达到隔离的目的。

linux下通过shell创建一个容器

我们直接用一个示例来演示一下namespace隔离资源的效果。在命令行下,我们可以通过unshare命令来启动一个新进程,并为其新建相应的命名空间。在这个示例中,我们将通过unshare为我们的容器创建除cgroup和user之外的所有命名空间,这也是docker run something默认为容器创建的命名空间。本示例依赖docker环境来为我们提供一些配置上的便利。完整的示例script放在这里,方便大家scriptreplay回看过程。

git clone https://github.com/DrmagicE/build-container-in-shell
cd ./build-container-in-shell
scriptreplay build_container.time build_container.his

准备一个rootfs

首先,我们要为我们的容器准备自己的rootfs,用来为容器进程隔离后执行环境的文件系统。我们治理直接导出alpine镜像作为我们的rootfs,选择/root/container目录作为镜像rootfs:

[root@drmagic container]# pwd 
/root/container
[root@drmagic container]# # 修改mount类型为private,确保后续的mount/umount不会在namespace之间传播
[root@drmagic container]# mount --make-rprivate / 
[root@drmagic container]# CID=$(docker run -d alpine true)
[root@drmagic container]# docker export $CID | tar  -xf-
[root@drmagic container]# ls # rootfs建立好啦
bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

命名空间隔离

[root@drmagic container]# # 使用unshare为新的shell创建命名空间
[root@drmagic container]# unshare --mount --uts --ipc --net --pid --fork /bin/bash
[root@drmagic container]# echo $$ # 看看新进程的pid
1
[root@drmagic container]# hostname unshare-bash # 修改一下hostname
[root@drmagic container]# exec bash #替换bash,显现hostname修改后的效果
[root@unshare-bash container]# # hostname变化了

通过上面的过程,我们可以看到UTS和PID这俩个命名空间的隔离效果。

如果你在这一步使用ps来查看所有的进程,结果可能会令你失望——你仍然会看到系统中的所有进程,就像没有隔离成功一样。但这是正常的,因为ps读取/proc下的信息,此时的/proc还是host的/proc,所以ps还是能看到所有的进程。

隔离挂载信息

[root@unshare-bash container]# mount # 还是能看到host上的mount
/dev/vda2 on / type xfs (rw,relatime,attr2,inode64,noquota)
devtmpfs on /dev type devtmpfs (rw,nosuid,size=1929332k,nr_inodes=482333,mode=755)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
mqueue on /dev/mqueue type mqueue (rw,relatime)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime)
.....

我们发现mount依然能够获取全局挂载信息,难道是mount命名空间隔离没生效?非也,mount命名空间已经生效了。当新建一个mount命名空间时,他会拷贝父进程的挂载点,但对该命名空间挂载点的后续修改将不会影响到其他命名空间。

命名空间内挂载点的修改不影响其他命名空间有一个前提条件——mount的propagation type要设置为MS_PRIVATE,这也是为什么一开始我们要执行 mount --make-rprivate / 的原因

因此我们看到的mount信息是父进程的一份拷贝,我们重新mount一下/proc,好让ps能正常显示。

[root@unshare-bash ~]# # 重新mount一下/proc
[root@unshare-bash ~]# mount -t proc none /proc
[root@unshare-bash ~]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 21:29 pts/0    00:00:00 bash
root        77     1  0 21:47 pts/0    00:00:00 ps -ef
[root@unshare-bash ~]# # 啊哈,现在我们的ps正常了!

处理完了/proc的挂载,我们还需要清理旧的挂载点,将他们umount掉,这一步我们需要借助pivot_root(new_root,put_old)来完成。pivot_root将当前mount namespace下的所有进程(线程)的根目录挂载点切换至new_root,并将旧的根目录挂载点放到put_old目录下。使用pivot_root的主要目的是用来umount一些从父进程copy过来的挂载点。

为了满足pivot_root的一些参数要求,需要额外做一次bind mount:

[root@unshare-bash container]# mount --bind /root/container/ /root/container/
[root@unshare-bash container]# cd /root/container/
[root@unshare-bash container]# mkdir oldroot/
[root@unshare-bash container]# pivot_root . oldroot/ 
[root@unshare-bash container]# cd /
[root@unshare-bash /]# PATH=$PATH:/bin:/sbin 
[root@unshare-bash /]# mount -t proc none /proc
[root@unshare-bash /]# ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 bash
   70 root      0:00 ps -ef
[root@unshare-bash /]# mount # 依旧能看到host上的信息
rootfs on / type rootfs (rw)
/dev/vda2 on /oldroot type xfs (rw,relatime,attr2,inode64,noquota)
devtmpfs on /oldroot/dev type devtmpfs (rw,nosuid,size=1929332k,nr_inodes=482333,mode=755)
tmpfs on /oldroot/dev/shm type tmpfs (rw,nosuid,nodev)
....
[root@unshare-bash /]# umount -a # umount全部
umount: can't unmount /: Resource busy
umount: can't unmount /oldroot: Resource busy
umount: can't unmount /: Resource busy
[root@unshare-bash /]# mount -t proc none /proc # 重新mount /proc
[root@unshare-bash /]# mount
rootfs on / type rootfs (rw)
/dev/vda2 on /oldroot type xfs (rw,relatime,attr2,inode64,noquota)  <-- oldroot 还在
/dev/vda2 on / type xfs (rw,relatime,attr2,inode64,noquota)
none on /proc type proc (rw,relatime)

可以看到oldroot这个旧跟目录的挂载信息还在,我们把它unmount掉:

[root@unshare-bash /]# umount -l oldroot/ # lazy umount
[root@unshare-bash /]# mount
rootfs on / type rootfs (rw)
/dev/vda2 on / type xfs (rw,relatime,attr2,inode64,noquota)
none on /proc type proc (rw,relatime)

至此,容器只能看到自己的挂载信息了,挂载隔离完成.

step4:为我们的容器添加网络

接下来,我们初始化容器的网络。使用veth pair,借助docker提供的docker0网桥,打通容器与主机的网络。

[root@unshare-bash /]# ping 8.8.8.8 # 配置网络前,网络显然是不通的
PING 8.8.8.8 (8.8.8.8): 56 data bytes
ping: sendto: Network unreachable
[root@unshare-bash /]# ifconfig -a
lo        Link encap:Local Loopback
          LOOPBACK  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

回到host的shell

[root@drmagic ~]# pidof unshare # 容器的pid
11363
[root@drmagic ~]# CPID=11363
[root@drmagic ~]# # 添加veth pair
[root@drmagic ~]# ip link add name h$CPID type veth peer name c$CPID
[root@drmagic ~]# # 将veth一边塞到容器里
[root@drmagic ~]# ip link set c$CPID netns $CPID
[root@drmagic ~]# # 将veth另一边挂到docker0网桥上
[root@drmagic ~]# ip link set h$CPID master docker0 up

设置完veth pair,回到容器中:

[root@unshare-bash /]# ifconfig -a # 设置完之后回来看
c11363    Link encap:Ethernet  HWaddr 1A:47:BF:B8:FB:88
          BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
 
 
lo        Link encap:Local Loopback
          LOOPBACK  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
 
 
[root@unshare-bash /]# ip link set lo up
[root@unshare-bash /]# ip link set c11363 name eth0 up
[root@unshare-bash /]# # 为eth0设置一个随机的docker网段内的IP地址
[root@unshare-bash /]# ip addr add 172.17.42.3/16 dev eth0
[root@unshare-bash /]# # 配置默认路由走docker的默认网关
[root@unshare-bash /]# ip route add default via 172.17.0.1
[root@unshare-bash /]# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=43 time=17.220 ms
64 bytes from 8.8.8.8: seq=1 ttl=43 time=16.996 ms
64 bytes from 8.8.8.8: seq=2 ttl=43 time=17.099 ms
64 bytes from 8.8.8.8: seq=3 ttl=43 time=17.118 ms
^C
--- 8.8.8.8 ping statistics ---
5 packets transmitted, 4 packets received, 20% packet loss
round-trip min/avg/max = 16.996/17.108/17.220 ms

网络配置完成,现在整个容器的资源已经跟宿主机隔离起来了。docker其实也是通过类似的步骤来创建容器的。

Linux Cgroup

简介

功能

CGroups,是Linux内核提供的物力资源隔离机制,通过这种机制,可以实现对Linux进程或者进程组的资源限制、隔离和统计功能。

比如可以通过cgroup限制特定进程的资源使用,比如使用特定树木的cpu核数和特定大小的内存,如果内存超限的情况下,会被暂停或者杀掉。

Cgroup是由于2.6内核由Google公司主导引入的,它是L:inux内核实现虚拟化资源的技术基石,LXC(Linux Containers)和Docker容器所用到的资源隔离技术,正是Cgroup。

相关概念

  • 任务(task):在cgroup中,任务就是一个进程
  • 控制组(control group):cgroup的资源控制是以控制组的方式实现,控制组知名了资源的配额限制。进程可以加入到某个控制组,也可以迁移到另一个控制组。
  • 层级(hierarchy):控制组有层级关系,类似树的结构,子节点的控制组集成父控件组的属性(资源配额、限制等)。
  • 子系统(subsystem):一个子系统其实就是一种资源控制器,比如memory子系统可以控制进程内存的使用。子系统需要加入到某个层级,然后该层级的所有控制组,均受到这个子系统的控制。

概念之间的关系

  • 子系统可以依附多个层级,当且晋档这些层级没有其他的子系统,比如俩个层级同时有一个cpu子系统,是可以的。
  • 一个层级可以附加多个子系统
  • 一个任务可以是多个cgroup的成员,但这些cgroup必须位于不同的层级。
  • 子进程自动称为父进程cgroup的成员,可按需求将子进程移到不同的cgroup中。

途中俩个任务组成了一个Task Group,并使用了CPU和Memory俩个子系统的cgroup,用于控制CPU和MEM的资源隔离。

子系统

  • cpu: 限制进程的 cpu 使用率。
  • cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
  • cpuset: 为cgroups中的进程分配单独的cpu节点或者内存节点。
  • memory: 限制进程的memory使用量。
  • blkio: 限制进程的块设备io。
  • devices: 控制进程能够访问某些设备。
  • net_cls: 标记cgroups中进程的网络数据包,然后可以使用tc模块(traffic control)对数据包进行控制。
  • net_prio: 限制进程网络流量的优先级。
  • huge_tlb: 限制HugeTLB的使用。
  • freezer:挂起或者恢复cgroups中的进程。
  • ns: 控制cgroups中的进程使用不同的namespace。

cgroups文件系统

Linux通过文件的方式,将cgroups的功能和配置暴露给用户,这得益于Linux的虚拟文件系统(VFS)。VFS将具体文件系统的细节隐藏起来,给用户态提供一个统一的文件系统API接口,cgroups和VFS之间的链接部分,称之为cgroups文件系统。

比如挂在 cpu、cpuset、memory 三个子系统到 /cgroups/cpu_mem 目录下

mount -t cgroup -o cpu,cpuset,memory cpu_mem /cgroups/cpu_mem

关于虚拟文件系统机制,见浅谈Linux虚拟文件系统机制

gpt生成的:linux中 cgroup和namespace的关系

在 Linux 中,cgroup(Control Group)和 namespace(命名空间)是两个不同但相关的概念,它们都用于实现容器化和进程隔离。

cgroup 是 Linux 内核中的一种机制,用于限制和控制进程组的资源使用。通过 cgroup,可以将一组进程组织在一起,并对它们的资源使用进行限制,如 CPU、内存、磁盘、网络等。cgroup 允许对进程组进行资源配额分配和限制,以实现资源隔离和管理。

namespace 是 Linux 内核的另一种机制,用于隔离和封装进程的资源视图。通过使用不同的命名空间,可以为每个进程提供独立的资源环境,如进程树、网络、文件系统、用户 ID、主机名等。这样,不同的进程可以在各自的命名空间中运行,互相隔离,不会干扰或感知其他命名空间中的资源。

cgroup 和 namespace 在容器技术中通常一起使用,以实现完整的容器隔离和资源管理。cgroup 负责限制和控制进程组的资源使用,而 namespace 提供了进程隔离的环境。通过将进程组与相应的资源限制和命名空间结合起来,可以创建出容器,使其在隔离的环境中运行,并具有独立的资源视图和隔离的运行环境。

Docker、Kubernetes 和其他容器化平台都使用 cgroup 和 namespace 来实现容器隔离和资源管理,以提供轻量级、隔离性强的容器运行环境。

详解

cgroup的API以一个伪文件的方式实现,即用户可以通过文件操作实现cgroups的组织管理。在大部分系统中,cgroup 已经被自动挂载到/sys/fs/cgroup目录下

cgroups包含不同的子系统(subsystem),每一个子系统其实是一种资源的控制器。查看/sys/fs/cgroup目录:

$ ll /sys/fs/cgroup/
drwxr-xr-x 7 root root  0 11月 11 22:49 blkio
lrwxrwxrwx 1 root root 11 11月 11 22:49 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 11月 11 22:49 cpuacct -> cpu,cpuacct
drwxr-xr-x 6 root root  0 11月 11 22:49 cpu,cpuacct
drwxr-xr-x 4 root root  0 11月 11 22:49 cpuset
drwxr-xr-x 6 root root  0 11月 11 23:40 devices
drwxr-xr-x 4 root root  0 11月 11 22:49 freezer
drwxr-xr-x 4 root root  0 11月 11 22:49 hugetlb
drwxr-xr-x 6 root root  0 11月 11 22:49 memory
lrwxrwxrwx 1 root root 16 11月 11 22:49 net_cls -> net_cls,net_prio
drwxr-xr-x 4 root root  0 11月 11 22:49 net_cls,net_prio
lrwxrwxrwx 1 root root 16 11月 11 22:49 net_prio -> net_cls,net_prio
drwxr-xr-x 4 root root  0 11月 11 22:49 perf_event
drwxr-xr-x 6 root root  0 11月 11 22:49 pids
drwxr-xr-x 6 root root  0 11月 11 22:49 systemd

除了systemd以外, 上述目录中的每一个目录都代表着一个子系统,从上图中可以看出其包含有cpu相关(cpu,cpuacct,cpuset), 内存相关(memory),块设备I/O相关(blkio),网络相关(net_cls,net_prio)等子系统。

cgroups用树形的层级关系来管理各项子系统,每个子系统下都有它们自己的树形结构。树中的节点就是一组进程(或线程),不同子系统的层级关系是相互独立的。例如cpu子系统和memory子系统的层级结构可以是不一样的:

cpu/                                
├── batch
│   ├── bitcoins
│   │   └── 52   // <-- 进程ID
│   └── hadoop
│       ├── 109
│       └── 88
└── docker
    ├── container1
    │   ├── 1
    │   ├── 2
    │   └── 3
    └── container2
        └── 4
 
memory/
├── 109
├── 52
├── 88
└── docker
    ├── container1
    │   ├── 1
    │   ├── 2
    │   └── 3
    └── container2
        └── 4

将一个进程加入一个分组很简单,只需要往对应分组目录中的tasks文件写入Pid即可:echo “pid” > tasks。

如果你使用docker启动一个容器,那么docker会为该容器在每个子系统目录下创建docker/$container_id目录。这样cgroups就能对该容器的资源进行管理和限制了。

UnionFS

UnionFS是一种文件系统,它允许将多个目录组合成一个逻辑目录,将逻辑目录包含在这些目录中的所有内容,并对外提供一个统一的视图。

举个例子,假设我们需要更新一块CD-ROM中的内容,但是CD-ROW是不可写的,这个时候可以将CD-ROW于另一个科协目录挂载成UnionFS。当我们更新文件的时候,内容会被写入可写的目录,就好像CD-ROW中的内容被更新了一样。

容器镜像(image)提供了一个描述容器的静态视图,镜像中包含了容器运行所依赖的各种文件。我们可以再运行的容器中修改这些文件而不会影响到镜像本身。这是因为容器内目录于镜像联合成了一个UnionFS,从容器视角看,镜像就好比CD-ROW(不可写),容器对目录的修改仅会写入容器自身的目录,并不会影响到镜像中的内容。

镜像是由许多仅可读的层组成的,当你使用该镜像创建一个容器时,一个可写层会被加到镜像的可读层之上,容器内所有文件的变化都会保存在这个可写层。

Copy-On-write

容器的启动速度是很快的(即便在镜像很大的情况下),这得益于copy-on-write(COW,写时复制)技术的运用。当我们启动一个容器时候,并不需要将整个镜像中的文件copy一份,容器直接引用镜像中的文件,任何读操作都直接从镜像读即可,当写操作发生时,才需要将镜像中相应文件copy到容器的科协层,在科协层进行写入。

java中的CopyOnWriteArrayList 就是使用读读并发 读写冲突的思想实现的。

OS领域的copy-on-write核心思想则是lazy copy。我们知道应用程序通常是不会直接和物理内存打交道的,所谓的内存寻址只是针对虚拟内存空间而言,而从虚拟内存到物理内存的映射需要借助MMU(存储管理单元)实现。

以Linux为例,当通过系统调用(syscall)从一个已经存在的进程P1中fork出一个子进程P2,OS会为P2创建一套与P1包吃一直映射关系的虚拟内存空间,从而实现了P1和P2对物理空间的共享,这样做的目的是为了减少对物理内存的消耗,毕竟俩分完全一样的数据没必要额外占用多一倍物理内存空间。伺候,如果P1或P2需要修改某段内存,则须为其分配额外物理内存,将共享数据拷贝出来,供其修改,请注意,无论是父还是子进程,只要有修改,就会涉及到内存拷贝,这里的影响力度范围是内存页,linux内存页大小为4KB。

通过OS copy-on-write的过程我们可以总结出俩个重要的特性:

  1. 父子进程的内存共享数据仅仅是fork那一时间点的数据,fork后的数据不会有任何共享
  2. 所谓lazy copy,就是需要修改的时候拷贝一个副本出来,如果没有任何改动,则不会占用额外物理内存。

基于这两个特性我们可以知道,copy-on-write 的在 OS 领域的设计初衷可能并非为了解决并发读的效率问题,参考维基[1]对 copy-on-write 的定义:

很明显,如若我们利用 OS 这层优化策略,我们将大大减少了对物理内存的消耗,同时也提高了创建进程的效率,因为 OS 一开始并不需要给 fork 出来的新进程分配物理内存空间。因此 copy-on-write 非常适合内存快照的 dump,例如 redis 的 rdb dump。


docker文档中有对COW的详细介绍和示例

https://docs.docker.com/storage/storagedriver/#the-copy-on-write-cow-strategy

OverFS

UnionFS的实现有许多种,docker也可以配置多种类型的storage driver,其比较耳熟的有:overlay2,aufs,devicemapper。

参考:

https://docs.docker.com/storage/storagedriver/select-storage-driver/

随着OverlayFS被合入Linux kernel mainline,overlay2越来越常用,也成为了docker推荐使用的storage driver。本文就以OverlayFS和overlay2为例,说明容器是如何得益于UnionFS和copy-on-write的。

挂载OverlayFS:

$ mount -t overlay overlay -o lowerdir=lower1:lower2:lower3...,upperdir=upper,workdir=work  merged

参考:http://man7.org/linux/man-pages/man8/mount.8.html#FILESYSTEM-SPECIFIC_MOUNT_OPTIONS (搜overlay)

上述命令将merged目录挂载成OverlayFS,其中lowerdir是只读层(镜像层),允许有多层,upperdir则是可写层(容器层)。这意味着当我们向merged目录写入文件时,文件会被写入upperdir。从merged目录读文件时,如果文件在upperdir不存在,则向下一层层从lowerdir中找。

workdir是系统用于做挂载前的一些准备工作。需要一个空目录,且跟upperdir在同一文件系统下。

通过一个示例直观展示OverlayFS的读写行为:

$ mkdir lower upper work merged
$ echo "lowerdir" > lower/test
$ echo "upper" > upper/test # upper跟lower都有相同的文件test
$ echo "lowerdir" > lower/lower # lower才有的文件
$ mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work  merged
$ ls merged/ # mount后看到lower跟upper的统一视图
lower  test
$ cat merged/test
upper # upper, lower都有该文件,读upper的文件
$ cat merged/lower # upper没有该文件,读lower文件
lowerdir
$ echo "write something" >> merged/test
$ cat upper/test # 向merged的写入仅影响upper层
upper
write something
$ cat lower/test
lowerdir

使用docker run创建一个容器后,docker就会为容器mount一个OverlayFS

$ docker run -itd alpine /bin/sh
$ mount | grep overlay2
overlay on /var/lib/docker/overlay2/a2a37f61c515f641dbaee62cf948817696ae838834fd62cf9395483ef19f2f55/merged type overlay
(rw,relatime,
lowerdir=/var/lib/docker/overlay2/l/RALFTJC6S7NV4INMLE5G2DUYVM:
         /var/lib/docker/overlay2/l/WQJ3RXIAJMUHQWBH7DMCM56PNK,
upperdir=/var/lib/docker/overlay2/a2a37f61c515f641dbaee62cf948817696ae838834fd62cf9395483ef19f2f55/diff,
workdir=/var/lib/docker/overlay2/a2a37f61c515f641dbaee62cf948817696ae838834fd62cf9395483ef19f2f55/work)

docker将镜像中的每个layer按顺序添加到lowerdir中,将upperdir设置为容器的可写层。

当我们使用docker pull image的时候,docker就已经将镜像中各只读层的目录创建好了,执行docker run时,基本上只需创建容器的可写层,并将它们挂载成OverlayFS即可。所以就算镜像很大,容器的启动依旧是非常迅速。

当你使用docker pull拉镜像的时候,一定出现过Already exists的标识。

docker pull xxxx
...
68ced04f60ab: Already exists <---
e6edbc456071: Pull complete
...

docker pull时如果本地已经有该层的内容了,就不需要再拉了。不同的镜像会共享相同的层,在/var/lib/docker/overlay2下也只会保存一份与之对应的文件目录,减少了磁盘开销。

docker文档对overlay2工作过程的详细介绍:

https://docs.docker.com/storage/storagedriver/overlayfs-driver/#how-the-overlay2-driver-works

Last modification:January 8, 2024
如果觉得我的文章对你有用,请随意赞赏