Redis
 sql >> Database >  >> NoSQL >> Redis

Alta disponibilità con Redis Sentinels:connessione ai set Redis Master/Slave

La connessione a un singolo server Redis autonomo è abbastanza semplice:punta semplicemente all'host, alla porta e fornisci la password di autenticazione, se presente. La maggior parte dei client Redis fornisce anche supporto per una sorta di specifica di connessione URI.

Tuttavia, per ottenere l'alta disponibilità (HA), è necessario implementare una configurazione master e slave. In questo post ti mostreremo come connetterti ai server Redis in una configurazione HA tramite un singolo endpoint.

Alta disponibilità in Redis

L'elevata disponibilità in Redis si ottiene attraverso la replica master-slave. Un server Redis master può avere più server Redis come slave, preferibilmente distribuiti su nodi diversi su più data center. Quando il master non è disponibile, uno degli slave può essere promosso a nuovo master e continuare a servire i dati con poche o nessuna interruzione.

Data la semplicità di Redis, sono disponibili molti strumenti ad alta disponibilità in grado di monitorare e gestire una configurazione di replica master-slave. Tuttavia, la soluzione HA più comune fornita in bundle con Redis è Redis Sentinels. Redis Sentinels viene eseguito come un insieme di processi separati che, in combinazione, monitorano i set Redis master-slave e forniscono failover e riconfigurazione automatici.

Connessione tramite Redis Sentinels

I Redis Sentinel fungono anche da provider di configurazione per i set master-slave. In altre parole, un client Redis può connettersi a Redis Sentinels per scoprire il master corrente e lo stato generale del set di repliche master/slave. La documentazione Redis fornisce dettagli su come i client devono interagire con le Sentinelle. Tuttavia, questo meccanismo di connessione a Redis presenta alcuni inconvenienti:

  • Ha bisogno dell'assistenza del cliente :la connessione a Redis Sentinels richiede un client "aware" di Sentinel. I client Redis più popolari hanno ora iniziato a supportare Redis Sentinels, ma alcuni ancora non lo fanno. Ad esempio, node_redis (Node.js), phpredis (PHP) e scala-redis (Scala) sono alcuni client consigliati che non hanno ancora il supporto Redis Sentinel.
  • Complessità :la configurazione e la connessione a Redis Sentinels non è sempre semplice, soprattutto quando la distribuzione avviene tra data center o zone di disponibilità. Ad esempio, le sentinelle ricordano gli indirizzi IP (non i nomi DNS) di tutti i server di dati e le sentinelle che incontrano e possono subire una configurazione errata quando i nodi vengono spostati dinamicamente all'interno dei data center. Le Sentinelle Redis condividono anche le informazioni IP con altre Sentinelle. Sfortunatamente, passano intorno agli IP locali che possono essere problematici se il client si trova in un data center separato. Questi problemi possono aggiungere una notevole complessità sia alle operazioni che allo sviluppo.
  • Sicurezza :Il server Redis stesso fornisce l'autenticazione primitiva tramite la password del server, le stesse Sentinelle non hanno tale funzionalità. Quindi un Redis Sentinel che è aperto a Internet espone tutte le informazioni di configurazione di tutti i master che è configurato per gestire. Pertanto, Redis Sentinels dovrebbe essere sempre distribuito dietro firewall correttamente configurati. Ottenere la corretta configurazione del firewall, specialmente per le configurazioni multizona, può essere davvero complicato.

Punto unico

Un singolo endpoint di connessione di rete per un set master-slave può essere fornito in molti modi. Potrebbe essere eseguito tramite IP virtuali o rimappando i nomi DNS o utilizzando un server proxy (ad es. HAProxy) davanti ai server Redis. Ogni volta che viene rilevato un errore del master corrente (da parte di Sentinel), il nome IP o DNS viene trasferito allo slave che è stato promosso a nuovo master da Redis Sentinel. Tieni presente che questa operazione richiede tempo e la connessione di rete all'endpoint dovrà essere ristabilita. Le Redis Sentinels riconoscono un padrone come a terra solo dopo che è rimasto a terra per un periodo di tempo (predefinito 30 secondi) e quindi votano per promuovere uno schiavo. Dopo la promozione di uno slave, l'indirizzo IP/voce DNS/proxy deve cambiare per puntare al nuovo master.

Connessione a set Master-Slave

L'importante considerazione durante la connessione ai set di repliche master-slave utilizzando un singolo endpoint è che è necessario predisporre i tentativi in ​​caso di errori di connessione per far fronte a eventuali errori di connessione durante un failover automatico del set di repliche.

Lo mostreremo con esempi in Java, Ruby e Node.js. In ogni esempio, scriviamo e leggiamo alternativamente da un cluster HA Redis mentre si verifica un failover in background. Nel mondo reale, i tentativi di ripetizione saranno limitati a una durata o a un conteggio particolari .

Connessione con Java

Jedis è il client Java consigliato per Redis.

Esempio di endpoint singolo

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

L'output di questo codice di test durante un failover è simile a:

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

Questo è un semplice programma di test. Nella vita reale, il numero di tentativi verrà fissato in base alla durata o al conteggio.

Esempio Redis Sentinel

Jedis supporta anche Redis Sentinels. Quindi ecco il codice che fa la stessa cosa dell'esempio sopra ma connettendosi 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();
                }
            }
        }
    }
...
}

Vediamo il comportamento del programma sopra durante un failover gestito da 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...
...

Come risulta dai log, un client che supporta Sentinels può eseguire il ripristino da un evento di failover abbastanza rapidamente.

Connettersi con Ruby

Redis-rb è il client Ruby consigliato per Redis.

Esempio di endpoint singolo

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

Ecco l'output di esempio durante un failover:

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

Anche in questo caso, il codice effettivo dovrebbe contenere un numero limitato di tentativi.

Esempio Redis Sentinel

Redis-rb supporta anche le Sentinelle.

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 gestisce i failover di Sentinel senza alcuna interruzione.

"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

Connessione con Node.js

Node_redis è il client Node.js consigliato per Redis.

Esempio di endpoint singolo

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

Ecco come apparirà un failover:

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

Puoi anche sperimentare l'opzione "retry_strategy" durante la creazione della connessione per modificare la logica dei tentativi in ​​base alle tue esigenze. La documentazione del cliente ha un esempio.

Esempio Redis Sentinel

Node_redis attualmente non supporta Sentinels, ma il popolare client Redis per Node.js, ioredis supporta Sentinels. Fare riferimento alla sua documentazione su come connettersi a Sentinels da Node.js.

Pronto per aumentare? Offriamo hosting per Redis™* e soluzioni completamente gestite su un cloud a tua scelta. Confrontaci con gli altri e scopri perché ti risparmiamo fatica e denaro.