Utilizzo di SignalR con il failover del bus di messaggi Redis utilizzando ConnectionUtils.Connect () di BookSleeve


112

Sto cercando di creare uno scenario di failover del bus di messaggi Redis con un'app SignalR.

All'inizio, abbiamo provato un semplice failover del bilanciamento del carico hardware, che controllava semplicemente due server Redis. L'applicazione SignalR puntava al singolare endpoint HLB. Quindi ho fallito un server, ma non sono riuscito a ricevere correttamente alcun messaggio sul secondo server Redis senza riciclare il pool di app SignalR. Presumibilmente questo è perché deve inviare i comandi di installazione al nuovo bus di messaggi Redis.

A partire da SignalR RC1, Microsoft.AspNet.SignalR.Redis.RedisMessageBusutilizza Booksleeve RedisConnection()per connettersi a un singolo Redis per pub / sub.

Ho creato una nuova classe, RedisMessageBusCluster()che utilizza Booksleeve ConnectionUtils.Connect()per connettersi a una in un cluster di server Redis.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BookSleeve;
using Microsoft.AspNet.SignalR.Infrastructure;

namespace Microsoft.AspNet.SignalR.Redis
{
    /// <summary>
    /// WIP:  Getting scaleout for Redis working
    /// </summary>
    public class RedisMessageBusCluster : ScaleoutMessageBus
    {
        private readonly int _db;
        private readonly string[] _keys;
        private RedisConnection _connection;
        private RedisSubscriberConnection _channel;
        private Task _connectTask;

        private readonly TaskQueue _publishQueue = new TaskQueue();

        public RedisMessageBusCluster(string serverList, int db, IEnumerable<string> keys, IDependencyResolver resolver)
            : base(resolver)
        {
            _db = db;
            _keys = keys.ToArray();

            // uses a list of connections
            _connection = ConnectionUtils.Connect(serverList);

            //_connection = new RedisConnection(host: server, port: port, password: password);

            _connection.Closed += OnConnectionClosed;
            _connection.Error += OnConnectionError;


            // Start the connection - TODO:  can remove this Open as the connection is already opened, but there's the _connectTask is used later on
            _connectTask = _connection.Open().Then(() =>
            {
                // Create a subscription channel in redis
                _channel = _connection.GetOpenSubscriberChannel();

                // Subscribe to the registered connections
                _channel.Subscribe(_keys, OnMessage);

                // Dirty hack but it seems like subscribe returns before the actual
                // subscription is properly setup in some cases
                while (_channel.SubscriptionCount == 0)
                {
                    Thread.Sleep(500);
                }
            });
        }


        protected override Task Send(Message[] messages)
        {
            return _connectTask.Then(msgs =>
            {
                var taskCompletionSource = new TaskCompletionSource<object>();

                // Group messages by source (connection id)
                var messagesBySource = msgs.GroupBy(m => m.Source);

                SendImpl(messagesBySource.GetEnumerator(), taskCompletionSource);

                return taskCompletionSource.Task;
            },
            messages);
        }

        private void SendImpl(IEnumerator<IGrouping<string, Message>> enumerator, TaskCompletionSource<object> taskCompletionSource)
        {
            if (!enumerator.MoveNext())
            {
                taskCompletionSource.TrySetResult(null);
            }
            else
            {
                IGrouping<string, Message> group = enumerator.Current;

                // Get the channel index we're going to use for this message
                int index = Math.Abs(group.Key.GetHashCode()) % _keys.Length;

                string key = _keys[index];

                // Increment the channel number
                _connection.Strings.Increment(_db, key)
                                   .Then((id, k) =>
                                   {
                                       var message = new RedisMessage(id, group.ToArray());

                                       return _connection.Publish(k, message.GetBytes());
                                   }, key)
                                   .Then((enumer, tcs) => SendImpl(enumer, tcs), enumerator, taskCompletionSource)
                                   .ContinueWithNotComplete(taskCompletionSource);
            }
        }

        private void OnConnectionClosed(object sender, EventArgs e)
        {
            // Should we auto reconnect?
            if (true)
            {
                ;
            }
        }

        private void OnConnectionError(object sender, BookSleeve.ErrorEventArgs e)
        {
            // How do we bubble errors?
            if (true)
            {
                ;
            }
        }

        private void OnMessage(string key, byte[] data)
        {
            // The key is the stream id (channel)
            var message = RedisMessage.Deserialize(data);

            _publishQueue.Enqueue(() => OnReceived(key, (ulong)message.Id, message.Messages));
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_channel != null)
                {
                    _channel.Unsubscribe(_keys);
                    _channel.Close(abort: true);
                }

                if (_connection != null)
                {
                    _connection.Close(abort: true);
                }                
            }

            base.Dispose(disposing);
        }
    }
}

Booksleeve ha il proprio meccanismo per determinare un master e passerà automaticamente a un altro server, e ora lo sto testando con SignalR.Chat.

In web.config, ho impostato l'elenco dei server disponibili:

<add key="redis.serverList" value="dbcache1.local:6379,dbcache2.local:6379"/>

Quindi in Application_Start():

        // Redis cluster server list
        string redisServerlist = ConfigurationManager.AppSettings["redis.serverList"];

        List<string> eventKeys = new List<string>();
        eventKeys.Add("SignalR.Redis.FailoverTest");
        GlobalHost.DependencyResolver.UseRedisCluster(redisServerlist, eventKeys);

Ho aggiunto due metodi aggiuntivi a Microsoft.AspNet.SignalR.Redis.DependencyResolverExtensions:

public static IDependencyResolver UseRedisCluster(this IDependencyResolver resolver, string serverList, IEnumerable<string> eventKeys)
{
    return UseRedisCluster(resolver, serverList, db: 0, eventKeys: eventKeys);
}

public static IDependencyResolver UseRedisCluster(this IDependencyResolver resolver, string serverList, int db, IEnumerable<string> eventKeys)
{
    var bus = new Lazy<RedisMessageBusCluster>(() => new RedisMessageBusCluster(serverList, db, eventKeys, resolver));
    resolver.Register(typeof(IMessageBus), () => bus.Value);

    return resolver;
}

Ora il problema è che quando ho diversi punti di interruzione abilitati, fino a quando non è stato aggiunto un nome utente, quindi disabilito tutti i punti di interruzione, l'applicazione funziona come previsto. Tuttavia, con i punti di interruzione disabilitati dall'inizio, sembra esserci una condizione di competizione che potrebbe non riuscire durante il processo di connessione.

Quindi, in RedisMessageCluster():

    // Start the connection
    _connectTask = _connection.Open().Then(() =>
    {
        // Create a subscription channel in redis
        _channel = _connection.GetOpenSubscriberChannel();

        // Subscribe to the registered connections
        _channel.Subscribe(_keys, OnMessage);

        // Dirty hack but it seems like subscribe returns before the actual
        // subscription is properly setup in some cases
        while (_channel.SubscriptionCount == 0)
        {
            Thread.Sleep(500);
        }
    });

Ho provato ad aggiungere sia un Task.Wait, sia un ulterioreSleep() (non mostrato sopra) - che erano in attesa / ecc, ma ottenendo ancora errori.

L'errore ricorrente sembra essere in Booksleeve.MessageQueue.cs~ ln 71:

A first chance exception of type 'System.InvalidOperationException' occurred in BookSleeve.dll
iisexpress.exe Error: 0 : SignalR exception thrown by Task: System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: The queue is closed
   at BookSleeve.MessageQueue.Enqueue(RedisMessage item, Boolean highPri) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\MessageQueue.cs:line 71
   at BookSleeve.RedisConnectionBase.EnqueueMessage(RedisMessage message, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\RedisConnectionBase.cs:line 910
   at BookSleeve.RedisConnectionBase.ExecuteInt64(RedisMessage message, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\RedisConnectionBase.cs:line 826
   at BookSleeve.RedisConnection.IncrementImpl(Int32 db, String key, Int64 value, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\IStringCommands.cs:line 277
   at BookSleeve.RedisConnection.BookSleeve.IStringCommands.Increment(Int32 db, String key, Int64 value, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\IStringCommands.cs:line 270
   at Microsoft.AspNet.SignalR.Redis.RedisMessageBusCluster.SendImpl(IEnumerator`1 enumerator, TaskCompletionSource`1 taskCompletionSource) in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Redis\RedisMessageBusCluster.cs:line 90
   at Microsoft.AspNet.SignalR.Redis.RedisMessageBusCluster.<Send>b__2(Message[] msgs) in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Redis\RedisMessageBusCluster.cs:line 67
   at Microsoft.AspNet.SignalR.TaskAsyncHelper.GenericDelegates`4.<>c__DisplayClass57.<ThenWithArgs>b__56() in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Core\TaskAsyncHelper.cs:line 893
   at Microsoft.AspNet.SignalR.TaskAsyncHelper.TaskRunners`2.<>c__DisplayClass42.<RunTask>b__41(Task t) in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Core\TaskAsyncHelper.cs:line 821
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.InvalidOperationException: The queue is closed
   at BookSleeve.MessageQueue.Enqueue(RedisMessage item, Boolean highPri) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\MessageQueue.cs:line 71
   at BookSleeve.RedisConnectionBase.EnqueueMessage(RedisMessage message, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\RedisConnectionBase.cs:line 910
   at BookSleeve.RedisConnectionBase.ExecuteInt64(RedisMessage message, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\RedisConnectionBase.cs:line 826
   at BookSleeve.RedisConnection.IncrementImpl(Int32 db, String key, Int64 value, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\IStringCommands.cs:line 277
   at BookSleeve.RedisConnection.BookSleeve.IStringCommands.Increment(Int32 db, String key, Int64 value, Boolean queueJump) in c:\Projects\Frameworks\BookSleeve-1.2.0.5\BookSleeve\IStringCommands.cs:line 270
   at Microsoft.AspNet.SignalR.Redis.RedisMessageBusCluster.SendImpl(IEnumerator`1 enumerator, TaskCompletionSource`1 taskCompletionSource) in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Redis\RedisMessageBusCluster.cs:line 90
   at Microsoft.AspNet.SignalR.Redis.RedisMessageBusCluster.<Send>b__2(Message[] msgs) in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Redis\RedisMessageBusCluster.cs:line 67
   at Microsoft.AspNet.SignalR.TaskAsyncHelper.GenericDelegates`4.<>c__DisplayClass57.<ThenWithArgs>b__56() in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Core\TaskAsyncHelper.cs:line 893
   at Microsoft.AspNet.SignalR.TaskAsyncHelper.TaskRunners`2.<>c__DisplayClass42.<RunTask>b__41(Task t) in c:\Projects\Frameworks\SignalR\SignalR.1.0RC1\SignalR\src\Microsoft.AspNet.SignalR.Core\TaskAsyncHelper.cs:line 821<---



public void Enqueue(RedisMessage item, bool highPri)
{
    lock (stdPriority)
    {
        if (closed)
        {
            throw new InvalidOperationException("The queue is closed");
        }

Dove viene generata un'eccezione di coda chiusa.

Prevedo un altro problema: poiché la connessione Redis è stata effettuata, Application_Start()potrebbero esserci alcuni problemi nella "riconnessione" a un altro server. Tuttavia, penso che questo sia valido quando si usa il singolare RedisConnection(), dove c'è solo una connessione tra cui scegliere. Tuttavia, con l'introduzione di ConnectionUtils.Connect()Mi piacerebbe sentire da @dfowlero da altri ragazzi di SignalR come viene gestito questo scenario in SignalR.


Darò un'occhiata, ma: la prima cosa che accade è che non è necessario chiamare Openpoiché la connessione che hai dovrebbe essere già aperta. Non sarò in grado di guardare subito, però, poiché mi sto preparando per un volo
Marc Gravell

Credo che ci siano due problemi qui. 1) come Booksleeve sta affrontando un failover; 2) Come SignalR utilizza i cursori per tenere traccia dei client. Quando un nuovo bus di messaggi viene inizializzato, tutti i cursori da mb1 non escono da mb2. Pertanto, quando si ripristina il pool di app SignalR, inizierà a funzionare, non prima, il che ovviamente non è un'opzione praticabile.
ElHaix

2
Link che descrive come SignalR utilizza i cursori: stackoverflow.com/questions/13054592/...
ElHaix

Prova a utilizzare l'ultima versione del bus di messaggi redis. Supporta il passaggio di una factory di connessione e gestisce i nuovi tentativi di connessione quando il server si arresta.
davidfowl

Hai un link per le note di rilascio? Grazie.
ElHaix

Risposte:


13

Il team di SignalR ha ora implementato il supporto per una factory di connessione personalizzata con StackExchange.Redis , il successore di BookSleeve, che supporta connessioni Redis ridondanti tramite ConnectionMultiplexer.

Il problema iniziale riscontrato era che, nonostante la creazione dei miei metodi di estensione in BookSleeve per accettare una raccolta di server, il failover non era possibile.

Ora, con l'evoluzione di BookSleeve in StackExchange.Redis, ora possiamo configurare la raccolta di server / porte direttamente nell'inizializzazione Connect.

La nuova implementazione è molto più semplice della strada che stavo percorrendo, nella creazione di un UseRedisClustermetodo, e il pluming back-end ora supporta il vero failover:

var conn = ConnectionMultiplexer.Connect("redisServer1:6380,redisServer2:6380,redisServer3:6380,allowAdmin=true");

StackExchange.Redis consente anche una configurazione manuale aggiuntiva come descritto nella Automatic and Manual Configurationsezione della documentazione:

ConfigurationOptions config = new ConfigurationOptions
{
    EndPoints =
    {
        { "redis0", 6379 },
        { "redis1", 6380 }
    },
    CommandMap = CommandMap.Create(new HashSet<string>
    { // EXCLUDE a few commands
        "INFO", "CONFIG", "CLUSTER",
        "PING", "ECHO", "CLIENT"
    }, available: false),
    KeepAlive = 180,
    DefaultVersion = new Version(2, 8, 8),
    Password = "changeme"
};

In sostanza, la capacità di inizializzare il nostro ambiente con scalabilità orizzontale SignalR con una raccolta di server ora risolve il problema iniziale.


Devo premiare la tua risposta con una taglia di 500 ripetizioni? ;)
nicael

Bene, se credi che ora sia la risposta :)
ElHaix

@ElHaix da quando hai posto la domanda, probabilmente sei più qualificato per dire se la tua risposta è definitiva o se è solo un pezzo del puzzle - suggerisco di aggiungere una frase per indicare se e possibilmente come ha risolto il tuo problema
Lars Höppner

Così? Ricompensa? Oppure posso aspettare che attiri più attenzione.
nicael

Mi manca qualcosa o è solo in un ramo di funzionalità, non nel pacchetto nuget principale (2.1)? Inoltre, sembra che nel ramo bug-stackexchange ( github.com/SignalR/SignalR/tree/bug-stackexchange/src/… ) non ci sia ancora un modo nella classe RedisScaleoutConfiguration per fornire il proprio multiplexer.
Steve
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.