Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Back-ends Modernos em Java: Reatividade, Observabilidade e Performance

Back-ends Modernos em Java: Reatividade, Observabilidade e Performance

Fundamentos do Reativo e Quarkus - Apresentação

Apresentando o curso e objetivos

Olá! Meu nome é João Vitor e serei o instrutor deste curso que visa alavancar nossa carreira como profissionais seniores. Neste curso, trabalharemos com a aplicação Java utilizando Quarkus e exploraremos muitos recursos que nos colocarão em destaque em nossa trajetória profissional.

Iniciaremos com a oportunidade de utilizar recursos como o modelo reativo em uma aplicação Quarkus, onde teremos a capacidade de aproveitar toda a parte do muting, bem como o Vertex, que é o servidor de aplicação nativo do Quarkus e também é reativo. Com o muting, poderemos trabalhar com métodos que, à primeira vista, parecem complexos, mas que, ao compreendermos a sintaxe, tornam-se elegantes e resultam em uma economia significativa de recursos, como threads e desempenho, tornando nossos sistemas muito mais escaláveis, especialmente em cenários de alto tráfego de rede em grandes companhias.

Explorando o uso de cache

Além disso, teremos a oportunidade de trabalhar com cache. Vamos entender em quais cenários o cache é eficiente, faz sentido e como implementá-lo. Aprenderemos a armazenar informações em cache, a removê-las quando não forem mais necessárias e a desenvolver estratégias de cache que façam sentido em nossa aplicação. Otimizaremos um dos caches mais renomados do mercado atualmente, o Redis, e exploraremos o cache distribuído.

Melhorando o tempo de inicialização da aplicação

Também abordaremos a necessidade de uma aplicação que tenha um desempenho excelente no momento do startup. Em alguns casos, o tempo de inicialização pode ser considerável, dependendo do tamanho da aplicação e da JVM utilizada. Em cenários que exigem um startup mais rápido, apresentaremos a opção de trabalhar com imagem nativa, combinando Quarkus com GraalVM para criar um binário da aplicação que funciona quase como um executável, permitindo que a aplicação seja iniciada instantaneamente.

Discutindo a importância da observabilidade

Além disso, discutiremos a questão da observabilidade, que é essencial para profissionais que desejam alcançar ou já estão na carreira sênior e buscam excelência. Analisaremos cenários de observabilidade na aplicação, como o uso do Prometheus para exportar métricas. Também trabalharemos com métricas do Matter Registry, do Micrometer, e aprenderemos a criar nossas próprias métricas, tanto de negócio quanto de aplicação, como CPU e memória.

Hoje, vamos configurar nosso próprio Prometheus para explorar suas funcionalidades, criar gráficos e verificar as métricas de nossa aplicação. Além disso, trabalharemos com logs, que são fundamentais para um conjunto completo de observabilidade, incluindo logs, métricas e tracing (rastreamento).

Implementando tracing com OpenTelemetry

Falando em tracing, utilizaremos o OpenTelemetry para rastrear nossa aplicação. Configuraremos o collector, que coletará as métricas da aplicação, e exportaremos para o Jaeger. Tudo isso será feito em containers, permitindo expor nossos traces de forma visual, com uma interface gráfica, para entender o fluxo de uma requisição, desde sua origem até o destino. Isso nos permitirá acompanhar exemplos reais, como quando uma API chamada pelo sistema está fora do ar, identificando exatamente onde a requisição falhou. Essa capacidade é extremamente útil para investigações diárias, especialmente em situações de incidentes, tornando a resolução de problemas mais rápida e precisa.

Realizando análises profundas da aplicação

Além disso, abordaremos tópicos como reatividade, Raw VM e observabilidade. Exploraremos também análises mais profundas da aplicação, como profiling. Verificaremos, por exemplo, se há vazamentos de memória ou threads ociosas. Utilizaremos ferramentas como o Visual VM e o JDK Mission, que são nativas do Java, para realizar essas análises. Avaliaremos o comportamento das threads, possíveis vazamentos de memória e a execução do garbage collector, que pode indicar problemas na aplicação ao salvar objetos na memória.

Convidando para o curso

Realizaremos uma análise detalhada de todos esses aspectos, essenciais para que uma pessoa desenvolvedora esteja capacitada a atuar em um nível sênior. Se tudo isso é interessante para você, convidamos a participar deste curso. Estamos confiantes de que, ao aprofundarmos nos temas, o interesse aumentará e a vontade de continuar até o final da jornada será cada vez maior.

Nos vemos no próximo vídeo.

Fundamentos do Reativo e Quarkus - Reatividade com Quarkus

Discutindo o sistema reativo e bibliotecas disponíveis

Até agora, discutimos como funciona um sistema reativo. Abordamos a otimização de recursos, o melhor uso de threads e outras vantagens de utilizar esse recurso, especialmente em alta escala. Mencionamos que, para alcançar resultados reativos em aplicações Java, temos várias bibliotecas disponíveis, como o Project Reactor, o RxJava, o Multi e o Vertex, que, quando utilizados em conjunto, fornecem toda a funcionalidade reativa.

Durante o curso, utilizaremos uma aplicação Quarkus e os recursos que ela oferece para trabalhar com reatividade. O Quarkus, por exemplo, já utiliza o Vertex, um servidor de aplicação nativamente reativo. Quando começarmos a parte prática, o código já estará estruturado com o Quarkus.

Entendendo os fundamentos técnicos do reativo em Quarkus e Java

Antes de nos aprofundarmos na parte técnica e no código, vamos entender os fundamentos técnicos do reativo em Quarkus e Java. Já discutimos os benefícios do reativo, que várias bibliotecas oferecem, mas o Quarkus possui especificidades que não estão presentes no Project Reactor, no WebFlux ou em outros projetos que fornecem o paradigma reativo.

No Quarkus, trabalharemos com retornos que serão um UNI ou um MULTI de algo. No modelo imperativo, ao trabalhar com Java, por exemplo, se quisermos retornar uma lista de pessoas ou uma pessoa através de um findById, teríamos um método public pessoa findById, passando o ID como parâmetro. Agora, há uma diferença nesses retornos: eles passam a ser um UNI de pessoa.

Explicando o conceito de UNI e MULTI

Para ilustrar, o UNI representa um resultado assíncrono único:

Uni: representa um resultado assíncrono único

O UNI e o MULTI podem ser vistos como uma promessa de algo. O Quarkus nos oferece a promessa de que haverá um retorno, pois estamos falando de reatividade. No paradigma imperativo, a requisição ficava esperando, presa, até retornar uma pessoa ou um erro. Agora, a thread faz a requisição e continua livre para buscar outra requisição, enquanto o event loop garante que a resposta ocorrerá.

As promises se tornaram famosas no JavaScript, e o UNI representa algo semelhante. A diferença entre UNI e MULTI é que o UNI representa um resultado assíncrono único, enquanto o MULTI representa um fluxo assíncrono de dados:

Multi: representa um fluxo assíncrono de dados

Vamos trabalhar basicamente com o UNI, mas o MULTI pode ser usado se fizer sentido para fluxos assíncronos de dados.

Integrando reatividade com o banco de dados

Para integração reativa com o banco de dados, precisamos de cuidados. É necessário usar um banco de dados com um driver reativo, pois a aplicação deve ser não bloqueante. A thread deve estar livre para buscar outras requisições, e o event loop controla as respostas. Se o driver não for reativo, a aplicação pode ser bloqueada. Existem anotações que permitem indicar chamadas bloqueantes, mas é importante entender que isso pode comprometer a eficiência dos recursos.

Internamente, como isso acontece? Temos diferentes thread pools (grupos de execução de tarefas), um para ações bloqueantes e outros para ações que trabalham com event loop (laço de eventos). Atualmente, mesmo em um cenário onde temos operações reativas e não reativas na mesma aplicação, conseguimos separar bem os conceitos, permitindo que a aplicação funcione adequadamente. No mundo ideal, tudo seria reativo, pois precisaríamos de menos threads, otimizando os recursos. No entanto, nem sempre o mundo é ideal e temos essa opção.

Explorando o conceito de non-blocking

Mencionamos anteriormente a execução non-blocking (não bloqueante) para otimizar threads. O conceito de non-blocking refere-se ao não bloqueio das threads. Outras linguagens possuem recursos similares, como as coroutines no Kotlin e as goroutines no Go. Cada linguagem possui seu próprio framework que oferece suporte reativo. É importante lembrar a diferença entre bloqueante e não bloqueante. No caso de operações não bloqueantes, as threads são seguras e precisamos escalar mais threads. Já as operações não bloqueantes, que não bloqueiam as threads, permitem que reutilizemos melhor as threads.

Analisando o projeto Banking Service

Vamos agora ver como isso funciona no projeto. Ainda não vamos desenvolver, mas mostraremos como está o corpo do projeto que vamos otimizar no curso. Nem toda aplicação está desenvolvida, então, em alguns casos, colocaremos a mão na massa, enquanto em outros faremos mais a análise do código. Dessa forma, podemos melhorar nosso conhecimento sobre cada recurso abordado no curso.

Na IDE, temos uma aplicação chamada Banking Service, cujo objetivo é o registro de agências bancárias. Adicionamos uma agência, mas há uma regra de negócio que faz uma requisição para uma API externa, que é outro sistema que temos, para validar se a situação cadastral dessa agência está ativa. Essa regra de negócio serve para termos uma chamada externa, e isso é relevante porque, assim como o banco de dados, essa chamada pode ser bloqueante ou não bloqueante. O que define isso é como chamamos a API. Se utilizarmos um cliente HTTP bloqueante, a aplicação reativa acaba sendo bloqueada. Com um cliente HTTP não bloqueante, conseguimos realizar a busca no sistema externo de forma não bloqueante, usando o Event Loop.

Explorando a estrutura da aplicação MVC

A aplicação é basicamente uma aplicação MVC. Temos a controller e, ao analisá-la, percebemos algumas diferenças em relação ao tradicional. Por exemplo, utilizamos o UNI, que é uma promessa de resposta com tipos. Em aplicações totalmente reativas, não precisamos anotar como non-blocking, mas podemos fazê-lo. Se a aplicação for reativa e houver uma chamada bloqueante, é necessário anotar como blocking. Vamos ver alguns cenários de uso disso. Após passar pelo UNI e pelo non-blocking, tudo segue normalmente. Temos um método público que chama um serviço, e nesse serviço há outros recursos. Por exemplo, no método cadastrar, há várias informações e métodos que fornecem detalhes sobre o funcionamento reativo. Não entraremos em detalhes agora, mas o método já é diferente de um método tradicional. O restante, como o método de busca por ID que chama o Repository, não apresenta muita diferença no retorno. Temos o If Session e o If Transaction, que vamos entender melhor. O que faz chamar a API externa e como garantimos que é um recurso não bloqueante? No situação cadastral HTTP Service, vemos que ele usa um recurso do Micro Profile que é não bloqueante. O próprio Quarkus já traz no seu núcleo todas essas dependências e extensões, que são não bloqueantes nativamente. Quando falamos de Base REST, Clients HTTP, e outras bibliotecas, como trabalhamos com o Quarkus, que opera sobre o Vertex, que é reativo por natureza, todos esses benefícios são aproveitados.

Considerando o desenvolvimento imperativo

Podemos desenvolver de forma imperativa? Sim, tranquilamente. Vamos ver isso quando estivermos rodando um pouco mais. Não há problema em escrever código imperativo, pois podemos utilizar a mesma extensão. Fizemos um panorama de como o Quarkus funciona, abordando uma parte teórica e conhecendo o código. No próximo vídeo, a ideia é trabalharmos um pouco mais, entrando nos métodos e mostrando o que está acontecendo, como funciona, e, ao final, subir a aplicação para ver como ela opera, realizando algumas requisições e operações.

Fundamentos do Reativo e Quarkus - Paradigma Reativo

Discutindo desafios em ambientes de produção

Muitas vezes, quando estamos trabalhando em uma aplicação em um ambiente de desenvolvimento, não conseguimos prever todos os problemas que ela pode enfrentar quando estiver em produção. Em um ambiente de teste, frequentemente subimos essa aplicação em um container ou em um ambiente cloud, mas com um escopo limitado. Realizamos alguns testes, como fazer uma requisição para um endpoint e salvar informações no banco de dados. Quando tudo parece estar funcionando bem, cobrindo todos os fluxos e regras de negócio, chega o momento de colocar a aplicação em produção. É nesse ponto que enfrentamos o acesso simultâneo de múltiplos clientes, especialmente em empresas conhecidas mundialmente ou no Brasil, como em sistemas de e-commerce, que recebem muitas requisições.

Durante o desenvolvimento, entendemos todos os fluxos, que ainda estão funcionais, mas eventualmente percebemos que a performance da aplicação começa a degradar. Ela não responde conforme esperado, o que resulta em reclamações sobre lentidão do site ou do e-commerce, criando um cenário caótico. Nesse momento, surge o interesse por sistemas reativos, como eles funcionam e como podemos implementá-los em uma aplicação Java.

Enfrentando limitações de modelos síncronos

O problema ocorre quando a aplicação, já em produção, começa a receber uma alta carga de requisições simultâneas, resultando em alta latência e tempos de resposta maiores. Isso não era visível durante o desenvolvimento, mas agora se torna um obstáculo. O banco de dados começa a responder mais lentamente, aumentando a latência. O modelo síncrono, ou não reativo, não responde bem para aplicações modernas, pois as threads são mal utilizadas.

Devemos considerar que nosso servidor possui um número limitado de threads, que podem lidar com um número específico de requisições, mas não são bem aproveitadas. Por exemplo, se temos um sistema em um servidor e o banco de dados em outro, todas as requisições dos usuários que chegam ao sistema e vão para o banco de dados fazem com que a thread responsável pela requisição fique presa, aguardando a resposta do banco de dados para então responder ao usuário. Isso representa um grande desperdício de recursos e é prejudicial para a escalabilidade.

Explorando a necessidade de escalabilidade

Quando atingimos o limite de threads, precisamos escalar, o que requer um servidor mais robusto e mais investimento financeiro para suportar o aumento de requisições. Com o crescimento das empresas e o aumento do uso dos sistemas, a necessidade de escalar se torna constante, gerando custos adicionais. No entanto, existem maneiras de economizar.

Esse é um panorama geral dos problemas enfrentados quando não utilizamos aplicações reativas. Uma aplicação reativa é baseada em fluxos de dados assíncronos. No modelo reativo, em vez de uma thread ficar bloqueada aguardando a resposta do banco de dados, frameworks como o Quarkus, que usaremos como exemplo, utilizam comunicação orientada a eventos e mensagens.

Implementando o modelo reativo

No modelo reativo, não temos mais uma única thread responsável por uma requisição. Quando uma requisição é feita ao banco de dados, liberamos a thread de plataforma, que é a thread do sistema operacional e da JVM, para atender novas requisições. Um event loop verifica as requisições, e quando a resposta do banco de dados é obtida, outra thread pode ser responsável por enviar a resposta ao usuário. No Quarkus, o event loop gerencia todas essas threads, permitindo um melhor aproveitamento dos recursos.

Com isso, as threads não ficam mais ociosas aguardando requisições, aumentando a escala da aplicação. O tempo que seria gasto esperando a resposta do banco de dados é agora utilizado para atender novas requisições. No modelo reativo, há um reaproveitamento dos recursos, reduzindo custos e facilitando a escalabilidade. Além disso, enfatiza-se a resiliência, elasticidade e backpressure, conceitos que serão explorados ao longo do curso.

Abordando sistemas distribuídos e microserviços

Estamos discutindo a adequação de sistemas distribuídos e microserviços, que envolvem múltiplas requisições para diversos serviços externos. Isso está relacionado ao manifesto reativo, que não estabelece regras, mas detalha os conceitos importantes para uma aplicação ser considerada reativa.

Uma aplicação reativa deve ser responsiva. Quando a resposta vem do banco de dados, precisamos de um sistema que responda prontamente, sem deixar a requisição ou a resposta perdida. O event loop e o framework reativo devem estar preparados para responder assim que a resposta for obtida.

Garantindo resiliência e escalabilidade

Além disso, a aplicação deve ser resiliente, lidando bem com falhas. Em um cenário reativo, não interrompemos a execução do programa com exceções, como no modelo imperativo. Em vez disso, tratamos erros e falhas de forma mais amigável e resiliente, processando mensagens de erro no event loop.

A escalabilidade é outro ponto crucial. Não nos preocupamos mais em escalar threads de plataforma ou CPU, mas sim em aumentar nosso próprio thread pool. Isso nos permite lidar com a demanda sem bloqueios entre servidores, como servidores de banco de dados ou de aplicação. A comunicação é orientada a mensagens e assíncrona, como vemos no event loop.

Diferenciando reatividade de assincronismo

Há uma confusão comum entre reatividade e assincronismo. Todo reativo é assíncrono, mas nem todo assíncrono é reativo. No modelo imperativo, as chamadas de método ocorrem em sequência, enquanto no assíncrono, operações podem ocorrer em paralelo. A reatividade exige um controle de fluxo, como o backpressure, para gerenciar a quantidade de mensagens no event loop e evitar alta demanda de memória e CPU.

Existem várias ferramentas para implementar reatividade em Java e Kotlin, como Reactor, MultiNir e RxJava. No curso, utilizaremos o Quarkus, que é baseado no Vertex com o MultiNir, oferecendo robustez com um código mais simples.

Concluindo com os benefícios do reativo

Os benefícios do reativo incluem melhor uso de recursos, menos bloqueios de threads e uma experiência aprimorada para o usuário final. Isso se alinha bem com aplicações Cloud Native, onde a escalabilidade e a economia de recursos são fundamentais.

Este foi um panorama geral sobre reatividade em aplicações Cloud Native. No curso, usaremos o Quarkus para explorar esses conceitos. No próximo vídeo, apresentaremos uma aplicação reativa, analisaremos seu funcionamento e faremos requisições para entender melhor o sistema. Neste vídeo, o objetivo foi apresentar o conceito de reatividade. Nos vemos no próximo, onde abordaremos a prática. Muito obrigado!

Sobre o curso Back-ends Modernos em Java: Reatividade, Observabilidade e Performance

O curso Back-ends Modernos em Java: Reatividade, Observabilidade e Performance possui 255 minutos de vídeos, em um total de 48 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Escolha a duração do seu plano e aproveite até 44% OFF

Conheça os Planos para Empresas