การใช้ SignalR กับ Redis messagebus failover โดยใช้ ConnectionUtils.Connect () ของ BookSleeve


112

ฉันกำลังพยายามสร้างสถานการณ์ล้มเหลวของบัสข้อความ Redis ด้วยแอป SignalR

ในตอนแรกเราลองใช้ความล้มเหลวของตัวโหลดบาลานซ์ฮาร์ดแวร์แบบง่ายๆซึ่งตรวจสอบเซิร์ฟเวอร์ Redis สองตัว แอปพลิเคชัน SignalR ชี้ไปที่จุดสิ้นสุด HLB เอกพจน์ จากนั้นฉันล้มเหลวหนึ่งเซิร์ฟเวอร์ แต่ไม่สามารถรับข้อความใด ๆ ผ่านเซิร์ฟเวอร์ Redis ที่สองได้สำเร็จโดยไม่ต้องรีไซเคิลพูลแอป SignalR สันนิษฐานว่าเป็นเพราะจำเป็นต้องออกคำสั่งการตั้งค่าให้กับบัสข้อความ Redis ใหม่

สำหรับ SignalR RC1 Microsoft.AspNet.SignalR.Redis.RedisMessageBusใช้ Booksleeve RedisConnection()เพื่อเชื่อมต่อกับ Redis เดียวสำหรับ pub / sub

ฉันสร้างคลาสใหม่RedisMessageBusCluster()ที่ใช้ Booksleeve ConnectionUtils.Connect()เพื่อเชื่อมต่อกับคลาสในคลัสเตอร์ของเซิร์ฟเวอร์ 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 SignalR.Chatมีกลไกของตัวเองสำหรับการกำหนดต้นแบบและจะล้มเหลวโดยอัตโนมัติไปยังเซิร์ฟเวอร์อื่นและตอนนี้กำลังทดสอบนี้กับ

ในweb.configฉันตั้งค่ารายการเซิร์ฟเวอร์ที่มี:

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

จากนั้นใน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);

ฉันได้เพิ่มสองวิธีเพิ่มเติมใน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;
}

ตอนนี้ปัญหาคือเมื่อฉันเปิดใช้งานเบรกพอยต์หลายจุดจนกระทั่งหลังจากเพิ่มชื่อผู้ใช้แล้วปิดใช้งานเบรกพอยต์ทั้งหมดแอปพลิเคชันจะทำงานตามที่คาดไว้ อย่างไรก็ตามเนื่องจากเบรกพอยต์ถูกปิดใช้งานตั้งแต่เริ่มต้นดูเหมือนว่าจะมีเงื่อนไขการแข่งขันบางอย่างที่อาจล้มเหลวในระหว่างกระบวนการเชื่อมต่อ

ดังนั้นใน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);
        }
    });

ฉันลองเพิ่มทั้ง a Task.Waitและแม้แต่ส่วนเพิ่มเติมSleep()(ไม่ได้แสดงไว้ด้านบน) - ซึ่งกำลังรอ / etc แต่ยังคงได้รับข้อผิดพลาด

ข้อผิดพลาดที่เกิดซ้ำดูเหมือนจะอยู่ใน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");
        }

ในกรณีที่มีการโยนข้อยกเว้นคิวปิด

ฉันคาดการณ์ปัญหาอื่น: เนื่องจากมีการเชื่อมต่อ Redis Application_Start()อาจมีปัญหาบางอย่างในการ "เชื่อมต่อใหม่" ไปยังเซิร์ฟเวอร์อื่น อย่างไรก็ตามฉันคิดว่าสิ่งนี้ใช้ได้เมื่อใช้เอกพจน์RedisConnection()ซึ่งมีเพียงการเชื่อมต่อเดียวให้เลือก อย่างไรก็ตามด้วยการแนะนำของConnectionUtils.Connect()ฉันต้องการได้ยินจาก@dfowlerหรือคน SignalR คนอื่น ๆ เกี่ยวกับวิธีการจัดการสถานการณ์นี้ใน SignalR


ฉันจะดู แต่: สิ่งแรกที่เกิดขึ้นคือคุณไม่จำเป็นต้องโทรOpenเนื่องจากการเชื่อมต่อที่คุณมีควรจะเปิดอยู่แล้ว แม้ว่าฉันจะไม่สามารถมองเห็นได้ในทันทีเพราะฉันกำลังเตรียมพร้อมสำหรับเที่ยวบิน
Marc Gravell

ฉันเชื่อว่ามีสองประเด็นที่นี่ 1) วิธีที่ Booksleeve จัดการกับความล้มเหลว 2) SignalR ใช้เคอร์เซอร์เพื่อติดตามลูกค้าอย่างไร เมื่อเริ่มต้นบัสข้อความใหม่เคอร์เซอร์ทั้งหมดจาก mb1 จะไม่ออกจาก mb2 ดังนั้นเมื่อรีเซ็ตพูลแอป SignalR มันจะเริ่มทำงาน - ไม่ใช่ก่อนหน้านี้ซึ่งเห็นได้ชัดว่าไม่ใช่ตัวเลือกที่ทำงานได้
ElHaix

2
ลิงก์อธิบายว่า SignalR ใช้เคอร์เซอร์อย่างไร: stackoverflow.com/questions/13054592/…
ElHaix

ลองใช้ redis message bus เวอร์ชันล่าสุด รองรับการส่งผ่านในโรงงานการเชื่อมต่อและจัดการการลองใหม่เพื่อเชื่อมต่อเมื่อเซิร์ฟเวอร์หยุดทำงาน
davidfowl

คุณมีลิงก์สำหรับบันทึกประจำรุ่นหรือไม่ ขอบคุณ
ElHaix

คำตอบ:


13

ขณะนี้ทีม SignalR ได้ใช้การสนับสนุนสำหรับโรงงานเชื่อมต่อแบบกำหนดเองด้วยStackExchange.Redisซึ่งเป็นตัวต่อจาก BookSleeve ซึ่งรองรับการเชื่อมต่อ Redis ซ้ำซ้อนผ่าน ConnectionMultiplexer

ปัญหาเริ่มต้นที่พบคือแม้ว่าฉันจะสร้างวิธีการขยายของฉันเองใน BookSleeve เพื่อยอมรับการรวบรวมเซิร์ฟเวอร์ แต่ก็ไม่สามารถเกิดข้อผิดพลาดได้

ตอนนี้ด้วยวิวัฒนาการของ BookSleeve เป็น StackExchange.Redis ตอนนี้เราสามารถกำหนดค่าการรวบรวมเซิร์ฟเวอร์ / พอร์ตได้ทันทีในการConnectเริ่มต้น

การใช้งานใหม่นั้นง่ายกว่าถนนที่ฉันกำลังลงไปในการสร้างUseRedisClusterวิธีการและตอนนี้การลดลงของส่วนหลังรองรับความล้มเหลวที่แท้จริง:

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

StackExchange.Redis ยังอนุญาตให้มีการกำหนดค่าด้วยตนเองเพิ่มเติมตามที่ระบุไว้ในAutomatic and Manual Configurationส่วนของเอกสารประกอบ:

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"
};

โดยพื้นฐานแล้วความสามารถในการเริ่มต้นสภาพแวดล้อม SignalR scale-out ของเราด้วยชุดเซิร์ฟเวอร์ช่วยแก้ปัญหาเบื้องต้นได้แล้ว


ฉันควรให้รางวัลคำตอบของคุณเป็นค่าตอบแทน 500 ครั้งหรือไม่? ;)
nicael

ถ้าคุณเชื่อว่าตอนนี้คือคำตอบ :)
ElHaix

@ElHaix ตั้งแต่คุณถามคำถามคุณอาจมีคุณสมบัติมากที่สุดที่จะบอกว่าคำตอบของคุณเป็นข้อสรุปหรือไม่หรือเป็นเพียงแค่ส่วนหนึ่งในปริศนา - ฉันขอแนะนำให้เพิ่มประโยคเพื่อระบุว่าจะแก้ปัญหาของคุณได้หรือไม่
Lars Höppner

ดังนั้น? รางวัลค่าหัว? หรือรอจนกว่ามันจะดึงดูดความสนใจมากขึ้น
nicael

ฉันทำอะไรหายไปหรือเป็นเฉพาะในสาขาฟีเจอร์ไม่ใช่ในแพ็กเกจ nuget หลัก (2.1) นอกจากนี้ดูเหมือนว่าในสาขา bug-stackexchange ( github.com/SignalR/SignalR/tree/bug-stackexchange/src/… ) ยังไม่มีวิธีใดในคลาส RedisScaleoutConfiguration ในการจัดเตรียมมัลติเพล็กเซอร์ของคุณเอง
สตีฟ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.