成功优化!Java 基础 Docker 镜像从 674MB 缩减到 58MB 的经验分享

简介: 本文分享了如何通过 jlink 和 jdeps 工具将 Java 基础 Docker 镜像从 674MB 优化至 58MB 的经验。首先介绍了选择合适的基础镜像的重要性,然后详细讲解了使用 jlink 构建自定义 JRE 镜像的方法,并通过 jdeps 自动化模块依赖分析,最终实现了镜像的大幅缩减。此外,文章还提供了实用的 .dockerignore 文件技巧和选择安全、兼容的基础镜像的建议,帮助开发者提升镜像优化的效果。

成功优化!Java 基础 Docker 镜像从 674MB 缩减到 58MB 的经验分享

在当今的软件开发领域,微服务架构和容器化应用已成为常态。随着应用程序的复杂性和规模不断增加,开发者们面临的一个主要挑战是如何有效管理和优化应用程序的体积。尤其是在使用 Java 进行开发时,生成的 Docker 镜像往往会相对较大,这不仅影响了部署速度,还增加了网络传输的负担和存储成本。因此,如何精简镜像大小成为了每个开发者亟待解决的问题。

本文将深入探讨如何通过 jlink 工具生成更小的 Java 运行时环境(JRE)镜像,并自动化整个过程。我们将分析不同模块的依赖关系,确保仅包括运行应用程序所需的最小模块。通过这样的方法,不仅可以提高应用程序的效率,还能优化资源的使用,让我们的微服务更加轻量、灵活。

我们将使用之前文章中构建的Spring Web应用来演示这些技巧,该文章是关于使用RFC-9457规范进行错误处理。我们的应用仅包含两个端点:

GET /users/

: 根据ID获取用户

POST /users : 创建新用户

@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注册表和生产服务器上浪费了磁盘空间。
  • 构建时间延长:镜像越大,构建和推送镜像所需的时间越长。
  • 安全性:镜像越大,依赖项越多,攻击面也越大。
  • 带宽:镜像越大,从注册表拉取和推送镜像时的带宽消耗越高。

使用简单明了的Dockerfile

基础镜像 Matter ✌🏽 : 选择合适的基础镜像

在考虑优化之前,你应该始终注意用于打包应用的基础镜像。你选择的基础镜像可能对最终镜像的大小产生显著影响。

可以用来打包Java应用的基础镜像有几种,包括:

  • JDK Alpine基础镜像:这些镜像体积较小,但不适合所有应用,因此可能会面临一些库的兼容性问题。
  • JDK Slim基础镜像:这些镜像基于Debian或Ubuntu,相较于完整的JDK镜像来说体积较小,但仍然比较大。
  • JDK完整基础镜像:这些镜像体积较大,包含运行应用所需的所有模块和依赖项。

为了给你一个基础镜像大小的概念,以下是openjdk:17-jdk-slim(瘦身版)和eclipse-temurin:17-jdk-alpine镜像大小的比较:

已知应用程序(jar)的大小约为20MB。

a73734a6dd92de77a836db737607d47.png

为了在Docker镜像中打包我们的工件,我们需要在应用根目录中定义一个Dockerfile,如下所示:

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。

77ea82aea702a64573e8edcd82d8868.png

等等,这只是一个只有两个端点的小项目,没有任何依赖项,那么对于一个有数十个依赖项和文件的应用来说,情况会如何呢?

使用 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上构建镜像,可能会遇到以下问题:

> [internal] load metadata for docker.io/library/eclipse-temurin:17-jdk-alpine:
Dockerfile:2
1 | # First stage, build the custom JRE
2 | >>> FROM eclipse-temurin:17-jdk-alpine AS jre-builder
3 |
4 | # Install binutils, required by jlink
ERROR: failed to solve: eclipse-temurin:17-jdk-alpine: no match for platform in manifest: not found

要解决此问题,你可以在Docker构建命令中添加:

--platform=linux/amd64

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

export DOCKER_DEFAULT_PLATFORM=linux/amd64

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

7c98abc6370e3d816275c5e3254067a.png

看看两个镜像的大小,使用 eclipse-temurin:17-jdk-alpine 作为基础镜像的镜像大小为180MB,比使用 openjdk:17-jdk-slim 作为基础镜像的674MB小73%。

实际优化

等一下,为什么我们不能使用JRE镜像而使用JDK镜像呢?

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

最重要的注意事项是“用户可以使用jlink创建更小的自定义运行时”。

4d09256dc5c8c03e82691131d83b0db.png

使用 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 镜像来使用 jlink 构建自定义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,比基础镜像小约95MB 🎉🥳

a3d5aca96806101d9b2e777038c2196.png

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

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

你应该会看到应用按预期运行。

7cb0cfd4024a3242ffe0824903a9d45.png

这还不够 🤌🏽

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

目前镜像的大小依然较大,这是因为在 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

我们可以简单地将这些模块替代 ALL-MODULE-PATH,修改 jlink 命令如下:

Dockerfile.jlink-known-modules.temurin

# 第一阶段,构建自定义 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 user-service:jlink-known-modules-temurin -f Dockerfile.jlink-known-modules.temurin .

这里是构建后的镜像大小:

869b83fff2dc103bfc9f3cee7797f06.png

我们得到了一个较小的镜像,大小为 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

d5d08ee9a7363ce9c7ac62a0ee115da.png

额外提示

在结束之前,请注意,您可以使用 .dockerignore 文件排除某些文件和目录,以减少镜像在中间阶段的大小。

您还应该注意,选择小型基础镜像是好的,但请确保它具备良好的安全策略,并与您的应用程序兼容。

结论

通过本文的探讨,我们成功展示了如何利用 jlink 工具和 jdeps 工具来生成更加精简的 Java 镜像。我们不仅减少了镜像的体积,从 85.3MB 降至 57.8MB,节省了大量的存储和传输资源,而且还引入了自动化的过程,进一步提升了开发效率。

在持续追求优化的过程中,自动化工具和最佳实践是每个开发者的得力助手。通过使用 .dockerignore 文件来排除不必要的文件和目录,我们还可以在构建镜像的中间阶段进一步减少体积。选择一个适合的基础镜像并确保其安全性和兼容性,也同样重要。

最后,优化镜像不仅能提升应用程序的性能,更能增强整体系统的可维护性和可扩展性。希望大家能够在实际项目中应用这些技术,进一步推动软件开发的高效化和现代化。

相关文章
|
4天前
|
弹性计算 双11 开发者
阿里云ECS“99套餐”再升级!双11一站式满足全年算力需求
11月1日,阿里云弹性计算ECS双11活动全面开启,在延续火爆的云服务器“99套餐”外,CPU、GPU及容器等算力产品均迎来了全年最低价。同时,阿里云全新推出简捷版控制台ECS Lite及专属宝塔面板,大幅降低企业和开发者使用ECS云服务器门槛。
|
21天前
|
存储 弹性计算 人工智能
阿里云弹性计算_通用计算专场精华概览 | 2024云栖大会回顾
阿里云弹性计算产品线、存储产品线产品负责人Alex Chen(陈起鲲)及团队内多位专家,和中国电子技术标准化研究院云计算标准负责人陈行、北京望石智慧科技有限公司首席架构师王晓满两位嘉宾,一同带来了题为《通用计算新品发布与行业实践》的专场Session。本次专场内容包括阿里云弹性计算全新发布的产品家族、阿里云第 9 代 ECS 企业级实例、CIPU 2.0技术解读、E-HPC+超算融合、倚天云原生算力解析等内容,并发布了国内首个云超算国家标准。
阿里云弹性计算_通用计算专场精华概览 | 2024云栖大会回顾
|
3天前
|
人工智能 弹性计算 文字识别
基于阿里云文档智能和RAG快速构建企业"第二大脑"
在数字化转型的背景下,企业面临海量文档管理的挑战。传统的文档管理方式效率低下,难以满足业务需求。阿里云推出的文档智能(Document Mind)与检索增强生成(RAG)技术,通过自动化解析和智能检索,极大地提升了文档管理的效率和信息利用的价值。本文介绍了如何利用阿里云的解决方案,快速构建企业专属的“第二大脑”,助力企业在竞争中占据优势。
|
2天前
|
人工智能 自然语言处理 安全
创新不设限,灵码赋新能:通义灵码新功能深度评测
自从2023年通义灵码发布以来,这款基于阿里云通义大模型的AI编码助手迅速成为开发者心中的“明星产品”。它不仅为个人开发者提供强大支持,还帮助企业团队提升研发效率,推动软件开发行业的创新发展。本文将深入探讨通义灵码最新版本的三大新功能:@workspace、@terminal 和 #team docs,分享这些功能如何在实际工作中提高效率的具体案例。
|
8天前
|
负载均衡 算法 网络安全
阿里云WoSign SSL证书申请指南_沃通SSL技术文档
阿里云平台WoSign品牌SSL证书是由阿里云合作伙伴沃通CA提供,上线阿里云平台以来,成为阿里云平台热销的国产品牌证书产品,用户在阿里云平台https://www.aliyun.com/product/cas 可直接下单购买WoSign SSL证书,快捷部署到阿里云产品中。
1851 6
阿里云WoSign SSL证书申请指南_沃通SSL技术文档
|
11天前
|
Web App开发 算法 安全
什么是阿里云WoSign SSL证书?_沃通SSL技术文档
WoSign品牌SSL证书由阿里云平台SSL证书合作伙伴沃通CA提供,上线阿里云平台以来,成为阿里云平台热销的国产品牌证书产品。
1791 2
|
20天前
|
编解码 Java 程序员
写代码还有专业的编程显示器?
写代码已经十个年头了, 一直都是习惯直接用一台Mac电脑写代码 偶尔接一个显示器, 但是可能因为公司配的显示器不怎么样, 还要接转接头 搞得桌面杂乱无章,分辨率也低,感觉屏幕还是Mac自带的看着舒服
|
26天前
|
存储 人工智能 缓存
AI助理直击要害,从繁复中提炼精华——使用CDN加速访问OSS存储的图片
本案例介绍如何利用AI助理快速实现OSS存储的图片接入CDN,以加速图片访问。通过AI助理提炼关键操作步骤,避免在复杂文档中寻找解决方案。主要步骤包括开通CDN、添加加速域名、配置CNAME等。实测显示,接入CDN后图片加载时间显著缩短,验证了加速效果。此方法大幅提高了操作效率,降低了学习成本。
5391 15
|
14天前
|
人工智能 关系型数据库 Serverless
1024,致开发者们——希望和你一起用技术人独有的方式,庆祝你的主场
阿里云开发者社区推出“1024·云上见”程序员节专题活动,包括云上实操、开发者测评和征文三个分会场,提供14个实操活动、3个解决方案、3 个产品方案的测评及征文比赛,旨在帮助开发者提升技能、分享经验,共筑技术梦想。
1154 152
|
22天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1585 14