使用多个实例扩展 SignalR 服务
SignalR 服务 SDK 支持对 SignalR 服务实例使用多个终结点。 可以使用此功能来扩展并发连接,或将其用于跨区域的消息传送。
对于 ASP.NET Core
通过配置添加多个终结点
使用 SignalR 服务连接字符串的密钥 Azure:SignalR:ConnectionString
或 Azure:SignalR:ConnectionString:
进行配置。
如果密钥以 Azure:SignalR:ConnectionString:
开头,则它应采用 Azure:SignalR:ConnectionString:{Name}:{EndpointType}
格式,其中,Name
和 EndpointType
是 ServiceEndpoint
对象的属性(可从代码访问)。
可以使用以下 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 来选取终结点。
默认行为
客户端请求路由:
当客户端通过
/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 容器:
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:ConnectionString
或 Azure:SignalR:ConnectionString:
进行配置。
如果密钥以 Azure:SignalR:ConnectionString:
开头,则它应采用 Azure:SignalR:ConnectionString:{Name}:{EndpointType}
格式,其中,Name
和 EndpointType
是 ServiceEndpoint
对象的属性(可从代码访问)。
可将多个实例连接字符串添加到 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: true
的 appsettings.json
),因此你无需更改代码。 若要添加某种自定义配置并使用热重载,请参阅 ASP.NET Core 中的配置。
注意
考虑到在服务器/服务与客户端/服务之间设置连接所需的时间可能不同,为了确保在缩放过程中不丢失消息,我们将提供一段过渡期,以便等待服务器连接准备就绪,然后向客户端开放新的 ServiceEndpoint。 此过程通常只需几秒钟即可完成,完成后你可以看到类似于 Succeed in adding endpoint: '{endpoint}'
的日志消息。
在某些预期情况下(例如跨区域网络问题,或者不同应用服务器上出现配置不一致情况),过渡期的操作无法正常完成。 在这种情况下,如果你发现缩放过程无法正常进行,建议重启应用服务器。
缩放的默认超时期限为 5 分钟,可以通过更改 ServiceOptions.ServiceScaleTimeout
中的值来自定义超时。 如果你有大量的应用服务器,建议将该值稍微增大一点。
注意
目前,仅 Persistent
传输类型支持多终结点功能。
对于 SignalR 函数扩展
配置
要启用多个 SignalR 服务实例,你应:
使用
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"
。在配置中配置多个 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>" }
路由
默认行为
默认情况下,函数绑定使用 DefaultEndpointRouter 来选取终结点。
客户端路由:从主联机终结点随机选择一个终结点。 如果所有主终结点都处于脱机状态,则随机选择一个辅助联机终结点。 如果选择再次失败,则会引发异常。
服务器消息路由:返回所有服务终结点。
自定义
C# 进程内模型
步骤如下:
实现自定义路由器。 可以利用
ServiceEndpoint
中提供的信息做出路由决策。 请参阅此处的指南:customize-route-algorithm。 请注意,如果在自定义协商方法中需要使用HttpContext
,则协商函数中需要使用 Http 触发器。将路由器注册到 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"
}
还可以添加其他绑定数据,例如 userId
、idToken
和 claimTypeList
,就像 SignalRConnectionInfo
一样。
从 SignalRNegotiation
绑定中获取的对象采用以下格式:
{
"endpoints": [
{
"endpointType": "Primary",
"name": "<EndpointName>",
"endpoint": "https://****.service.signalr.azure.cn",
"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.azure.cn",
"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}
,其中 Name
和 EndpointType
是 ServiceEndpoint
对象的属性,并且可以从代码访问。
可以使用以下 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
对象包含值为 primary
或 secondary
的 EndpointType
属性。
主要终结点是接收客户端流量的首选终结点,因为它们的网络连接更可靠。 辅助终结点的网络连接不太可靠,仅用于处理服务器到客户端的流量。 例如,辅助终结点用于广播消息,而不用于处理客户端到服务器的流量。
在跨区域案例中,网络可能不稳定。 对于位于“中国东部”的某个应用服务器,同样位于“中国东部”区域的 SignalR 服务终结点为 primary
,其他区域中的终结点将标记为 secondary
。 在此配置中,其他区域中的服务终结点可以接收来自此“中国东部”应用服务器的消息,但不会将任何跨区域客户端路由到此应用服务器。 下图说明了此体系结构:
当客户端尝试使用默认路由器通过 /negotiate
来与应用服务器协商时,SDK 会从可用的 primary
终结点集内随机选择一个终结点。 当主要终结点不可用时,SDK 会从所有可用的 secondary
终结点中随机选择。 当服务器与服务终结点之间的连接处于活动状态时,终结点将标记为可用。
在跨区域方案中,如果客户端尝试通过 /negotiate
来与“中国东部”的应用服务器协商,则默认情况下,始终会返回位于同一区域中的 primary
终结点。 当“中国东部”的所有终结点都不可用时,路由器会将客户端重定向到其他区域中的终结点。 以下故障转移部分详细介绍了该方案。
故障转移
当没有可用的 primary
终结点时,客户端的 /negotiate
将从可用的 secondary
终结点中进行选择。 此故障转移机制要求每个终结点充当至少一个应用服务器的 primary
终结点。
后续步骤
可以在高可用性和灾难恢复方案中使用多个终结点。