LoginSignup
1
1

ASP.NET Core SignalR × Redis でHubを冗長化

Posted at

初めに

ASP.NET Core SignalR の冗長化について調査する。

[参考]ASP.NET Core SignalR 概要
https://learn.microsoft.com/ja-jp/aspnet/core/signalr/introduction

課題

ネットワーク構成例は下図の通り。(HubServer = Hubの機能を有するサーバー)
HubServerが停止したらシステムは成り立たなくなる。HubServerを冗長化したい。
image.png

答え:SignalRのバックプレーンに Redis を使用する

調査したところ、SignalRのバックプレーンに Redis を使用することでHubServerを冗長化ができそうなことが分かった。

[参考]ASP.NET Core SignalR のホスティングとスケーリング
https://learn.microsoft.com/ja-jp/aspnet/core/signalr/scale

見直し後のネットワーク構成

見直し後のネットワークは下図の通りとなる。
HubServerを2台構成にし、バックプレーンにRedisを接続する。
HubServer - Redis間はRESP通信が行われるっぽい?
(なお、Client - HubServer間で処理分散化を行いたいのであればロードバランサー等を導入する必要があるが、ここでは割愛)
image.png

この構成にすれば、どちらかのHubServerが停止してもHubは機能し続ける。らしい。
image.png

試してみる

ということでテスト環境を作って試してみる。

テスト環境におけるネットワーク構成は下図の通り。
HubServerのOSは最小インストールしたAlmaLinux 9.2を使用。
Clientは自PCのWindows 11を使用。
image.png

テストコード

Clientは1秒毎に"Hello."と自分のプロセスIDをHubServerにメッセージ送信する。
HubServerは受信したメッセージをClientにオウム返しする。
だけの単純なコード。

Client用テストコード

プロジェクトテンプレート:コンソール アプリ (.NET 6)
プロジェクト名:SignalRClientApplication
nuget追加:Microsoft.AspNetCore.SignalR.Client (6.0.22)

Program.cs
using Microsoft.AspNetCore.SignalR.Client;

namespace SignalRClientApplication
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            while (true)
            {
                try
                {
                    // 接続先URL入力
                    WriteLine("Input HubServer URL : ", ConsoleColor.Yellow);
                    var url = Console.ReadLine();
                    if (string.IsNullOrEmpty(url)) continue;

                    // 接続情報作成
                    WriteLine($"HubServer Connecting...");
                    var connection = new HubConnectionBuilder()
                        .WithUrl(url)
                        .Build();

                    // 接続開始
                    await connection.StartAsync();
                    WriteLine($"HubServer Connected!");

                    // Clientのメソッド登録
                    connection.On<string, string>("ClientMethod", (user, message) =>
                    {
                        WriteLine($"Recv ClientMethod : {message} from {user}{(user == Environment.ProcessId.ToString() ? "(my)" : "(other)")}", ConsoleColor.Blue);
                    });

                    // 接続中
                    while (true)
                    {
                        if (connection.State != HubConnectionState.Connected)
                        {
                            WriteLine($"HubServer DisConnected {url}");
                            break; // 再接続へ
                        }

                        // Hubのメソッド呼び出し
                        await connection.InvokeAsync("HubMethod", Environment.ProcessId.ToString(), "Hello.");

                        await Task.Delay(1000);
                    }
                }
                catch (Exception ex)
                {
                    WriteLine(ex.Message, ConsoleColor.Red);
                }
            };
        }

        // コンソール画面に色分け表示して表示
        private static readonly object lockobj = new();
        private static void WriteLine(string value, ConsoleColor ForegroundColor = ConsoleColor.Gray)
        {
            lock (lockobj)
            {
                Console.ForegroundColor = ForegroundColor;
                Console.WriteLine(value);
                Console.ResetColor();
            }
        }
    }
}

HubServer用テストコード

プロジェクトテンプレート:ASP.NET Core Web アプリ (.NET 6)
プロジェクト名:SignalRHubServerWebApplication
nuget追加:Microsoft.AspNetCore.SignalR.StackExchangeRedis (6.0.22)

Program.cs
using StackExchange.Redis;

namespace SignalRHubServerWebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddRazorPages();
            builder.Services.AddSignalR().AddStackExchangeRedis(configure =>
            {
                configure.Configuration = new ConfigurationOptions
                {
                    ChannelPrefix = "MyApp",
                    EndPoints =
                    {
                        { "192.168.56.106", 6379 } // Redis
                    },
                    AbortOnConnectFail = false,
                };
            });

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            //app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapRazorPages();
            app.MapHub<HubServer>("/hub");

            app.Run("http://*:5000");
        }
    }
}
HubServer.cs
using Microsoft.AspNetCore.SignalR;

namespace SignalRHubServerWebApplication
{
    public class HubServer : Hub
    {
        public async Task HubMethod(string user, string message)
        {
            // Clientのメソッド呼び出し
            await Clients.All.SendAsync("ClientMethod", user, message);
        }
    }
}

Redis構築

さらっとメモ書き程度。

# テストには邪魔なファイアウォール停止
> sudo systemctl stop firewalld
> sudo systemctl disable firewalld

# Redisインストール
> sudo dnf install redis
全てy

# localhostのみ接続許可になっているので bind から始まる行を見つけて変更する
> sudo vi /etc/redis/redis.conf
# bind 127.0.0.1 -::1
bind * -::*

> sudo systemctl start redis

HubServer構築

2台用意する。

# テストには邪魔なファイアウォール停止
> sudo systemctl stop firewalld
> sudo systemctl disable firewalld

# .NET 6ランタイム
> sudo dnf install aspnetcore-runtime-6.0
全てy

# HostServer用テストコードをpublishしてリモートにコピー、実行
> dotnet SignalRHubServerWebApplication.dll
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /root/publish/

Client構築

Clientは自PCを使う。

テスト

下図の通り、ClientAをHubServerA、ClientBをHubServerBに接続する。
image.png

Client A
> dotnet SignalRClientApplication.dll
Input HubServer URL [e.g. http://localhost:5005/hub] :
http://192.168.56.107:5000/hub
HubServer Connecting...
HubServer Connected!
Client B
> dotnet SignalRClientApplication.dll
Input HubServer URL [e.g. http://localhost:5005/hub] :
http://192.168.56.108:5000/hub
HubServer Connecting...
HubServer Connected!

接続できた。

Client A
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
:
Client B
Recv ClientMethod : Hello. from 23380(my)
Recv ClientMethod : Hello. from 1816(other)
Recv ClientMethod : Hello. from 23380(my)
Recv ClientMethod : Hello. from 1816(other)
:

全クライアントに"Hello."が伝播しているのが分かる。

HubServer Bを停止してみる。
image.png

Client BはHubServer Bの停止を検知して切断した。

Client B
HubServer DisConnected http://192.168.56.108:5000/hub
Input HubServer URL :

Client Aは問題なく稼働しているが、Client Bからのメッセージは受信しなくなった。

Client A
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 1816(my)
:

Client BをHubServer Aに接続する。
image.png

Client Aは再びClient Bからのメッセージを受信する。

Client A
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
:

Client BもClient Aからのメッセージを受信する。

Client B
Input HubServer URL :
http://192.168.56.107:5000/hub
HubServer Connecting...
HubServer Connected!
Recv ClientMethod : Hello. from 23380(my)
Recv ClientMethod : Hello. from 1816(other)
:

HubServer Bを復活させ、Client CをHubServer Bに接続する。
image.png

Client AはClient Cからのメッセージも受信する。

Client A
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
Recv ClientMethod : Hello. from 24432(other)
:

Client BはClient Cからのメッセージも受信する。

Client B
Recv ClientMethod : Hello. from 23380(my)
Recv ClientMethod : Hello. from 24432(other)
Recv ClientMethod : Hello. from 1816(other)
:

Client CはClient A/Bからのメッセージを受信する。

Client C
Input HubServer URL :
http://192.168.56.108:5000/hub
HubServer Connecting...
HubServer Connected!
Recv ClientMethod : Hello. from 24432(my)
Recv ClientMethod : Hello. from 1816(other)
Recv ClientMethod : Hello. from 23380(other)
:

HubServerの冗長化が上手く動作していることが確認できた。

Redisの冗長化

HubServerの冗長化は出来たが、Redisの冗長化は出来ていないのでRedisが停止したらシステムは停止してしまう。
image.png

が、しかし本記事ではRedisの冗長化については扱わない。
Google検索すれば有益な情報が見つかるので…。

Redisの冗長化
https://www.sraoss.co.jp/tech-blog/redis/redis-ha/

Redisを冗長化構成にする(フェールオーバー環境を構築するーその2)
https://qiita.com/KurosawaTsuyoshi/items/936c32bb8947fb907ef5

AWSを使用しているならば、AWS ElastiCache for RedisやAmazon MemoryDB for Redis
Azureを使用しているならば、Azure SignalR Service
を使うほうが簡単に冗長化できる。

以上。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1