我是如何将 Java 基础 docker 镜像大小从 674Mb 优化到 58Mb的

我是如何将 Java 基础 docker 镜像大小从 674Mb 优化到 58Mb的

如果您是 Java 开发人员,并且正在使用 Docker 打包应用程序,您可能已经注意到,即使是"hello world"类型的项目,最终镜像的大小也可能非常大。在本文中,我们将介绍一些优化 Java 应用程序的 Docker 镜像大小的技巧。

我们将使用上一篇文章《使用 RFC-9457 规范在 Spring web 中处理错误》中构建的相同 Spring web 应用程序来演示这些技巧。我们的应用程序仅包含 2 个端点:

  • GET /users/:id :通过 id 获取用户
  • POST /users:创建新用户

Controller.java

java 复制代码
@RestController 
@RequestMapping("/api/users") 
@RequiredArgsConstructor 
public  class  UserController { 

    private  final UserService userService; 

    @GetMapping("{id}") 
    public User getUser ( @PathVariable Long id) { 
        return userService.getUserById(id) 
                .orElseThrow(() -> new  UserNotFoundException (id, "/api/users" )); 
    } 

    @PostMapping 
    public User createUser ( @Valid  @RequestBody User user) { 
        return userService.createUser(user); 
    } 

不是太多吧?但正如你所看到的,最简单的docker镜像(没有应用一些优化)的大小可能相当大。

不是太多吧?但正如你所看到的,最简单的docker镜像(没有应用一些优化)的大小可能相当大。

本文的源代码可以在github上找到

为什么我们应该关心镜像大小?

镜像大小会对您的绩效产生重大影响,无论是作为开发人员还是组织。特别是当您在处理包含多项服务的大型项目时,镜像的大小可能会非常大,这可能会花费您大量的金钱和时间。

应避免使用大镜像的原因如下:

  • 磁盘空间:您正在浪费 Docker 注册表和生产服务器中的磁盘空间。
  • 构建速度较慢:镜像越大,构建和推送镜像所需的时间越长。
  • 安全性:镜像越大,依赖关系越大,攻击面就越大。
  • 带宽:镜像越大,从注册表拉取和推送镜像时消耗的带宽就越大。

使用简单的 Dockerfile

基础镜像很重要 ✌🏽 :选择正确的基础镜像

在开始考虑优化之前,你应该始终小心用于打包应用程序的基础镜像。你选择的基础镜像会对最终镜像的大小产生重大影响(如下所示)。

您可以使用多个基础镜像来打包 Java 应用程序,其中一些是:

  • JDK Alpine 基础镜像 :这些镜像体积相当小,但并不适合所有应用程序,因此您可能会遇到与某些库的兼容性问题。
  • JDK Slim 基础镜像 :这些镜像基于 Debian 或 Ubuntu,与完整的 JDK 镜像相比,它们的尺寸非常小,但仍然相当大。
  • JDK 完整基础镜像 :这些镜像尺寸相当大,它们包含运行应用程序所需的所有模块和依赖项。

openjdk:17-jdk-slim为了让您对基础镜像的大小有一个概念,这里是(slim) 和 eclipse-temurin:17-jdk-alpine(alpine) 镜像大小的比较:

知道应用程序工件(jar)的大小为:~20MB

openjdk:17-jdk-slim为了让您对基础镜像的大小有一个概念,这里是(slim) 和 eclipse-temurin:17-jdk-alpine(alpine) 镜像大小的比较:

知道应用程序工件(jar)的大小为:~20MB

将我们的工件打包到docker镜像中,我们需要 Dockefile在应用程序根目录中定义如下:

使用 openjdk:17-jdk-slim 作为基础镜像。

Dockerfile.base-openjdk

bash 复制代码
FROM openjdk: 17 -jdk-slim 

# 设置容器中的工作目录
WORKDIR /app 

# 创建用户
RUN addgroup -- system spring && adduser -- system spring --ingroup spring 

# 更改为用户
USER spring:spring 

COPY target/*.jar app.jar 

EXPOSE 8080

 CMD [ "java" , "-jar" , "app.jar" ]

定义Dockerfile后,我们可以使用以下命令构建镜像:

docker build-t 用户服务。

此后,你应该有一个名为的 docker 镜像 user-service,你可以看到,与应用程序工件的大小相比,镜像的大小相当大,大约为 674MB

等什么🤯!!然而,这只是一个具有 2 个端点且没有依赖关系的小项目,那么具有十几个依赖项和文件的应用程序怎么办呢?

使用 eclipse-temurin:17-jdk-alpine 作为基础镜像。

Dockerfile.base-temurin

bash 复制代码
FROM eclipse-temurin:17-jdk-alpine 

ARG APPLICATION_USER=spring 
# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

# 设置用户来运行应用程序
USER $APPLICATION_USER 

# 将 jar 文件复制到容器
COPY -- chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

# 设置工作目录
WORKDIR /app 

# 公开端口
EXPOSE 8080 

# 运行应用程序
ENTRYPOINT [ "java" , "-jar" , "/app/app.jar" ]

使用以下命令构建镜像后:

bash 复制代码
docker build -t 用户服务:alpine -f Dockerfile.base-alpine . --platform=linux/amd64

🚨 附注

重要提示:如果您在 Apple Silicon 上使用 MAC,则在构建映像时可能会遇到以下问题:


[internal] 加载 docker.io/library/eclipse-temurin:17-jdk-alpine 的元数据:


Dockerfile:2


1 | # 第一阶段,构建自定义 JRE

2 | >>> FROM eclipse-temurin:17-jdk-alpine AS jre-builder

3 |

4 | # 安装 jlink 所需的 binutils


错误:无法解决:eclipse-temurin:17-jdk-alpine:清单中的平台不匹配:未找到


要解决此问题,您可以将其添加到 docker build命令中:

--平台=linux/amd64

linux/amd64或者您可以通过运行以下命令将默认平台设置为:

导出 DOCKER_DEFAULT_PLATFORM=linux/amd64

linux/amd64或者您可以通过运行以下命令将默认平台设置为:
export DOCKER_DEFAULT_PLATFORM=linux/amd64

eclipse-temurin:17-jdk-alpine使用该镜像作为基础镜像构建镜像后,我们得到了以下结果:

查看两个镜像的大小,即使不进行任何调整,用作基础镜像的镜像大小也为 180MB,比用作基础镜像的 eclipse-temurin:17-jdk-alpine镜像(674MB)小 73%openjdk:17-jdk-slim

动手优化

等一下,为什么我们不能使用 JREimage 来代替 JDKimage ?

好问题!这是因为从 Java 11 开始,JRE不再可用

其中需要考虑的最重要的一点是这部分"用户可以使用 jlink 创建更小的自定义运行时。"

jlink是一种可用于创建自定义运行时映像的工具,该映像仅包含运行应用程序所需的模块;

👉 如果您的应用程序不与数据库交互,则无需 java.sql在映像中包含该模块。 如果您不与桌面 GUI 交互,则无需 java.desktop在映像中包含该模块。 等等。

它有点像 JRE镜像的替代品,但对您想要在镜像中使用的模块有更多的控制权。

因此 jlink我们的 Dockerfile 应该是这样的:

bash 复制代码
# 第一阶段,构建自定义 JRE
 FROM eclipse-temurin:17-jdk-alpine AS jre-builder 

# 安装 jlink 所需的 binutils
 RUN apk update && \ 
    apk add binutils 

# 构建小型 JRE 映像
RUN $JAVA_HOME /bin/jlink \ 
         --verbose \ 
         --add-modules ALL-MODULE-PATH \ 
         --strip-debug \ 
         --no-man-pages \ 
         --no-header-files \ 
         --compress=2 \ 
         --output /optimized-jdk-17 

# 第二阶段,使用自定义 JRE 并构建应用程序映像
FROM alpine:latest 
ENV JAVA_HOME=/opt/jdk/jdk-17 
ENV PATH= " ${JAVA_HOME} /bin: ${PATH} " 

# 从基础映像复制 JRE
 COPY --from=jre-builder /optimized-jdk-17 $JAVA_HOME 

# 添加应用程序用户
ARG APPLICATION_USER=spring 

# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

COPY -- chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

WORKDIR /app 

USER $APPLICATION_USER

 EXPOSE 8080 
ENTRYPOINT [ "java" , "-jar" , "/app/app.jar" ]

那么让我们解释一下我们在这里所做的事情:

  • 我们有两个阶段,第一阶段用于构建自定义 JRE 映像 jlink,第二阶段用于将应用程序打包到 slim alpine 映像中。
  • 在第一阶段,我们使用 eclipse-temurin:17-jdk-alpine来构建自定义 JRE 映像 jlink。然后我们安装 binutils所需的 jlink,然后我们运行 jlink构建一个小型 JRE 映像,该映像包含 --add-modules ALL-MODULE-PATH运行应用程序所需的所有模块(目前)。
  • 在第二阶段,我们使用 alpine镜像(相当小的 3Mb)来打包我们的应用程序)作为基础镜像,然后我们 JRE从第一阶段获取自定义并将其用作我们的 JAVA_HOME
  • Dockerfile 的其余部分与前一个相同,只是复制工件并使用自定义用户(非 root)设置入口点。

然后我们可以使用以下命令构建镜像:

docker build -t 用户服务:jlink-all-modules-temurin -f Dockerfile.jlink-all-modules.temurin 。

如果运行以下命令:

docker 镜像用户服务

您将看到该镜像的新 Docker 镜像大小现在为 85.3MB,比使用 eclipse-temurin 基础镜像的基础镜像小约 95MB 🎉🥳

为了确保镜像按预期工作,您可以运行以下命令:

docker run -p 8080:8080 用户服务:jlink-all-modules-temurin

您应该会看到应用程序按预期运行。

这还不够

作为一名优秀的开发人员,我们总是希望改进我们的工作,所以让我们看看如何进一步改善镜像尺寸。

--add-modules ALL-MODULE-PATH镜像大小仍然很大,这是因为在命令中使用时 jlink,我们包含了运行应用程序所需的所有模块,但我们肯定不需要所有模块。因此,让我们看看如何通过仅包含运行应用程序所需的模块来获得较小的镜像大小。

如何知道运行应用程序需要哪些模块?

我们可以使用 jdepsJDK自带的工具。jdeps该工具可以用来分析jar文件的依赖关系并生成运行应用程序所需的模块列表。

为此,我们可以在项目根目录运行以下命令:

jdeps --ignore-missing-deps -q \ 
      --recursive \ 
      --multi-release 17 \ 
      --print-module-deps \ 
      --class-path BOOT-INF/lib/* \ 
      target/spring-error-handling-rfc-9457-0.0.1-SNAPSHOT.jar

这将打印出运行应用程序所需的模块列表,在我们的例子中是:

bash 复制代码
java.base、java.compiler、java.desktop、java.instrument、java.management、java.naming、java.net.http、java.prefs、java.rmi、java.scripting、java.security.jgss、java.sql、jdk.jfr、jdk.unsupported

我们可以简单地将其放入命令 ALL-MODULE-PATHjlink

Dockerfile.jlink-已知模块.temurin

bash 复制代码
# 第一阶段,构建自定义 JRE
 FROM openjdk:17-jdk-slim AS jre-builder 

# 安装 jlink 所需的 binutils
 RUN apt-get update -y && \ 
    apt-get install -y binutils 

# 构建小型 JRE 映像
RUN $JAVA_HOME/bin/jlink \ 
         --verbose \ 
         --add-modules java.base,java.compiler,java.desktop,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql,jdk.jfr,jdk.unsupported \ 
         --strip-debug \ 
         --no-man-pages \ 
         --no-header-files \ 
         --compress = 2 \ 
         --output /optimized-jdk-17 

# 第二阶段,使用自定义 JRE 并构建应用程序映像
FROM alpine:latest 
ENV JAVA_HOME =/opt/jdk/jdk- 17
 ENV PATH = "${JAVA_HOME}/bin:${PATH}" 

# 从基础镜像复制 JRE
 COPY --from =jre-builder /optimized-jdk- 17  $JAVA_HOME 

# 添加应用程序用户
ARG APPLICATION_USER =spring 

# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

COPY --chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

WORKDIR /app 

USER $APPLICATION_USER 

EXPOSE 8080 
ENTRYPOINT [ "java", "-jar", "/app/app.jar" ]

然后我们可以使用以下命令构建镜像:

docker build - t用户-服务:jlink -已知-模块- temurin - f Dockerfile.jlink -已知-模块.temurin 。

构建后镜像的尺寸如下:

我们得到的镜像尺寸较小,为 57.8MB,而不是 85.3MB。

这很好,但是我们不能自动化这个过程,而是 jdeps手动运行命令,然后将模块复制到 jlink命令中?

dockerfile 内部流程自动化

Dockerfile.jlink-带有-jdeps.temurin

bash 复制代码
# 第一阶段,构建自定义 JRE
 FROM eclipse-temurin:17-jdk-alpine AS jre-builder 

RUN mkdir /opt/app 
COPY 。 /opt/app 

WORKDIR /opt/app

环境 MAVEN_VERSION 3.5.4
环境 MAVEN_HOME /usr/lib/mvn
环境 PATH $MAVEN_HOME/bin:$PATH

运行 apk 更新 && \ 
    apk 添加 --no-cache tar binutils


运行 wget http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 
  tar -zxvf apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 
  rm apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 
  mv apache-maven-$MAVEN_VERSION /usr/lib/mvn

运行 mvn package -DskipTests
运行 jar xvf target/spring-error-handling-rfc-9457-0.0.1-SNAPSHOT.jar 
RUN jdeps --ignore-missing-deps -q \ 
    --recursive \ 
    --multi-release 17 \ 
    --print-module-deps \ 
    --class-path 'BOOT-INF/lib/*' \ 
    target/spring-error-handling-rfc-9457-0.0.1-SNAPSHOT.jar > modules.txt 

# 构建小型 JRE 映像
RUN $JAVA_HOME/bin/jlink \ 
         --verbose \ 
         --add-modules $(cat modules.txt) \ 
         --strip-debug \ 
         --no-man-pages \ 
         --no-header-files \ 
         --compress = 2 \ 
         --output /optimized-jdk-17 

# 第二阶段,使用自定义 JRE 并构建应用程序映像
FROM alpine:latest 
ENV JAVA_HOME =/opt/jdk/jdk- 17
 ENV PATH = "${JAVA_HOME}/bin:${PATH}" 

# 从基础镜像复制 JRE
 COPY --from =jre-builder /optimized-jdk- 17  $JAVA_HOME 

# 添加应用程序用户
ARG APPLICATION_USER =spring 

# 创建用户来运行应用程序,不要以 root 身份运行
RUN addgroup --system $APPLICATION_USER && adduser --system $APPLICATION_USER --ingroup $APPLICATION_USER 

# 创建应用程序目录
RUN mkdir /app && chown -R $APPLICATION_USER /app 

COPY --chown = $APPLICATION_USER : $APPLICATION_USER target/*.jar /app/app.jar 

WORKDIR /app 

USER $APPLICATION_USER 

EXPOSE 8080 
ENTRYPOINT [ "java", "-jar", "/app/app.jar" ]

然后我们可以使用以下命令构建镜像:

docker build -t 用户服务:jlink-with-jdeps.temurin -f Dockerfile.jlink-with-jdeps.temurin . --platform=linux/amd64

奖金

在我们完成之前,请注意,您可以使用一个 .dockerignore文件来排除一些文件和目录被复制到镜像中,这有助于减少中间阶段镜像的大小。

您还应该知道,选择小型的基础镜像是好的,但要确保它具有良好的安全策略并且与您的应用程序兼容。

结论

希望这篇文章对您有所帮助。如果您有任何问题或意见,请随时通过TwitterLinkedIn 与我联系。请务必访问我的网站https://www.abdelrani.com查看新文章。

参考

以上文章翻译自medium,原文链接

相关推荐
o独酌o3 分钟前
递归的‘浅’理解
java·开发语言
Book_熬夜!5 分钟前
Python基础(六)——PyEcharts数据可视化初级版
开发语言·python·信息可视化·echarts·数据可视化
无问81715 分钟前
数据结构-排序(冒泡,选择,插入,希尔,快排,归并,堆排)
java·数据结构·排序算法
m0_6312704033 分钟前
高级c语言(五)
c语言·开发语言
customer0836 分钟前
【开源免费】基于SpringBoot+Vue.JS在线文档管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
2401_8582861139 分钟前
53.【C语言】 字符函数和字符串函数(strcmp函数)
c语言·开发语言
Flying_Fish_roe1 小时前
Spring Boot-版本兼容性问题
java·spring boot·后端
程序猿进阶1 小时前
如何在 Visual Studio Code 中反编译具有正确行号的 Java 类?
java·ide·vscode·算法·面试·职场和发展·架构
程序猿练习生1 小时前
C++速通LeetCode中等第5题-无重复字符的最长字串
开发语言·c++·leetcode
slandarer1 小时前
MATLAB | R2024b更新了哪些好玩的东西?
java·数据结构·matlab