paint-brush
Observabilidad del backend de Java con trazas de OpenTelemetry y código mínimopor@apanasevich
292 lecturas

Observabilidad del backend de Java con trazas de OpenTelemetry y código mínimo

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

Demasiado Largo; Para Leer

Cómo integramos el marco OpenTelemetry en nuestro backend Java, obteniendo seguimiento con una codificación mínima.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Observabilidad del backend de Java con trazas de OpenTelemetry y código mínimo
Dmitriy Apanasevich HackerNoon profile picture

¡Hola a todos! Soy Dmitriy Apanasevich, desarrollador de Java en MY.GAMES, y trabajo en el juego Rush Royale. Me gustaría compartir nuestra experiencia con la integración del marco OpenTelemetry en nuestro backend de Java. Hay mucho que cubrir aquí: cubriremos los cambios de código necesarios para implementarlo, así como los nuevos componentes que tuvimos que instalar y configurar y, por supuesto, compartiremos algunos de nuestros resultados.

Nuestro objetivo: lograr la observabilidad del sistema

Demos un poco más de contexto a nuestro caso. Como desarrolladores, queremos crear software que sea fácil de monitorear, evaluar y comprender (y este es precisamente el propósito de implementar OpenTelemetry: maximizar el rendimiento del sistema). Observabilidad ).


Los métodos tradicionales para recopilar información sobre el rendimiento de las aplicaciones a menudo implican el registro manual de eventos, métricas y errores:



Por supuesto, hay muchos marcos que nos permiten trabajar con registros, y estoy seguro de que todos los que leen este artículo tienen un sistema configurado para recopilar, almacenar y analizar registros.


El registro también estaba completamente configurado para nosotros, por lo que no utilizamos las capacidades proporcionadas por OpenTelemetry para trabajar con registros.


Otra forma común de monitorear el sistema es aprovechando métricas:


También teníamos un sistema completamente configurado para recopilar y visualizar métricas, por lo que aquí también ignoramos las capacidades de OpenTelemetry en términos de trabajar con métricas.


Pero una herramienta menos común para obtener y analizar este tipo de datos del sistema son rastros .


Un rastro representa la ruta que sigue una solicitud a través de nuestro sistema durante su vida útil y, por lo general, comienza cuando el sistema recibe una solicitud y finaliza con la respuesta. Los rastros constan de varios abarca , cada uno de los cuales representa una unidad de trabajo específica determinada por el desarrollador o la biblioteca de su elección. Estos intervalos forman una estructura jerárquica que ayuda a visualizar cómo el sistema procesa la solicitud.


Para esta discusión, nos concentraremos en el aspecto de rastreo de OpenTelemetry.

Un poco más de información sobre OpenTelemetry

Arrojemos también algo de luz sobre el proyecto OpenTelemetry, que surgió de la fusión de Seguimiento abierto y Censo abierto proyectos.


OpenTelemetry ahora ofrece una amplia gama de componentes basados en un estándar que define un conjunto de API, SDK y herramientas para varios lenguajes de programación, y el objetivo principal del proyecto es generar, recopilar, administrar y exportar datos.


Dicho esto, OpenTelemetry no ofrece un backend para herramientas de almacenamiento o visualización de datos.


Como solo nos interesaba el rastreo, exploramos las soluciones de código abierto más populares para almacenar y visualizar rastreos:

  • Cazador
  • Cremallera
  • Grafana Tempo


Finalmente, elegimos Grafana Tempo por sus impresionantes capacidades de visualización, su rápido ritmo de desarrollo y su integración con nuestra configuración de Grafana existente para la visualización de métricas. Tener una herramienta única y unificada también fue una ventaja significativa.

Componentes de OpenTelemetry

También diseccionaremos un poco los componentes de OpenTelemetry.


La especificación:

  • API: tipos de datos, operaciones, enumeraciones

  • SDK: implementación de especificaciones, API en diferentes lenguajes de programación. Un lenguaje diferente significa un estado de SDK diferente, desde alfa hasta estable.

  • Protocolo de datos (OTLP) y convenciones semánticas


La API de Java del SDK:

  • Bibliotecas de instrumentación de código
  • Exportadores: herramientas para exportar los rastros generados al backend
  • Propagadores de servicios cruzados: una herramienta para transferir el contexto de ejecución fuera del proceso (JVM)


El recopilador OpenTelemetry es un componente importante, un proxy que recibe datos, los procesa y los transmite: veámoslo más de cerca.

Recopilador OpenTelemetry

En el caso de los sistemas de alta carga que manejan miles de solicitudes por segundo, la gestión del volumen de datos es crucial. Los datos de seguimiento suelen superar en volumen a los datos empresariales, por lo que es esencial priorizar qué datos recopilar y almacenar. Aquí es donde entra en juego nuestra herramienta de filtrado y procesamiento de datos, que le permite determinar qué datos vale la pena almacenar. Normalmente, los equipos quieren almacenar los seguimientos que cumplen criterios específicos, como:


  • Rastros con tiempos de respuesta que exceden un cierto umbral.
  • Rastros que encontraron errores durante el procesamiento.
  • Rastros que contienen atributos específicos, como aquellos que pasaron por un determinado microservicio o fueron marcados como sospechosos en el código.
  • Una selección aleatoria de rastros regulares que proporcionan una instantánea estadística de las operaciones normales del sistema, ayudándole a comprender el comportamiento típico e identificar tendencias.

A continuación se presentan los dos métodos de muestreo principales que se utilizan para determinar qué rastros guardar y cuáles descartar:

  • Muestreo de cabeza : decide al comienzo de un seguimiento si conservarlo o no
  • Muestreo de cola : toma una decisión solo después de que esté disponible el rastro completo. Esto es necesario cuando la decisión depende de datos que aparecen más adelante en el rastro. Por ejemplo, datos que incluyen intervalos de error. Estos casos no se pueden manejar mediante el muestreo de cabeza, ya que requieren analizar primero todo el rastro.


El recopilador OpenTelemetry ayuda a configurar el sistema de recopilación de datos para que guarde solo los datos necesarios. Hablaremos de su configuración más adelante, pero por ahora, pasemos a la cuestión de qué es necesario cambiar en el código para que comience a generar rastros.

Instrumentación de código cero

Obtener la generación de seguimiento realmente requirió una codificación mínima: solo fue necesario iniciar nuestras aplicaciones con un agente Java, especificando configuración :


-javaagent:/opentelemetry-javaagent-1.29.0.jar

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


OpenTelemetry admite una gran cantidad de bibliotecas y marcos , por lo que después de iniciar la aplicación con el agente, recibimos inmediatamente seguimientos con datos sobre las etapas de procesamiento de solicitudes entre servicios, en el DBMS, etc.


En nuestra configuración de agente, deshabilitamos las bibliotecas que estamos usando cuyos intervalos no queríamos ver en los seguimientos y, para obtener datos sobre cómo funcionaba nuestro código, lo marcamos con anotaciones :


 @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 */); }


En este ejemplo, se utiliza la anotación @WithSpan para el método, que señala la necesidad de crear un nuevo intervalo llamado " acquire locks ", y el atributo " locks " se agrega al intervalo creado en el cuerpo del método.


Cuando el método termina de funcionar, el intervalo se cierra y es importante prestar atención a este detalle en el caso del código asincrónico. Si necesita obtener datos relacionados con el funcionamiento del código asincrónico en funciones lambda llamadas desde un método anotado, debe separar estas lambdas en métodos separados y marcarlos con una anotación adicional.

Nuestra configuración de recopilación de rastros

Ahora, hablemos sobre cómo configurar todo el sistema de recopilación de seguimiento. Todas nuestras aplicaciones JVM se lanzan con un agente Java que envía datos al recopilador OpenTelemetry.


Sin embargo, un único recopilador no puede gestionar un flujo de datos grande y esta parte del sistema debe escalarse. Si se lanza un recopilador independiente para cada aplicación JVM, el muestreo de cola se interrumpirá, porque el análisis de trazas debe realizarse en un recopilador y, si la solicitud pasa por varias JVM, los tramos de una traza terminarán en diferentes recopiladores y su análisis será imposible.


Aquí, un colector configurado mientras un equilibrador viene al rescate.


Como resultado, obtenemos el siguiente sistema: cada aplicación JVM envía datos al mismo recolector balanceador, cuya única tarea es distribuir los datos recibidos de diferentes aplicaciones, pero relacionados con una traza dada, al mismo recolector-procesador. Luego, el recolector-procesador envía los datos a Grafana Tempo.



Echemos un vistazo más de cerca a la configuración de los componentes de este sistema.

Colector de equilibrio de carga

En la configuración del recolector-balanceador, hemos configurado las siguientes partes principales:


 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 : donde se configuran los métodos (a través de los cuales el recolector puede recibir los datos). Hemos configurado la recepción de datos únicamente en formato OTLP. (Es posible configurar la recepción de datos a través de muchos otros protocolos , por ejemplo Zipkin, Jaeger.)
  • Exportadores : la parte de la configuración donde se configura el balanceo de datos. Entre los recopiladores-procesadores especificados en esta sección, los datos se distribuyen en función del hash calculado a partir del identificador de seguimiento.
  • En la sección Servicio se especifica la configuración de cómo funcionará el servicio: solo con trazas, utilizando el receptor OTLP configurado encima y transmitiendo datos como balanceador, es decir, sin procesamiento.

El recopilador con procesamiento de datos

La configuración de los recolectores-procesadores es más complicada, así que veámosla:


 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]


De manera similar a la configuración del recolector-balanceador, la configuración de procesamiento consta de las secciones Receptores, Exportadores y Servicios. Sin embargo, nos centraremos en la sección Procesadores, que explica cómo se procesan los datos.


En primer lugar, la sección tail_sampling demuestra una configuración que permite filtrar los datos necesarios para su almacenamiento y análisis:


  • latency500-policy : esta regla selecciona rastros con una latencia superior a 500 milisegundos.

  • error-policy : esta regla selecciona los rastros que encontraron errores durante el procesamiento. Busca un atributo de cadena llamado "error" con valores "true" o "True" en los intervalos de seguimiento.

  • probabilistic10-policy : esta regla selecciona aleatoriamente el 10% de todos los rastros para proporcionar información sobre el funcionamiento normal de la aplicación, los errores y el procesamiento de solicitudes largas.


Además de tail_sampling, este ejemplo muestra la sección resource/delete para eliminar atributos innecesarios que no son necesarios para el análisis y almacenamiento de datos.

Resultados

La ventana de búsqueda de rastros de Grafana resultante le permite filtrar datos según varios criterios. En este ejemplo, simplemente mostramos una lista de rastros recibidos del servicio de lobby, que procesa metadatos del juego. La configuración permite realizar un filtrado futuro por atributos como latencia, errores y muestreo aleatorio.


La ventana de vista de seguimiento muestra la línea de tiempo de ejecución del servicio de lobby, incluidos los distintos intervalos que componen la solicitud.


Como se puede ver en la imagen, la secuencia de eventos es la siguiente: se adquieren los bloqueos, luego se recuperan los objetos del caché, seguido de la ejecución de una transacción que procesa las solicitudes, después de lo cual los objetos se almacenan nuevamente en el caché y se liberan los bloqueos.


Los intervalos relacionados con las solicitudes de bases de datos se generaron automáticamente gracias a la instrumentación de las bibliotecas estándar. Por el contrario, los intervalos relacionados con la gestión de bloqueos, las operaciones de caché y el inicio de transacciones se agregaron manualmente al código comercial mediante las anotaciones mencionadas anteriormente.



Al visualizar un lapso, puede ver atributos que le permiten comprender mejor lo que sucedió durante el procesamiento, por ejemplo, ver una consulta en la base de datos.



Una de las características interesantes de Grafana Tempo es la gráfico de servicio , que muestra gráficamente todos los servicios que exportan rastros, las conexiones entre ellos, la velocidad y latencia de las solicitudes:


Terminando

Como hemos visto, trabajar con el rastreo de OpenTelemetry ha mejorado bastante nuestras capacidades de observación. Con cambios mínimos en el código y una configuración de recopilador bien estructurada, obtuvimos información detallada; además, vimos cómo las capacidades de visualización de Grafana Tempo complementaron aún más nuestra configuración. ¡Gracias por leer!