Mejorando Rendimiento y Escalabilidad: Guía de Caché Distribuido en ASP.NET Core
Isaac Ojeda
Posted on December 15, 2023
Introducción
En el ámbito dinámico de las aplicaciones web modernas, la optimización del rendimiento y la escalabilidad son esenciales. El uso estratégico de la caché es una herramienta fundamental para mejorar la velocidad y la capacidad de escalar, especialmente en entornos de múltiples servidores o en la nube.
ASP.NET Core ofrece una solución potente: el caché distribuido. Esta herramienta permite compartir la caché entre varios servidores, manteniéndola como un servicio externo accesible para todas las instancias de la aplicación. Esta capacidad distribuida garantiza la coherencia de los datos, incluso en entornos con múltiples nodos de servidor.
En este artículo, exploraremos cómo implementar y utilizar el caché distribuido en ASP.NET Core. Desde la configuración inicial hasta la integración con diferentes proveedores de caché, pasando por ejemplos de código prácticos, descubriremos cómo aprovechar al máximo esta potente funcionalidad para mejorar el rendimiento y la escalabilidad de tus aplicaciones.
¿Qué es la Caché Distribuida?
La caché distribuida es un recurso compartido entre varios servidores, mantenido por lo general como un servicio externo a los servidores que acceden a él. Esta estrategia de almacenamiento puede marcar la diferencia en la eficiencia de nuestras aplicaciones, garantizando la coherencia y accesibilidad de los datos entre múltiples servidores y sobreviviendo a reinicios o actualizaciones de servidores.
Ventajas de la Caché Distribuida
- Coherencia de Datos: La información se mantiene consistente entre distintos servidores.
- Resistente a reinicios: Sobrevive a reinicios y deployments de servidores.
- Uso eficiente de memoria: No depende de la memoria local de cada servidor.
- Escalabilidad: Cuando una aplicación utiliza caché y esta tiene escalado horizontal, es necesario tener una caché distribuida para que todas las instancias accedan a la misma información.
Cuándo Utilizar Caché Distribuida
La implementación de una caché distribuida puede ser beneficiosa en varios escenarios, especialmente cuando se requiere mejorar el rendimiento y la eficiencia de las aplicaciones. Algunos casos comunes donde el uso de caché es altamente recomendado incluyen:
- Acceso Frecuente a Datos Estáticos o Poco Cambiantes
- Acceso a información costosa de sistemas externos o bases de datos
- Escalabilidad horizontal
Implementaciones de Caché Distribuida en ASP.NET Core
Redis
Redis, un almacén de datos en memoria, es una opción robusta para una caché distribuida. En este artículo, aprenderemos cómo configurar y utilizar Redis en conjunto con ASP.NET Core para mejorar el rendimiento de nuestras aplicaciones.
SQL Server
Utilizar SQL Server como caché distribuida puede ser una opción sólida. Sin embargo, es esencial entender sus implicaciones y cómo implementarlo correctamente para evitar impactos negativos en el rendimiento.
Proyecto DistributedCacheExample (.NET 8)
El código fuente de este ejemplo se encuentra disponible en DevToPosts/DistributedCacheExample · isaacOjeda/DevToPosts.
Para iniciar, creamos un proyecto web vacío (o API) y agregamos las siguientes dependencias:
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
La interfaz IDistributedCache
ofrece métodos para escribir y leer caché, lo que normalmente involucra guardar objetos o cualquier tipo de dato en caché.
El siguiente servicio facilita la lectura y escritura en la caché:
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace DistributedCacheExample;
public class DistributedCacheService(IDistributedCache distributedCache)
{
public async Task<T?> GetCachedItem<T>(string key)
where T : class
{
var dataInBytes = await distributedCache.GetAsync(key);
if (dataInBytes is null)
{
return null;
}
var rawJson = System.Text.Encoding.UTF8.GetString(dataInBytes);
return JsonSerializer.Deserialize<T>(rawJson);
}
public async Task SaveItem<T>(T item, string key, int expirationInMinutes)
{
var dataJson = JsonSerializer.Serialize(item);
var dataInBytes = System.Text.Encoding.UTF8.GetBytes(dataJson);
await distributedCache.SetAsync(key, dataInBytes, new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(expirationInMinutes)
});
}
}
Esta clase maneja las operaciones de lectura y escritura en la caché, todos los datos los serializamos de JSON a bytes o viceversa según aplique.
En Program.cs registramos las dependencias necesarias e incluimos un ejemplo de Endpoint para mostrar cómo podemos utilizar la caché.
En este ejemplo simulamos la consulta del clima desde un servicio externo. Deseamos evitar realizar esta consulta con frecuencia ya que los datos climáticos no cambian rápidamente. En el ejemplo, guardamos en caché los datos por 5 minutos, pero esto puede variar según el caso de uso.
using DistributedCacheExample;
var builder = WebApplication.CreateBuilder(args);
// Registro de servicios
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<DistributedCacheService>();
//
builder.Services.AddDistributedMemoryCache();
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("RedisCache");
options.InstanceName = "DistributedCacheExample";
});
var app = builder.Build();
// Configuración del pipeline de solicitud HTTP
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("api/weather", async (DistributedCacheService cache) =>
{
string[] summaries = {
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
var data = await cache.GetCachedItem<IEnumerable<WeatherForecast>>("GetWeatherForecast");
if (data is null)
{
data = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
await cache.SaveItem(data, "GetWeatherForecast", expirationInMinutes: 5);
}
return data;
});
app.Run();
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
La función AddDistributedMemoryCache
registra las dependencias como IDistributedCache
, pero se limita a proporcionar soporte para caché en memoria dentro de la misma instancia. A pesar de no ser un verdadero sistema de caché distribuido, resulta útil cuando estamos en una etapa de desarrollo y no contamos con una infraestructura disponible.
Por otro lado, al añadir AddStackExchangeRedisCache
, estamos incorporando la implementación de IDistributedCache
, esta vez con Redis. Sin embargo, es fundamental tener la infraestructura necesaria para poder probar y utilizar Redis en un entorno de desarrollo. Este enfoque nos permite aprovechar las capacidades de Redis como sistema de caché distribuido y representa una opción sólida cuando ya buscamos tener algo en producción.
Además agregamos un endpoint que simula la consulta al clima desde un servicio externo. Si los datos climáticos no están en la caché, generamos datos aleatorios de prueba, pero en un caso real, tendríamos que llamar a una API del clima. Si los datos están en la caché, se devuelven desde la caché para evitar consultas frecuentes al servicio externo.
También se agregó la cadena de conexión para la comunicación con Redis en el archivo de configuración:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RedisCache": "redis"
}
}
Nota 💡: Aquí en la cadena de conexión estamos poniendo el nombre del host que tendrá en la red de Docker (lo veremos en la sección de infraestructura) pero si no se usa docker, tendríamos que poner el host y puerto que apunten a la instancia de Redis.
Infraestructura
Antes de ejecutar la aplicación, es necesario tener Redis en funcionamiento.
En este ejemplo, opté por utilizar Docker y docker-compose, pero tienes libertad para elegir el método que prefieras. En este enlace puedes leer más sobre Redis y su instalación.
Archivo Docker
El siguiente archivo Docker fue generado por Visual Studio al añadir soporte para Docker y docker-compose. Puedes seguir este ejemplo o escribirlo manualmente (o simplemente ignorarlo, no es imprescindible):
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["DistributedCacheExample/DistributedCacheExample.csproj", "DistributedCacheExample/"]
RUN dotnet restore "DistributedCacheExample/DistributedCacheExample.csproj"
COPY . .
WORKDIR "/src/DistributedCacheExample"
RUN dotnet build "DistributedCacheExample.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "DistributedCacheExample.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DistributedCacheExample.dll"]
Archivo Docker Compose
Para ejecutar mi API y Redis juntos, usamos este archivo docker-compose
para orquestar la infraestructura con su propia red de forma sencilla:
version: '3.4'
services:
distributedcacheexample:
image: ${DOCKER_REGISTRY-}distributedcacheexample
build:
context: .
dockerfile: DistributedCacheExample/Dockerfile
ports:
- 7198:443
- 5106:80
networks:
- balusoft
redis:
image: redis/redis-stack:latest
container_name: redis-stack-server
restart: always
ports:
- 6379:6379
- 8001:8001
networks:
- balusoft
volumes:
- redis:/redis/data
networks:
balusoft:
name: balusoft-network
volumes:
redis:
driver: local
Nota 💡: También puedes usar .NET Aspire (.NET Aspire overview - .NET Aspire | Microsoft Learn), que es una forma mucho más sencilla de orquestar esta infraestructura
Con esto será suficiente para correr la aplicación.
Probando el Caché
Si estás usando Visual Studio, puedes ejecutar el proyecto a través de Docker Compose:
Pero sin problemas, se puede correr también desde VS Code y la Terminal utilizando el comando docker compose up
:
Independientemente de cómo hayas ejecutado docker-compose
, en Docker Desktop podrás ver los servicios activos junto con los puertos asignados:
En esta configuración, estamos usando redis-stack-server
, un conjunto de tecnologías aparte del caché que ofrece Redis.
Si accedes al puerto localhost:8001
, se abrirá un explorador de Redis:
Ahora, al dirigirte a Swagger en la API (por ejemplo, en mi caso, se encuentra en http://localhost:5106/swagger/
), verás el endpoint de ejemplo que creamos:
Al ejecutar este endpoint, observarás que se crea una entrada de caché en Redis:
De esta forma, estamos confirmando que el caché funciona, si ejecutas varias veces el endpoint, se estará leyendo esta información del caché.
En un escenario real, esto ayuda a optimizar el rendimiento, evitando consultar información costosa de forma innecesaria. Cada caso es distinto, pero el principio es el mismo y esto es una pequeña introducción. Aquí vemos una implementación más completa sobre este mismo principio.
Conclusión
El caché distribuido en ASP.NET Core se revela como una herramienta imprescindible para optimizar el rendimiento y la escalabilidad de tus aplicaciones. La capacidad de compartir datos en caché entre múltiples servidores, garantizando coherencia y supervivencia a reinicios o despliegues, abre un abanico de posibilidades para mejorar la eficiencia y la experiencia del usuario.
Al aprender a configurar y utilizar el caché distribuido, descubrimos cómo aprovechar su flexibilidad para adaptarse a diferentes escenarios y proveedores. Desde Redis y SQL Server hasta soluciones personalizadas como NCache, las opciones son amplias y versátiles, permitiendo adaptar la implementación a las necesidades específicas de cada aplicación.
La capacidad de almacenar datos comunes en caché y minimizar las consultas repetitivas a bases de datos o servicios externos se traduce en mejoras significativas de rendimiento. Además, el control sobre la caducidad de los datos nos otorga la posibilidad de equilibrar la frescura de la información con la eficiencia del sistema.
En resumen, el uso estratégico del caché distribuido representa un pilar fundamental en la mejora del rendimiento y la escalabilidad de las aplicaciones ASP.NET Core. Dominar esta herramienta no solo potencia la velocidad y la eficiencia, sino que también contribuye a una experiencia de usuario más fluida y satisfactoria.
Referencias
Posted on December 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 15, 2023