O Vert.x é um ecossistema de ferramentas para a Máquina Virtual Java (JVM) focado na construção de aplicações reativas. Diferente de frameworks tradicionais, o Vert.x é orientado a eventos e utiliza I/O não bloqueante (Non-blockign I/O), o que permite processar um volume massivo de requisições simultâneas utilizando uma quantidade mínima de threads, tornando-o ideal para sistemas de alta escalabilidade.
O Paradigma Reativo
Tradicionalmente, a concorrência em servidores web é gerenciada através de threads. Neste modelo ("um fio por conexão"), cada nova requisição ocupa uma thread do sistema. Embora seja um modelo intuitivo e alinhado à programação imperativa, ele apresenta gargalos significativos:
- Consumo de Memória: Cada thread reserva uma fatia considerável de memória (frequentemente cerca de 1MB de stack).
- Troca de Contexto: O sistema operacional gasta ciclos de CPU alternando entre milhares de threads, o que degrada a performance sob carga extrema.
- Bloqueio de I/O: Enquanto uma thread aguarda uma resposta do banco de dados ou de uma rede, ela permanece inativa, desperdiçando recursos.
A abordagem assíncrona do Vert.x mitiga esses problemas. Em vez de bloquear, a thread é liberada para processar outras tarefas enquanto a operação de I/O é executada em segundo plano. Quando a operação termina, o sistema é notificado para retomar o trabalho original via callbacks, promises ou futures.
Arquitetura Orientada a Eventos
Neste modelo, o fluxo da aplicação é determinado por eventos (como o recebimento de um pacote de rede ou o término de uma leitura de disco). Diferente da chamada de função convencional, onde o chamador aguarda o resultado, a arquitetura orientada a eventos foca na reação: "quando o evento X ocorrer, execute a lógica Y". Isso permite um desacoplamento eficiente e facilita a multiplexação de I/O.
O Padrão Reactor
O Reactor é o padrão de design que sustenta sistemas como Vert.x e Netty. Ele separa a recepção de eventos da execução da lógica de negócio. Podemos categorizá-lo em três variações principais:
1. Reactor Mono-thread
Uma única thread é responsável por aceitar conexões e processar leituras/escritas. Para manter a performance, qualquer lógica de negócio pesada deve ser movida para fora dessa thread para não interromper o ciclo de eventos.
2. Reactor com Pool de Workers
Mantém a estrutura do mono-thread para I/O, mas delega o processamento da lógica de negócio para um pool spearado de threads (Worker Pool), evitando o travamento do loop de eventos principal.
3. Multi-Reactor (Main/Sub Reactors)
Utiliza uma thread principal (MainReactor) exclusivamente para escutar novas conexões. Uma vez estabelecidas, essas conexões são distribuídas para um conjunto de sub-reactors. Cada sub-reactor gerencia seu próprio seletor para operações de leitura e escrita.
- MainReactor: Equivale ao BossGroup no Netty. Focado em aceitar conexões.
- SubReactor: Equivale ao WorkerGroup no Netty. Geralmente dimensionado como o dobro do número de núcleos de CPU para maximizar a taxa de transferência.
Implementação Prática do Padrão Reactor
Abaixo, apresentamos uma estrutura simplificada em Java demonstrendo como um gerenciador de conexões distribui carga para processadores de eventos independentes.
package br.com.tech.reactor;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class CoordenadorConexoes {
private final Selector seletorPrincipal;
private final ProcessadorEventos[] processadores;
private int proximoProcessador = 0;
public CoordenadorConexoes(String ip, int porta, int qtdSubReactors) throws IOException {
this.seletorPrincipal = Selector.open();
this.processadores = new ProcessadorEventos[qtdSubReactors];
for (int i = 0; i < qtdSubReactors; i++) {
processadores[i] = new ProcessadorEventos();
new Thread(processadores[i]).start();
}
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(ip, porta));
serverChannel.register(seletorPrincipal, SelectionKey.OP_ACCEPT);
}
public void executar() throws IOException {
while (true) {
if (seletorPrincipal.select() <= 0) continue;
Iterator<SelectionKey> chaves = seletorPrincipal.selectedKeys().iterator();
while (chaves.hasNext()) {
SelectionKey chave = chaves.next();
if (chave.isAcceptable()) {
ServerSocketChannel servidor = (ServerSocketChannel) chave.channel();
SocketChannel cliente = servidor.accept();
// Distribuição Round-Robin para os SubReactors
processadores[proximoProcessador % processadores.length].vincularCanal(cliente);
proximoProcessador++;
}
chaves.remove();
}
}
}
public static void main(String[] args) {
try {
new CoordenadorConexoes("localhost", 8080, 2).executar();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package br.com.tech.reactor;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ProcessadorEventos implements Runnable {
private final Selector seletorLocal;
private final ConcurrentLinkedQueue<SocketChannel> filaCanais = new ConcurrentLinkedQueue<>();
public ProcessadorEventos() throws IOException {
this.seletorLocal = Selector.open();
}
public void vincularCanal(SocketChannel canal) {
filaCanais.add(canal);
seletorLocal.wakeup();
}
@Override
public void run() {
while (true) {
try {
// Registra novos canais pendentes
SocketChannel novoCanal;
while ((novoCanal = filaCanais.poll()) != null) {
novoCanal.configureBlocking(false);
novoCanal.register(seletorLocal, SelectionKey.OP_READ);
}
if (seletorLocal.select(1000) <= 0) continue;
Iterator<SelectionKey> chavesAtivas = seletorLocal.selectedKeys().iterator();
while (chavesAtivas.hasNext()) {
SelectionKey chave = chavesAtivas.next();
if (chave.isReadable()) {
// Lógica de leitura de dados do buffer
}
chavesAtivas.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}