最小化 Java 镜像的经常使用技巧

背景

随着容器技术的普及,愈来愈多的应用被容器化。人们使用容器的频率愈来愈高,但经常忽略一个基本但又很是重要的问题 - 容器镜像的体积。本文将介绍精简容器镜像的必要性并以基于 spring boot 的 java 应用为例描述最小化容器镜像的经常使用技巧。java

精简容器镜像的必要性

精简容器镜像是很是必要的,下面分别从安全性和敏捷性两个角度进行阐释。node

安全性python

基于安全方面的考虑,将没必要要的组件从镜像中移除能够减小攻击面、下降安全风险。虽然 docker 支持用户经过 Seccomp 限制容器内能够执行操做或者使用 AppArmor 为容器配置安全策略,但它们的使用门槛较高,要求用户具有安全领域的专业素养。git

敏捷性github

精简的容器镜像能提升容器的部署速度。假设某一时刻访问流量激增,您须要经过增长容器副本数以应对突发压力。若是某些宿主机不包含目标镜像,须要先拉取镜像,而后启动容器,这时使用体积较小的镜像能加速这一过程、缩短扩容时间。另外,镜像体积越小,其构建速度也越快,同时还能减小存储和传输的成本。spring

经常使用技巧

将一个 java 应用容器化所需的步骤可概括以下:docker

  1. 编译 java 源码并生成 jar 包。
  2. 将应用 jar 包和依赖的第三方 jar 包移动到合适的位置。

本章所用的样例是一个基于 spring boot 的 java 应用 spring-boot-docker,所用的未经优化的 dockerfile 以下:shell

FROM maven:3.5-jdk-8
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]

因为应用使用 maven 构建,dockerfile 中指定maven:3.5-jdk-8做为基础镜像,该镜像的大小为 635MB。经过这种方式最终构建出的镜像很是大,达到了 719MB,这是由于一方面基础镜像自己就很大,另外一方面 maven 在构建过程当中会下载许多用于执行构建任务的 jar 包。缓存

多阶段构建安全

Java 程序的运行只依赖 JRE,并不须要 maven 或者 JDK 中众多用于编译、调试、运行的工具,所以一个明显的优化方法是将用于编译构建 java 源码的镜像和用于运行 java 应用的镜像分开。为了达到这一目的,在 docker 17.05 版本以前须要用户维护 2 个 dockerfile 文件,这无疑增长了构建的复杂性。好在自 17.05 开始,docker 引入了多阶段构建的概念,它容许用户在一个 dockerfile 中使用多个 From 语句。每一个 From 语句能够指定不一样的基础镜像并将开启一个全新的构建流程。您能够选择性地将前一阶段的构建产物复制到另外一个阶段,从而只将必要的内容保留在最终的镜像里。优化后的 dockerfile 以下:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

该 dockerfile 选用maven:3.5-jdk-8做为第一阶段的构建镜像,选用openjdk:8-jre做为运行 java 应用的基础镜像而且只拷贝了第一阶段编译好的.claass文件和依赖的第三方 jar 包到最终的镜像里。经过这种方式优化后的镜像大小为 459MB。

使用 distroless 做为基础镜像

虽然经过多阶段构建能减少最终生成的镜像的大小,但 459MB 的体积仍相对过大。经调查发现,这是由于使用的基础镜像openjdk:8-jre体积过大,到达了 443MB,所以下一步的优化方向是减少基础镜像的体积。

Google 开源的项目 distroless 正是为了解决基础镜像体积过大这一问题。Distroless 镜像只包含应用程序及其运行时依赖项,不包含包管理器、shell 以及在标准 Linux 发行版中能够找到的任何其余程序。目前,distroless 为依赖 javapythonnodejsdotnet 等环境的应用提供了基础镜像。

使用 distroless 的 dockerfile 以下:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM gcr.io/distroless/java
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

该 dockerfile 和上一版的惟一区别在于将运行阶段依赖的基础镜像由openjdk:8-jre(443 MB)替换成了gcr.io/distroless/java(119 MB)。通过这一优化,最终镜像的大小为 135MB。

使用 distroless 的惟一不即是您没法 attach 到一个正在运行的容器上排查问题,由于镜像中不包含 shell。虽然 distroless 的 debug 镜像提供 busybox shell,但须要用户从新打包镜像、部署容器,对于那些已经基于非 debug 镜像部署的容器无济于事。 但从安全角度来看,没法 attach 容器并不彻底是坏事,由于攻击者没法经过 shell 进行攻击。

使用 alpine 做为基础镜像

若是您确实有 attach 容器的需求,又但愿最小化镜像的大小,能够选用 alpine 做为基础镜像。Alpine 镜像的特色是体积很是下,基础款镜像的体积仅 4 MB 左右。

使用 alpine 后的 dockerfile 以下:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre-alpine
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

这里并未直接继承基础款 alpine,而是选用从 alpine 构建出的包含 java 运行时的openjdk:8-jre-alpine(83MB)做为基础镜像。使用该 dockerfile 构建出的镜像体积为 99.2MB,比基于 distroless 的还要小。

执行命令docker exec -ti <container_id> sh能够成功 attach 到运行的容器中。

distroless vs alpine

既然 distroless 和 alpine 都能提供很是小的基础镜像,那么在生产环境中到底应该选择哪种呢?若是安全性是您的首要考虑因素,建议选用 distroless,由于它惟一可运行的二进制文件就是您打包的应用;若是您更关注镜像的体积,能够选用 alpine。

其余技巧

除了能够经过上述技巧精简镜像外,还有如下方式:

  1. 将 dockerfile 中的多条指令合并成一条,经过减小镜像层数的方式达到精简镜像体积的目的。
  2. 将稳定且体积较大的内容置于镜像下层,将变更频繁且体积较小的内容置于镜像上层。虽然该方式没法直接精简镜像体积,但充分利用了镜像的缓存机制,一样能够达到加快镜像构建和容器部署的目的。

想了解更多优化 dockerfile 的小窍门可参考教程 Best practices for writing Dockerfiles

总结

  1. 本文经过一系列的优化,将 java 应用的镜像体积由最初的 719MB 缩小到 100MB 左右。若是您的应用依赖其余环境,也能够用相似的原则进行优化。
  2. 针对 java 镜像,google 提供的另外一款工具 jib 能为您屏蔽镜像构建过程当中的复杂细节,自动构建出精简的 java 镜像。使用它您无须编写 dockerfile,甚至不须要安装 docker。
  3. 对于相似 distroless 这样没法 attach 或者不方便 attach 的容器,建议您将它们的日志中心化存储,以便问题的追踪和排查。具体方法可参考文章面向容器日志的技术实践



本文做者:吴波bruce_wu

阅读原文

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索