Identifica las causas de la latencia de las apps con Cloud Monitoring y OpenCensus

En este documento, se brinda información para ayudarte a identificar las causas de la latencia final con OpenCensus y Monitoring a fin de supervisar las métricas y el seguimiento distribuido de los desarrolladores de apps.

Cuando observas cómo la app entrega contenido a los usuarios, notas que algunos usuarios experimentan una latencia más alta que otros. Las diferencias se amplifican con grupos de usuarios más diversos y un mayor uso de los recursos. Identificar y abordar las causas de la latencia alta puede ayudarte a expandir la base de usuarios a un grupo más grande de usuarios con más niveles óptimos de uso de recursos. Si aumenta el uso de recursos, la latencia final es lo primero que se manifiesta debido a la contención de recursos.

Aunque los usuarios que experimentan estas latencias más altas pueden ser pocos, también pueden ser importantes, por ejemplo, si se trata de los líderes de un mercado potencial nuevo.

En este documento, se analiza cómo identificar y explicar las causas de la latencia alta que experimentan estos usuarios. En especial, el documento responde a las siguientes preguntas:

  • ¿Cómo puedes medir la latencia final con precisión?
  • ¿Cuáles son las principales causas de la latencia de la app?
  • ¿Cómo puedes evaluar las estrategias para minimizar la latencia final?

Los temas principales que responden estas preguntas son los siguientes:

  • Identificar las causas de la falta de precisión en los cálculos de las métricas de supervisión
  • Identificar las causas de la latencia alta
  • Minimizar la latencia final

El código fuente de la app de prueba se proporciona en un repositorio de GitHub.

En este documento, suponemos que estás familiarizado con el uso básico de OpenCensus, Cloud Monitoring, Cloud Trace, BigQuery y el desarrollo de apps en Java.

Terminología

Métrica
Es una medida de rendimiento de la app.
Latencia
Es el tiempo que tarda la app en realizar una tarea. Los valores de latencia de este documento se miden desde el código de la app.
Latencia final
Son los valores de latencia más altos que experimenta un conjunto pequeño de usuarios. La latencia final se puede definir en niveles de percentiles. Por ejemplo, el percentil 99 es el 1% más alto de los valores de latencia para las solicitudes medidas en un intervalo de tiempo. Para obtener más información, consulta la sección “Preocupaciones relacionadas con la latencia final” en el capítulo “Supervisión de sistemas distribuidos” de Ingeniería de confiabilidad de sitios.
Modelo de agregación
Es una representación de las métricas de supervisión que muestra la distribución estadística de las mediciones recopiladas. En este documento, se adopta un modelo de histograma con el rango de latencias dividido en bucket s y los recuentos de cada bucket registrados en cada intervalo de tiempo. En Monitoring, se usa el término “métrica de distribución” para las métricas con este tipo de representación.

Información general

Los propietarios de apps de Cloud suelen supervisar una combinación de valores de latencia mediana y final. La latencia mediana es un buen indicador del estado general de la app. Sin embargo, a medida que esta escala verticalmente, es posible que aparezcan problemas sutiles que no se pueden observar con facilidad a través de la latencia mediana.

Comprender la latencia final puede ser útil para varios propósitos:

  • Detectar a tiempo problemas emergentes que no son evidentes en la latencia mediana
  • Asegurarse de que no se logre un uso alto a costa de un control subóptimo de algunas solicitudes
  • Supervisar una flota cuando un solo servidor está en mal estado o cuando no se administra bien una ruta de código que solo una minoría de usuarios sigue
  • Informar estrategias a fin de minimizar la latencia final, como configurar valores apropiados para los tiempos de espera y los reintentos de retirada Puedes ajustar el tiempo de espera para que sea un poco mayor que la latencia del percentil 99 a fin de finalizar las conexiones y reintentarlas, en lugar de dejar que las solicitudes queden abiertas durante períodos prolongados.

Puede haber varias causas de latencia final. Las causas típicas son las siguientes:

  • Variación en el tamaño de la carga útil de la solicitud
  • Uso alto de CPU
  • Estructura de código de la aplicación no óptima
  • Variación en la distancia de enrutamiento de las llamadas a la red
  • Reinicios inesperados ocasionales del servidor con inicios en frío y conexiones interrumpidas asociadas
  • Almacenamiento en búfer o colas
  • Administración de conexiones, como el restablecimiento de conexiones inactivas
  • Congestión de la red, incluida la pérdida de paquetes
  • Recolección de elementos no utilizados o compactación de datos que llevan a la suspensión del control de las solicitudes de los usuarios
  • Latencia variable de los servicios en la nube
  • Configuración no óptima para el tiempo de espera de la conexión

Las estrategias para identificar las causas de la latencia son las siguientes:

  • Establecer métodos para la medición precisa de la latencia: debido a que la latencia final es por definición una proporción pequeña de tráfico, es necesario comprender la precisión de las mediciones.
  • Implementar soluciones de supervisión, seguimiento y registro: mediante OpenCensus y Monitoring, la app crea un seguimiento del intervalo raíz para las solicitudes que contienen intervalos secundarios para diferentes mediciones. Los intervalos secundarios se generan de forma automática con la integración HTTP de OpenCensus y se agrega la integración de seguimiento de registros a fin de grabar el contexto adicional. Esto minimiza el código adicional necesario para la supervisión y el seguimiento.
  • Explorar los datos: observa los informes y gráficos que muestran la cola de las solicitudes de larga duración e identifica qué intervalos secundarios llevan más tiempo en ejecutarse.
  • Identificar los motivos de la latencia final: recopila información de fuentes relacionadas que puedan ayudar a explicar los motivos de los intervalos secundarios prolongados y compárala con las anotaciones en los intervalos de muestra.
  • Comprender consultas más detalladas: exporta registros a BigQuery y combínalos con la información de seguimiento a fin de desglosar y cuantificar el comportamiento de la app.

El análisis que se describe en este documento es el resultado del análisis de los sistemas de tráfico alto que se escalan horizontalmente con Cloud Storage y los microservicios. Las causas principales de la variabilidad en la latencia final fueron los errores de medición, el tamaño de la carga útil, la latencia variable de los servicios en la nube y la carga en las máquinas virtuales que alojan la app.

Los usuarios de la nube pueden elegir entre usar métricas de integraciones ya compiladas en bibliotecas, métricas compatibles de proveedores de servicios en la nube y métricas personalizadas. Algunos ejemplos de métricas de integraciones ya compiladas son las integraciones de gRPC, HTTP y SQL de OpenCensus. Google Cloud incluye una gran cantidad de métricas compatibles, que se exponen a través de Monitoring. Otra alternativa es desarrollar código para las métricas personalizadas adicionales, que suelen ser temporizadores y contadores. La comparación de series temporales de estas tres fuentes es una tarea usual en el análisis de la fuente de latencia en los microservicios y el almacenamiento de datos de app.

Descripción general de la app de prueba

La app de prueba simula problemas de latencia final y se integra en OpenCensus y Monitoring para demostrar las herramientas y los enfoques necesarios a fin de identificar los problemas. El código fuente, los archivos de compilación y las instrucciones se proporcionan en el proyecto de GitHub. La app de ejemplo está escrita en Java, pero puedes aplicar este documento a otros lenguajes. Puedes obtener más información sobre la instrumentación de OpenCensus de las solicitudes HTTP en Jetty Integration for Java (Integración de Jetty para Java) y en Go. La app de prueba se basa en este código de ejemplo de GitHub.

A continuación, se muestra un diagrama esquemático de la app.

Diagrama de la arquitectura de la app cliente que se comunica con Cloud Storage y la app del servidor en Compute Engine.

La app cliente se comunica con Cloud Storage y una app del servidor alojada en una instancia de Compute Engine.

Este es un flujo de solicitud típico:

  1. La API de Cloud Storage recupera un archivo JSON de Cloud Storage en la app cliente.
  2. El contenido del archivo se envía al servidor.
  3. El servidor procesa los datos que envía el cliente para simular el tiempo y los recursos que ocuparía una app real.
  4. Se envía una respuesta HTTP al cliente.
  5. El cliente realiza el procesamiento posterior. En este punto, la app de prueba sustituye la aritmética simple por la lógica empresarial.

Durante el inicio, la app inicializa lo siguiente:

  • Java Util Logging (JUL) de cliente para exportar Cloud Logging con los ID de seguimiento incluidos. El servidor usa un adjuntador de registros de Logback para la correlación de registros de seguimiento.
  • Se registran las vistas de las métricas de HTTP de OpenCensus. La integración de HTTP para Java de OpenCensus incluye varias vistas de métricas de latencia, código de respuesta HTTP y bytes enviados y recibidos.
  • El exportador stats de Monitoring de OpenCensus envía métricas agregadas a Monitoring en un subproceso en segundo plano.
  • El exportador de seguimiento de OpenCensus para Monitoring envía intervalos de seguimiento a un subproceso en segundo plano.
  • La API de cliente de Cloud Storage descarga los archivos que se usan para cargas útiles de HTTP.
  • Se crea un grupo de subprocesos de la app para dirigir la carga a la app. Varios subprocesos generan una carga mayor en el servidor que un solo subproceso.

Las métricas de Monitoring y los datos de seguimiento se recopilan y procesan mediante los siguientes pasos:

  1. Las bibliotecas de OpenCensus recopilan las métricas y realizan el seguimiento de los datos.
  2. OpenCensus agrega de forma periódica datos de métricas y los exporta a Monitoring. Los datos de seguimiento se exportan a Trace sin agregación.
  3. OpenCensus agrega los ID de seguimiento a las entradas de Cloud Logging. Las entradas de registro se exportan al servicio de Cloud Logging.

Puedes exportar las métricas recopiladas con OpenCensus a varios sistemas de supervisión de backend que proporcionan almacenamiento, análisis, visualización y alertas. Cada sistema tiene un formato de representación de datos diferente. En este documento, se describe Monitoring.

Precisión de las métricas de supervisión agregadas

OpenCensus y Monitoring proporcionan herramientas que puedes usar para supervisar el rendimiento de la app. Para evitar una falsa sensación de confianza en las líneas de los gráficos, debes comprender cómo se crearon, en especial, en el caso de las líneas que representan la latencia final, como el percentil 99 (p99). En las siguientes secciones, se analizan las limitaciones de la agregación de datos de métricas.

Representación de series temporales de métricas en Monitoring

Monitoring calcula los percentiles de latencia para las métricas de distribución en función de los límites del bucket en intervalos numéricos. Este es un método común que usan Monitoring y OpenCensus, en el que OpenCensus representa y exporta datos de métricas a Monitoring. El método TimeSeries.list para la API de Cloud Monitoring muestra los recuentos de depósitos y límites de los tipos de métricas y del proyecto. Puedes recuperar los límites del bucket en el objeto BucketOptions de la API de Cloud Monitoring, con el que puedes experimentar en el Explorador de API para TimeSeries.list.

Para filtrar la latencia del cliente HTTP de OpenCensus en el Explorador de API, puedes usar el siguiente filtro:

resource.type=global metric.type="custom.googleapis.com/opencensus/opencensus.io/http/client/roundtrip_latency"

El resultado incluye un arreglo de objetos de datos. En el siguiente ejemplo, se muestra el esquema de depósitos y los recuentos de depósitos:

"points": [ {
  "interval": { "startTime": "2019-02-14T18:11:24.971Z",
                "endTime": "2019-02-14T18:33:35.541Z" },
  "value": {
    "distributionValue": {
...
    "explicitBuckets": {
      "bounds": [0, 1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130, ...] }
              },
      "bucketCounts": [0, 0, 0, 0, 0, 14, 17, 164, 3, 0, 1, 0, 0, 0, 0, ...]
}}]

Cada punto que ves en un gráfico de Monitoring para un valor de latencia deriva de una distribución. Puedes definir los bucket s mediante un factor de crecimiento, en lugar de definir límites para cada bucket. Por ejemplo, un factor de crecimiento de 2.0 daría límites de bucket de 0 a 1, de 1 a 2, de 2 a 4, de 4 a 8,… Para obtener más información sobre las métricas de Monitoring, consulta Estructura de los tipos de métricas.

Puedes editar el gráfico para que se muestren las líneas de latencia de los percentiles 50 (mediana), 95 y 99 a fin de obtener el siguiente gráfico de líneas.

Gráfico de latencia final en el que se muestran los percentiles 50, 95 y 99.

Para comprender los datos que se usan a fin de calcular estos valores de percentiles, agrega el tipo de gráfico de mapa de calor. Cuando el agregador se establece en suma, se muestra el siguiente mapa de calor de latencia del cliente.

Gráfico de mapa de calor de latencia final

Los mapas de calor son una forma de mostrar los datos de distribución que almacena Monitoring. Las áreas del mapa de calor corresponden a los límites del bucket que se recuperaron mediante la API de REST. Puedes mantener el cursor sobre las áreas del mapa de calor para averiguar cuántos puntos contiene un bucket determinado durante un período. Los datos que se usan para calcular los valores de percentiles en el gráfico de líneas están dispersos.

Si deseas obtener mediciones de latencia más precisas, puedes recurrir a estas estrategias disponibles:

  • Agrega más datos. Puedes generar más datos en los experimentos, pero es posible que el tráfico a una app de producción no sea lo bastante alto.
  • Reduce el tamaño de los depósitos y cambia los límites para que se adapten mejor a tus datos. Puedes hacerlo si creas tus propias métricas personalizadas. Sin embargo, es posible que no puedas realizar estos ajustes si usas datos de integraciones de OpenCensus ya compiladas o recopiladas por Google Cloud de sus servicios de backend. Los límites de depósitos para las integraciones de OpenCensus están determinados por los implementadores de las integraciones de OpenCensus en las vistas. Por ejemplo, los límites de depósitos de latencia del cliente HTTP se definen en la clase HttpViewConstants.java. Del mismo modo, las métricas de Google Cloud se definen de forma interna en Monitoring.
  • Usa seguimientos o registros en lugar de datos de métricas para los percentiles de latencia final. Estas opciones se exploran en las secciones posteriores de este documento.

Datos simulados

Puedes comparar la precisión de los modelos de agregación con datos simulados sin procesar que se extrajeron de una distribución aleatoria para ver la diferencia que pueden hacer los límites de depósitos. En este ejemplo, una distribución Beta modela la distribución sesgada típica de los valores de latencia reales. Esta es la distribución de una variable aleatoria continua sin valores menores que cero, con un máximo distinto y una latencia final descendente prolongada. La distribución Beta usa la función random.beta de NumPy con parámetros a = 2, b = 5 y se multiplica por un factor constante de 70 para simular valores de latencia realistas con un valor mediano de 18.5 ms. La simulación incluye 100,000 valores aleatorios de la distribución Beta en la hoja de Colab tail_latency_lab.ipynb incluida en el proyecto de GitHub.

En el siguiente gráfico, se muestra que los depósitos están muy poco separados con growthFactor = 2.0 como para proporcionar estimaciones precisas de latencias finales.

Modelo de agregación que muestra un factor de crecimiento de 2.0.

En el eje lineal del gráfico, se muestra la forma prevista de la distribución aleatoria. En el siguiente gráfico, se muestran los límites de depósitos con growthFactor = 1.4.

Modelo de agregación que muestra un factor de crecimiento de 1.4.

En el siguiente gráfico, se muestra que los límites de depósitos con growthFactor = 1.2 se asemejan más a la forma prevista, pero aún son un poco imprecisos en el extremo superior de la distribución.

Modelo de agregación que muestra un factor de crecimiento de 1.2.

En la siguiente tabla, se muestra una comparación de las estimaciones en función de la distribución basada en depósitos y un cálculo basado en valores sin procesar.

Distribución Mediana Percentil 99
Sin procesar 18.5 49.5
Factor de crecimiento 2.0 19.2 62.0
Factor de crecimiento 1.4 18.7 53.9
Factor de crecimiento 1.2 18.6 51.1

En este caso, se requiere un factor de crecimiento de 1.2 para obtener una estimación bastante precisa de la latencia del percentil 99. Desde el exportador de OpenCensus, puedes convertir los valores sin procesar en una distribución de muestreo. Cuando creas una vista de OpenCensus, puedes configurar los límites de depósitos. Las vistas contienen un objeto Aggregation. Para obtener más información, consulta la documentación de Java o la documentación de Go.

Medición de la latencia HTTP mediante OpenCensus

Cuando se mide la latencia de HTTP, se mide el tiempo necesario para enviar y recibir una solicitud HTTP de un cliente a un servidor y registrar el resultado. Para implementar esto en el código, puedes crear un Tag de OpenCensus o usar la integración ya compilada para clientes HTTP. La integración de HTTP se proporciona en Go con el paquete HTTP de Go y en Java con el cliente HTTP de Jetty. La ventaja de usar la integración de HTTP de OpenCensus es que no tienes que agregar objetos Tag para cada métrica que deseas recopilar.

Identifica las fuentes de latencia

La app de prueba que se describió antes generó estrés en sí misma, lo que generó una mayor latencia final. Debido a esto, se perciben los siguientes problemas:

  • Cargas útiles grandes (en ocasiones)
  • Uso alto de CPU
  • Tiempos de espera cortos
  • Variaciones en la ruta del código de la app con reintentos

En las siguientes secciones, se describe cómo detectar y cuantificar estos problemas.

Efecto de una carga útil grande

Para investigar el efecto del tamaño de la carga útil, mediante el siguiente código, se aumentó de forma aleatoria el tamaño de la carga útil que se envió en 1 de 20 solicitudes HTTP:

static byte[] getContent(String bucket) {
  BlobId blobId = null;
  int n = rand.nextInt(100);
  if (n >= 95) {
    blobId = BlobId.of(bucket, LARGE_FILE);
  } else {
    blobId = BlobId.of(bucket, SMALL_FILE);
  }
  return storage.readAllBytes(blobId);
}

Cuando ves los seguimientos de las solicitudes de latencia más alta en la Lista de seguimientos (Trace list), se pueden correlacionar con un tamaño de carga útil más grande.

Lista de seguimientos en la que se muestran solicitudes de latencia más alta.

Si haces clic en Mostrar eventos, puedes ver más detalles sobre la carga útil enviada y recibida. Si hay una latencia alta, se muestra un tamaño de carga útil más grande.

Cloud Trace es mejor que Monitoring para identificar la latencia por las siguientes razones:

  • Trace proporciona valores de latencia de las solicitudes en lugar de valores agregados.
  • Trace proporciona el tamaño de la carga útil para la solicitud.

Efecto del uso alto de CPU

El uso alto de CPU puede causar una mayor latencia final debido al procesamiento simultáneo, lo que puede provocar la contención de recursos y la puesta en cola de la CPU para controlar las solicitudes. Monitoring proporciona una métrica integrada para el uso de CPU de las instancias de Compute Engine. Sin embargo, cuando ejecutes pruebas de rendimiento o investigues problemas de rendimiento, te recomendamos que instales el agente de Monitoring, porque proporciona más métricas de código abierto y sistemas operativos, incluidas las métricas de memoria, de uso del disco y otras que pueden mostrar distintos tipos de contención de recursos. Para obtener más información, consulta la lista de métricas del agente.

Administrar el uso de CPU es más fácil para las apps de servidor que para las apps cliente debido al ajuste de escala automático. En Compute Engine, un grupo de instancias administrado puede usar una política de ajuste de escala automático en función del uso promedio de CPU, la capacidad de entrega de balanceo de cargas, las métricas de Monitoring y los programas. Un escalador automático agrega máquinas virtuales para ajustar el tamaño de la flota de forma adecuada a fin de controlar la carga. El uso de CPU del cliente puede ser más difícil de administrar de manera coherente y puede afectar la medición de la latencia, ya que las cargas útiles deben leerse desde la red y deserializarse, lo que requiere el uso de CPU. Debido a que la vista de la latencia de la app cliente determina la experiencia del usuario en mayor medida, en este documento, nos enfocamos en ese tipo de latencia.

Monitoring es más apropiado que el seguimiento para identificar el efecto del uso alto de CPU, ya que este uso no suele ser el resultado de una solicitud HTTP. En el siguiente ejemplo, el efecto del uso de CPU se investiga mediante la app de prueba para ejecutar varios subprocesos de ejecución a la vez y por medio de la ejecución del código de cliente en una máquina virtual g1-small. Luego, el uso de CPU y la latencia se comparan con los valores de la app cliente que se ejecuta en una máquina virtual n1-standard-1 con CPU virtual.

En los siguientes gráficos, se muestra la latencia mediana y la latencia de extremo a extremo del percentil 95 de un experimento. El porcentaje de solicitudes se incrementó a las 3:21 a.m. mediante la eliminación de una declaración de suspensión entre las solicitudes para mostrar la diferencia en la latencia con un gran aumento en el uso de CPU. Los comandos que permiten crear las máquinas virtuales y ejecutar las pruebas están disponibles en el repositorio de GitHub.

Gráfico de latencia mediana por ID de instancia

En el gráfico anterior, se muestra la latencia mediana para el cliente y el servidor durante un experimento de carga. Observa el aumento a las 3:21 a.m. cuando se aumentó la carga. En el siguiente gráfico, se muestra la latencia del percentil 95 durante la misma prueba.

Gráfico de latencia del percentil 95 por ID de instancia

En el siguiente gráfico, se muestra el uso de CPU para la prueba. También hay un aumento a las 3:21 a.m.

Gráfico de uso de CPU de la instancia de Compute Engine

El aumento del porcentaje de solicitudes afecta la latencia, pero no en proporción al uso de CPU. Es sorprendente que la latencia sea coherente en la instancia pequeña y en una CPU virtual, aunque la instancia pequeña se ejecutaba con un uso de CPU de alrededor del 78%. Este resultado sugiere que puedes ejecutar una flota con un uso alto de CPU. Como advertencia, ten en cuenta que esta prueba no es un estudio comparativo, y los resultados pueden variar durante la implementación de la app.

Cloud Profiler es una herramienta útil para investigar instancias de alto consumo de CPU.

Latencia debido a las ramas del código de la app y los valores de los parámetros

Logging puede ayudarte a resolver los problemas de latencia más complejos de la app. En esta sección, se muestra cómo combinar los datos de registro y seguimiento a fin de investigar los problemas de latencia que se producen por las diferentes rutas de código y los valores de los parámetros. Un enfoque de seguimiento para registrar evita que debas agregar varias declaraciones de tiempo en el código. El problema común en las apps basadas en microservicios es la administración de los tiempos de espera y los reintentos. Los tiempos de espera prolongados pueden tener un efecto negativo en la experiencia del usuario y pueden bajar la eficiencia de una app si vinculan demasiadas conexiones. Te recomendamos configurar un tiempo de espera breve, esperar un momento y volver a intentarlo. Sin embargo, los reintentos dinámicos pueden generar tormentas de reintentos y dificultar la recuperación ante incidentes. Debes establecer el tiempo de espera para que solo un porcentaje pequeño de solicitudes, por ejemplo, menos del 1%, requiera reintentos. Sin embargo, la app de prueba establece el tiempo de reintento en un valor bajo para las solicitudes HTTP al servidor de prueba a fin de demostrar los reintentos en acción sin tener que esperar que ocurra una interrupción real.

Los reintentos también se implementan en la biblioteca cliente de la API de Google para Cloud Storage, por lo que no necesitas implementarlos en tu propio código. Para obtener más información sobre la retirada exponencial, consulta Retirada exponencial truncada.

Con la integración de la correlación de registro de OpenCensus para Monitoring, puedes ver las entradas de registro de los seguimientos. Esta integración usa un Enhancer de Cloud Logging para agregar información de seguimiento a los registros. Puedes agregarlo a la app si configuras Logging para Java y agregas la dependencia opencensus-contrib-log-correlation-stackdriver1 de Maven a la compilación. Cuando se escribió este documento, no se implementó la integración de registros con Go, pero puedes solicitar esa función. La alternativa actual para Go es agregar anotaciones a fin de hacer un seguimiento de los intervalos con la información requerida.

El método en la app de prueba para preparar, enviar y procesar solicitudes al microservicio es el siguiente:

private void prepareSendProcess(
    HttpClient httpClient,
    HttpMethod method,
    Function<Integer[], Integer> downStreamFn,
    String fnName)
    throws InterruptedException {
  Tracer tracer = Tracing.getTracer();
  try (Scope scope = tracer.spanBuilder("main").startScopedSpan()) {
    StopWatch s = StopWatch.createStarted();
    byte[] content = new byte[0];
    if (method == HttpMethod.POST) {
      content = TestInstrumentation.getContent(testOptions.bucket());
    }
    byte[] payload = sendWithRetry(httpClient, method, content);
    TestInstrumentation.processPayload(payload, downStreamFn, fnName);
    TestInstrumentation.recordTaggedStat(
        method.toString(), s.getTime(TimeUnit.NANOSECONDS) / 1.0e6);
  }
}

El método crea un intervalo con alcance como un seguimiento de nivel superior para la solicitud que abarca el código de todas las fases de preparación, el envío de la solicitud HTTP al servidor y el procesamiento posterior. El método prepareSendProcess llama a sendWithRetry, que une la solicitud HTTP con un mecanismo de reintento:

private byte[] sendWithRetry(HttpClient httpClient, HttpMethod method, byte[] content)
    throws InterruptedException {
  ExponentialBackOff backoff = new ExponentialBackOff.Builder()
    .setInitialIntervalMillis(500)
    .setMaxElapsedTimeMillis(5*60*1000)
    .setMultiplier(2.0)
    .setRandomizationFactor(0.5)
    .build();
  for (int i = 0; i < MAX_RETRIES; i++) {
    try {
      return sendRequest(httpClient, method, content);
    } catch (RetryableException e) {
      LOGGER.log(Level.WARNING, "RetryableException attempt: " + (i + 1) + " " + e.getMessage());
    } catch (InterruptedException e) {
      LOGGER.log(
          Level.WARNING, "InterruptedException attempt: " + (i + 1) + " " + e.getMessage());
    } catch (TimeoutException e) {
      LOGGER.log(Level.WARNING, "TimeoutException attempt: " + (i + 1) + " " + e.getMessage());
    } catch (ExecutionException e) {
      LOGGER.log(Level.WARNING, "ExecutionException attempt: " + (i + 1) + " " + e.getMessage());
    }
    try {
      Thread.sleep(backoff.nextBackOffMillis());
    } catch(IOException e) {
      throw new RuntimeException("MaxElapsedTime exceeded");
    }
  }
  throw new RuntimeException("Max retries exceeded");
}

El siguiente método envía la solicitud HTTP:

private byte[] sendRequest(HttpClient httpClient, HttpMethod method, byte[] content)
    throws InterruptedException, TimeoutException, ExecutionException, RetryableException {
  String targetURL = testOptions.targetURL();
  HttpRequest request = (HttpRequest) httpClient.newRequest(targetURL).method(method);
  if (request == null) {
    throw new RetryableException("Request is null");
  }
  if (method == HttpMethod.POST) {
    ContentProvider contentProvider =
        new StringContentProvider(new String(content, StandardCharsets.UTF_8));
    request.content(contentProvider, "application/json");
  }
  request.timeout(testOptions.httpTimeout(), TimeUnit.MILLISECONDS);
  ContentResponse response = request.send();
  int status = response.getStatus();
  LOGGER.info("Response status: " + status + ", " + method);
  if (HttpStatus.isSuccess(status)) {
    byte[] payload = response.getContent();
    LOGGER.info("Response payload: " + payload.length + " bytes");
    return payload;
  } else if (HttpStatus.isServerError(status)) {
    throw new RetryableException(response.getReason());
  }
  return new byte[0];
}

Este método crea un objeto HTTPRequest y, luego, agrega la carga útil si se envía una solicitud POST. A continuación, se detalla un seguimiento de ejemplo.

Seguimiento de ejemplo de un objeto “HTTPRequest”.

En Monitoring, cuando haces clic en Mostrar registros, los registros se muestran en los detalles de seguimiento. Cuando haces clic en un ícono de Detalles de seguimiento, como se describe en , aparece la información de seguimiento. Puedes hacer coincidir el texto del registro con el código fuente para comprender qué ruta de código se tomó a fin de generar el seguimiento visto y descubrir detalles de otros parámetros, como la respuesta y el método HTTP. En el ejemplo anterior, se muestra que se produjo un tiempo de espera en este seguimiento. El intervalo es largo, ya que la retirada lo penaliza. También hay seis reintentos, que es el número máximo configurado.

Para ver los eventos de seguimiento, haz clic en Mostrar eventos.

Eventos de seguimiento con latencia alta.

La latencia alta se generó debido a una carga útil grande. El tiempo de espera de 8 ms era demasiado bajo para controlar esta carga útil tan grande. El tiempo de espera debe restablecerse a un valor más alto.

Puedes agregar anotaciones para hacer un seguimiento de los intervalos con información similar a las declaraciones de registro. Sin embargo, agregar información clave sobre las diferentes rutas de código en la app a los intervalos de seguimiento requiere que agregues el código para estos intervalos en el código de la app. Si tu equipo de desarrollo no está familiarizado con el seguimiento, podría ser más fácil para ellos agregarlo en las instrucciones de registro con JUL o Logback y configurar Logging para Java. Además, es posible que ya tengas instrucciones de registro en el código.

Para identificar las solicitudes más lentas, puedes generar un informe de análisis de seguimiento, que incluya seguimientos de muestra en percentiles de solicitud diferentes, como se ve en la siguiente captura de pantalla.

Informe de análisis de seguimiento con seguimientos de muestra en percentiles de solicitudes diferentes

Para ver seguimientos detallados de los percentiles más altos, puedes hacer clic en los seguimientos de muestra.

Combina las instrucciones de registro y los ID de seguimiento en BigQuery

Para buscar más información sobre registros, puedes consultar los registros directamente en el visor de registros de Cloud Logging con un filtro.

Consulta de registro con un filtro de registro.

Una capacidad útil de este filtro es que conecta tres entradas de registro con el mismo ID de seguimiento. Puedes conectar entradas de registro de la misma solicitud con los ID de seguimiento. Luego, puedes exportar las entradas de registro a BigQuery con un filtro como resource.type="gce_instance".

Filtro de registro para la instancia de Compute Engine

Si exportas los registros a un conjunto de datos llamado application_test_dataset, puedes explorarlos con la siguiente consulta:

SELECT
  TIME(timestamp) AS Time,
  SUBSTR(textPayload, 90) as Message,
  SUBSTR(trace, 31) as trace_id
FROM
  `application_test_dataset.java_log_20190328`
WHERE REGEXP_CONTAINS(textPayload, "attempt")
LIMIT 10;
...
Time             Message                            trace_id
10:59:11.782000  WARNING: TimeoutException attempt: 008a0ce...

Se muestra una fila de datos de muestra. El resultado de esta consulta proporciona información similar al visor de registros. Sin embargo, ahora que los datos de registro están en BigQuery, puedes realizar consultas más potentes. Mediante la siguiente consulta, se muestra cuántas solicitudes tuvieron una cantidad diferente de reintentos:

SELECT
  RetryCount,
  COUNT(trace) AS RequestCount
FROM(
  SELECT
    COUNT(REGEXP_CONTAINS(textPayload, "attempt")) as RetryCount,
    trace
  FROM
    `application_test_dataset.java_log_20190328`
  WHERE NOT REGEXP_CONTAINS(trace, '00000000000000000000000000000000')
  Group BY trace
)
GROUP BY RetryCount
ORDER BY RetryCount DESC
LIMIT 10;
...
RetryCount RequestCount
8          13
7          180
6          2332
5          242
4          507
3          416605
1          1

En los resultados de la consulta, puedes ver que hubo 13 solicitudes con el máximo de 8 recuentos de reintentos. Puedes usar estos resultados para definir mejor los parámetros de reintento. Por ejemplo, puedes aumentar y variar el tiempo de espera con el tamaño de la carga útil o aumentar la cantidad máxima de reintentos.

Próximos pasos