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.
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).
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
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
Nesta discussão, vamos nos concentrar no aspecto de rastreamento do OpenTelemetry.
Vamos também lançar alguma luz sobre o projeto OpenTelemetry, que surgiu da fusão do
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:
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.
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
A API Java do SDK:
O OpenTelemetry Collector é um componente importante, um proxy que recebe dados, os processa e os repassa – vamos dar uma olhada mais de perto.
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:
Aqui estão os dois principais métodos de amostragem usados para determinar quais traços salvar e quais descartar:
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.
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
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
O OpenTelemetry oferece suporte a um grande número de
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
@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.
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
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.
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]
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
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.
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
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!