Linux 内核从版本 2.4.19 开始陆续引入了 namespace 的概念。其目的是将某个特定的全局系统资源(global system resource)通过抽象方法使得namespace 中的进程看起来拥有它们自己的隔离的全局系统资源实例(The purpose of each namespace is to wrap a particular global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. )。Linux 内核中实现了六种 namespace,按照引入的先后顺序,列表如下:
namespace | 引入的相关内核版本 | 被隔离的全局系统资源 | 在容器语境下的隔离效果 |
---|---|---|---|
Mount namespaces | Linux 2.4.19 | 文件系统挂接点 | 每个容器能看到不同的文件系统层次结构 |
UTS namespaces | Linux 2.6.19 | nodename 和 domainname | 每个容器可以有自己的 hostname 和 domainame |
IPC namespaces | Linux 2.6.19 | 特定的进程间通信资源,包括System V IPC 和 POSIX message queues | 每个容器有其自己的 System V IPC 和 POSIX 消息队列文件系统,因此,只有在同一个 IPC namespace 的进程之间才能互相通信 |
PID namespaces | Linux 2.6.24 | 进程 ID 数字空间 (process ID number space) | 每个 PID namespace 中的进程可以有其独立的 PID; 每个容器可以有其 PID 为 1 的root 进程;也使得容器可以在不同的 host 之间迁移,因为 namespace 中的进程 ID 和 host 无关了。这也使得容器中的每个进程有两个PID:容器中的 PID 和 host 上的 PID。 |
Network namespaces | 始于Linux 2.6.24 完成于 Linux 2.6.29 | 网络相关的系统资源 | 每个容器用有其独立的网络设备,IP 地址,IP 路由表,/proc/net 目录,端口号等等。这也使得一个 host 上多个容器内的同一个应用都绑定到各自容器的 80 端口上。 |
User namespaces | 始于 Linux 2.6.23 完成于 Linux 3.8) | 用户和组 ID 空间 | 在 user namespace 中的进程的用户和组 ID 可以和在 host 上不同; 每个 container 可以有不同的 user 和 group id;一个 host 上的非特权用户可以成为 user namespace 中的特权用户; |
Linux namespace 的概念说简单也简单说复杂也复杂。简单来说,我们只要知道,处于某个 namespace 中的进程,能看到独立的它自己的隔离的某些特定系统资源;复杂来说,可以去看看 Linux 内核中实现 namespace 的原理,网络上也有大量的文档供参考,这里不再赘述。
当 Docker 创建一个容器时,它会创建新的以上六种 namespace 的实例,然后把容器中的所有进程放到这些 namespace 之中,使得Docker 容器中的进程只能看到隔离的系统资源。
我们能看到同一个进程,在容器内外的 PID 是不同的:
类似地,容器可以有自己的 hostname 和 domainname:
root@onfocus:/home/lee# hostname onfocus root@devstack:/home/lee# docker exec -it web hostname 8b7dd09fbcae
2.3.1 Linux 内核中的 user namespace
老版本中,Linux 内核里面只有一个数据结构负责处理用户和组。内核从3.8 版本开始实现了 user namespace。通过在 clone() 系统调用中使用 CLONE_NEWUSER 标志,一个单独的 user namespace 就会被创建出来。在新的 user namespace 中,有一个虚拟的用户和用户组的集合。这些用户和用户组,从 uid/gid 0 开始,被映射到该 namespace 之外的 非 root 用户。
在现在的linux内核中,管理员可以创建成千上万的用户。这些用户可以被映射到每个 user namespace 中。通过使用 user namespace 功能,不同的容器可以有完全不同的 uid 和 gid 数字。容器 A 中的 User 500 可能被映射到容器外的 User 1500,而容器 B 中的 user 500 可能被映射到容器外的用户 2500.
为什么需要这么做呢?因为在容器中,提供 root 访问权限有其特殊用途。想象一下,容器 A 中的 root 用户 (uid 0) 被映射到宿主机上的 uid 1000,容器B 中的 root 被映射到 uid 2000.类似网络端口映射,这允许管理员在容器中创建 root 用户,而不需要在宿主机上创建。
2.3.2 Docker 对 user namespace 的支持
在 Docker 1.10 版本之前,Docker 是不支持 user namespace。也就是说,默认地,容器内的进程的运行用户就是 host 上的 root 用户,这样的话,当 host 上的文件或者目录作为 volume 被映射到容器以后,容器内的进程其实是有 root 的几乎所有权限去修改这些 host 上的目录的,这会有很大的安全问题。
举例:
而 Docker 1.10 中引入的 user namespace 就可以让容器有一个 “假”的 root 用户,它在容器内是 root,它被映射到容器外一个非 root 用户。也就是说,user namespace 实现了 host users 和 container users 之间的映射。
启用步骤:
root@onfocus:/home/lee# ps -ef | grep python 231072 1726 1686 0 01:44 ? 00:00:00 python app.py root@onfocus:/home/lee# docker exec web35 ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 17:44 ? 00:00:00 python app.py
root@onfocus:/home/lee# cat /etc/subuid lee:100000:65536 stack:165536:65536 dockremap:231072:65536 root@onfocus:/home/lee# cat /etc/subgid lee:100000:65536 stack:165536:65536 dockremap:231072:65536
root@onfocus:/home/lee# cat /proc/1726/uid_map 0 231072 65536
root@80993d821f7b:/host/bin# touch test2 touch: cannot touch 'test2': Permission denied
这说明通过使用 user namespace,使得容器内的进程运行在非 root 用户,我们就成功地限制了容器内进程的权限。
正是/proc/
ID-inside-ns ID-outside-ns length
其中:
举个例子, 0 1000 256这条配置就表示父user namespace中的1000~1256映射到新user namespace中的0~256。
比如,把真实的uid=1000映射成容器内的uid=0:
把namespace内部从0开始的uid映射到外部从0开始的uid,其最大范围是无符号32位整形:
上面的截图中正是后面这种情形,也就是容器中的 uid 和宿主机上的 uid 是从0开始一一对应着映射的。
备注:linux user namespace 非常复杂,应该是所有 namespace 中最复杂的一个。这里只是一个简单介绍,还进一步理解,还需要阅读更多材料,比如https://lwn.net/Articles/532593/
默认情况下,当 docker 实例被创建出来后,使用 ip netns 命令无法看到容器实例对应的 network namespace。这是因为 ip netns 命令是从 /var/run/netns 文件夹中读取内容的。
步骤:
1. 找到容器的主进程 ID
root@onfocus:/home/lee# docker inspect --format '{{.State.Pid}}' web5 2704
2. 创建 /var/run/netns 目录以及符号连接
root@onfocus:/home/lee# mkdir /var/run/netns root@onfocus:/home/lee# ln -s /proc/2704/ns/net /var/run/netns/web5
3. 此时可以使用 ip netns 命令了
root@onfocus:/home/lee# ip netns web5 root@onfocus:/home/lee# ip netns exec web5 ip addr 1: lo:mtu 65536 qdisc noqueue state UNKNOWN group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 15: eth0: mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff inet 172.17.0.3/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe11:3/64 scope link valid_lft forever preferred_lft forever
其他的几个 namespace,比如 network,mnt 等,比较简单,这里就不多说了。总之,Docker 守护进程为每个容器都创建了六种namespace 的实例,使得容器中的进程都处于一种隔离的运行环境之中:
root@devstack:/proc/1726/ns# ls -l total 0 lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 ipc -> ipc:[4026532210] lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 mnt -> mnt:[4026532208] lrwxrwxrwx 1 231072 231072 0 Sep 18 01:44 net -> net:[4026532213] lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 pid -> pid:[4026532211] lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 user -> user:[4026532207] lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 uts -> uts:[4026532209]
Docker run 命令有几个参数和 namespace 相关:
--userns:指定容器使用的 user namespace
你可以在启用了 user namespace 的情况下,强制某个容器运行在 host user namespace 之中:
root@onfocus:/proc/2835# docker run -d -v /bin:/host/bin --name web37 --userns host training/webapp python app.py 9c61e9a233abef7badefa364b683123742420c58d7a06520f14b26a547a9476c root@onfocus:/proc/2835# ps -ef | grep python root 2962 2930 1 02:17 ? 00:00:00 python app.py
否则默认的话,就会运行在特定的 user namespace 之中了。
同样的,可以指定容器使用 Docker host pid namespace,这样,在容器中的进程,可以看到 host 上的所有进程。注意此时不能启用 user namespace。
同样地,可以使容器使用 Docker host uts namespace。此时,最明显的是,容器的 hostname 和 Docker hostname 是相同的。
扩展阅读:
上前面讲到 Docker 容器使用 linux namespace 来隔离其运行环境,使得容器中的进程看起来就像爱一个独立环境中运行一样。但是,光有运行环境隔离还不够,因为这些进程还是可以不受限制地使用系统资源,比如网络、磁盘、CPU以及内存 等。关于其目的,一方面,是为了防止它占用了太多的资源而影响到其它进程;另一方面,在系统资源耗尽的时候,linux 内核会触发 OOM,这会让一些被杀掉的进程成了无辜的替死鬼。因此,为了让容器中的进程更加可控,Docker 使用 Linux cgroups 来限制容器中的进程允许使用的系统资源。
Linux Cgroup 可让您为系统中所运行任务(进程)的用户定义组群分配资源 — 比如 CPU 时间、系统内存、网络带宽或者这些资源的组合。您可以监控您配置的 cgroup,拒绝 cgroup 访问某些资源,甚至在运行的系统中动态配置您的 cgroup。所以,可以将 controll groups 理解为 controller (system resource) (for) (process)groups,也就是是说它以一组进程为目标进行系统资源分配和控制。
它主要提供了如下功能:
使用 cgroup,系统管理员可更具体地控制对系统资源的分配、优先顺序、拒绝、管理和监控。可更好地根据任务和用户分配硬件资源,提高总体效率。
在实践中,系统管理员一般会利用CGroup做下面这些事(有点像为某个虚拟机分配资源似的):
查看 linux 内核中是否启用了 cgroup:
我们看到 /sys/fs/cgroup 目录中有若干个子目录,我们可以认为这些都是受 cgroups 控制的资源以及这些资源的信息。
4.2.1 通过 cgroups 限制进程的 CPU
写一段最简单的 C 程序:
int main(void) { int i = 0; for(;;) i++; return 0; }
编译,运行,发现它占用的 CPU 几乎到了 100%:
top - 22:43:02 up 1:14, 3 users, load average: 0.24, 0.06, 0.06 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2304 root 20 0 4188 356 276 R 99.6 0.0 0:11.77 hello
接下来我们做如下操作:
root@onfocus:/home/lee/c# mkdir /sys/fs/cgroup/cpu/hello root@onfocus:/home/lee/c# cd /sys/fs/cgroup/cpu/hello root@onfocus:/sys/fs/cgroup/cpu/hello# ls cgroup.clone_children cgroup.procs cpu.cfs_quota_us cpu.stat tasks cgroup.event_control cpu.cfs_period_us cpu.shares notify_on_release root@onfocus:/sys/fs/cgroup/cpu/hello# cat cpu.cfs_quota_us -1 root@onfocus:/sys/fs/cgroup/cpu/hello# echo 20000 > cpu.cfs_quota_us root@onfocus:/sys/fs/cgroup/cpu/hello# cat cpu.cfs_quota_us 20000 root@onfocus:/sys/fs/cgroup/cpu/hello# echo 2428 > tasks
然后再来看看这个进程的 CPU 占用情况:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2428 root 20 0 4188 356 276 R 19.9 0.0 0:46.03 hello
它占用的 CPU 几乎就是 20%,也就是我们预设的阈值。这说明我们通过上面的步骤,成功地将这个进程运行所占用的 CPU 资源限制在某个阈值之内了。
如果此时再启动另一个 hello 进程并将其 id 加入 tasks 文件,则两个进程会共享设定的 CPU 限制:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2428 root 20 0 4188 356 276 R 10.0 0.0 285:39.54 hello 12526 root 20 0 4188 356 276 R 10.0 0.0 0:25.09 hello
4.2.2 通过 cgroups 限制进程的 Memory
同样地,我们针对它占用的内存做如下操作:
root@onfocus:/sys/fs/cgroup/memory# mkdir hello root@onfocus:/sys/fs/cgroup/memory# cd hello/ root@onfocus:/sys/fs/cgroup/memory/hello# cat memory.limit_in_bytes 18446744073709551615 root@onfocus:/sys/fs/cgroup/memory/hello# echo 64k > memory.limit_in_bytes root@onfocus:/sys/fs/cgroup/memory/hello# echo 2428 > tasks root@onfocus:/sys/fs/cgroup/memory/hello#
上面的步骤会把进程 2428 所占用的内存阈值设置为 64K。超过的话,它会被杀掉。
4.2.3 限制进程的 I/O
运行命令:
sudo dd if=/dev/sda1 of=/dev/null
通过 iotop 命令看 IO (此时磁盘在快速转动),此时其写速度为 242M/s:
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND 2555 be/4 root 242.60 M/s 0.00 B/s 0.00 % 61.66 % dd if=/dev/sda1 of=/dev/null
接着做下面的操作:
root@onfocus:/home/lee# mkdir /sys/fs/cgroup/blkio/io root@onfocus:/home/lee# cd /sys/fs/cgroup/blkio/io root@onfocus:/sys/fs/cgroup/blkio/io# ls -l /dev/sda1 brw-rw---- 1 root disk 8, 1 Sep 18 21:46 /dev/sda1 root@onfocus:/sys/fs/cgroup/blkio/io# echo '8:0 1048576' > /sys/fs/cgroup/blkio/io/blkio.throttle.read_bps_device root@onfocus:/sys/fs/cgroup/blkio/io# echo 2725 > /sys/fs/cgroup/blkio/io/tasks
结果,这个进程的IO 速度就被限制在 1Mb/s 之内了:
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND 2555 be/4 root 990.44 K/s 0.00 B/s 0.00 % 96.29 % dd if=/dev/sda1 of=/dev/null
cgroups 的术语包括:
block IO: --blkio-weight value Block IO (relative weight), between 10 and 1000 --blkio-weight-device value Block IO weight (relative device weight) (default []) --cgroup-parent string Optional parent cgroup for the container CPU: --cpu-percent int CPU percent (Windows only) --cpu-period int Limit CPU CFS (Completely Fair Scheduler) period --cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota -c, --cpu-shares int CPU shares (relative weight) --cpuset-cpus string CPUs in which to allow execution (0-3, 0,1) --cpuset-mems string MEMs in which to allow execution (0-3, 0,1) Device: --device value Add a host device to the container (default []) --device-read-bps value Limit read rate (bytes per second) from a device (default []) --device-read-iops value Limit read rate (IO per second) from a device (default []) --device-write-bps value Limit write rate (bytes per second) to a device (default []) --device-write-iops value Limit write rate (IO per second) to a device (default []) Memory: --kernel-memory string Kernel memory limit -m, --memory string Memory limit --memory-reservation string Memory soft limit --memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap --memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
扩展阅读:
一个典型的 Linux 系统要能运行的话,它至少需要两个文件系统:
Linux 系统在启动时,roofs 首先会被挂载为只读模式,然后在启动完成后被修改为读写模式,随后它们就可以被修改了。
AUFS 是一种 Union File System(联合文件系统),又叫 Another UnionFS,后来叫Alternative UnionFS,再后来叫成高大上的 Advance UnionFS。所谓 UnionFS,就是把不同物理位置的目录合并mount到同一个目录中。UnionFS的一个最主要的应用是,把一张CD/DVD和一个硬盘目录给联合 mount在一起,然后,你就可以对这个只读的CD/DVD上的文件进行修改(当然,修改的文件存于硬盘上的目录里)。
举个例子,假设在 Ubuntu 系统上现有如下目录结构:
tree . ├── fruits │ ├── apple │ └── tomato └── vegetables ├── carrots └── tomato # 创建一个mount目录 $ mkdir mnt # 把水果目录和蔬菜目录union mount到 ./mnt目录中 $ sudo mount -t aufs -o dirs=./fruits:./vegetables none ./mnt # 查看./mnt目录 $ tree ./mnt ./mnt ├── apple ├── carrots └── tomato
我们可以看到在./mnt目录下有三个文件,苹果apple、胡萝卜carrots和蕃茄tomato。水果和蔬菜的目录被union到了./mnt目录下了。
我们来修改一下其中的文件内容:
$ echo mnt > ./mnt/apple $ cat ./mnt/apple mnt $ cat ./fruits/apple mnt
上面的示例,我们可以看到./mnt/apple的内容改了,./fruits/apple的内容也改了。
$ echo mnt_carrots > ./mnt/carrots $ cat ./vegetables/carrots $ cat ./fruits/carrots mnt_carrots
关于 AUFS 的几个特点:
详情请转至Docker基础技术:AUFS
OverlayFS是一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如ext4fs和xfs等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现,这也就是联合挂载技术,对比于AUFS,OverlayFS速度更快,实现更简单。 而Linux 内核为Docker提供的OverlayFS驱动有两种:overlay和overlay2。而overlay2是相对于overlay的一种改进,在inode利用率方面比overlay更有效。但是overlay有环境需求:docker版本17.06.02+,宿主机文件系统需要是ext4或xfs格式。
overlayfs通过三个目录:lower目录、upper目录、以及work目录实现,其中lower目录可以是多个,work目录为工作基础目录,挂载后内容会被清空,且在使用过程中其内容用户不可见,最后联合挂载完成给用户呈现的统一视图称为为merged目录。以下使用mount将演示其如何工作的。
使用mount命令挂载overlayfs语法如下:
mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged_dir
创建三个目录A、B、C,以及worker目录:
然后使用mount联合挂载到/tmp/test 下:
然后我们再去查看/tmp/test目录,你会发现目录A、B、C被合并到了一起,并且相同文件名的文件会进行“覆盖”,这里覆盖并不是真正的覆盖,而是当合并时候目录中两个文件名称都相同时,merged层目录会显示离它最近层的文件:
同时我们还可以通过mount命令查看其挂载的选项:
以上这样的方式也就是联合挂载技术。
介绍了overlay驱动原理以后再来看Docker中的overlay存储驱动,以下是来自docker官网关于overlay的工作原理图:
在上述图中可以看到三个层结构,即:lowerdir、uperdir、merged,其中lowerdir是只读的image layer,其实就是rootfs,对比我们上述演示的目录A和B,我们知道image layer可以分很多层,所以对应的lowerdir是可以有多个目录。而upperdir则是在lowerdir之上的一层,这层是读写层,在启动一个容器时候会进行创建,所有的对容器数据更改都发生在这里层,对比示例中的C。最后merged目录是容器的挂载点,也就是给用户暴露的统一视角,对比示例中的/tmp/test。而这些目录层都保存在了/var/lib/docker/overlay2/或者/var/lib/docker/overlay/(如果使用overlay)。
启动一个容器
查看其overlay挂载点,可以发现其挂载的merged目录、lowerdir、upperdir以及workdir:
overlay2的lowerdir可以有多个,并且是软连接方式挂载,后续我们会进行说明。
当容器中发生数据修改时候overlayfs存储驱动又是如何进行工作的?以下将阐述其读写过程:
读:
修改:
注意事项
从仓库pull一个ubuntu镜像,结果显示总共拉取了4层镜像如下:
此时4层被存储在了/var/lib/docker/overlay2/目录下:
这里面多了一个l目录包含了所有层的软连接,短链接使用短名称,避免mount时候参数达到页面大小限制(演示中mount命令查看时候的短目录):
处于底层的镜像目录包含了一个diff和一个link文件,diff目录存放了当前层的镜像内容,而link文件则是与之对应的短名称:
在这之上的镜像还多了work目录和lower文件,lower文件用于记录父层的短名称,work目录用于联合挂载指定的工作目录。而这些目录和镜像的关系是怎么组织在的一起呢?答案是通过元数据关联。元数据分为image元数据和layer元数据。
镜像元数据存储在了/var/lib/docker/image/
查看其对应的元数据(使用vim :%!python -m json.tool格式化成json) 截取了其rootfs的构成:
上面的 diff_id 对应的的是一个镜像层,其排列也是有顺序的,从上到下依次表示镜像层的最低层到最顶层:
diff_id如何关联进行层?具体说来,docker 利用 rootfs 中的每个diff_id 和历史信息计算出与之对应的内容寻址的索引(chainID) ,而chaiID则关联了layer层,进而关联到每一个镜像层的镜像文件。
layer 对应镜像层的概念,在 docker 1.10 版本以前,镜像通过一个 graph 结构管理,每一个镜像层都拥有元数据,记录了该层的构建信息以及父镜像层 ID,而最上面的镜像层会多记录一些信息作为整个镜像的元数据。graph 则根据镜像 ID(即最上层的镜像层 ID) 和每个镜像层记录的父镜像层 ID 维护了一个树状的镜像层结构。
在 docker 1.10 版本后,镜像元数据管理巨大的改变之一就是简化了镜像层的元数据,镜像层只包含一个具体的镜像层文件包。用户在 docker 宿主机上下载了某个镜像层之后,docker 会在宿主机上基于镜像层文件包和 image 元数据构建本地的 layer 元数据,包括 diff、parent、size 等。而当 docker 将在宿主机上产生的新的镜像层上传到 registry 时,与新镜像层相关的宿主机上的元数据也不会与镜像层一块打包上传。
Docker 中定义了 Layer 和 RWLayer 两种接口,分别用来定义只读层和可读写层的一些操作,又定义了 roLayer 和 mountedLayer,分别实现了上述两种接口。其中,roLayer 用于描述不可改变的镜像层,mountedLayer 用于描述可读写的容器层。具体来说,roLayer 存储的内容主要有索引该镜像层的 chainID、该镜像层的校验码 diffID、父镜像层 parent、storage_driver 存储当前镜像层文件的 cacheID、该镜像层的 size 等内容。这些元数据被保存在 /var/lib/docker/image/
每个chainID目录下会存在三个文件cache-id、diff、zize:
cache-id文件:
docker随机生成的uuid,内容是保存镜像层的目录索引,也就是/var/lib/docker/overlay2/中的目录,这就是为什么通过chainID能找到对应的layer目录。以chainID为d801a12f6af7beff367268f99607376584d8b2da656dcd8656973b7ad9779ab4 对应的目录为 130ea10d6f0ebfafc8ca260992c8d0bef63a1b5ca3a7d51a5cd1b1031d23efd5,也就保存在/var/lib/docker/overlay2/130ea10d6f0ebfafc8ca260992c8d0bef63a1b5ca3a7d51a5cd1b1031d23efd5
diff文件:
保存了镜像元数据中的diff_id(与元数据中的diff_ids中的uuid对应)
size文件:
保存了镜像层的大小
在 layer 的所有属性中,diffID 采用 SHA256 算法,基于镜像层文件包的内容计算得到。而 chainID 是基于内容存储的索引,它是根据当前层与所有祖先镜像层 diffID 计算出来的,具体算如下:
mountedLayer 信息存储的可读init层以及容器挂载点信息包括:容器 init 层ID(init-id)、联合挂载使用的ID(mount-id)以及容器层的父层镜像的 chainID(parent)。相关文件位于/var/lib/docker/image/
如下启动一个id为3c96960b3127的容器:
查看其对应的mountedLayer三个文件:
可以看到initID是在mountID后加了一个-init,同时initID就是存储在/var/lib/docker/overlay2/的目录名称:
查看mountID还可以直接通过mount命令查看对应挂载的mountID,对应着/var/lib/docker/overlay2/目录,这也是overlayfs呈现的merged目录:
在容器中创建了一文件:
此时到宿主的merged目录就能看到对应的文件:
init层是以一个uuid+-init结尾表示,夹在只读层和读写层之间,作用是专门存放/etc/hosts、/etc/resolv.conf等信息,需要这一层的原因是当容器启动时候,这些本该属于image层的文件或目录,比如hostname,用户需要修改,但是image层又不允许修改,所以启动时候通过单独挂载一层init层,通过修改init层中的文件达到修改这些文件目的。而这些修改往往只读当前容器生效,而在docker commit提交为镜像时候,并不会将init层提交。该层文件存放的目录为/var/lib/docker/overlay2/
通过以上的内容介绍,一个容器完整的层应由三个部分组成,如下图:
以overlayfs作为存储驱动的的镜像存储原理其中每层的镜像数据保存在/var/lib/docker/overlay2/
参考:
《use overlayfs driver 》
《Docker 镜像之存储管理》
https://www.cnblogs.com/wdliu/p/10483252.html