Capítulo 4. Gráficos y alertas

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

El monitoreo no tiene por qué ser una propuesta "todo incluido". Si sólo añades una medida de la tasa de errores para las interacciones con el usuario final en las que no tienes monitoreo (o sólo monitoreo de recursos como la utilización de CPU/memoria), ya has dado un gran paso adelante en términos de comprensión de tu software. Al fin y al cabo, la CPU y la memoria pueden tener buen aspecto, pero una API orientada al usuario falla en el 5% de todas las solicitudes, y la tasa de fallos es una idea mucho más fácil de comunicar entre las organizaciones de ingeniería y sus socios comerciales.

Mientras que en los Capítulos 2 y 3 se trataron diferentes formas de instrumentación del monitoreo, aquí presentamos las formas en que podemos utilizar esos datos eficazmente para promover la acción mediante la alerta y la visualización. Este capítulo abarca tres temas principales.

En primer lugar, debemos pensar en lo que constituye una buena visualización de un SLI. Sólo vamos a mostrar gráficos de la herramienta de gráficos y alertas Grafana, de uso común, porque es una herramienta de código abierto disponible gratuitamente que tiene complementos de fuentes de datos para muchos sistemas de monitoreo diferentes (por lo que aprender un poco de Grafana es una habilidad en gran medida transferible de un sistema de monitoreo a otro). Muchas de las mismas sugerencias se aplican a las soluciones de gráficos integradas en productos de proveedores.

A continuación, hablaremos específicamente de las mediciones que generan más valor y de cómo visualizarlas y alertar sobre ellas. Trátalos como una lista de control de los SLI que puedes añadir de forma incremental. El incrementalismo puede ser incluso preferible a implantarlos todos a la vez, porque al añadir un indicador cada vez, puedes estudiar y comprender realmente lo que significa en el contexto de tu empresa y darle forma poco a poco para que te genere el mayor valor. Si entrara en el centro de operaciones de red de una compañía de seguros, me sentiría mucho más aliviado de ver sólo indicadores sobre el índice de error en la calificación y presentación de pólizas que de ver un centenar de señales de bajo nivel y ninguna medida del rendimiento empresarial.

Adoptar un enfoque gradual para introducir las alertas es también un importante ejercicio de creación de confianza. Introducir demasiadas alertas demasiado deprisa corre el riesgo de abrumar a los ingenieros y provocar "fatiga de alertas". Quieres que los ingenieros se sientan cómodos suscribiéndose a más alertas, ¡no que las silencien! Además, formar a los ingenieros sobre cómo responder a una condición de alerta cada vez ayuda al equipo a crear una reserva de conocimientos sobre cómo abordar las anomalías.

Por eso, en este capítulo nos centraremos en dar consejos sobre aquellos SLI que estén lo más cerca posible del rendimiento empresarial (por ejemplo, la tasa de fallos de la API y los tiempos de respuesta que ven los usuarios), sin estar vinculados a ningún negocio en particular. En la medida en que cubramos cosas como la utilización de la pila o los descriptores de archivo, serán un grupo selecto de indicadores que tienen más probabilidades de ser la causa directa de la degradación del rendimiento empresarial.

Recrear el control de misión de la NASA(Figura 4-1) no debe ser el resultado final de un sistema distribuido bien monitorizado. Aunque disponer pantallas a lo largo de una pared y llenarlas de cuadros de mando puede resultar visualmente impresionante, las pantallas no son acciones. Requieren que alguien las esté mirando para responder a un indicador visual de un problema. Creo que esto tiene sentido cuando estás monitorizando una única instancia de un cohete con costes desorbitados y vidas humanas en juego. Tus peticiones a la API, por supuesto, no tienen la misma importancia por ocurrencia.

srej 0401
Figura 4-1. ¡Éste no es un buen modelo a seguir!

Casi todos los recopiladores de métricas recogerán más datos de los que te resultarán útiles en un momento dado. Aunque cada métrica puede tener utilidad en alguna circunstancia, trazar cada una de ellas no es útil. Sin embargo, varios indicadores (por ejemplo, la latencia máxima, la proporción de errores, la utilización de recursos) son señales sólidas de fiabilidad para prácticamente todos los microservicios Java (con algunos ajustes en los umbrales de alerta). En estos nos centraremos.

Por último, el mercado está ansioso por aplicar métodos de inteligencia artificial a los datos de monitoreo para automatizar la entrega de información sobre tus sistemas sin necesidad de comprender demasiado los criterios de alerta y los indicadores clave de rendimiento. En este capítulo, examinaremos varios métodos estadísticos tradicionales y métodos de inteligencia artificial en el contexto del monitoreo de aplicaciones. Deberías tener un conocimiento sólido de los puntos fuertes y débiles de cada método para que puedas dejar de lado el bombo publicitario y aplicar los mejores métodos para tus necesidades.

Antes de seguir adelante, merece la pena considerar la gran variedad de sistemas de monitoreo que hay en el mercado y el impacto que ello tiene en tus decisiones sobre cómo instrumentar el código y obtener datos de estos sistemas.

Diferencias en los sistemas de monitoreo

El motivo de hablar aquí de las diferencias en los sistemas de monitoreo es que vamos a ver detalles concretos sobre cómo trazar gráficos y alertas con Prometheus. Un producto como Datadog tiene un sistema de consulta muy diferente al de Prometheus. Ambos son útiles. En el futuro van a surgir más productos con capacidades que aún no imaginamos. Idealmente, queremos que nuestra instrumentación de monitoreo (lo que pondremos en nuestras aplicaciones) sea portable a través de estos sistemas de monitoreo sin que se requieran cambios en el código de la aplicación (aparte de una nueva dependencia binaria y alguna configuración a nivel de registro ).

Suele haber bastante más coherencia en la forma en que los sistemas backend de seguimiento distribuido reciben los datos que en la forma en que los reciben los sistemas de métricas. Las bibliotecas de instrumentación del seguimiento distribuido pueden tener diferentes formatos de propagación, lo que requiere cierto grado de uniformidad en la selección de una biblioteca de instrumentación en toda la pila, pero los datos en sí son fundamentalmente similares de backend a backend. Esto tiene sentido intuitivamente debido a lo que son los datos: el rastreo distribuido consiste realmente en información temporal por evento (cosida contextualmente por ID de rastreo).

Los sistemas de métricas podrían representar potencialmente no sólo información de tiempo agregada, sino también indicadores, contadores, datos de histograma, percentiles, etc. No se ponen de acuerdo sobre la forma en que deben agregarse estos datos. No tienen las mismas capacidades para realizar agregaciones o cálculos posteriores en el momento de la consulta. Existe una relación inversa entre el número de series temporales que debe publicar una biblioteca de instrumentación de métricas y las capacidades de consulta de un determinado backend de métricas, como se muestra en la Figura 4-2.

srej 0402
Figura 4-2. Relación inversa entre series temporales publicadas y capacidades de consulta

Así, por ejemplo, cuando Dropwizard Metrics se desarrolló inicialmente, el sistema de monitoreo popular era Graphite, que no disponía de funciones de cálculo de tasas disponibles en sistemas de monitoreo modernos como Prometheus. Como resultado, al publicar un contador, Dropwizard tenía que publicar el recuento acumulado, la tasa de 1 minuto, la tasa de 5 minutos, la tasa de 15 minutos, etc. Y como esto era ineficaz si nunca necesitabas consultar una tasa, la propia biblioteca de instrumentación distinguía entre @Counted y @Metered. La API de instrumentación se diseñó teniendo en cuenta las capacidades de sus sistemas de monitoreo contemporáneos.

Un avance rápido hasta hoy, y una biblioteca de instrumentación de métricas que pretenda publicar en múltiples sistemas de métricas de destino debe ser consciente de estas sutilezas. Un Micrómetro Counter se va a presentar a Graphite en términos de un recuento acumulativo y varias tasas móviles, pero a Prometheus sólo como un recuento acumulativo, porque estas tasas se pueden calcular en tiempo de consulta con una función PromQL rate.

Es importante para el diseño de la API de cualquier biblioteca de instrumentación actual no limitarse a elevar hacia adelante todos los conceptos encontrados en implementaciones anteriores, sino tener en cuenta el contexto histórico de por qué existían estas construcciones en aquel momento. La Figura 4-3 muestra dónde se solapa Micrómetro con los predecesores del cliente simple Dropwizard y Prometheus, y dónde ha ampliado las capacidades más allá de las de sus predecesores. Significativamente, algunos conceptos se han quedado atrás, reconociendo la evolución en el espacio del monitoreo desde entonces. En algunos casos, esta diferencia es sutil. Micrómetro incorpora histogramas como característica de un Timer plano (o DistributionSummary). A menudo no está claro, en el punto de instrumentación profundo de una biblioteca donde se cronometra una operación, si la aplicación que incorpora esta funcionalidad considera esta operación lo suficientemente crítica como para justificar el gasto extra de enviar los datos del histograma. (Por tanto, la decisión debe dejarse en manos del autor de la aplicación posterior y no del autor de la biblioteca).

srej 0403
Figura 4-3. Superposición de capacidades de instrumentación de métricas

Del mismo modo, en la era de Dropwizard Metrics, los sistemas de monitoreo no incluían funciones de consulta que ayudaran a razonar sobre los datos de temporización (no había aproximaciones de percentiles, ni mapas de calor de latencia, etc.). Así que el concepto de "no midas algo que puedas contar, no cuentes algo que puedas cronometrar" aún no era aplicable. No era raro añadir @Counted a un método, cuando ahora @Counted casi nunca es la opción adecuada para un método (que es inherentemente cronometrable, y los cronómetros siempre se publican también con un recuento).

Aunque en el momento de escribir esto la API de métricas de OpenTelemetry aún está en fase beta, no ha cambiado sustancialmente en los últimos dos años, y parece que las primitivas del medidor no harán un trabajo suficiente para construir abstracciones utilizables para la temporización y el recuento. El Ejemplo 4-1 muestra un Micrómetro Timer con etiquetas variables, dependiendo del resultado de una operación (esto es lo más verboso que llega a ser un temporizador en Micrómetro).

Ejemplo 4-1. Un temporizador micrométrico con una etiqueta de resultado variable
public class MyService {
  MeterRegistry registry;

  public void call() {
    try (Timer.ResourceSample t = Timer.resource(registry, "calls")
        .description("calls to something")
        .publishPercentileHistogram()
        .serviceLevelObjectives(Duration.ofSeconds(1))
        .tags("service", "hi")) {
      try {
        // Do something
        t.tag("outcome", "success");
      } catch (Exception e) {
        t.tags("outcome", "error", "exception", e.getClass().getName());
      }
    }
  }
}

Incluso intentar acercarse a esto con la API de métricas de OpenTelemetry ahora mismo es difícil, como se muestra en el Ejemplo 4-2. No se ha intentado registrar algo similar a histogramas de percentiles o recuentos de límites SLO como en el equivalente de Micrometer. Eso, por supuesto, aumentaría sustancialmente la verbosidad de esta implementación, que ya se está alargando.

Ejemplo 4-2. Cronometraje OpenTelemetry con etiquetas de resultado variable
public class MyService {
  Meter meter = OpenTelemetry.getMeter("registry");
  Map<String, AtomicLong> callSum = Map.of(
      "success", new AtomicLong(0),
      "failure", new AtomicLong(0)
  );

  public MyService() {
    registerCallSum("success");
    registerCallSum("failure");
  }

  private void registerCallSum(String outcome) {
    meter.doubleSumObserverBuilder("calls.sum")
        .setDescription("calls to something")
        .setConstantLabels(Map.of("service", "hi"))
        .build()
        .setCallback(result -> result.observe(
            (double) callSum.get(outcome).get() / 1e9,
            "outcome", outcome));
  }

  public void call() {
    DoubleCounter.Builder callCounter = meter
        .doubleCounterBuilder("calls.count")
        .setDescription("calls to something")
        .setConstantLabels(Map.of("service", "hi"))
        .setUnit("requests");

    long start = System.nanoTime();
    try {
      // Do something
      callCounter.build().add(1, "outcome", "success");
      callSum.get("success").addAndGet(System.nanoTime() - start);
    } catch (Exception e) {
      callCounter.build().add(1, "outcome", "failure",
          "exception", e.getClass().getName());
      callSum.get("failure").addAndGet(System.nanoTime() - start);
    }
  }
}

Creo que el problema de OpenTelemetry es el énfasis en el soporte políglota, que naturalmente presiona al proyecto para que quiera definir una estructura de datos coherente para primitivas de contador como el "observador de suma doble" o el "contador doble". El impacto en la API resultante obliga al usuario final a componer a partir de bloques de construcción de nivel inferior las partes constituyentes de una abstracción de nivel superior como un Micrómetro Timer. Esto no sólo conduce a un código de instrumentación excesivamente prolijo, sino también a una instrumentación específica de un sistema de monitoreo concreto. Por ejemplo, si intentamos publicar un contador en un sistema de monitoreo antiguo como Graphite mientras migramos gradualmente a Prometheus, tendremos que calcular explícitamente las tasas de movimiento por intervalo y enviarlas también. La estructura de datos "contador doble" no admite esto. También existe el problema inverso, la necesidad de incluir la unión de todas las estadísticas posiblemente utilizables para un "contador doble" en la estructura de datos de OpenTelemetry para satisfacer la gama más amplia de sistemas de monitoreo, a pesar de que el envío de estos datos adicionales es un puro desperdicio para un backend de métricas moderno.

Cuando empieces a explorar los gráficos y las alertas, puede que quieras experimentar con diferentes backends. Y haciendo hoy una selección basada en lo que sabes, puede que dentro de un año te encuentres haciendo la transición con más experiencia. Asegúrate de que tu instrumentación de métricas te permite moverte con fluidez entre sistemas de monitoreo (e incluso publicar en ambos mientras haces la transición).

Antes de entrar en ningún SLI concreto, repasemos primero qué hace que un gráfico sea eficaz.

Visualizaciones eficaces de los indicadores del nivel de servicio

Las recomendaciones que aquí se ofrecen son, naturalmente, subjetivas. Voy a manifestar mi preferencia por líneas más atrevidas y menos "tinta" en el gráfico, desviándome en ambos casos de los valores predeterminados de Grafana. Para ser sincero, me da un poco de vergüenza ofrecer estas sugerencias, porque no quiero presumir que mi sentido estético es de algún modo superior al del excelente equipo de diseño de Grafana.

La sensibilidad estilística que voy a ofrecer se deriva de dos influencias significativas a lo largo de mis últimos años de trabajo:

Ver a los ingenieros mirar y entrecerrar los ojos ante los gráficos

Me preocupa cuando un ingeniero mira un gráfico y entrecierra los ojos. Me preocupa sobre todo que la lección que saquen de una visualización excesivamente compleja sea que el propio monitoreo es complejo, y quizá demasiado complejo para ellos. La mayoría de estos indicadores son realmente sencillos cuando se presentan correctamente. Deberían parecerlo.

La presentación visual de la información cuantitativa

Durante un tiempo, hice la misma pregunta a todos los miembros de la escasa población de diseñadores de experiencia de usuario que conocí y que se dedican a la ingeniería de operaciones y la experiencia del desarrollador: ¿qué libro(s) les había(n) influido más? La Visualización de la Información Cuantitativa de Edward Tufte (Graphics Press) estaba siempre entre sus respuestas. Una de las ideas más relevantes para la visualización de series temporales que procede de este libro es la relación "datos-tinta", concretamente aumentarla todo lo posible. Si la "tinta" (o píxeles) de un gráfico no transmite información, transmite complejidad. La complejidad hace que entrecierre los ojos. Entrecerrar los ojos hace que me preocupe.

Pensemos entonces desde esta perspectiva que la relación datos-tinta debe aumentar. Las recomendaciones específicas que siguen cambian el estilo por defecto de Grafana para maximizar esta relación.

Estilos de anchura de línea y sombreado

El gráfico por defecto de Grafana contiene una línea sólida de 1 px, un relleno de transparencia del 10% bajo la línea e interpolación entre los cortes de tiempo. Para mejorar la legibilidad, aumenta el ancho de la línea sólida a 2 px y elimina el relleno. El relleno reduce la relación datos-tinta del gráfico, y los colores superpuestos de los rellenos desorientan con más de un par de líneas en un gráfico. La interpolación es un poco engañosa, ya que implica para un observador casual que el valor puede haber existido brevemente en puntos intermedios a lo largo de la diagonal entre dos cortes temporales. Lo contrario de la interpolación se denomina "paso" en las opciones de Grafana. El gráfico de la parte superior de la Figura 4-4 utiliza las opciones por defecto, y el gráfico de la parte inferior se ajusta con estas recomendaciones.

srej 0404
Figura 4-4. Estilo de gráfico de Grafana por defecto frente al recomendado

Cambia las opciones de la pestaña "Visualización" del editor de gráficos, como se muestra en la Figura 4-5.

srej 0405
Figura 4-5. Opciones de ancho de línea de Grafana

Errores frente a aciertos

Trazar una representación apilada de los resultados (éxito, error, etc.) es muy habitual en los temporizadores, como veremos en "Errores", y también aparece en otros escenarios. Cuando pensamos en aciertos y errores como colores, muchos de nosotros pensamos inmediatamente en verde y rojo: colores de semáforo. Por desgracia, una parte importante de la población tiene deficiencias en la visión de los colores que afectan a su capacidad para percibir las diferencias de color. Para las deficiencias más comunes, la deuteranopía y la protanopía, la diferencia entre el verde y el rojo es difícil o imposible de distinguir. Los afectados por monocromía no pueden distinguir los colores en absoluto, sólo el brillo. Como este libro está impreso monocromáticamente, todos podemos experimentarlo brevemente en el gráfico apilado de errores y aciertos de la Figura 4-6.

srej 0406
Figura 4-6. Mostrar los errores con un estilo de línea diferente para la accesibilidad

Necesitamos proporcionar algún tipo de indicador visual de los errores frente a los aciertos que no sea estrictamente el color. En este caso, hemos optado por trazar los resultados "exitosos" como líneas apiladas y los errores sobre estos resultados como puntos gruesos para que destaquen.

Además, Grafana no ofrece una opción para especificar el orden de las series temporales tal y como aparecen en una representación apilada (es decir, "éxito" en la parte inferior o superior de la pila), ni siquiera para un conjunto limitado de valores posibles. Podemos forzar su ordenación seleccionando cada valor en una consulta independiente y ordenando las propias consultas, como se muestra en la Figura 4-7.

srej 0407
Figura 4-7. Ordenar los resultados en una representación de pila de Grafana

Por último, podemos modificar el estilo de cada consulta individual, como se muestra en la Figura 4-8.

srej 0408
Figura 4-8. Anulación de estilos de línea para cada resultado

"Visualizaciones "Top k

En muchos casos, queremos mostrar algún indicador de los "peores" resultados por alguna categoría. Muchos sistemas de monitoreo ofrecen algún tipo de función de consulta para seleccionar las "k peores" series temporales según algún criterio. Sin embargo, seleccionar los "3 peores" resultados no significa que haya un máximo de tres líneas en el gráfico, porque esta carrera hacia el fondo es perpetua, y los peores resultados pueden cambiar en el transcurso del intervalo de tiempo visualizado por el gráfico. En el peor de los casos, estarás mostrando N puntos de datos en una visualización concreta, ¡y se mostrarán 3*N series temporales distintas! Si trazas una línea vertical por cualquier parte de la Figura 4-9 y cuentas el número de colores únicos que cruza, siempre será menor o igual a tres, porque este gráfico se construyó con una consulta "top 3". Pero hay seis elementos en la leyenda.

srej 0409
Figura 4-9. Visualización Top k con más de k series temporales distintas

Puede llegar a estar mucho más ocupado que esto muy fácilmente. Considera la Figura 4-10, que muestra los cinco tiempos más largos de las tareas de construcción de Gradle durante un periodo de tiempo. Dado que el conjunto de tareas de construcción en ejecución cambia rápidamente a lo largo de los intervalos de tiempo mostrados en este gráfico, la leyenda se llena con muchos más valores que simplemente cinco.

srej 0410
Figura 4-10. El tope k todavía puede dar muchos más elementos en la leyenda que k

En estos casos, la leyenda está abrumada de etiquetas, hasta el punto de resultar ilegible. Utiliza las opciones de Grafana para cambiar la leyenda a una tabla a la derecha, y añade una estadística de resumen como "máximo", como se muestra en la Figura 4-11. A continuación, puedes hacer clic en la estadística de resumen de la tabla para ordenar la leyenda-como-tabla por esta estadística. Ahora, cuando miremos el gráfico, podremos ver rápidamente cuáles son los peores intérpretes en general para el intervalo de tiempo que estamos viendo.

srej 0411
Figura 4-11. Anulación de estilos de línea para cada resultado

Selección del intervalo de velocidad Prometheus

A lo largo de este capítulo, vamos a ver consultas Prometheus que utilizan vectores de rango. Te recomiendo encarecidamente que utilices vectores de rango que sean al menos el doble de largos que el intervalo de raspado (por defecto, un minuto). De lo contrario, corres el riesgo de perder puntos de datos debido a ligeras variaciones en el tiempo de raspado que pueden hacer que los puntos de datos adyacentes estén separados por una distancia ligeramente superior al intervalo de raspado. Del mismo modo, si se reinicia un servicio y falta un punto de datos, la función de tasa no podrá establecer una tasa durante el intervalo o el siguiente punto de datos hasta que el intervalo contenga al menos dos puntos. Utilizar un intervalo mayor para la tasa evita estos problemas. Como el inicio de la aplicación puede ser más largo que el intervalo de raspado, dependiendo de tu aplicación, si es importante para ti evitar totalmente los huecos, puedes elegir un vector de rango más largo que el doble del intervalo de raspado (algo más cercano, de hecho, a lo que sería el inicio de la aplicación más dos intervalos).

Los vectores de intervalo son un concepto algo exclusivo de Prometheus, pero el mismo principio se aplica en otros contextos en otros sistemas de monitoreo. Por ejemplo, te convendría construir una consulta del tipo "min sobre intervalo" para compensar posibles lagunas durante el reinicio de la aplicación si estás estableciendo un umbral mínimo en una alerta.

Manómetros

Una representación de series temporales de un indicador presenta más información de forma tan compacta como un indicador instantáneo. Es igual de obvio cuando una línea cruza un umbral de alerta, y la información histórica sobre los valores anteriores del indicador proporciona un contexto útil. Por ello, en la Figura 4-12 es preferible el gráfico inferior.

srej 0412
Figura 4-12. Prefiere un gráfico de líneas a un indicador instantáneo

Las galgas tienen tendencia a ser puntiagudas. Los grupos de hilos pueden parecer estar temporalmente cerca del agotamiento y luego recuperarse. Las colas se llenan y luego se vacían. La utilización de la memoria en Java es especialmente difícil de vigilar, ya que las asignaciones a corto plazo pueden parecer que llenan rápidamente una parte significativa del espacio asignado, sólo para que la recogida de basura elimine gran parte del consumo.

Uno de los métodos más eficaces para limitar la cháchara de alertas es utilizar una función de recuento rodante, cuyos resultados se muestran en la Figura 4-13. De esta forma podemos definir una alerta que sólo se dispare si se supera un umbral más de tres veces en los últimos cinco intervalos, o alguna otra combinación de frecuencia y número de intervalos de retrospección. Cuanto más larga sea la retrospectiva, más tiempo transcurrirá antes de que se dispare la alerta por primera vez, así que ten cuidado de no mirar demasiado hacia atrás en busca de indicadores críticos.

The alarm fires only when there is clearly a problem developing
Figura 4-13. Recuento rotativo para limitar la cháchara de las alertas

Al ser valores instantáneos, los medidores se grafican básicamente tal cual en cada sistema de monitoreo. Los contadores son un poco más matizados.

Contadores

A menudo, los contadores se comprueban con respecto a un umbral máximo (o, con menos frecuencia, mínimo). La necesidad de comprobar un umbral refuerza la idea de que los contadores deben observarse como tasas y no como una estadística acumulativa, independientemente de cómo se almacene la estadística en el sistema de monitoreo.

La Figura 4-14 muestra el rendimiento de las solicitudes de un punto final HTTP como tasa (línea sólida amarilla) y también el recuento acumulado (puntos verdes) de todas las solicitudes a este punto final desde que se inició el proceso de aplicación. Además, el gráfico muestra una alerta de umbral mínimo fijo (línea y área rojas) de 1.000 peticiones/segundo que se ha establecido en el rendimiento de este punto final. Este umbral tiene sentido en relación con el rendimiento representado como tasa (que en esta ventana varía entre 1.500 y 2.000 solicitudes/segundo). Sin embargo, no tiene mucho sentido en relación con el recuento acumulado, ya que el recuento acumulado es efectivamente una medida tanto de la tasa de rendimiento como de la longevidad del proceso. La longevidad del proceso es irrelevante para esta alerta.

The alert threshold only makes sense against the rate.
Figura 4-14. Un contador con un umbral mínimo de alerta sobre la tasa, en el que también se muestra el recuento acumulado

A veces es difícil determinar a priori un umbral fijo. Además, el ritmo al que se produce un evento puede fluctuar periódicamente en función de algo como las horas punta y las horas valle de la empresa. Esto es especialmente común con una medida de rendimiento como peticiones/segundo, como se ve en la Figura 4-15. Si estableciéramos un umbral fijo en este servicio para detectar cuándo el tráfico no llega repentinamente al servicio (un umbral mínimo), tendríamos que fijarlo en algún punto por debajo de 40 RPS, el rendimiento mínimo que ve este servicio. Supongamos que el umbral mínimo se fija en 30 RPS. Esta alerta se dispara cuando el tráfico cae por debajo del 75% del valor esperado durante las horas valle, ¡pero sólo cuando el tráfico cae por debajo del 10% del valor esperado durante las horas punta! El umbral de alerta no tiene el mismo valor en todos los periodos.

A fixed threshold would not quickly detect a sudden change in RPS during peak hours.
Figura 4-15. Un servicio con aumentos periódicos de tráfico en función de la hora del día

En estos casos, considera enmarcar una alerta en términos de encontrar aumentos o disminuciones bruscos de la tasa. Un buen enfoque general para esto, que se ve en la Figura 4-16, es tomar la tasa del contador, aplicarle una función de suavizado y multiplicar la función de suavizado por algún factor (85% en el ejemplo). Como la función de suavizado tarda naturalmente al menos un poco de tiempo en responder a un cambio repentino en la tasa, una prueba para asegurarse de que la tasa del contador no cae por debajo de la línea suavizada detecta el cambio repentino sin tener que saber en absoluto cuál es la tasa esperada. Una explicación mucho más detallada de los métodos estadísticos de suavizado para alertas dinámicas se presenta en "Construir alertas utilizando métodos de previsión".

The alert fires when there is a sudden drop in throughput.
Figura 4-16. Un contador con un umbral suavizado de forma exponencial doble, que forma un umbral de alerta dinámico

Es responsabilidad de Micrometer enviar los datos al sistema de monitoreo que elijas, de forma que puedas dibujar una representación de la tasa de un contador en tu gráfico. En el caso de Atlas, los contadores ya se envían de forma normalizada, por lo que la consulta de un contador ya devuelve un valor de tasa que se puede trazar directamente, como se muestra en el Ejemplo 4-3.

Ejemplo 4-3. Los contadores Atlas ya son una tasa, por lo que seleccionarlos grafica una tasa
name,cache.gets,:eq,

Otros sistemas de monitoreo esperan que los valores acumulados se envíen al sistema de monitoreo e incluyen algún tipo de función de tasa para utilizarla en el momento de la consulta. El Ejemplo 4-4 mostraría aproximadamente la misma línea de tasa que el equivalente Atlas, dependiendo de lo que selecciones como vector de rango (el periodo de tiempo en el []).

Ejemplo 4-4. Los contadores de Prometheus son acumulativos, por lo que tenemos que convertirlos explícitamente en una tasa
rate(cache_gets[2m])

Hay un problema con la función de tasa de Prometheus: cuando se añaden rápidamente nuevos valores de etiqueta dentro del dominio temporal de un gráfico, la función de tasa de Prometheus puede generar un valor NaN en lugar de un cero. En la Figura 4-17, estamos representando gráficamente el rendimiento de las tareas de construcción de Gradle a lo largo del tiempo. Como en esta ventana, las tareas de construcción se describen de forma única por el nombre del proyecto y de la tarea, y una vez completada una tarea no se incrementa de nuevo, aparecen varias series temporales nuevas dentro del dominio temporal que hemos seleccionado para el gráfico.

srej 0417
Figura 4-17. Llenado a cero de las tasas del contador de Prometheus cuando aparecen nuevos valores de etiqueta en el dominio del tiempo

La consulta del Ejemplo 4-5 muestra el método que podemos utilizar para rellenar los huecos a cero.

Ejemplo 4-5. La consulta para poner a cero las tasas del contador Prometheus
sum(gradle_task_seconds_count) by (gradle_root_project_name) -
(
  sum(gradle_task_seconds_count offset 10s) by (gradle_root_project_name) > 0 or
  (
    (sum(gradle_task_seconds_count) by (gradle_root_project_name)) * 0
  )
)

La forma de trazar los contadores varía un poco de un sistema de monitoreo a otro. A veces tenemos que crear tasas explícitamente, y a veces los contadores se almacenan como tasas por adelantado. Los temporizadores tienen aún más opciones.

Temporizadores

Un medidor micrométrico Timer genera distintas series temporales con una sola operación. Basta con envolver un bloque de código con un temporizador (timer.record(() -> { ... })) para recoger datos sobre el rendimiento a través de este bloque, la latencia máxima (que decae con el tiempo), la suma total de latencia y, opcionalmente, otras estadísticas de distribución como histogramas, percentiles y límites SLO.

En los cuadros de mando, la latencia es lo más importante de ver, porque está más directamente ligada a la experiencia del usuario. Al fin y al cabo, a los usuarios les importa sobre todo el rendimiento de sus solicitudes individuales. Les importa poco o nada el rendimiento total del sistema, salvo indirectamente en la medida en que, a un determinado nivel de rendimiento, su tiempo de respuesta se vea afectado.

En segundo lugar, se puede incluir el rendimiento si se espera que el tráfico tenga una forma determinada (que puede ser periódica en función del horario comercial, las zonas horarias de los clientes, etc.). Por ejemplo, un descenso brusco del rendimiento durante un periodo punta esperado puede ser un fuerte indicador de un problema sistémico en el que el tráfico que debería llegar al sistema no lo hace.

Para muchos casos, lo mejor es establecer alertas sobre la latencia máxima (en este caso significa la máxima observada para cada intervalo) y utilizar aproximaciones de percentil alto, como el percentil 99, para el análisis comparativo (consulta "Análisis canario automatizado").

Establecer alertas de temporizador sobre la latencia máxima

En las aplicaciones Java es muy frecuente que los tiempos máximos sean un orden de magnitud peor que el percentil 99. Lo mejor es que fijes tus alertas en la latencia máxima.

No descubrí la importancia de medir siquiera la latencia máxima hasta que dejé Netflix y conocí un argumento convincente de Gil Tene a favor de alertar sobre la latencia máxima. Hace una observación especialmente visceral sobre el peor de los casos, estableciendo una analogía con el funcionamiento de un marcapasos y subrayando que "'tu corazón seguirá latiendo el 99,9% del tiempo' no es tranquilizador". Siempre aficionado a los argumentos bien razonados, añadí la latencia máxima como estadística clave en las implementaciones de Micrometer Timer y DistributionSummary justo a tiempo para la conferencia SpringOne de 2017. Allí conocí a un antiguo colega de Netflix y le sugerí tímidamente esta nueva idea, consciente de que Netflix no estaba monitorizando realmente la latencia máxima. Inmediatamente se rió de la idea y se marchó a dar una charla, dejándome un poco desanimado. Poco después, recibí un mensaje suyo con el gráfico de la Figura 4-18, que mostraba que la latencia máxima era un orden de magnitud peor que la P99 en un servicio interno clave de Netflix (al que había ido y añadido la latencia máxima como experimento rápido para probar esta hipótesis).

srej 0418
Figura 4-18. Máx frente a P99 en un servicio de registro de Netflix (en nanosegundos)

Y lo que es aún más asombroso, Netflix había sufrido recientemente un cambio arquitectónico que mejoró un poco P99, pero empeoró sustancialmente Max. Es fácil argumentar que en realidad estaba peor por haber hecho el cambio. Conservo el recuerdo de esta interacción porque ilustra muy bien cómo cada organización tiene algo que puede aprender de otra: en este caso, una cultura de monitoreo muy sofisticada en Netflix aprendió un truco de Domo, que a su vez lo aprendió de Azul Systems.

En la Figura 4-19, vemos la diferencia de orden de magnitud entre el percentil máximo y el 99. La latencia de respuesta tiende a agruparse estrechamente en torno al percentil 99, con al menos una agrupación separada cerca del máximo que refleja la recogida de basura, las pausas de la máquina virtual, etc.

A comparison of maximum and P99 latency
Figura 4-19. Latencia máxima frente a P99

En la Figura 4-20, un servicio del mundo real muestra la característica de que la media flota por encima del percentil 99, porque las peticiones están muy densamente empaquetadas alrededor del 99.

srej 0420
Figura 4-20. Latencia media frente a P99

Por insignificante que pueda parecer este 1% superior, los usuarios reales se ven afectados por estas latencias, por lo que es importante reconocer dónde está ese límite y compensarlo cuando sea necesario. Un enfoque reconocido para limitar el efecto del 1% superior es una estrategia de equilibrio de carga en el lado del cliente denominada solicitudes de cobertura (ver "Solicitudes de cobertura").

Establecer una alerta sobre la latencia máxima es clave (hablaremos más sobre por qué en "Latencia"). Pero una vez que un ingeniero ha sido alertado de un problema, el panel de control que utiliza para empezar a entenderlo no tiene por qué tener este indicador. Sería mucho más útil ver la distribución de las latencias como un mapa de calor (como se muestra en la Figura 4-21), que incluiría un cubo distinto de cero donde está el máximo que causó la alerta, para ver lo importante que es el problema en relación con la solicitud normativa que pasa por el sistema en ese momento. En la visualización de un mapa de calor, cada columna vertical representa un histograma (consulta "Histogramas" para ver una definición) en un determinado intervalo de tiempo. Los cuadros de color representan la frecuencia de latencias que se encuentran en un intervalo de tiempos definido en el eje y. Por tanto, la latencia normativa que experimenta un usuario final debería verse "caliente" y los valores atípicos, más fríos.

srej 0421
Figura 4-21. Mapa térmico de un temporizador

¿La mayoría de las solicitudes fallan cerca del valor máximo, o sólo hay uno o unos pocos valores atípicos? La respuesta a esta pregunta probablemente afecte a la rapidez con que un ingeniero alertado escalará el problema y traerá a otros para que le ayuden. No es necesario trazar tanto el valor máximo como el mapa de calor en un panel de diagnóstico, como se muestra en la Figura 4-22. Basta con incluir el valor máximo y el mapa de calor. Basta con incluir el mapa de calor.

srej 0422
Figura 4-22. Latencia máxima frente a mapa de calor de la distribución de latencia

El mapa de calor de latencia también es caro de dibujar, ya que implica recuperar potencialmente docenas o cientos de cubos (que son series temporales individuales en el sistema de monitoreo) para cada tramo de tiempo en el gráfico, para un total que a menudo asciende a miles de series temporales. Esto refuerza la idea de que no hay razón para tener este gráfico auto-actualizándose en una pantalla destacada en algún lugar colgado de una pared. Deja que el sistema de alertas haga su trabajo y visualiza el cuadro de mandos cuando sea necesario para limitar la carga del sistema de monitoreo.

La caja de herramientas de las representaciones útiles ha crecido hasta el punto de que es necesaria una palabra de precaución.

Cuándo dejar de crear cuadros de mando

En 2019 visité a un antiguo colega mío, ahora vicepresidente de operaciones de Datadog. Se lamentaba de que, irónicamente, la falta de moderación saludable en los cuadros de mando creados por los clientes es uno de los principales problemas de capacidad a los que se enfrenta Datadog. Imagina legiones de pantallas de ordenador y de televisión repartidas por todo el mundo, cada una de las cuales actualiza automáticamente a intervalos prescritos una serie de gráficos de aspecto agradable. Me pareció un problema empresarial fascinante, porque está claro que montones de pantallas de TV con la marca Datadog mejoran la visibilidad y la adherencia del producto, al tiempo que producen una pesadilla operativa para un SaaS.

Siempre me ha parecido un poco curiosa la vista del cuadro de mando de "control de misión". Después de todo, ¿qué tiene un gráfico que me indica visualmente un problema? Si se trata de un pico agudo, una depresión profunda o simplemente un valor que se ha deslizado por encima de toda expectativa razonable, entonces se puede crear un umbral de alerta para definir dónde está ese punto de inaceptabilidad, y la métrica se puede monitorizar automáticamente (y las 24 horas del día).

Como ingeniero de guardia, es agradable recibir una alerta con una visualización instantánea del indicador (o un enlace a uno). En última instancia, cuando abrimos alertas, queremos indagar en la información para descubrir una causa raíz (o a veces determinar que no merece la pena prestar atención a la alerta). Si la alerta enlaza con un panel de control, lo ideal es que ese panel esté configurado de forma que permita una explosión o exploración dimensional inmediata. En otras palabras, el salpicadero de la pantalla de TV trata a los seres humanos como una especie de sistema de alerta de escasa capacidad de atención y notoriamente poco fiable.

Las visualizaciones útiles para las alertas pueden no serlo en absoluto para incluirlas en un cuadro de mando, y no todos los gráficos de un cuadro de mando son posibles para construir alertas sobre ellos. Por ejemplo, la Figura 4-22 muestra dos representaciones del mismo temporizador: un máximo decreciente y un mapa de calor. El sistema de alertas va a ver el máximo, pero cuando un ingeniero es alertado de la condición anómala, es mucho más útil ver la distribución de latencias en torno a ese momento para saber cómo de grave fue el impacto (y el máximo debe capturarse en un cubo de latencia que sea visible en el mapa de calor).

Sin embargo, ¡ten cuidado con cómo construyes estas consultas! Si te fijas bien, verás que no hay latencia en torno a los 15 ms en el mapa de calor. En este caso, el vector de rango de Prometheus estaba demasiado cerca del intervalo de raspado, ¡y el hueco momentáneo resultante en el gráfico que es invisible oculta la latencia de 15 ms! Como Micrómetro decae al máximo, seguimos viéndolo en el gráfico de máximos.

Los mapas de calor también son mucho más caros computacionalmente que una simple línea máxima. Para un gráfico está bien, pero si se suma este coste en muchas visualizaciones individuales de las unidades de negocio de una gran organización, puede resultar gravoso para el propio sistema de monitoreo.

Los gráficos no sustituyen a las alertas. Céntrate primero en darlas como alertas a las personas adecuadas cuando se desvíen de los niveles aceptables, en lugar de apresurarte a instalar monitores.

Consejo

Un ser humano vigilando constantemente un monitor no es más que un costoso sistema de alerta que sondea visualmente los niveles inaceptables.

Las alertas deben entregarse al personal de guardia de forma que puedan saltar rápidamente a un cuadro de mandos y empezar a profundizar en la métrica que falla dimensionalmente para razonar dónde está el problema.

No todas las alertas o violaciones de un SLO deben tratarse como una emergencia de parar el mundo .

Indicadores de nivel de servicio para cada microservicio Java

Ahora que tenemos una idea de cómo presentar visualmente los SLI en los gráficos, nos centraremos en los indicadores que puedes añadir. Se presentan aproximadamente por orden de importancia. Así que si sigues el enfoque incrementalista para añadir gráficos y alertas, impleméntalos en secuencia.

Errores

Al cronometrar un bloque de código es útil diferenciar entre operaciones con éxito y sin éxito por dos razones.

En primer lugar, podemos utilizar directamente la relación entre tiempos fallidos y totales como medida de la frecuencia de los errores que se producen en el sistema.

Además, los resultados exitosos y fallidos pueden tener tiempos de respuesta radicalmente distintos, dependiendo del modo de fallo. Por ejemplo, un NullPointerException resultante de hacer una suposición errónea sobre la presencia de algunos datos en la entrada de la solicitud puede fallar al principio de un gestor de solicitudes. Entonces no llega lo suficientemente lejos como para llamar a otros servicios posteriores, interactuar con la base de datos, etc., donde se pasa la mayor parte del tiempo cuando una solicitud tiene éxito. En este caso, las solicitudes infructuosas que fallen de este modo sesgarán nuestra perspectiva sobre la latencia del sistema. De hecho, ¡la latencia parecerá mejor de lo que es en realidad! Por otra parte, un gestor de solicitudes que hace una solicitud de bloqueo a otro microservicio que está bajo presión y cuya respuesta finalmente se agota, puede mostrar una latencia mucho mayor de lo normal (algo cercano al tiempo de espera del cliente HTTP que hace la llamada). Al no segregar los errores, presentamos una visión demasiado pesimista de la latencia de nuestro sistema.

Las etiquetas de estado (recuerda "Nombrar métricas") deben añadirse a la instrumentación de temporización en la mayoría de los casos en dos niveles.

Estado

Una etiqueta que proporciona un código de error detallado, el nombre de la excepción o algún otro indicador específico del modo de fallo

Resultado

Una etiqueta que proporciona una categoría de error más detallada que separa el éxito, el error causado por el usuario y el error causado por el servicio.

Al escribir alertas, en lugar de intentar seleccionar una etiqueta haciendo coincidir un patrón de código de estado (por ejemplo, utilizando el selector de etiquetas no-regex de Prometheus para status !~"2.."), es preferible realizar una coincidencia exacta en la etiqueta de resultado (outcome="SERVER_ERROR"). Al seleccionar "no 2xx", estamos agrupando errores del servidor, como el común HTTP 500 Internal Server Error, con errores causados por el usuario, como HTTP 400 Bad Request o HTTP 403 Forbidden. Un alto índice de HTTP 400 puede indicar que has publicado recientemente código que contenía una incompatibilidad accidental con versiones anteriores en una API, o podría indicar que un nuevo usuario final (por ejemplo, algún otro microservicio upstream) está tratando de incorporarse al uso de tu servicio y aún no ha entendido bien la carga útil.

Las alertas de chat de Panera Faced no distinguen los errores del cliente de los del servidor

Panera Bread, Inc. se enfrentó a una alerta excesiva de un detector de anomalías implementado por su proveedor de sistemas de monitoreo de errores HTTP. Provocó varias alertas por correo electrónico en un solo día porque un solo usuario proporcionó una contraseña incorrecta cinco veces. Los ingenieros descubrieron que el detector de anomalías no diferenciaba entre la proporción de errores del cliente y del servidor. Las alertas sobre el ratio de errores del cliente podrían estar bien para la detección de intrusos, pero el umbral sería mucho más alto que el ratio de errores del servidor (y, desde luego, más alto que cinco errores en un breve periodo de tiempo).

Un HTTP 500 básicamente siempre es culpa tuya como propietario del servicio y requiere atención. En el mejor de los casos, un HTTP 500 pone de manifiesto que una mayor validación previa podría haber devuelto un HTTP 400 útil al usuario final. Creo que "HTTP 500-Error interno del servidor" es demasiado pasivo. Algo como "HTTP 500-Lo siento, es culpa mía" me parece mejor.

Cuando escribas tus propios temporizadores, un patrón habitual consiste en utilizar una muestra de Timer y aplazar la determinación de las etiquetas hasta que se sepa si la solicitud tendrá éxito o fracasará, como en el Ejemplo 4-6. La muestra mantiene el estado del momento en que se inició la operación por ti.

Ejemplo 4-6. Determinar una etiqueta de error y resultado dinámicamente en función del resultado de una operación
Timer.Sample sample = Timer.start();
try {
  // Some operation that might fail...

  sample.stop(
    registry.timer(
      "my.operation",
      Tags.of(
        "exception", "none", 1
        "outcome", "success"
      )
    )
  );
} catch(Exception e) {
  sample.stop(
    registry.timer(
      "my.operation",
      Tags.of(
        "exception", e.getClass().getName(), 2
        "outcome", "failure"
      )
    )
  );
}
1

Algunos sistemas de monitoreo como Prometheus esperan que aparezca un conjunto coherente de claves de etiquetas en las métricas con el mismo nombre. Así que, aunque aquí no haya ninguna excepción, deberíamos etiquetarla con algún valor de marcador de posición, como "ninguno", para reflejar qué etiquetas están presentes también en los casos de fallo.

2

Tal vez tengas una forma de catalogar mejor las condiciones de fallo y puedas proporcionar aquí un valor de etiqueta más descriptivo, pero incluso añadir el nombre de la clase de excepción puede ayudar mucho a comprender qué tipos de fallos hay. NullPointerException es un tipo de excepción muy diferente de un tiempo de espera de conexión mal gestionado en una llamada a un servicio descendente. Cuando la proporción de errores aumenta, es útil poder profundizar en el nombre de la excepción para echar un breve vistazo a lo que es el error. A partir de este nombre de excepción, puedes saltar a tus herramientas de observabilidad de depuración, como los registros, y buscar ocurrencias del nombre de excepción en torno al momento de la condición de alerta.

Cuidado con Class.getSimpleName(), etc., como valor de etiqueta

Ten en cuenta que Class.getSimpleName() y Class.getCanonicalName() pueden devolver valores nulos o vacíos, por ejemplo en el caso de instancias de clases anónimas. Si utilizas uno de ellos como valor de etiqueta, comprueba al menos que el valor sea nulo o vacío y recurre a Class.getName().

Para las métricas de solicitudes HTTP, por ejemplo, Spring Boot etiqueta automáticamente http.server.requests con una etiqueta status que indica el código de estado HTTP y una etiqueta outcome que es una de SUCCESS, CLIENT_ERROR o SERVER_ERROR.

A partir de esta etiqueta, es posible trazar la tasa de error por intervalo. Es difícil establecer un umbral de alerta para la tasa de errores, porque puede fluctuar mucho en las mismas condiciones de fallo, dependiendo de la cantidad de tráfico que pase por el sistema.

Para Atlas, utiliza el operador :and para seleccionar sólo los resultados de SERVER_ERROR, como se muestra en el Ejemplo 4-7.

Ejemplo 4-7. Tasa de errores de solicitudes HTTP al servidor en Atlas
# don't do this because it fluctuates with throughput!
name,http.server.requests,:eq,
outcome,SERVER_ERROR,:eq,
:and,
uri,$ENDPOINT,:eq,:cq

Para Prometheus, utiliza un selector de etiquetas, como se muestra en el Ejemplo 4-8.

Ejemplo 4-8. Tasa de errores de solicitudes HTTP al servidor en Prometheus
# don't do this because it fluctuates with throughput!
sum(
  rate(
    http_server_requests_seconds_count{outcome="SERVER_ERROR", uri="$ENDPOINT"}[2m]
  )
)

Si falla una de cada 10 solicitudes, y pasan por el sistema 100 solicitudes/segundo, la tasa de error es de 10 fallos/segundo. Si pasan por el sistema 1.000 solicitudes/segundo, ¡la tasa de error asciende a 100 fallos/segundo! En ambos casos, la tasa de error relativa al rendimiento es del 10%. Este ratio de error normaliza la tasa y es fácil establecer un umbral fijo para él. En la Figura 4-23, la tasa de error ronda el 10-15% a pesar de que el rendimiento, y por tanto la tasa de error, se disparan.

srej 0423
Figura 4-23. Proporción de error frente a tasa de error

La etiqueta de resultado de grano grueso se utiliza para construir consultas que representan la proporción de errores de la operación cronometrada. En el caso de http.server.requests, es la relación entre SERVER_ERROR y el número total de peticiones.

Para Atlas, utiliza la función :div para dividir los resultados de SERVER_ERROR entre el recuento total de todas las peticiones, como se muestra en el Ejemplo 4-9.

Ejemplo 4-9. Proporción de errores de las solicitudes HTTP al servidor en Atlas
name,http.server.requests,:eq,
:dup,
outcome,SERVER_ERROR,:eq,
:div,
uri,$ENDPOINT,:eq,:cq

Para Prometheus, utiliza el operador / de forma similar, como en el Ejemplo 4-10.

Ejemplo 4-10. Proporción de errores de las solicitudes HTTP al servidor en Prometheus
sum(
  rate(
    http_server_requests_seconds_count{outcome="SERVER_ERROR", uri="$ENDPOINT"}[2m]
  )
) /
sum(
  rate(
    http_server_requests_seconds_count{uri="$ENDPOINT"}[2m]
  )
)

La Tasa de Errores es Mejor que la Relación de Errores para los Servicios de Bajo Rendimiento

En general, prefiere la proporción de errores a la tasa de errores, a menos que el punto final tenga un rendimiento muy bajo. En este caso, incluso una pequeña diferencia en los errores puede provocar cambios salvajes en la proporción de errores. En estas situaciones es más apropiado elegir un umbral de tasa de error fijo.

La tasa y el ratio de error son sólo una visión de un temporizador. La latencia es la otra visión esencial.

Latencia

Alerta sobre la latencia máxima (en este caso significa la máxima observada para cada intervalo), y utiliza aproximaciones de percentiles altos como el percentil 99 para el análisis comparativo, como se muestra en "Análisis Canario Automatizado". Los marcos web Java más populares, como parte de su autoconfiguración de métricas de "caja blanca" (ver "Monitorización de caja negra frente a caja blanca"), ofrecen instrumentación de las peticiones entrantes y salientes con etiquetas enriquecidas. Presentaré los detalles de la instrumentación automática de peticiones de Spring Boot, pero la mayoría de los demás marcos web Java populares han hecho algo muy parecido con Micrometer.

Peticiones del servidor (entrantes)

Spring Boot autoconfigura una métrica de temporización llamada http.server.requests tanto para los puntos finales REST bloqueantes como para los reactivos. Si la latencia de un punto o puntos finales concretos es un indicador clave del rendimiento de una aplicación y también se utilizará para análisis comparativos, añade la propiedad management.metrics.distribution.percentiles-histogram.http.server.requests=true a tu application.properties para exportar histogramas de percentiles de tu aplicación. Para ser más preciso a la hora de habilitar los histogramas de percentiles para un conjunto concreto de puntos finales de la API, puedes añadir la anotación @Timed en Spring Boot, como en el Ejemplo 4-11.

Ejemplo 4-11. Utilizar @Timed para añadir histogramas a un solo punto final
@Timed(histogram = true)
@GetMapping("/api/something")
Something getSomething() {
  ...
}

Alternativamente, puedes añadir un MeterFilter que responda a una etiqueta, como se muestra en el Ejemplo 4-12.

Ejemplo 4-12. Un MeterFilter que añade histogramas de percentiles para determinados puntos finales
@Bean
MeterFilter histogramsForSomethingEndpoints() {
  return new MeterFilter() {
    @Override
    public DistributionStatisticConfig configure(Meter.Id id,
        DistributionStatisticConfig config) {
      if(id.getName().equals("http.server.requests") &&
          id.getTag("uri").startsWith("/api/something")) {
        return DistributionStatisticConfig.builder()
            .percentilesHistogram(true)
            .build()
            .merge(config);
      }
      return config;
    }
  };
}

Para Atlas, el Ejemplo 4-13 muestra cómo comparar la latencia máxima con algún umbral predeterminado.

Ejemplo 4-13. Latencia máxima de la API de Atlas
name,http.server.requests,:eq,
statistic,max,:eq,
:and,
$THRESHOLD,
:gt

Para Prometheus, el ejemplo 4-14 es una simple comparación.

Ejemplo 4-14. Latencia máxima de la API de Prometheus
http_server_requests_seconds_max > $THRESHOLD

Las etiquetas que se añaden a http.server.requests son personalizables. Para el modelo Spring WebMVC de bloqueo, utiliza una WebMvcTagsProvider. Por ejemplo, podríamos extraer información sobre el navegador y su versión de la cabecera de solicitud "User-Agent", como se muestra en el Ejemplo 4-15. Este ejemplo utiliza la biblioteca Browscap, con licencia MIT, para extraer información sobre el navegador de la cabecera "User-Agent".

Ejemplo 4-15. Añadir etiquetas de navegador a las métricas Spring WebMVC
@Configuration
public class MetricsConfiguration {
  @Bean
  WebMvcTagsProvider customizeRestMetrics() throws IOException, ParseException {
    UserAgentParser userAgentParser = new UserAgentService().loadParser();

    return new DefaultWebMvcTagsProvider() {
      @Override
      public Iterable<Tag> getTags(HttpServletRequest request,
        HttpServletResponse response, Object handler, Throwable exception) {

        Capabilities capabilities = userAgentParser.parse(request
          .getHeader("User-Agent"));

        return Tags
          .concat(
            super.getTags(request, response, handler, exception),
            "browser", capabilities.getBrowser(),
            "browser.version", capabilities.getBrowserMajorVersion()
          );
      }
    };
  }
}

Para Spring WebFlux (el modelo reactivo no bloqueante), configura un WebFluxTagsProvider de forma similar, como en el Ejemplo 4-16.

Ejemplo 4-16. Añadir etiquetas de navegador a las métricas Spring WebFlux
@Configuration
public class MetricsConfiguration {
  @Bean
  WebFluxTagsProvider customizeRestMetrics() throws IOException, ParseException {
      UserAgentParser userAgentParser = new UserAgentService().loadParser();

      return new DefaultWebFluxTagsProvider() {
          @Override
          public Iterable<Tag> httpRequestTags(ServerWebExchange exchange,
              Throwable exception) {

            Capabilities capabilities = userAgentParser.parse(exchange.getRequest()
              .getHeaders().getFirst("User-Agent"));

            return Tags
              .concat(
                super.httpRequestTags(exchange, exception),
                "browser", capabilities.getBrowser(),
                "browser.version", capabilities.getBrowserMajorVersion()
              );
          }
      };
  }
}

Ten en cuenta que el temporizador http.server.requests sólo empieza a cronometrar una solicitud una vez que el servicio la está procesando. Si la reserva de hilos de solicitud está habitualmente al límite de su capacidad, las solicitudes de los usuarios se quedan en la reserva de hilos esperando a ser gestionadas, y este lapso de tiempo es muy real para el usuario que espera una respuesta. La información que falta en http.server.requests es un ejemplo de un problema mayor descrito por primera vez por Gil Tene, denominado omisión coordinada (véase "Omisión coordinada"), que se presenta de varias otras formas.

También es útil monitorizar la latencia desde la perspectiva de quien llama (cliente). En este caso, por cliente me refiero generalmente a los que llaman de servicio a servicio y no a los consumidores humanos a tu pasarela API o a la primera interacción con el servicio. El punto de vista de un servicio sobre su propia latencia no incluye los efectos de los retrasos de la red o la contención del grupo de hilos (por ejemplo, el grupo de hilos de solicitud de Tomcat o el grupo de hilos de un proxy como Nginx).

Peticiones de clientes (salientes)

Spring Boot también autoconfigura una métrica de temporización llamada http.client.requests tanto para las llamadas salientes bloqueantes como para las reactivas. Esto te permite, en su lugar (o también), monitorizar la latencia de un servicio desde la perspectiva de todos sus llamantes, siempre que cada uno de ellos llegue a la misma conclusión sobre cuál es el nombre del servicio llamado. La Figura 4-24 muestra tres instancias de servicio que llaman al mismo servicio.

srej 0424
Figura 4-24. Métricas de clientes HTTP de múltiples llamantes

Podemos identificar el rendimiento de un punto final concreto para el servicio llamado seleccionando las etiquetas uri y serviceName. Al agregar todas las demás etiquetas, vemos el rendimiento del punto final de todas las personas que llaman. Si se desglosa dimensionalmente por la etiqueta clientName, se mostraría el rendimiento del servicio sólo desde la perspectiva de ese cliente. Aunque el servicio llamado procese todas las solicitudes en el mismo tiempo, la perspectiva del cliente podría variar (por ejemplo, si un cliente está implementado en una zona o región diferente). Cuando exista la posibilidad de esta variación entre clientes, puedes utilizar algo como la consulta topk de Prometheus para comparar con un umbral de alerta, de modo que la totalidad de la experiencia del rendimiento de un punto final para todos los clientes no elimine el valor atípico de algún cliente en particular, como se muestra en el Ejemplo 4-17.

Ejemplo 4-17. Latencia máxima de las solicitudes salientes por nombre de cliente
topk(
  1,
  sum(
    rate(
      http_client_requests_seconds_max{serviceName="CALLED", uri="/api/..."}[2m]
    )
  ) by (clientName)
) > $THRESHOLD

Para autoconfigurar la instrumentación del cliente HTTP para las interfaces RestTemplate (bloqueante) y WebClient (no bloqueante) de Spring, tienes que tratar las variables de ruta y los parámetros de solicitud de una determinada manera. Concretamente, tienes que dejar que las implementaciones sustituyan por ti las variables de ruta y los parámetros de solicitud, en lugar de utilizar la concatenación de cadenas o una técnica similar para construir una ruta, como se muestra en el Ejemplo 4-18.

Ejemplo 4-18. Permitir que RestTemplate gestione la sustitución de variables de ruta
@RestController
public class CustomerController { 1
  private final RestTemplate client;

  public CustomerController(RestTemplate client) {
    this.client = client;
  }

  @GetMapping("/customers")
  public Customer findCustomer(@RequestParam String q) {
    String customerId;
    // ... Look up customer ID according to 'q'

    return client.getForEntity(
      "http://customerService/customer/{id}?detail={detail}",
      Customer.class,
      customerId,
      "no-address"
    );
  }
}

...

@Configuration
public class RestTemplateConfiguration {
  @Bean
  RestTemplateBuilder restTemplateBuilder() { 2
    return new RestTemplateBuilder()
      .addAdditionalInterceptors(..)
      .build();
  }
}
1

¿Suena nefasto?

2

Para aprovechar la autoconfiguración de Spring Boot de las métricas de RestTemplate, asegúrate de que estás creando cualquier bean personalizado cableado para RestTemplateBuilder y no para RestTemplate (y ten en cuenta que Spring también te proporciona un RestTemplateBuilder automáticamente con los valores predeterminados mediante la autoconfiguración). Spring Boot adjunta un interceptor de métricas adicional a cualquier frijol de este tipo que encuentre. Una vez creado el RestTemplate, es demasiado tarde para que tenga lugar esta configuración.

La idea es que la etiqueta uri siga conteniendo la ruta solicitada con variables de ruta previas a la sustitución, de modo que puedas razonar sobre el número total y la latencia de las solicitudes que se dirigen a ese punto final, independientemente de los valores concretos que se estén buscando. Además, esto es esencial para controlar el número total de etiquetas que contiene la métrica http.client.requests. Permitir un crecimiento ilimitado de etiquetas únicas acabaría saturando el sistema de monitoreo (o resultaría muy caro para ti si el proveedor del sistema de monitoreo cobra por series temporales).

El equivalente para el WebClient no bloqueante se muestra en el Ejemplo 4-19.

Ejemplo 4-19. Permitir que WebClient gestione la sustitución de variables de ruta
@RestController
public class CustomerController { 1
  private final WebClient client;

  public CustomerController(WebClient client) {
    this.client = client;
  }

  @GetMapping("/customers")
  public Mono<Customer> findCustomer(@RequestParam String q) {
    Mono<String> customerId;
    // ... Look up customer ID according to 'q', hopefully in a non-blocking way

    return customerId
      .flatMap(id -> webClient
          .get()
          .uri(
            "http://customerService/customer/{id}?detail={detail}",
            id,
            "no-address"
          )
          .retrieve()
          .bodyToMono(Customer.class)
      );
  }
}

...

@Configuration
public class WebClientConfiguration {
  @Bean
  WebClient.Builder webClientBuilder() { 2
    return WebClient
      .builder();
  }
}
1

¿Suena nefasto?

2

Asegúrate de que estás creando bean wirings para WebClient.Builder y no para WebClient. Spring Boot adjunta una métrica adicional WebClientCustomizer al constructor, no la instancia WebClient completada.

Aunque el conjunto predeterminado de etiquetas que Spring Boot añade a las métricas del cliente es razonablemente completo, es personalizable. Es especialmente habitual etiquetar métricas con el valor de alguna cabecera de solicitud (o cabecera de respuesta). Asegúrate, cuando añadas personalizaciones de etiquetas, de que el número total de valores de etiqueta posibles está bien acotado. No deberías añadir etiquetas para cosas como ID de cliente único (cuando puedes tener más de quizás 1.000 clientes), un ID de solicitud generado aleatoriamente, etc. Recuerda que el propósito de las métricas es tener una idea del rendimiento agregado, no del rendimiento de alguna solicitud individual.

Como ejemplo ligeramente distinto del que utilizamos anteriormente en la personalización de etiquetas http.server.requests, podríamos etiquetar adicionalmente las recuperaciones de clientes por su nivel de suscripción, donde el nivel de suscripción es una cabecera de respuesta en la recuperación de un cliente por ID. De este modo, podríamos trazar por separado la latencia y la tasa de error de la recuperación de clientes premium frente a la de clientes básicos. Tal vez la empresa ponga un mayor nivel de expectativas en la fiabilidad o el rendimiento de las solicitudes a clientes premium, manifestándose en un acuerdo de nivel de servicio más estricto basado en esta etiqueta personalizada.

Para personalizar las etiquetas de RestTemplate, añade tu propia @Bean RestTemplateExchangeTagsProvider, como se muestra en el Ejemplo 4-20.

Ejemplo 4-20. Permitir que RestTemplate gestione la sustitución de variables de ruta
@Configuration
public class MetricsConfiguration {
  @Bean
  RestTemplateExchangeTagsProvider customizeRestTemplateMetrics() {
    return new DefaultRestTemplateExchangeTagsProvider() {
      @Override
      public Iterable<Tag> getTags(String urlTemplate,
        HttpRequest request, ClientHttpResponse response) {

        return Tags.concat(
          super.getTags(urlTemplate, request, response),
          "subscription.level",
          Optional
            .ofNullable(response.getHeaders().getFirst("subscription")) 1
            .orElse("basic")
        );
      }
    };
  }
}
1

¡Ten en cuenta que response.getHeaders().get("subscription") puede devolver null! Así que, tanto si utilizamos get como getFirst, necesitamos comprobar null de alguna manera.

Para personalizar las etiquetas de WebClient, añade tu propia @Bean WebClientExchangeTagsProvider, como se muestra en el Ejemplo 4-21.

Ejemplo 4-21. Permitir que WebClient gestione la sustitución de variables de ruta
@Configuration
public class MetricsConfiguration {
  @Bean
  WebClientExchangeTagsProvider webClientExchangeTagsProvider() {
    return new DefaultWebClientExchangeTagsProvider() {
      @Override
      public Iterable<Tag> tags(ClientRequest request,
        ClientResponse response, Throwable throwable) {

        return Tags.concat(
          super.tags(request, response, throwable),
          "subscription.level",
          response.headers().header("subscription").stream()
            .findFirst()
            .orElse("basic")
        );
      }
    };
  }
}

Hasta ahora nos hemos centrado en la latencia y los errores. Consideremos ahora una medida común de saturación relacionada con el consumo de memoria .

Tiempos de pausa de la recogida de basura

Las pausas de la recolección de basura (GC) suelen retrasar la entrega de una respuesta a una petición del usuario, y pueden ser un indicador de un fallo inminente de la aplicación "sin memoria". Hay varias formas de ver este indicador.

Tiempo máximo de pausa

Establece un umbral de alerta fijo sobre el tiempo máximo de pausa de GC que consideres aceptable (sabiendo que una pausa de GC también contribuye directamente al tiempo de respuesta del usuario final), pudiendo seleccionar umbrales diferentes para los tipos de GC menores y mayores. Traza el máximo del temporizador jvm.gc.pause para establecer tus umbrales, como se muestra en la Figura 4-25. Un mapa de calor de los tiempos de pausa también puede ser interesante si tu aplicación sufre pausas frecuentes y quieres entender cómo es el comportamiento típico a lo largo del tiempo.

srej 0425
Figura 4-25. Tiempos máximos de pausa de recogida de basura

Proporción de tiempo dedicado a la recogida de basura

Como jvm.gc.pause es un temporizador, podemos observar su suma de forma independiente. Concretamente, podemos sumar los incrementos de esta suma en un intervalo de tiempo y dividirlo por el intervalo para determinar qué proporción del tiempo la CPU se dedica a hacer la recogida de basura . Y como nuestro proceso Java no hace nada más durante estos tiempos, cuando una proporción de tiempo suficientemente significativa se dedica a la GC, se justifica una alerta. El Ejemplo 4-22 muestra la consulta Prometheus para esta técnica.

Ejemplo 4-22. Consulta de Prometheus sobre el tiempo pasado en la recogida de basuras por causa
sum( 1
  sum_over_time( 2
    sum(increase(jvm_gc_pause_seconds_sum[2m])[1m:] 3
  )
) / 60 4
1

Suma todas las causas individuales, como "fin del CG menor".

2

El tiempo total empleado en una causa individual en el último minuto.

3

Es la primera vez que vemos una subconsulta Prometheus. Nos permite tratar la operación sobre los dos indicadores como un vector de rango para introducirlo en sum_over_time.

4

Como jvm_gc_pause_seconds_sum tiene una unidad de segundos (y por tanto también las sumas) y hemos sumado en un periodo de 1 minuto, divide entre 60 segundos para obtener un porcentaje en el intervalo [0, 1] del tiempo que hemos pasado en GC en el último minuto.

Esta técnica es flexible. Puedes utilizar una etiqueta para seleccionar determinadas causas de GC y evaluar, por ejemplo, sólo la proporción de tiempo empleado en los principales eventos de GC. O, como hemos hecho aquí, puedes simplemente sumar todas las causas y razonar sobre el tiempo total de GC en un intervalo determinado. Lo más probable es que descubras que, si separas estas sumas por causas, los eventos menores de GC no contribuyen de forma tan significativa a la proporción de tiempo empleado en GC. La aplicación monitorizada en la Figura 4-26 realizaba recolecciones menores cada minuto y, como era de esperar, sólo dedicó el 0,0182% de su tiempo a actividades relacionadas con la GC.

srej 0426
Figura 4-26. Proporción de tiempo dedicado a eventos menores de CG

Si no utilizas un sistema de monitoreo que proporcione funciones de agregación como sum_over_time, Micrometer proporciona un enlazador de medidores llamado JvmHeapPressureMetrics, mostrado en el Ejemplo 4-23, que precalcula esta sobrecarga de GC y envía un medidor llamado jvm.gc.overhead que es un porcentaje en el rango [0, 1] contra el que puedes establecer una alerta de umbral fijo. En una aplicación Spring Boot, sólo tienes que añadir una instancia de JvmHeapPressureMetrics como @Bean y se vinculará automáticamente a tus registros de medidores.

Ejemplo 4-23. Configurar el aglutinante del medidor de presión del montón de la JVM
MeterRegistry registry = ...

new JvmHeapPressureMetrics(
  Tags.empty(),
  Duration.ofMinutes(1), 1
  Duration.ofSeconds(30)
).register(meterRegistry);
1

Controla la ventana de retroceso.

La presencia de cualquier asignación gigantesca

Además de elegir una de las formas anteriores para monitorear el tiempo empleado en la GC, también es una buena idea establecer una alerta sobre la presencia de la asignación gigantesca que la GC provoca en el colector G1, ¡porque indica que en alguna parte de tu código estás asignando un objeto >50% del tamaño total del espacio Eden! Lo más probable es que haya una forma de refactorizar la aplicación para evitar tal asignación mediante la fragmentación o el flujo de datos. Una asignación enorme podría producirse al hacer algo como analizar una entrada o recuperar un objeto de un almacén de datos que aún no es tan grande como la aplicación podría ver teóricamente, y un objeto más grande muy bien podría hacer caer la aplicación.

Para ello, en concreto, buscas un recuento distinto de cero para jvm.gc.pause en el que la etiqueta cause sea igual a G1 Humongous Allocation.

Hace tiempo, en "Monitoreo de la disponibilidad", mencionamos que las métricas de saturación suelen ser preferibles a las de utilización cuando puedes elegir entre ambas. Esto es ciertamente cierto en el caso del consumo de memoria. Es más fácil acertar con la visión del tiempo empleado en la recogida de basura como medida de los problemas de recursos de memoria. También podemos hacer algunas cosas interesantes con las medidas de utilización, si tenemos cuidado.

Utilización de la pila

El montón de Java está separado en varios pools, cada uno de los cuales tiene un tamaño definido. Las instancias de objetos Java se crean en el espacio del montón. Las partes más importantes del montón son las siguientes:

Espacio Edén (generación joven)

Aquí se asignan todos los objetos nuevos. Se produce un evento menor de recogida de basura cuando se llena este espacio.

Espacio superviviente

Cuando se produce una recogida de basura menor, todos los objetos vivos (que demostrablemente aún tienen referencias y, por tanto, no pueden ser recogidos) se copian al espacio superviviente. A los objetos que llegan al espacio superviviente se les incrementa la edad y, una vez alcanzado un umbral de edad, son promovidos a la generación antigua. La promoción puede ocurrir prematuramente si el espacio superviviente no puede contener todos los objetos vivos de la generación joven (los objetos se saltan el espacio superviviente y pasan directamente a la generación vieja). Este último hecho será clave para medir los niveles peligrosos de presión de asignación.

Antigua generación

Aquí es donde se almacenan los objetos de larga supervivencia. Cuando se almacenan objetos en el espacio Eden, se establece una edad para ese objeto; y cuando alcanza esa edad, el objeto se traslada a la generación antigua.

Fundamentalmente, queremos saber cuándo uno o más de estos espacios se está "llenando" y se mantiene demasiado "lleno". Esto es algo difícil de monitorear porque la recolección de basura de la JVM se activa por diseño cuando los espacios se llenan. Así que el hecho de que un espacio se llene no es en sí mismo un indicador de un problema. Lo que es preocupante es cuando permanece lleno.

El binder medidor JvmMemoryMetrics de Micrometer recoge automáticamente el uso del pool de memoria de la JVM, junto con el tamaño máximo total actual del montón (ya que éste puede aumentar y disminuir en tiempo de ejecución). La mayoría de los frameworks web Java configuran automáticamente este aglutinante.

En la Figura 4-27 se representan varias métricas. La idea más directa para medir la presión de la pila es utilizar un umbral fijo simple, como un porcentaje de la pila total consumida. Como vemos, la alerta de umbral fijo se dispara con demasiada frecuencia. La alerta más temprana se dispara a las 11:44, mucho antes de que sea evidente que hay una fuga de memoria en esta aplicación. Aunque el montón supere temporalmente el umbral de porcentaje del montón total que hemos fijado, los eventos de recogida de basura devuelven rutinariamente el consumo total por debajo del umbral.

En la Figura 4-27:

  • Las barras verticales sólidas juntas son un gráfico de pila del consumo de memoria por espacio.

  • La línea fina alrededor del nivel de 30,0 M es el espacio máximo de montón permitido. Observa cómo fluctúa a medida que la JVM intenta elegir el valor correcto entre el tamaño inicial del montón (-Xms) y el tamaño máximo del montón (-Xmx) para el proceso.

  • La línea en negrita alrededor del nivel 24,0 M representa un porcentaje fijo de esta memoria máxima permitida. Éste es el umbral. Es un umbral fijo en relación con el máximo, pero dinámico en el sentido de que es un porcentaje del máximo que, a su vez, puede fluctuar.

  • Las barras más claras representan puntos en los que la utilización real de la pila (la parte superior del gráfico de la pila) supera el umbral. Ésta es la "condición de alerta".

The alarm fires frequently
Figura 4-27. Alerta de utilización de memoria con un umbral fijo

Así que este simple umbral fijo no funcionará. Hay mejores opciones disponibles, dependiendo de las capacidades de tu sistema de monitoreo de objetivos.

Conteo rodante de ocurrencias de llenado del espacio de la pila

Utilizando una característica como la función de recuento rodante de Atlas, podemos alertar sólo cuando el montón supere el umbral -digamos, tres de los cinco intervalos anteriores- indicando que, a pesar del mejor esfuerzo del recolector de basura, el consumo del montón sigue siendo un problema (ver Figura 4-28).

Por desgracia, no muchos sistemas de monitoreo tienen una función como el recuento rodante de Atlas. Prometheus puede hacer algo parecido con su funcionamiento count_over_time, pero es difícil conseguir una dinámica similar de "tres de cinco".

The alarm fires only when there is clearly a problem developing
Figura 4-28. Utilizar el recuento rodante para limitar la cháchara de las alertas

Existe un enfoque alternativo que también funciona bien.

Poca memoria del pool después de la recogida

JvmHeapPressureMetrics de Micrometer añade un indicador para el porcentaje de montón de la Generación Antigua utilizado tras el último evento de recogida de basura. jvm.memory.usage.after.gc

jvm.memory.usage.after.gc es un porcentaje expresado en el intervalo [0, 1]. Cuando es alto (un buen umbral de alerta inicial es superior al 90%), la recogida de basura no es capaz de barrer mucha basura. Por lo tanto, es de esperar que se produzcan con frecuencia pausas de larga duración que ocurren cuando se barre la Generación Antigua, y las pausas frecuentes de larga duración degradan significativamente el rendimiento de la aplicación y, en última instancia, provocan errores fatales OutOfMemoryException.

También es eficaz una variación sutil de medir la memoria de la piscina baja después de la recogida.

Memoria total baja

Esta técnica consiste en mezclar indicadores del uso del montón y de la actividad de recogida de basura. Se indica un problema cuando ambos superan un umbral:

jvm.gc.overhead > 50%

Observa que éste es un umbral de alerta más bajo que el sugerido en "Tiempos de pausa de la recogida de basura" para el mismo indicador (donde sugerimos un 90%). Podemos ser más agresivos con este indicador porque lo emparejamos con un indicador de utilización.

jvm.memory.used/jvm.memory.max > 90% en cualquier momento de los últimos 5 minutos

Ahora tenemos una idea de que la sobrecarga de GC está aumentando porque uno o más de los pools se siguen llenando. Si tu aplicación genera mucha basura a corto plazo en circunstancias normales, también podrías limitarla al repositorio de Generación antigua.

El criterio de alerta para el indicador de sobrecarga GC es una simple prueba contra el valor del indicador.

La consulta sobre el uso total de memoria es un poco menos obvia. La consulta Prometheus se muestra en el Ejemplo 4-24.

Ejemplo 4-24. Consulta de Prometheus sobre la memoria máxima utilizada en los últimos cinco minutos
max_over_time(
  (
    jvm_memory_used_bytes{id="G1 Old Gen"} /
    jvm_memory_committed_bytes{id="G1 Old Gen"}
  )[5m:]
)

Para entender mejor lo que hace max_over_time, la Figura 4-29 muestra la cantidad total de espacio Eden (jvm.memory.used{id="G1 Eden Space"} en este caso) consumido en varios momentos (los puntos) y el resultado de aplicar una consulta max_over_time de un minuto a la misma consulta (la línea continua). Se trata de una ventana máxima móvil sobre un intervalo prescrito.

Mientras el uso de la pila aumente (y no haya estado por debajo del valor actual de la ventana de retroceso), max_over_time lo rastrea exactamente. En cuanto se produce un evento de recogida de basura, la vista actual del uso cae y max_over_time se "pega" al valor más alto de la ventana de retroceso.

srej 0429
Figura 4-29. Prometheus max_over_time mirando el espacio máximo de Eden utilizado en una mirada retrospectiva de un minuto

También es la primera vez que consideramos una alerta basada en más de una condición. Los sistemas de alerta suelen permitir la combinación booleana de varios criterios. En la Figura 4-30, suponiendo que el indicador jvm.gc.overhead representa la Consulta A y el indicador de uso representa la Consulta B, se puede configurar una alerta en Grafana sobre ambas juntas.

srej 0430
Figura 4-30. Configuración de una alerta de Grafana basada en dos indicadores de memoria total baja

Otra medida de utilización habitual de es la CPU, que no tiene un análogo de saturación fácil.

Utilización de la CPU

El uso de la CPU es una alerta de utilización que suele fijarse, pero desgraciadamente es difícil establecer una regla general de lo que es una cantidad saludable de CPU debido a los diferentes modelos de programación que se describen a continuación: esto tendrá que determinarse para cada aplicación, en función de sus características.

Por ejemplo, un microservicio Java típico que se ejecute en Tomcat y atienda solicitudes mediante un modelo de servlets de bloqueo, normalmente consumirá los hilos disponibles en el pool de hilos de Tomcat mucho antes de sobreutilizar la CPU. En este tipo de aplicaciones, la saturación de memoria es mucho más común (por ejemplo, mucho exceso de basura creada en el manejo de cada solicitud o grandes cuerpos de solicitud/respuesta).

Un microservicio Java que se ejecute en Netty y utilice un modelo de programación reactivo en toda su extensión aceptará un rendimiento mucho mayor por instancia, por lo que la utilización de la CPU tiende a ser mucho mayor. De hecho, ¡saturar mejor los recursos de CPU disponibles se cita habitualmente como una ventaja del modelo de programación reactiva!

En algunas plataformas, ten en cuenta la utilización conjunta de CPU y memoria antes de redimensionar las instancias

Una característica común de la plataforma como servicio es la simplificación del dimensionamiento de instancias hasta la cantidad de CPU o memoria que desees, con la otra variable creciendo proporcionalmente a medida que subes de tamaño. En el caso de Cloud Foundry, esta proporcionalidad entre CPU y memoria se decidió en un momento en que era casi universal un modelo de bloqueo de la gestión de peticiones como Tomcat. Como se ha señalado, la CPU tiende a infrautilizarse en este modelo. Una vez asesoré a una empresa que había adoptado un modelo reactivo no bloqueante para su aplicación, y al darme cuenta de que la memoria se infrautilizaba considerablemente, reduje el tamaño de las instancias Cloud Foundry de la empresa para que no consumieran tanta memoria. Pero la CPU se asigna a las instancias en esta plataforma proporcionalmente a la cantidad de memoria solicitada. Al elegir un requisito de memoria más bajo, la empresa también privó inadvertidamente a su aplicación reactiva de la CPU que, de otro modo, habría saturado con tanta eficacia.

Micrometer exporta dos métricas clave para el monitoreo de la CPU, que se enumeran en la Tabla 4-1. Ambas métricas se informan desde el sistema operativo MXBean de Java (ManagementFactory.getOperatingSystemMXBean()).

Tabla 4-1. Métricas del procesador comunicadas por Micrometer
Métrica Tipo Descripción

uso.cpu.del.sistema

Galga

El uso reciente de la CPU de todo el sistema

uso.cpu.proceso

Galga

El uso reciente de CPY para el proceso de la máquina virtual Java

Para el caso más común en la empresa, en el que una aplicación atiende peticiones mediante un modelo de servlet bloqueante, es razonable realizar pruebas con un umbral fijo del 80%. Las aplicaciones reactivas deberán probarse empíricamente para determinar su punto de saturación adecuado.

Para Atlas, utiliza la función :gt, como se muestra en el Ejemplo 4-25.

Ejemplo 4-25. Umbral de alerta CPU Atlas
name,process.cpu.usage,:eq,
0.8,
:gt

Para Prometheus, el Ejemplo 4-26 es sólo una expresión de comparación.

Ejemplo 4-26. Umbral de alerta de CPU de Prometheus
process_cpu_usage > 0.8

El uso de la CPU del proceso debe trazarse como un porcentaje (donde el sistema de monitoreo debe esperar una entrada en el rango 0-1 para dibujar adecuadamente el eje y). Fíjate en el eje y de la Figura 4-31 para saber qué aspecto debería tener.

srej 0431
Figura 4-31. Uso de la CPU del proceso en porcentaje

En Grafana, "porcentaje" es una de las unidades seleccionables en la pestaña "Visualización". Asegúrate de seleccionar la opción de "porcentaje (0,0-1,0)", como se muestra en la Figura 4-32.

srej 0432
Figura 4-32. Unidad de porcentaje de Grafana

Hay otro indicador basado en recursos que deberías medir en cada aplicación, relacionado con los descriptores de archivo.

Descriptores de archivo

La función "ulimits" de Unix limita cuántos recursos puede utilizar un solo usuario, incluidos los descriptores de archivo abiertos simultáneamente. Los descriptores de archivo no sólo se consumen para acceder a archivos, sino también para conexiones de red, conexiones a bases de datos, etc.

Puedes ver los ulímites actuales de tu shell con ulimit -a. La salida se muestra en el Ejemplo 4-27. En muchos sistemas operativos, 1.024 es el límite por defecto para los descriptores de archivo abiertos. Escenarios como cada solicitud de servicio que requiera acceso para leer o escribir un archivo, en los que el número de hilos concurrentes puede superar el límite del sistema operativo, son vulnerables a este problema. Un rendimiento de miles de solicitudes simultáneas no es irrazonable para un microservicio moderno, especialmente uno no bloqueante.

Ejemplo 4-27. Salida de ulimit -a en un intérprete de comandos Unix
$ ulimit -a
...
open files (-n) 1024 1
...
cpu time (seconds, -t) unlimited
max user processes (-u) 63796
virtual memory (kbytes, -v) unlimited
1

Esto representa el número de archivos abiertos permitidos, no el número de archivos abiertos actualmente.

Este problema no es necesariamente común, pero el impacto de alcanzar el límite de descriptores de archivo puede ser fatal, haciendo que la aplicación deje de responder por completo, dependiendo de cómo se utilicen los descriptores de archivo. A diferencia de un error de memoria agotada o una excepción fatal, a menudo la aplicación simplemente se bloqueará pero parecerá que sigue en servicio, por lo que este problema es especialmente pernicioso. Como el monitoreo de la utilización de los descriptores de archivo es tan barato, alerta sobre esto en cada aplicación. Las aplicaciones que utilizan técnicas y marcos web comunes probablemente nunca superarán el 5% de utilización del descriptor de archivo (y a veces mucho menos); pero cuando se cuela un problema, es un problema.

Experimentando el Problema del Descriptor de Fichero Mientras Escribía Este Libro

Hacía tiempo que sabía que monitoreaba esto, pero nunca había experimentado un problema hasta que escribí este libro. Un paso de la compilación de Go para compilar Grafana desde el código fuente se bloqueaba repetidamente, sin llegar a completarse. Evidentemente, el mecanismo de resolución de dependencias de Go no limita cuidadosamente el número de descriptores de archivo abiertos.

Una aplicación que puede tener sockets abiertos a cientos de llamantes, conexiones HTTP a servicios descendentes, conexiones abiertas a fuentes de datos y archivos de datos abiertos podría llegar al límite de descriptores de archivo. Cuando un proceso se queda sin descriptores de archivo, no suele acabar bien. Puedes ver errores en registros como los del Ejemplo 4-28 y el Ejemplo 4-29.

Ejemplo 4-28. Descriptores de archivo agotados de Tomcat aceptando una nueva conexión HTTP
java.net.SocketException: Too many open files
  at java.net.PlainSocketImpl.socketAccept(Native Method)
  at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:398)
Ejemplo 4-29. Fallo de Java al abrir un archivo cuando se agotan los descriptores de archivo
java.io.FileNotFoundException: /myfile (Too many open files)
  at java.io.FileInputStream.open(Native Method)

Micrometer informa de dos métricas mostradas en la Tabla 4-2 para alertarte de un problema de descriptor de archivo en tus aplicaciones.

Tabla 4-2. Métricas del descriptor de archivo informadas por el micrómetro
Métrica Tipo Descripción

proceso.max.fds

Galga

Descriptores de archivo abiertos máximos permitidos, correspondientes a la salida ulimit -a

proceso.abierto.fds

Galga

Número de descriptores de archivo abiertos

Normalmente, los descriptores de archivo abiertos deben permanecer por debajo del máximo, por lo que una prueba contra un umbral fijo como el 80% es un buen indicador de un problema inminente. Esta alerta debe activarse en todas las aplicaciones, ya que los límites de archivos son un límite duro de aplicación universal que dejará tu aplicación fuera de servicio.

Para Atlas, utiliza las funciones :div y :gt, como se muestra en el Ejemplo 4-30.

Ejemplo 4-30. Umbral de alerta del descriptor de archivo Atlas
name,process.open.fds,:eq,
name,process.max.fds,:eq,
:div,
0.8,
:gt

Para Prometheus, el Ejemplo 4-31 parece aún más sencillo.

Ejemplo 4-31. Umbral de alerta del descriptor de archivo Prometheus
process_open_fds / process_max_fds > 0.8

Llegados a este punto, hemos cubierto las señales que son aplicables a la mayoría de los microservicios Java. Las que siguen suelen ser útiles, pero no tan ubicuas.

Tráfico sospechoso

Otro indicador sencillo que puede derivarse de métricas como http.server.requests consiste en observar la aparición de códigos de estado inusuales. Una rápida sucesión de HTTP 403 Prohibido (y similares) o HTTP 404 No Encontrado puede indicar un intento de intrusión.

A diferencia de los errores de trazado, monitoriza las apariciones totales de un código de estado sospechoso como una tasa y no como una proporción relativa al rendimiento total. Probablemente sea seguro decir que 10.000 HTTP 403 por segundo son igualmente sospechosos si el sistema procesa normalmente 15.000 solicitudes por segundo o 15 millones de solicitudes por segundo, así que no dejes que el rendimiento total oculte la anomalía.

La consulta Atlas del Ejemplo 4-32, es similar a la consulta de la tasa de error que hemos comentado antes, pero busca en la etiqueta status más granularidad que en la etiqueta outcome.

Ejemplo 4-32. 403 sospechosos en solicitudes HTTP al servidor en Atlas
name,http.server.requests,:eq,
status,403,:eq,
:and,
uri,$ENDPOINT,:eq,:cq

Utiliza la función Prometheus rate para conseguir el mismo resultado en Prometheus, como en el Ejemplo 4-33.

Ejemplo 4-33. 403 sospechosos en solicitudes HTTP al servidor en Prometheus
sum(
  rate(
    http_server_requests_seconds_count{status="403", uri="$ENDPOINT"}[2m]
  )
)

El siguiente indicador está especializado en un tipo concreto de aplicación, pero sigue siendo lo suficientemente común como para incluirlo.

Ejecuciones por lotes u otras tareas de larga duración

Uno de los mayores riesgos de cualquier tarea de larga duración es que se ejecute durante mucho más tiempo del previsto. Al principio de mi carrera, estaba de guardia habitualmente para implementaciones de producción, que siempre se realizaban después de una serie de ejecuciones por lotes a medianoche. En circunstancias normales, la secuencia por lotes debería haberse completado tal vez a la 1:00 a.m. El programa de implementación se basaba en esta suposición. Así que un administrador de red que cargara manualmente el artefacto desplegado (esto es anterior al Capítulo 5) debía estar en un ordenador preparado para realizar la tarea a la 1:00 a.m. Como representante del equipo de ingeniería del producto, yo debía estar preparado para realizar una breve prueba de humo aproximadamente a la 1:15 a.m. y estar disponible para ayudar a solucionar cualquier problema que surgiera. En ese momento, vivía en una zona rural sin acceso a Internet, así que viajé por una carretera estatal hacia un centro de población hasta que pude conseguir una señal de móvil lo suficientemente fiable como para conectar mi teléfono y conectarme a la VPN. Cuando los procesos por lotes no se completaban en un tiempo razonable, a veces pasaba horas sentado en mi coche en alguna carretera rural esperando a que se completaran. Los días en que no se producían Implementaciones de producción, quizá nadie se enteraba de que el ciclo por lotes había fallado hasta el siguiente día laborable.

Si envolvemos una tarea de larga duración en un Micrómetro Timer, no sabremos que se ha superado el SLO hasta que la tarea finalice realmente. Por lo tanto, si se suponía que la tarea no iba a durar más de 1 hora, pero en realidad se ejecuta durante 16 horas, no veremos que esto aparece en un gráfico de monitoreo hasta el primer intervalo de publicación después de 16 horas, cuando la muestra se registra en el temporizador.

Para monitorizar las tareas de larga ejecución, es mejor observar el tiempo de ejecución de las tareas en vuelo o activas. LongTaskTimer realiza este tipo de medición. Podemos añadir este tipo de cronometraje a una tarea potencialmente de larga ejecución, como en el Ejemplo 4-34.

Ejemplo 4-34. Un temporizador de tarea larga basado en anotaciones para una operación programada
@Timed(name = "policy.renewal.batch", longTask = true)
@Scheduled(fixedRateString = "P1D")
void renewPolicies() {
  // Bill and renew insurance policies that are beginning new terms today
}

Los temporizadores de tareas largas envían varias estadísticas de distribución: el recuento de tareas activas, la duración máxima de las solicitudes en vuelo, la suma de todas las duraciones de las solicitudes en vuelo y, opcionalmente, información sobre percentiles e histogramas de las solicitudes en vuelo.

Para el Atlas, haz la prueba con nuestra expectativa de una hora en nanosegundos, como se muestra en el Ejemplo 4-35.

Ejemplo 4-35. Umbral máximo de alerta del temporizador de tarea larga Atlas
name,policy.renewal.batch.max,:eq,
3.6e12, 1
:gt
1

Una hora en nanosegundos

Para Prometheus, el Ejemplo 4-36 se prueba con una hora en segundos.

Ejemplo 4-36. Umbral máximo de alerta del temporizador de tareas largas de Prometheus
policy_renewal_batch_max_seconds > 3600

Hemos visto algunos ejemplos de indicadores eficaces en los que fijarse, y en este punto es de esperar que tengas uno o más de ellos trazados en un panel de control y puedas ver algunas percepciones significativas. A continuación veremos cómo automatizar las alertas cuando estos indicadores vayan mal, de modo que no tengas que mirar tus cuadros de mando todo el tiempo para saber cuándo algo no va bien.

Construir alertas utilizando métodos de previsión

Los umbrales de alerta fijos suelen ser difíciles de determinar a priori, y como el rendimiento del sistema está sujeto a la deriva con el tiempo, puede ser algo que haya que reajustar continuamente. Si el rendimiento a lo largo del tiempo tiende a disminuir (pero de tal forma que la disminución sigue estando dentro de niveles aceptables), entonces un umbral de alerta fijo puede volverse fácilmente demasiado charlatán. Si el rendimiento tiende a mejorar, entonces el umbral ya no es una medida tan fiable del rendimiento esperado, a menos que se afine.

El aprendizaje automático es objeto de mucho bombo y platillo en el sentido de que el sistema de monitoreo determinará automáticamente los umbrales de alerta, pero no ha producido los resultados prometidos. Para los datos de series temporales, los métodos estadísticos clásicos más sencillos siguen siendo increíblemente potentes. Sorprendentemente, el artículo de S. Makridakis et al., "Statistical and Machine Learning Forecasting Methods: Concerns and Ways Forward", muestra que los métodos estadísticos tienen un error de predicción menor (como se muestra en la Figura 4-33) que los métodos de aprendizaje automático.

srej 0433
Figura 4-33. Error de previsión en un paso de las técnicas estadísticas frente a las de aprendizaje automático

Veamos algunos de estos métodos estadísticos, empezando por el método ingenuo menos predictivo, que puede utilizarse con cualquier sistema de monitoreo. Los enfoques posteriores tienen un apoyo menos universal por parte de los sistemas de monitoreo, ya que su matemática es lo suficientemente complicada como para requerir funciones de consulta incorporadas.

Método ingenuo

El método ingenuo es una heurística simple que predice el siguiente valor basándose en el último valor observado:

y ^ T+1|T = α y T

Se puede determinar un umbral de alerta dinámico con el método ingenuo multiplicando el desplazamiento de una serie temporal por algún factor. Entonces podemos comprobar si la línea verdadera cae alguna vez por debajo (o la supera si el multiplicador es mayor que uno) de la línea prevista. Por ejemplo, si la línea verdadera es una medida del rendimiento a través de un sistema, una caída repentina y sustancial del rendimiento puede indicar una interrupción.

El criterio de alerta para Atlas es entonces siempre que la consulta del Ejemplo 4-37 devuelva 1. La consulta está diseñada contra el conjunto de datos de prueba de Atlas, por lo que te resultará fácil hacer pruebas y probar diferentes multiplicadores para observar el efecto.

Ejemplo 4-37. Criterios de alerta Atlas para el método de previsión ingenuo
name,requestsPerSecond,:eq,
:dup,
0.5,:mul, 1
1m,:offset, 2
:rot,
:lt
1

La estanqueidad del umbral se fija con este factor.

2

"Mira hacia atrás" a algún intervalo anterior para la previsión.

El efecto del método ingenuo puede verse en la Figura 4-34. El factor multiplicativo (0,5 en el ejemplo de la consulta) controla lo cerca del valor real que queremos fijar el umbral y también reduce en la misma medida la imprecisión de la previsión (es decir, cuanto más laxo sea el umbral, menos imprecisa será la previsión). Como el suavizado del método es proporcional a la holgura del ajuste, el umbral de alerta se dispara cuatro veces en esta ventana temporal (indicado por las barras verticales del centro del gráfico), aunque hayamos permitido una desviación del 50% respecto a lo "normal".

srej 0434
Figura 4-34. Previsión con el método ingenuo

Para evitar una alerta charlatana, tendríamos que reducir el ajuste de la previsión a nuestro indicador (en este caso, un multiplicador de 0,45 silencia la alerta para esta ventana temporal). Por supuesto, hacer esto también permite una mayor desviación de lo "normal" antes de que se dispare una alerta.

Suavizado exponencial único

Al suavizar el indicador original antes de multiplicarlo por algún factor, podemos ajustar el umbral más cerca del indicador. El suavizado exponencial simple se define mediante la Ecuación 4-1.

Ecuación 4-1. Donde 0 α 1
y ^ T+1|T = α y T + α ( 1 - α ) y T-1 + α (1-α) 2 y T-2 + ... = α n=0 k (1-a) n y T-n

α es un parámetro de suavizado. Cuando α = 1 todos los términos excepto el primero se reducen a cero y nos quedamos con el método ingenuo. Los valores inferiores a 1 sugieren la importancia que deben tener las muestras anteriores.

Al igual que para el método ingenuo, el criterio de alerta para Atlas es siempre que la consulta del Ejemplo 4-38 devuelva 1.

Ejemplo 4-38. Criterios de alerta Atlas para el suavizado exponencial simple
alpha,0.2,:set,
coefficient,(,alpha,:get,1,alpha,:get,:sub,),:set,
name,requestsPerSecond,:eq,
:dup,:dup,:dup,:dup,:dup,:dup,
0,:roll,1m,:offset,coefficient,:fcall,0,:pow,:mul,:mul,
1,:roll,2m,:offset,coefficient,:fcall,1,:pow,:mul,:mul,
2,:roll,3m,:offset,coefficient,:fcall,2,:pow,:mul,:mul,
3,:roll,4m,:offset,coefficient,:fcall,3,:pow,:mul,:mul,
4,:roll,5m,:offset,coefficient,:fcall,4,:pow,:mul,:mul,
5,:roll,6m,:offset,coefficient,:fcall,5,:pow,:mul,:mul,
:add,:add,:add,:add,:add,
0.83,:mul, 1
:lt,
1

La estanqueidad del umbral se fija con este factor.

La suma α n=0 k (1-a) n es una serie geométrica que converge a 1. Por ejemplo, para α = 0 . 5 ver Tabla 4-3.

Tabla 4-3. La convergencia a 1 de la serie geométrica donde α = 0 . 5
T (1-α) T α n=0 k (1-a) n

0

0.5

0.5

1

0.25

0.75

2

0.125

0.88

3

0.063

0.938

4

0.031

0.969

5

0.016

0.984

Como no incluimos todos los valores de T, en realidad la función suavizada ya está multiplicada por un factor igual a la suma acumulada de esta serie geométrica hasta el número de términos que hayamos elegido. La Figura 4-35 muestra las sumas de un término y dos términos de la serie respecto al valor real (respectivamente, de abajo arriba).

srej 0435
Figura 4-35. Efecto de escala al elegir una suma limitada

La Figura 4-36 muestra cómo diferentes selecciones de α y T afectan al umbral dinámico, tanto en lo que se refiere a su suavizado como a su factor de escala aproximado respecto al indicador verdadero.

srej 0436
Figura 4-36. Efecto de suavizado y escalado al elegir diferentes α y T

Ley de Escalabilidad Universal

En esta sección, vamos a cambiar totalmente nuestra mentalidad, pasando de suavizar puntos de datos que ocurrieron en el pasado (que utilizamos como umbrales de alerta dinámicos) a una técnica que nos permite predecir cómo será el rendimiento futuro si la concurrencia/rendimiento aumenta por encima de los niveles actuales, utilizando sólo un pequeño conjunto de muestras de cómo ha sido el rendimiento en niveles de concurrencia ya vistos. De este modo, podemos establecer alertas predictivas a medida que nos acercamos al límite de un objetivo de nivel de servicio, con la esperanza de evitar problemas en lugar de reaccionar ante algo que ya ha superado el límite. En otras palabras, esta técnica nos permite probar un valor de indicador de nivel de servicio previsto frente a nuestro SLO con un rendimiento que aún no hemos experimentado.

Esta técnica se basa en un principio matemático conocido como Ley de Little y Ley de Escalabilidad Universal (LSU). Reduciremos al mínimo la explicación matemática. Lo poco que se discuta puedes pasarlo por alto. Para más detalles, el libro de libre acceso Practical Scalability Analysis with the Universal Scalability Law (VividCortex) de Baron Schwartz es una gran referencia.

Utilización de la Ley de Escalabilidad Universal en el Proceso de Entrega

Además de predecir violaciones inminentes de los ANS en los sistemas de producción, podemos utilizar la misma telemetría en un canal de entrega para lanzar algo de tráfico a una pieza de software que no tiene por qué estar ni cerca del tráfico máximo que podría ver en producción y predecir si el tráfico a nivel de producción cumplirá un ANS. ¡Y podemos hacerlo antes de implementar una nueva versión del software en producción!

La Ley de Little, Ecuación 4-2, describe el comportamiento de las colas como una relación entre tres variables: tamaño de la cola ( N ), la latencia ( R ) y el rendimiento ( X ). Si la aplicación de la teoría de colas a las predicciones de SLI te parece un poco alucinante, no te preocupes (porque lo es). Pero para nuestro propósito de predecir un SLI, N representará el nivel de concurrencia de las solicitudes que pasan por nuestro sistema, X el rendimiento, y R una medida de latencia como la media o un valor de percentil alto. Como se trata de una relación entre tres variables, siempre que tengamos dos cualesquiera podemos derivar la tercera. Como nos interesa predecir la latencia ( R ), necesitaríamos predecirla en las dos dimensiones de concurrencia ( N ) y rendimiento ( X ).

Ecuación 4-2. Ley de Little
N = X R X = N / R R = N / X

La Ley de Escalabilidad Universal, Ecuación 4-3, nos permite en cambio proyectar la latencia en función de una sola variable: el rendimiento o la concurrencia. Esta ecuación requiere tres coeficientes, que se derivarán y actualizarán a partir de un modelo mantenido por Micrometer basado en observaciones reales sobre el rendimiento del sistema hasta este punto. La USL define κ como el coste de la diafonía, ϕ el coste de la contención y λ para ser la velocidad a la que funciona el sistema en condiciones sin carga. Los coeficientes se convierten en valores fijos que hacen que las predicciones sobre latencia, rendimiento o concurrencia dependan sólo de uno de los otros tres. Micrometer también publicará los valores de estos coeficientes a medida que cambien con el tiempo, para que puedas comparar las principales características de rendimiento que rigen el sistema a lo largo del tiempo.

Ecuación 4-3. Ley de escalabilidad universal
X ( N ) = λN 1+ϕ(N-1)+κN(N-1)

Con una serie de sustituciones, llegamos a expresar R en términos de X o N (ver Ecuación 4-4). De nuevo, no pienses demasiado en estas relaciones, porque Micrómetro hará estos cálculos por ti.

Ecuación 4-4. Latencia prevista en función del rendimiento o de la concurrencia
R ( N ) = 1+ϕ(N-1)+κN(N-1) λ R ( X ) = -X 2 (κ 2 +2κ(ϕ-2)+ϕ 2 )+2λX(κ-ϕ)+λ 2 +κX+λ-ϕX 2κX 2

Lo que obtendremos en su lugar es una bonita proyección bidimensional, como se muestra en la Figura 4-37.

srej 0437
Figura 4-37. Predicción USL de latencia basada en diferentes niveles de rendimiento

La previsión USL es una forma de Meter "derivada" en Micrómetro y puede activarse como se muestra en el Ejemplo 4-39. Micrómetro publicará un conjunto de Gauge metros formando una serie de previsiones a varios niveles de rendimiento/concurrencia para cada intervalo de publicación. El rendimiento y la concurrencia son medidas correlacionadas, así que piensa en ellas indistintamente a partir de este momento. Cuando selecciones un grupo relacionado de temporizadores (que siempre tendrán el mismo nombre) para publicar una previsión, Micrometer publicará varias métricas adicionales utilizando el nombre común de la métrica como prefijo:

temporizador.nombre.previsión

Una serie de medidores Gauge con una etiqueta throughput o concurrency en función del tipo de variable independiente seleccionada. En un intervalo de tiempo determinado, el trazado de estos medidores generaría una visualización como la de la Figura 4-37.

temporizador.nombre.diafonía

Una medida directa de la diafonía del sistema (por ejemplo, fan-out en un sistema distribuido como el descrito en el artículo de S. Cho et al., " Moolle: Fan-out Control for Scalable Distributed Data Stores").

temporizador.nombre.contencion

Una medida directa de la contención del sistema (por ejemplo, el bloqueo de tablas de bases de datos relacionales y, en general, cualquier otra forma de sincronización de bloqueos).

temporizador.nombre.descargado.rendimiento

Cabe esperar que las mejoras en el rendimiento ideal sin carga (por ejemplo, mejoras en el rendimiento del marco) también produzcan mejoras en condiciones de carga.

Ejemplo 4-39. Configuración de previsión de la ley de escalabilidad universal en Micrómetro
UniversalScalabilityLawForecast
    .builder(
      registry
        .find("http.server.requests") 1
        .tag("uri", "/myendpoint") 2
        .tag("status", s -> s.startsWith("2")) 3
    )
    .independentVariable(UniversalScalabilityLawForecast.Variable.THROUGHPUT) 4
    // In this case, forecast to up to 1,000 requests/second (throughput)
    .maximumForecast(1000)
    .register(registry);
1

La previsión se basará en los resultados de la búsqueda en el medidor Micrométrico de uno o varios temporizadores con el nombre http.server.requests (recuerda que puede haber varios temporizadores de este tipo con diferentes valores de etiqueta).

2

Podemos limitar aún más el conjunto de temporizadores en los que basar la previsión haciendo coincidir sólo los temporizadores que tengan un par específico de etiqueta clave-valor.

3

Como en cualquier búsqueda, también se puede restringir el valor de la etiqueta con una lambda. Un buen ejemplo es restringir la previsión a cualquier "2xx". estados HTTP.

4

El dominio del histograma Gauge será UniversalScalabilityLawForecast.Variable.CONCURRENCY o UniversalScalabilityLawForecast.Variable.THROUGHPUT, por defecto THROUGHPUT.

La latencia que experimenta una aplicación con su rendimiento actual en uno de estos intervalos de tiempo seguirá de cerca la latencia "prevista" en la previsión. Podemos establecer una alerta basada en un valor ampliado de cualquiera que sea el rendimiento actual, para determinar si la latencia prevista con ese rendimiento ampliado seguiría estando por debajo de nuestro SLO.

Además de predecir un SLI bajo un mayor rendimiento, los valores modelados para la diafonía, la contención y el rendimiento sin carga son un fuerte indicador de dónde pueden hacerse mejoras de rendimiento en una aplicación. Al fin y al cabo, la disminución de la diafonía y la contención y el aumento del rendimiento sin carga repercuten directamente en la latencia prevista y real del sistema bajo distintos niveles de carga.

Resumen

Este capítulo te ha presentado las herramientas que necesitas para empezar a monitorizar la disponibilidad de cada microservicio Java con señales incluidas en marcos de trabajo Java como Spring Boot. También hemos tratado de forma más general cómo alertar y visualizar clases de métricas como contadores y temporizadores.

Aunque deberías esforzarte por encontrar formas de medir la disponibilidad de los microservicios en términos de métricas centradas en el negocio, utilizar estas señales básicas es un gran paso adelante respecto a limitarse a mirar las métricas de caja en términos de entender cómo está funcionando tu servicio.

Desde el punto de vista organizativo, te has comprometido a poner en marcha una herramienta de cuadros de mando/alertas. En este capítulo hemos mostrado Grafana. Su disponibilidad de código abierto y sus fuentes de datos para una amplia gama de sistemas de monitoreo populares lo convierten en una opción sólida para construir sobre él sin atarse completamente a un proveedor en particular.

En el próximo capítulo, pasaremos a la automatización de la entrega, donde veremos cómo se utilizan algunas de estas señales de disponibilidad para tomar decisiones sobre la idoneidad de las nuevas versiones de microservicios. La entrega eficaz no consiste estrictamente en el movimiento de la implementación; convierte el monitoreo en acción.

Get SRE con Microservicios Java now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.