Trazado de servicios, OpenTracing y Jaeger

Trazado de servicios, OpenTracing y Jaeger

En nuestros proyectos utilizamos una arquitectura de microservicios. Cuando surgen cuellos de botella en el rendimiento, se gasta mucho tiempo en la monitorización y el análisis de los registros. Al registrar los tiempos de operaciones individuales en el archivo de registro, generalmente es difícil entender qué condujo a la llamada de estas operaciones, rastrear la secuencia de acciones o el desfase en el tiempo de una operación con respecto a otra en diferentes servicios.

Para minimizar el trabajo manual, decidimos utilizar una de las herramientas de trazado. De qué manera y para qué se puede utilizar el trazado y cómo lo hicimos, es de lo que trata este artículo.

Qué problemas se pueden resolver con el trazado

  1. Encontrar cuellos de botella en el rendimiento tanto dentro de un solo servicio como en todo el árbol de ejecución entre todos los servicios involucrados. Por ejemplo:
    • Muchas llamadas secuenciales cortas entre servicios, por ejemplo, para geocodificación o a una base de datos.
    • Largas esperas de entrada/salida, por ejemplo, la transferencia de datos a través de la red o la lectura desde el disco.
    • Un análisis prolongado de datos.
    • Operaciones largas que requieren CPU.
    • Secciones de código que no son necesarias para obtener el resultado final y que pueden ser eliminadas, o ejecutadas de forma diferida.
  2. Comprender visualmente en qué secuencia se llaman las operaciones y qué sucede cuando se ejecuta una operación.
    Trazado de servicios, OpenTracing y Jaeger
    Se puede ver que, por ejemplo, la Solicitud llegó al servicio WS -> el servicio WS complementó los datos a través del servicio R -> luego envió una solicitud al servicio V -> el servicio V cargó muchos datos del servicio R -> consultó al servicio P -> el servicio P volvió a consultar al servicio R -> el servicio V ignoró el resultado y fue al servicio J -> y solo después devolvió la respuesta al servicio WS, mientras continuaba calculando algo más en segundo plano.
    Sin este trazado o documentación detallada sobre todo el proceso, es muy difícil entender lo que está sucediendo al mirar el código por primera vez, además, el código está disperso en diferentes servicios y está oculto tras una multitud de beans e interfaces.
  3. Recopilación de información sobre el árbol de ejecución para un análisis diferido posterior. En cada etapa de la ejecución, se puede añadir información al trazado que esté disponible en esa etapa y luego entender qué datos de entrada llevaron a dicho escenario. Por ejemplo:
    • ID del usuario
    • Derechos
    • Tipo de método seleccionado
    • Registro o error de ejecución
  4. La transformación de las trazas en un subconjunto de métricas y el posterior análisis ya en forma de métricas.

Qué es capaz de registrar la trazabilidad. Span

En la trazabilidad existe el concepto de span, que es análogo a un registro en la consola. Un span tiene:

  • Nombre, que generalmente es el nombre del método que se ejecutó
  • Nombre del servicio en el que se generó el span
  • Identificador único propio
  • Alguna información meta en forma de key/value que se registró en él. Por ejemplo, parámetros del método o si el método terminó con un error o no
  • Tiempo de inicio y fin de la ejecución de este span
  • ID del span padre

Cada span se envía al collector de spans para ser guardado en la base de datos para su posterior revisión tan pronto como haya terminado su ejecución. Posteriormente, se puede construir un árbol de todos los spans conectándolos por el ID del padre. En el análisis, se puede encontrar, por ejemplo, todos los spans en un servicio que tardaron más de un tiempo específico. Luego, al acceder a un span concreto, se puede ver todo el árbol por encima y por debajo de ese span.

Trazado de servicios, OpenTracing y Jaeger

Opentrace, Jaeger y cómo lo implementamos en nuestros proyectos

Hay un estándar común Opentrace, que describe cómo y qué debe ser recopilado, sin estar atado la trazabilidad a una implementación específica en un lenguaje determinado. Por ejemplo, en Java, todo el trabajo con las trazas se realiza a través de una API común de Opentrace, y debajo de eso puede estar, por ejemplo, Jaeger o una implementación por defecto vacía que no hace nada.
Usamos Jaeger como implementación de Opentrace. Consiste en varios componentes:

Trazado de servicios, OpenTracing y Jaeger

  • Jaeger-agent — un agente local que normalmente se encuentra en cada máquina y al que los servicios registran en el puerto local por defecto. Si no hay agente, las trazas de todos los servicios en esta máquina están normalmente desactivadas.
  • Jaeger-collector — recibe todas las trazas recopiladas de los agentes y las coloca en la base de datos seleccionada.
  • Base de datos — la preferida es Cassandra, pero nosotros usamos Elasticsearch, tiene implementaciones también para un par de otras bases de datos y una implementación en memoria que no guarda nada en el disco.
  • Jaeger-query — es el servicio que accede a la base de datos y proporciona las trazas ya recopiladas para análisis.
  • Jaeger-ui — es la interfaz web para buscar y visualizar trazas, accede a jaeger-query.

Trazado de servicios, OpenTracing y Jaeger

Un componente aparte se puede considerar como la implementación de opentrace Jaeger para lenguajes específicos, a través de los cuales los spans se envían al jaeger-agent.
Conexión de Jaeger en Java se reduce a implementar la interfaz io.opentracing.Tracer, después de lo cual todos los rastros se enviarán a un agente real a través de él.

Trazado de servicios, OpenTracing y Jaeger

También se puede conectar para componentes de Spring opentracing-spring-cloud-starter y la implementación de Jaeger opentracing-spring-jaeger-cloud-starter que configurará automáticamente el rastreo en todo lo que pase a través de estos componentes, como las solicitudes http en los controladores, las consultas a la base de datos a través de jdbc, etc.

Registro de rastros en Java

En algún lugar en el nivel más alto se debe crear el primer Span, esto puede hacerse automáticamente por ejemplo mediante un controlador de Spring al recibir una solicitud, o manualmente si no hay uno. Luego se pasa a través del Scope hacia abajo. Si algún método inferior quiere añadir un Span, toma el activeSpan actual del Scope, crea un nuevo Span y le indica que su padre es el activeSpan recibido, y hace que el nuevo Span sea activo. Al llamar a servicios externos, se les pasa el span activo actual, y esos servicios crean nuevos spans vinculados a este span.
Todo el trabajo se realiza a través de una instancia de Tracer, que se puede obtener a través del mecanismo DI, o GlobalTracer.get() como una variable global, si el mecanismo DI no funciona. Por defecto, si el tracer no ha sido inicializado, regresará NoopTracer que no hace nada.
Luego, del tracer a través de ScopeManager se obtiene el scope actual, se crea un nuevo scope desde el actual vinculando el nuevo span, y más adelante se cierra el Scope creado, que cierra el span creado y regresa al estado activo el Scope anterior. El Scope está vinculado al hilo, por lo que al programar en múltiples hilos, no se debe olvidar pasar el span activo a otro hilo, para la posterior activación del Scope de otro hilo vinculado a este span.

io.opentracing.Tracer tracer = ...; // GlobalTracer.get()

void DoSmth () {
   try (Scope scope = tracer.buildSpan("DoSmth").startActive(true)) {
      ...
   }
}
void DoOther () {
    Span span = tracer.buildSpan("someWork").start();
    try (Scope scope = tracer.scopeManager().activate(span, false)) {
        // Hacer cosas.
    } catch(Exception ex) {
        Tags.ERROR.set(span, true);
        span.log(Map.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, ex, Fields.MESSAGE, ex.getMessage()));
    } finally {
        span.finish();
    }
}

void DoAsync () {
    try (Scope scope = tracer.buildSpan("ServiceHandlerSpan").startActive(false)) {
        ...
        final Span span = scope.span();
        doAsyncWork(() -> {
            // PASO 2 ARRIBA: reactivar el Span en la llamada de retorno, pasando true a
            // startActive() si/cuando el Span debe ser finalizado.
            try (Scope scope = tracer.scopeManager().activate(span, false)) {
                ...
            }
        });
    }
}

Para la programación multihilo también existen TracedExecutorService y envoltorios similares que automáticamente propagan el span actual en el hilo al ejecutar tareas asíncronas:

private ExecutorService executor = new TracedExecutorService(
    Executors.newFixedThreadPool(10), GlobalTracer.get()
);

Para solicitudes http externas hay TracingHttpClient

HttpClient httpClient = new TracingHttpClientBuilder().build();

Los problemas que hemos encontrado

  • Los beans y DI no siempre funcionan si el tracer no se usa en un servicio o componente, entonces Autowired Tracer puede no funcionar y habrá que usar GlobalTracer.get().
  • Las anotaciones no funcionan si no son un componente o servicio, o si la llamada al método se realiza desde otro método de la misma clase. Debes tener cuidado, verificar qué funciona y usar la creación manual del trace si @Traced no funciona. También se puede añadir un compilador adicional para las anotaciones java, y entonces deberían funcionar en todas partes.
  • En las versiones antiguas de spring y spring boot no funciona la autoconfiguración de opentracing spring cloud debido a errores en DI, entonces si se quiere que los traces funcionen automáticamente en los componentes de spring se puede hacer por analogía con github.com/opentracing-contrib/java-spring-jaeger/blob/master/opentracing-spring-jaeger-starter/src/main/java/io/opentracing/contrib/java/spring/jaeger/starter/JaegerAutoConfiguration.java
  • En groovy no funciona try with resources, es necesario usar try finally.
  • Cada servicio debe especificar su spring.application.name bajo el cual se registrarán los traces. Además, un nombre separado para producción y prueba, para no mezclarlos entre sí.
  • Si se utiliza GlobalTracer y tomcat, todos los servicios ejecutados en este tomcat tendrán un único GlobalTracer, por lo que tendrán el mismo nombre de servicio en todos.
  • Al añadir traces en un método, hay que estar seguro de que no se llama en un bucle muchas veces. Hay que añadir un trace común para todas las llamadas, que registre el tiempo total de ejecución. De lo contrario, se generará una carga excesiva.
  • Una vez en jaeger-ui hicimos solicitudes demasiado grandes sobre una gran cantidad de traces y, como no esperábamos la respuesta, hicimos otra vez. Como resultado, jaeger-query empezó a consumir mucha memoria y ralentizó elastic. Ayudó reiniciar jaeger-query.

Muestreo, almacenamiento y visualización de traces

Hay tres tipos de muestreo de traces:

  1. Constante que envía y guarda todos los traces.
  2. Probabilístico que filtra los traces con alguna probabilidad dada.
  3. Limitación de tasa que restringe el número de trazas por segundo. Se pueden configurar estos parámetros en el cliente, en jaeger-agent o en el colector. Actualmente, en nuestra pila de validadores se utiliza const 1 ya que no hay muchas solicitudes, pero estas tardan un tiempo considerable. En el futuro, si esto ejerce una carga excesiva en el sistema, se puede limitar.

Si se utiliza Cassandra, por defecto solo almacena trazas durante dos días. Nosotros usamos elasticsearch y las trazas se almacenan indefinidamente y no se eliminan. Para cada día se crea un índice separado, por ejemplo, jaeger-service-2019-03-04. En el futuro, es necesario configurar una limpieza automática de trazas antiguas.

Para ver las trazas, es necesario:

  • Seleccionar el servicio por el cual se desea filtrar las trazas, por ejemplo, tomcat7-default para el servicio que se está ejecutando en Tomcat y que no puede tener su propio nombre.
  • Luego, seleccionar la operación, el rango de tiempo y el tiempo mínimo de operación, por ejemplo, a partir de 10 segundos, para obtener solo las ejecuciones largas.
    Trazado de servicios, OpenTracing y Jaeger
  • Acceder a una de las trazas y ver qué estaba causando la ralentización.
    Trazado de servicios, OpenTracing y Jaeger

Además, si se conoce algún id de solicitud, se puede encontrar la traza por este id a través de búsqueda por etiquetas, siempre y cuando este id esté registrado en la traza del span.

La documentación

Artículos

Vídeo

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster