Migración de Redis a DragonflyDB: lecciones aprendidas tras un colapso de caché

Un sistema de monitoreo de alertas de seguridad sufrió una caída severa a las 3 AM. El nodo maestro de Redis alcanzó su límite de memoria (maxmemory), lo que provocó la expulsión masiva de claves en caché. Como resultado, todas las solicitudes se redirigieron directamente a PostgreSQL, saturando el pool de conexiones y provocando una reacción en cadena que derribó toda la infraestructura.

La recuperación tomó 40 minutos. Tras evaluar alternativas, DragonflyDB surgió como la opción más viable para sustituir Redis.

Por qué DragonflyDB frente a Redis

Redis no era el problema per se — el uso que le dábamos era subóptimo. Sin embargo, DragonflyDB abordó varios puntos críticos:

Aspecto Redis DragonflyDB
Consumo de memoria 1 GB de datos ocupa ~1.5-2 GB reales 1 GB de datos ocupa ~1.1 GB reales
Modelo de hilos Multihilo parcial desde 6.0; comandos principales siguen en un solo hilo Multihilo nativo, aprovecha todos los núcleos automáticamente
Persistencia Las snapshots RDB usan fork; con mucha memoria provoca pausas Sin fork; las snapshots no bloquean
Compatibilidad Protocolo Redis 100% compatible

Proceso de migración

Fase 1: Despliegue de DragonflyDB

Se inició con un contenedro Docker para validación inicial:

# Levantar instancia de DragonflyDB
docker run -d \
  --name dfly-server \
  -p 6390:6390 \
  -v /opt/dfly-data:/data \
  docker.dragonflydb.io/dragonflydb/dragonfly:latest

# Para reemplazar Redis en producción, detener primero el servicio
systemctl stop redis-server

Fase 2: Pruebas de compatibilidad

El stack utiliza Python con redis-py. Al ser DragonflyDB compatible con el protocolo RESP, no se requieren modificaciones en el código cliente. Se ejecutaron pruebas de validación:

import redis

# Conexión a DragonflyDB usando el mismo driver
client = redis.Redis(host='127.0.0.1', port=6390, decode_responses=True)

# Operaciones básicas de strings
client.set('validate:string', 'ok')
assert client.get('validate:string') == 'ok'

# Estructuras hash
client.hset('validate:hash', mapping={'service': 'ids', 'release': '2.0'})
assert client.hgetall('validate:hash')['service'] == 'ids'

# Sorted sets para ranking de alertas
client.zadd('validate:ranking', {'evt:100': 85, 'evt:101': 72, 'evt:102': 91})
top_events = client.zrevrange('validate:ranking', 0, -1, withscores=True)
assert top_events[0][0] == 'evt:102'

# Escritura masiva con pipeline
with client.pipeline(transaction=False) as batch:
    for n in range(500):
        batch.set(f'mass:{n}', f'data-{n}')
    batch.execute()

print(f"Total de claves: {client.dbsize()}")

Todos los tests pasaron sin modificar una sola línea del código de producción.

Fase 3: Migración de datos

Una advertencia crítica: no se puede simplemente copiar el archivo RDB de Redis y cargarlo directamente en DragonflyDB. Aunque DragonflyDB soporta carga de RDB, existen incompatibilidades de formato entre versiones. Con Redis 7.0, algunas estructuras no se importaron correctamente.

La estrategia final fue una migración en línea clave por clave:

# Exportar snapshot desde Redis original
redis-cli -h legacy-redis -p 6379 --rdb /tmp/redis_backup.rdb

# Intentar carga directa en DragonflyDB (puede fallar parcialmente)
# Como respaldo, migración iterativa por clave:
redis-cli -h legacy-redis -p 6379 --scan | while IFS= read -r k; do
  redis-cli -h legacy-redis -p 6379 DUMP "$k" | \
  head -c -1 | \
  redis-cli -h 127.0.0.1 -p 6390 RESTORE "$k" 0
done

El volumen de datos era de aproximadamente 50 GB y la migración completó en unas 2 horas.

Fase 4: Transición de tráfico

# Actualizar variables de entorno para apuntar a DragonflyDB
export CACHE_ENDPOINT=127.0.0.1
export CACHE_PORT=6390

# Despliegue gradual: iniciar con 10% del tráfico y monitorear
# El mecanismo exacto depende de la configuración del API gateway

Problemas encontrados durante la migración

Problema 1: Pico temporal de memoria

Inmediatamente después de importar los datos, el consumo de memoria de DragonflyDB superó al de Redis en un 30%. Esto se debió a procesos de compactación en segundo plano. Tras 10 minutos, la memoria se redujo terminando un 40% por debajo de Redis.

Problema 2: Comportamiento del comando KEYS

En Redis, KEYS * bloquea el hilo principal. DragonflyDB no bloquea, pero el orden de retorno de las claves difiere. Si existe dependencia del ordenamiento de KEYS, la migración provocará fallos. La solución es usar SCAN:

# Usar SCAN en lugar de KEYS para evitar problemas
redis-cli --scan --pattern "evt:*" | head -20

Problema 3: Scripts Lua con diferencias sutiles

DragonflyDB soporta la mayoría de scripts Lua, pero ciertos casos límite presentan diferencias. Por ejemplo, el formato de retorno de redis.call('TIME') varía ligeramente. De tres scripts Lua en producción, uno requirió ajustes menores.

Problema 4: Monitoreo y métricas

El redis-exporter estándar no reconoce el formato de salida INFO de DragonflyDB. Se configuró el exportador nativo de DragonflyDB para Prometheus:

# prometheus.yml
scrape_configs:
  - job_name: 'dragonfly_metrics'
    static_configs:
      - targets: ['dfly-host:6390']
    metrics_path: /metrics

Resultados tras la migración

Métrica Redis (antes) DragonflyDB (después) Variación
Memoria consumida 48 GB 29 GB -40%
Latencia P99 12 ms 3 ms -75%
QPS máximo ~80,000 ~180,000 +125%
Tiempo de snapshot 45 s (con bloqueo) 8 s (sin bloqueo) -82%

Etiquetas: Redis DragonflyDB cache migración PostgreSQL

Publicado el 8-24 15:10