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 analisar os componentes, o histórico recente do projeto, alguns casos de uso e, é claro, as etapas necessárias para usá-lo nos seus projetos.

O projeto Spring Native está em fase experimental. Portanto, ele exige 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 o projeto alguns meses antes do lançamento.

Embora a compilação just-in-time tenha sido muito bem otimizada para processos de longa duração, há alguns casos de uso em que os aplicativos compilados antecipadamente 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 nativo Spring
  • Implantar um app no Cloud Run

O que é necessário

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 apps nativos aos desenvolvedores.

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

Compilação AOT

Quando os desenvolvedores executam o javac normalmente no tempo de compilação, nosso código-fonte .java é compilado em arquivos .class gravados em bytecode. Esse bytecode só pode ser entendido pela Java Virtual Machine. Portanto, a JVM precisa interpretar esse código em outras máquinas para que possamos executar nosso código.

Esse processo é o que nos dá a portabilidade de assinatura do Java, permitindo que "escrevamos uma vez e executemos em qualquer lugar", mas é caro quando comparado à execução de código nativo.

Felizmente, a maioria das implementações da JVM usa a compilação just-in-time para atenuar esse custo de interpretação. Isso é feito contando as invocações de uma função e, se ela for invocada com frequência suficiente para passar um limite ( 10.000 por padrão), ela será compilada para código nativo no tempo de execução para evitar uma interpretação mais cara.

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

5042e8e62a05a27.png

Essa é uma troca e nem sempre vale a pena. No entanto, a compilação AOT pode brilhar em determinados casos de uso, como:

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

Como um fato divertido, a compilação AOT foi introduzida como um recurso experimental no JDK 9, embora essa implementação fosse cara de manter e nunca tenha sido muito usada. Portanto, ela foi removida silenciosamente no Java 17 em favor dos desenvolvedores que usam o GraalVM.

GraalVM

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

O GraalVM está em desenvolvimento ativo, ganhando novos recursos e melhorando os atuais 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 ( 18/01/2021)
  • Suporte ao Java 17 ( 18/01/2022)
  • Ativação da compilação de várias camadas por padrão para melhorar os tempos de compilação poliglota ( 20/04/2021)

Spring Native

Em termos simples, 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 execução de uma análise estática do aplicativo no tempo de compilação para encontrar todos os métodos no aplicativo que podem ser acessados pelo ponto de entrada.

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

É importante observar que a geração de imagens nativas é um processo com uso intensivo de memória que leva mais tempo do que a compilação de 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 um valor de referência de desempenho que possa ser comparado à versão nativa mais tarde.

1. Como criar o projeto

Vamos começar a receber nosso app de 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

Esse app inicial usa o Spring Boot 2.6.4, que é a versão mais recente com suporte do projeto spring-native no momento da redação.

Observe que, desde o lançamento do GraalVM 21.0.3, você também pode usar o Java 17 para esta amostra. Ainda vamos usar o Java 11 neste tutorial para minimizar a configuração envolvida.

Depois de ter o arquivo zip 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. 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 o executarmos.

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

Nesse momento, nosso app de linha de base está pronto. Portanto, crie uma imagem e execute-a localmente para ter uma ideia do tempo de inicialização antes de convertê-la em um app 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 linha de base: 6ecb403e9af1475e.png

Para executar nosso app:

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

3. Implantar o app de linha de base

Agora que temos nosso app, vamos implantá-lo e anotar os tempos, que serão comparados aos tempos de inicialização do app nativo mais tarde.

Dependendo do tipo de aplicativo que você está criando, há várias maneiras de hospedar seu conteúdo.

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

Se você estiver seguindo as instruções na sua própria máquina, instale e atualize a ferramenta da CLI gcloud.

Se você estiver no Cloud Shell, tudo será cuidado e você poderá simplesmente 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, precisamos 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 nosso pom.xml, que você pode fazer no editor de sua preferência.

Adicione as seções repositories 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 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 a pegada 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 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 builder tiny é apenas uma das várias opções. É uma boa opção para nosso caso de uso porque tem poucas bibliotecas e utilitários extras, o que ajuda a minimizar nossa 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 seria mais adequado.

5. Criar e executar o app nativo

Depois que tudo estiver no lugar, poderemos criar nossa imagem e executar nosso app nativo compilado.

Antes de executar o build, considere algumas coisas:

  • Isso vai levar mais tempo do que um build normal (alguns minutos) d420322893640701.png
  • Esse processo de build pode usar muita memória (alguns gigabytes) cda24e1eb11fdbea.png
  • Esse processo de build exige que o daemon do Docker seja acessível
  • Embora neste exemplo estejamos passando pelo processo manualmente, também é possível configurar as fases de build para acionar automaticamente um perfil de build nativo.

Para criar nossa imagem:

mvn spring-boot:build-image

Depois de criado, estamos prontos para ver o app nativo em ação.

Para executar nosso app:

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

Nesse momento, estamos em uma ótima posição para ver os dois lados da equação do app nativo.

Perdemos um pouco de tempo e uso extra da memória no tempo de compilação, mas, em troca, recebemos um aplicativo que pode ser iniciado 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, veremos uma redução drástica:

e667f65a011c1328.png

Também precisamos observar que, em casos de uso mais complexos, são necessárias outras modificações para informar ao compilador AOT o que o app fará no tempo de 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 mais difíceis.

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, precisamos verificar se estamos autenticados para enviar por push ao 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 então podemos usar docker push para enviá-la 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 aplicativo está fazendo um excelente uso dos custos de infraestrutura durante a execução.

Compare os tempos de inicialização do nosso app de linha de base com esse novo nativo.

6dde63d35959b1bb.png

7. Resumo/revisão dos dados

Parabéns por criar e implantar um app nativo Spring no Google Cloud.

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

Opcional: liberar espaço e/ou desativar o serviço

Se você criou um projeto na nuvem do Google Cloud para este codelab ou está reutilizando um projeto atual, evite cobranças desnecessárias dos recursos que usamos.

É possível 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 úteis para ajudar os primeiros usuários a resolver problemas e participar:

Outros recursos

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