初めに
ASP.NET Core SignalR の冗長化について調査する。
[参考]ASP.NET Core SignalR 概要
https://learn.microsoft.com/ja-jp/aspnet/core/signalr/introduction
課題
ネットワーク構成例は下図の通り。(HubServer = Hubの機能を有するサーバー)
HubServerが停止したらシステムは成り立たなくなる。HubServerを冗長化したい。
答え: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間で処理分散化を行いたいのであればロードバランサー等を導入する必要があるが、ここでは割愛)
この構成にすれば、どちらかのHubServerが停止してもHubは機能し続ける。らしい。
試してみる
ということでテスト環境を作って試してみる。
テスト環境におけるネットワーク構成は下図の通り。
HubServerのOSは最小インストールしたAlmaLinux 9.2を使用。
Clientは自PCのWindows 11を使用。
テストコード
Clientは1秒毎に"Hello."と自分のプロセスIDをHubServerにメッセージ送信する。
HubServerは受信したメッセージをClientにオウム返しする。
だけの単純なコード。
Client用テストコード
プロジェクトテンプレート:コンソール アプリ (.NET 6)
プロジェクト名:SignalRClientApplication
nuget追加:Microsoft.AspNetCore.SignalR.Client (6.0.22)
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)
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");
}
}
}
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に接続する。
> dotnet SignalRClientApplication.dll
Input HubServer URL [e.g. http://localhost:5005/hub] :
http://192.168.56.107:5000/hub
HubServer Connecting...
HubServer Connected!
> dotnet SignalRClientApplication.dll
Input HubServer URL [e.g. http://localhost:5005/hub] :
http://192.168.56.108:5000/hub
HubServer Connecting...
HubServer Connected!
接続できた。
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
:
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."が伝播しているのが分かる。
Client BはHubServer Bの停止を検知して切断した。
HubServer DisConnected http://192.168.56.108:5000/hub
Input HubServer URL :
Client Aは問題なく稼働しているが、Client Bからのメッセージは受信しなくなった。
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 Aは再びClient Bからのメッセージを受信する。
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からのメッセージを受信する。
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に接続する。
Client AはClient Cからのメッセージも受信する。
Recv ClientMethod : Hello. from 1816(my)
Recv ClientMethod : Hello. from 23380(other)
Recv ClientMethod : Hello. from 24432(other)
:
Client BはClient Cからのメッセージも受信する。
Recv ClientMethod : Hello. from 23380(my)
Recv ClientMethod : Hello. from 24432(other)
Recv ClientMethod : Hello. from 1816(other)
:
Client CはClient A/Bからのメッセージを受信する。
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が停止したらシステムは停止してしまう。
が、しかし本記事では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
を使うほうが簡単に冗長化できる。
以上。