Gradle lento compilação no Docker. Armazenamento em cache gradle build

8

Estou fazendo um projeto universitário em que precisamos executar vários aplicativos Spring Boot ao mesmo tempo.

Eu já havia configurado a compilação em vários estágios com a imagem do gradle docker e, em seguida, execute o aplicativo na imagem openjdk: jre.

Aqui está o meu 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"]

Estou construindo e executando tudo com o docker-compose. Parte da janela de encaixe-compor:

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

É claro que a primeira compilação leva idades. O Docker está puxando todas as suas dependências. E eu estou bem com isso.

Tudo está funcionando bem por enquanto, mas cada pequena alteração no código causa cerca de 1 min de tempo de compilação para um aplicativo.

Parte do log de construção: 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

Toda vez que congela após Starting a Gradle Daemon (subsequent builds will be faster)

Eu estava pensando em adicionar volume com dependências gradle em cache, mas não sei se esse é o núcleo do problema. Também não consegui encontrar bons exemplos para isso.

Existe alguma maneira de acelerar a compilação?

PAwel_Z
fonte
Não estou realmente familiarizado com Java e Gradle, mas não é o mesmo comportamento do desenvolvimento local? Quero dizer, se você fez algumas alterações no seu código, precisará recompilar o projeto para aplicar as alterações também no tempo de execução. Talvez o que você quis dizer seja que o Gradle recompile todo o projeto em vez de apenas as partes alteradas?
Charlie
Publicado Dockerfile funciona bem, mas o problema é a velocidade. Construir localmente leva cerca de 8 segundos e no Docker ~ 1 a 1,5 minutos. Eu queria saber se existe uma maneira de acelerar a construção do docker.
PAwel_Z

Respostas:

14

O Build leva muito tempo porque o Gradle toda vez que a imagem do Docker é criada baixa todos os plugins e dependências.

Não há como montar um volume no tempo de criação da imagem. Mas é possível introduzir um novo estágio que fará o download de todas as dependências e será armazenado em cache como camada de imagem do 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"]

O plug-in Gradle e o cache de dependência estão localizados em $GRADLE_USER_HOME/caches. GRADLE_USER_HOMEdeve ser definido como algo diferente de /home/gradle/.gradle. /home/gradle/.gradlena imagem principal do Gradle Docker é definida como volume e é apagada após cada camada da imagem.

No código de amostra GRADLE_USER_HOMEestá definido como /home/gradle/cache_home.

Na builderfase de cache Gradle é copiado para evitar o download as dependências novamente: COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle.

O palco cacheserá reconstruído apenas quando build.gradlefor alterado. Quando as classes Java são alteradas, a camada de imagem em cache com todas as dependências é reutilizada.

Essas modificações podem reduzir o tempo de criação, mas a maneira mais limpa de criar imagens do Docker com aplicativos Java é o Jib do Google. Há um plug-in Jib Gradle que permite criar imagens de contêiner para aplicativos Java sem criar manualmente o Dockerfile. Criar imagem com o aplicativo e executar o contêiner é semelhante a:

gradle clean build jib
docker-compose up
Evgeniy Khyst
fonte
2
A construção de vários estágios com um estágio incluindo apenas build.gradledo contexto é definitivamente o caminho a percorrer. Copiando apenas build.gradleem cachevocê garante dependências só será transferido uma vez, se o arquivo de construção Gradle não muda (Docker vai voltar a usar o cache)
Pierre B.
4

O Docker armazena em cache suas imagens em "camadas". Cada comando que você executa é uma camada. Cada alteração detectada em uma determinada camada invalida as camadas que vêm depois dela. Se o cache for invalidado, as camadas invalidadas deverão ser criadas do zero, incluindo dependências .

Eu sugeriria dividir suas etapas de compilação. Tenha uma camada anterior que copie apenas a especificação de dependência na imagem e execute um comando que resultará no download das dependências pelo Gradle. Depois de concluído, copie sua fonte no mesmo local em que você acabou de fazer isso e execute a compilação real.

Dessa forma, as camadas anteriores serão invalidadas apenas quando os arquivos de gradação forem alterados.

Não fiz isso com Java / Gradle, mas segui o mesmo padrão em um projeto Rust, guiado por esta postagem no blog.

asthasr
fonte
1

Você pode tentar usar o BuildKit (agora ativado por padrão na última janela de encaixe-composição 1.25 )

Consulte " Acelere as imagens do Docker do aplicativo java construídas com o BuildKit! " Da Aboullaite Med .

(Isso foi para maven, mas a mesma idéia se aplica a gradle)

vamos considerar o seguinte Dockerfile:

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

A modificação da segunda linha sempre invalida o cache do maven devido à falsa dependência, que expõe um problema ineficiente do cache.

O BuildKit resolve essa limitação apresentando o solucionador de gráfico de compilação simultâneo, que pode executar etapas de compilação paralelamente e otimizar comandos que não afetam o resultado final.

Além disso, o Buildkit rastreia apenas as atualizações feitas nos arquivos entre chamadas de compilação repetidas que otimizam o acesso aos arquivos de origem locais. Portanto, não é necessário aguardar a leitura ou o upload de arquivos locais antes que o trabalho possa começar.

VonC
fonte
O problema não tem a ver com a construção de imagens do Docker, mas com a execução de comandos no Dockerfile. Eu acho que é a questão do cache. Eu tentei fazer o cache, mas ele ainda baixa o Gradle etc. a cada execução. Eu tentei diferentes combinações de destinos de volume também.
Neel Kamath
@NeelKamath "executar comandos no Dockerfile" faz parte da "construção de imagens do Docker"! E o BuildKit foi criado para armazenar em cache e acelerar a construção de janelas de encaixe. De uma chance.
VonC 22/11/19
Usar o BuildKit sozinho não resolverá esse problema: copiando todo o contexto no início da compilação e usando RUN, o BuildKit sempre recriará tudo em cada alteração de código (porque o contexto mudou), mas além da resposta do @Evgeniy Khyst ela pode se mover em direção a um resultado melhor
Pierre B.
@PierreB. ESTÁ BEM. Portanto, qualquer solução será mais complexa do que eu pensava.
VonC 26/11/19
0

Não sei muito sobre o docker internals, mas acho que o problema é que cada novo docker buildcomando copia todos os arquivos e os cria (se detectar alterações em pelo menos um arquivo). Então isso provavelmente mudará vários frascos e os segundos passos também precisam ser executados.

Minha sugestão é construir no terminal (fora da janela de encaixe) e apenas a janela de encaixe construir a imagem do aplicativo.

Isso pode até ser automatizado com um plugin gradle:

Vetras
fonte
Então, gradle edifício no docker é um caminho errado? A idéia era que você não precisará de nenhuma dependência instalada para criar e executar o código em seu ambiente.
PAWEL_Z
Ah eu vejo! Eu não acho que você mencionou isso na sua pergunta. Nesse caso, parece que a solução atual está correta ... levará tempo. Outra pergunta é: por que você deseja que seu env dev não tenha dependências? é chamado de dev env porque terá coisas de dev nele.
Vetras 7/11
Este é um bom ponto. Eu deveria ser mais específico. Toda essa janela de encaixe no desenvolvimento de contêiner foi causada pelo fato de o projeto estar sendo editado por 10 pessoas. Por isso, pensei que seria bom não ter nenhuma dependência de sistema operacional ou sdk. Mas talvez isso seja um exagero.
PAwel_Z
Na minha experiência (equipes de até 6/7 desenvolvedores), todos têm a configuração local. Geralmente, há um arquivo leia-me em cada raiz de repositório com os comandos das etapas e tudo o que precisa de configuração para esse repositório. Entendo seu problema, mas não acho que a janela de encaixe seja a ferramenta certa para isso. Talvez, tente simplificar / minimizar a configuração necessária em primeiro lugar, por exemplo: refatorar o código, definir padrões melhores, usar convenções de nomenclatura, menos dependências, melhores documentos de configuração do leia-me.
Vetras 8/11/19
0

Assim como as respostas de outras pessoas, se a sua conexão à Internet estiver lenta, pois ele faz o download de dependências todas as vezes, convém configurar o sonatype nexus, a fim de manter as dependências já baixadas.

Cristian Cordova
fonte
0

Como as outras respostas mencionaram, a janela de encaixe armazena em cache cada etapa de uma camada. Se você pudesse, de alguma forma, obter apenas as dependências baixadas em uma camada, não seria necessário fazer o download novamente a cada vez, assumindo que as dependências não foram alteradas.

Infelizmente, o gradle não tem uma tarefa interna para fazer isso. Mas você ainda pode contornar isso. Aqui está o que eu fiz:

# 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

Além disso, verifique se o .dockerignorearquivo possui pelo menos esses itens, para que não sejam enviados no contexto de construção da janela de encaixe quando a imagem for criada:

.gradle/
bin/
build/
gradle/
zwbetz
fonte