当我们拉取Docker Image时,如果仔细观察的话,你就会发现:它被拉成不同的层。另外,当然,我们创建自己的Docker Image时,也会创建多个层。在本文中,我们将尝试更好地去探究Docker层次的秘密。
Docker Image由多层组成。每层都对应于Dockerfile中的某些指令。以下说明创建了一个图层:RUN、COPY、ADD。 让我们来看一个例子,我们将使用预先创建的Spring Boot MVC应用程序,并在Maven构建中创建Docker镜像。Dockerfile文件如下:
[administrator@JavaLangOutOfMemory luga ]% vi Dockerfile
FROM openjdk:10-jdk
VOLUME /tmp
RUN useradd -d /home/luga -m -s /bin/bash luga
USER luga
HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost:8080/actuator/health/ || exit 1
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
使用docker build 进行构建Docker镜像,当然,也可以使用mvn clean install命令行进行操作。如下图所示:(因涉及项目中信息,故对部分数据进行参数化处理)
[administrator@JavaLangOutOfMemory luga ]% docker build -f Dockerfile -t luga-demo-springboot .
[+] Building 2718.8s (9/9) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 358B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/openjdk:10-jdk 13.2s
=> [auth] library/openjdk:pull token for registry-1.docker.io 0.0s
=> [1/3] FROM docker.io/library/openjdk:10-jdk@sha256:9f17c917630d5e95667840029487b6561b752f1be6a3c4a90c4716907c1aad65 2703.4s
=> => resolve docker.io/library/openjdk:10-jdk@sha256:9f17c917630d5e95667840029487b6561b752f1be6a3c4a90c4716907c1aad65 0.0s
=> => sha256:9f17c917630d5e95667840029487b6561b752f1be6a3c4a90c4716907c1aad65 2.37kB / 2.37kB 0.0s
=> => sha256:923d074ef1f4f0dceef68d9bad8be19c918d9ca8180a26b037e00576f24c2cb4 2.00kB / 2.00kB 0.0s
=> => sha256:b11e88dd885d8b2621d448f3d2099068d181c5c778c2ab0cf0f61b573fa429b7 4.99kB / 4.99kB 0.0s
=> => sha256:16e82e17faef9e90ceefcd8175e9899edce768aa6008cc16dd1e3fe7d3b88bb8 49.26MB / 49.26MB 39.8s
=> => sha256:117dc02416a34c62e28a030f27828f2f31af6b8b1f02c85b009a1ffb390d01dc 7.38MB / 7.38MB 21.3s
=> => sha256:7e4c717259ac9c550efbbf41c6fe0dc9598046f4bfd4b398deb63f7a0c19cb3f 9.78MB / 9.78MB 51.9s
=> => sha256:7a518b8f48be0323544739e175909b24be833b4a2bf39939f91fbcc2ab0e48a4 50.64MB / 50.64MB 42.5s
=> => sha256:add32d44f708eff65a3ad250f4c87b8c3c912f0240c8a0acd76a88df6cd3ebdc 883.70kB / 883.70kB 44.2s
=> => sha256:a0158fa0854313538e2807a9098aa49227ea46a44c3fc9120e9dc74e45baa408 237B / 237B 43.4s
=> => extracting sha256:16e82e17faef9e90ceefcd8175e9899edce768aa6008cc16dd1e3fe7d3b88bb8 12.9s
=> => sha256:9eb8cb7aab2603e0ca5015d7f512794de6fdf25375be1f135a3b890c5a17a298 131B / 131B 44.2s
=> => sha256:a9448aba0bc3432cb23b86d5c0fe808e4f0c8b7fdbe46fa26cdada8bcd582aa9 378.28MB / 378.28MB 2654.1s
=> => extracting sha256:117dc02416a34c62e28a030f27828f2f31af6b8b1f02c85b009a1ffb390d01dc 2.1s
=> => extracting sha256:7e4c717259ac9c550efbbf41c6fe0dc9598046f4bfd4b398deb63f7a0c19cb3f 3.3s
=> => extracting sha256:7a518b8f48be0323544739e175909b24be833b4a2bf39939f91fbcc2ab0e48a4 14.2s
=> => extracting sha256:add32d44f708eff65a3ad250f4c87b8c3c912f0240c8a0acd76a88df6cd3ebdc 0.4s
=> => extracting sha256:a0158fa0854313538e2807a9098aa49227ea46a44c3fc9120e9dc74e45baa408 0.0s
=> => extracting sha256:9eb8cb7aab2603e0ca5015d7f512794de6fdf25375be1f135a3b890c5a17a298 0.0s
=> => extracting sha256:a9448aba0bc3432cb23b86d5c0fe808e4f0c8b7fdbe46fa26cdada8bcd582aa9 47.3s
=> [internal] load build context 0.3s
=> => transferring context: 1.36MB 0.2s
=> [2/3] RUN useradd -d /home/luga -m -s /bin/bash luga 1.6s
=> [3/3] COPY app.jar 0.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:6bc6d7e6e0b2deaca0451ebd5d78fcbd43036ddf917dc17e29a741565d9070ff 0.0s
=> => naming to docker.io/library/luga-demo-springboot 0.0s
这里会发生什么?我们注意到已经创建了图层,并且大多数图层都被删除(删除中间容器)。那么,为什么说删除中间容器而不删除中间层呢?那是因为构建步骤是在中间容器中执行的。完成构建步骤后,可以删除中间容器。除此之外,层是只读的。一层包含前一层和当前层之间的差异。在这些层的顶层,有一个可写层(当前层),称为容器层。如前所述,只有特定的指令才能创建新层。让我们看一下我们的Docker镜像:
[administrator@JavaLangOutOfMemory luga ] % docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
luga-demo-springboot latest 6bc6d7e6e0b2 21 hours ago 989MB
nacos/nacos-server latest 9c0b55a5ab2c 34 hours ago 935MB
grafana/grafana latest 13afb861111c 2 days ago 187MB
mysql 5.7 cc8775c0fe94 4 days ago 449MB
prom/prometheus latest 53fd5ed1cd48 10 days ago 173MB
openzipkin/zipkin latest 9b4acc3eb019 3 weeks ago 150MB
redis latest ef47f3b6dc11 5 weeks ago 104MB
tomee latest 9e19954eefcc 5 weeks ago 336MB
... ...
[administrator@JavaLangOutOfMemory luga ] % docker histoty 6bc6d7e6e0b2
IMAGE CREATED CREATED BY SIZE COMMENT
6bc6d7e6e0b2 About a minute ago /bin/sh -c #(nop) ENTRYPOINT ["java" "-Djav… 0B
63c18567012b About a minute ago /bin/sh -c #(nop) COPY file:2a5b71774c60e0f6… 17.4MB
135fa7df95ac About a minute ago /bin/sh -c #(nop) ARG JAR_FILE 0B
77f95436a3ff 2 minutes ago /bin/sh -c #(nop) HEALTHCHECK &{["CMD-SHELL… 0B
eaf6b8af5709 2 minutes ago /bin/sh -c #(nop) USER luga 0B
04f6b2716819 2 minutes ago /bin/sh -c useradd -d /home/luga -m -s /b… 399kB
b6f9ca000de6 2 minutes ago /bin/sh -c #(nop) VOLUME [/tmp] 0B
b11e88dd885d 2 months ago /bin/sh -c #(nop) CMD ["jshell"] 0B
<missing> 2 months ago /bin/sh -c set -ex; if [ ! -d /usr/share/m… 697MB
<missing> 2 months ago /bin/sh -c #(nop) ENV JAVA_DEBIAN_VERSION=1… 0B
...
从上面的命令行,我们注意到,中间容器的大小确实为0B,与预期的一样。Dockerfile中只有RUN和COPY命令会影响Docker镜像的大小。openjdk:10-jdkimage的层也被列出,并由丢失的关键字识别。这仅意味着这些层建立在不同的系统上,并且在本地不可用。
如果我们在不对源代码进行任何更改的情况下再次运行Maven构建,会发生什么情况?
Image will be built as luga-demo-springboot:latest
Step 1/8 : FROM openjdk:10-jdk
Pulling from library/openjdk
t: sha256:9f17c917630d5e95667840029487b6561b752f1be6a3c4a90c4716907c1aad65
Status: Image is up to date for openjdk:10-jdk
---> b11e88dd885d
Step 2/8 : VOLUME /tmp
---> Using cache
---> b6f9ca000de6
... ...
Step 6/8 : ARG JAR_FILE
---> Using cache
---> 135fa7df95ac
Step 7/8 : COPY ${JAR_FILE} app.jar
---> 409f2fee0cde
Step 8/8 : ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
---> Running in 75f07955bbc8
Removing intermediate container 75f07955bbc8
---> e5d7b72aad05
Successfully built e5d7b72aad05
Successfully tagged luga-demo-springboot:latest
我们注意到第一层与我们先前的版本相同。图层ID相同。在日志中,我们注意到图层是从缓存中提取的。在步骤7中,使用新ID创建新层。我们确实创建了一个新的JAR文件,Docker将其解释为一个新文件,因此创建了一个新层。在步骤8中,还创建了一个新层,因为它是建立在新层之上的。
让我们再次列出Docker镜像:
[administrator@JavaLangOutOfMemory luga ] %docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
luga-demo-springboot latest d7u9b72aad85 13 seconds ago 1GB
<none> <none> 8e2b049f9783 5 minutes ago 1GB
openjdk 10-jdk b11e88dd885d 2 months ago 987MB
我们的标签latest已收到我们上一个版本的图片ID。我们的旧图片ID的存储库和标签已删除,使用none关键字表示。这称为悬空图像。我们将在本文结尾处对此进行更详细的说明。
当我们查看新创建的镜像构建历史时,我们注意到两个顶层是新的,就像构建日志中一样:
[administrator@JavaLangOutOfMemory luga ] %docker history d7u9b72aad85
IMAGE CREATED CREATED BY SIZE COMMENT
d7u9b72aad85 38 seconds ago /bin/sh -c #(nop) ENTRYPOINT ["java" "-Djav… 0B
409f2fee0cde 42 seconds ago /bin/sh -c #(nop) COPY file:4b04c6500d340c9e… 17.4MB
135fa7df95ac 6 minutes ago /bin/sh -c #(nop) ARG JAR_FILE 0B
...
当我们更改源代码时,结果是相同的,因为在这种情况下,还会生成一个新的JAR文件。
[administrator@JavaLangOutOfMemory luga ] %docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
luga-demo-springboot latest eced642d4f5c 30 seconds ago 1GB
<none> <none> d7u9b72aad85 3 minutes ago 1GB
<none> <none> 8e2b049f9783 8 minutes ago 1GB
openjdk 10-jdk b11e88dd885d 2 months ago 987MB
[administrator@JavaLangOutOfMemory luga ] %docker history eced642d4f5c
IMAGE CREATED CREATED BY SIZE COMMENT
eced642d4f5c About a minute ago /bin/sh -c #(nop) ENTRYPOINT ["java" "-Djav… 0B
44a9097b8bad About a minute ago /bin/sh -c #(nop) COPY file:1d5276778b53310e… 17.4MB
135fa7df95ac 9 minutes ago /bin/sh -c #(nop) ARG JAR_FILE 0B
...
现在大小如何?
让我们仔细看看docker image ls命令的最新输出。我们注意到两个悬挂的图像,大小为1 GB。但这对存储真正意味着什么?首先,我们需要知道图像数据的存储位置。使用以下命令以检索存储位置:
[administrator@JavaLangOutOfMemory luga ] %ocker image inspect eced642d4f5c
...
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/655be8bea8e54c31ebb7e3adf05db227d194a49c1e2f95552d593d623e024b92/diff:/var/lib/docker/overlay2/993f77b91a487e19b3696836efee23c8a17791d71096d348c54c38fba3dc8478/diff:/var/lib/docker/overlay2/d62d6ca8ce1960d057e11d163d458563628e5a337de06455e714900f72005589/diff:/var/lib/docker/overlay2/cabdf4de81557a8047e3670bd2eecb5449de7de8fe9dfd4ad0c81d7dd2c61e9d/diff:/var/lib/docker/overlay2/062bf99d6a563ee2ef7824ec02ff5cd09fb8721cb23f6a55f8927edc2607f9c1/diff:/var/lib/docker/overlay2/ba024c24b20771dbf409f501423273e13225cf675f30896720cadace1c7be000/diff:/var/lib/docker/overlay2/d15f4477b53508127bebd1224c9ea09cd767f7db7429ffb1e8aa79b01ab77506/diff:/var/lib/docker/overlay2/ea434348d6625bc49875d0aba886b24ff0e1e204a350099981dcfc4029bc688d/diff:/var/lib/docker/overlay2/05e003c0522c7049110aa3ce09814ff2167da1e53ec83481fef03324011ce6e6/diff",
"MergedDir": "/var/lib/docker/overlay2/205b55ee2f0e06394b6d17067338845410609887ccd18f53bf0646ff60452ffb/merged",
"UpperDir": "/var/lib/docker/overlay2/205b55ee2f0e06394b6d17067338845410609887ccd18f53bf0646ff60452ffb/diff",
"WorkDir": "/var/lib/docker/overlay2/205b55ee2f0e06394b6d17067338845410609887ccd18f53bf0646ff60452ffb/work"
},
"Name": "overlay2"
},
...
我们的Docker映像存储在/ var / lib / docker / overlay2中。我们可以简单地检索overlay2目录的大小,以了解分配的存储空间:
m
1059 overlay2
openjdk:10-jdk镜像为987 MB。我们镜像中的JAR文件为17.4 MB。总大小应约为987 MB + 3 * 17.4 MB(两个悬挂的镜像和一个真实的镜像)。这大约是1,040 MB。我们可以得出结论,Docker镜像有某种智能存储,我们不能简单地添加所有Docker镜像的大小来检索实际存储大小。差异是由于存在中间图像。这些可以显示如下:
[administrator@JavaLangOutOfMemory luga ] %docker images -a
REPOSITORY TAG IMAGE ID CREATED SIZE
luga-demo-springboot latest eced642d4f5c 7 days ago 1GB
<none> <none> 44a9097b8bad 7 days ago 1GB
<none> <none> e5d7b72aad05 7 days ago 1GB
<none> <none> 409f2fee0cde 7 days ago 1GB
<none> <none> 8e2b049f9783 7 days ago 1GB
<none> <none> 63c18567012b 7 days ago 1GB
<none> <none> 135fa7df95ac 7 days ago 987MB
<none> <none> 77f95436a3ff 7 days ago 987MB
<none> <none> eaf6b8af5709 7 days ago 987MB
<none> <none> 04f6b2716819 7 days ago 987MB
<none> <none> b6f9ca000de6 7 days ago 987MB
openjdk 10-jdk b11e88dd885d 2 months ago 987MB
我们如何优化这些悬而未决的镜像呢? 我们不再需要它们,它们仅分配存储空间。我们可以使用docker rmi命令删除经常不用的镜像,或者,可以使用docker image prune命令执行此操作,以将其释放。
在本文中,我们试图更好地理解Docker层。我们注意到,如果我们不定期清洁中间层,则会继续创建中间层,并且悬挂的镜像仍然保留在我们的系统中。