使用 BookSleeve 的 ConnectionUtils.Connect() 将 SignalR 与 Redis 消息总线故障转移结合使用

2024-01-04

我正在尝试使用 SignalR 应用程序创建 Redis 消息总线故障转移场景。

首先,我们尝试了一个简单的硬件负载平衡器故障转移,它只是监控两个 Redis 服务器。 SignalR 应用程序指向单个 HLB 端点。然后,我使一台服务器出现故障,但在不回收 SignalR 应用程序池的情况下,无法在第二台 Redis 服务器上成功获取任何消息。大概这是因为它需要向新的 Redis 消息总线发出设置命令。

从 SignalR RC1 开始,Microsoft.AspNet.SignalR.Redis.RedisMessageBus使用书套的RedisConnection()连接到单个 Redis 进行发布/订阅。

我创建了一个新类,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.

In web.config,我设置了可用服务器列表:

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

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

我添加了两个额外的方法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);
        }
    });

我尝试添加两个Task.Wait,甚至还有一个额外的Sleep()(上面未显示) - 正在等待/等等,但仍然出现错误。

重复出现的错误似乎是在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 中处理此场景。


SignalR 团队现已实现对自定义连接工厂的支持StackExchange.Redis https://github.com/StackExchange/StackExchange.Redis,BookSleeve 的后继者,它通过 ConnectionMultiplexer 支持冗余 Redis 连接。

遇到的最初问题是,尽管在 BookSleeve 中创建了我自己的扩展方法来接受服务器集合,但故障转移是不可能的。

现在,随着 BookSleeve 演变为 StackExchange.Redis,我们现在可以配置 https://github.com/StackExchange/StackExchange.Redis/blob/master/Docs/Configuration.md服务器/端口的集合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 横向扩展环境的能力现在解决了最初的问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 BookSleeve 的 ConnectionUtils.Connect() 将 SignalR 与 Redis 消息总线故障转移结合使用 的相关文章

随机推荐

  • 为什么 Visual Studio 2019 社区中我的 SSIS 工具箱为空?

    我安装了 Visual Studio 2019 Community 然后安装了数据工具 我可以打开 Integration Services 项目 但当我查看 SSIS 工具箱时 它是空的 我该如何解决 我使用的是 Visual Studi
  • 无法加载 DLL“mqrt.dll”

    我开发了一个 WCF 服务 它作为 Windows 服务托管并公开 MSMQ 端点 我在 SERVER1 上有客户端应用程序 在 SERVER2 上有 MSMQ 和 WCF 服务 当 SERVER1 ClientApp 尝试将消息推送到 S
  • 数据准备好后如何关闭Loader

    In my Ionic 2app 中 我有一个使用服务的组件 该服务使用 http GET 来获取一些数据 然后 我的组件调用该服务 当数据可用时 它会设置并呈现该数据 看起来像以下 export class FarmList implem
  • 在 Access 中导入 Excel 数据

    我的 Access 应用程序中有一个表 需要填充一堆 Excel 文件中的数据 我尝试了这段代码 DoCmd TransferSpreadsheet acImport acSpreadsheetTypeExcel8 strTable str
  • 使用 BouncyCastle 在 C# 中读取 DER 私钥

    我正在尝试使用 BouncyCastle 将 RSA 私钥读入 Net 来测试我之前加密的数据 加密数据使用公钥和 Bouncy Castle 工作正常 我还使用了与下面相同的私钥 DER 格式 在 PHP 应用程序中成功解密我的数据 但我
  • VHDL门控时钟如何避免

    我收到了避免使用门控时钟的建议 因为它可能会导致松弛和时序限制问题 但我想问一下我可以认为什么是门控时钟 例如 此代码对时钟进行门控 因为 StopCount 对它进行门控 process ModuleCLK begin if rising
  • 无法从 Windows 主机连接到 WSL2 上的本地服务器

    我有一个Python项目使用waitress在 WSL2 Ubuntu 20 上的本地主机上提供服务 我从 VSCode 远程启动服务器 但无法使用地址从 Windows 上的浏览 器连接到它http 127 0 0 1 5998 http
  • Objective-C:如何替换 HTML 实体? [复制]

    这个问题在这里已经有答案了 我从互联网获取文本 它包含 html 实体 即 oacute 我想将此文本显示到自定义 iPhone 单元格中 我尝试在自定义单元格中使用 UIWebView 但我更喜欢使用多行 UILabel 问题是我找不到任
  • 如何让 favicon.ico 在龙卷风上工作

    龙卷风服务器默认不执行 favicon ico 所以我总是得到这样的信息 W 130626 10 38 16 web 1514 404 GET favicon ico 192 168 1 57 0 57ms 我以各种方式使用 web sta
  • 是否有用于 Java 或 PHP 的 OData 服务器库来公开 OData?

    我想知道是否有或为什么没有适用于 Java 的 ADO NET 数据服务服务器库 我需要从 Java 服务器公开数据库 但我只看到 Microsoft 为 java 提供客户端 而不是服务器部分 当您需要 NET Windows 来公开它时
  • CSS :before 和 :first-child 组合

    我使用以下代码在菜单项之间添加分隔符 navigation center li before content color fff 现在我希望第一个项目前面没有分隔符 所以我想出了以下代码 navigation center li befor
  • 在 Python 中递归地重新加载包(及其子模块)

    在 Python 中 您可以按如下方式重新加载模块 import foobar import importlib importlib reload foobar 这适用于 py文件 但对于 Python 包 它只会重新加载包并not任何嵌套
  • Angular HttpClient:“Blob”类型上不存在属性“headers”[重复]

    这个问题在这里已经有答案了 我正在使用 Angular 5 这是我从服务器下载文件的代码 1 服务 export url return this http get url responseType blob 2 组件代码 public do
  • ios6如何播放视频

    我很困惑 MPMoviePlayerViewController 和 MPMoviePlayerController 在 ios6 中本地播放视频的最佳方式是什么 这是我的代码 NSURL url NSURL fileURLWithPath
  • 导航回屏幕时不会调用 useEffect - React Navigation

    我有一个屏幕 可以调用 api 来获取一些数据 然后显示 我看到的一个问题是 当我离开屏幕 我使用的是 React navigation 6 x 然后返回到屏幕时useEffect 没有被调用 从我到目前为止读到的内容来看 这取决于user
  • 如何从进程名获取进程id?

    我正在尝试创建一个 shell 脚本来获取进程号我的 Mac 上的 Skype 应用程序 ps clx grep Skype grep Skype awk print 2 头 1 上面的方法工作正常 但是有两个问题 1 The grep如果
  • 如何获取其他应用的日志?

    我想从其他应用程序读取日志并过滤它们 以便当记录某个关键字时 我的应用程序将执行特定任务 我找到了几种读取日志的方法 但从我的测试来看 我只能获取我的应用程序日志 这是我最初尝试使用的方法 try Process process Runti
  • 如何知道 postNotificationName:object:userInfo 崩溃的位置

    有什么方法可以知道 Xcode 4 6 中的崩溃原因吗 The crash stack is Exception Type SIGSEGV Exception Codes SEGV ACCERR at 0xd9f2c061 Crashed
  • 使用 WebSockets...高效吗?

    我几乎阅读了所有关于 WebSockets 的指南和教程 但没有一个涵盖如何有效地使用它们 有人有关于如何执行此操作的任何指南吗 我担心单个连接可以占用的带宽量 特别是当应用程序打开数百个连接时 WebSocket 的效率取决于处理它们的
  • 使用 BookSleeve 的 ConnectionUtils.Connect() 将 SignalR 与 Redis 消息总线故障转移结合使用

    我正在尝试使用 SignalR 应用程序创建 Redis 消息总线故障转移场景 首先 我们尝试了一个简单的硬件负载平衡器故障转移 它只是监控两个 Redis 服务器 SignalR 应用程序指向单个 HLB 端点 然后 我使一台服务器出现故