你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

使用多个实例扩展 SignalR 服务

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

重要

本文中出现的原始连接字符串仅用于演示目的。

连接字符串包括应用程序访问 Azure SignalR 服务所需的授权信息。 连接字符串中的访问密钥类似于服务的根密码。 在生产环境中,请始终保护访问密钥。 使用 Azure 密钥保管库安全地管理和轮换密钥,使用 Microsoft Entra ID 保护连接字符串,并使用 Microsoft Entra ID 授权访问

避免将访问密钥分发给其他用户、对其进行硬编码或将其以纯文本形式保存在其他人可以访问的任何位置。 如果你认为访问密钥可能已泄露,请轮换密钥。

对于 ASP.NET Core

通过配置添加多个终结点

本文中出现的原始连接字符串仅用于演示目的。 在生产环境中,请始终保护访问密钥。 使用 Azure Key Vault 安全地管理和轮换密钥,使用 Microsoft Entra ID 保护连接字符串,并使用 Microsoft Entra ID 授权访问

使用 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>

通过代码添加多个终结点

ServiceEndpoint 类描述 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)
    {
          // Sample code showing how to choose endpoints based on the incoming request endpoint query
          var endpointName = context.Request.Query["endpoint"].FirstOrDefault() ?? "";
          // Select from the available endpoints, don't construct a new ServiceEndpoint object here
          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>")
                };
            });

ServiceOptions.Endpoints 还支持热重载。 以下示例代码演示了如何从一个配置部分加载连接字符串,以及如何从另一个配置部分加载反向代理公开的公共 URL。只要配置支持热重载,终结点就可以即时更新。

services.Configure<ServiceOptions>(o =>
{
        o.Endpoints = [
            new ServiceEndpoint(Configuration["ConnectionStrings:AzureSignalR:East"], name: "east")
            {
                ClientEndpoint = new Uri(Configuration.GetValue<string>("PublicClientEndpoints:East"))
            },
            new ServiceEndpoint(Configuration["ConnectionStrings:AzureSignalR:West"], name: "west")
            {
                ClientEndpoint = new Uri(Configuration.GetValue<string>("PublicClientEndpoints:West"))
            },
        ];
});

对于 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)
    {
        // Sample code showing how to choose endpoints based on the incoming request endpoint query
        var endpointName = context.Request.Query["endpoint"] ?? "";
        // Select from the available endpoints, don't construct a new ServiceEndpoint object here
        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 中的值来自定义超时。 如果你有大量的应用服务器,建议将该值稍微增大一点。

注意

目前,仅 Persistent 传输类型支持多终结点功能。

对于 SignalR 函数扩展

配置

要启用多个 SignalR 服务实例,你应:

  1. 使用 Persistent 传输类型。

    默认传输类型为 Transient 模式。 应将以下条目添加到 local.settings.json 文件或 Azure 上的应用程序设置。

    {
        "AzureSignalRServiceTransportType":"Persistent"
    }
    

    注意

    Transient 模式切换到 Persistent 模式后,可能会发生 JSON 序列化行为更改,因为在 Transient 模式下,Newtonsoft.Json 库用于序列化中心方法的参数,但在模式 Persistent 下,System.Text.Json 库将用作默认值。 System.Text.Json 在默认行为方面与 Newtonsoft.Json 存在一些关键差异。 如果要在 Persistent 模式下使用 Newtonsoft.Json,可以在 local.settings.json 文件或 Azure 门户上的 Azure__SignalR__HubProtocol=NewtonsoftJson 中添加配置项 "Azure:SignalR:HubProtocol":"NewtonsoftJson"

  2. 在配置中配置多个 SignalR 服务终结点条目。

    我们使用 ServiceEndpoint 对象来表示 SignalR 服务实例。 可以使用服务终结点在条目键中的 <EndpointName><EndpointType> 以及条目值中的连接字符串来定义服务终结点。 键采用以下格式:

    Azure:SignalR:Endpoints:<EndpointName>:<EndpointType>
    

    <EndpointType> 是可选的,默认值为 primary。 请参阅以下示例:

    {
        "Azure:SignalR:Endpoints:EastUs":"<ConnectionString>",
    
        "Azure:SignalR:Endpoints:EastUs2:Secondary":"<ConnectionString>",
    
        "Azure:SignalR:Endpoints:WestUs:Primary":"<ConnectionString>"
    }
    

    注意

    • 在 Azure 门户的应用服务中配置 Azure SignalR 终结点时,不要忘记将 ":" 替换为键中的双下划线 "__"。 要了解原因,请参阅 [环境变量]

    • 使用键 {ConnectionStringSetting} 配置的连接字符串(默认为“AzureSignalRConnectionString”)也会识别为名称为空的主服务终结点。 但不建议将此配置样式用于多个终结点。

路由

默认行为

默认情况下,函数绑定使用 DefaultEndpointRouter 来选取终结点。

  • 客户端路由:从主联机终结点随机选择一个终结点。 如果所有主终结点都处于脱机状态,则随机选择一个辅助联机终结点。 如果选择再次失败,则会引发异常。

  • 服务器消息路由:返回所有服务终结点。

自定义

C# 进程内模型

步骤如下:

  1. 实现自定义路由器。 可以利用 ServiceEndpoint 中提供的信息做出路由决策。 请参阅此处的指南:customize-route-algorithm请注意,如果在自定义协商方法中需要使用 HttpContext,则协商函数中需要使用 HTTP 触发器。

  2. 将路由器注册到 DI 容器。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.SignalR;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(SimpleChatV3.Startup))]
namespace SimpleChatV3
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddSingleton<IEndpointRouter, CustomizedRouter>();
        }
    }
}
独立进程模型

对于在独立进程模型上运行的函数,我们支持在每个请求中指定目标终结点。 你将使用新的绑定类型来获取终结点信息。

客户端路由

SignalRConnectionInfo 绑定会根据默认传递规则选择一个终结点。 如果要自定义传递规则,则应使用 SignalRNegotiation 绑定而不是 SignalRConnectionInfo 绑定。

SignalRNegotiation 绑定配置属性与 SignalRConnectionInfo 相同。 下面是 function.json 文件示例:

{
    "type": "signalRNegotiation",
    "name": "negotiationContext",
    "hubName": "<HubName>",
    "direction": "in"
}

还可以添加其他绑定数据,例如 userIdidTokenclaimTypeList,就像 SignalRConnectionInfo 一样。

SignalRNegotiation 绑定中获取的对象采用以下格式:

{
    "endpoints": [
        {
            "endpointType": "Primary",
            "name": "<EndpointName>",
            "endpoint": "https://****.service.signalr.net",
            "online": true,
            "connectionInfo": {
                "url": "<client-access-url>",
                "accessToken": "<client-access-token>"
            }
        },
        {
            "...": "..."
        }
    ]
}

下面是 SignalRNegotiation 绑定的 JavaScript 用法示例:

module.exports = function (context, req, negotiationContext) {
    var userId = req.query.userId;
    if (userId.startsWith("east-")) {
        //return the first endpoint whose name starts with "east-" and status is online.
        context.res.body = negotiationContext.endpoints.find(endpoint => endpoint.name.startsWith("east-") && endpoint.online).connectionInfo;
    }
    else {
        //return the first online endpoint
        context.res.body = negotiationContext.endpoints.filter(endpoint => endpoint.online)[0].connectionInfo;
    }
}
消息路由

消息或操作路由需要两种绑定类型进行合作。 一般情况下,首先需要使用一个新的输入绑定类型 SignalREndpoints 来获取所有可用的终结点信息。 然后,筛选终结点,并获取包含要发送到的所有终结点的数组。 最后,在 SignalR 输出绑定中指定目标终结点。

下面是 functions.json 文件中的 SignalREndpoints 绑定配置属性:

{
      "type": "signalREndpoints",
      "direction": "in",
      "name": "endpoints",
      "hubName": "<HubName>"
}

SignalREndpoints 获取的对象是终结点数组,其中每个终结点都表示为具有以下架构的 JSON 对象:

{
    "endpointType": "<EndpointType>",
    "name": "<EndpointName>",
    "endpoint": "https://****.service.signalr.net",
    "online": true
}

获取目标终结点数组后,请将 endpoints 属性添加到输出绑定对象。 这是一个 JavaScript 示例:

module.exports = function (context, req, endpoints) {
    var targetEndpoints = endpoints.filter(endpoint => endpoint.name.startsWith("east-"));
    context.bindings.signalRMessages = [{
        "target": "chat",
        "arguments": ["hello-world"],
        "endpoints": targetEndpoints,
    }];
    context.done();
}

对于管理 SDK

通过配置添加多个终结点

使用 SignalR 服务连接字符串的键 Azure:SignalR:Endpoints 进行配置。 该键应采用格式 Azure:SignalR:Endpoints:{Name}:{EndpointType},其中 NameEndpointTypeServiceEndpoint 对象的属性,并且可以从代码访问。

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

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

通过代码添加多个终结点

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

var serviceManager = new ServiceManagerBuilder()
                    .WithOptions(option =>
                    {
                        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"),
                        };
                    })
                    .BuildServiceManager();

自定义终结点路由器

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

默认行为

  • 客户端请求路由:

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

  • 服务器消息路由:

    向特定的连接发送消息时,如果目标连接路由到当前服务器,则消息将直接转到这个已连接的终结点。 否则,消息将广播到每个 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 容器:

var serviceManager = new ServiceManagerBuilder()
                    .WithOptions(option =>
                    {
                        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"),
                        };
                    })
                    .WithRouter(new CustomRouter())
                    .BuildServiceManager();

跨区域方案中的配置

ServiceEndpoint 对象包含值为 primarysecondaryEndpointType 属性。

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

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

跨地域基础结构

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

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

正常协商

故障转移

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

显示故障转移机制过程的示意图。

后续步骤

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