Redis, al ser una base de datos en memoria de alto rendimiento, debe su velocidad a la manipulación directa de datos en la RAM. Comprender su modelo de memoria es fundamental para optimizar su uso, lo que permite una mejor estimación del consumo, la optimización del espacio y la resolución de problemas relacionados con el rendimiento.
Monitoreo de la Utilización de Memoria en Redis
Para obtener información sobre el consumo de memoria de una instancia de Redis, se puede utilizar el comando INFO MEMORY desde la interfaz de línea de comandos de Redis:
info memory
Este comando proporciona diversas métricas relevantes:
used_memory: Indica la cantidad total de bytes asignados por el asignador de memoria de Redis. Esto incluye la memoria utilizada por los datos, los búferes internos y otros componentes.used_memory_rss: Representa la memoria residente (Resident Set Size), es decir, la memoria física que el sistema operativo ha asignado al proceso de Redis. Incluye la memoria de fragmentación y la necesaria para la ejecución del proceso, pero excluye la memoria virtual.
La relación entre used_memory_rss y used_memory es crucial. Si used_memory_rss es significativamente mayor que used_memory, puede indicar una fragmentación de memoria considerable. Por el contrario, si used_memory_rss es menor que used_memory, sugiere que Redis está utilizando memoria de intercambio (swap), lo que impacta negativamente el rendimiento.
mem_fragmentation_ratio: Este valor, calculado comoused_memory_rss / used_memory, es un indicador de la fragmentación de memoria. Un ratio superior a 1.03 (parajemalloc) suele considerarse saludable. Valores mucho más altos indican una fragmentación excesiva. Un ratio inferior a 1.0 implica que Redis está recurriendo a la memoria de intercambio, una situación que debe abordarse de inmediato.mem_allocator: Muestra el asignador de memoria utilizado por Redis, típicamentejemalloc,tcmallocolibc.jemalloces el predeterminado y es conocido por su eficiencia en la gestión de la fragmentación.
Distribución de la Memoria en Redis
La memoria utilizada por Redis se puede dividir en varias categorías principales:
- Datos de Usuario: Es la porción más grande y crucial, que almacena las claves y valores. Redis encapsula estos datos en estructuras como
redisObjectySDS, que se detallarán más adelante. - Memoria para el Proceso de Redis: Incluye el código del servidor, la pila, el montón y otras estructuras internas esenciales para la operación de Redis. Esta porción es relativamente pequeña (unos pocos megabytes) y no es gestionada por el asignador principal.
- Memoria de Búfer: Comprende búferes de cliente (para entradas y salidas), búferes de replicación (para la pila de retroceso de réplicas) y búferes AOF (para reescrituras AOF). Estos búferes son gestionados por el asignador de memoria principal y contribuyen a
used_memory. - Fragmentación de Memoria: Se refiere al espacio no utilizado debido a la forma en que el asignador de memoria distribuye y libera bloques. Una fragmentación alta puede deberse a patrones de escritura/borrado que dejan huecos pequeños o no contiguos. Reiniciar Redis puede ayudar a reducir la fragmentación, ya que reorganiza los datos en la memoria.
Detalles de Almacenamiento de Datos en Redis
Redis no almacena los datos directamente en la memoria. Utiliza una serie de abstracciones y estructuras internas para gestionar las claves y valores de manera eficiente.
Cuando se ejecuta un comando como SET mykey myvalue, las estructuras involucradas son:
dictEntry: Redis es una base de datos de clave-valor. Cada par clave-valor se representa mediante undictEntry, que contiene punteros a la clave y al valor.- Clave (Key): La clave ("mykey") no se guarda como una cadena de C simple, sino dentro de una estructura SDS (Simple Dynamic String).
- Valor (Value): El valor ("myvalue") se encapsula en un
redisObject, el cual a su vez puede contener un puntero a una estructura SDS (para cadenas), u otras estructuras para tipos de datos complejos. jemalloc: Todas estas estructuras (dictEntry,redisObject,SDS) requieren memoria, la cual es asignada por el asignador de memoria configurado (por defecto,jemalloc).
Asignador de Memoria: jemalloc
jemalloc es el asignador de memoria predeterminado en Redis debido a su buen rendimiento en la reducción de la fragmentación. Divide el espacio de memoria en rangos (pequeño, grande, enorme) y asigna bloques de tamaño predefinido que son los más adecuados para el tamaño solicitado. Por ejemplo, para un objeto de 130 bytes, jemalloc podría asignar un bloque de 160 bytes, dejando un pequeño margen.
La Estructura redisObject
Todos los valores en Redis se almacenan dentro de una estructura redisObject. Esta estructura es crucial porque gestiona la tipificación, codificación, recolección de memoria y la compartición de objetos. Su definición simplificada es:
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj;
Los campos importantes son:
type: Un campo de 4 bits que especifica el tipo de dato del objeto (cadena, lista, hash, conjunto, conjunto ordenado).encoding: También de 4 bits, indica la codificación interna del objeto. Cada tipo de dato puede tener varias codificaciones, permitiendo a Redis optimizar el uso de memoria o el rendimiento según el contexto. Por ejemplo, una lista puede almacenarse como unaziplisto unalinkedlist.lru: Registra la última vez que el objeto fue accedido, utilizado para el algoritmo LRU (Least Recently Used) de desalojo de memoria cuando se alcanza el límitemaxmemory.refcount: Un contador de referencias que indica cuántas veces el objeto está siendo referenciado. Se utiliza para la gestión de memoria y para implementar objetos compartidos. Cuandorefcountllega a cero, el objeto puede ser liberado.ptr: Un puntero que apunta a la estructura de datos real que contiene el valor (por ejemplo, una SDS para una cadena, o un puntero a una lista para una lista).
Redis utiliza la capacidad de objetos compartidos para valores enteros pequeños (típicamente entre 0 y 9999). Estos objetos se crean una vez al inicio del servidor y se reutilizan, lo que ahorra memoria y CPU.
Cadenas Dinámicas Simples (SDS)
Redis no usa las cadenas de C tradicionales, sino su propia implementación llamada SDS (Simple Dynamic String). Un SDS se define como:
struct sdshdr {
int len;
int free;
char buf[];
};
len: Longitud de la cadena actualmente en uso.free: Cantidad de espacio libre disponible en el búfer.buf[]: El arreglo de bytes que almacena la cadena, finalizado con un carácter nulo (\0).
Las ventajas de SDS sobre las cadenas de C incluyen:
- Determinación de longitud en tiempo O(1).
- Prevención de desbordamientos de búfer gracias a la gestión de memoria automática.
- Optimización de reasignaciones de memoria (pre-asignación de espacio y liberación perezosa).
- Capacidad para almacenar datos binarios, ya que
lendefine la longitud y no el carácter nulo.
Tanto las claves como los valores de tipo cadena en Redis se almacenan como SDS. Los SDS también se utilizan para varios búferes internos.
Tipos de Objeto y Codificaciones Internas de Redis
Redis ofrece cinco tipos de datos principales, cada uno con una o más codificaciones internas para optimizar el rendimiento o el consumo de memoria. La conversión entre codificaciones suele ocurrir durante la escritura de datos y es unidireccional, generalmente de una codificaicón más eficiente en memoria a una más flexible o de alto rendimiento.
1. Cadenas (Strings)
Las cadenas son el tipo de dato fundamental. Pueden almacenar hasta 512 MB.
int: Para cadenas que representan un número entero de 8 bytes (long).embstr: Para cadenas cortas (generalmente hasta 39 bytes en Redis 3.0). Optimizadas para una sola asignación de memoria que incluyeredisObjectySDS. Son inmutables.raw: Para cadenas más largas. Requieren dos asignaciones de memoria separadas (una pararedisObjecty otra paraSDS). Son mutables.
Si una cadena int deja de ser un entero o excede el rango de long, se convierte a raw. Una cadena embstr siempre se convierte a raw si se modifica.
2. Listas (Lists)
Las listas son colecciones ordenadas de cadenas, con soporte para operaciones de inserción y extracción en ambos extremos.
ziplist(Lista Comprimida): Una implementación de lista eficiente en memoria para listas pequeñas. Almacena los elementos de forma contigua. Se utiliza si la lista tiene menos de 512 elementos y todos los elementos son cadenas de menos de 64 bytes.linkedlist(Lista Enlazada Doble): Una lista enlazada tradicional para listas más grandes. Cada nodo apunta a unredisObjectde tipo cadena.
Una ziplist se convierte a linkedlist si se supera alguna de las condiciones (número de elementos o tamaño de la cadena). La conversión es unidireccional.
3. Hashes
Los hashes almacenan colecciones de pares campo-valor, donde tanto el campo como el valor son cadenas.
ziplist: Se utiliza para hashes pequeños, similar a las listas. Las condiciones son que el hash tenga menos de 512 pares campo-valor y que tanto los campos como los valores sean cadenas de menos de 64 bytes.hashtable: Un hash table tradicional para hashes más grandes, utilizando una estructura de diccionario interna de Redis.
La estructura hashtable de Redis es compleja, utilizando dos tablas hash para permitir un rehash gradual en segundo plano. Cada entrada del hash (dictEntry) contiene un puntero a la clave y al valor, y un puntero para manejar colisiones. La tabla de punteros (bucket) tiene un tamaño que es la potencia de 2 más cercana y mayor que el número de elementos. Una dictht contiene los detalles de la tabla hash, y un dict gestiona el proceso de rehash utilizando dos dictht (ht[0] y ht[1]).
Una ziplist de hash se convierte a hashtable si se supera alguna de las condiciones, y esta conversión es unidireccional.
4. Conjuntos (Sets)
Los conjuntos son colecciones desordenadas de cadenas únicas.
intset(Conjunto de Enteros): Una implementación compacta para conjuntos que contienen solo números enteros. Se utiliza si todos los elementos son enteros y el conjunto tiene menos de 512 elementos.hashtable: Un hash table tradicional donde los valores de las entradas se establecen anull, ya que solo importan las claves.
Un intset se convierte a hashtable si no se cumplen las condiciones de elementos enteros o el límite de 512 elementos. La conversión es unidireccional.
5. Conjuntos Ordenados (Sorted Sets)
Los conjuntos ordenados son colecciones de miembros únicos, cada uno asociado a una puntuación (score) que determina su orden.
ziplist: Se utiliza para conjuntos ordenados pequeños. Las condiciones son que el conjunto tenga menos de 128 elementos y todos los miembros (cadenas) tengan menos de 64 bytes.skiplist(Lista de Saltos): Una estructura de datos probabilística que permite búsquedas, inserciones y eliminaciones en un promedio de tiempo O(log N). Es más simple de implementar que un árbol balanceado y ofrece un rendimiento comparable.
Una ziplist de conjunto ordenado se convierte a skiplist si se superan las condiciones de tamaño o número de elementos. Esta conversión es unidireccional.
Aplicaciones Prácticas del Modelo de Memoria
1. Estimación del Consumo de Memoria
Comprender el modelo de memoria permite estimar el uso de RAM. Por ejemplo, al almacenar 90,000 pares clave-valor donde cada clave y valor son cadenas de 7 bytes (no enteros), ambos utiliazrán la codificación embstr.
Cada dictEntry (en un sistema de 64 bits) requiere 24 bytes para sus punteros, que jemalloc asigna en un bloque de 32 bytes.
- La clave de 7 bytes requiere un SDS de 7 (longitud) + 9 (metadatos SDS) = 16 bytes.
jemallocasigna 16 bytes. - El valor de 7 bytes requiere un SDS de 7 (longitud) + 9 (metadatos SDS) = 16 bytes.
jemallocasigna 16 bytes. - El
redisObjectpara el valor ocupa 16 bytes.jemallocasigna 16 bytes.
Así, cada par clave-valor consume aproximadamente 32 (dictEntry) + 16 (SDS clave) + 16 (redisObject) + 16 (SDS valor) = 80 bytes. Además, el array de bucket para 90,000 entradas necesita el siguiente tamaño potencia de 2, que es 131,072 punteros. Cada puntero es de 8 bytes en 64 bits. Total: 90,000 * 80 bytes + 131,072 * 8 bytes = 7,200,000 + 1,048,576 = 8,248,576 bytes.
Podemos verificar esto con un ejemplo en Java:
import redis.clients.jedis.Jedis;
public class MemoryUsageEstimator {
private static Jedis redisClient = new Jedis("localhost", 6379);
public static void main(String[] args) throws Exception {
long initialMemory = getUsedMemoryBytes();
// Insertar 90,000 pares clave-valor (de 10000 a 99999)
// Claves como "key_10000", valores como "val_10000"
// Cada clave y valor tiene 7 caracteres (ej. "key_100" o "val_100")
for (int i = 10000; i < 100000; i++) {
String keyPrefix = "key_"; // 4 caracteres
String valPrefix = "val_"; // 4 caracteres
String currentKey = keyPrefix + i; // 4 + 5 = 9 chars, for `i` from 10000.
// Let's adjust for exactly 7 chars
// e.g., "aaaa" + i for 3 digits i (100-999)
// Original example used "aa" + i where i starts from 10000
// so key and value length is 7 chars.
// Let's maintain that example.
String formattedNum = String.format("%05d", i); // Ensure 5 digits for the number part
redisClient.set("aa" + formattedNum, "aa" + formattedNum);
}
long finalMemory = getUsedMemoryBytes();
System.out.println("Memoria consumida por los datos: " + (finalMemory - initialMemory) + " bytes");
// Limpiar para pruebas repetidas si es necesario
// redisClient.flushDB();
redisClient.close();
}
private static long getUsedMemoryBytes() {
String memoryInfo = redisClient.info("memory");
for (String line : memoryInfo.split("\r\n")) {
if (line.startsWith("used_memory:")) {
return Long.parseLong(line.substring(line.indexOf(':') + 1));
}
}
return 0;
}
}
El resultado de la ejecución con "aa" + 5 dígitos (ej. "aa10000", "aa10001", ... "aa99999") generaría claves y valores de 7 caracteres, y el resultado sería cercano a los 8,24 MB estimados, validando el cálculo.
Si la longitud de clave/valor cambiara a 8 bytes, el SDS se convertiría a 17 bytes, y jemalloc asignaría un bloque de 32 bytes en lugar de 16, lo que duplicaría el espacio para la clave/valor individual.
2. Optimización del Uso de Memoria
- Aprovechar la asignación de
jemalloc: Al diseñar claves o valores, se puede intentar ajustar sus longitudes para que caigan justo por debajo de los límites de los bloques de asignación dejemalloc, minimizando el espacio desperdiciado. - Usar Enteros: Si un valor puede representarse como un número entero, Redis lo almacenará usando el tipo
int(8 bytes) en lugar de un SDS, lo que resulta en un ahorro significativo. - Objetos Compartidos: Para valores enteros de 0 a 9999, Redis utiliza objetos compartidos. Aumentar el rango de
REDIS_SHARED_INTEGERS(oOBJ_SHARED_INTEGERSen versiones recientes) puede ser útil si se tienen muchos valores enteros repetidos dentro de un rango más amplio. - Evitar la sobre-optimización: Las optimizaciones de memoria son más críticas para grandes volúmenes de datos. Para datasets pequeños, la complejidad adicional en el diseño de las claves puede no justificar el ahorro marginal de memoria.
3. Monitoreo de la Fragmentación de Memoria
El ratio de fragmentación (mem_fragmentation_ratio) es un indicador vital. Si es alto (por ejemplo, significativamente mayor a 1.03 para jemalloc), indica un desperdicio de memoria. Un reinicio seguro de Redis puede compactar los datos y liberar memoria. Si es menor que 1.0, sugiere que Redis está utilizando memoria de intercambio, lo que degradará drásticamente el rendimiento. En este caso, se necesita aumentar la memoria RAM disponible, añadir más nodos Redis, o reducir la cantidad de datos almacenados mediante una política de desalojo de memoria (maxmemory-policy) adecuada.