# Docker核心技术(三)文件系统

Docker核心技术-文件系统

文件系统

在最开始用到docker的时候,比较直观的感受就是不用担心搞错东西了,不再为搞错有些东西之后,修复不了不得已只能重做系统担忧了,因为我们完全可以从新再搞一个容器,很快而且又很干净。 image.png

根路径隔离rootfs

在说rootfs之前,我们先一起看看linux操作系统,一般的对于我们使用来讲,可以将linux系统分为shell+内核(kernel),内核可以理解为是linux操作系统的心脏,是运行程序和管理磁盘,打印机等硬件设备的核心程序,它提供了一个在裸设备与应用程序间的抽象层。而我们常说的centos,ubuntu是linux的发行版,他们可以高效使用linux内核的操作系统,涵盖了内核还有其他工具,命令行shell和图形桌面等。

在linux系统中我们经常执行的shell命令,全部都是可执行文件(sh,ssh等)。我们之所以能够登陆进入系统就可以使用是因为这些文件路径都被写入了环境变量中,所以我们可以直接使用。

[root@i-hrhoufpl ~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
[root@i-hrhoufpl ~]# which sh bash ssh
/usr/bin/sh
/usr/bin/bash
/usr/bin/ssh
[root@i-hrhoufpl ~]#
复制代码

那么我们试想一下,我们如果将这些可执行文件放到一个特定的目录下,然后给这个路径下放大多数常用的linux shell命令的可执行文件,那么我们就可以在这个路径下执行很多命令(即使这时候环境变量中没有提前配置)。

下面举个例子结合chroot命令将一个bash进程囚禁到一个指定的目录下/opt/os

[root@docker opt]# mkdir os
[root@docker opt]# J=/opt/os
[root@docker opt]# mkdir -p $J/{bin,lib64,lib}
[root@docker opt]# cd $J

#copy bash,ls命令到要囚禁的bin目录下
[root@docker os]# cp -v /bin/{bash,ls} $J/bin
"/bin/bash" -> "/opt/os/bin/bash"
"/bin/ls" -> "/opt/os/bin/ls"

#ldd可以列出一个程序所需要得动态链接库,查看ls命令动态库
[root@docker os]# ldd /bin/ls
	linux-vdso.so.1 =>  (0x00007ffc73754000)
	libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fecc6782000)
	libcap.so.2 => /lib64/libcap.so.2 (0x00007fecc657d000)
	libacl.so.1 => /lib64/libacl.so.1 (0x00007fecc6374000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fecc5fa6000)
	libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fecc5d44000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007fecc5b40000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fecc69a9000)
	libattr.so.1 => /lib64/libattr.so.1 (0x00007fecc593b000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fecc571f000)
[root@docker os]# list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
[root@docker os]# for i in $list; do cp  -v "$i" "${J}${i}"; done
"/lib64/libselinux.so.1" -> "/opt/os/lib64/libselinux.so.1"
"/lib64/libcap.so.2" -> "/opt/os/lib64/libcap.so.2"
"/lib64/libacl.so.1" -> "/opt/os/lib64/libacl.so.1"
"/lib64/libc.so.6" -> "/opt/os/lib64/libc.so.6"
"/lib64/libpcre.so.1" -> "/opt/os/lib64/libpcre.so.1"
"/lib64/libdl.so.2" -> "/opt/os/lib64/libdl.so.2"
"/lib64/ld-linux-x86-64.so.2" -> "/opt/os/lib64/ld-linux-x86-64.so.2"
"/lib64/libattr.so.1" -> "/opt/os/lib64/libattr.so.1"
"/lib64/libpthread.so.0" -> "/opt/os/lib64/libpthread.so.0"

#ldd查看bash命令动态库,并且copy到lib64对应目录下
[root@docker os]# ldd /bin/bash
	linux-vdso.so.1 =>  (0x00007ffdbe3c7000)
	libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007fc288774000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007fc288570000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fc2881a2000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fc28899e000)
[root@docker os]# list="$(ldd /bin/bash | egrep -o '/lib.*\.[0-9]')"
[root@docker os]# for i in $list; do cp  -v "$i" "${J}${i}"; done
"/lib64/libtinfo.so.5" -> "/opt/os/lib64/libtinfo.so.5"
cp:是否覆盖"/opt/os/lib64/libdl.so.2"? y
"/lib64/libdl.so.2" -> "/opt/os/lib64/libdl.so.2"
cp:是否覆盖"/opt/os/lib64/libc.so.6"? y
"/lib64/libc.so.6" -> "/opt/os/lib64/libc.so.6"
cp:是否覆盖"/opt/os/lib64/ld-linux-x86-64.so.2"? y
"/lib64/ld-linux-x86-64.so.2" -> "/opt/os/lib64/ld-linux-x86-64.so.2"

#启动一个bash进程并将其囚禁到/opt/os下
[root@docker os]# sudo chroot /opt/os /bin/bash
bash-4.2# ls /
bin  lib  lib64
bash-4.2#
复制代码

可以看到我们将这个bash囚禁到了我们制定的路径下/opt/os。

注:如果在chroot进程中进去之后,发现ls 命令提示command not found首先确保copy了ls命令到/opt/os/bin/目录下,如果已经copy进去了在执行chroot的时候添加上sudo,就可以正常访问了。 image.png

联合文件系统

联合文件系统(Union File System)UnionFS是docker制作镜像和容器文件的主要技术。通过联合文件系统,可以将多个路径挂载到同一个挂载点上,实现多个path的合并操作(上层同名文件会将下层文件覆盖),最后通过挂载点向上层应用/用户呈现一个合并之后的视图。联合文件系统有多种实现,ubuntu采用AUFS实现,centos采用overlay/overlay2来实现。

我们这里看下overlay的方式,overlay将文件路径分为三类:

  • lowerdir: lowerdir作为最底层,lower里面的文件是只读文件,lowerdir可以由多个路径组成(一个路径表示一层);
  • upperdir: upperdir在lowerdir之上,upper里面的文件可以读写;
  • merged: merged为lower和upper合并之后暴露给用户的视图,在merged层的修改内容最终会反馈到upperdir;

下面通过一个例子来看一下overlay的工作原理

[root@docker layers]# tree
.
├── A
│   ├── 1.txt
│   └── a.txt
├── B
│   ├── 1.txt
│   └── b.txt
├── merge
├── upper
└── work

5 directories, 4 files
[root@docker layers]#

## A/1.txt的内容是 this is a txt
## B/1.txt的内容是 this is b txt
复制代码

执行挂在文件操作使用overlay的方式mount -t overlay overlay -o lowerdir=A:B,upperdir=upper,workdir=work merge的意思是

  • 将A,B按照顺序作为lower层,A是叠加在B之上的
  • upper文件作为upper层,通过overlay联合挂在到merge下面
[root@docker layers]# mount -t overlay overlay -o lowerdir=A:B,upperdir=upper,workdir=work merge
[root@docker layers]# tree
.
├── A
│   ├── 1.txt
│   └── a.txt
├── B
│   ├── 1.txt
│   └── b.txt
├── merge
│   ├── 1.txt
│   ├── a.txt
│   └── b.txt
├── upper
└── work
    └── work

6 directories, 7 files
[root@docker layers]#
复制代码

我们可以看到merge中出现了1.txt,那么A/B/merge下的1.txt内容一样吗?内容有什么差异呢?这时候我们看一下A/B/merge中1.txt的内容

[root@docker ~]# cd layers/
[root@docker layers]# cat A/1.txt
this is a txt
[root@docker layers]# cat B/1.txt
this is b txt
[root@docker layers]# cat merge/1.txt
this is a txt
[root@docker layers]#
复制代码

我们可以看到在执行UnionFS挂在后,merge路径下出现A,B文件合并之后的结果。并且A,B中如果有相同的文件,在合并到merge目录下的时候,会选择用上层路径(A)的文件覆盖下层路径(B)的1.txt文件,也就说在将lower层文件合并的时候,如果多个了路径下有相同文件,那么上层的文件会覆盖下层路径下的文件

可以看下docker官网对于overlayFS的介绍,当下层和上层有同名文件(file2)的时候,在最终的merge层呈现的是上层的文件。 image.png 我们在回到我们的例子中看一下如果在merge中修改了文件会发生什么。(再上面merge之后upper层还没有出现文件)

[root@docker layers]# echo update in merge folder > merge/1.txt
[root@docker layers]# echo hellworld > merge/n.txt
[root@docker layers]# tree
.
├── A
│   ├── 1.txt
│   └── a.txt
├── B
│   ├── 1.txt
│   └── b.txt
├── merge
│   ├── 1.txt
│   ├── a.txt
│   ├── b.txt
│   └── n.txt
├── upper
│   ├── 1.txt
│   └── n.txt
└── work
    └── work

6 directories, 10 files
[root@docker layers]#
复制代码

我们可以看到在merge中修改和添加文件后,在upper层中也出现了改动的文件,那么我们这时候再看看lower,upper,merge三个目录中的文件都是什么样的。

[root@docker layers]# cat A/1.txt
this is a txt
[root@docker layers]# cat B/1.txt
this is b txt
[root@docker layers]# cat merge/1.txt
update in merge folder
[root@docker layers]# cat upper/1.txt
update in merge folder
[root@docker layers]# cat merge/n.txt
hellworld
[root@docker layers]# cat upper/n.txt
hellworld
复制代码

 可以看到当进行merge层的改动的时候,改动的文件只会影响到upper层,不会改动lower层的文件。

在刚才的例子中1.txt和n.txt最初都不在upper层中,那么现在upper层中都已经存在1.txt我们再次修改merge层文件,看看会发生什么

[root@docker layers]# echo 'new msg' > merge/1.txt
[root@docker layers]# cat merge/1.txt
new msg
[root@docker layers]# cat upper/1.txt
new msg
[root@docker layers]#
复制代码

结合以上例子可以看到,如果修改了merge层的文件,文件只会影响upper层的文件,如果upper层不存在文件,那么copy一份到upper层,如果upper层文件已经存在,那么直接更新文件内容。

docker分层原理

在理解了联合文件系统之后,我们看下docker镜像的分层管理 image.png

docker push registry.tkestack.anchnet.com/tsf_100000001/smartops-sync-hws:v20220125-223633
The push refers to repository [registry.tkestack.anchnet.com/tsf_100000001/smartops-sync-hws]
e90f13ea8e00: Preparing
611df1350105: Preparing
c7d7bbf0266c: Preparing
b2fbaed13fca: Preparing
e626b19900c1: Preparing
05d3177d5441: Preparing
ce6c8756685b: Waiting
30339f20ced0: Waiting
05d3177d5441: Waiting
ff7f0198eafa: Waiting
0eb22bfb707d: Waiting
a2ae92ffcd29: Waiting
0f687b817f3f: Waiting
2a81589b70f3: Waiting
789dbe3baa15: Waiting
e626b19900c1: Layer already exists
05d3177d5441: Layer already exists
611df1350105: Pushed
ff7f0198eafa: Layer already exists
789dbe3baa15: Layer already exists
0f687b817f3f: Layer already exists
35c20f26d188: Layer already exists
e90f13ea8e00: Pushed
2a81589b70f3: Layer already exists
6ed1a81ba5b6: Layer already exists
a3483ce177ce: Layer already exists
ce6c8756685b: Layer already exists
c7d7bbf0266c: Pushed
30339f20ced0: Layer already exists
c3fe59dd9556: Layer already exists
0eb22bfb707d: Layer already exists
a2ae92ffcd29: Layer already exists
b2fbaed13fca: Pushed
v20220125-223633: digest: sha256:4b8a417a50599aa9a23a1a63796a4d7b002a605fa16a948c8ffbd1ccaaaf4566 size: 4089
Job succeeded
复制代码

我们一般拉/推镜像的时候可以看到docker也是利用了分层的方式来管理镜像,结合对于unionfs的理解我们可以发现,如果底层的镜像已经存在并且在新的tag中并没有修改到这个文件,那么新的merge层就不需要改动这些文件,那么就可以直接提示layer already exists。这样的话就不需要额外的来存储相同的文件。

一般在我们使用docker tag命令的将镜像进行新版本升级的,其实大多数我们都是只有少部分文件所在的层发生改动,因此也就只需要将新版本的container的upperdir保存起来,并且将新的upperdie加入到原镜像的lowerdir层放到最上面,因此我们不断的对container打tag可以理解为不断将docker容器读写层变成只读层的过程(在原先镜像的基础上+新容器中改动的内容=新版本镜像,v1 image lowerdir + changed upperdir(new lowderdir) = v2 image),也就是说对于整个文件系统并不是将所有原镜像文件都要在copy一份,而是只多一个新的层放到原lowder上面就可以,这样可以大大节省磁盘空间。

在使用dockerfile构建镜像的时候,每一层都是一个RUN指令运行的结果,因此为了避免构建时候出现过多镜像层,一般都建议在dockerfile中一个RUN下声明多个shell命令(这就解释了为什么不建议太多RUN),如果多个镜像中都包含一个相同的RUN命令,那么最后构建出来的镜像层会是一样的,这样docker只会存储一份到本地磁盘,这样会大大节省磁盘空间。 我们一般可以使用docker inspect $image_id来查看docker镜像文件的信息,

[root@docker ~]# docker inspect 10cd3e2f1d52
[
    {
        "Id": "sha256:10cd3e2f1d5230ea21ad28b51c9189cfe97d813a6f57c13c7df2e14cf5cf112b",
        "RepoTags": [
            "13227829078/gogin:v1.0"
        ],
        "RepoDigests": [],
        "Parent": "sha256:7b4fb5715275d656fc7574667faca2574ed3978f652f94e4ad4f94fd2ce92268",
        "Comment": "",
        "Created": "2022-01-28T07:05:25.299810802Z",
        "Container": "de812032b431a50207aaef2afc260533852a04eec9ccb5024af8a668e97da13c",
        "ContainerConfig": {
            "Hostname": "6249d683c30e",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "8888/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "GOLANG_VERSION=1.14.15",
                "GOPATH=/go",
                "GOPROXY=https://goproxy.io",
                "GO111MODULE=on"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "ENTRYPOINT [\"./go-gin\"]"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:7b4fb5715275d656fc7574667faca2574ed3978f652f94e4ad4f94fd2ce92268",
            "Volumes": null,
            "WorkingDir": "/app",
            "Entrypoint": [
                "./go-gin"
            ],
            "OnBuild": [],
            "Labels": {}
        },
        "DockerVersion": "1.13.1",
        "Author": "",
        "Config": {
            "Hostname": "6249d683c30e",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "8888/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "GOLANG_VERSION=1.14.15",
                "GOPATH=/go",
                "GOPROXY=https://goproxy.io",
                "GO111MODULE=on"
            ],
            "Cmd": null,
            "ArgsEscaped": true,
            "Image": "sha256:7b4fb5715275d656fc7574667faca2574ed3978f652f94e4ad4f94fd2ce92268",
            "Volumes": null,
            "WorkingDir": "/app",
            "Entrypoint": [
                "./go-gin"
            ],
            "OnBuild": [],
            "Labels": {}
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 385918407,
        "VirtualSize": 385918407,
        "GraphDriver": {
            "Name": "overlay2",
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/ddcca9619288c19bdf2030b46a40d8c225bac189bb3f40c596c16314edcda859/diff:/var/lib/docker/overlay2/98d466e757d023119ae48246d813b885f52d53d3e950e9c54f5813a1deb72d74/diff:/var/lib/docker/overlay2/d36ba264846fbe454c121b840c8892fec77eff1b6d392a88cc9dbbb1b843cfac/diff:/var/lib/docker/overlay2/5618d51e93bdddad560080d3dfd4cb85a24e8cd97b9a5db1028e13582fbaa7cf/diff:/var/lib/docker/overlay2/6ca5a266f6247183984b2aed218588f368e10d5e9e38bd65f592e03d779a3993/diff:/var/lib/docker/overlay2/125d68741b4f2f59fa0629b06d10e17fc2f76fbff23922de5487112397a75614/diff:/var/lib/docker/overlay2/751bae6662d4368cabe965f259a48b5c620873a254ba874d6227d5b5a472d60a/diff",
                "MergedDir": "/var/lib/docker/overlay2/e6e12126e1071c3cf4ad58fa2c8ffae5471ba42d1099922036676797fa5a20a0/merged",
                "UpperDir": "/var/lib/docker/overlay2/e6e12126e1071c3cf4ad58fa2c8ffae5471ba42d1099922036676797fa5a20a0/diff",
                "WorkDir": "/var/lib/docker/overlay2/e6e12126e1071c3cf4ad58fa2c8ffae5471ba42d1099922036676797fa5a20a0/work"
            }
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:1119ff37d4a9531330e3b8487863ee8ae0308337351be9d5f8bb38f80790acd9",
                "sha256:6bdf11a0a4c7366050a7a4ebf393fc34f398ffd696967e95217e931f023f5086",
                "sha256:f97a68b932e1e71d605468b3affc73b470854bbd51755a06d5fb3d36221efe4a",
                "sha256:9f9d00b69565cf96dc6f631b44f4b30eb05d58afed57bc362011f931acf20740",
                "sha256:9986a8f6a81b3ededf764bc456aa45479fd7bd8635edb8a09ae9838b539eccb0",
                "sha256:9c6ece032733daed56f18829e060cb62975d84e015cb26831118119fd79c77ec",
                "sha256:37cd7d664d3a8549921601642be4e21b607c7b478f8bb20761088904d2e03ab1",
                "sha256:a7dd98c6789e20155d68e7d96d991a10967a14cc22802d1589f156f24b3916f2"
            ]
        }
    }
]
复制代码

可以使用docker inspect $image_id --format {{.GraphDriver.Data}}查看具体的文件层信息以及存储路径。

[root@docker ~]# docker inspect 10cd3e2f1d52 --format {{.GraphDriver.Data}}
map[UpperDir:/var/lib/docker/overlay2/e6e12126e1071c3cf4ad58fa2c8ffae5471ba42d1099922036676797fa5a20a0/diff WorkDir:/var/lib/docker/overlay2/e6e12126e1071c3cf4ad58fa2c8ffae5471ba42d1099922036676797fa5a20a0/work LowerDir:/var/lib/docker/overlay2/ddcca9619288c19bdf2030b46a40d8c225bac189bb3f40c596c16314edcda859/diff:/var/lib/docker/overlay2/98d466e757d023119ae48246d813b885f52d53d3e950e9c54f5813a1deb72d74/diff:/var/lib/docker/overlay2/d36ba264846fbe454c121b840c8892fec77eff1b6d392a88cc9dbbb1b843cfac/diff:/var/lib/docker/overlay2/5618d51e93bdddad560080d3dfd4cb85a24e8cd97b9a5db1028e13582fbaa7cf/diff:/var/lib/docker/overlay2/6ca5a266f6247183984b2aed218588f368e10d5e9e38bd65f592e03d779a3993/diff:/var/lib/docker/overlay2/125d68741b4f2f59fa0629b06d10e17fc2f76fbff23922de5487112397a75614/diff:/var/lib/docker/overlay2/751bae6662d4368cabe965f259a48b5c620873a254ba874d6227d5b5a472d60a/diff MergedDir:/var/lib/docker/overlay2/e6e12126e1071c3cf4ad58fa2c8ffae5471ba42d1099922036676797fa5a20a0/merged]
复制代码

docker镜像加载原理

在一开始使用docker的时候肯定都会有一个疑问,容器为什么比虚拟机占用空间小启动还更快。

  • 容器直接使用宿主机的硬件资源,不需要进行硬件的虚拟化所以容器的创建会比虚拟机更快。
  • docker利用的是宿主机的内核,而不需要Guest OS(这也就解释了为什么不能在linux机器运行windows容器,windows机器不能运行linux容器),因此当创建一个新容器的时候不需要像虚拟机一样加载一个操作系统内核,避免了引导加载操作系统的过程。(现在有一种通过在windows机器上运行linux虚拟机之后启动linux docker容器,来反驳windows可以运行linux容器,其实这是不对的,因容器最终还是运行在虚拟机上的linux操作系统的,相当于还是使用的linux内核。)

同样的在了解docker的镜像加载原理前我们先看看linux的启动过程。正常linux的启动过程为bootloader首先运行,然后将内核(kernel)复制到内存中,并在内存中设置好传递给内核的参数最后运行内核。内核启动后,挂在根文件系统(root filesystem),启动文件系统中的应用程序。

docker镜像加载原理

  • Docker的镜像实际上是由UnionFS一层一层的文件系统组成;
  • boots(boot file system)主要包含bootloader和kernel(内核), bootloader主要是引导加载 kernel, linux刚启动时会加载bootfs文件系统,当boots加载完成之后,整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核此时系统也会卸载bootfs,docker底层直接宿主机的kernel,自己只需要提供rootfs就可以了(对容器而言省掉了这一个过程)
  • rootfs(root file system)在bootfs之上,容器也可以通过chroot来制定rootfs的位置;

猜你喜欢

转载自juejin.im/post/7068912318028972040