Redis
 sql >> Base de données >  >> NoSQL >> Redis

Haute disponibilité avec Redis Sentinels :connexion aux ensembles maître/esclave Redis

Se connecter à un serveur Redis unique et autonome est assez simple :il suffit de pointer vers l'hôte, le port et de fournir le mot de passe d'authentification, le cas échéant. La plupart des clients Redis prennent même en charge une sorte de spécification de connexion URI.

Cependant, pour obtenir une haute disponibilité (HA), vous devez déployer une configuration maître et esclave(s). Dans cet article, nous allons vous montrer comment vous connecter aux serveurs Redis dans une configuration haute disponibilité via un point de terminaison unique.

Haute disponibilité dans Redis

La haute disponibilité dans Redis est obtenue grâce à la réplication maître-esclave. Un serveur Redis maître peut avoir plusieurs serveurs Redis en tant qu'esclaves, de préférence déployés sur différents nœuds dans plusieurs centres de données. Lorsque le maître n'est pas disponible, l'un des esclaves peut être promu pour devenir le nouveau maître et continuer à servir les données avec peu ou pas d'interruption.

Compte tenu de la simplicité de Redis, de nombreux outils haute disponibilité sont disponibles pour surveiller et gérer une configuration de réplica maître-esclave. Cependant, la solution HA la plus courante fournie avec Redis est Redis Sentinels. Les Sentinelles Redis s'exécutent comme un ensemble de processus distincts qui, combinés, surveillent les ensembles maître-esclave Redis et fournissent un basculement et une reconfiguration automatiques.

Se connecter via Redis Sentinels

Les Sentinelles Redis agissent également en tant que fournisseurs de configuration pour les ensembles maître-esclave. Autrement dit, un client Redis peut se connecter aux Sentinelles Redis pour connaître l'état actuel du maître et de l'état général du jeu de réplicas maître/esclave. La documentation Redis fournit des détails sur la manière dont les clients doivent interagir avec les sentinelles. Cependant, ce mécanisme de connexion à Redis présente quelques inconvénients :

  • A besoin de l'assistance client  :La connexion à Redis Sentinels nécessite un client "conscient" de Sentinel. Les clients Redis les plus populaires ont maintenant commencé à prendre en charge Redis Sentinels, mais certains ne le font toujours pas. Par exemple, node_redis (Node.js), phpredis (PHP) et scala-redis (Scala) sont des clients recommandés qui ne bénéficient toujours pas de la prise en charge de Redis Sentinel.
  • Complexité :La configuration et la connexion à Redis Sentinels ne sont pas toujours simples, en particulier lorsque le déploiement s'effectue dans des centres de données ou des zones de disponibilité. Par exemple, les sentinelles se souviennent des adresses IP (et non des noms DNS) de tous les serveurs de données et sentinelles qu'elles rencontrent et peuvent être mal configurées lorsque les nœuds se déplacent dynamiquement dans les centres de données. Les Sentinelles Redis partagent également des informations IP avec d'autres Sentinelles. Malheureusement, ils transmettent des adresses IP locales, ce qui peut être problématique si le client se trouve dans un centre de données séparé. Ces problèmes peuvent compliquer considérablement les opérations et le développement.
  • Sécurité :Le serveur Redis lui-même fournit une authentification primitive via le mot de passe du serveur, les Sentinelles elles-mêmes n'ont pas une telle fonctionnalité. Ainsi, une Redis Sentinel ouverte sur Internet expose l'intégralité des informations de configuration de tous les maîtres qu'elle est configurée pour gérer. Ainsi, Redis Sentinels doit toujours être déployé derrière des pare-feu correctement configurés. Obtenir la bonne configuration du pare-feu, en particulier pour les configurations multizones, peut être très délicat.

Point de terminaison unique

Un seul point de terminaison de connexion réseau pour un ensemble maître-esclave peut être fourni de plusieurs façons. Cela peut être fait via des adresses IP virtuelles ou en remappant les noms DNS ou en utilisant un serveur proxy (par exemple HAProxy) devant les serveurs Redis. Chaque fois qu'une défaillance du maître actuel est détectée (par Sentinel), l'adresse IP ou le nom DNS est basculé vers l'esclave qui a été promu pour devenir le nouveau maître par les Sentinelles Redis. Notez que cela prend du temps et que la connexion réseau au point de terminaison devra être rétablie. Les Sentinelles Redis reconnaissent qu'un maître est en panne uniquement après qu'il a été en panne pendant un certain temps (30 secondes par défaut), puis votent pour promouvoir un esclave. Lors de la promotion d'un esclave, l'adresse IP/l'entrée DNS/le proxy doit changer pour pointer vers le nouveau maître.

Connexion aux ensembles Maître-Esclave

La considération importante lors de la connexion à des ensembles de réplicas maître-esclave à l'aide d'un seul point de terminaison est qu'il faut prévoir des tentatives en cas d'échec de connexion pour tenir compte de tout échec de connexion lors d'un basculement automatique du jeu de répliques.

Nous allons le montrer avec des exemples en Java, Ruby et Node.js. Dans chaque exemple, nous écrivons et lisons alternativement à partir d'un cluster HA Redis pendant qu'un basculement se produit en arrière-plan. Dans le monde réel, les nouvelles tentatives seront limitées à une durée ou à un nombre particulier .

Se connecter avec Java

Jedis est le client Java recommandé pour Redis.

Exemple de point de terminaison unique

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 sortie de ce code de test lors d'un basculement ressemble à :

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...

Ceci est un programme de test simple. Dans la vraie vie, le nombre de tentatives sera fixé par la durée ou le nombre.

Exemple Sentinelle Redis

Jedis prend également en charge Redis Sentinels. Voici donc le code qui fait la même chose que l'exemple ci-dessus mais en se connectant aux Sentinelles.

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();
                }
            }
        }
    }
...
}

Voyons le comportement du programme ci-dessus lors d'un basculement géré par 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...
...

Comme le montrent les journaux, un client qui prend en charge Sentinels peut récupérer assez rapidement d'un événement de basculement.

Se connecter avec Ruby

Redis-rb est le client Ruby recommandé pour Redis.

Exemple de point de terminaison unique

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

Voici l'exemple de sortie lors d'un basculement :

"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..."

Encore une fois, le code réel doit contenir un nombre limité de tentatives.

Exemple Sentinelle Redis

Redis-rb prend également en charge les Sentinelles.

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 gère les basculements Sentinel sans aucune interruption.

"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

Se connecter avec Node.js

Node_redis est le client Node.js recommandé pour Redis.

Exemple de point de terminaison unique

...
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);
...

Voici à quoi ressemblera un basculement :

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...

Vous pouvez également expérimenter l'option « retry_strategy » lors de la création de la connexion pour ajuster la logique de nouvelle tentative en fonction de vos besoins. La documentation client contient un exemple.

Exemple Sentinelle Redis

Node_redis ne prend actuellement pas en charge les Sentinelles, mais le client Redis populaire pour Node.js, ioredis prend en charge les Sentinelles. Reportez-vous à sa documentation sur la façon de se connecter à Sentinels depuis Node.js.

Prêt à passer à l'échelle ? Nous proposons un hébergement pour Redis™* et des solutions entièrement gérées sur le cloud de votre choix. Comparez-nous à d'autres et voyez pourquoi nous vous épargnons des tracas et de l'argent.