Executando verificação de segurança...
8

Criando um serviço com spring graalvm leve e com monitoramento configurado

Introdução

Esse tutorial tem o objetivo de transformar uma aplicação web spring em código nativo usando graalvm e integrando com o grafana usando o open telemetry.

Pré Requisitos

Ter experiência com o spring, grafana e com o docker porque só vamos falar sobre como funciona o build na graalvm e como integrar com o grafana todo o resto eu vou assumir que você já sabe.

O que é graalvm ?

É uma JDK de alta performance que usa um JIT alternativo e troca também o algoritmo do garbage collector para ficar mais performática. Outra coisa legal é que depois de compilado esse binário pode ser executado por qualquer imagem de slim de linux. A desvantagem é que a gente perde todas as facilidades da JVM padrão como o javaagent.

Setup do grafana no docker

Crie um arquivo docker-compose.yml e coloque esse conteúdo abaixo, nesse docker tem o grafana o prometheus e o collector que é o carinha que nós vamos nos integrar para enviar os dados para o grafana. Copie os arquivos da pasta docker do meu repositório não vou focar em como eles funcionam Link da pasta.

services:
  collector:
    container_name: collector
    image: otel/opentelemetry-collector-contrib:0.91.0
    command:
      - --config=/etc/otelcol-contrib/otel-collector.yml
    volumes:
      - ./docker/collector/otel-collector.yml:/etc/otelcol-contrib/otel-collector.yml
    ports:
      - "4317:4317" # OTLP gRPC receiver
      # - "4318:4318" # OTLP HTTP receiver
      - "8889" # Prometheus exporter metrics
    depends_on:
      - loki
      - jaeger-all-in-one
      - zipkin-all-in-one
      - tempo
    networks:
      - spring_native_network

  tempo:
    container_name: tempo
    image: grafana/tempo:latest
    command: [ "-config.file=/etc/tempo.yml" ]
    volumes:
      - ./docker/tempo/tempo.yml:/etc/tempo.yml
    ports:
      - "4317"  # otlp grpc
      - "3200"  # tempo as grafana datasource
    networks:
      - spring_native_network

  loki:
    container_name: loki
    image: grafana/loki:latest
    command: -config.file=/etc/loki/local-config.yaml
    ports:
      - "3100"
    networks:
      - spring_native_network

  prometheus:
    container_name: prometheus
    image: prom/prometheus
    volumes:
      - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --enable-feature=exemplar-storage
      - --web.enable-remote-write-receiver
    ports:
      - '9090:9090'
    depends_on:
      - collector
    networks:
      - spring_native_network

  grafana:
    container_name: grafana
    image: grafana/grafana
    volumes:
      - ./docker/grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
      - loki
      - jaeger-all-in-one
      - zipkin-all-in-one
      - tempo
    networks:
      - spring_native_network

  jaeger-all-in-one:
    container_name: jaeger
    image: jaegertracing/all-in-one:latest
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    ports:
      - "16686:16686"
      - "4317"
    networks:
      - spring_native_network

  zipkin-all-in-one:
    container_name: zipkin
    image: openzipkin/zipkin:latest
    ports:
      - "9411:9411"
    networks:
      - spring_native_network

networks:
  spring_native_network:
    driver: bridge

Agora rode docker compose up e veja se todos os conteiners subiram.

Criando a App do spring

Crie uma app spring normal adicione a dependência do spring native e spring-boot-starter-web depois que o projeto foi criado adicione as dependências do opentelemetry.

dependencyManagement {
	imports {
		mavenBom("io.opentelemetry:opentelemetry-bom:1.41.0")
		mavenBom("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.7.0")
	}
}

dependencies {
    implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter'
}

Depois de baixar a dependencia configure o endpoint do coletor de logs/trace do grafana, nele nós podemos escolher qual protocolo vamos usar grpc/http e os atributos que seram enviados para o grafana como nome da app, env e namespace.

otel:
  instrumentation:
    spring-web:
      enabled: true
  exporter:
    otlp:
      protocol: grpc
      endpoint: http://localhost:4317
    #   protocol: http/protobuf
    #   endpoint: http://localhost:4318 # se for usar o http tem que trocar a porta para 4318
  logs:
    exporter: none
  resource:
    attributes:
      service.name: spring-service
      service: spring-service
      env: dev
      namespace: B.O

Crie um application-prod.yml onde ficará apenas o endpoint que será usado no ambiente do docker assim a gente consegue testa com o localhost na idea e fazer o build com a url que vai funcionar certo no conteiner.

otel:
  exporter:
    otlp:
      endpoint: http://collector:4317

Dockerfile e compilação

Na compilação do spring native ele não pode ter um profile default como local ou dev porque ele seta o profile na compilação nesse caso ele vai setar o default e quando a gente for adicionar o profile no docker-compose ele vai subistituir pelo prod, mais se já tivesse outro por exemplo dev ele ficaria com dois profiles depois de compilado.

Então se a sua ideia é passar o profile na compilação pode passar a env SPRING_PROFILES_ACTIVE como argumento do docker build na pipeline e ele vai setar o profile.

Nós vamos usar um dockerfile com multiplos estagios porque depois de compilado o spring não vai precisar mais da jvm para rodar e ele pode ser executado como um binario de maquina o que pode tornar o conteiner mais leve.

# Imagem da graalvm
FROM ghcr.io/graalvm/graalvm-community:21 AS build

# não fazer isso em prod pode dar ruim
USER root

# copiando os arquivos do projeto para dentro do conteiner
# copie só o necessario para o build assim vc evita problemas
COPY settings.gradle .
COPY build.gradle .
COPY src src
COPY gradlew gradlew
COPY gradle gradle


# compilando sem rodar os testes na compilação a ideia é q rode os testes antes na pipeline de deploy
RUN ./gradlew nativeCompile -x test

# usando uma imagem leve apenas para rodar o binario poderia ser qualquer outro linux
FROM debian:12-slim

# ENV SPRING_PROFILES_ACTIVE=prod

# copiando o binario do estagio de build para o estagio final
COPY --from=build /build/native/nativeCompile/* .

EXPOSE 8080

# rodando o programa o nome do binario pode mudar dependendo do nome do projeto compile local para ver o nome e evitar erros
CMD ["./Spring"]

Agora vamos adicionar o nosso serviço no docker-compose para fazer o build e ver se ele vai enviar os dados para o grafana.

services:
  spring-native-api:
    container_name: spring-native-api
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    ports:
      - "8080:8080"
    depends_on:
      - collector
    networks:
      - spring_native_network

Agora é só fazer o build e ser feliz com o novo conteiner leve usando o que tem de mais massa no spring.

Se estiver usando o spring 3.3 e o java 21 tambem podemos habilitar as virtual threads no framework spring.threads.virtual.enabled=true.

Conclusão

Espero que isso ajude o seu time dependendo do contexto tem um ganho de performace no uso de memoria que é bizarro, qualquer duvida manda ai vou deixa o link do repositorio do github em baixo.

https://github.com/Claudio-code/spring-native-open-telemetry

Carregando publicação patrocinada...