Azure SignalR 服务中的复原能力和灾难恢复

复原能力和灾难恢复是联机系统的常见需求。 Azure SignalR 服务已提供 99.9% 的可用性,但它仍然是一个区域性的服务。 由于你的服务实例始终在一个区域中运行,因此在出现区域范围的服务中断时不会故障转移到另一个区域。

对于区域灾难恢复,建议采用以下两种方法:

  • 启用异地复制(简便方法)。 此功能将自动处理区域故障转移。 启用后,将仅保留一个 Azure SignalR 实例,并且不会引入任何代码更改。 有关详细信息,请查看异地复制
  • 利用 Service SDK 中的多个终结点。 我们的服务 SDK 支持多个 SignalR 服务实例,并在其中一些实例不可用时自动切换到其他实例。 发生灾难时,可以使用此功能进行恢复,但仍需要自行设置正确的系统拓扑。 本文档将介绍此操作。

SignalR 服务的高可用性体系结构

若要确保 SignalR 服务的跨区域复原能力,需要在不同的区域中设置多个服务实例。 这样,当某个区域出现故障时,可将其他区域用作备用区域。 当应用服务器连接到多个服务实例时,有两个角色:主要角色和辅助角色。 主要实例负责接收联机流量,而辅助实例充当完全正常运行的回退实例。 在 SDK 实现中,协商仅返回主要终结点,因此在正常情况下客户端只连接到主要终结点。 但当主要实例出现故障时,协商会返回辅助终结点,因此客户端仍可建立连接。 主要实例和应用服务器通过正常的服务器连接进行连接,但辅助实例和应用服务器通过一种称作“弱连接”的特殊连接进行连接。 弱连接的一个不同特征是它无法接受客户端连接路由,因为辅助实例位于另一个区域中。 将客户端路由到另一个区域不是最佳选择(会增大延迟)。

一个服务实例在连接到多个应用服务器时可以有不同的角色。 跨区域方案的一种典型设置是使用两对或更多对 SignalR 服务实例和应用服务器。 在每一对中,应用服务器和 SignalR 服务位于同一区域,SignalR 服务作为主要角色连接到应用服务器。 在每对之间,应用服务器和 SignalR 服务也会建立连接,但是,在连接到另一区域中的服务器时,SignalR 将变成辅助角色。

使用此拓扑时,来自一台服务器的消息仍可传送到所有客户端,因为所有应用服务器和 SignalR 服务实例是互连的。 但是,客户端在连接后,会路由到同一区域中的应用服务器,以实现最佳网络延迟。

下图阐释了这种方案:

该图显示了两个区域,每个区域都有一个应用服务器和一个 SignalR 服务,其中每个服务器在其区域中都与 SignalR 服务关联为主要,在另一区域中与该服务关联为次要。

配置多个 SignalR 服务实例

应用服务器和 Azure Functions 都支持多个 SignalR 服务实例。

在每个区域中创建 SignalR 服务和应用服务器/Azure Functions 后,可将应用服务器/Azure Functions 配置为连接到所有 SignalR 服务实例。

通过配置

你应该已经知道如何通过环境变量/应用设置/web.cofig 在名为 Azure:SignalR:ConnectionString 的配置项中设置 SignalR 服务连接字符串。 如果有多个终结点,可在多个配置项中设置这些终结点,每个项采用以下格式:

Azure:SignalR:ConnectionString:<name>:<role>

在 ConnectionString 中,<name> 是终结点的名称,<role> 是其角色(主要或辅助角色)。 名称是可选的,但如果你想要进一步自定义多个终结点之间的路由行为,则名称非常有用。

通过代码

如果你偏向于将连接字符串存储到其他位置,则也可以在代码中读取连接字符串,并在调用 AddAzureSignalR()(在 ASP.NET Core 中)或 MapAzureSignalR()(在 ASP.NET 中)时将其用作参数。

下面是示例代码:

ASP.NET Core:

services.AddSignalR()
        .AddAzureSignalR(options => options.Endpoints = new ServiceEndpoint[]
        {
            new ServiceEndpoint("<connection_string1>", EndpointType.Primary, "region1"),
            new ServiceEndpoint("<connection_string2>", EndpointType.Secondary, "region2"),
        });

ASP.NET:

app.MapAzureSignalR(GetType().FullName, hub,  options => options.Endpoints = new ServiceEndpoint[]
    {
        new ServiceEndpoint("<connection_string1>", EndpointType.Primary, "region1"),
        new ServiceEndpoint("<connection_string2>", EndpointType.Secondary, "region2"),
    };

可以配置多个主要或次要实例。 如果有多个主要实例和/或次要实例,则协商会按以下顺序返回终结点:

  1. 如果有至少一个主要实例处于联机状态,则会返回一个随机的联机主要实例。
  2. 如果所有主要实例都停机,则会返回一个随机的联机次要实例。

对于 Azure Functions 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”)也会识别为名称为空的主服务终结点。 但不建议将此配置样式用于多个终结点。

对于 [管理 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();

故障转移序列和最佳做法

现已设置正确的系统拓扑。 每当某个 SignalR 服务实例出现故障时,联机流量都会路由到其他实例。 下面是当主要实例出现故障时(以及在一段时间后进行恢复时)发生的情况:

  1. 主要服务实例出现故障,此实例上的所有服务器连接都会断开。
  2. 连接到此实例的所有服务器都会将此实例标记为脱机,协商会停止返回此终结点,开始返回辅助终结点。
  3. 此实例上的所有客户端连接也会关闭,然后客户端会重新进行连接。 由于应用服务器现在返回辅助终结点,因此客户端会连接到辅助实例。
  4. 现在,辅助实例将接收所有联机流量。 由于辅助实例已连接到所有应用服务器,因此从服务器发往客户端的所有消息仍可传送。 但是,从客户端发往服务器的消息只能路由到同一区域中的应用服务器。
  5. 主要实例恢复并重新联机后,应用服务器将与它重新建立连接,并将其标记为联机。 协商现在会再次返回主要终结点,因此,新客户端会重新连接到主要实例。 但现有客户端不会掉线,在断开自身连接之前仍会路由到辅助客户端。

下图演示了 SignalR 服务中如何实现故障转移:

图 1 故障转移之前故障转移之前

图 2:故障转移之后故障转移之后

图 3 主实例恢复后的短时间内主实例恢复后的短时间内

可以看到,在正常情况下,只有主要应用服务器和 SignalR 服务包含联机流量(以蓝色表示)。 故障转移后,辅助应用服务器和 SignalR 服务也处于活动状态。 主要 SignalR 服务重新联机后,新客户端将连接到主要 SignalR。 但是,现有客户端仍连接到辅助实例,因此这两个实例都包含流量。 所有现有客户端断开连接后,系统将会恢复正常(图 1)。

可以使用两种主要模式来实现跨区域的高可用性体系结构:

  1. 第一种模式是使用一对应用服务器和 SignalR 服务实例来接收所有联机流量,并使用另一对作为备用实例(称为主动/被动配置,如图 1 所示)。
  2. 另一种模式是使用两对(或更多对)应用服务器和 SignalR 服务实例,其中每个实例接收一部分联机流量,并充当其他对的备用实例(称为主动/主动配置,类似于图 3)。

SignalR 服务支持这两种模式,主要差别在于实现应用服务器的方式。 如果应用服务器采用主动/被动配置,则 SignalR 服务也采用主动/被动配置(因为主要应用服务器仅返回其主要 SignalR 服务实例)。 如果应用服务器采用主动/主动配置,则 SignalR 服务也采用主动/主动配置(因为所有应用服务器都将返回其自己的主要 SignalR 实例,因此它们都可以获得流量)。

请注意,无论选择使用什么模式,都需要将每个 SignalR 服务实例作为主要实例连接到应用服务器。

另外,由于 SignalR 连接的性质(远距离连接),发生灾难和故障转移时,客户端会遇到连接断开的情况。 需要在客户端上处理此类情况,使其对最终客户透明。 例如,关闭连接后不要重新连接。

如何测试故障转移

按照以下步骤触发故障转移:

  1. 在门户中主要资源的“网络”选项卡中,禁用公用网络访问。 如果资源已启用专用网络,请使用访问控制规则来拒绝所有流量。
  2. 重启主要资源。

后续步骤

在本文中,你已了解如何配置应用程序以实现 SignalR 服务的复原能力。 若要更详细地了解 SignalR 服务中的服务器/客户端连接和连接路由,请阅读此文,其中介绍了 SignalR 服务的内部情况。

对于使用多个实例一起处理大量连接的缩放方案(例如分片),请阅读如何缩放多个实例

有关如何使用多个 SignalR 服务实例配置 Azure Functions 的详细信息,请阅读 Azure Functions 中的多个 Azure SignalR 服务实例支持