当你拉取 Docker 镜像时,你会注意到它被拉取成不同的层。另外,当你创建自己的 Docker 镜像时,也会创建多个层。在本文中,我们将更好地理解 Docker 层。
1.什么是 Docker 层?
Docker 镜像由几层组成。每层都对应 Dockerfile
中的特定指定。Docker 层创建指令有: RUN
, COPY
, ADD
。其他指令将创建中间层,并且不会影响镜像的大小。
我们看一个例子:创建一个Spring Boot MVC应用程序, 并且在 Maven 构建中创建 Docker 镜像。这些资源可从 GitHub获得。
我们使用的 feature/dockerbenchsecurity
分支,它是 master
分支中更安全的版本。
Dockerfile 如下:
FROM openjdk:10-jdk
VOLUME /tmp
RUN useradd -d /home/appuser -m -s /bin/bash appuser
USER appuser
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"]
复制代码
我们使用mvn clean install
命令构建的应用程序,还将创建 Docker 镜像。为了简洁起见,我们没有列出openjdk:10-jdk
镜像中所有图层的拉取 。
Image will be built as mydeveloperplanet/mykubernetesplanet:0.0.3-SNAPSHOT
Step 1/8 : FROM openjdk:10-jdk
Pulling from library/openjdk
Image 16e82e17faef: Pulling fs layer
...
Image a9448aba0bc3: Pull complete
Digest: sha256:9f17c917630d5e95667840029487b6561b752f1be6a3c4a90c4716907c1aad65
Status: Downloaded newer image for openjdk:10-jdk
---> b11e88dd885d
Step 2/8 : VOLUME /tmp
---> Running in 21329898c3a6
Removing intermediate container 21329898c3a6
---> b6f9ca000de6
Step 3/8 : RUN useradd -d /home/appuser -m -s /bin/bash appuser
---> Running in 82645047e6e7
Removing intermediate container 82645047e6e7
---> 04f6b2716819
Step 4/8 : USER appuser
---> Running in 697b663dadbb
Removing intermediate container 697b663dadbb
---> eaf6b8af5709
Step 5/8 : HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost:8080/actuator/health/ || exit 1
---> Running in f420b9d060c5
Removing intermediate container f420b9d060c5
---> 77f95436a3ff
Step 6/8 : ARG JAR_FILE
---> Running in 60b9d25ad2ac
Removing intermediate container 60b9d25ad2ac
---> 135fa7df95ac
Step 7/8 : COPY ${JAR_FILE} app.jar
---> 63c18567012b
Step 8/8 : ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
---> Running in 79203446934a
Removing intermediate container 79203446934a
---> 8e2b049f9783
Successfully built 8e2b049f9783
Successfully tagged mydeveloperplanet/mykubernetesplanet:0.0.3-SNAPSHOT
复制代码
镜像构建发生了什么?我们注意到创建了多个 docker 层,并且大多数层都被删除(删除中间容器)。那么,为什么说删除中间容器而不是删除中间层呢?这是因为构建步骤是在中间容器中执行的。完成构建步骤后,旧可以删除中间容器。除此之外,层是只读的。一层包含前一层和当前层之间的差异。在这些层的顶层,有一个可写层(当前层),被称为容器层。如前所述,只有特定的指令会创建一个新层。
我们来看一下我们的 Docker 镜像:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mykubernetesplanet 0.0.3-SNAPSHOT 8e2b049f9783 About a minute ago 1GB
openjdk
复制代码
看看我们的 mykubernetesplanet 镜像的构建历史记录 :
$ docker history 8e2b049f9783
IMAGE CREATED CREATED BY SIZE COMMENT
8e2b049f9783 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 appuser 0B
04f6b2716819 2 minutes ago /bin/sh -c useradd -d /home/appuser -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。Dockerfile
中只有 RUN
and COPY
指令会影响 Docker 镜像的大小。openjdk:10-jdk
镜像的各层 也被列举出,显示为 missing ,这意味着这些层可以建立在不同的系统上,并且在本地不可用。
2.重新创建 Docker 镜像
如果在不对源代码进行任何更改的情况下,再次运行 Maven 构建,会发生什么情况?
Image will be built as mydeveloperplanet/mykubernetesplanet:0.0.3-SNAPSHOT
Step 1/8 : FROM openjdk:10-jdk
Pulling from library/openjdk
Digest: 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 mydeveloperplanet/mykubernetesplanet:0.0.3-SNAPSHOT
复制代码
从上面我们注意到,镜像第一层与我们先前的版本相同--层 ID 相同。在日志中,我们注意到 docker 层是从缓存中提取的。
在步骤 7 中,docker 使用新 ID 创建了一个新层。因为我们确实创建了一个新的 JAR 文件,Docker 也识别它是一个新文件,因此创建了一个新层。
在步骤 8 中,还创建了一个新层,因为它是建立在新层之上的。
让我们再次列出 Docker 镜像:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mykubernetesplanet 0.0.3-SNAPSHOT e5d7b72aad05 13 seconds ago 1GB
<none> <none> 8e2b049f9783 5 minutes ago 1GB
openjdk 10-jdk b11e88dd885d 2 months ag
复制代码
0.0.3-SNAPSHOT
中可以看到上次 docker 构建的镜像 ID。而且,上个版本镜像 ID 和标签都已被删除,并以 none 关键字表示 。这称为 虚悬镜像(dangling image) 。我们将在本文结尾处对虚悬镜像(dangling image) 进行更详细的说明。
当我们查看新创建的镜像的历史时,我们注意到两个顶层是新的,就像 docker 构建日志中一样:
$ docker history e5d7b72aad05
IMAGE CREATED CREATED BY SIZE COMMENT
e5d7b72aad05 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 文件。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mykubernetesplanet 0.0.3-SNAPSHOT eced642d4f5c 30 seconds ago 1GB
<none> <none> e5d7b72aad05 3 minutes ago 1GB
<none> <none> 8e2b049f9783 8 minutes ago 1GB
openjdk 10-jdk b11e88dd885d 2 months ago 987MB
$ 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
...
复制代码
3.镜像有多大?
通过docker image ls
命令的输出 ,我们注意到两个**虚悬镜像(dangling image)**,大小为 1 GB。这对存储有什么影响?
首先,我们需要知道镜像数据的存储位置。使用以下命令可以检索存储位置:
$ docker 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
目录的大小,来了解它占用的存储空间:
$ du -sh -m overlay2
1059 overlay2
复制代码
openjdk:10-jdk
镜像大小是 987 MB,JAR 文件为 17.4 MB,总大小应约为 987 MB + 3 * 17.4 MB(两个虚悬镜像(dangling image) 和一个真实的镜像)。这大约是 1,040 MB。
可以看出,我们不能简单地添加所有 Docker 镜像的大小来确认实际存储大小。
其中的差异是由于存在中间镜像。
这些可以显示如下:
$ docker images -a
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mykubernetesplanet 0.0.3-SNAPSHOT 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
复制代码
4. 如何去掉虚悬镜像(dangling image)
虚悬镜像(dangling image) ,我们不需要它们,它们还占用存储空间。我们如何去掉呢?
首先,列出悬空的镜像:
$ docker images -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> e5d7b72aad05 7 days ago 1GB
<none> <none> 8e2b049f9783 7 days ago 1GB
复制代码
我们可以使用以下 docker rmi
命令删除镜像:
$ docker rmi e5d7b72aad05
Deleted: sha256:e5d7b72aad054100d142d99467c218062a2ef3bc2a0994fb589f9fc7ff004afe
Deleted: sha256:409f2fee0cde9b5144f8e92887b61e49f3ccbd2b0e601f536941d3b9be32ff47
Deleted: sha256:2162a2af22ee26f7ac9bd95c39818312dc9714b8fbfbeb892ff827be15c7795b
复制代码
或者,你可以使用 docker image prune
命令来执行此操作。
现在已经删除了虚悬镜像(dangling image),让我们看一下overlay2
目录的大小 :
$ du -sh -m overlay2
1026 overlay2
复制代码
我们节省了 33 MB。
似乎还不算什么,但是当你经常构建 Docker 镜像时,随着时间的流逝,它会显著增长。
5.总结
在本文中,我们尝试更好地理解 Docker 层。我们注意到构建 Docker 镜像时会创建中间层,如果我们不定期清理中间层,那么虚悬镜像(dangling image)会一直保留在我们的系统中。我们还查看了系统上 Docker 镜像的大小。
译文链接: https://dzone.com/articles/docker-layers-explained
评论