docker

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > 云和虚拟化 > docker > Docker镜像层

一文打你掌握Docker镜像层(Layer)原理与优化

作者:Genlt

Docker镜像是多层只读文件的堆叠,每一层记录文件系统的变更差异,本文主要为大家详细介绍了Docker镜像层的核心原理与优化,感兴趣的小伙伴可以了解下

一句话总结:Docker 镜像是多层只读文件的堆叠,每一层记录文件系统的变更差异;利用层的共享与缓存机制,可以加速构建、节省存储和网络带宽,尤其对 Java 这类依赖复杂、构建缓慢的应用收益巨大。

一、为什么需要理解“层”?—— 痛点场景

你在使用 Docker 时是否遇到过这些问题?

这些问题的根源都在于 你没有理解 Docker 镜像的层(Layer)。一旦你搞懂层的原理,就能像搭积木一样优化镜像的构建、分发和运行。

二、核心原理:层是什么?怎么工作?

2.1 层的本质

Docker 镜像由一系列只读层叠加而成,每一层都记录了与上一层相比,文件系统的差异(新增、修改、删除的文件)。你可以把镜像想象成一本变更日志,而不是完整文件的压缩包。

FROM ubuntu:22.04          # 第1层:基础文件系统
RUN apt-get update         # 第2层:更新了软件源列表(新增/修改了一些文件)
RUN apt-get install -y jdk # 第3层:安装了 JDK(新增大量二进制文件)
COPY app.jar /app/         # 第4层:添加了你自己的 jar 包

当你在容器中看到一个完整文件系统时,其实是 Union FS(联合文件系统)把这些只读层合并成一个统一视图。如果多个层里有相同路径的文件,上层会“遮盖”下层。

2.2 层的三大作用

作用说明
构建缓存某层未变化,Docker 直接复用该层及之前层的缓存,跳过后续未变化的指令。
存储共享不同镜像可以共用相同的底层(例如两个 Java 镜像共用同一个 openjdk:17-jre-slim 基础层),磁盘上只存一份。
并行分发拉取或推送镜像时,可以同时下载/上传多个层(层之间物理独立),加速传输。

疑问:上层依赖下层,为什么可以同时下载多个层?

:层在存储上是独立的压缩包(tar 文件),依赖关系只体现在 manifest 元数据中。下载器可以并行获取所有层,全部下载完成后,再按顺序解压组装。就像你可以同时从超市货架上拿饼干、奶油和巧克力,回来再按顺序叠成夹心饼干。

2.3 Manifest —— 镜像的“配料表”

manifest 是一个 JSON 文件,记录了镜像的层列表、大小、哈希值以及运行时配置(CMD、环境变量等)。它的作用是让 Docker 知道:

推送镜像时,Docker 会上传 manifest 以及仓库中缺失的层;拉取镜像时,先获取 manifest,再根据里面的层 digest 决定哪些层需要下载。

三、最小可用示例:Java 应用的层优化

3.1 糟糕的做法(层缓存完全失效)

FROM openjdk:17-jre-slim
WORKDIR /app
COPY . .                # 只要任何文件改动,整个缓存失效
RUN ./mvnw package
ENTRYPOINT ["java", "-jar", "target/*.jar"]

问题:复制整个项目目录(包含源码、pom.xml、.mvn 等)到镜像中。一旦你修改了任意一个 .java 文件,COPY . . 这一层的哈希就会改变,导致后面 RUN mvn package 也必须重新执行 —— 每次都重新下载依赖、重新编译,耗时巨大。

3.2 正确做法:分层缓存 + 多阶段构建

# 阶段1:构建(使用完整 JDK + Maven)
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /build

# 先复制 pom.xml,单独一层用于下载依赖(依赖变化频率低)
COPY pom.xml .
RUN mvn dependency:go-offline   # 预下载依赖到本地仓库

# 再复制源码,单独一层用于编译(源码变化频繁)
COPY src ./src
RUN mvn package -DskipTests

# 阶段2:运行(仅使用 JRE)
FROM openjdk:17-jre-slim
WORKDIR /app
# 从 builder 阶段复制编译好的 jar 包
COPY --from=builder /build/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

效果

3.3 如何验证镜像的层?

# 查看镜像的层历史
docker history your-image:tag

# 输出示例:
# IMAGE          CREATED        CREATED BY                         SIZE
# a1b2c3d4       2 min ago      COPY target/*.jar app.jar         20MB
# e5f6g7h8       3 min ago      RUN /bin/sh -c mvn package ...    150MB
# i9j0k1l2       5 min ago      COPY pom.xml .                    5KB
# ...

每一行 CREATED BY 对应 Dockerfile 中的一条指令(合并后的层)。

四、关键注意事项和常见坑

4.1 合并相关操作,避免“删除幽灵”

在层中删除文件并不会真正释放空间,因为下层被删除的文件依然存在于只读层中,只是被上层“遮盖”了。

错误示例

RUN wget http://large-file.zip
RUN unzip large-file.zip
RUN rm large-file.zip        # 这一层删除了,但前一层的 large-file.zip 依然存在!

正确做法:在同一层内完成下载、解压、删除:

RUN wget http://large-file.zip && \
    unzip large-file.zip && \
    rm large-file.zip

4.2 注意指令顺序 —— 把容易变化的层放在后面

# 好:pom.xml (低频变化) 在前,src (高频变化) 在后
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package

# 坏:所有文件混在一起复制,任何改动都导致缓存失效
COPY . .
RUN mvn package

4.3 敏感信息会永久留在历史层中

即使你在后面的层删除了密码文件,它依然存在于之前的某层。任何人都可以通过 docker historydocker save 导出镜像查看所有层的内容。

千万别做

COPY .env .          # 里面写了数据库密码
RUN rm .env          # 以为删掉了,其实还在历史层中

正确做法:使用 Docker secrets(Swarm 模式)或运行时通过环境变量/挂载配置文件注入敏感信息。

4.4 多阶段构建不等于删除文件,而是完全抛弃中间层

多阶段构建通过 FROM ... AS ...COPY --from=... 实现。最终镜像只包含最后一阶段的层,前一阶段的所有层(包括 JDK、Maven、源码)都不会进入最终镜像。这是真正的空间释放,比在单阶段中 rm 更彻底。

五、与我常混淆的 X 技术的区别

Docker 层 vs. 容器层(可写层)

对比项镜像层容器层(可写层)
性质只读可读写
生命周期持久存在(除非删除镜像)随容器删除而消失
共享性多个容器/镜像可共享每个容器独有
存储位置/var/lib/docker/overlay2/... 下的只读目录同一目录下的可写层(通常是 diff 目录)
内容应用静态文件 + 依赖容器运行时产生的日志、临时文件、修改

一句话:镜像层是“食谱”,容器层是“烹饪过程中加的调料”,容器删除后调料也没了。

六、总结与建议

到此这篇关于一文打你掌握Docker镜像层(Layer)原理与优化的文章就介绍到这了,更多相关Docker镜像层内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文