paint-brush
Observabilidade do backend Java com OpenTelemetry Traces e código mínimopor@apanasevich
Novo histórico

Observabilidade do backend Java com OpenTelemetry Traces e código mínimo

por Dmitriy Apanasevich10m2024/11/15
Read on Terminal Reader
Read this story w/o Javascript

Muito longo; Para ler

Como integramos a estrutura OpenTelemetry ao nosso backend Java, obtendo rastreamento com codificação mínima.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Observabilidade do backend Java com OpenTelemetry Traces e código mínimo
Dmitriy Apanasevich HackerNoon profile picture

Olá a todos! Eu sou Dmitriy Apanasevich, desenvolvedor Java na MY.GAMES, trabalhando no jogo Rush Royale, e gostaria de compartilhar nossa experiência integrando a estrutura OpenTelemetry em nosso backend Java. Há bastante coisa para cobrir aqui: cobriremos as mudanças de código necessárias para implementá-lo, bem como os novos componentes que precisamos instalar e configurar – e, claro, compartilharemos alguns de nossos resultados.

Nosso objetivo: alcançar a observabilidade do sistema

Vamos dar mais contexto ao nosso caso. Como desenvolvedores, queremos criar software que seja fácil de monitorar, avaliar e entender (e esse é precisamente o propósito da implementação do OpenTelemetry — maximizar a eficiência do sistema). observabilidade ).


Os métodos tradicionais para coletar insights sobre o desempenho do aplicativo geralmente envolvem o registro manual de eventos, métricas e erros:



É claro que existem muitas estruturas que nos permitem trabalhar com logs, e tenho certeza de que todos que estão lendo este artigo têm um sistema configurado para coletar, armazenar e analisar logs.


O registro também foi totalmente configurado para nós, então não usamos os recursos fornecidos pelo OpenTelemetry para trabalhar com registros.


Outra maneira comum de monitorar o sistema é aproveitando métricas:


Também tínhamos um sistema totalmente configurado para coletar e visualizar métricas, então aqui também ignoramos os recursos do OpenTelemetry em termos de trabalho com métricas.


Mas uma ferramenta menos comum para obter e analisar esse tipo de dados do sistema são vestígios .


Um trace representa o caminho que uma solicitação percorre em nosso sistema durante sua vida útil e normalmente começa quando o sistema recebe uma solicitação e termina com a resposta. Os traces consistem em múltiplos vãos , cada um representando uma unidade específica de trabalho determinada pelo desenvolvedor ou sua biblioteca de escolha. Esses spans formam uma estrutura hierárquica que ajuda a visualizar como o sistema processa a solicitação.


Nesta discussão, vamos nos concentrar no aspecto de rastreamento do OpenTelemetry.

Mais algumas informações básicas sobre OpenTelemetry

Vamos também lançar alguma luz sobre o projeto OpenTelemetry, que surgiu da fusão do Rastreamento aberto e Censo Aberto projetos.


O OpenTelemetry agora fornece uma gama abrangente de componentes com base em um padrão que define um conjunto de APIs, SDKs e ferramentas para várias linguagens de programação, e o objetivo principal do projeto é gerar, coletar, gerenciar e exportar dados.


Dito isto, o OpenTelemetry não oferece um backend para armazenamento de dados ou ferramentas de visualização.


Como estávamos interessados apenas em rastreamento, exploramos as soluções de código aberto mais populares para armazenar e visualizar rastros:

  • Jaeger
  • Zipkin
  • Grafana Tempo


Por fim, escolhemos o Grafana Tempo devido às suas impressionantes capacidades de visualização, ritmo rápido de desenvolvimento e integração com nossa configuração Grafana existente para visualização de métricas. Ter uma ferramenta única e unificada também foi uma vantagem significativa.

Componentes OpenTelemetry

Vamos também dissecar um pouco os componentes do OpenTelemetry.


A especificação:

  • API — tipos de dados, operações, enums

  • SDK — implementação de especificação, APIs em diferentes linguagens de programação. Uma linguagem diferente significa um estado de SDK diferente, de alfa para estável.

  • Protocolo de dados (OTLP) e convenções semânticas


A API Java do SDK:

  • Bibliotecas de instrumentação de código
  • Exportadores — ferramentas para exportar rastros gerados para o backend
  • Cross Service Propagators — uma ferramenta para transferir contexto de execução para fora do processo (JVM)


O OpenTelemetry Collector é um componente importante, um proxy que recebe dados, os processa e os repassa – vamos dar uma olhada mais de perto.

Coletor OpenTelemetry

Para sistemas de alta carga que lidam com milhares de solicitações por segundo, gerenciar o volume de dados é crucial. Os dados de rastreamento geralmente superam os dados comerciais em volume, tornando essencial priorizar quais dados coletar e armazenar. É aqui que nossa ferramenta de processamento e filtragem de dados entra e permite que você determine quais dados valem a pena armazenar. Normalmente, as equipes desejam armazenar rastreamentos que atendam a critérios específicos, como:


  • Rastreamentos com tempos de resposta que excedem um certo limite.
  • Rastreia que encontrou erros durante o processamento.
  • Rastros que contêm atributos específicos, como aqueles que passaram por um determinado microsserviço ou foram sinalizados como suspeitos no código.
  • Uma seleção aleatória de rastros regulares que fornecem um instantâneo estatístico das operações normais do sistema, ajudando você a entender o comportamento típico e identificar tendências.

Aqui estão os dois principais métodos de amostragem usados para determinar quais traços salvar e quais descartar:

  • Amostragem de cabeça — decide no início de um rastreamento se deve mantê-lo ou não
  • Amostragem de cauda — decide somente após o rastreamento completo estar disponível. Isso é necessário quando a decisão depende de dados que aparecem mais tarde no rastreamento. Por exemplo, dados incluindo intervalos de erro. Esses casos não podem ser tratados pela amostragem de cabeça, pois exigem a análise de todo o rastreamento primeiro


O OpenTelemetry Collector ajuda a configurar o sistema de coleta de dados para que ele salve apenas os dados necessários. Discutiremos sua configuração mais tarde, mas, por enquanto, vamos passar para a questão do que precisa ser alterado no código para que ele comece a gerar rastros.

Instrumentação de código zero

Obter a geração de rastreamento realmente exigiu codificação mínima – foi necessário apenas iniciar nossos aplicativos com um agente Java, especificando o configuração :


-javaagent:/opentelemetry-javaagent-1.29.0.jar

-Dotel.javaagent.configuration-file=/otel-config.properties


O OpenTelemetry oferece suporte a um grande número de bibliotecas e frameworks , então, após iniciar o aplicativo com o agente, imediatamente recebemos rastros com dados sobre os estágios de processamento de solicitações entre serviços, no SGBD e assim por diante.


Na configuração do nosso agente, desabilitamos as bibliotecas que estamos usando cujos intervalos não queríamos ver nos rastros e, para obter dados sobre como nosso código funcionava, marcamos com Anotações :


 @WithSpan("acquire locks") public CompletableFuture<Lock> acquire(SortedSet<Object> source) { var traceLocks = source.stream().map(Object::toString).collect(joining(", ")); Span.current().setAttribute("locks", traceLocks); return CompletableFuture.supplyAsync(() -> /* async job */); }


Neste exemplo, a anotação @WithSpan é usada para o método, o que sinaliza a necessidade de criar um novo intervalo chamado " acquire locks ", e o atributo " locks " é adicionado ao intervalo criado no corpo do método.


Quando o método termina de funcionar, o intervalo é fechado, e é importante prestar atenção a esse detalhe para código assíncrono. Se você precisa obter dados relacionados ao trabalho de código assíncrono em funções lambda chamadas de um método anotado, você precisa separar essas lambdas em métodos separados e marcá-las com uma anotação adicional.

Nossa configuração de coleta de rastreamento

Agora, vamos falar sobre como configurar todo o sistema de coleta de rastros. Todos os nossos aplicativos JVM são iniciados com um agente Java que envia dados para o coletor OpenTelemetry.


No entanto, um único coletor não pode manipular um grande fluxo de dados e essa parte do sistema deve ser dimensionada. Se você iniciar um coletor separado para cada aplicativo JVM, a amostragem de cauda será interrompida, porque a análise de rastreamento deve ocorrer em um coletor e, se a solicitação passar por várias JVMs, os intervalos de um rastreamento acabarão em coletores diferentes e sua análise será impossível.


Aqui, um coletor configurado como um equilibrador que vem ao resgate.


Como resultado, obtemos o seguinte sistema: Cada aplicação JVM envia dados para o mesmo coletor balanceador, cuja única tarefa é distribuir dados recebidos de diferentes aplicações, mas relacionados a um dado trace, para o mesmo coletor-processador. Então, o coletor-processador envia dados para o Grafana Tempo.



Vamos dar uma olhada mais de perto na configuração dos componentes deste sistema.

Coletor de balanceamento de carga

Na configuração do coletor-balanceador, configuramos as seguintes partes principais:


 receivers:  otlp:    protocols:      grpc: exporters:  loadbalancing:    protocol:      otlp:        tls:          insecure: true    resolver:      static:        hostnames:          - collector-1.example.com:4317          - collector-2.example.com:4317          - collector-3.example.com:4317 service:  pipelines:    traces:      receivers: [otlp]      exporters: [loadbalancing]


  • Receptores — onde os métodos (por meio dos quais os dados podem ser recebidos pelo coletor) são configurados. Configuramos a recepção de dados somente no formato OTLP. (É possível configurar a recepção de dados por meio de muitos outros protocolos , por exemplo Zipkin, Jaeger.)
  • Exportadores — a parte da configuração onde o balanceamento de dados é configurado. Entre os coletores-processadores especificados nesta seção, os dados são distribuídos dependendo do hash calculado a partir do identificador de rastreamento.
  • A seção Serviço especifica a configuração de como o serviço funcionará: somente com rastreamentos, utilizando o receptor OTLP configurado na parte superior e transmitindo dados como balanceador, ou seja, sem processamento.

O coletor com processamento de dados

A configuração dos coletores-processadores é mais complicada, então vamos dar uma olhada lá:


 receivers:  otlp:    protocols:      grpc:        endpoint: 0.0.0.0:14317 processors:  tail_sampling:    decision_wait: 10s    num_traces: 100    expected_new_traces_per_sec: 10    policies:      [          {            name: latency500-policy,            type: latency,            latency: {threshold_ms: 500}          },          {            name: error-policy,            type: string_attribute,            string_attribute: {key: error, values: [true, True]}          },          {            name: probabilistic10-policy,            type: probabilistic,            probabilistic: {sampling_percentage: 10}          }      ]  resource/delete:    attributes:      - key: process.command_line        action: delete      - key: process.executable.path        action: delete      - key: process.pid        action: delete      - key: process.runtime.description        action: delete      - key: process.runtime.name        action: delete      - key: process.runtime.version        action: delete exporters:  otlp:    endpoint: tempo:4317    tls:      insecure: true service:  pipelines:    traces:      receivers: [otlp]      exporters: [otlp]


Semelhante à configuração do coletor-balanceador, a configuração de processamento consiste nas seções Receivers, Exporters e Service. No entanto, vamos nos concentrar na seção Processors, que explica como os dados são processados.


Primeiro, a seção tail_sampling demonstra uma configuração que permite filtrar os dados necessários para armazenamento e análise:


  • latency500-policy : esta regra seleciona rastros com latência superior a 500 milissegundos.

  • error-policy : esta regra seleciona rastros que encontraram erros durante o processamento. Ela procura por um atributo de string chamado "error" com valores "true" ou "True" nos intervalos de rastro.

  • probabilistic10-policy : esta regra seleciona aleatoriamente 10% de todos os rastros para fornecer insights sobre a operação normal do aplicativo, erros e processamento de solicitações longas.


Além do tail_sampling, este exemplo mostra a seção resource/delete para excluir atributos desnecessários não necessários para análise e armazenamento de dados.

Resultados

A janela de pesquisa de rastros do Grafana resultante permite que você filtre dados por vários critérios. Neste exemplo, simplesmente exibimos uma lista de rastros recebidos do serviço de lobby, que processa metadados do jogo. A configuração permite filtragem futura por atributos como latência, erros e amostragem aleatória.


A janela de exibição de rastreamento exibe o cronograma de execução do serviço de lobby, incluindo os vários períodos que compõem a solicitação.


Como você pode ver na imagem, a sequência de eventos é a seguinte: os bloqueios são adquiridos, então os objetos são recuperados do cache, seguido pela execução de uma transação que processa as solicitações, após o que os objetos são armazenados no cache novamente e os bloqueios são liberados.


Os spans relacionados a solicitações de banco de dados foram gerados automaticamente devido à instrumentação de bibliotecas padrão. Em contraste, os spans relacionados ao gerenciamento de bloqueio, operações de cache e iniciação de transação foram adicionados manualmente ao código de negócios usando as anotações mencionadas anteriormente.



Ao visualizar um intervalo, você pode ver atributos que permitem entender melhor o que aconteceu durante o processamento, por exemplo, ver uma consulta no banco de dados.



Uma das características interessantes do Grafana Tempo é o gráfico de serviço , que exibe graficamente todos os serviços que exportam rastros, as conexões entre eles, a taxa e a latência das solicitações:


Encerrando

Como vimos, trabalhar com o rastreamento OpenTelemetry melhorou muito bem nossas habilidades de observação. Com alterações mínimas no código e uma configuração de coletor bem estruturada, obtivemos insights profundos – além disso, vimos como os recursos de visualização do Grafana Tempo complementaram ainda mais nossa configuração. Obrigado pela leitura!