Optimización del rendimiento: varios servicios de back-end

Azure Kubernetes Service (AKS)
Azure Cosmos DB

En este artículo se describe el uso de métricas para encontrar cuellos de botella y mejorar el rendimiento de un sistema distribuido por parte de un equipo de desarrollo. El artículo se basa en la prueba de carga real que se realizó para una aplicación de ejemplo. La aplicación es de la Base de referencia de Azure Kubernetes Service (AKS) para microservicios, junto con un proyecto de prueba de carga de Visual Studio que se usa para generar los resultados.

Este artículo forma parte de una serie. Puede consultar la primera parte aquí.

Escenario: Llamar a varios servicios back-end para recuperar información y, a continuación, agregar los resultados.

Este escenario implica una aplicación de entrega con drones. Los clientes pueden consultar una API de REST para obtener la información de la factura más reciente. La factura incluye un resumen de las entregas, los paquetes y el uso total de drones del cliente. Esta aplicación usa una arquitectura de microservicios que se ejecuta en AKS, y la información necesaria para la factura se reparte entre varios microservicios.

En lugar de ser el cliente quien llame a cada servicio directamente, la aplicación implementa el patrón Agregación de puertas de enlace. Con este patrón, el cliente realiza una solicitud única a un servicio de puerta de enlace. A su vez, la puerta de enlace llama a los servicios back-end en paralelo y, a continuación, agrega los resultados a una única carga útil de respuesta.

Diagrama que muestra el patrón Agregación de puertas de enlace

Prueba 1: Rendimiento de línea base

Para establecer una línea base, el equipo de desarrollo comenzó con una prueba de carga por pasos, que supuso un aumento de la carga de un usuario simulado hasta 40 usuarios, con una duración total de 8 minutos. En el siguiente gráfico, tomado de Visual Studio, se muestran los resultados. La línea púrpura muestra la carga de usuarios y la línea naranja muestra el rendimiento (promedio de solicitudes por segundo).

Gráfico de resultados de la prueba de carga de Visual Studio

La línea roja a lo largo de la parte inferior del gráfico muestra que no se devolvieron errores al cliente, lo que es alentador. Sin embargo, el rendimiento medio es máximo hacia la mitad de la prueba y cae durante el resto, incluso si la carga sigue aumentando. Esto indica que el back-end no puede continuar. El patrón que se ve aquí es habitual cuando un sistema empieza a alcanzar los límites de los recursos (después de alcanzar un máximo, el rendimiento se reduce significativamente). La contención de recursos, los errores transitorios o un aumento de la tasa de excepciones pueden contribuir a este patrón.

Vamos a profundizar en los datos de supervisión para obtener información sobre lo que sucede dentro del sistema. El siguiente gráfico se ha extraído de Application Insights. Muestra las duraciones medias de las llamadas HTTP desde la puerta de enlace a los servicios back-end.

Gráfico de duraciones de las llamadas HTTP

Este gráfico muestra que una operación en particular, GetDroneUtilization, tarda mucho más de media, por orden de magnitud. La puerta de enlace realiza estas llamadas en paralelo, por lo que la operación más lenta determina cuánto tiempo tarda en completarse la solicitud completa.

Claramente, el paso siguiente es profundizar en la operación GetDroneUtilization y buscar posibles cuellos de botella. Una posibilidad es el agotamiento de los recursos. Es posible que este servicio de back-end concreto se esté quedando sin CPU o memoria. En el caso de un clúster de AKS, esta información está disponible en Azure Portal a través de la característica Container Insights de Azure Monitor. Los gráficos siguientes muestran el uso de recursos en el nivel de clúster:

Gráfico de utilización del nodo AKS

En esta captura de pantalla se muestran los valores promedio y máximo. Es importante observar algo más que el promedio, ya que el promedio puede ocultar picos en los datos. En este caso, el uso promedio de la CPU permanece por debajo del 50 %, pero hay un par de picos del 80 %. Está cerca de la capacidad, pero se mantiene dentro de las tolerancias. Alguna otra cosa está causando el cuello de botella.

El siguiente gráfico revela la verdadera causa. En este gráfico se muestran los códigos de respuesta HTTP de la base de datos de back-end del servicio de entrega, que en este caso es Azure Cosmos DB. La línea azul representa códigos correctos (HTTP 2xx), mientras que la línea verde representa los errores HTTP 429. Un código de retorno HTTP 429 significa que Azure Cosmos DB está limitando temporalmente las solicitudes, ya que el autor de la llamada está consumiendo más unidades de recursos (RU) de las aprovisionadas.

Gráfico de solicitudes limitadas

Para obtener más información, el equipo de desarrollo usó Application Insights para ver la telemetría de un extremo a otro para una muestra representativa de solicitudes. A continuación se muestra un ejemplo:

Captura de pantalla de la vista de una transacción de un extremo a otro

Esta vista muestra las llamadas relacionadas con una única solicitud de cliente, junto con la información de tiempo y los códigos de respuesta. Las llamadas de nivel superior las realiza la puerta de enlace a los servicios de back-end. La llamada a GetDroneUtilization está expandida para mostrar las llamadas a dependencias externas, en este caso, a Azure Cosmos DB. La llamada en rojo devolvió un error HTTP 429.

Observe el gran lapso entre el error HTTP 429 y la siguiente llamada. Cuando la biblioteca de cliente de Azure Cosmos DB recibe un error HTTP 429, se retira automáticamente y espera para volver a intentar la operación. Esta vista muestra que, durante los 672 ms que tardó esta operación, la mayor parte de ese tiempo se dedicó a esperar el reintento de Azure Cosmos DB.

El siguiente es otro gráfico interesante para este análisis. Muestra el consumo de RU por partición física frente a las RU aprovisionadas por partición física:

Gráfico de consumo de RU por partición

Para encontrar sentido a este gráfico, debe comprender la manera en que Azure Cosmos DB administra las particiones. Las colecciones de Azure Cosmos DB pueden tener una clave de partición. Cada valor de clave posible define una partición lógica de los datos dentro de la colección. Azure Cosmos DB distribuye estas particiones lógicas entre una o varias particiones físicas. Azure Cosmos DB controla automáticamente la administración de particiones físicas. A medida que almacena más datos, Azure Cosmos DB podría trasladar las particiones lógicas a nuevas particiones físicas, con el fin de distribuir la carga entre las particiones físicas.

Para esta prueba de carga, la colección de Azure Cosmos DB se aprovisionó con 900 RU. El gráfico muestra 100 RU por partición física, lo que implica un total de nueve particiones físicas. Aunque Azure Cosmos DB controla automáticamente el particionamiento de las particiones físicas, conocer el número de particiones puede proporcionar información sobre el rendimiento. El equipo de desarrollo usará esta información más adelante, a medida que sigan realizando optimizaciones. Cuando la línea azul cruza la línea horizontal púrpura, el consumo de RU ha superado el número de RU aprovisionadas. Este es el punto en el que Azure Cosmos DB comenzará a limitar las llamadas.

Prueba 2: Aumento de las unidades de recursos

En la segunda prueba de carga, el equipo escaló horizontalmente la colección de Azure Cosmos DB de 900 RU a 2500 RU. El rendimiento aumentó de 19 solicitudes/segundo a 23 solicitudes/segundo y la latencia media cayó de 669 ms a 569 ms.

Métrica Prueba 1 Prueba 2
Rendimiento (solicitudes/s) 19 23
Latencia media (ms) 669 569
Solicitudes correctas 9800 11 000

No son grandes ventajas, pero si observamos el gráfico a lo largo del tiempo, se muestra una imagen más completa:

Gráfico de los resultado de la prueba de carga de Visual Studio en el que se muestran un rendimiento más consistente.

Mientras que la prueba anterior mostraba un pico inicial seguido de una caída marcada, esta prueba muestra un rendimiento más constante. Sin embargo, el rendimiento máximo no es mucho mayor.

Todas las solicitudes a Azure Cosmos DB devuelven un estado 2xx y los errores HTTP 429 han desaparecido:

Gráfico de llamadas de Azure Cosmos DB

El gráfico del consumo de RU frente a las RU aprovisionadas muestra que hay mucha capacidad de aumento. Hay aproximadamente 275 RU por partición física y la prueba de carga muestra un pico de aproximadamente 100 RU consumidas por segundo.

Gráfico de consumo de unidades de solicitud, en comparación con las unidades de solicitud aprovisionadas, en el que se muestra que hay mucha capacidad de aumento.

Otra métrica interesante es el número de llamadas a Azure Cosmos DB por operación correcta:

Métrica Prueba 1 Prueba 2
Llamadas por operación 11 9

Suponiendo que no se produzcan errores, el número de llamadas debería coincidir con el plan de consulta real. En este caso, la operación implica una consulta entre particiones que llega a las nueve particiones físicas. El valor más alto de la primera prueba de carga refleja el número de llamadas que devolvieron un error 429.

Esta métrica se calculó mediante la ejecución de una consulta de Log Analytics personalizada:

let start=datetime("2020-06-18T20:59:00.000Z");
let end=datetime("2020-07-24T21:10:00.000Z");
let operationNameToEval="GET DroneDeliveries/GetDroneUtilization";
let dependencyType="Azure DocumentDB";
let dataset=requests
| where timestamp > start and timestamp < end
| where success == true
| where name == operationNameToEval;
dataset
| project reqOk=itemCount
| summarize
    SuccessRequests=sum(reqOk),
    TotalNumberOfDepCalls=(toscalar(dependencies
    | where timestamp > start and timestamp < end
    | where type == dependencyType
    | summarize sum(itemCount)))
| project
    OperationName=operationNameToEval,
    DependencyName=dependencyType,
    SuccessRequests,
    AverageNumberOfDepCallsPerOperation=(TotalNumberOfDepCalls/SuccessRequests)

En resumen, la segunda prueba de carga muestra una mejora. Sin embargo, la operación GetDroneUtilization sigue teniendo un orden de magnitud mayor que la siguiente operación más lenta. Examinar las transacciones de un extremo a otro ayuda a explicar por qué:

Captura de pantalla de la segunda prueba de carga, en la que se muestra la mejora.

Como se mencionó anteriormente, la operación GetDroneUtilization implica una consulta entre particiones a Azure Cosmos DB. Esto significa que el cliente de Azure Cosmos DB tiene que realizar la distribución ramificada de la consulta en cada partición física y recopilar los resultados. Como se muestra en la vista de la transacción de un extremo a otro, estas consultas se ejecutan en serie. La duración de la operación equivale a la suma de todas las consultas, y este problema solo empeorará a medida que el tamaño de los datos crezca y se agreguen más particiones físicas.

Prueba 3: Consultas en paralelo

En función de los resultados anteriores, una manera obvia de reducir la latencia es emitir las consultas en paralelo. El SDK de cliente de Azure Cosmos DB tiene una opción de configuración que controla el grado máximo de paralelismo.

Valor Descripción
0 Sin paralelismo (valor predeterminado)
> 0 Número máximo de llamadas paralelas
-1 El SDK de cliente selecciona un grado óptimo de paralelismo

En la tercera prueba de carga, este valor se cambió de 0 a -1. En la tabla siguiente se resumen los resultados.

Métrica Prueba 1 Prueba 2 Prueba 3
Rendimiento (solicitudes/s) 19 23 42
Latencia media (ms) 669 569 215
Solicitudes correctas 9800 11 000 20 000
solicitudes limitadas 272 000 0 0

En el gráfico de prueba de carga, no solo el rendimiento global es mucho mayor (línea naranja), sino que el rendimiento también sigue el ritmo de la carga (línea púrpura).

Gráfico de los resultado de la prueba de carga de Visual Studio en el que se muestra un rendimiento global mayor que se ajusta al ritmo de la car.

Para comprobar que el cliente de Azure Cosmos DB está realizando consultas en paralelo, podemos examinar la vista de la transacción de un extremo a otro:

Captura de pantalla de la vista de la transacción de un extremo a otro en la que se muestra que el cliente de Azure Cosmos DB está realizando consultas en paralelo.

Curiosamente, un efecto secundario de aumentar el rendimiento es que también aumenta el número de RU consumidas por segundo. Aunque Azure Cosmos DB no limitaba las solicitudes durante esta prueba, el consumo se acercaba al límite de RU aprovisionadas:

Gráfico del consumo de unidades de solicitud cerca del límite de unidades aprovisionadas.

Este gráfico puede ser una señal para aumentar el escalado horizontal de la base de datos. Sin embargo, resulta que se puede optimizar la consulta en su lugar.

Paso 4: Optimización de la consulta

La prueba de carga anterior mostró un mejor rendimiento en términos de latencia y rendimiento. La latencia media de solicitudes se redujo en un 68 % y el rendimiento aumentó un 220 %. Sin embargo, la consulta entre particiones es una preocupación.

El problema con las consultas entre particiones es que se paga por RU en cada partición. Si la consulta solo se ejecuta ocasionalmente (por ejemplo, una vez por hora), es posible que no sea un problema. Pero siempre que vea una carga de trabajo con muchas lecturas que implique una consulta entre particiones, debe ver si la consulta se puede optimizar mediante la inclusión de una clave de partición. (Es posible que tenga que volver a diseñar la colección para usar una clave de partición diferente).

Esta es la consulta para este escenario en particular:

SELECT * FROM c
WHERE c.ownerId = <ownerIdValue> and
      c.year = <yearValue> and
      c.month = <monthValue>

Esta consulta selecciona los registros que coinciden con un id. de propietario determinado y un mes/año. En el diseño original, ninguna de estas propiedades es la clave de partición. Ello requiere que el cliente realice la distribución ramificada de la consulta en cada partición física y recopile los resultados. Para mejorar el rendimiento de la consulta, el equipo de desarrollo cambió el diseño para que el id. de propietario sea la clave de partición de la colección. De este modo, la consulta puede estar orientada a una partición física específica. (Azure Cosmos DB lo controla automáticamente; no tiene que administrar la asignación entre los valores de la clave de partición y las particiones físicas).

Después de cambiar la colección a la nueva clave de partición, se produjo una mejora espectacular en el consumo de RU, lo que se traduce directamente en costos menores.

Métrica Prueba 1 Prueba 2 Prueba 3 Prueba 4
RU por operación 29 29 29 3.4
Llamadas por operación 11 9 10 1

La vista de la transacción de un extremo a otro muestra que, tal y como se predijo, la consulta solo lee una partición física:

Captura de pantalla de la transacción de un extremo a otro en la que se muestra que la consulta lee solo una partición física.

La prueba de carga muestra un rendimiento y una latencia mejorados:

Métrica Prueba 1 Prueba 2 Prueba 3 Prueba 4
Rendimiento (solicitudes/s) 19 23 42 59
Latencia media (ms) 669 569 215 176
Solicitudes correctas 9800 11 000 20 000 29 000
solicitudes limitadas 272 000 0 0 0

Una consecuencia del rendimiento mejorado es que el uso de la CPU del nodo es muy elevado:

Graph en el que se muestra un uso elevado de la CPU del nodo.

Hacia el final de la prueba de carga, el promedio de uso de CPU alcanzó el 90 % y el uso máximo de CPU alcanzó el 100 %. Esta métrica indica que la CPU es el siguiente cuello de botella del sistema. Si se necesita un mayor rendimiento, el paso siguiente podría ser escalar horizontalmente el servicio de entrega a más instancias.

Resumen

En este escenario, se identificaron los siguientes cuellos de botella:

  • Azure Cosmos DB limita las solicitudes debido a un número insuficiente de RU aprovisionadas.
  • Latencia alta debida a la consulta de varias particiones de base de datos en serie.
  • Consulta entre particiones ineficaz, porque la consulta no incluía la clave de partición.

Además, el uso de la CPU se identificó como un cuello de botella potencial a mayor escala. Para diagnosticar estos problemas, el equipo de desarrollo examinó lo siguiente:

  • La latencia y el rendimiento de la prueba de carga.
  • Los errores de Azure Cosmos DB y el consumo de RU.
  • Vista de la transacción de un extremo a extremo en Application Insights.
  • El uso de CPU y memoria en Container Insights de Azure Monitor.

Pasos siguientes

Consulte Antipatrones de rendimiento.