sql >> Base de Datos >  >> NoSQL >> Redis

Alta disponibilidad con Redis Sentinels:conexión a conjuntos Redis Master/Slave

Conectarse a un único servidor Redis independiente es bastante simple:simplemente señale el host, el puerto y proporcione la contraseña de autenticación, si corresponde. La mayoría de los clientes de Redis incluso brindan soporte para algún tipo de especificación de conexión URI.

Sin embargo, para lograr alta disponibilidad (HA), debe implementar una configuración de maestro y esclavo(s). En esta publicación, le mostraremos cómo conectarse a los servidores de Redis en una configuración HA a través de un único punto final.

Alta disponibilidad en Redis

La alta disponibilidad en Redis se logra a través de la replicación maestro-esclavo. Un servidor Redis maestro puede tener varios servidores Redis como esclavos, preferiblemente implementados en diferentes nodos en varios centros de datos. Cuando el maestro no está disponible, uno de los esclavos puede ser promovido para convertirse en el nuevo maestro y continuar sirviendo datos con poca o ninguna interrupción.

Dada la simplicidad de Redis, hay muchas herramientas de alta disponibilidad disponibles que pueden monitorear y administrar una configuración de réplica maestro-esclavo. Sin embargo, la solución HA más común que viene con Redis es Redis Sentinels. Redis Sentinels se ejecuta como un conjunto de procesos separados que, en conjunto, monitorean los conjuntos maestro-esclavo de Redis y brindan conmutación por error y reconfiguración automáticas.

Conexión a través de Redis Sentinels

Redis Sentinels también actúa como proveedor de configuración para conjuntos maestro-esclavo. Es decir, un cliente de Redis puede conectarse a Redis Sentinels para averiguar el estado general y maestro actual del conjunto de réplicas maestro/esclavo. La documentación de Redis proporciona detalles sobre cómo los clientes deben interactuar con los Sentinels. Sin embargo, este mecanismo de conexión a Redis tiene algunos inconvenientes:

  • Necesita asistencia del cliente :La conexión a Redis Sentinels necesita un cliente "consciente" de Sentinel. Los clientes de Redis más populares ahora han comenzado a admitir Redis Sentinels, pero algunos aún no lo hacen. Por ejemplo, node_redis (Node.js), phpredis (PHP) y scala-redis (Scala) son algunos clientes recomendados que aún no son compatibles con Redis Sentinel.
  • Complejidad :Configurar y conectarse a Redis Sentinels no siempre es sencillo, especialmente cuando la implementación se realiza en centros de datos o zonas de disponibilidad. Por ejemplo, los centinelas recuerdan las direcciones IP (no los nombres DNS) de todos los servidores de datos y centinelas con los que se encuentran y pueden configurarse incorrectamente cuando los nodos se mueven dinámicamente dentro de los centros de datos. Los Redis Sentinels también comparten información de IP con otros Sentinels. Desafortunadamente, pasan direcciones IP locales, lo que puede ser problemático si el cliente está en un centro de datos separado. Estos problemas pueden agregar una complejidad significativa tanto a las operaciones como al desarrollo.
  • Seguridad :El propio servidor Redis proporciona una autenticación primitiva a través de la contraseña del servidor, los propios Sentinels no tienen esa característica. Entonces, un Redis Sentinel que está abierto a Internet expone toda la información de configuración de todos los maestros que está configurado para administrar. Por lo tanto, Redis Sentinels siempre debe implementarse detrás de firewalls configurados correctamente. Conseguir que la configuración del cortafuegos sea correcta, especialmente para las configuraciones multizona, puede ser realmente complicado.

Punto final único

Se puede proporcionar un único punto final de conexión de red para un conjunto maestro-esclavo de muchas maneras. Se puede hacer a través de IP virtuales o reasignando nombres de DNS o usando un servidor proxy (por ejemplo, HAProxy) frente a los servidores de Redis. Cada vez que se detecta una falla del maestro actual (por parte de Sentinel), el nombre de IP o DNS se conmuta por error al esclavo que Redis Sentinels promovió para convertirse en el nuevo maestro. Tenga en cuenta que esto lleva tiempo y será necesario restablecer la conexión de red al punto final. Los Redis Sentinels reconocen a un maestro como inactivo solo después de que haya estado inactivo durante un período de tiempo (predeterminado 30 segundos) y luego votan para promover un esclavo. Tras la promoción de un esclavo, la dirección IP/entrada de DNS/proxy debe cambiar para apuntar al nuevo maestro.

Conexión a conjuntos Maestro-Esclavo

La consideración importante al conectarse a conjuntos de réplicas maestro-esclavo usando un único punto de conexión es que se deben prever reintentos en fallas de conexión para adaptarse a cualquier falla de conexión durante una conmutación por error automática del conjunto de réplicas.

Mostraremos esto con ejemplos en Java, Ruby y Node.js. En cada ejemplo, alternativamente escribimos y leemos desde un clúster HA Redis mientras se produce una conmutación por error en segundo plano. En el mundo real, los intentos de reintento se limitarán a una duración o cantidad determinada .

Conexión con Java

Jedis es el cliente Java recomendado para Redis.

Ejemplo de punto final único

public class JedisTestSingleEndpoint {
...
    public static final String HOSTNAME = "SG-cluster0-single-endpoint.example.com";
    public static final String PASSWORD = "foobared";
...
    private void runTest() throws InterruptedException {
        boolean writeNext = true;
        Jedis jedis = null;
        while (true) {
            try {
                jedis = new Jedis(HOSTNAME);
                jedis.auth(PASSWORD);
                Socket socket = jedis.getClient().getSocket();
                printer("Connected to " + socket.getRemoteSocketAddress());
                while (true) {
                    if (writeNext) {
                        printer("Writing...");
                        jedis.set("java-key-999", "java-value-999");
                        writeNext = false;
                    } else {
                        printer("Reading...");
                        jedis.get("java-key-999");
                        writeNext = true;
                    }
                    Thread.sleep(2 * 1000);
                }
            } catch (JedisException e) {
                printer("Connection error of some sort!");
                printer(e.getMessage());
                Thread.sleep(2 * 1000);
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    }
...
}

La salida de este código de prueba durante una conmutación por error se ve así:

Wed Sep 28 10:57:28 IST 2016: Initializing...
Wed Sep 28 10:57:31 IST 2016: Connected to SG-cluster0-single-endpoint.example.com/54.71.60.125:6379 << Connected to node 1
Wed Sep 28 10:57:31 IST 2016: Writing...
Wed Sep 28 10:57:33 IST 2016: Reading...
..
Wed Sep 28 10:57:50 IST 2016: Reading...
Wed Sep 28 10:57:52 IST 2016: Writing...
Wed Sep 28 10:57:53 IST 2016: Connection error of some sort! << Master went down!
Wed Sep 28 10:57:53 IST 2016: Unexpected end of stream.
Wed Sep 28 10:57:58 IST 2016: Connected to SG-cluster0-single-endpoint.example.com/54.71.60.125:6379
Wed Sep 28 10:57:58 IST 2016: Writing...
Wed Sep 28 10:57:58 IST 2016: Connection error of some sort!
Wed Sep 28 10:57:58 IST 2016: java.net.SocketTimeoutException: Read timed out  << Old master is unreachable
Wed Sep 28 10:58:02 IST 2016: Connected to SG-cluster0-single-endpoint.example.com/54.71.60.125:6379
Wed Sep 28 10:58:02 IST 2016: Writing...
Wed Sep 28 10:58:03 IST 2016: Connection error of some sort!
...
Wed Sep 28 10:59:10 IST 2016: Connected to SG-cluster0-single-endpoint.example.com/54.214.164.243:6379  << New master ready. Connected to node 2
Wed Sep 28 10:59:10 IST 2016: Writing...
Wed Sep 28 10:59:12 IST 2016: Reading...

Este es un programa de prueba simple. En la vida real, el número de reintentos se fijará por duración o conteo.

Ejemplo de Redis Sentinel

Jedis también es compatible con Redis Sentinels. Así que aquí está el código que hace lo mismo que el ejemplo anterior pero conectándose a Sentinels.

public class JedisTestSentinelEndpoint {
    private static final String MASTER_NAME = "mymaster";
    public static final String PASSWORD = "foobared";
    private static final Set sentinels;
    static {
        sentinels = new HashSet();
        sentinels.add("mymaster-0.servers.example.com:26379");
        sentinels.add("mymaster-1.servers.example.com:26379");
        sentinels.add("mymaster-2.servers.example.com:26379");
    }

    public JedisTestSentinelEndpoint() {
    }

    private void runTest() throws InterruptedException {
        boolean writeNext = true;
        JedisSentinelPool pool = new JedisSentinelPool(MASTER_NAME, sentinels);
        Jedis jedis = null;
        while (true) {
            try {
                printer("Fetching connection from pool");
                jedis = pool.getResource();
                printer("Authenticating...");
                jedis.auth(PASSWORD);
                printer("auth complete...");
                Socket socket = jedis.getClient().getSocket();
                printer("Connected to " + socket.getRemoteSocketAddress());
                while (true) {
                    if (writeNext) {
                        printer("Writing...");
                        jedis.set("java-key-999", "java-value-999");
                        writeNext = false;
                    } else {
                        printer("Reading...");
                        jedis.get("java-key-999");
                        writeNext = true;
                    }
                    Thread.sleep(2 * 1000);
                }
            } catch (JedisException e) {
                printer("Connection error of some sort!");
                printer(e.getMessage());
                Thread.sleep(2 * 1000);
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    }
...
}

Veamos el comportamiento del programa anterior durante una conmutación por error administrada por Sentinel:

Wed Sep 28 14:43:42 IST 2016: Initializing...
Sep 28, 2016 2:43:42 PM redis.clients.jedis.JedisSentinelPool initSentinels
INFO: Trying to find master from available Sentinels...
Sep 28, 2016 2:43:42 PM redis.clients.jedis.JedisSentinelPool initSentinels
INFO: Redis master running at 54.71.60.125:6379, starting Sentinel listeners...
Sep 28, 2016 2:43:43 PM redis.clients.jedis.JedisSentinelPool initPool
INFO: Created JedisPool to master at 54.71.60.125:6379
Wed Sep 28 14:43:43 IST 2016: Fetching connection from pool
Wed Sep 28 14:43:43 IST 2016: Authenticating...
Wed Sep 28 14:43:43 IST 2016: auth complete...
Wed Sep 28 14:43:43 IST 2016: Connected to /54.71.60.125:6379
Wed Sep 28 14:43:43 IST 2016: Writing...
Wed Sep 28 14:43:45 IST 2016: Reading...
Wed Sep 28 14:43:48 IST 2016: Writing...
Wed Sep 28 14:43:50 IST 2016: Reading...
Sep 28, 2016 2:43:51 PM redis.clients.jedis.JedisSentinelPool initPool
INFO: Created JedisPool to master at 54.214.164.243:6379
Wed Sep 28 14:43:52 IST 2016: Writing...
Wed Sep 28 14:43:55 IST 2016: Reading...
Wed Sep 28 14:43:57 IST 2016: Writing...
Wed Sep 28 14:43:59 IST 2016: Reading...
Wed Sep 28 14:44:02 IST 2016: Writing...
Wed Sep 28 14:44:02 IST 2016: Connection error of some sort!
Wed Sep 28 14:44:02 IST 2016: Unexpected end of stream.
Wed Sep 28 14:44:04 IST 2016: Fetching connection from pool
Wed Sep 28 14:44:04 IST 2016: Authenticating...
Wed Sep 28 14:44:04 IST 2016: auth complete...
Wed Sep 28 14:44:04 IST 2016: Connected to /54.214.164.243:6379
Wed Sep 28 14:44:04 IST 2016: Writing...
Wed Sep 28 14:44:07 IST 2016: Reading...
...

Como se desprende de los registros, un cliente compatible con Sentinels puede recuperarse de un evento de conmutación por error con bastante rapidez.

Conectando con Rubí

Redis-rb es el cliente de Ruby recomendado para Redis.

Ejemplo de punto final único

require 'redis'

HOST = "SG-cluster0-single-endpoint.example.com"
AUTH = "foobared"
...

def connect_and_write
  while true do
    begin
      logmsg "Attempting to establish connection"
      redis = Redis.new(:host => HOST, :password => AUTH)
      redis.ping
      sock = redis.client.connection.instance_variable_get(:@sock)
      logmsg "Connected to #{sock.remote_address.ip_address}, DNS: #{sock.remote_address.getnameinfo}"
      while true do
        if $writeNext
          logmsg "Writing..."
          redis.set("ruby-key-1000", "ruby-value-1000")
          $writeNext = false
        else
          logmsg "Reading..."
          redis.get("ruby-key-1000")
          $writeNext = true
        end
        sleep(2)
      end
    rescue Redis::BaseError => e
      logmsg "Connection error of some sort!"
      logmsg e.message
      sleep(2)
    end
  end
end

...
logmsg "Initiaing..."
connect_and_write

Aquí está el resultado de muestra durante una conmutación por error:

"2016-09-28 11:36:42 +0530: Initiaing..."
"2016-09-28 11:36:42 +0530: Attempting to establish connection"
"2016-09-28 11:36:44 +0530: Connected to 54.71.60.125, DNS: [\"ec2-54-71-60-125.us-west-2.compute.amazonaws.com\", \"6379\"] " << Connected to node 1
"2016-09-28 11:36:44 +0530: Writing..."
"2016-09-28 11:36:47 +0530: Reading..."
...
"2016-09-28 11:37:08 +0530: Writing..."
"2016-09-28 11:37:09 +0530: Connection error of some sort!"  << Master went down!
...
"2016-09-28 11:38:13 +0530: Attempting to establish connection"
"2016-09-28 11:38:15 +0530: Connected to 54.214.164.243, DNS: [\"ec2-54-214-164-243.us-west-2.compute.amazonaws.com\", \"6379\"] " << Connected to node 2
"2016-09-28 11:38:15 +0530: Writing..."
"2016-09-28 11:38:17 +0530: Reading..."

Nuevamente, el código real debe contener un número limitado de reintentos.

Ejemplo de Redis Sentinel

Redis-rb también es compatible con Sentinels.

AUTH = 'foobared'

SENTINELS = [
  {:host => "mymaster0.servers.example.com", :port => 26379},
  {:host => "mymaster0.servers.example.com", :port => 26379},
  {:host => "mymaster0.servers.example.com", :port => 26379}
]
MASTER_NAME = "mymaster0"

$writeNext = true
def connect_and_write
  while true do
    begin
      logmsg "Attempting to establish connection"
      redis = Redis.new(:url=> "redis://#{MASTER_NAME}", :sentinels => SENTINELS, :password => AUTH)
      redis.ping
      sock = redis.client.connection.instance_variable_get(:@sock)
      logmsg "Connected to #{sock.remote_address.ip_address}, DNS: #{sock.remote_address.getnameinfo} "
      while true do
        if $writeNext
          logmsg "Writing..."
          redis.set("ruby-key-1000", "ruby-val-1000")
          $writeNext = false
        else
          logmsg "Reading..."
          redis.get("ruby-key-1000")
          $writeNext = true
        end
        sleep(2)
      end
    rescue Redis::BaseError => e
      logmsg "Connection error of some sort!"
      logmsg e.message
      sleep(2)
    end
  end
end

Redis-rb administra las conmutaciones por error de Sentinel sin interrupciones.

"2016-09-28 15:10:56 +0530: Initiaing..."
"2016-09-28 15:10:56 +0530: Attempting to establish connection"
"2016-09-28 15:10:58 +0530: Connected to 54.214.164.243, DNS: [\"ec2-54-214-164-243.us-west-2.compute.amazonaws.com\", \"6379\"] "
"2016-09-28 15:10:58 +0530: Writing..."
"2016-09-28 15:11:00 +0530: Reading..."
"2016-09-28 15:11:03 +0530: Writing..."
"2016-09-28 15:11:05 +0530: Reading..."
"2016-09-28 15:11:07 +0530: Writing..."
...
<<failover>>
...
"2016-09-28 15:11:10 +0530: Reading..."
"2016-09-28 15:11:12 +0530: Writing..."
"2016-09-28 15:11:14 +0530: Reading..."
"2016-09-28 15:11:17 +0530: Writing..."
...
# No disconnections noticed at all by the application

Conexión con Node.js

Node_redis es el cliente de Node.js recomendado para Redis.

Ejemplo de punto final único

...
var redis = require("redis");
var hostname = "SG-cluster0-single-endpoint.example.com";
var auth = "foobared";
var client = null;
...

function readAndWrite() {
  if (!client || !client.connected) {
    client = redis.createClient({
      'port': 6379,
      'host': hostname,
      'password': auth,
      'retry_strategy': function(options) {
        printer("Connection failed with error: " + options.error);
        if (options.total_retry_time > 1000 * 60 * 60) {
          return new Error('Retry time exhausted');
        }
        return new Error('retry strategy: failure');
      }});
    client.on("connect", function () {
      printer("Connected to " + client.address + "/" + client.stream.remoteAddress + ":" + client.stream.remotePort);
    });
    client.on('error', function (err) {
      printer("Error event: " + err);
      client.quit();
    });
  }

  if (writeNext) {
    printer("Writing...");
    client.set("node-key-1001", "node-value-1001", function(err, res) {
      if (err) {
        printer("Error on set: " + err);
        client.quit();
      }
      setTimeout (readAndWrite, 2000)
    });

    writeNext = false;
  } else {
    printer("Reading...");
    client.get("node-key-1001", function(err, res) {
      if (err) {
        client.quit();
        printer("Error on get: " + err);
      }
      setTimeout (readAndWrite, 2000)
    });
    writeNext = true;
  }
}
...
setTimeout(readAndWrite, 2000);
...

Así es como se verá una conmutación por error:

2016-09-28T13:29:46+05:30: Writing...
2016-09-28T13:29:47+05:30: Connected to SG-meh0-6-master.devservers.mongodirector.com:6379/54.214.164.243:6379 << Connected to node 1
2016-09-28T13:29:50+05:30: Reading...
...
2016-09-28T13:30:02+05:30: Writing...
2016-09-28T13:30:04+05:30: Reading...
2016-09-28T13:30:06+05:30: Connection failed with error: null << Master went down
...
2016-09-28T13:30:50+05:30: Connected to SG-meh0-6-master.devservers.mongodirector.com:6379/54.71.60.125:6379 << Connected to node 2
2016-09-28T13:30:52+05:30: Writing...
2016-09-28T13:30:55+05:30: Reading...

También puede experimentar con la opción 'retry_strategy' durante la creación de la conexión para modificar la lógica de reintento para satisfacer sus necesidades. La documentación del cliente tiene un ejemplo.

Ejemplo de Redis Sentinel

Node_redis actualmente no es compatible con Sentinels, pero el popular cliente de Redis para Node.js, ioredis es compatible con Sentinels. Consulte su documentación sobre cómo conectarse a Sentinels desde Node.js.

¿Listo para escalar? Ofrecemos hosting para Redis™* y soluciones completamente administradas en una nube de su elección. Compárenos con otros y vea por qué le ahorramos molestias y dinero.