Spring Native no Google Cloud

1. Visão geral

Neste codelab, você aprenderá sobre o projeto Spring Native, criará um aplicativo que o usa e o implantará no Google Cloud.

Abordaremos os componentes dele, o histórico recente do projeto, alguns casos de uso e, obviamente, as etapas necessárias para usá-lo em seus projetos.

O projeto Spring Native está em fase experimental, então será necessária uma configuração específica para começar. No entanto, conforme anunciado no SpringOne 2021, o Spring Native está configurado para ser integrado ao Spring Framework 6.0 e ao Spring Boot 3.0 com suporte de primeira classe. Portanto, este é o momento ideal para ver mais detalhes do projeto alguns meses antes do lançamento.

Embora a compilação just-in-time tenha sido bem otimizada para itens como processos de longa duração, há alguns casos de uso em que apps compilados antecipadamente têm um desempenho ainda melhor, que será discutido durante o codelab.

Você aprenderá como realizar as seguintes tarefas:

  • Usar o Cloud Shell
  • Ative a API Cloud Run
  • Criar e implantar um app Spring Native
  • Implantar um aplicativo como esse no Cloud Run

Pré-requisitos

Pesquisa

Como você usará este tutorial?

Apenas leitura Leitura e exercícios

Como você classificaria sua experiência com Java?

Iniciante Intermediário Proficiente

Como você classificaria sua experiência de uso dos serviços do Google Cloud?

Iniciante Intermediário Proficiente

2. Contexto

O projeto Spring Native usa várias tecnologias para oferecer desempenho de aplicativos nativos aos desenvolvedores.

Para entender completamente a Spring Native, é útil entender algumas dessas tecnologias de componentes, o que elas nos permitem e como funcionam juntas aqui.

Compilação AOT

Quando os desenvolvedores executam o javac normalmente durante a compilação, nosso código-fonte .java é compilado em arquivos .class que são escritos em bytecode. Esse bytecode só pode ser compreendido pela máquina virtual Java. Portanto, a JVM precisará interpretar esse código em outras máquinas para executar o código.

Esse processo nos dá a portabilidade de assinatura do Java, o que nos permite "escrever uma vez e executar em qualquer lugar", mas é caro em comparação com a execução de código nativo.

Felizmente, a maioria das implementações da JVM usa a compilação just-in-time para reduzir esse custo de interpretação. Isso é feito contando as invocações de uma função e, se for invocado com frequência suficiente para transmitir um limite ( 10.000 por padrão), ele é compilado para código nativo no tempo de execução para evitar outras interpretações caras.

A compilação antecipada adota uma abordagem oposta, compilando todo o código acessível em um executável nativo durante a compilação. Isso comercializa a portabilidade para a eficiência da memória e outros ganhos de desempenho no tempo de execução.

5042e8e62a05a27.png

É claro que essa é uma decisão comercial nem sempre vale a pena ser tomada. No entanto, a compilação AOT pode brilhar em alguns casos de uso como:

  • Aplicativos de curta duração em que o tempo de inicialização é importante
  • Ambientes com muita memória e onde o JIT pode ser muito caro

Um fato divertido: a compilação AOT foi introduzida como um recurso experimental no JDK 9, embora essa implementação fosse cara e nunca fosse presa, por isso foi removida. em Java 17 em favor de desenvolvedores que usam apenas a GraalVM.

GraalVM (em inglês)

GraalVM é uma distribuição de JDK de código aberto altamente otimizada que conta com tempos de inicialização extremamente rápidos, compilação de imagem nativa AOT e recursos de polígono que permitem que os desenvolvedores combinem vários idiomas em um único aplicativo.

O GraalVM está em desenvolvimento ativo, ganhando novos recursos e melhorando os já existentes o tempo todo. Por isso, encorajo os desenvolvedores a ficarem atentos.

Alguns marcos recentes são:

  • Nova saída de versão de imagem nativa fácil de usar ( 2021-01-18)
  • Compatibilidade com Java 17 ( 18-01-2022)
  • Como ativar a compilação em várias camadas por padrão para melhorar os tempos de compilação de polígonos ( 2021-04-20)

Nativo da Spring

Simplificando: o Spring Native permite o uso do compilador de imagens nativas do GraalVM para transformar aplicativos Spring em executáveis nativos.

Esse processo envolve a realização de uma análise estática do aplicativo no momento da compilação para encontrar todos os métodos no aplicativo que podem ser acessados no ponto de entrada.

Isso basicamente cria uma concepção de mundo fechado do aplicativo, em que todo o código é considerado conhecido no momento da compilação e nenhum código novo pode ser carregado no tempo de execução.

É importante observar que a geração de imagens nativas é um processo que usa muita memória e leva mais tempo do que compilar um aplicativo normal e impõe limitações sobre determinados aspectos do Java.

Em alguns casos, nenhuma mudança de código é necessária para que um aplicativo funcione com o Spring Native. No entanto, algumas situações exigem configurações nativas específicas para funcionar corretamente. Nessas situações, o Spring Native geralmente fornece Dicas nativas para simplificar esse processo.

3. Configuração/Pré-trabalho

Antes de começar a implementar o Spring Native, precisaremos criar e implantar o app para estabelecer um valor de referência de desempenho que possa ser comparado à versão nativa posteriormente.

1. Criar o projeto

Começaremos com o download do app em start.spring.io:

curl https://start.spring.io/starter.zip -d dependencies=web \
           -d javaVersion=11 \
           -d bootVersion=2.6.4 -o io-native-starter.zip

Este app inicial usa a Spring Boot 2.6.4, que é a versão mais recente compatível com o projeto nativo da primavera no momento da gravação.

Desde o lançamento do GraalVM 21.0.3, também é possível usar o Java 17 para essa amostra. Neste tutorial, ainda usaremos o Java 11 para minimizar a configuração envolvida.

Quando o zip estiver na linha de comando, podemos criar um subdiretório para nosso projeto e descompactar a pasta nele:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. Alterações no código

Quando o projeto estiver aberto, adicionaremos rapidamente um sinal de vida e mostraremos o desempenho do Spring Native assim que o executarmos.

Edite o DemoApplication.java para corresponder a:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.Instant;

@RestController
@SpringBootApplication
public class DemoApplication {
    private static Instant startTime;
    private static Instant readyTime;

    public static void main(String[] args) {
        startTime = Instant.now();
                SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/")
    public String index() {
        return "Time between start and ApplicationReadyEvent: "
                + Duration.between(startTime, readyTime).toMillis()
                + "ms";
    }

    @EventListener(ApplicationReadyEvent.class)
    public void ready() {
                readyTime = Instant.now();
    }
}

Nosso app de referência está pronto. Portanto, fique à vontade para criar uma imagem e executá-la localmente para ter uma ideia do tempo de inicialização antes de convertê-la em um aplicativo nativo.

Para criar nossa imagem:

mvn spring-boot:build-image

Também é possível usar docker images demo para ter uma ideia do tamanho da imagem de referência: 6ecb403e9af1475e.png.

Para executar o app:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3. Implantar o app de referência

Agora que temos o app, ele será implantado e anotado com os horários, que será comparado aos tempos de inicialização do app nativo.

Dependendo do tipo de aplicativo que você está criando, há diversas opções de hospedagem do seu material.

No entanto, como nosso exemplo é um aplicativo da Web bem simples e direto, podemos simplificar tudo e confiar no Cloud Run.

Se você estiver fazendo o acompanhamento na sua máquina, instale e atualize a ferramenta CLI do gcloud.

Se você estiver no Cloud Shell, tudo isso será resolvido e você poderá simplesmente executar o seguinte no diretório de origem:

gcloud run deploy

4. Configuração do aplicativo

1. Configurar nossos repositórios do Maven

Como este projeto ainda está na fase experimental, será necessário configurar nosso aplicativo para encontrar artefatos experimentais, que não estão disponíveis no repositório central do Maven.

Isso envolverá a adição dos seguintes elementos ao nosso pom.xml, que você pode fazer no editor de sua escolha.

Adicione as seguintes seções de repositórios e pluginRepositories ao nosso pom:

<repositories>
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

2. Como adicionar dependências

Em seguida, adicione a dependência nativa da Spring, que é necessária para executar um aplicativo Spring como uma imagem nativa. Observação: essa etapa não será necessária se você estiver usando o Gradle.

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.11.2</version>
    </dependency>
</dependencies>

3. Adicionar/ativar nossos plug-ins

Agora, adicione o plug-in AOT para melhorar a compatibilidade e o tamanho da imagem nativa ( Saiba mais):

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-aot-maven-plugin</artifactId>
        <version>0.11.2</version>
        <executions>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

Agora, atualizaremos o spring-boot-maven-plugin para ativar a compatibilidade com imagens nativas e usar o builder paketo para criar nossa imagem nativa:

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                    <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
            </image>
        </configuration>
    </plugin>
</plugins>

Observe que a imagem do pequeno builder é apenas uma das várias opções. É uma boa opção para nosso caso de uso porque tem poucas bibliotecas e utilitários, o que ajuda a minimizar a superfície de ataque.

Por exemplo, se você estiver criando um app que precise de acesso a algumas bibliotecas C comuns ou ainda não tiver certeza dos requisitos dele, o builder completo talvez seja a melhor opção.

5. Criar e executar app nativo

Depois que tudo isso estiver configurado, será possível criar nossa imagem e executar o app compilado nativo.

Antes de fazer o build, tenha em mente o seguinte:

  • Isso levará mais tempo do que um build normal (alguns minutos) d420322893640701.png
  • Esse processo de compilação pode consumir muita memória (alguns gigabytes) cda24e1eb11fdbea.png
  • Esse processo de compilação exige que o daemon do Docker esteja acessível.
  • Neste exemplo, estamos realizando o processo manualmente, mas também é possível configurar as fases de compilação para acionar automaticamente um perfil de build nativo.

Para criar nossa imagem:

mvn spring-boot:build-image

Depois que ele for criado, vamos ver o app nativo em ação.

Para executar o app:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

No momento, estamos na posição ideal para ver os dois lados da equação do aplicativo nativo.

Otimizamos um pouco de tempo e o uso de memória extra durante a compilação, mas, em troca, conseguimos um aplicativo que pode iniciar muito mais rapidamente e consumir significativamente menos memória (dependendo da carga de trabalho).

Se executarmos docker images demo para comparar o tamanho da imagem nativa com a original, poderemos ver uma redução drástica:

e667f65a011c1328.png

Em casos de uso mais complexos, também é necessário fazer modificações adicionais para informar ao compilador AOT o que o app fará no momento da execução. Por esse motivo, algumas cargas de trabalho previsíveis (como jobs em lote) podem ser muito adequadas para isso, enquanto outras podem ser um aumento maior.

6. Como implantar nosso app nativo

Para implantar o app no Cloud Run, será preciso transferir a imagem nativa para um gerenciador de pacotes, como o Artifact Registry.

1. Preparar o repositório do Docker

Para começar esse processo, crie um repositório:

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

Em seguida, precisamos garantir que você seja autenticado para enviar ao nosso novo registro.

A CLI gcloud simplifica bastante esse processo:

gcloud auth configure-docker us-central1-docker.pkg.dev

2. Enviar nossa imagem para o Artifact Registry

Depois, vamos marcar nossa imagem:

export PROJECT_ID=$(gcloud config list --format 'value(core.project)')

docker tag  demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

Em seguida, podemos usar docker push para enviá-lo ao Artifact Registry:

docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

3. Como implantar no Cloud Run

Agora estamos prontos para implantar a imagem que armazenamos no Artifact Registry no Cloud Run:

gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

Como criamos e implantamos nosso app como uma imagem nativa, podemos ter certeza de que nosso app está fazendo um ótimo uso dos custos de infraestrutura durante a execução.

Fique à vontade para comparar o tempo de inicialização do nosso app de referência com o novo formato nativo.

6dde63d35959b1bb.png

7. Resumo/limpeza

Parabéns por criar e implantar um aplicativo Spring Native no Google Cloud.

Esperamos que este tutorial incentive você a se familiarizar com o projeto Spring Native e mantê-lo em mente caso ele atenda às suas necessidades no futuro.

Opcional: limpar e/ou desativar o serviço

Se você criou um projeto do Google Cloud para este codelab ou está reutilizando um existente, tome cuidado para evitar cobranças desnecessárias dos recursos que usamos.

É possível excluir ou desativar os serviços do Cloud Run que criamos, excluir a imagem hospedada ou encerrar todo o projeto.

8. Outros recursos

Embora o projeto Spring Native seja um projeto novo e experimental, ele já tem uma infinidade de recursos para ajudar os usuários iniciais a resolver problemas e participar:

Outros recursos

Veja abaixo recursos on-line que podem ser relevantes para este tutorial:

Licença

Este conteúdo está sob a licença Atribuição 2.0 Genérica da Creative Commons.