Gerando um Executável Nativo do Banco de Dados H2 Usando a GraalVM

Já imaginou o banco de dados H2, escrito em Java, executando com apenas 10MB de memória? Você acha que isso é possível?
Na rinha de backend de 2024, eu enfrentei um desafio onde precisei otimizar ao máximo o uso de recursos de memória e CPU. Para isso, uma das minhas soluções foi gerar um executável nativo do banco de dados H2 utilizando a GraalVM. Neste post, vou mostrar passo a passo como você pode fazer isso também.
Passo 1: Instalando o SDKMAN
Primeiro, precisamos instalar o SDKMAN, uma ferramenta para gerenciar versões de SDKs de maneira fácil:
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk version
Passo 2: Instalando a GraalVM CE
Com o SDKMAN instalado, podemos facilmente instalar a versão Community Edition da GraalVM:
sdk install java 22.0.2-graalce
Essa versão será instalada no diretório:
~/.sdkman/candidates/java/22.0.2-graalce/
Passo 3: Preparando o Ambiente
Vamos criar um diretório para o H2 e preparar os arquivos necessários. Crie o diretório h2 e dentro dele crie os seguintes arquivos:
Arquivo reflectconfig.json
[
{ "name": "org.h2.store.fs.disk.FilePathDisk", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.mem.FilePathMem", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.mem.FilePathMemLZF", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.niomem.FilePathNioMem", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.niomem.FilePathNioMemLZF", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.split.FilePathSplit", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.niomapped.FilePathNioMapped", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.async.FilePathAsync", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.zip.FilePathZip", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.store.fs.retry.FilePathRetryOnInterrupt", "methods": [ { "name": "<init>", "parameterTypes": [] } ] },
{ "name": "org.h2.server.TcpServer", "queryAllDeclaredConstructors": true, "queryAllPublicConstructors": true, "queryAllDeclaredMethods": true, "queryAllPublicMethods": true, "allDeclaredClasses": true, "allPublicClasses": true, "methods": [ { "name": "<init>", "parameterTypes": [] } ] }
]
Arquivo create-native-image.sh
#!/bin/bash
wget https://github.com/h2database/h2database/releases/download/version-2.3.230/h2-2024-07-15.zip
unzip h2-2024-07-15.zip
cp h2/bin/h2-2.3.230.jar .
~/.sdkman/candidates/java/22.0.2-graalce/bin/native-image --no-fallback -cp h2-2.3.230.jar -H:Name=application -H:Class=org.h2.tools.Server --verbose --allow-incomplete-classpath -H:ReflectionConfigurationFiles=reflectconfig.json -H:IncludeResources=".*/data.zip$"
Passo 4: Criando o Executável Nativo
Execute o script create-native-image.sh
:
chmod +x create-native-image.sh
./create-native-image.sh
Esse script fará o download do H2, descompactará o arquivo, copiará o JAR para a pasta atual e usará a GraalVM para gerar um executável nativo.
Passo 5: Executando o Banco de Dados
Após a criação do executável, você pode iniciar o banco de dados com o comando:
./application -baseDir /work -ifNotExists -tcp -tcpAllowOthers -tcpPort 9092 -web -webAllowOthers -webPort 8082
Isso configurará o H2 para aceitar conexões TCP na porta 9092 e habilitará o cliente web na porta 8082.
Passo 6: Criando uma Imagem Docker
Para empacotar o executável nativo em uma imagem Docker, crie um arquivo Dockerfile com o seguinte conteúdo:
FROM registry.access.redhat.com/ubi9-minimal:9.2 AS ubi
FROM registry.access.redhat.com/ubi9-micro:9.2 AS scratch
FROM scratch
COPY --from=ubi /usr/lib64/libgcc_s.so.1 /usr/lib64/libgcc_s.so.1
COPY --from=ubi /usr/lib64/libstdc++.so.6 /usr/lib64/libstdc++.so.6
COPY --from=ubi /usr/lib64/libz.so.1 /usr/lib64/libz.so.1
WORKDIR /work/
RUN chown 1001 /work && chmod "g+rwX" /work && chown 1001:root /work
COPY --chown=1001:root application /work/application
USER 1001
CMD ["/bin/bash", "-c", "./application -baseDir /work -ifNotExists -tcp -tcpAllowOthers -tcpPort 9092 -web -webAllowOthers -webPort 8082"]
Passo 7: Construindo e Executando a Imagem Docker
Construa a imagem Docker:
docker build -t gasparbarancelli/h2-nativo:1 .
Execute um contêiner Docker com a imagem gerada:
docker run -d -p 9092:9092 -p 8082:8082 gasparbarancelli/h2-nativo:1
Acesse a URL localhost:8082 e conecte-se ao banco de dados usando o usuário sa
e a senha password
.


Passo 8: Verificando o Consumo de Recursos
Para verificar o consumo de recursos do container, execute o seguinte comando:
docker stats
Isso exibirá estatísticas em tempo real sobre o uso de CPU, memória e I/O do container. Aqui está um exemplo que mostra o H2 nativo consumindo apenas 10MB de memória:

Conclusão
Neste post, vimos como gerar um executável nativo do banco de dados H2 utilizando a GraalVM, desde a instalação do SDKMAN e da GraalVM até a criação do executável e sua execução. Este processo não só otimiza o uso de recursos de memória e CPU, mas também facilita a distribuição e a execução do banco de dados em diferentes ambientes por meio de uma imagem Docker. Essa solução é particularmente útil em cenários onde a performance é crítica, como na rinha de backend de 2024. Espero que este guia tenha sido útil e que você possa aplicar essas técnicas em seus próprios projetos para obter melhores resultados.