Fenómeno de Excepción
En un clúster de Redis con despliegue de alta disponibilidad (n maestros y n esclavos), cuando un nodo maestro falla y un esclavo se promociona a maestro, la conmutación por error del servidor de Redis se activa. Sin embargo, las aplicaciones dependientes aún envían algunas solicitudes al nodo fallido, generando excepciones de tiempo de espera que persistne. El error típico se manifiesta como:
org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70)
... (traza de pila completa omitida por brevedad)
Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
at io.lettuce.core.ExceptionFactory.createTimeoutException(ExceptionFactory.java:51)
...
Análisis de Causa
La observación revela que solo algunas aplicaciones reportan errores, y la diferencia clave radica en el cliente de Redis utilizado. Las aplicaciones con Jedis se recuperan rápidamente, pero aquellas con Lettuce mantienen los tiempos de espera. Jedis sincroniza automáticamente la información del clúster entre cliente y servidor al detectar fallos, permitiendo una recuperación rápida. En contraste, Lettuce no refresca la topología del clúster de forma predeterminada tras una conmutación por error.
En proyectos con Spring Boot que usan autoconfiguración, el objeto LettuceConnectionFactory generado por defecto no habilita el refresco de topología. Versiones de Spring Boot iguales o superiores a 2.3.0 ofrecen soporte para activar el refresco automático mediante configuración de propiedades. Pruebas de validación confirman que habilitar el refresco resuelve el problema en escenarios como conmutaciones manuales o fallos de nodos.
Solución Propuesta
Enfoque Fundamental
La solución implica activar la funcionalidad de refresco de topología del clúster en el cliente de Redis. Cada cliente tiene sus particularidades:
- El cliente Jedis soporta esto por defecto, ya que utiliza mecanismos de retroalimentación para detectar fallos y actualizar la información del clúster, garantizando recuperación automática.
- El cliente Lettuce no habilita el refresco por defecto y requiere configuración explícita.
Detalles de Implementación
Independientemente del enfoque, se recomienda establecer el máximo de redirecciones igual al número de nodos del clúster. Además, configurar los nodos del clúster con todas las direcciones disponibles para evitar problemas de conexión en caso de fallos.
Proyectos Spring estándar
Para aplicaciones Spring tradicionales, persoanlizar la configuración del cliente Lettuce mediante un bean de configuración. A continuación, un ejemplo modificado que altera nombres de variables y estructura manteniendo la funcionalidad:
@Bean
public RedisConnectionFactory crearConexiónPersonalizada() {
OpcionesDeRefrescoTopológica opcionesRefresco = OpcionesDeRefrescoTopológica.builder()
.habilitarRefrescoPeriódico(Duration.ofMillis(25000))
.activarTodosDesencadenantesAdaptativos()
.build();
OpcionesClienteClúster opcionesClúster = OpcionesClienteClúster.builder()
.validarMembresíaNodo(false)
.opcionesDeRefresco(opcionesRefresco)
.build();
ConfiguraciónClienteLettuce configCliente = ConfiguraciónClienteLettuce.builder()
.tiempoDeEsperaPropiedades(redisPropiedades.getTiempoEspera())
.tiempoApagado(Duration.ZERO)
.opcionesCliente(opcionesClúster)
.construir();
ConfiguraciónClústerRedis configServidor = new ConfiguraciónClústerRedis(redisPropiedades.getClúster().getNodos());
return new FábricaConexiónLettuce(configServidor, configCliente);
}
Proyectos Spring Boot con versiones anteriores a 2.3.0
En Spring Boot 2.x (menor a 2.3.0), el cliente predeterminado es Lettuce sin refresco de topología. Para habilitarlo sin actualizar el framwork, se puede implementar un personalizador de configuración. Aquí un ejemplo reestructurado:
@Configuración
public class ConfigPersonalizadorLettuce {
@Valor("${propiedades.redis.lettuce.refresco.adaptativo:false}")
private boolean refrescoAdaptativo;
@Valor("${propiedades.redis.lettuce.refresco.periodo:}")
private Duración periodoRefresco;
@Bean
@CondicionalEnAusenciaDe(PersonalizadorConfiguraciónClienteLettuce.class)
public PersonalizadorConfiguraciónClienteLettuce crearPersonalizador(PropiedadesRedis propiedades) {
return new PersonalizadorLettuce(propiedades, refrescoAdaptativo, periodoRefresco);
}
static class PersonalizadorLettuce implements PersonalizadorConfiguraciónClienteLettuce {
private final PropiedadesRedis propiedades;
private final boolean refrescoAdaptativo;
private final Duración periodoRefresco;
PersonalizadorLettuce(PropiedadesRedis propiedades, boolean refrescoAdaptativo, Duración periodoRefresco) {
this.propiedades = propiedades;
this.refrescoAdaptativo = refrescoAdaptativo;
this.periodoRefresco = periodoRefresco;
}
@Override
public void personalizar(GeneradorConfiguraciónClienteLettuce generador) {
ConstructorOpcionesCliente constructor = ConstructorOpcionesCliente.builder();
if (this.propiedades.getClúster() != null) {
ConstructorOpcionesRefrescoTopológica constructorRefresco = ConstructorOpcionesRefrescoTopológica.builder();
if (periodoRefresco != null) {
constructorRefresco.habilitarRefrescoPeriódico(periodoRefresco);
}
if (refrescoAdaptativo) {
constructorRefresco.activarTodosDesencadenantesAdaptativos();
}
constructor = ConstructorOpcionesClienteClúster.builder().opcionesDeRefresco(constructorRefresco.construir());
}
generador.opcionesCliente(constructor.opcionesTiempoEspera(TiempoEsperaHabilitado.crear()).construir());
}
}
}
La configuración en propiedades YAML sería:
propiedades:
redis:
lettuce:
clúster:
refresco:
adaptativo: true
periodo: 25000
Proyectos Spring Boot 2.3.0 o superiores
Para versiones de Spring Boot iguales o superiores a 2.3.0, basta con agregar las propiedades de configuración correspondientes. Ejemplo en formato YAML:
propiedades:
redis:
lettuce:
clúster:
refresco:
adaptativo: true
periodo: 25000
Estas configuraciones pueden ajustarse según el entorno de despliegue, asegurando que el cliente de Redis mantenga una visión actualizada de la topología del clúster para una conmutación por error fluida.