Construcción lenta de gradle en Docker. Almacenamiento en caché de gradle

8

Estoy haciendo un proyecto universitario donde necesitamos ejecutar múltiples aplicaciones Spring Boot a la vez.

Ya había configurado la compilación de varias etapas con la imagen de gradle docker y luego ejecuté la aplicación en openjdk: jre image.

Aquí está mi 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"]

Estoy construyendo y ejecutando todo con docker-compose. Parte de docker-compose:

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

Por supuesto, la primera construcción lleva años. Docker está sacando todas sus dependencias. Y estoy de acuerdo con eso.

Todo está funcionando bien por ahora, pero cada pequeño cambio en el código causa alrededor de 1 minuto de tiempo de compilación para una aplicación.

Parte del registro de compilación: 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

Cada vez que se congela después Starting a Gradle Daemon (subsequent builds will be faster)

Estaba pensando en agregar volumen con dependencias de gradle en caché, pero no sé si ese es el núcleo del problema. Además, no pude encontrar buenos ejemplos para eso.

¿Hay alguna forma de acelerar la construcción?

PAwel_Z
fuente
No estoy realmente familiarizado con Java y Gradle, pero ¿no es el mismo comportamiento que en el desarrollo local? Es decir, si realizó algunos cambios en su código, debe volver a compilar el proyecto para aplicar los cambios también en el tiempo de ejecución. ¿Quizás lo que quisiste decir es que Gradle recompila todo el proyecto en lugar de solo cambiar las partes?
Charlie
El Dockerfile publicado funciona bien pero el problema es la velocidad. Construir localmente toma ~ 8 segundos y en Docker ~ 1 a 1,5 minutos. Me preguntaba si hay una manera de acelerar la construcción de Docker.
PAwel_Z

Respuestas:

14

La compilación lleva mucho tiempo porque Gradle cada vez que se construye la imagen de Docker descarga todos los complementos y dependencias.

No hay forma de montar un volumen en el momento de creación de la imagen. Pero es posible introducir una nueva etapa que descargará todas las dependencias y se almacenará en caché como capa de imagen de 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"]

El complemento Gradle y la caché de dependencia se encuentran en $GRADLE_USER_HOME/caches. GRADLE_USER_HOMEdebe establecerse en algo diferente a /home/gradle/.gradle. /home/gradle/.gradleen la imagen principal de Gradle Docker se define como volumen y se borra después de cada capa de imagen.

En el código de muestra GRADLE_USER_HOMEse establece en /home/gradle/cache_home.

En la builderetapa de caché Gradle se copia para evitar la descarga de las dependencias de nuevo: COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle.

El escenario cachese reconstruirá solo cuando build.gradlese cambie. Cuando las clases Java son cambios, la capa de imagen en caché con todas las dependencias se reutiliza.

Esta modificación puede reducir el tiempo de construcción, pero de forma más limpia de la construcción de imágenes acoplables con las aplicaciones Java es la horca por Google. Hay un complemento Jib Gradle que permite crear imágenes de contenedor para aplicaciones Java sin crear manualmente Dockerfile. Crear una imagen con la aplicación y ejecutar el contenedor es similar a:

gradle clean build jib
docker-compose up
Evgeniy Khyst
fuente
2
La construcción de varias etapas con una etapa que solo incluye el build.gradlecontexto es definitivamente el camino a seguir. Al copiar solo build.gradleen cacheusted, se asegura de que las dependencias solo se descargarán una vez si el archivo de compilación de Gradle no cambia (Docker volverá a usar el caché)
Pierre B.
4

Docker almacena en caché sus imágenes en "capas". Cada comando que ejecuta es una capa. Cada cambio que se detecta en una capa dada invalida las capas que le siguen. Si la memoria caché está invalidada, las capas invalidadas deben construirse desde cero, incluidas las dependencias .

Sugeriría dividir sus pasos de compilación. Tenga una capa anterior que solo copie la especificación de dependencia en la imagen, luego ejecute un comando que hará que Gradle descargue las dependencias. Una vez que esté completo, copie su fuente en la misma ubicación donde acaba de hacer eso y ejecute la compilación real.

De esta manera, las capas anteriores se invalidarán solo cuando cambien los archivos gradle.

No he hecho esto con Java / Gradle, pero he seguido el mismo patrón con un proyecto Rust, guiado por esta publicación de blog.

asthasr
fuente
1

Puede probar y usar BuildKit (ahora activado por defecto en la última versión de Docker -compose 1.25 )

Consulte " Acelere la creación de imágenes Docker de su aplicación java con BuildKit! " De Aboullaite Med .

(Esto fue para Maven, pero la misma idea se aplica a Gradle)

Consideremos el siguiente Dockerfile:

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

La modificación de la segunda línea siempre invalida la memoria caché de Maven debido a una falsa dependencia, lo que expone un problema de almacenamiento en caché ineficiente.

BuildKit resuelve esta limitación al introducir el solucionador de gráficos de compilación concurrente, que puede ejecutar los pasos de compilación en paralelo y optimizar los comandos que no tienen un impacto en el resultado final.

Además, Buildkit rastrea solo las actualizaciones realizadas a los archivos entre invocaciones de compilación repetidas que optimizan el acceso a los archivos de origen locales. Por lo tanto, no es necesario esperar a que los archivos locales se lean o carguen antes de que el trabajo pueda comenzar.

VonC
fuente
El problema no tiene que ver con la creación de imágenes de Docker, sino con la ejecución de comandos en el Dockerfile. Creo que es el problema del almacenamiento en caché. He intentado el almacenamiento en caché pero todavía descarga Gradle, etc. en cada ejecución. También probé diferentes combinaciones de destinos de volumen.
Neel Kamath
¡@NeelKamath "ejecutar comandos en el Dockerfile" es parte de "construir imágenes Docker"! Y BuildKit está hecho para almacenar en caché y acelerar las construcciones de acopladores. Darle una oportunidad.
VonC
Usar BuildKit solo no resolverá este problema: al copiar todo el contexto al comienzo de la compilación y usar RUN, BuildKit siempre reconstruirá todo en cada cambio de código (porque el contexto cambió), pero además con la respuesta @Evgeniy Khyst puede avanzar hacia un mejor resultado
Pierre B.
@PierreB. OKAY. Entonces, cualquier solución será más compleja de lo que pensaba.
VonC
0

No sé mucho sobre los componentes internos de Docker, pero creo que el problema es que cada nuevo docker buildcomando copiará todos los archivos y los compilará (si detecta cambios en al menos un archivo). Entonces, lo más probable es que esto cambie varios frascos y los segundos pasos también deben ejecutarse.

Mi sugerencia es construir en la terminal (fuera de Docker) y solo Docker construye la imagen de la aplicación.

Esto incluso se puede automatizar con un complemento de gradle:

Vetras
fuente
Entonces, ¿construir gradle en docker es un camino equivocado? La idea era que no necesitaría ninguna dependencia instalada para compilar y ejecutar código en su entorno.
PAwel_Z
¡Oh ya veo! No creo que menciones eso en tu pregunta. En ese caso, parece que la solución actual está bien ... llevará tiempo. Otra pregunta es, ¿por qué quieres que tu entorno de desarrollo no tenga las dependencias? se llama dev env porque tendrá cosas de desarrollo.
Vetras
Ese es un buen punto. Debería ser más específico. Todo ese estibador en el desarrollo de contenedores fue causado por el hecho de que 10 personas están editando el proyecto. Así que pensé que sería bueno no tener ninguna dependencia del sistema operativo o SDK. Pero tal vez eso sea una exageración.
PAwel_Z
En mi experiencia (equipos de hasta 6/7 desarrolladores) todos tienen la configuración local. Por lo general, hay un archivo Léame en cada raíz de repositorio con los comandos de pasos y todo lo que necesita configuración para ese repositorio. Entiendo su problema, pero no creo que Docker sea la herramienta adecuada para esto. Tal vez, intente simplificar / minimizar la configuración necesaria en primer lugar, por ejemplo: ya sea por código de refactorización, estableciendo mejores valores predeterminados, usando convenciones de nomenclatura, menos dependencias, mejores documentos de configuración de archivo Léame.
Vetras
0

Del mismo modo que responde una adición a otras personas, si su conexión a Internet es lenta, ya que descarga dependencias cada vez, es posible que desee configurar sonatype nexus, para mantener las dependencias ya descargadas.

Cristian Cordova
fuente
0

Como las otras respuestas han mencionado, la ventana acoplable almacena en caché cada paso de una capa. Si de alguna manera pudiera obtener solo las dependencias descargadas en una capa, entonces no tendría que volver a descargarse cada vez, suponiendo que las dependencias no hayan cambiado.

Desafortunadamente, gradle no tiene una tarea incorporada para hacer esto. Pero aún puedes solucionarlo. Esto es lo que hice:

# 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

Además, asegúrese de que su .dockerignorearchivo tenga al menos estos elementos, para que no se envíen en el contexto de compilación de la ventana acoplable cuando se genera la imagen:

.gradle/
bin/
build/
gradle/
zwbetz
fuente