使用多个实例扩展 SignalR 服务

SignalR 服务 SDK 支持对 SignalR 服务实例使用多个终结点。 可以使用此功能来扩展并发连接,或将其用于跨区域的消息传送。

对于 ASP.NET Core

通过配置添加多个终结点

使用 SignalR 服务连接字符串的密钥 Azure:SignalR:ConnectionStringAzure:SignalR:ConnectionString: 进行配置。

如果密钥以 Azure:SignalR:ConnectionString:开头,则它应采用 Azure:SignalR:ConnectionString:{Name}:{EndpointType} 格式,其中,NameEndpointTypeServiceEndpoint 对象的属性(可从代码访问)。

可以使用以下 dotnet 命令添加多个实例连接字符串:

dotnet user-secrets set Azure:SignalR:ConnectionString:east-region-a <ConnectionString1>
dotnet user-secrets set Azure:SignalR:ConnectionString:east-region-b:primary <ConnectionString2>
dotnet user-secrets set Azure:SignalR:ConnectionString:backup:secondary <ConnectionString3>

通过代码添加多个终结点

ServicEndpoint 类描述 Azure SignalR 服务终结点的属性。 使用 Azure SignalR 服务 SDK 时,可通过以下代码配置多个实例终结点:

services.AddSignalR()
        .AddAzureSignalR(options => 
        {
            options.Endpoints = new ServiceEndpoint[]
            {
                // Note: this is just a demonstration of how to set options.Endpoints
                // Having ConnectionStrings explicitly set inside the code is not encouraged
                // You can fetch it from a safe place such as Azure KeyVault
                new ServiceEndpoint("<ConnectionString0>"),
                new ServiceEndpoint("<ConnectionString1>", type: EndpointType.Primary, name: "east-region-a"),
                new ServiceEndpoint("<ConnectionString2>", type: EndpointType.Primary, name: "east-region-b"),
                new ServiceEndpoint("<ConnectionString3>", type: EndpointType.Secondary, name: "backup"),
            };
        });

自定义终结点路由器

默认情况下,SDK 使用 DefaultEndpointRouter 来选取终结点。

默认行为

  1. 客户端请求路由:

    当客户端通过 /negotiate 与应用服务器协商时, SDK 默认会从可用服务终结点集内随机选择一个终结点。

  2. 服务器消息路由:

    向特定的连接发送消息时,如果目标连接路由到当前服务器,则消息将直接转到这个已连接的终结点。 否则,消息将广播到每个 Azure SignalR 终结点。

自定义路由算法

如果你具备专业的知识,可以识别消息会转到哪些终结点,则可以创建自己的路由器。

以下示例定义一个自定义路由器,该路由器使用一个以 east- 开头的组将消息路由到名为 east 的终结点:

private class CustomRouter : EndpointRouterDecorator
{
    public override IEnumerable<ServiceEndpoint> GetEndpointsForGroup(string groupName, IEnumerable<ServiceEndpoint> endpoints)
    {
        // Override the group broadcast behavior, if the group name starts with "east-", only send messages to endpoints inside east
        if (groupName.StartsWith("east-"))
        {
            return endpoints.Where(e => e.Name.StartsWith("east-"));
        }

        return base.GetEndpointsForGroup(groupName, endpoints);
    }
}

以下示例替代默认协商行为,并根据应用服务器的位置选择终结点。

private class CustomRouter : EndpointRouterDecorator
{    public override ServiceEndpoint GetNegotiateEndpoint(HttpContext context, IEnumerable<ServiceEndpoint> endpoints)
    {
        // Override the negotiate behavior to get the endpoint from query string
        var endpointName = context.Request.Query["endpoint"];
        if (endpointName.Count == 0)
        {
            context.Response.StatusCode = 400;
            var response = Encoding.UTF8.GetBytes("Invalid request");
            context.Response.Body.Write(response, 0, response.Length);
            return null;
        }

        return endpoints.FirstOrDefault(s => s.Name == endpointName && s.Online) // Get the endpoint with name matching the incoming request
               ?? base.GetNegotiateEndpoint(context, endpoints); // Or fallback to the default behavior to randomly select one from primary endpoints, or fallback to secondary when no primary ones are online
    }
}

请不要忘记使用以下代码将路由器注册到 DI 容器:

services.AddSingleton(typeof(IEndpointRouter), typeof(CustomRouter));
services.AddSignalR()
        .AddAzureSignalR(
            options => 
            {
                options.Endpoints = new ServiceEndpoint[]
                {
                    new ServiceEndpoint(name: "east", connectionString: "<connectionString1>"),
                    new ServiceEndpoint(name: "west", connectionString: "<connectionString2>"),
                    new ServiceEndpoint("<connectionString3>")
                };
            });

对于 ASP.NET

通过配置添加多个终结点

使用 SignalR 服务连接字符串的密钥 Azure:SignalR:ConnectionStringAzure:SignalR:ConnectionString: 进行配置。

如果密钥以 Azure:SignalR:ConnectionString:开头,则它应采用 Azure:SignalR:ConnectionString:{Name}:{EndpointType} 格式,其中,NameEndpointTypeServiceEndpoint 对象的属性(可从代码访问)。

可将多个实例连接字符串添加到 web.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="Azure:SignalR:ConnectionString" connectionString="<ConnectionString1>"/>
    <add name="Azure:SignalR:ConnectionString:en-us" connectionString="<ConnectionString2>"/>
    <add name="Azure:SignalR:ConnectionString:zh-cn:secondary" connectionString="<ConnectionString3>"/>
    <add name="Azure:SignalR:ConnectionString:Backup:secondary" connectionString="<ConnectionString4>"/>
  </connectionStrings>
  ...
</configuration>

通过代码添加多个终结点

ServiceEndpoint 类描述 Azure SignalR 服务终结点的属性。 使用 Azure SignalR 服务 SDK 时,可通过以下代码配置多个实例终结点:

app.MapAzureSignalR(
    this.GetType().FullName, 
    options => {
            options.Endpoints = new ServiceEndpoint[]
            {
                // Note: this is just a demonstration of how to set options. Endpoints
                // Having ConnectionStrings explicitly set inside the code is not encouraged.
                // You can fetch it from a safe place such as Azure KeyVault
                new ServiceEndpoint("<ConnectionString1>"),
                new ServiceEndpoint("<ConnectionString2>"),
                new ServiceEndpoint("<ConnectionString3>"),
            }
        });

自定义路由器

ASP.NET SignalR 与 ASP.NET Core SignalR 之间的唯一差别在于 GetNegotiateEndpoint 的 HTTP 上下文类型。 ASP.NET SignalR 的 HTTP 上下文类型为 IOwinContext

以下代码是 ASP.NET SignalR 的自定义协商示例:

private class CustomRouter : EndpointRouterDecorator
{
    public override ServiceEndpoint GetNegotiateEndpoint(IOwinContext context, IEnumerable<ServiceEndpoint> endpoints)
    {
        // Override the negotiate behavior to get the endpoint from query string
        var endpointName = context.Request.Query["endpoint"];
        if (string.IsNullOrEmpty(endpointName))
        {
            context.Response.StatusCode = 400;
            context.Response.Write("Invalid request.");
            return null;
        }

        return endpoints.FirstOrDefault(s => s.Name == endpointName && s.Online) // Get the endpoint with name matching the incoming request
               ?? base.GetNegotiateEndpoint(context, endpoints); // Or fallback to the default behavior to randomly select one from primary endpoints, or fallback to secondary when no primary ones are online
    }
}

请不要忘记使用以下代码将路由器注册到 DI 容器:

var hub = new HubConfiguration();
var router = new CustomRouter();
hub.Resolver.Register(typeof(IEndpointRouter), () => router);
app.MapAzureSignalR(GetType().FullName, hub, options => {
    options.Endpoints = new ServiceEndpoint[]
                {
                    new ServiceEndpoint(name: "east", connectionString: "<connectionString1>"),
                    new ServiceEndpoint(name: "west", connectionString: "<connectionString2>"),
                    new ServiceEndpoint("<connectionString3>")
                };
});

服务终结点指标

为了启用高级路由器,SignalR 服务器 SDK 提供了多个指标来帮助服务器做出明智的决策。 属性位于 ServiceEndpoint.EndpointMetrics 下。

标准名称 说明
ClientConnectionCount 服务终结点的所有中心上的并发客户端连接总数
ServerConnectionCount 服务终结点的所有中心上的并发服务器连接总数
ConnectionCapacity 服务终结点的连接数总配额,包括客户端和服务器连接数

以下代码是根据 ClientConnectionCount 自定义路由器的示例。

private class CustomRouter : EndpointRouterDecorator
{
    public override ServiceEndpoint GetNegotiateEndpoint(HttpContext context, IEnumerable<ServiceEndpoint> endpoints)
    {
        return endpoints.OrderBy(x => x.EndpointMetrics.ClientConnectionCount).FirstOrDefault(x => x.Online) // Get the available endpoint with minimal clients load
               ?? base.GetNegotiateEndpoint(context, endpoints); // Or fallback to the default behavior to randomly select one from primary endpoints, or fallback to secondary when no primary ones are online
    }
}

动态缩放 ServiceEndpoint

从 SDK 版本 1.5.0 开始,我们首先为 ASP.NET Core 版本启用动态缩放 ServiceEndpoint。 因此,当你需要添加/删除 ServiceEndpoint 时,不必要重启应用服务器。 由于 ASP.NET Core 原生就支持默认配置(例如包含 reloadOnChange: trueappsettings.json),因此你无需更改代码。 若要添加某种自定义配置并使用热重载,请参阅 ASP.NET Core 中的配置

注意

考虑到在服务器/服务与客户端/服务之间设置连接所需的时间可能不同,为了确保在缩放过程中不丢失消息,我们将提供一段过渡期,以便等待服务器连接准备就绪,然后向客户端开放新的 ServiceEndpoint。 此过程通常只需几秒钟即可完成,完成后你可以看到类似于 Succeed in adding endpoint: '{endpoint}' 的日志消息。

在某些预期情况下(例如跨区域网络问题,或者不同应用服务器上出现配置不一致情况),过渡期的操作无法正常完成。 在这种情况下,如果你发现缩放过程无法正常进行,建议重启应用服务器。

缩放的默认超时期限为 5 分钟,可以通过更改 ServiceOptions.ServiceScaleTimeout 中的值来自定义超时。 如果你有大量的应用服务器,建议将该值稍微增大一点。

跨区域方案中的配置

ServiceEndpoint 对象包含值为 primarysecondaryEndpointType 属性。

主要终结点是接收客户端流量的首选终结点,因为它们的网络连接更可靠。 辅助终结点的网络连接不太可靠,仅用于处理服务器到客户端的流量。 例如,辅助终结点用于广播消息,而不用于处理客户端到服务器的流量。

在跨区域案例中,网络可能不稳定。 对于位于“中国东部”的某个应用服务器,同样位于“中国东部”区域的 SignalR 服务终结点为 primary,其他区域中的终结点将标记为 secondary。 在此配置中,其他区域中的服务终结点可以接收来自此“中国东部”应用服务器的消息,但不会将任何跨区域客户端路由到此应用服务器。 下图说明了此体系结构:

Cross-Geo Infra

当客户端尝试使用默认路由器通过 /negotiate 来与应用服务器协商时,SDK 会从可用的 primary 终结点集内随机选择一个终结点。 当主要终结点不可用时,SDK 会从所有可用的 secondary 终结点中随机选择。 当服务器与服务终结点之间的连接处于活动状态时,终结点将标记为可用

在跨区域方案中,如果客户端尝试通过 /negotiate 来与“中国东部”的应用服务器协商,则默认情况下,始终会返回位于同一区域中的 primary 终结点。 当“中国东部”的所有终结点都不可用时,路由器会将客户端重定向到其他区域中的终结点。 以下故障转移部分详细介绍了该方案。

Normal Negotiate

故障转移

当没有可用的 primary 终结点时,客户端的 /negotiate 将从可用的 secondary 终结点中进行选择。 此故障转移机制要求每个终结点充当至少一个应用服务器的 primary 终结点。

Diagram showing the Failover mechanism process.

后续步骤

可以在高可用性和灾难恢复方案中使用多个终结点。