亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

將 Java 基礎 Docker 鏡像的大小從 674MB 優化到 58MB

如果你是一名Java开发者,并且使用Docker来打包你的应用程序,你可能会注意到即使是“hello world”这样的项目,最终生成的镜像大小也可能会相当大。在这篇文章中,我们将介绍一些优化Java应用程序Docker镜像大小的技巧。

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

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

UserController.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镜像(没有进行一些优化的情况下)的大小可能会相当大。

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

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

镜像的大小会对你的性能产生显著影响,无论是作为开发者还是作为组织。特别是在大型项目中,当你处理多个服务时,镜像的大小可能会变得很大,这可能会花费你大量的金钱和时间。

你应避免使用大镜像的原因包括:

  • 磁盘空间:你在 Docker 注册表和生产服务器上浪费了磁盘空间。
  • 构建速度变慢:镜像越大,构建和推送镜像所需的时间就越长。
  • 安全性:镜像越大,依赖项就越多,攻击面也就越大。
  • 带宽:镜像越大,在从注册表拉取和推送镜像时消耗的带宽就越多。
使用简洁的 Dockerfile
基础镜像选择 Matter ✌🏽 :选择合适的基础镜像

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

你可以使用一些基础镜像来打包你的 Java 应用程序,其中一些包括:

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

为了让你了解基础镜像的大小,这里比较了 openjdk:17-jdk-slim(slim)和 eclipse-temurin:17-jdk-alpine(alpine)镜像的大小:

知道应用程序构建 artifact (jar) 的大小约为:20MB

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

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

Dockerfile.base-openjdk

     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 user-service .

在此之后,你应该会有一个名为 user-service 的 Docker 镜像,如你所见,该镜像的大小与应用程序工件的大小相比相当大,大约为 674MB。

等等什么 🤯 !!这只是一个小项目,只有两个端点且没有依赖,那么对于一个有几十个依赖和文件的应用程序呢!!

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

Dockerfile.base-temurin

     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"]

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

    docker build -t user-service:alpine -f Dockerfile.base-alpine . --platform=linux/amd64

🚨 重要说明

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

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

[内部] 加载元数据 docker.io/library/eclipse-temurin:17-jdk-alpine:
— — —
Dockerfile:2
— — — — — — — — — —
1 | # 第一阶段,构建自定义 JRE
2 | >>> FROM eclipse-temurin:17-jdk-alpine AS jre-builder
3 |
4 | # 安装 binutils,jlink 所需
— — — — — — — — — —
错误:无法解决:eclipse-temurin:17-jdk-alpine: 平台在清单中未找到
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

要解决此问题,您可以在 docker build 命令中添加以下参数:

-- platform=linux/amd64

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

export DOCKER_DEFAULT_PLATFORM=linux/amd64

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

export DOCKER_DEFAULT_PLATFORM=linux/amd64

使用 eclipse-temurin:17-jdk-alpine 作为基础镜像构建镜像后,我们得到了这个:

看看两个镜像的大小,即使没有进行任何调整,以 eclipse-temurin:17-jdk-alpine 为基础镜像构建的镜像大小为 180MB,比以 openjdk:17-jdk-slim 为基础镜像构建的 674MB 镜像小 73%。

实战优化
等等,为什么我们不能使用 JRE 镜像而是要用 JDK 镜像呢?

好的问题!从Java 11开始,JRE 不再可用

最重要的是要注意这一部分:“用户可以使用 jlink 创建更小的自定义运行时。”

使用 jlink 构建自己的 JRE 镜像

jlink 是一个工具,可以用来创建一个自定义的运行时镜像,其中只包含运行你的应用程序所需的模块。

👉 如果你的应用不需要与数据库交互,你不需要在镜像中包含 java.sql 模块。如果你的应用不需要与桌面 GUI 交互,你也不需要在镜像中包含 java.desktop 模块,以此类推。

它有点像 JRE 镜像的替代品,但你可以更灵活地控制在镜像中使用的模块。

所以在这里使用 jlink,我们的 Dockerfile 应该是这样的:

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

    # 安装binutils,jlink所需  
    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" ]

所以让我们来解释一下这里我们做了什么:

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

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

    docker build -t user-service:jlink-all-modules-temurin -f Dockerfile.jlink-all-modules.temurin .

如果你运行以下命令:

    docker images user-service

你会看到新的 Docker 镜像大小现在是 85.3MB,比使用 eclipse-temurin 基础镜像时大约少了 95MB 🎉🥳

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

    docker run -p 8080:8080 user-service:jlink-all-modules-temurin

你应该看到应用程序如预期的那样运行。

这还远远不够 🤌🏽

作为优秀的开发者,我们总是希望改进我们的工作,所以让我们看看如何进一步减小镜像的大小。

镜像大小仍然很大,这是因为我们在 jlink 命令中使用了 --add-modules ALL-MODULE-PATH,这包含了运行应用程序所需的所有模块,但实际上我们并不需要所有这些模块。所以让我们看看如何通过只包含运行应用程序所需的模块来减小镜像大小。

如何知道运行应用程序所需的模块?

我们可以使用 JDK 中自带的 jdeps 工具。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

这会打印出运行应用程序所需的所有模块,对于我们的情况是:

    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

我们可以将这个内容替换掉 jlink 命令中的 ALL-MODULE-PATH

Dockerfile.jlink-known-modules.temurin

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

    # 安装binutils,jlink需要  
    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 user-service:jlink-known-modules-temurin -f Dockerfile.jlink-known-modules.temurin .

并且这是构建镜像后的大小:

我们得到了一个更小的镜像大小,为 57.8MB,而不是 85.3MB。

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

在 Dockerfile 中自动化流程

Dockerfile.jlink-with-jdeps.temurin

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

    RUN mkdir /opt/app  
    COPY . /opt/app  

    WORKDIR /opt/app  

    ENV MAVEN_VERSION 3.5.4  
    ENV MAVEN_HOME /usr/lib/mvn  
    ENV PATH $MAVEN_HOME/bin:$PATH  

    RUN apk update && \  
        apk add --no-cache tar binutils  

    RUN 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  

    RUN mvn package -DskipTests  
    RUN 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 user-service:jlink-with-jdeps.temurin -f Dockerfile.jlink-with-jdeps.temurin . --platform=linux/amd64

奖励

在我们结束之前,请注意你可以使用 .dockerignore 文件来排除一些文件和目录不被复制到镜像中,这可以在中间阶段帮助减小镜像的大小。

你也应该注意,选择一个小的基础镜像是好的,但要确保它具有良好的安全策略,并且与你的应用程序兼容。

结论

希望这篇文章对你有所帮助。如果你有任何问题或意见,请随时通过 twitterlinkedin 联系我。并且一定要访问我的网站 https://www.abdelrani.com 查看新文章。

参考资料
點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消