Implementación de cachés distribuidas usando Redis Cluster

13 de Marzo de 2023 · 6 min de lectura

Network

Implementación de cachés distribuidas usando Redis Cluster

Imaginemos un sistema que requiera de acceso a datos de forma concurrente y con tiempos de respuesta muy rápidos. Necesitamos una caché en memoria para que los tiempos de respuesta sean lo más rápidos posibles. Además, dependiendo del tamaño de los objetos que queramos almacenar en esta caché, si son objetos de gran tamaño, tendríamos que distribuir la información en distintos nodos.

Redis permite guardar datos en forma de diccionario (clave – valor), en memoria directamente, lo que hace que los tiempos de acceso a esos datos sean mucho más rápidos comparados, por ejemplo, con los accesos que haríamos a un motor de base de datos relacional.

Redis Cluster ofrece una forma sencilla de escalar datos, en nuestro caso, datos que se encuentren en una memoria caché. En la caché que supuestamente utilizaremos no se encuentran sólo los datos más utilizados, vamos a cachear todos los registros de una estructura de datos, pueden ser miles o millones de registros. Redis Cluster permite agregar o quitar nodos dinámicamente, si es necesario.

Permite la definición de réplicas de los nodos donde se encuentren nuestros datos, por lo que una caída de uno de los nodos, no implica la caída del servicio o de parte de él. Si un nodo cae, se levanta una de las réplicas definidas para ese nodo, por lo que el sistema puede seguir funcionando con normalidad. Esto se traduce en una mayor integridad y disponibilidad de los datos.

Particiona los datos de forma automática y evita la sobrecarga de uno sólo de los nodos. Su algoritmo de partición “slot-based partitioning”, asigna cada clave a un slot específico y este slot se encuentra siempre en el mismo nodo. Si se agrega o quita uno del cluster, los slots vuelven a asignarse al nuevo conjunto para que el sistema siempre esté equilibrado.

En este artículo vamos a definir Redis Cluster para implementar el acceso a la caché mencionada anteriormente.

Utilizando Docker para crear los nodos del Cluster

Para obtener datos de nuestra caché podemos implementar una API. De esta manera mediante un Post a un endpoint, podremos obtener los valores de la claves que contiene la memoria en Redis.

No vamos a describir cómo definir el manager de la caché o la API, ya que podemos implementar esas capas de muchas formas. Nos vamos a centrar en la creación de los nodos Redis como contenedores de Docker, en la creación de la claves, y la recuperación de sus valores en los nodos de Redis.

Utilizaremos un Docker compose file, para especificar cada uno de los servicios que definirán nuestro Redis Cluster. En nuestro ejemplo vamos a definir 3 nodos maestros de Redis y un nodo réplica para cada uno de los nodos maestros. Cuando uno de los nodos maestros cae, la réplica se convertirá en maestro.

Necesitamos definir una red de Docker con ips estáticas, ya que Redis Cluster no funciona con nombres de dominio. Por lo que definiremos una subred y asignaremos ips de esta subred tanto a los nodos de Redis como al servicio de nuestra API.

networks: 
  redis_cluster_net:
    driver: bridge
    ipam:      
      driver: default
      config:
        - subnet: 173.18.0.0/16

Los contenedores de los nodos Redis tienen una configuración asociada que les indicará que son parte de un Cluster, esta configuración se encuentra dentro de un fichero .conf que se copiará a cada uno de los nodos. Podemos definir un fichero .conf para todos los nodos, que se especificará en el apartado “volumes” de cada uno de los servicios del docker compose.

port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
enable-debug-command yes

Esta es la especificación de uno de los nodos de Redis Cluster en el fichero compose.

redis_1:
    image: 'redis:latest'
    container_name: redis_1
    ports:
      - "6379"
    volumes:
      - redis_1_data:/data
      - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
    command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ]
    networks:
      redis_cluster_net:
        ipv4_address: 173.18.0.2

Tenemos que definir 6 servicios en el fichero compose de Docker. Para crear el Cluster hemos de utilizar la instrucción de Redis, “redis-cli –cluster create”. Lo más sencillo será crear un contenedor que sirva sólo para inicializar los nodos, este contenedor servirá únicamente para ese propósito.

Crearemos un fichero ejecutable clustercreate.sh, donde se ejecutará la instrucción de creación del Cluster.

echo "yes" | redis-cli --cluster create \
  173.18.0.2:6379 \
  173.18.0.3:6379 \
  173.18.0.4:6379 \
  173.18.0.5:6379 \
  173.18.0.6:6379 \
  173.18.0.7:6379 \
  --cluster-replicas 1
echo "🚀 Redis cluster ready."

Llamaremos al servicio que monta el Cluster desde un contenedor “mount_cluster”. Este contenedor necesita Redis para poder ejecutar una instrucción “redis-cli”, así que el contenedor usará como base la última imagen de Redis copiando el fichero “clustercreate.sh” dentro del contenedor para luego ejecutarlo.

mount-cluster:
    container_name: mount-cluster
    build:
      context: redis
      dockerfile: Dockerfile
    tty: true
    depends_on:
      - redis_1
      - redis_2
      - redis_3
      - redis_4
      - redis_5
      - redis_6
    networks:
      redis_cluster_net:
        ipv4_address: 173.18.0.8

Al levantar los servicios (“docker-compose up –build”), comprobaremos que el contenedor “mount_cluster”, crea correctamente el cluster, donde los tres primero contenedores serán los maestros de los 3 últimos.

mount_cluster | Redis cluster ready.
mount_cluster exited with code 0

Redis Commander

Podemos hacer uso de Redis CLI para gestionar nuestros nodos de Redis, pero dado que ahora tenemos 6 instancias de Redis funcionando, gestionar las claves de las seis instancias mediante la línea de comando puede hacerse una tarea tediosa.

Para solucionar esto podemos montar un servicio adicional en nuestro Docker Compose, Redis Commander, una aplicación web para gestionar nuestros nodos Redis. Crearemos el servicio y le asignaremos una de las ips de la subred creada, además del puerto 4000.

redis_commander:
    image: rediscommander/redis-commander:latest
    container_name: redis_web
    environment:
      REDIS_HOSTS: local:redis_1:6379,local:redis_2:6379,local:redis_3:6379, local:redis_4:6379, local:redis_5:6379, local:redis_6:6379
    ports:
      - "4000:8081"
    depends_on:
      - redis_1
      - redis_2
      - redis_3
      - redis_4
      - redis_5
      - redis_6
      - mount-cluster
    networks:
      redis_cluster_net:
        ipv4_address: 173.18.0.9

Si accedemos a la URL http://localhost:4000/, accederemos al interfaz de Redis Commander.

Conectar con Redis

Vamos a elegir GO como lenguaje de acceso a nuestro Cluster, además como conector usaremos "go-redis". Podríamos haber elegido cualquier lenguaje y cualquier conector, siempre que el conector soporte Redis Cluster.

Después de hacer el import correspondiente de go-redis, definiremos un módulo cliente, que engloba al cliente Redis. En el init() de la librería definiremos el cluster client.

func init() {
    redisClient := redis.NewClusterClient(&redis.ClusterOptions{
        Addrs: []string{"173.18.0.2:6379", "173.18.0.3:6379", "173.18.0.4:6379", "173.18.0.5:6379", "173.18.0.6:6379", "173.18.0.7:6379"},
    })
    err := redisClient.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {
        return shard.Ping(ctx).Err()
    })
    if err != nil {
        panic(err)
    }
    client = redisClient
}

Si nos fijamos en cómo se define la instancia de cliente de Redis, estamos añadiendo las ips de todos los nodos del clúster. Una vez definido el cliente podemos insertar claves y acceder a los datos del clúster, mediante Set y Get, especificando clave y valor.

func Get(key string) (interface{}, error) {
    val, err := client.Get(ctx, key).Result()
    if err != nil {
        return nil, err
    }
    return val, nil
}

func Set(key string, value interface{}) error {
    exp := time.Duration(600 * time.Second)
    return client.Set(ctx, key, value, exp).Err()
}

¿Por qué usar Redis Cluster frente a otras soluciones?

Ya hemos visto cómo montar un Redis Cluster, por lo que podemos extraer las siguientes ventajas de su uso:

  • Replicación de datos
  • Escalabilidad automática
  • Desarrollo sencillo gracias a su código abierto y cantidad de herramientas
  • Tiempos muy rápidos de lectura/escritura
Comparte este artículo
Etiquetas
Artículos recientes