上一篇Blog详细介绍了如何快速按照几个中间件或服务器,并简单的测试使用,那么截止上篇文章,用了两篇Blog来完成形而下的东西,也就是怎么玩儿Docker,本篇Blog就来唠唠一些原理层面的内容,方便再更深度使用的时候有理论基础来支撑。学习过程就类似这样,先用、再学理论去引导、再深度的去用,一层一层的。
Docker镜像加载原理
什么是镜像?镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需要的所有内容,包括代码,运行时(一个程序在运行或者在被执行的依赖)、库,环境变量和配置文件。其实也可以理解为一个文件目录。
UnionFS-联合文件系统
Docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统是UnionFS联合文件系统, Union文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承, 基于基础镜像, 可以制作各种具体的应用镜像
特性: 一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录
典型的Linux文件系统由bootfs和rootfs两部分组成:
-
bootfs(boot file system)主要包含 bootloader和kernel,bootloader主要是引导加载kernel,当kernel被加载到内存中后 bootfs就被umount了,也就是卸载了
- 传统的Linux加载bootfs时会先将rootfs设为read-only,然后在系统自检之后将rootfs从read-only改为read-write,然后我们就可以在rootfs上进行写和读的操作了。
- Docker的镜像不是这样,它在bootfs自检完毕之后并不会把rootfs的read-only改为read-write。而是利用union mount(UnionFS的一种挂载机制)将一个或多个read-only的rootfs加载到之前的read-only的rootfs层之上。在加载了这么多层的rootfs之后,仍然让它看起来只像是一个文件系统,在Docker的体系里把union mount的这些read-only的rootfs叫做Docker的镜像。但是,此时的每一层rootfs都是read-only的,我们此时还不能对其进行操作。当我们创建一个容器,也就是将Docker镜像进行实例化,系统会在一层或是多层read-only的rootfs之上分配一层空的read-write的rootfs
-
rootfs (root file system) 包含的就是典型 Linux 系统中的
/dev,/proc,/bin,/etc
等标准目录和文件,对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了,因为底层直接用Host的kernel,自己只需要提供 rootfs 就行了。由此可见对于不同的linux发行版, bootfs是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs
还记得我之前在第一篇Blog中的描述【Docker学习笔记 一】Docker基本概念及理论基础,Docker容器内的应用直接运行在宿主机的内核,容器是没有自己的内核的,也没有虚拟硬件,是内核级别的虚拟化技术。
镜像分层机制
Docker镜像的设计中,引入了层(layer)的概念,也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量目录:
分层的好处显而易见:资源共享,假如有多个镜像都从相同的 base 镜像构建而来,那么宿主机只需在磁盘上保存一份base镜像,同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。
统一文件系统(Union File System)技术能够将不同的层整合成一个文件系统,为这些层提供了统一的视角,这样就隐藏了多层的存在在用户的角度来看,只存在一个文件系统。
例如安装一个tomcat,会发现基础镜像层如果已经存在就不需要重复下载
[root@192 ~]# docker pull tomcat
Using default tag: latest
latest: Pulling from library/tomcat
0e29546d541c: Already exists
9b829c73b52b: Already exists
cb5b7ae36172: Already exists
6494e4811622: Already exists
668f6fcc5fa5: Already exists
dc120c3e0290: Already exists
8f7c0eebb7b1: Already exists
77b694f83996: Already exists
0f611256ec3a: Already exists
4f25def12f23: Already exists
Digest: sha256:9dee185c3b161cdfede1f5e35e8b56ebc9de88ed3a79526939701f3537a52324
Status: Downloaded newer image for tomcat:latest
docker.io/library/tomcat:latest
可以通过如下命令查看分层加载的镜像:
[root@192 ~]# docker image inspect tomcat
[
{
"Id": "sha256:fb5657adc892ed15910445588404c798b57f741e9921ff3c1f1abe01dbb56906",
"RepoTags": [
"tomcat:latest"
],
"RepoDigests": [
"tomcat@sha256:9dee185c3b161cdfede1f5e35e8b56ebc9de88ed3a79526939701f3537a52324"
],
"Parent": "",
"Comment": "",
"Created": "2021-12-22T17:07:13.333084424Z",
"Container": "de0900b3a6caf902ccdaa1c7871d244e29978119ad8a1cce799cf47f1717b679",
"ContainerConfig": {
"Hostname": "de0900b3a6ca",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"8080/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/tomcat/bin:/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"JAVA_HOME=/usr/local/openjdk-11",
"LANG=C.UTF-8",
"JAVA_VERSION=11.0.13",
"CATALINA_HOME=/usr/local/tomcat",
"TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
"LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
"GPG_KEYS=A9C5DF4D22E99998D9875A5110C01C5A2F6059E7",
"TOMCAT_MAJOR=10",
"TOMCAT_VERSION=10.0.14",
"TOMCAT_SHA512=c2d2ad5ed17f7284e3aac5415774a8ef35434f14dbd9a87bc7230d8bfdbe9aa1258b97a59fa5c4030e4c973e4d93d29d20e40b6254347dbb66fae269ff4a61a5"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"catalina.sh\" \"run\"]"
],
"Image": "sha256:6e2683bf6f13f0050833b6807871b4980142835747139a2c2ae91b274787e399",
"Volumes": null,
"WorkingDir": "/usr/local/tomcat",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"DockerVersion": "20.10.7",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"8080/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/tomcat/bin:/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"JAVA_HOME=/usr/local/openjdk-11",
"LANG=C.UTF-8",
"JAVA_VERSION=11.0.13",
"CATALINA_HOME=/usr/local/tomcat",
"TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib",
"LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib",
"GPG_KEYS=A9C5DF4D22E99998D9875A5110C01C5A2F6059E7",
"TOMCAT_MAJOR=10",
"TOMCAT_VERSION=10.0.14",
"TOMCAT_SHA512=c2d2ad5ed17f7284e3aac5415774a8ef35434f14dbd9a87bc7230d8bfdbe9aa1258b97a59fa5c4030e4c973e4d93d29d20e40b6254347dbb66fae269ff4a61a5"
],
"Cmd": [
"catalina.sh",
"run"
],
"Image": "sha256:6e2683bf6f13f0050833b6807871b4980142835747139a2c2ae91b274787e399",
"Volumes": null,
"WorkingDir": "/usr/local/tomcat",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"Architecture": "amd64",
"Os": "linux",
"Size": 679618222,
"VirtualSize": 679618222,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/2fa9b45db352ce43e33bc21cbcbac9570ed998ba2d0e6f2ea1bd849848aa378e/diff:/var/lib/docker/overlay2/4ed31015b0a7955642669dfa51bcc51d41050d08fe3b27a103d088e366a83c85/diff:/var/lib/docker/overlay2/6ec7f2f6d2420d08adcaa7f8f4c63050bd4621bc7c9a39f1042c0d3c1ecaef03/diff:/var/lib/docker/overlay2/24a7055f343fe15c5fe4957e0266a7cdccaf5e09debe2a13224afd16eb4dacb6/diff:/var/lib/docker/overlay2/3a8a1619c0ca510532d8062be2b40e09f59bbb91ea48704da894006c34680d29/diff:/var/lib/docker/overlay2/c2a07a3ad966e9e6bb504c23e3007a491e82f5561c5538ed1d73278612fe2ca3/diff:/var/lib/docker/overlay2/3cfbce5aac5aa34a20a12b86a0d76a2b757eb39f23e2d27c2ce937e041129f6c/diff:/var/lib/docker/overlay2/cc83174f85ca6519fd5f5b439acb26ea36d45bcff37704e1e4e19ef0d747499e/diff:/var/lib/docker/overlay2/e38e7ef258495ff25d8c928367274a6097f2b950527f03a941f4746debb77215/diff",
"MergedDir": "/var/lib/docker/overlay2/7f19eb1e3fdaed283c2e2c1a2eb865150de7f8ee50ce64e9ebdf6e595695ad75/merged",
"UpperDir": "/var/lib/docker/overlay2/7f19eb1e3fdaed283c2e2c1a2eb865150de7f8ee50ce64e9ebdf6e595695ad75/diff",
"WorkDir": "/var/lib/docker/overlay2/7f19eb1e3fdaed283c2e2c1a2eb865150de7f8ee50ce64e9ebdf6e595695ad75/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:11936051f93baf5a4fb090a8fa0999309b8173556f7826598e235e8a82127bce",
"sha256:31892cc314cb1993ba1b8eb5f3002c4e9f099a9237af0d03d1893c6fcc559aab",
"sha256:8bf42db0de72f74f4ef0c1d1743f5d54efc3491ee38f4af6d914a6032148b78e",
"sha256:26a504e63be4c63395f216d70b1b8af52263a5289908df8e96a0e7c840813adc",
"sha256:f9e18e59a5651609a1503ac17dcfc05856b5bea21e41595828471f02ad56a225",
"sha256:832e177bb5008934e2f5ed723247c04e1dd220d59a90ce32000b7c22bd9d9b54",
"sha256:3bb5258f46d2a511ddca2a4ec8f9091d676a116830a7f336815f02c4b34dbb23",
"sha256:59c516e5b6fafa2e6b63d76492702371ca008ade6e37d931089fe368385041a0",
"sha256:bd2befca2f7ef51f03b757caab549cc040a36143f3b7e3dab94fb308322f2953",
"sha256:3e2ed6847c7a081bd90ab8805efcb39a2933a807627eb7a4016728f881430f5f"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]
[root@192 ~]#
其中的加载过程如下:
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:11936051f93baf5a4fb090a8fa0999309b8173556f7826598e235e8a82127bce",
"sha256:31892cc314cb1993ba1b8eb5f3002c4e9f099a9237af0d03d1893c6fcc559aab",
"sha256:8bf42db0de72f74f4ef0c1d1743f5d54efc3491ee38f4af6d914a6032148b78e",
"sha256:26a504e63be4c63395f216d70b1b8af52263a5289908df8e96a0e7c840813adc",
"sha256:f9e18e59a5651609a1503ac17dcfc05856b5bea21e41595828471f02ad56a225",
"sha256:832e177bb5008934e2f5ed723247c04e1dd220d59a90ce32000b7c22bd9d9b54",
"sha256:3bb5258f46d2a511ddca2a4ec8f9091d676a116830a7f336815f02c4b34dbb23",
"sha256:59c516e5b6fafa2e6b63d76492702371ca008ade6e37d931089fe368385041a0",
"sha256:bd2befca2f7ef51f03b757caab549cc040a36143f3b7e3dab94fb308322f2953",
"sha256:3e2ed6847c7a081bd90ab8805efcb39a2933a807627eb7a4016728f881430f5f"
]
},
这样我们来理解一下几个矛盾点:
为什么tomcat普通安装需要15M,通过docker安装确需要680M
这是我本机的一个tomcat
这是Docker安装的tomcat
这是因为docker镜像原理是分层构建,比如tomcat最上边一层是tomcat。由于tomcat依赖于jdk所以下边一层是jdk,基础镜像是rootfs(Ubuntu、centos),最下边一层是bootfs。由于tomcat镜像构建需要依赖其他文件导致docker安装Tomcat要680MB,可以看下tomcat的镜像内容:
Docker中一个centos镜像为什么只有200M,而一个centos操作系统的iso文件要几个G?
Docker镜像是分层构建的,每一层可以复用,Linux 系统底层(bootfs)基本一致,所以linux系列系统中安装docker镜像会复用宿主机Linux底层的内核,只有rootfs和其他镜像层需要下载,所以比较小。
容器启动过程
Docker镜像都是只读的,当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作容器层,容器层之下的都叫镜像层如果用户想修改一个镜像的话,可以通过可读写的容器构建一个镜像。
这里容器的运行使用了所有驱动都用到的技术——写时复制(CoW)。CoW就是copy-on-write,表示只在需要写时才去复制,这个是针对已有文件的修改场景。比如基于一个image启动多个Container,如果为每个Container都去分配一个image一样的文件系统,那么将会占用大量的磁盘空间。而CoW技术可以让所有的容器共享image的文件系统,所有数据都从image中读取,只有当要对文件进行写操作时,才从image里把要写的文件复制到自己的文件系统进行修改。所以无论有多少个容器共享同一个image,所做的写操作都是对从image中复制到自己的文件系统中的复本上进行,并不会修改image的源文件,且多个容器操作同一个文件,会在每个容器的文件系统里生成一个复本,每个容器修改的都是自己的复本,相互隔离,相互不影响。使用CoW可以有效的提高磁盘的利用率
提交一个镜像
如上所述,当启动容器时我们可以将结构分为两层,read-only被当做镜像层,read-write被当做容器层,如果我们想要基于当前镜像再制作一个新的镜像,实际上就是把read-write提交为一个新的layer层。
使用docker commit 命令提交容器成为一个新的版本
docker commit -m=“提交的描述信息” -a="作者" 容器id 目标镜像名:[TAG]
因为原始下载的tomcat无法启动,所以我们把昨天制作好的tomcattomcat-tml发布为一个新镜像:
[root@192 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
78664208a245 portainer/portainer "/portainer" 24 hours ago Up 2 hours 0.0.0.0:8088->9000/tcp, :::8088->9000/tcp thirsty_gauss
2f59536a92da tomcat "catalina.sh run" 26 hours ago Up 23 seconds 0.0.0.0:3335->8080/tcp, :::3335->8080/tcp tomcat-tml
[root@192 ~]# docker commit -m="add webapps" -a="Ethan" 2f59536a92da mytomcat:1.0
sha256:251e3ac9aff1efe0651c0dfb0e60ce097ab0e56e7b813b31291300988b6e4668
[root@192 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mytomcat 1.0 251e3ac9aff1 5 seconds ago 684MB
nginx latest 605c77e624dd 7 weeks ago 141MB
tomcat latest fb5657adc892 8 weeks ago 680MB
mysql latest 3218b38490ce 2 months ago 516MB
hello-world latest feb5d9fea6a5 4 months ago 13.3kB
centos latest 5d0da3dc9764 5 months ago 231MB
portainer/portainer latest 580c0e4e98b0 11 months ago 79.1MB
elasticsearch 7.6.2 f29a1ee41030 23 months ago 791MB
elasticsearch latest 5acf0e8da90b 3 years ago 486MB
[root@192 ~]#
从面板也能看到镜像制作完成:
总结一下
镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需要的所有内容,包括代码,运行时(一个程序在运行或者在被执行的依赖)、库,环境变量和配置文件。其实也可以理解为一个文件目录。这也就解释了为什么基于镜像的容器可以独立的隔离的运行了。UnionFS的文件系统组织模式、镜像分层、容器写时复制特性也能从原理上说明容器运行时为什么可以只占用不多的内存、启动速度为什么可以做到秒级。