¡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.
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).
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
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
Para esta discusión, nos concentraremos en el aspecto de rastreo de OpenTelemetry.
Arrojemos también algo de luz sobre el proyecto OpenTelemetry, que surgió de la fusión de
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:
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.
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
La API de Java del SDK:
El recopilador OpenTelemetry es un componente importante, un proxy que recibe datos, los procesa y los transmite: veámoslo más de cerca.
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:
A continuación se presentan los dos métodos de muestreo principales que se utilizan para determinar qué rastros guardar y cuáles descartar:
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.
Obtener la generación de seguimiento realmente requirió una codificación mínima: solo fue necesario iniciar nuestras aplicaciones con un agente Java, especificando
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
OpenTelemetry admite una gran cantidad de
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
@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.
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
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.
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]
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
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.
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
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!