Nativo do Spring no Google Cloud

1. Visão geral

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

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

O projeto nativo do Spring está atualmente em fase experimental, por isso requer algumas configurações específicas para ser iniciado. No entanto, conforme anunciado no SpringOne 2021, o Spring Native será integrado ao Spring Framework 6.0 e ao Spring Boot 3.0 com suporte de primeira classe. Portanto, esse é o momento perfeito para analisar o projeto alguns meses antes do lançamento.

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

Você vai aprender a

  • Usar o Cloud Shell
  • ativar a API Cloud Run
  • Criar e implantar um app nativo do Spring
  • 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 conhecer algumas dessas tecnologias de componentes, o que elas possibilitam e como funcionam juntas.

Compilação AOT

Quando os desenvolvedores executam javac normalmente durante a compilação, nosso código-fonte .java é compilado em arquivos .class, que são escritos em bytecode. Esse bytecode deve ser entendido apenas pela máquina virtual Java, portanto, a JVM terá que interpretar esse código em outras máquinas para podermos executá-lo.

É esse processo que nos dá a portabilidade característica do Java, o que nos permite "escrever uma vez e executar em todos os lugares", mas isso é 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. Para isso, basta contar as invocações de uma função. Se ela for invocada com frequência suficiente para transmitir um limite ( 10.000 por padrão), ela será compilado em código nativo em tempo de execução para evitar interpretações mais caras.

A compilação antecipada usa 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 outros ganhos de desempenho no ambiente de execução.

5042e8e62a05a27.png

É claro que essa é uma troca, e nem sempre vale a pena aceitar. No entanto, a compilação AOT pode se destacar em certos casos de uso, como:

  • Aplicativos de curta duração em que o tempo de inicialização é importante
  • Ambientes altamente limitados por memória em que o JIT pode ser caro demais

Como curiosidade, 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 bem pegada, por isso foi silenciosamente removida no Java 17 em favor dos desenvolvedores que usavam apenas o GraalVM.

GraalVM

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

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

Alguns marcos recentes:

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

Nativo da primavera

Simplificando: o Spring Native permite o uso do compilador de imagens nativas 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 tempo de compilação para encontrar todos os métodos que podem ser acessados do ponto de entrada.

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

É importante observar que a geração de imagens nativas é um processo que consome mais memória do que a compilação de um aplicativo normal e impõe limitações em certos aspectos do Java.

Em alguns casos, nenhuma alteração 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çarmos a implementar o Spring Native, precisamos criar e implantar nosso aplicativo para estabelecer um valor de referência de desempenho que podemos comparar com a versão nativa posteriormente.

1. Como criar o projeto

Vamos começar obtendo nosso aplicativo 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 da primavera (link em inglês) no momento da criação.

Desde o lançamento do GraalVM 21.0.3, também é possível usar o Java 17 para esse exemplo. Ainda usaremos o Java 11 neste tutorial para minimizar a configuração envolvida.

Depois de colocar o 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

Assim que abrirmos o projeto, adicionaremos rapidamente um sinal de vida e mostraremos o desempenho do Spring Native quando ele for executado.

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

Neste ponto, nosso app básico está pronto, então fique à vontade para 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

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

Para executar nosso app:

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

3. implantar um app de referência

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

Dependendo do tipo de aplicativo que você está criando, há diferentes formas de hospedar seu conteúdo.

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

Se você estiver acompanhando o processo no seu próprio computador, verifique se a ferramenta gcloud CLI está instalada e atualizada.

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

gcloud run deploy

4. Configuração do aplicativo

1. Como configurar os repositórios do 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 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 nossas dependências

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

Agora adicione o plug-in AOT para melhorar a compatibilidade e a abrangência da imagem nativa ( leia 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 plug-in spring-boot-maven-plugin para ativar o suporte a imagens nativas e usar o builder Paketo para criar a 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 tiny builder é apenas uma de 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 precisava de acesso a algumas bibliotecas C comuns ou ainda não conhecesse os requisitos do seu app, full-builder pode ser uma opção melhor.

5. Criar e executar app nativo

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

Antes de executar o build, considere o seguinte:

  • Isso vai levar mais tempo 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 exige que o daemon do Docker esteja acessível
  • Neste exemplo, o processo é feito 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 da criação, podemos ver o app nativo em ação.

Para executar nosso app:

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

Neste ponto, estamos em uma ótima posição para ver os dois lados da equação do aplicativo nativo.

Dissemos de um pouco de tempo e uso extra de memória no tempo de compilação, mas, em troca, temos 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

Também é importante notar que, em casos de uso mais complexos, são necessárias outras modificações para informar ao compilador AOT o que o app vai fazer no momento da execução. Por isso, certas cargas de trabalho previsíveis (como jobs em lote) podem ser muito adequadas para isso, enquanto outras podem apresentar um aumento maior.

6. Como implantar o app nativo

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

1. preparar o 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 confirmar se estamos autenticados para enviar ao nosso novo registro.

A CLI gcloud pode simplificar esse processo bastante:

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

2. enviar a imagem para o Artifact Registry

Em seguida, marcamos a 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 com tudo pronto para implantar no Cloud Run a imagem armazenada no Artifact Registry:

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

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

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

6dde63d35959b1bb.png

7. Resumo/limpeza

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

Esperamos que este tutorial incentive você a se familiarizar mais com o projeto Spring Native e considerá-lo 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 que já existe, 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 que hospedamos ou encerrar todo o projeto.

8. Outros recursos

Embora o projeto Spring Native atualmente seja um projeto novo e experimental, já existe uma grande quantidade de bons recursos para ajudar os primeiros usuários a solucionar 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.