Langsamer Gradle Build in Docker. Caching Gradle Build

8

Ich mache ein Universitätsprojekt, bei dem wir mehrere Spring Boot-Anwendungen gleichzeitig ausführen müssen.

Ich hatte bereits einen mehrstufigen Build mit Gradle Docker-Image konfiguriert und dann die App in openjdk: jre image ausgeführt.

Hier ist meine Docker-Datei:

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

Ich baue und betreibe alles mit Docker-Compose. Teil von Docker-Compose:

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

Natürlich dauert der erste Bau ewig. Docker zieht alle seine Abhängigkeiten. Und damit bin ich einverstanden.

Im Moment funktioniert alles in Ordnung, aber jede kleine Änderung des Codes führt zu einer Erstellungszeit von ca. 1 Minute für eine App.

Teil des Build-Protokolls: 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

Jedes Mal friert es danach ein Starting a Gradle Daemon (subsequent builds will be faster)

Ich habe darüber nachgedacht, Volumen mit zwischengespeicherten Gradle-Abhängigkeiten hinzuzufügen, aber ich weiß nicht, ob dies der Kern des Problems ist. Auch dafür konnte ich keine guten Beispiele finden.

Gibt es eine Möglichkeit, den Build zu beschleunigen?

PAwel_Z
quelle
Ich bin mit Java und Gradle nicht wirklich vertraut, aber ist es nicht das gleiche Verhalten wie in der lokalen Entwicklung? Ich meine, wenn Sie einige Änderungen an Ihrem Code vorgenommen haben, müssen Sie das Projekt neu kompilieren, um die Änderungen auch auf die Laufzeit anzuwenden. Vielleicht haben Sie damit gemeint, dass der Gradle das gesamte Projekt neu kompiliert, anstatt nur geänderte Teile?
Charlie
Gepostete Docker-Datei funktioniert in Ordnung, aber das Problem ist die Geschwindigkeit. Das lokale Bauen dauert ca. 8 Sekunden und in Docker ca. 1 bis 1,5 Minuten. Ich habe mich gefragt, ob es eine Möglichkeit gibt, den Docker-Build zu beschleunigen.
PAwel_Z

Antworten:

13

Das Erstellen nimmt viel Zeit in Anspruch, da Gradle jedes Mal, wenn das Docker-Image erstellt wird, alle Plugins und Abhängigkeiten herunterlädt.

Es gibt keine Möglichkeit, ein Volume zum Zeitpunkt der Image-Erstellung bereitzustellen. Es ist jedoch möglich, eine neue Phase einzuführen, in der alle Abhängigkeiten heruntergeladen und als Docker-Bildebene zwischengespeichert werden.

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

Das Gradle-Plugin und der Abhängigkeits-Cache befinden sich in $GRADLE_USER_HOME/caches. GRADLE_USER_HOMEmuss auf etwas anderes eingestellt sein als /home/gradle/.gradle. /home/gradle/.gradleIm übergeordneten Gradle Docker-Bild wird das Bild als Volumen definiert und nach jeder Bildebene gelöscht.

Im Beispiel wird der Code auf GRADLE_USER_HOMEgesetzt /home/gradle/cache_home.

In der builderPhase wird der Gradle-Cache kopiert, um ein erneutes Herunterladen der Abhängigkeiten zu vermeiden : COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle.

Die Bühne cachewird erst wieder aufgebaut, wenn sie build.gradlegeändert wird. Wenn Java-Klassen geändert werden, wird die zwischengespeicherte Bildebene mit allen Abhängigkeiten wiederverwendet.

Diese Änderungen können die Erstellungszeit verkürzen, aber eine sauberere Methode zum Erstellen von Docker-Images mit Java-Anwendungen ist Jib von Google. Es gibt ein Jib Gradle-Plugin , mit dem Container-Images für Java-Anwendungen erstellt werden können, ohne Dockerfile manuell erstellen zu müssen. Das Erstellen eines Images mit einer Anwendung und das Ausführen des Containers ähnelt:

gradle clean build jib
docker-compose up
Evgeniy Khyst
quelle
2
Ein mehrstufiger Build mit einer Bühne, die nur build.gradleaus dem Kontext stammt, ist definitiv der richtige Weg. Wenn Sie nur build.gradlein kopieren, stellen cacheSie sicher, dass Abhängigkeiten nur einmal heruntergeladen werden, wenn sich die Gradle-Builddatei nicht ändert (Docker verwendet den Cache erneut)
Pierre B.
4

Docker speichert seine Bilder in "Ebenen" zwischen. Jeder Befehl, den Sie ausführen, ist eine Ebene. Jede Änderung, die in einer bestimmten Ebene erkannt wird, macht die darauf folgenden Ebenen ungültig. Wenn der Cache ungültig ist, müssen die ungültigen Ebenen einschließlich der Abhängigkeiten von Grund auf neu erstellt werden .

Ich würde vorschlagen, Ihre Build-Schritte aufzuteilen. Haben Sie eine vorherige Ebene, die nur die Abhängigkeitsspezifikation in das Bild kopiert, und führen Sie dann einen Befehl aus, der dazu führt, dass Gradle die Abhängigkeiten herunterlädt. Nachdem das erledigt ist, kopieren Sie die Quelle in der gleichen Stelle , wo Sie das nur tat, und führen Sie den realen bauen.

Auf diese Weise werden die vorherigen Ebenen nur ungültig, wenn sich die Gradle-Dateien ändern.

Ich habe dies nicht mit Java / Gradle gemacht, aber ich bin dem gleichen Muster mit einem Rust-Projekt gefolgt, das von diesem Blog-Beitrag geleitet wird.

asthasr
quelle
1

Sie können versuchen, BuildKit zu verwenden (jetzt standardmäßig in der neuesten Docker-Compose 1.25 aktiviert ).

Siehe " Beschleunigen Sie die Erstellung Ihrer Java-Anwendung Docker-Images mit BuildKit! " Von Aboullaite Med .

(Dies war für Maven, aber die gleiche Idee gilt für Gradle)

Betrachten wir die folgende Docker-Datei:

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

Durch Ändern der zweiten Zeile wird der Maven-Cache aufgrund einer falschen Abhängigkeit immer ungültig, was zu einem ineffizienten Caching-Problem führt.

BuildKit löst diese Einschränkung, indem der gleichzeitige Build-Graph-Solver eingeführt wird, mit dem Build-Schritte parallel ausgeführt und Befehle optimiert werden können, die keinen Einfluss auf das Endergebnis haben.

Darüber hinaus verfolgt Buildkit nur die Aktualisierungen, die zwischen wiederholten Buildaufrufen an den Dateien vorgenommen wurden, um den Zugriff auf die lokalen Quelldateien zu optimieren. Sie müssen also nicht warten, bis lokale Dateien gelesen oder hochgeladen wurden, bevor die Arbeit beginnen kann.

VonC
quelle
Das Problem hat nicht mit dem Erstellen von Docker-Images zu tun, sondern mit dem Ausführen von Befehlen in der Dockerfile. Ich denke, es ist das Caching-Problem. Ich habe versucht, zwischenzuspeichern, aber es lädt immer noch Gradle usw. bei jedem einzelnen Lauf herunter. Ich habe auch verschiedene Kombinationen von Volumenzielen ausprobiert.
Neel Kamath
@NeelKamath "Ausführen von Befehlen in der Docker-Datei" ist Teil des "Erstellens von Docker-Images"! Und BuildKit wurde zum Zwischenspeichern von Builds und zum Beschleunigen von Docker-Builds entwickelt. Versuche es.
VonC
Die Verwendung von BuildKit allein löst dieses Problem nicht: Durch Kopieren des gesamten Kontexts zu Beginn des RUNBuilds und Verwenden von BuildKit wird bei jeder Codeänderung immer alles neu erstellt (da sich der Kontext geändert hat), jedoch zusätzlich mit der Antwort von @Evgeniy Khyst es könnte sich in Richtung eines besseren Ergebnisses bewegen
Pierre B.
@PierreB. OK. Jede Lösung wird also komplexer sein als ich dachte.
VonC
0

Ich weiß nicht viel über Docker-Interna, aber ich denke, dass das Problem darin besteht, dass jeder neue docker buildBefehl alle Dateien kopiert und erstellt (wenn er Änderungen in mindestens einer Datei erkennt). Dann werden höchstwahrscheinlich mehrere Gläser geändert, und die zweiten Schritte müssen ebenfalls ausgeführt werden.

Mein Vorschlag ist, auf dem Terminal (außerhalb von Docker) zu bauen und nur Docker das App-Image zu erstellen.

Dies kann sogar mit einem Gradle-Plugin automatisiert werden:

Vetras
quelle
Gradle Building im Docker ist also ein falscher Weg? Die Idee war, dass Sie keine Abhängigkeiten installieren müssen, um Code in Ihrer Umgebung zu erstellen und auszuführen.
PAwel_Z
Oh ich verstehe! Ich glaube nicht, dass Sie das in Ihrer Frage erwähnen. In diesem Fall scheint die aktuelle Lösung in Ordnung zu sein ... es wird einige Zeit dauern. Eine andere Frage ist, warum soll Ihr Entwickler nicht die Abhängigkeiten haben? Es wird Dev Env genannt, weil es Dev-Zeug enthält.
Vetras
Das ist ein guter Punkt. Ich sollte genauer sein. All dieser Docker in der Containerentwicklung wurde durch die Tatsache verursacht, dass das Projekt von etwa 10 Personen bearbeitet wird. Also dachte ich, ich wäre nett, keine Betriebssystem- oder SDK-Abhängigkeiten zu haben. Aber vielleicht ist das ein Overkill.
PAwel_Z
Nach meiner Erfahrung (Teams bis zu 6/7 Entwicklern) hat jeder das lokale Setup. Normalerweise befindet sich auf jedem Repo-Stammverzeichnis eine Readme-Datei mit den Schritten-Befehlen und allem, was für dieses Repository eingerichtet werden muss. Ich verstehe Ihr Problem, aber ich denke nicht, dass Docker das richtige Werkzeug dafür ist. Versuchen Sie möglicherweise zunächst, das erforderliche Setup zu vereinfachen / zu minimieren, z. B. durch Refactor-Code, Festlegen besserer Standardeinstellungen, Verwenden von Namenskonventionen, weniger Abhängigkeiten und bessere Readme-Setup-Dokumente.
Vetras
0

Wenn Ihre Internetverbindung langsam ist und jedes Mal Abhängigkeiten heruntergeladen werden, möchten Sie möglicherweise einen Sonatyp-Nexus einrichten, um die bereits heruntergeladenen Abhängigkeiten beizubehalten.

Cristian Cordova
quelle
0

Wie in den anderen Antworten erwähnt, speichert Docker jeden Schritt in einer Ebene zwischen. Wenn Sie irgendwie nur die heruntergeladenen Abhängigkeiten in eine Ebene bringen könnten, müsste sie nicht jedes Mal neu heruntergeladen werden, vorausgesetzt, die Abhängigkeiten haben sich nicht geändert.

Leider hat gradle keine eingebaute Aufgabe, um dies zu tun. Aber Sie können es trotzdem umgehen. Folgendes habe ich getan:

# 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

Stellen Sie außerdem sicher, dass Ihre .dockerignoreDatei mindestens diese Elemente enthält, damit sie beim Erstellen des Images nicht im Docker-Build-Kontext gesendet werden:

.gradle/
bin/
build/
gradle/
zwbetz
quelle