DockerでのGradleビルドが遅い。Gradleビルドのキャッシュ


8

複数の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つのアプリのビルド時間は約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のビルドを高速化する方法があるかどうか疑問に思っていました。
PAwel_Z

回答:


13

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/cachesます。GRADLE_USER_HOMEとは異なる値に設定する必要があります/home/gradle/.gradle/home/gradle/.gradle親のGradle Dockerイメージではボリュームとして定義され、各イメージレイヤーの後に消去されます。

サンプルコードでGRADLE_USER_HOMEはに設定されてい/home/gradle/cache_homeます。

ではbuilder、ステージのGradleキャッシュは再び依存関係をダウンロードしないようにコピーされますCOPY --from=cache /home/gradle/cache_home /home/gradle/.gradle

ステージは、変更されたcache場合にのみ再構築さbuild.gradleれます。Javaクラスが変更されると、すべての依存関係を持つキャッシュされたイメージレイヤーが再利用されます。

この変更によりビルド時間を短縮できますが、JavaアプリケーションでDockerイメージをビルドするよりクリーンな方法は、Jib by Googleです。ありジブのGradleプラグインを手動Dockerfileを作成することなく、Javaアプリケーション用のコンテナイメージを構築することができます。アプリケーションを使用してイメージを作成し、コンテナーを実行する方法は次のようになります。

gradle clean build jib
docker-compose up

2
build.gradleコンテキストからのみを含むステージを使用したマルチステージビルドは、間違いなく進むべき道です。自分だけbuild.gradleをコピーcacheすることで、Gradleビルドファイルが変更されない場合(Dockerがキャッシュを再利用する場合)に依存関係が1回だけダウンロードされることを確認します
Pierre B.

4

Dockerはそのイメージを「レイヤー」にキャッシュします。実行する各コマンドはレイヤーです。特定のレイヤーで検出された各変更は、それ以降のレイヤーを無効にします。キャッシュが無効化されている場合、無効化されたレイヤーは、依存関係を含め、ゼロから構築する必要があります。

ビルドステップを分割することをお勧めします。依存関係の仕様のみをイメージにコピーする前のレイヤーを用意し、Gradleが依存関係をダウンロードするコマンドを実行します。それが完了したら、ソースを同じ場所にコピーして、実際のビルドを実行します。

これにより、gradleファイルが変更された場合にのみ、以前のレイヤーが無効になります。

私はJava / Gradleでこれを行っていませんが、このブログの投稿に基づいて、Rustプロジェクトで同じパターンに従っています。


1

BuildKitを試して使用できます(現在、最新のdocker-compose 1.25ではデフォルトでアクティブになっています)

Aboullaite MedのJavaアプリケーションのスピードアップ、BuildKitを使用したDockerイメージのビルド」を参照してください

(これはmaven用でしたが、gradleにも同じ考え方が当てはまります)

次のDockerfileについて考えてみましょう。

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

2行目を変更すると、誤った依存関係のために常にMavenキャッシュが無効になり、非効率的なキャッシュの問題が発生します。

BuildKitは、並行ビルドグラフソルバーを導入することでこの制限を解決します。これにより、ビルドステップを並列に実行し、最終結果に影響を与えないコマンドを最適化できます。

さらに、Buildkitは、ローカルソースファイルへのアクセスを最適化する、繰り返されるビルド呼び出しの間にファイルに対して行われた更新のみを追跡します。したがって、作業を開始する前にローカルファイルが読み取られるかアップロードされるのを待つ必要はありません。


問題はDockerイメージの構築に関係するのではなく、でコマンドを実行することDockerfileです。キャッシュの問題だと思います。キャッシュを試しましたが、実行ごとにGradleなどがダウンロードされます。ボリュームの宛先のさまざまな組み合わせも試しました。
Neel Kamath

@NeelKamath「Dockerfileでコマンドを実行する」は「Dockerイメージの構築」の一部です。また、BuildKitは、ビルドをキャッシュし、Docker ビルドを高速化するために作成さています。試してみる。
VonC、

BuildKitを単独で使用してもこの問題は解決されません。ビルドの開始時にコンテキスト全体をコピーしてを使用することによりRUN、BuildKitはコードが変更されるたびに常にすべてを再構築します(コンテキストが変更されたため)、さらに@Evgeniy Khystの回答より良い結果になる可能性があります
ピエールB.

@PierreB。OK。したがって、どのソリューションも思ったよりも複雑になります。
VonC

0

私はドッカーの内部についてはあまり知りませんが、問題は新しいdocker buildコマンドごとにすべてのファイルをコピーしてビルドすることです(少なくとも1つのファイルで変更が検出された場合)。その後、これはいくつかのjarを変更する可能性が高く、2番目のステップも実行する必要があります。

私の提案は、ターミナル(Dockerの外)でビルドし、Dockerのみがアプリイメージをビルドすることです。

これはgradleプラグインで自動化することもできます:


では、DockerでのGradleビルドは間違った方法ですか?環境でコードをビルドして実行するために依存関係をインストールする必要はないという考えでした。
PAwel_Z

ああなるほど!あなたの質問であなたがそれを言及するとは思わない。その場合、現在のソリューションは問題ないようです...時間がかかります。もう1つの質問は、開発環境に依存関係を持たせたくないのはなぜですか?開発環境が含まれるため、開発環境と呼ばれます。
Vetras

それは良い点です。もっと具体的に。コンテナー開発におけるそのすべてのDockerは、プロジェクトが10人程度のユーザーによって編集されているという事実によって引き起こされました。そのため、OSやSDKの依存関係がないことがいいと思いました。しかし、多分それはやり過ぎです。
PAwel_Z

私の経験では(6/7の開発者までのチーム)誰もがローカルセットアップを持っています。通常、各リポジトリーのルートには、そのリポジトリーの、stepsコマンドとセットアップが必要なすべてを含むreadmeファイルがあります。私はあなたの問題を理解していますが、Dockerがこのための適切なツールだとは思いません。たとえば、最初に必要な設定を単純化/最小化してみてください。たとえば、コードのリファクタリング、より適切なデフォルトの設定、命名規則の使用、依存関係の減少、Readme設定ドキュメントの改善などです。
Vetras

0

他の人への追加が答えるように、インターネット接続が遅い場合、依存関係を毎回ダウンロードするため、すでにダウンロードされている依存関係を維持するためにsonatypeネクサスを設定することができます。


0

他の答えが述べたように、ドッカーはレイヤーの各ステップをキャッシュします。ダウンロードされた依存関係のみを何らかの方法でレイヤーに取得できる場合、依存関係が変更されていないと想定して、毎回再ダウンロードする必要はありません。

残念ながら、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ビルドコンテキストで送信されないように、ファイルに少なくとも次のアイテムが含まれていることを確認してください。

.gradle/
bin/
build/
gradle/
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.