Blog de Amazon Web Services (AWS)

Tecnología .NET 5 con AWS Graviton2: Benchmarks

Por Kirk Davis, Developer Advocate para Modernización de Aplicaciones.

 

En 2019, AWS anunció nuevos tipos de instancias de Amazon EC2 con tecnología del procesador AWS Graviton2. El procesador AWS Graviton2 se basa en la arquitectura ARM64 que aprovecha los núcleos ARM Neoverse N1 de 64 bits. Desde 2019, AWS ha lanzado muchas nuevas instancias de EC2 construidas sobre Graviton2, entre ellas los tipos de propósito general (M6g), optimizado para computación (C6g), optimizado para memoria (R6g) y ampliable de propósito general (T4g). Estas instancias basadas en Graviton2 proporcionan hasta un 40% mejor rendimiento en relación con sus instancias x86-64 de generación comparable. Estos tipos de instancias utilizan la misma convención de nomenclatura que otros tipos, pero con una “g” anexada a la familia. Por ejemplo, una t4g.large, o un c6g.2xlarge. Muchos clientes ya están ejecutando cargas de trabajo en estas instancias Graviton2, incluidas las aplicaciones .NET Core. Tenga en cuenta que en este artículo me referiré a estos procesadores de 64 bits como “x86”.

Organizaciones como AnandTech han realizado en profundidad un benchmarking de Graviton2 contra instancias de EC2 con arquitectura x86 y encontraron que Graviton2 tiene una ventaja significativa de rendimiento y costo. Al comparar familias de instancias similares, las instancias Graviton2 tienen un costo por hora de aproximadamente un 20% menos que las instancias Intel x86 y hasta un 40% de mejor rendimiento. Con .NET 5 lanzado oficialmente en noviembre, pensé que sería interesante ver qué ventajas tiene Graviton2 para las aplicaciones web .NET 5 como seguimiento al blog .NET 5 en AWS publicado anteriormente. Sigue este blog para aprender cómo realicé las pruebas de benchmarking, las aplicaciones que elegí para benchmark y para ver los resultados.

 

Panorama general

Decidí ejecutar algunos benchmarks de .NET 5 que probaron ASP.NET Core bajo carga tanto para instancias basadas en x86 como en Graviton2. ASP.NET Core ejecuta código de aplicación en hilos de grupo de hilos, por lo que aprovecha múltiples núcleos para manejar múltiples solicitudes de forma concurrente. Algo a tener en cuenta es que los tipos de instancia de EC2 basados en x86 usan multithreading simultáneo y una vCPU se mapea a un núcleo lógico. No obstante, para instancias Graviton2 una vCPU se mapea a un núcleo físico. Entonces, para estos benchmarks, utilicé tipos de instancia x86 y ARM64 con 4 vCPU: tipos de instancia m5.xlarge, que tienen cuatro núcleos x86 lógicos (dos físicos) e instancias m6g.xlarge, que tienen cuatro núcleos ARM físicos. Quería comparar la latencia y el rendimiento de solicitudes por segundo para diferentes escenarios y luego comparar el rendimiento ajustado para el costo por hora de las instancias. Utilicé el precio por hora de la región us-east-2 (Ohio):

 

M5.xlarge M6g.xlarge
Costo $0.192 $0.154
vCPU 4 4
RAM 16 16

 

Framework de pruebas y benchmarks.

Utilicé el software de código abierto Crank para ejecutar los benchmarks y reunir resultados. Crank abstrae muchos de los detalles desordenados en ejecutar benchmarks y entrega resultados consistentes. De la página de GitHub:

“Crank es la infraestructura de benchmarking utilizada por el equipo de .NET para ejecutar benchmarks que incluyen (pero no limitado a) escenarios de los Benchmarks Web Framework TechEmpower”.

Crank utiliza un controlador (Crank controller), que se comunica con uno o más agentes (Crank agent). Los agentes descargan, compilan y ejecutan el código, luego reportan los resultados al controlador. En este caso, utilicé tres agentes: uno en cada una en las instancias a probar y otro en una instancia test-runner (una m5.xlarge) que ejecutaba bombardier, una herramienta común de prueba de carga que ya está integrada en Crank. También puede elegir wrk2, u otras herramientas si lo prefiere (los archivos readme de Crank proporcionan ejemplos para ambos). Corrí todas las instancias en la misma Zona de Disponibilidad (AZ) para minimizar cualquier otra fuente de latencia. El montaje se veía así:

 

Nota: Con el fin de utilizar el agente de Crank con la versión de lanzamiento .NET 5, realicé cambios menores en su clase Startup.cs. Estos cambios obligaron a Crank a descargar la versión correcta .NET 5 SDK y corrigió un problema en el que no estaba anexando los parámetros de compilación correctos para arm64 al compilar código en la instancia m6g.xlarge. Es posible que el proyecto Microsoft.Crank.Agent se haya actualizado desde que lo usé. También actualicé todos los proyectos a .NET 5.

 

Pruebas de benchmark

Dado que muchas de las cargas de trabajo .NET Core que los clientes están ejecutando en AWS son sitios web o APIs de ASP.NET Core, me enfoqué solo en este tipo de aplicaciones. Seleccioné el proyecto Mvc del repositorio de GitHub de benchmarks ASP.NET. El controlador en este proyecto define una clase “Entry” y luego las crea y devuelve como List<Entry> (que se serializa a JSON por ASP.NET Core). Para obtener el código fuente de estos métodos, consulte los enlaces anteriores de GitHub. En el proyecto, el archivo YAML de configuración de Crank define tres escenarios (tenga en cuenta que utilicé estos escenarios pero intercambié wrk por bombardier).

  • MVCJsonNet2k: llama al método json2k() del JsonController (devuelve ocho Entradas)
  • MVCJsonOutput60k: llama al método jsonnk() del JsonController para 60,000 bytes
  • MVCJsonOutput2M: llama al método jsonnk() del JsonController para 221 bytes

Adicionalmente, creé otra aplicación ASP.NET Core Web API basada en el proyecto de API web ASP.NET y agregué EF Core. Hago esto porque muchas aplicaciones ASP.NET Core usan Entity Framework Core (EF Core) y hacen un trabajo computacionalmente más costoso que solo serializar JSON. Para aislar el rendimiento de las dos instancias, utilicé el proveedor en memoria para EF Core y poblé un DBSet con resúmenes meteorológicos al inicio. Modifiqué el WeatherForecastController para cifrar la propiedad Summary de cada entidad WeatherForecast usando la clase RSACryptoServiceProvider de .NET y luego agregué otro controlador que consulta las previsiones del DBSet y los serializa en cadenas. Para ese método, agregué un retraso asincrónico (usando Task.Delay) para simular la búsqueda de una base de datos relacional. Para ejecutar las pruebas, creé un archivo YAML de configuración de Crank que define tres escenarios:

  • AsyncParallelJson100: devuelve 100 pronósticos de EF Core serializados a una cadena usando Text.Json
  • AsyncParallelJson500: devuelve 500 pronósticos de EF Core serializados a una cadena usando Text.Json
  • ParallelEncriptWather100: cifra resúmenes de 100 pronósticos y devuelve los pronósticos como IENumerable<WeatherForecast>

Esta aplicación utiliza la versión 5.0.0 de los paquetes Microsoft.Entityframeworkcore y Microsoft.Entityframeworkcore.inMemory NuGet. El siguiente es el código fuente para los dos métodos que utilicé en las pruebas:

Método get de JsonSerializeController:

[HttpGet]
public async Task<IEnumerable<string>> Get(int count = 100)
{
    List<WeatherForecast> forecasts;
    List<string> jsons = new List<string>();

    using (var context = new WeatherContext())
    {
        forecasts = context.WeatherForecasts.Take(count).ToList();
    }
    await Task.Delay(5);
    Parallel.ForEach(forecasts, x => jsons.Add(JsonSerializer.Serialize(x)));

    return jsons;
}

Método get WeatherForecastController

[HttpGet]
public IEnumerable<WeatherForecast> Get(int count = 100)
{
    List<WeatherForecast> forecasts;

    using (var context = new WeatherContext())
    {
        forecasts = context.WeatherForecasts.Take(count).ToList();
    }
    UnicodeEncoding ByteConverter = new UnicodeEncoding();

    using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
    {
        Parallel.ForEach(forecasts, x => x.EncryptedSummary = RSAEncrypt(ByteConverter.GetBytes(x.Summary), RSA.ExportParameters(false), false));
    }
    return forecasts;
}

 

Nota: El método RSAEncrypt se copió del código de ejemplo en los docs de RSACryptoServiceProvider.

Configurar las instancias

 Para ejecutar los benchmarks, seleccioné la imagen de máquina de Amazon (AMI) para Ubuntu Server 20.04 LTS y elegí “64-bit (x86)” para el m5.xlarge y “64-bit (Arm)” para el m6g.xlarge. Les asigné a ambos 20GB de almacenamiento de Amazon Elastic Block Store (EBS) y elegí un grupo de seguridad con el puerto 22 abierto a mi dirección IP de casa, para que pudiera conectarme por SSH a ellos. Si bien es posible instalar y usar .NET 5 en Amazon Linux 2 (AL2), actualmente esa no es una distribución Linux compatible para .NET 5 en ARM y quería la misma distribución tanto para x86 como para ARM64. Para obtener más información sobre el lanzamiento de instancias Graviton2 desde AWS Management Console, consulte la publicación del blog .NET 5 en AWS del 10 de noviembre de 2020.

Ubuntu 20.04 es una versión compatible para instalar .NET 5 usando apt-get, pero aún no se admiten arquitecturas ARM. Entonces—y para usar el mismo método en ambas instaciones— instalé manualmente el SDK .NET 5 usando los siguientes comandos, especificando el enlace de descarga apropiado para la arquitectura para los binarios*. Las instrucciones para la instalación manual también están disponibles en el enlace anterior “instalación.NET 5”.

 

<code class="lang-bash">curl -SL -o dotnet.tar.gz &lt;link to architecture-specific binary file*&gt;
sudo mkdir -p /usr/share/dotnet
sudo tar -zxf dotnet.tar.gz -C /usr/share/dotnet
sudo ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet
echo &quot;export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true&quot; &gt;&gt; ~/.bash_profile
</code>

 

 

Después, utilicé SCP para subir el código fuente de mi solución de benchmarking a las instancias y me conecté a ambas por SSH, usando dos pestañas en la nueva Terminal de Windows.

*En el momento en que se escribió este artículo, los binarios utilizados eran:

dotnet-sdk-5.0.100-linux-arm64.tar.gz

dotnet-sdk-5.0.100-linux-x64.tar.gz

 

Resultados del benchmark

Corridas y unidades del benchmark

Utilicé Crank para realizar dos corridas de cada uno de los seis benchmarks en cada una de las dos instancias y tomé el promedio de las dos corridas para cada uno. Hubo una variación mínima entre corridas. Para cada prueba, tracé la latencia en microsegundos (μs), con las barras para MVCJsonOutput2M y ParallelEncriptWather100 escaladas trazando μs/100 y barras para AsyncParallelJson100 y AsyncParallelJson500 escaladas con μs/10. Para la latencia, las barras más cortas son mejores.

También mapeé el desempeño en solicitudas/segundo y el valor general como rendimiento/dólar, donde el rendimiento es las solicitudas/segundo y los dólares es el costo/hora del tipo de instancia dado. Para tener las barras legibles en el mismo gráfico, se escalaron algunos valores como se muestra debajo del gráfico (se aplicó la misma escala a todos los valores para un benchmark dado). Tanto para el rendimiento bruto como para el rendimiento/precio, las barras más largas son mejores.

Tenga en cuenta que no hice ninguna optimización específica para ARM64 o x86.

Resumen de resultados

La instancia Graviton2 tuvo menor latencia en general para las pruebas que ejecuté, con la instancia m6g.xlarge (Graviton2) teniendo hasta 24.7% menor latencia (para MVCJsonOutput2M) que la m5.xlarge (x86-64). Es notable que en general, cuanto más trabajo estaba haciendo el método de prueba, mayor fue la ventaja de Graviton2.

Los resultados fueron ampliamente similares para las solicitudes/segundo, con Graviton2 entregando hasta un 31.6% mejor rendimiento (para MVCJsonOutput2m). Para la prueba más costosa computacionalmente — ParallelEncriptWather100 — la instancia Graviton2 generó 16.6% más solicitudes por segundo. Y todo esto es sin considerar la diferencia de precio. Además, lo que no se refleja en los gráficos es que la instancia x86 tenía el doble de solicitudes malas (promedio de 16) que la instancia Graviton2 (promedio de 8) para la prueba ParallelEncriptWather100. ParallelEncriptWather100 fue la única prueba donde hubo alguna mala respuesta a través de todas las pruebas.

Al escalar el rendimiento para el precio por hora de cada tipo de instancia, las diferencias son más marcadas. El Graviton2 ofrece hasta 64% más solicitudas/segundo por costo por hora de la instancia (para MVCJsonOutput2M). Incluso en la prueba con la menor ventaja (MVCJsonNet2k), el Graviton2 proporcionó 30.8% mejor rendimiento/costo, donde el rendimiento está en solicitudes/segundo. Este tipo de resultados pueden traducirse en ahorros significativos para cargas de trabajo incluso de tamaño modesto.

Gráficas

 

En el gráfico anterior, la latencia media se muestra en microsegundos (μs), con los valores de algunas pruebas divididos entre 10 o 100 para hacer visibles todas las barras en el gráfico. La instancia Graviton2 tuvo 24.7% menor latencia para la prueba MVCJsonOutput2m y tuvo menor latencia en todas las pruebas.

 

 

Este segundo gráfico muestra cómo la instancia m6g.xlarge Graviton2 manejó más solicitudes para cada prueba. Las barras representan las solicitudes crudas por segundo para cada prueba. Los valores de algunas pruebas se escalan por un factor de 10 para hacer visibles todas las barras en el gráfico. Para la prueba MVCJsonOutput2M, que serializa dos megabytes a JSON, manejó 31.6% más solicitudes por segundo y fue más rápido para cada prueba que ejecuté.

 

Conclusión

Si está adoptando .NET 5 para sus aplicaciones, tiene una variedad de opciones para desplegarlas en AWS. Puede ejecutarlos en contenedores en Amazon Elastic Container Service (ECS) o Amazon Elastic Kubernetes Service (EKS) con o sin AWS Fargate, puede implementarlos como funciones serverless en AWS Lambda, o implementarlas en EC2 utilizando instancias basadas en x86 o basadas en Graviton2.

Para ejecutar aplicaciones web escalables construidas en ASP.NET Core 5.0, las nuevas familias de instancias Graviton2 ofrecen importantes ventajas de rendimiento y aún más convincentes ventajas de rendimiento/precio de hasta el 64% sobre las familias de instancias equivalentes de Intel x86 sin realizar ningún cambio de código. En conjunto con las mejoras de rendimiento de ARM64 en .NET 5, pasar de .NET Core 3.1 en x86 a .NET 5 en Graviton2 promete ahorros significativos en costos. También permite a los desarrolladores codificar y probar localmente en sus máquinas de desarrollo basadas en x86 (o incluso nuevas laptops macOS basadas en ARM) y utilizar sus mecanismos de implementación existentes. Si su aplicación sigue basada en .NET Framework, considere utilizar AWS Portable Assistant para .NET para comenzar a portar a .NET Core.

 

Obtenga más información sobre las instancias basadas en AWS Graviton2.

 

Este artículo fue traducido del Blog de AWS en Inglés

 


Sobre el autor

Kirk Davis es Arquitecto de Soluciones App Modernization en AWS.