Nativo do Spring no Google Cloud

1. Visão geral

Neste codelab, vamos aprender sobre o projeto Spring Native, criar um app que o usa e implantá-lo no Google Cloud.

Vamos abordar os componentes, o histórico recente do projeto, alguns casos de uso e, claro, as etapas necessárias para você usá-lo nos seus projetos.

O projeto Spring Native está em fase experimental, então vai exigir uma configuração específica para começar. No entanto, conforme anunciado na SpringOne 2021, o Spring Native será integrado ao Spring Framework 6.0 e ao Spring Boot 3.0 com suporte de primeira classe. Portanto, este é o momento perfeito para analisar melhor o projeto alguns meses antes do lançamento.

Embora a compilação just-in-time tenha sido muito bem otimizada para coisas como processos de execução longa, há casos de uso em que os aplicativos compilados com antecedência têm um desempenho ainda melhor, o que vamos discutir durante o codelab.

Você vai aprender a

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

O que é necessário

Pesquisa

Como você vai 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 o Spring Native, é útil entender algumas dessas tecnologias de componentes, o que elas permitem e como elas funcionam juntas.

Compilação AOT

Quando os desenvolvedores executam o javac normalmente no momento da compilação, o código-fonte .java é compilado em arquivos .class, que são escritos em bytecode. Esse bytecode só pode ser entendido pela máquina virtual Java. Portanto, a JVM precisa interpretar esse código em outras máquinas para que possamos executá-lo.

Esse processo é o que nos dá a portabilidade de assinatura do Java, permitindo que você "escreva uma vez e execute em todos os lugares", 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. Se ela for invocada com frequência suficiente para passar de um limite ( 10.000 por padrão), ela será compilada para código nativo no momento da execução para evitar interpretações mais caras.

A compilação antecipada tem a abordagem oposta, compilando todo o código acessível em um executável nativo no momento da compilação. Isso troca a portabilidade pela eficiência da memória e outras melhorias de desempenho no momento da execução.

5042e8e62a05a27.png

É claro que isso é um trade-off, e nem sempre vale a pena. No entanto, a compilação AOT pode se destacar em determinados casos de uso, como:

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

Como curiosidade, a compilação AOT foi introduzida como um recurso experimental no JDK 9, mas essa implementação era cara de manter e nunca pegou. Por isso, ela foi removida silenciosamente no Java 17 para que os desenvolvedores usassem apenas a GraalVM.

GraalVM

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

O GraalVM está em desenvolvimento ativo, ganhando novos recursos e melhorando os existentes o tempo todo. Por isso, recomendo que os desenvolvedores fiquem atentos.

Alguns marcos recentes são:

  • Uma nova saída de build de imagem nativa fácil de usar ( 2021-01-18)
  • Suporte a Java 17 ( 2022-01-18)
  • Ativação da compilação em vários níveis por padrão para melhorar os tempos de compilação de poliglot ( 2021-04-20).

Spring Native

Em poucas palavras, o Spring Native permite o uso do compilador de imagem nativa do GraalVM para transformar aplicativos do 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 que podem ser acessados pelo ponto de entrada.

Isso cria essencialmente 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 momento de execução.

É importante observar que a geração de imagens nativas é um processo que consome muita memória, leva mais tempo do que compilar um aplicativo normal e impõe limitações a 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 uma configuração nativa específica 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, precisamos criar e implantar nosso app para estabelecer uma referência de desempenho que possa ser comparada com a versão nativa mais tarde.

1. Como criar o projeto

Vamos começar instalando o 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 o Spring Boot 2.6.4, que é a versão mais recente com suporte do projeto nativo do Spring no momento da escrita.

Desde o lançamento do GraalVM 21.0.3, você também pode usar o Java 17 para este exemplo. Ainda vamos usar o Java 11 para este tutorial para minimizar a configuração envolvida.

Depois de ter o arquivo zip na linha de comando, podemos criar um subdiretório para o projeto e descompactar a pasta nele:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. Mudanças no código

Depois de abrir o projeto, vamos adicionar rapidamente um sinal de vida e mostrar o desempenho do Spring Native quando ele for executado.

Edite o DemoApplication.java para que ele corresponda a este:

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();
    }
}

Neste ponto, nosso app de referência está pronto. Você pode criar uma imagem e executá-la localmente para ter uma ideia do tempo de inicialização antes de convertê-lo em um aplicativo nativo.

Para criar nossa imagem:

mvn spring-boot:build-image

Você também pode 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 nosso app, vamos implantá-lo e anotar os tempos, que serão comparados com os tempos de inicialização do app nativo mais tarde.

Dependendo do tipo de aplicativo que você está criando, há várias opções de hospedagem para seus recursos.

No entanto, como nosso exemplo é um aplicativo da Web muito simples e direto, podemos manter as coisas simples e usar o Cloud Run.

Se você estiver seguindo as instruções na sua máquina, verifique se a ferramenta CLI gcloud está instalada e atualizada.

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

gcloud run deploy

4. Configuração do aplicativo

1. Como configurar nossos repositórios Maven

Como esse projeto ainda está na fase experimental, vamos precisar configurar nosso app para encontrar artefatos experimentais, que não estão disponíveis no repositório central do Maven.

Isso envolve adicionar os seguintes elementos ao pom.xml, o que pode ser feito no editor de sua preferência.

Adicione as seções "repositories" e "pluginRepositories" ao 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 nossas dependências

Em seguida, adicione a dependência spring-native, que é necessária para executar um aplicativo Spring como uma imagem nativa. Observação: essa etapa não é 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. Como adicionar/ativar nossos plug-ins

Agora adicione o plug-in AOT para melhorar a compatibilidade e o espaço 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 vamos atualizar o spring-boot-maven-plugin para ativar o suporte a imagens nativas e usar o criador de pacotes 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>

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

Se, por exemplo, você estivesse criando um app que precisasse de acesso a algumas bibliotecas C comuns ou ainda não tivesse certeza dos requisitos do app, o builder completo poderia ser uma opção melhor.

5. Criar e executar um app nativo

Depois que tudo estiver pronto, vamos poder criar a imagem e executar o app nativo compilado.

Antes de executar o build, considere o seguinte:

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

Para criar nossa imagem:

mvn spring-boot:build-image

Depois de criar isso, vamos conferir o app nativo em ação.

Para executar o app:

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

Agora, temos uma ótima posição para conferir os dois lados da equação do aplicativo nativo.

Gastamos um pouco mais de tempo e memória no momento da compilação, mas, em troca, temos um aplicativo que pode ser iniciado muito mais rápido e consome muito menos memória (dependendo da carga de trabalho).

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

e667f65a011c1328.png

Também é importante observar que, em casos de uso mais complexos, há outras modificações necessárias para informar ao compilador AOT o que o app vai fazer 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 nosso app no Cloud Run, precisamos colocar nossa imagem nativa em um gerenciador de pacotes, como o Artifact Registry.

1. Como preparar nosso repositório do Docker

Podemos iniciar esse processo criando 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, vamos verificar se a autenticação para push foi feita no novo registro.

A CLI gcloud pode simplificar bastante esse processo:

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

2. Como enviar nossa imagem para o Artifact Registry

Em seguida, 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

E podemos usar docker push para enviar 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 armazenada 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 o app como uma imagem nativa, podemos ter certeza de que ele está usando muito bem os custos de infraestrutura durante a execução.

Compare os tempos de inicialização do nosso app de referência com os do novo app 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 mais com o projeto Spring Native e a considerar esse projeto para atender à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 projeto existente, tome cuidado para evitar cobranças desnecessárias dos recursos que usamos.

Você pode excluir ou desativar os serviços do Cloud Run que criamos, excluir a imagem que hospedamos ou encerrar todo o projeto.

8. Outros recursos

Embora o projeto Spring Native seja novo e experimental, já há muitos recursos para ajudar os primeiros usuários a resolver problemas e se envolver:

Outros recursos

Confira abaixo alguns 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.