Resiliencia de una API
La capacidad de enfrentar desafíos, aprender de los errores, adaptarse a situaciones adversas y nunca comprometer la disponibilidad del servicio es el lema de la resiliencia de una API.
Este artículo está diseñado para programadores que deseen explorar cómo se aborda la resiliencia en el contexto de las API. ¿Te has preguntado alguna vez cuál es el conjunto mínimo de consideraciones que debes tener en cuenta? ¿O cuáles son los escenarios que merecen tu atención? Si estás en busca de una guía urgente que responda estas preguntas de manera clara, estás en el lugar adecuado. Aquí, buscamos proporcionar respuestas claras y establecer un camino simple pero efectivo hacia la comprensión y aplicación de la resiliencia en tus desarrollos de API.
Para lograr un diseño efectivo de una API resiliente, es necesario considerar diversas prácticas que apunten a “la capacidad de resistir y recuperarse de eventos inesperados o condiciones adversas”. Para alcanzar este objetivo, analicemos los principales casos de uso con ejemplos claros.
Comencemos con lo esencial
Comenzar con lo básico nos salva el pellejo en más de una ocasión. Algo tan simple como un buen manejo de logs puede marcar la diferencia entre pasar horas dando vueltas y ver la luz al final del tunel.
Control de Errores
Implementar un manejo de errores robusto y consistente en la API para proporcionar respuestas claras y significativas en caso de fallo.
Utilizar códigos de estado HTTP apropiados para indicar el resultado de la operación y proporcionar información detallada en el cuerpo de la respuesta.
Escenario idóneo:
En cualquier situación en la que se pueda producir un error durante la ejecución de una operación. Es fundamental para proporcionar respuestas claras y significativas al usuario y facilitar la detección y resolución de problemas.
Ejemplo csharp (c#):
public IActionResult MiAccion()
{
try
{
// Lógica de la acción
return Ok("Operación exitosa");
}
catch (Exception ex)
{
// Manejo de errores
_logger.LogError(ex, "Error en MiAccion");
return StatusCode(500, "Error interno del servidor");
}
}
Respuestas Asíncronas
Diseñar la API para admitir operaciones asíncronas donde sea posible. Esto permite que las operaciones que pueden llevar más tiempo se manejen eficientemente sin bloquear el hilo principal.
Escenario idóneo:
Cuando se realizan operaciones que pueden llevar un tiempo considerable y no se desea bloquear el hilo principal. Ejemplos incluyen operaciones de larga duración como procesamiento de archivos, envío de correos electrónicos, etc.
Ejemplo csharp (c#):
public async Task<IActionResult> OperacionAsincrona()
{
// Lógica asíncrona
var resultado = await _servicio.OperacionAsincrona();
return Ok(resultado);
}
Respuestas Degradadas
Ofrecer funcionalidades degradadas o versiones simplificadas de la API en caso de problemas, permitiendo que ciertas operaciones continúen aunque con un conjunto limitado de características.
Escenario idóneo:
En situaciones de problemas temporales o parciales en los que es preferible ofrecer una funcionalidad limitada en lugar de ninguna. Esto permite que algunas partes de la aplicación sigan siendo utilizables, incluso en condiciones no ideales.
Ejemplo csharp (c#):
public IActionResult FuncionalidadDegradada()
{
try
{
// Lógica de la acción con funcionalidad degradada
return Ok("Operación con funcionalidad degradada");
}
catch (Exception ex)
{
// Manejo de errores
_logger.LogWarning(ex, "Error en FuncionalidadDegradada");
return Ok("Operación degradada: funcionalidad limitada");
}
}
Si algo malo puede pasar, va a pasar… La ley de Murphy Enfrentemos los desafíos con estrategias sólidas. En algunas ocasiones, todo lo que no debería pasar, sucede de golpe y en un solo momento. Nunca esta de más estar bien preparados.
Circuit Breaker
Implementar el patrón de circuit breaker para evitar la propagación de errores y reducir la carga en el sistema cuando se experimentan fallas recurrentes.
El circuit breaker puede abrirse cuando se detectan errores frecuentes, evitando así que las solicitudes lleguen al servidor hasta que la situación mejore.
Escenario idóneo:
Cuando se experimentan fallas recurrentes en un servicio o componente. El circuit breaker ayuda a prevenir la propagación de errores, reducir la carga en el sistema y evitar la saturación del servidor.
Ejemplo csharp (c#):
var circuitBreakerPolicy = Policy
.Handle<Exception>()
.CircuitBreaker(2, TimeSpan.FromMinutes(1));
public IActionResult AccionConCircuitBreaker()
{
circuitBreakerPolicy.Execute(() =>
{
// Lógica de la acción
return Ok("Operación exitosa");
});
return StatusCode(500, "Circuit Breaker abierto");
}
Bulkhead
El patrón de Bulkhead se refiere a la práctica de dividir el sistema en secciones aisladas llamadas “compartimentos”, de modo que si un compartimento falla, otros compartimentos pueden seguir funcionando sin ser afectados.
En una API, cada función o módulo podría ser un compartimento independiente.
Escenario idóneo:
En sistemas complejos donde la falla de un componente no debe afectar a otros. Es especialmente útil cuando una parte de la aplicación depende de servicios externos o módulos que pueden tener problemas ocasionales.
Ejemplo csharp (c#):
var bulkheadPolicy = Policy
.Bulkhead(5, 10);
public IActionResult AccionConBulkhead()
{
bulkheadPolicy.Execute(() =>
{
// Lógica de la acción en compartimento independiente
return Ok("Operación en compartimento");
});
return Ok("Operación completa");
}
Rate Limiter
El Rate Limiter impone límites en la tasa de solicitudes que un cliente puede realizar a una API en un período de tiempo específico para prevenir la sobrecarga del servidor y proteger contra ataques de denegación de servicio (DoS). Limitar el número de solicitudes por minuto que un único cliente o dirección IP puede realizar es un ejemplo práctico en una API.
Escenario idóneo:
Es útil en situaciones donde se desea limitar la carga en el servidor y asegurar un uso equitativo de los recursos.
Ejemplo csharp (c#):
var rateLimitPolicy = Policy
.RateLimit(5, TimeSpan.FromSeconds(10));
public IActionResult AccionConRateLimiter()
{
rateLimitPolicy.Execute(() =>
{
// Lógica de la acción
return Ok("Operación exitosa");
});
return StatusCode(429, "Límite de tasa excedido");
}
Retry
Retry (Reintento) es la práctica de volver a intentar una operación que ha fallado temporalmente.
Si una solicitud a la API falla debido a un error temporal, la aplicación puede intentar la misma solicitud nuevamente después de un breve período de tiempo, incluyendo estrategias de reintento exponenciales.
Escenario idóneo:
En operaciones que pueden fallar temporalmente debido a condiciones transitorias, como errores de red o tiempos de espera excedidos. Es útil para mitigar problemas temporales y permitir que la operación tenga éxito en intentos posteriores.
Ejemplo csharp (c#):
var retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
public IActionResult AccionConRetry()
{
retryPolicy.Execute(() =>
{
// Lógica de la acción con reintento
// Puede ser una operación que podría fallar temporalmente
if (new Random().Next(0, 2) == 1)
{
throw new Exception("Error temporal");
}
return Ok("Operación exitosa");
});
return StatusCode(500, "Todos los intentos de reintentos fallaron");
}
Time Limiter
Time Limiter implica establecer límites de tiempo para las operaciones. Si una operación no se completa dentro de un tiempo especificado, se cancela para evitar que una solicitud consuma recursos de manera indefinida. Por ejemplo, si una solicitud a la API no se completa dentro del límite de tiempo establecido, la API puede cancelar la operación y devolver una respuesta indicando el tiempo de espera excedido.
Escenario idóneo:
Para evitar que una solicitud consuma recursos de manera indefinida. Útil en situaciones donde es crucial establecer límites de tiempo para operaciones, evitando bloqueos prolongados y mejorando la capacidad de respuesta.
Ejemplo csharp (c#):
var timeLimitPolicy = Policy
.Timeout(TimeSpan.FromSeconds(5), TimeoutStrategy.Pessimistic);
public IActionResult AccionConTimeLimiter()
{
timeLimitPolicy.Execute(() =>
{
// Lógica de la acción con límite de tiempo
// Si la operación no se completa en 5 segundos, se cancela
Thread.Sleep(6000); // Simulando una operación que excede el límite de tiempo
return Ok("Operación exitosa");
});
return StatusCode(500, "Tiempo límite excedido");
}
Cache
La práctica de utilizar una memoria caché implica almacenar temporalmente los resultados de operaciones costosas en tiempo o recursos. Si una API realiza consultas de base de datos costosas, los resultados pueden almacenarse en caché durante un tiempo determinado. Las solicitudes subsiguientes pueden recuperar los resultados desde la caché en lugar de ejecutar la consulta nuevament
Escenario idóneo:
En operaciones costosas en tiempo o recursos que generan resultados estáticos o cambian lentamente. Utilizar una memoria caché reduce la necesidad de repetir la misma operación, mejorando la eficiencia y la velocidad de respuesta.
Ejemplo csharp (c#):
// En este ejemplo, asumimos que estás usando alguna biblioteca de caché como MemoryCache
private readonly IMemoryCache _cache;
public IActionResult AccionConCache()
{
// Verificar si los resultados están en la caché
if (!_cache.TryGetValue("ResultadoCacheado", out string resultado))
{
// Si no están en caché, realizar la operación y almacenar en caché por 10 minutos
resultado = RealizarOperacionCostosa();
_cache.Set("ResultadoCacheado", resultado, TimeSpan.FromMinutes(10));
}
return Ok(resultado);
}
private string RealizarOperacionCostosa()
{
// Lógica de la operación costosa
return "Operación costosa exitosa";
}
Si has llegado hasta este punto, felicitaciones has adquirido conocimientos sobre diversas estrategias que te ayudarán a diseñar APIs más robustas y preparadas para enfrentar cualquier escenario. Te animo a poner en práctica estas estrategias, implementándolas en el código de tu preferencia. La clave está en seguir aprendiendo constantemente. Nunca dejes de aprender.