Docker 中缓慢的 gradle 构建。缓存 gradle 构建

Posted

技术标签:

【中文标题】Docker 中缓慢的 gradle 构建。缓存 gradle 构建【英文标题】:Slow gradle build in Docker. Caching gradle build 【发布时间】:2020-02-23 21:07:59 【问题描述】:

我正在做一个大学项目,我们需要一次运行多个 Spring Boot 应用程序。

我已经使用 gradle docker 镜像配置了多阶段构建,然后在 openjdk:jre 镜像中运行应用程序。

这是我的 Dockerfile:

FROM gradle:5.3.0-jdk11-slim as builder
USER root
WORKDIR /usr/src/java-code
COPY . /usr/src/java-code/

RUN gradle bootJar

FROM openjdk:11-jre-slim
EXPOSE 8080
WORKDIR /usr/src/java-app
COPY --from=builder /usr/src/java-code/build/libs/*.jar ./app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

我正在使用 docker-compose 构建和运行所有内容。 docker-compose 的一部分:

 website_server:
    build: website-server
    image: website-server:latest
    container_name: "website-server"
    ports:
      - "81:8080"

当然,第一次构建需要很长时间。 Docker 正在拉取它的所有依赖项。我对此没意见。

目前一切正常,但代码中的每一个小改动都会导致一个应用的构建时间约为 1 分钟。

部分构建日志:docker-compose up --build

Step 1/10 : FROM gradle:5.3.0-jdk11-slim as builder
 ---> 668e92a5b906
Step 2/10 : USER root
 ---> Using cache
 ---> dac9a962d8b6
Step 3/10 : WORKDIR /usr/src/java-code
 ---> Using cache
 ---> e3f4528347f1
Step 4/10 : COPY . /usr/src/java-code/
 ---> Using cache
 ---> 52b136a280a2
Step 5/10 : RUN gradle bootJar
 ---> Running in 88a5ac812ac8

Welcome to Gradle 5.3!

Here are the highlights of this release:
 - Feature variants AKA "optional dependencies"
 - Type-safe accessors in Kotlin precompiled script plugins
 - Gradle Module Metadata 1.0

For more details see https://docs.gradle.org/5.3/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJar

BUILD SUCCESSFUL in 48s
3 actionable tasks: 3 executed
Removing intermediate container 88a5ac812ac8
 ---> 4f9beba838ed
Step 6/10 : FROM openjdk:11-jre-slim
 ---> 0e452dba629c
Step 7/10 : EXPOSE 8080
 ---> Using cache
 ---> d5519e55d690
Step 8/10 : WORKDIR /usr/src/java-app
 ---> Using cache
 ---> 196f1321db2c
Step 9/10 : COPY --from=builder /usr/src/java-code/build/libs/*.jar ./app.jar
 ---> d101eefa2487
Step 10/10 : ENTRYPOINT ["java", "-jar", "app.jar"]
 ---> Running in ad02f0497c8f
Removing intermediate container ad02f0497c8f
 ---> 0c63eeef8c8e
Successfully built 0c63eeef8c8e
Successfully tagged website-server:latest

每次在Starting a Gradle Daemon (subsequent builds will be faster)之后冻结

我正在考虑使用缓存的 gradle 依赖项添加卷,但我不知道这是否是问题的核心。我也找不到很好的例子。

有什么方法可以加快构建速度?

【问题讨论】:

我对Java和Gradle不是很熟悉,但是这和本地开发中的行为不一样吗?我的意思是,如果您对代码进行了一些更改,则需要重新编译项目以将更改也应用到运行时。也许您的意思是 Gradle 重新编译所有项目,而不仅仅是更改的部分? 发布的 Dockerfile 工作正常,但问题是速度。在本地构建大约需要 8 秒,在 Docker 中大约需要 1 到 1.5 分钟。我想知道是否有办法加快 docker build。 【参考方案1】:

构建需要很多时间,因为每次构建 Docker 映像时 Gradle 都会下载所有插件和依赖项。

无法在映像构建时挂载卷。但是可以引入新的阶段,将下载所有依赖项并缓存为 Docker 镜像层。

FROM gradle:5.6.4-jdk11 as cache
RUN mkdir -p /home/gradle/cache_home
ENV GRADLE_USER_HOME /home/gradle/cache_home
COPY build.gradle /home/gradle/java-code/
WORKDIR /home/gradle/java-code
RUN gradle clean build -i --stacktrace

FROM gradle:5.6.4-jdk11 as builder
COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle
COPY . /usr/src/java-code/
WORKDIR /usr/src/java-code
RUN gradle bootJar -i --stacktrace

FROM openjdk:11-jre-slim
EXPOSE 8080
USER root
WORKDIR /usr/src/java-app
COPY --from=builder /usr/src/java-code/build/libs/*.jar ./app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Gradle 插件和依赖缓存位于$GRADLE_USER_HOME/cachesGRADLE_USER_HOME 必须设置为不同于 /home/gradle/.gradle 的值。父 Gradle Docker 镜像中的/home/gradle/.gradle 被定义为卷,并在每个镜像层之后被擦除。

在示例代码中GRADLE_USER_HOME 设置为/home/gradle/cache_home

builder阶段复制Gradle缓存以避免再次下载依赖项:COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle

只有在build.gradle 更改时,才会重建舞台cache。 当Java类发生变化时,所有依赖的缓存图像层都会被重用。

这种修改可以减少构建时间,但使用 Java 应用程序构建 Docker 镜像的更简洁的方法是由 Google 提供的Jib。 有一个Jib Gradle plugin 允许为 Java 应用程序构建容器映像,而无需手动创建 Dockerfile。 使用应用程序构建镜像并运行容器类似于:

gradle clean build jib
docker-compose up

【讨论】:

多阶段构建,一个阶段只包含上下文中的build.gradle,这绝对是要走的路。通过在 cache 中仅复制 build.gradle,您可以确保在 Gradle 构建文件未更改的情况下仅下载一次依赖项(Docker 将重新使用缓存) @Evgeniy Khyst 当缓存阶段使用 build 任务时,它应该如何工作,Spring Boot 也在调用 bootJar 并且你不会有主类的源,这将产生 @ 987654340@ 下载依赖后。谢谢 好吧,其实我这里弄错了。如果您在booJar 任务中设置mainClassName,您就可以开始了。当有源任务时,如果没有明确设置属性,则从它们中解析主类名。此外,如果使用 kotlin,即使主类是 com.xyz.Main.kt 文件,我也需要使用 com.xyz.MainKt 名称。 @Saris 我遇到了同样的问题,并且能够在缓存阶段用RUN gradle clean build -i --stacktrace -x bootJar 排除bootJar,而不必担心指定mainClassName @Evgeniy Khyst 很好的解决方案,谢谢!【参考方案2】:

Docker 将其图像缓存在“层”中。您运行的每个命令都是一个层。在给定层中检测到的每个更改都会使其之后的层失效。如果缓存失效,则必须从头开始构建失效层,包括依赖项

我建议拆分您的构建步骤。有一个前一层,它只将依赖项规范复制到图像中,然后运行一个命令,这将导致 Gradle 下载依赖项。 完成后,将您的源代码复制到您刚刚执行此操作的同一位置,然后运行真正的构建。

这样,只有当 gradle 文件发生变化时,之前的层才会失效。

我没有使用 Java/Gradle 完成此操作,但我在 this 博客文章的指导下对 Rust 项目遵循了相同的模式。

【讨论】:

【参考方案3】:

您可以尝试使用BuildKit(现在默认在latest docker-compose 1.25中激活)

参见“Speed up your java application Docker images build with BuildKit!”来自 Aboullaite Med.

(这是针对 maven 的,但同样的想法也适用于 gradle)

让我们考虑以下 Dockerfile:

FROM maven:3.6.1-jdk-11-slim AS build  
USER MYUSER  
RUN mvn clean package  

修改第二行总是因为错误的依赖而使maven缓存无效,这暴露了缓存效率低下的问题。

BuildKit 通过引入并发构建图求解器解决了这个限制,它可以并行运行构建步骤并优化对最终结果没有影响的命令。

此外,Buildkit 仅跟踪在重复构建调用之间对文件所做的更新,以优化对本地源文件的访问。因此,无需等待本地文件被读取或上传即可开始工作。

【讨论】:

问题与构建 Docker 镜像无关,而是在Dockerfile 中运行命令。我认为这是缓存问题。我已经尝试过caching,但每次运行它仍然会下载 Gradle 等。我也尝试了不同的卷目标组合。 @NeelKamath “在 Dockerfile 中运行命令”是“构建 Docker 镜像”的一部分! BuildKit 是 made 用于缓存构建和加速 docker 构建的。试一试。 单独使用 BuildKit 并不能解决这个问题:通过在构建开始时复制整个上下文并使用 RUN,BuildKit 将始终在每次代码更改时重新构建所有内容(因为上下文已更改),但除了@Evgeniy Khyst 的回答外,它可能会朝着更好的结果前进 @PierreB。好的。所以任何解决方案都会比我想象的更复杂。【参考方案4】:

正如其他答案所提到的,docker 将每个步骤缓存在一个层中。如果您可以以某种方式仅将下载的依赖项放入一个层,那么假设依赖项没有更改,则不必每次都重新下载它。

不幸的是,gradle 没有内置任务来执行此操作。但是您仍然可以解决它。这是我所做的:

# Only copy dependency-related files
COPY build.gradle gradle.properties settings.gradle /app/

# Only download dependencies
# Eat the expected build failure since no source code has been copied yet
RUN gradle clean build --no-daemon > /dev/null 2>&1 || true

# Copy all files
COPY ./ /app/

# Do the actual build
RUN gradle clean build --no-daemon

另外,请确保您的 .dockerignore 文件至少包含这些项目,以便在构建映像时不会在 docker build 上下文中发送它们:

.gradle/
bin/
build/
gradle/

【讨论】:

【参考方案5】:

就像其他人的回答一样,如果您的互联网连接很慢,因为它每次都会下载依赖项,您可能需要设置 sonatype nexus,以保持已下载的依赖项。

【讨论】:

【参考方案6】:

我使用了一个稍微不同的想法。我安排了在我的 Jenkins 上构建整个 Gradle 项目的每晚构建:

docker build -f Dockerfile.cache --tag=gradle-cache:latest .

# GRADLE BUILD CACHE
FROM gradle:6.7.1-jdk11

COPY build.gradle.kts /home/gradle/code/
COPY settings.gradle.kts /home/gradle/code/
COPY gradle.properties /home/gradle/code/
COPY ./src /home/gradle/code/src

WORKDIR /home/gradle/code

RUN gradle bootJar -i -s

然后我从这个“缓存图像”开始构建,这样我就可以利用 Gradle 的所有优点:

docker build --tag=my-app:$version .

# GRADLE BUILD
FROM gradle-cache:latest as gradle

COPY build.gradle.kts /home/gradle/code/
COPY settings.gradle.kts /home/gradle/code/
COPY gradle.properties /home/gradle/code/

RUN rm -rf /home/gradle/code/src
COPY ./src /home/gradle/code/src

WORKDIR /home/gradle/code

RUN gradle bootJar -i -s

# SPRING BOOT
FROM openjdk:11.0.9.1-jre

COPY --from=gradle /home/gradle/code/build/libs/app.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-Xmx2G", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]

请记住每周左右修剪未使用的图像。

【讨论】:

【参考方案7】:

我不太了解 docker 内部结构,但我认为问题在于每个新的 docker build 命令都会复制所有文件并构建它们(如果它检测到至少一个文件中的更改)。 那么这很可能会更改几个 jar,并且第二步也需要运行。

我的建议是在终端上构建(在 docker 之外),并且只有 docker 构建应用程序映像。

这甚至可以通过 gradle 插件自动化:

https://github.com/Transmode/gradle-docker(一个例子,我没有仔细搜索)

【讨论】:

所以在 docker 中构建 gradle 是错误的方式吗?这个想法是你不需要安装任何依赖项来在你的环境中构建和运行代码。 哦,我明白了!我不认为你在你的问题上提到了这一点。在那种情况下,看起来当前的解决方案很好......这需要时间。另一个问题是,为什么您希望您的开发环境没有依赖项?它被称为开发环境,因为它会包含开发的东西。 这很好。我应该更具体。容器开发中的所有 docker 都是由大约 10 个人正在编辑项目这一事实造成的。所以我认为我会很高兴没有任何操作系统或 sdk 依赖项。但也许这有点过头了。 根据我的经验(最多 6/7 个开发人员的团队),每个人都有本地设置。通常每个 repo 根目录上都有一个自述文件,其中包含该存储库的步骤命令和所有需要设置的内容。我了解您的问题,但我认为 docker 不是解决此问题的正确工具。也许,首先尝试简化/最小化所需的设置,例如:通过重构代码、设置更好的默认值、使用命名约定、更少的依赖关系、更好的自述设置文档。 我强烈认为完全在 docker build 命令中构建是一件很正常的事情,以至于我没想到他们不得不解释这一点

以上是关于Docker 中缓慢的 gradle 构建。缓存 gradle 构建的主要内容,如果未能解决你的问题,请参考以下文章

Flutter - 缓慢的 gradle 构建

为啥 Docker 构建命令在 Elastic Beanstalk 中运行如此缓慢?

如何在Kotlin中使用Gradle构建缓存?

Docker构建镜像过于缓慢解决-----Docker构建服务之部署和备份jekyll网站

Gradle项目构建docker镜像(支持Gradle多模块)

gradle 连续构建技巧在 docker 容器中不起作用