镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时库、环境变量和配置文件。
举个例子:
你开发了一款软件,假设叫做app,这个app运行起来需要jdk,tomcat等。那么镜像会将软件运行环境(jdk,tomcat)和基于运行环境开发的软件(app)同时打包。
在聊Docker镜像加载原理之前,我们首先要了解什么是UnionFS。什么是UnionFS呢?其实就是联合文件系统。
UnionFS(联合文件系统):Union文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录
有多少人是这个表情?别着急,这里你只要知道有这么个文件系统,而且在Docker中使用到了就行,继续往下看。
docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统通过UnionFS构成了一个完整的镜像。
bootfs(boot file system)主要包含bootloader和kernel,bootloader主要是引导加载kernel,Linux刚启动时会加载bootfs文件系统,在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。
rootfs (root file system) ,在bootfs之上。包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。rootfs就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。
啥意思呢?其实就是我们看到的镜像只包含了rootfs的部分,bootfs的部分只是作为底层的引导作用,在镜像生成完会被卸载,并不存在于镜像中。这也就是为啥我们使用Docker制作的Centos镜像只要几百兆,而使用虚拟机安装的Centos需要好几个G。
对于一个精简的OS,rootfs 可以很小,只需要包含最基本的命令,工具和程序库就可以了,因为底层直接用Host的kernel,自己只需要提供rootfs就可以了。由此可见对于不同的linux发行版,bootfs基本是一致的,rootfs会有差别,因此不同的发行版可以公用bootfs。
我们这里下载一个tomcat镜像来看一下。
[root@jiangnan ~]# docker pull tomcat
Using default tag: latest
latest: Pulling from library/tomcat
0e29546d541c: Pull complete
9b829c73b52b: Pull complete
cb5b7ae36172: Pull complete
6494e4811622: Pull complete
668f6fcc5fa5: Pull complete
dc120c3e0290: Pull complete
8f7c0eebb7b1: Pull complete
77b694f83996: Pull complete
0f611256ec3a: Pull complete
4f25def12f23: Pull complete
Digest: sha256:9dee185c3b161cdfede1f5e35e8b56ebc9de88ed3a79526939701f3537a52324
Status: Downloaded newer image for tomcat:latest
docker.io/library/tomcat:latest
[root@jiangnan ~]#
我们发现,对于我们来说,tomcat不是完整的吗,但是当我们下载镜像的时候它是一层一层下载的。
刚才我们使用了docker pull tomcat
默认下载的就是最新版本,我们现在再来下载一个指定版本,比如8.5
[root@jiangnan ~]# docker pull tomcat:8.5
8.5: 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
b7c26350ecc2: Pull complete
7365b3b02e1b: Pull complete
Digest: sha256:421c2a2c73f3e339c787beaacde0f7bbc30bba957ec653d41a77d08144c6a028
Status: Downloaded newer image for tomcat:8.5
docker.io/library/tomcat:8.5
[root@jiangnan ~]#
这里我们发现也是使用了分层下载,但是第一次下载,每一层都是
Pull complete
,而第二次下载有的层却是Already exists
,Already exists就是说这一层已经有了,不需要下载了,我只下载了没有的层。
思考:为什么Docker镜像要采用这种分层的结构呢?
从两次下载tomcat我们可以看到,对于已经存在的层就没有必要再次下载了,而是实现了资源的共享。我们可以引申为所有的镜像都是如此,多个镜像都从相同的Base镜像构建而来,那么宿主机只需在磁盘上保留一份base镜像,同时内存中也只需要加载一份base镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享。
当然,我们通过docker inspect 可以看得更加清晰,主要看layer(层)部分
[root@jiangnan ~]# docker image inspect tomcat:latest
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:11936051f93baf5a4fb090a8fa0999309b8173556f7826598e235e8a82127bce",
"sha256:31892cc314cb1993ba1b8eb5f3002c4e9f099a9237af0d03d1893c6fcc559aab",
"sha256:8bf42db0de72f74f4ef0c1d1743f5d54efc3491ee38f4af6d914a6032148b78e",
"sha256:26a504e63be4c63395f216d70b1b8af52263a5289908df8e96a0e7c840813adc",
"sha256:f9e18e59a5651609a1503ac17dcfc05856b5bea21e41595828471f02ad56a225",
"sha256:832e177bb5008934e2f5ed723247c04e1dd220d59a90ce32000b7c22bd9d9b54",
"sha256:3bb5258f46d2a511ddca2a4ec8f9091d676a116830a7f336815f02c4b34dbb23",
"sha256:59c516e5b6fafa2e6b63d76492702371ca008ade6e37d931089fe368385041a0",
"sha256:bd2befca2f7ef51f03b757caab549cc040a36143f3b7e3dab94fb308322f2953",
"sha256:3e2ed6847c7a081bd90ab8805efcb39a2933a807627eb7a4016728f881430f5f"
]
}
[root@jiangnan ~]# docker image inspect tomcat:8.5
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:11936051f93baf5a4fb090a8fa0999309b8173556f7826598e235e8a82127bce",
"sha256:31892cc314cb1993ba1b8eb5f3002c4e9f099a9237af0d03d1893c6fcc559aab",
"sha256:8bf42db0de72f74f4ef0c1d1743f5d54efc3491ee38f4af6d914a6032148b78e",
"sha256:26a504e63be4c63395f216d70b1b8af52263a5289908df8e96a0e7c840813adc",
"sha256:f9e18e59a5651609a1503ac17dcfc05856b5bea21e41595828471f02ad56a225",
"sha256:832e177bb5008934e2f5ed723247c04e1dd220d59a90ce32000b7c22bd9d9b54",
"sha256:3bb5258f46d2a511ddca2a4ec8f9091d676a116830a7f336815f02c4b34dbb23",
"sha256:59c516e5b6fafa2e6b63d76492702371ca008ade6e37d931089fe368385041a0",
"sha256:af88ee51802bd5af6bdd8d04fba5cc1766d66fa35daa759d834145e3e205a285",
"sha256:534054ff942e53faa42e4df2a49e80cc8cb3473d90657446624b10598c8da8c1"
]
}
发现他们的layer(层)确实有些是相同的。
所有的 Docker 镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上,创建新的镜像层。
举一个简单的例子,假如基于 Ubuntu Linux 16.04 创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加 Python包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。
该镜像当前已经包含 3 个镜像层,如下图所示。
在添加额外的镜像层的同时,镜像始终保持是当前所有镜像的组合,理解这一点非常重要。下图中举了一个简单的例子,每个镜像层包含 3 个文件,而镜像包含了来自两个镜像层的 6 个文件。
上图中的镜像层跟之前图中的略有区别,主要目的是便于展示文件。
下图中展示了一个稍微复杂的三层镜像,在外部看来整个镜像只有 6 个文件,这是因为最上层中的文件
7 是文件 5 的一个更新版本。
这种情况下,上层镜像层中的文件覆盖了底层镜像层中的文件。这样就使得文件的更新版本作为一个新镜像层添加到镜像当中。
下图展示了与系统显示相同的三层镜像。所有镜像层堆叠并合并,对外提供统一的视图。
说白了就是:镜像是分层的,从基础镜像开始,一层一层叠加起来的。上一层始终是以下面的层为基础,下面的层相对于上面的层就是一个完整的镜像,我们所有的操作都是在最上面的一层进行。在外面看来你是一个镜像,但是在内是多个层(镜像)叠加起来的。比如我的一个镜像有3层,制作的时候从第1层开始有了第2层,这两层合起来供第三层使用,那对于第3层来说,第1、2层合起来是不是就是一个完整的镜像呢?但是这3层合起来相对于第4层(假如有)就又是一个完整的镜像。以此类推,都是一样的。
又点绕,后面我们在制作镜像的时候在给大家细说一下。
这样可以得到镜像的一个特点:
Docker镜像都是只读的,当容器启动时,一个新的可写层被加载到镜像的顶部!
这一层就是我们通常说的容器层,容器之下的都叫镜像层!
容器和镜像的关系,我在前面的博客中有讲到,可以去看看。
docker commit 从容器创建一个新的镜像。
我们知道了,运行态的镜像叫做容器,我们可以在里面做一些修改,添加,删除等操作。做完操作之后,我觉得这是一个很好的模板,我下次可以直接拿来使用,怎么做呢,使用docker commit
将当前容器创建一个镜像出来,下次要使用的时候直接使用即可。
举例:
前面我们下载了一个tomcat镜像,但是当我运行起来之后,页面显示404,这不是不能访问,而是没有资源,什么意思呢?默认的tomcat中webapps中是空的。
[root@jiangnan ~]# docker run -it -p 8081:8080 tomcat:latest
Using CATALINA_BASE: /usr/local/tomcat
Using CATALINA_HOME: /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME: /usr/local/openjdk-11
Using CLASSPATH: /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
Using CATALINA_OPTS:
NOTE: Picked up JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
21-Feb-2022 15:11:00.005 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.0.14
21-Feb-2022 15:11:00.029 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Dec 2 2021 22:01:36 UTC
21-Feb-2022 15:11:00.029 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.0.14.0
21-Feb-2022 15:11:00.029 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
21-Feb-2022 15:11:00.029 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 3.10.0-1127.19.1.el7.x86_64
21-Feb-2022 15:11:00.029 INFO [main]
# webapps下是空的,肯定包404
[root@jiangnan ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
677a42e9efab tomcat:latest "catalina.sh run" 21 seconds ago Up 21 seconds 0.0.0.0:8081->8080/tcp, :::8081->8080/tcp strange_ptolemy
[root@jiangnan ~]# docker exec -it 677a42e9efab /bin/bash
root@677a42e9efab:/usr/local/tomcat# ls
BUILDING.txt LICENSE README.md RUNNING.txt conf logs temp webapps.dist
CONTRIBUTING.md NOTICE RELEASE-NOTES bin lib native-jni-lib webapps work
root@677a42e9efab:/usr/local/tomcat# cd webapps
root@677a42e9efab:/usr/local/tomcat/webapps# ls
root@677a42e9efab:/usr/local/tomcat/webapps#
为了避免再报404,我们将webapps.dist下的文件复制到webapps下,再次访问。
这不就完美了,为了避免404的尴尬,我想把我现在这个tomcat提交成一个新的镜像,下次直接使用不就好了,于是我用了docker commit
语法:
docker commit -a="作者名" -m="备注信息" 容器id 新的镜像名:版本
这里注意镜像的名字不能有大写,否则报错:invalid reference format
# 先试用ctrl + p + q退出,其实后台还在运行
docker commit -a="jiangnan" -m="myTomcat" 677a42e9efab tomcat02:1.1
[root@jiangnan ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
677a42e9efab tomcat:latest "catalina.sh run" 5 minutes ago Up 5 minutes 0.0.0.0:8081->8080/tcp, :::8081->8080/tcp strange_ptolemy
[root@jiangnan ~]# docker commit -a="jiangnan" -m="myTomcat" 677a42e9efab tomcat02:1.1
sha256:b4249b680e8da80841a52bcc8360211cf38652aaa20d5d64dd5bcfec581d60c9
[root@jiangnan ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
tomcat02 1.1 b4249b680e8d 9 seconds ago 684MB
tomcat 8.5 2d2bccf89f53 2 months ago 678MB
tomcat latest fb5657adc892 2 months ago 680MB
centos latest 5d0da3dc9764 5 months ago 231MB
[root@jiangnan ~]#
发现这里多了一个tomcat02,话不多说,直接启动看效果。
[root@jiangnan ~]# docker run -it -p 8082:8080 tomcat02:1.1
Using CATALINA_BASE: /usr/local/tomcat
Using CATALINA_HOME: /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME: /usr/local/openjdk-11
Using CLASSPATH: /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
Using CATALINA_OPTS:
NOTE: Picked up JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
21-Feb-2022 15:20:45.874 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.0.14
21-Feb-2022 15:20:45.881 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Dec 2 2021 22:01:36 UTC
21-Feb-2022 15:20:45.882 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.0.14.0
21-Feb-2022 15:20:45.882 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
21-Feb-2022 15:20:45.882 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 3.10.0-1127.19.1.el7.x86_64
21-Feb-2022 15:20:45.882 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
21-Feb-2022 15:20:45.882 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /usr/local/openjdk-11
21-Feb-2022 15:20:45.882 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 11.0.13+8
404没有了,很完美。