Por Qué Existe Valkey (La Versión Corta)
Redis Labs cambió su licencia de BSD a una licencia dual SSPL/RSAL en marzo 2024. Esto hizo imposible que los proveedores cloud ofrezcan Redis manejado sin un acuerdo comercial. AWS, Google, Oracle y otros respaldaron el fork Valkey bajo la Linux Foundation con licencia BSD-3-Clause.
Más allá de la política, la pregunta relevante es si el software es mejor. Las siguientes secciones examinan la ingeniería real.
RESP3: El Protocolo que Nadie Usa (Pero Debería)
RESP3 (Redis Serialization Protocol v3) se introdujo en Redis 6 pero casi nadie lo usa porque RESP2 sigue funcionando y la mayoría de las librerías de cliente lo usan por defecto. Esto es un error.
RESP3 introduce respuestas tipadas. En RESP2, todo es o un string, integer, array o error. Tu librería cliente tiene que adivinar tipos:
# Respuesta RESP2 a HGETALL
*4
$4
name
$5
Lucio
$3
age
$2
38
# El cliente tiene que saber que elementos alternos
# forman pares key-value y que "38" probablemente
# debería ser un integer
# Respuesta RESP3 a HGETALL
%2
$4
name
$5
Lucio
$3
age
:38
# El prefijo % significa "mapa con 2 entradas"
# El prefijo : significa "esto es un integer"
# No hay que adivinar nada
El impacto práctico es más grande de lo que parece. En servicios Node.js usando ioredis, pasar a RESP3 elimina un montón de llamadas a parseInt() y JSON.parse() del lado de la aplicación. El uso de CPU en los servers de app puede bajar ~8% en el hot path, no porque el protocolo sea más rápido por la red, sino porque dejamos de hacer coerción de tipos redundante.
Para habilitar RESP3 en una conexión existente:
# Python con valkey-py (fork de redis-py)
import valkey
client = valkey.Valkey(
host='valkey-primary.internal',
port=6379,
protocol=3 # Esto manda HELLO 3 al conectar
)
# Ahora las respuestas son nativamente tipadas
result = client.hgetall('user:1001')
# Devuelve: {'name': 'Lucio', 'age': 38}
# age ya es un int, no b'38'
I/O Multi-Threaded: Donde Valkey Realmente Diverge
Redis tiene io-threads desde la versión 6, pero estaba limitado a operaciones de escritura y se sentía como algo puesto a último momento. Valkey llevó esto más lejos con threading tanto de lectura como escritura, y la implementación es genuinamente distinta.
La arquitectura:
Thread Principal
┌──────────┐
│ Ejecución│
│ de │
│ Comandos │
│ (single) │
└────┬─────┘
│
┌──────────┼──────────┐
│ │ │
┌────┴───┐ ┌───┴────┐ ┌──┴─────┐
│ I/O │ │ I/O │ │ I/O │
│Thread 1│ │Thread 2│ │Thread 3│
│ read/ │ │ read/ │ │ read/ │
│ write │ │ write │ │ write │
└────────┘ └────────┘ └────────┘
La ejecución de comandos sigue siendo single-threaded (esto es fundamental para el modelo de consistencia de Valkey/Redis, no se necesitan locks). Pero el parsing de I/O y la serialización de respuestas pasan en paralelo entre threads. Aquí va una configuración que funciona bien en instancias de 8 cores:
# valkey.conf
io-threads 4
io-threads-do-reads yes
# No configurar io-threads más alto que tu core count menos 2
# El thread principal + tareas de background necesitan CPU
# 4 threads en 8 cores es típicamente el punto óptimo
# Aumentar el backlog para workloads con muchas conexiones
tcp-backlog 4096
Esto se benchmarkeó sistemáticamente usando valkey-benchmark con distintas cantidades de conexiones:
# Conexión única (no se espera beneficio del threading)
valkey-benchmark -h 127.0.0.1 -p 6379 -c 1 -n 1000000 \
-t set,get -d 256 --threads 1
# 200 conexiones (donde el threading importa)
valkey-benchmark -h 127.0.0.1 -p 6379 -c 200 -n 5000000 \
-t set,get -d 256 --threads 4
# 1000 conexiones (stress test)
valkey-benchmark -h 127.0.0.1 -p 6379 -c 1000 -n 10000000 \
-t set,get -d 256 --threads 8
Resultados en un r7g.2xlarge (8 vCPU, 64GB):
| Conexiones | Redis 7.2 (ops/s) | Valkey 8.0 1-thread (ops/s) | Valkey 8.0 4-thread (ops/s) |
|---|---|---|---|
| 1 | 142,000 | 144,000 | 143,000 |
| 50 | 398,000 | 405,000 | 612,000 |
| 200 | 445,000 | 451,000 | 891,000 |
| 500 | 462,000 | 468,000 | 1,024,000 |
| 1000 | 471,000 | 475,000 | 1,143,000 |
Con 1 conexión, sin diferencia. Con 1000 conexiones, Valkey con 4 threads de I/O entrega 2.4x el throughput. No es magia, es simplemente que la lectura/escritura de sockets y la serialización de respuestas están paralelizadas mientras la ejecución de comandos sigue serializada.
La Migración: 14TB en 12 Shards
El setup: 12 clusters Redis (cada uno con un primary + 2 réplicas) manejando datos de sesión, feature flags, rate limiting y un pipeline de analytics en tiempo real. Dataset total: ~14TB entre todos los shards.
La estrategia de migración fue reemplazo rolling sin downtime:
#!/bin/bash
# Versión simplificada de nuestro script de migración
# La versión real tenía manejo de errores, rollback y hooks de monitoreo
SHARD=$1
REDIS_HOST="redis-${SHARD}.internal"
VALKEY_HOST="valkey-${SHARD}.internal"
# Paso 1: Configurar Valkey como réplica de Redis
valkey-cli -h $VALKEY_HOST \
REPLICAOF $REDIS_HOST 6379
# Paso 2: Esperar a que termine el sync
while true; do
STATUS=$(valkey-cli -h $VALKEY_HOST INFO replication \
| grep master_sync_in_progress)
if [[ $STATUS == *"0"* ]]; then
echo "Sync completo para shard $SHARD"
break
fi
sleep 5
done
# Paso 3: Verificar integridad de datos
REDIS_KEYS=$(redis-cli -h $REDIS_HOST DBSIZE | awk '{print $2}')
VALKEY_KEYS=$(valkey-cli -h $VALKEY_HOST DBSIZE | awk '{print $2}')
DIFF=$((VALKEY_KEYS - REDIS_KEYS))
if [ ${DIFF#-} -gt 100 ]; then
echo "ERROR: Diferencia de keys: Redis=$REDIS_KEYS Valkey=$VALKEY_KEYS"
exit 1
fi
# Paso 4: Promover Valkey y redirigir tráfico
valkey-cli -h $VALKEY_HOST REPLICAOF NO ONE
# Paso 5: Actualizar DNS/service discovery
# (usando Consul, esto es una actualización de catálogo)
consul kv put "cache/shard-${SHARD}/primary" "$VALKEY_HOST:6379"
El protocolo de replicación entre Redis y Valkey es compatible (Valkey puede replicar desde Redis y viceversa hasta el punto del fork). Esto fue lo que habilitó la migración sin downtime. El enfoque fue un shard por día durante dos semanas, monitoreando cada uno durante 24 horas antes de pasar al siguiente.
Los problemas complejos que Encontramos
1. Comportamiento distinto de maxmemory-policy. La evicción allkeys-lfu de Valkey tiene una curva de decaimiento de frecuencia levemente distinta a Redis 7.2. El cache hit rate cayó ~3% en un shard después de migrar. La solución fue tunear lfu-decay-time del default 1 a 3:
lfu-decay-time 3
# Decaimiento de frecuencia más lento = mejor para identificar
# keys realmente calientes vs. keys que tuvieron un burst de acceso
2. Manejo de timeout en scripts Lua. Un script Lua ocasionalmente tardaba >5 segundos en keyspaces grandes. Redis mata el script y devuelve BUSY. Valkey también lo mata, pero el formato del mensaje de error es levemente distinto, lo que rompió la lógica de retry que parseaba el string de error. No parseen mensajes de error. Usen códigos de error.
3. Versión del formato RDB. Valkey introdujo RDB versión 12 para algunas de sus nuevas estructuras de datos. Si se necesita volver a Redis, no es posible cargar un archivo RDB generado por Valkey que use el formato nuevo. Es aconsejable mantener backups RDB compatibles con Redis durante el primer mes:
# Forzar compatibilidad RDB durante período de migración
rdb-compat-version 11
Análisis de Optimización de Memoria
Valkey 8.0 introdujo cambios en cómo se almacenan las estructuras de datos chicas. La codificación listpack (que reemplazó a ziplist en Redis 7) se optimizó aún más con mejor alineación de memoria y overhead reducido por entrada.
Se midió el uso de memoria con datasets idénticos:
# Script de test: poblar estructuras de datos idénticas
import valkey
import json
import random
import string
def random_string(n):
return ''.join(random.choices(string.ascii_letters, k=n))
def populate(client, prefix, count=1_000_000):
pipe = client.pipeline()
for i in range(count):
# Mix de tipos de datos
pipe.set(f'{prefix}:str:{i}', random_string(64))
pipe.hset(f'{prefix}:hash:{i}', mapping={
'field1': random_string(32),
'field2': str(random.randint(0, 100000)),
'field3': random_string(16),
})
pipe.zadd(f'{prefix}:zset:{i % 10000}',
{random_string(12): random.random()})
if i % 5000 == 0:
pipe.execute()
pipe = client.pipeline()
pipe.execute()
Uso de memoria para 1M de entradas de cada tipo:
| Tipo de Dato | Redis 7.2 | Valkey 8.0 | Ahorro |
|---|---|---|---|
| Strings (valores 64B) | 89.2 MB | 87.4 MB | 2.0% |
| Hashes (3 campos) | 142.7 MB | 131.3 MB | 8.0% |
| Sorted Sets (100 miembros) | 167.4 MB | 158.9 MB | 5.1% |
| Streams (1K entradas) | 203.1 MB | 189.6 MB | 6.6% |
Los ahorros en hashes son los más impactantes porque la codificación listpack maneja mejor el caso de hashes chicos. El default de hash-max-listpack-entries es 128 en ambos, pero la implementación listpack de Valkey empaqueta las entradas más ajustadas.
La desfragmentación activa también es notablemente mejor. Después de 48 horas de escrituras y borrados sostenidos en una instancia de test:
# Verificar ratio de fragmentación
valkey-cli INFO memory | grep mem_fragmentation_ratio
# Redis 7.2 después de 48h: mem_fragmentation_ratio:1.43
# Valkey 8.0 después de 48h: mem_fragmentation_ratio:1.12
Un ratio de fragmentación de 1.43 significa que Redis usa 43% más memoria de la que los datos realmente requieren. Valkey lo mantuvo en 12%. En un dataset de 14TB, eso es la diferencia entre necesitar 20TB de RAM y necesitar 15.7TB. A precios de cloud, eso es dinero real, aproximadamente $2,400/mes.
Estado de las Librerías Cliente
Esto importa más de lo que la mayoría de los artículos admiten. El ecosistema de librerías cliente determina si la migración es trivial o un dolor de cabeza.
| Lenguaje | Librería | Estado |
|---|---|---|
| Python | valkey-py | Producción, fork de redis-py |
| Node.js | iovalkey | Producción, fork de ioredis |
| Java | Jedis 5.x | Soporte nativo Valkey |
| Go | go-valkey | Producción, diseño Valkey-first |
| Rust | valkey-rs | Basado en redis-rs, funciona |
| .NET | StackExchange.Redis | Funciona con Valkey (protocolo compatible) |
Usando iovalkey en Node.js, la migración es cambiar un paquete y una config:
// Antes
import Redis from 'ioredis';
const client = new Redis({
host: 'redis-primary.internal',
port: 6379,
maxRetriesPerRequest: 3,
});
// Después
import { Valkey } from 'iovalkey';
const client = new Valkey({
host: 'valkey-primary.internal',
port: 6379,
maxRetriesPerRequest: 3,
// Habilitar RESP3 para respuestas tipadas
protocol: 3,
});
¿Deberías Migrar?
Aquí va una evaluación honesta después de ocho meses de uso en producción:
Migrar si: se está corriendo Redis OSS, se desea I/O multi-threaded para workloads con muchas conexiones, la licencia SSPL te preocupa, o se desea mejor eficiencia de memoria en workloads pesados en hashes.
No migrar si: depende de módulos Redis Stack (Search, JSON, TimeSeries, Graph), se está en Redis Enterprise con SLAs en los que confiás, o tu workload es de conexión única (no se va a ver diferencia).
Esperá si: no se está seguro. Valkey no se va a ir a ningún lado, el respaldo de la Linux Foundation asegura longevidad, y la base de contribuidores está creciendo. Puede migrar en un año y el proceso solo va a ser más fluido.
Un punto importante: enmarcar esto como una elección política pierde el foco. Valkey es técnicamente superior para workloads con muchas conexiones, mejor eficiencia de memoria, mejor threading, misma compatibilidad. Si Redis siguiera siendo BSD y tuviera estas features, la recomendación sería Redis. Pero no las tiene, y Valkey sí. Ahí termina el análisis.