WCF 路由服务 - 第 III 部分:故障转移和负载均衡






4.93/5 (26投票s)
本文介绍了使用 WCF 路由服务实现故障转移和负载均衡。
目录
所有文章
- WCF 路由服务 - 第 IV 部分:服务版本控制和多播
- WCF 路由服务 - 第三部分:故障转移和负载均衡
- WCF 路由服务 - 第 II 部分:基于上下文的路由和协议桥接
- WCF 路由服务 - 第一部分:基本概念、简单路由服务与基于内容的路由
引言
这是WCF 路由系列文章的第三部分。在本篇博文中,我将探讨与WCF RoutingService
相关的故障转移和负载均衡功能。故障转移或高可用性主要是用于在应用程序故障或崩溃时提供冗余,并将停机时间降至最低。负载均衡则与在高峰负载下提供高性能请求处理有关。让我们在接下来的章节中逐一探讨这些功能。
故障转移
对于关键服务而言,可靠性和高可用性都至关重要。在发生单服务器故障或托管应用程序故障时,服务应始终可供其最终用户使用。这意味着无论发生何种故障,客户端应用程序都不应中断。
因此,应设计高可用性的应用程序基础设施层,以防止因任何类型的故障而导致的失服务。服务必须部署在多台服务器上,以提供冗余并最大限度地减少停机时间。如果其中一台服务器不可用,另一台服务器将接管并继续为最终用户提供服务。这种现象称为故障转移。发生故障转移时,用户可以继续使用服务,并且对提供服务的来源(服务器)一无所知。
使用路由服务进行故障转移
您可以使用 RoutingService
来实现故障转移。RoutingService
在定义路由表时,提供了基本的容错功能,以应对运行时通信错误。您可以定义一组备用终结点(备份终结点),当与初始目标终结点通信失败时,RoutingService
将使用这些终结点。
让我们使用 RoutingService
来实现故障转移。但请注意,在本文中,我将继续使用上一篇文章(系列第二部分)中的“ComplexNumberCalculator
”服务。该服务已配置了以下终结点,并在控制台应用程序中托管:
<services>
<service name="CalculatorService.ComplexNumberCalculator">
<endpoint address="" binding="basicHttpBinding" contract="CalculatorService.IComplexNumber" />
<endpoint address="binary" binding="basicHttpBinding" contract="CalculatorService.IUnaryOperation" />
<endpoint address="unary" binding="basicHttpBinding" contract="CalculatorService.IUnaryOperation" />
<endpoint address="mex" kind="mexEndpoint" />
<host>
<baseAddresses>
<add baseAddress="https://:8081/ComplexNumberCalculator" />
</baseAddresses>
</host>
</service>
</services>
在此演示中,我将使用此服务的三个实例;一个作为主服务,另外两个作为备份服务。
下一步将配置 RoutingService
以支持故障转移。因此,我首先使用以下虚拟终结点配置了 RoutingService
:
<services>
<service name="System.ServiceModel.Routing.RoutingService">
<endpoint address="binary" binding="basicHttpBinding"
contract="System.ServiceModel.Routing.IRequestReplyRouter" name="VirtualEndpoint" />
<host>
<baseAddresses>
<add baseAddress="https://:8080/RoutingService/Router" />
</baseAddresses>
</host>
</service>
</services>
接下来,我定义了三个目标终结点:
<client>
<endpoint address="https://:8081/ComplexNumberCalculator1/binary" binding="basicHttpBinding"
contract="*" name="BinaryOperation1" />
<endpoint address="https://:8081/ComplexNumberCalculator2/binary" binding="basicHttpBinding"
contract="*" name="BinaryOperation2" />
<endpoint address="https://:8081/ComplexNumberCalculator3/binary" binding="basicHttpBinding"
contract="*" name="BinaryOperation3" />
</client>
为了简单起见,我只使用了 ComplexNumbrCalculator
服务的一个终结点,该终结点仅处理二进制运算。
接下来,我启用了 RoutingBehavior
,然后指定了路由表的名称。我通过定义默认行为来实现这一点,如下所示:
<behaviors>
<serviceBehaviors>
<behavior name="">
<routing filterTableName="RoutingTable" />
</behavior>
</serviceBehaviors>
</behaviors>
接下来,我使用 MatchAll
filterType
定义了以下筛选器:
<filters>
<filter name="BinaryOperationFilter" filterType="MatchAll" />
</filters>
接下来,我在 <backupLists> 元素中定义了备份终结点列表,如下所示:
<backupLists>
<backupList name="BackUps">
<add endpointName="BinaryOperation2"/>
<add endpointName="BinaryOperation3" />
</backupList>
</backupLists>
接下来,我在路由表 ‘RoutigTable
’ 中将“BinaryOperationFilter
”筛选器映射到第一个目标服务终结点“BinaryOperation1
”,并将路由表与备份终结点列表关联起来:
<filterTables>
<filterTable name="RoutingTable">
<add filterName="BinaryOperationFilter" endpointName="BinaryOperation1" backupList="BackUps" />
</filterTable>
</filterTables>
最后,我使用以下单个终结点配置了客户端应用程序:
<client>
<endpoint address="https://:8080/RoutingService/Router/binary" binding="basicHttpBinding"
contract="IUnaryOperation" name="BasicHttpBinding_IUnaryOperationr" />
</client>
下面是此文章提供的解决方案的屏幕截图:
现在,将 ConsoleClient 和 ConsoleHostRouter 项目设置为启动项目,然后按 Ctrl+F5 键运行项目。接下来,从管理员模式下的Visual Studio Developer Command Prompt运行 WCFRoutingPart3\ComplexNumberServices\StartThreeServices.cmd 文件,以启动 ComplexNumberCalculator1
、ComplexNumberCalculator2
和 ComplexNumberCalculator3
服务。
现在,最小化 RoutingService
控制台窗口,然后在控制台客户端窗口中按任意键;您可以通过中间的 RoutingService
验证复杂的二进制运算消息已路由到 ComplexNumberCalculator1
服务。
如果您再次在控制台客户端窗口中按任意键;您将看到传入的消息仍然路由到相同的 ComplexNumberCalculator1
服务,正如中间的 RoutingService
所预期的那样。
现在,只需关闭正在运行 ComplexNumberCalculator1
的控制台窗口,然后在控制台客户端窗口中再次按任意键;您会注意到,复杂的二进制运算消息现在已由中间的 RoutingService
路由到 ComplexNumberCalculator2
服务。
接下来,关闭正在运行 ComplexNumberCalculator2
的控制台窗口,然后再次按任意键;您可以验证复杂的二进制运算消息这次已由中间的 RoutingService
路由到 ComplexNumberCalculator3
服务。
最后,关闭正在运行 ComplexNumberCalculator3
的控制台窗口,并最后一次在控制台客户端窗口中按任意键,这次您将遇到一个错误,因为备份列表中没有更多可用的备份服务来路由传入的消息。实际上,这是最糟糕的情况,我们应该在备份列表中有足够的备份服务。
因此,您已经看到 RoutingService
在每次故障转移时都将传入的消息路由到下一个可用的服务。
负载均衡
除了高可用性和可靠性之外,性能对于关键服务来说也至关重要。性能是指服务完成请求所需的时间。即使在高峰负载下,服务也应能够为最终用户提供高速请求处理。可以通过负载均衡技术来提高服务的性能。可以在分布式环境中的各种机器上部署服务的多个实例,以维持可接受的性能。当收到多个并发请求时,通常使用算法(例如,轮询法、随机法、加权轮询等)将它们分发到可用机器(服务)之间。算法的作用是确定处理活动请求最少的机器(服务)。
我们也可以使用 RoutingService
来实现负载均衡。让我们以我们的 ComplexNumberCalculator
服务为例。假设该服务的最终用户有三种类型;第一种类型只能执行二进制运算,第二种类型只能执行一元运算,第三种类型可以执行二进制运算和一元运算。ComplexNumberCalculator
服务的一个实例可以轻松处理这三种类型的最终用户请求。但在高峰负载下,性能可能会成为一个问题(本演示中的假设)。因此,为了在高峰负载下提供高速处理,我们可以基于内容或使用某些算法(例如,轮询法、随机法、加权轮询等)来平衡传入的请求。
在接下来的章节中,我将通过使用基于内容的路由技术和轮询法来演示我们的 ComplexNumberCalculator
服务场景中的负载均衡。
基于内容的路由实现负载均衡
让我们考虑 ComplexNumberCalculator
服务的两个实例,而不是一个,以便基于消息的内容(操作类型)来平衡传入的请求。第一个实例将仅处理二进制运算,而第二个实例将仅处理一元运算。因此,第一类最终用户的请求将由 ComplexNumberCalculator
服务的第一个实例处理,而第二类最终用户的请求将由第二个实例处理。那么第三类最终用户呢?他们的请求将如何处理?请注意,第三类最终用户可以访问两种类型的操作:二进制和一元。嗯,第三类最终用户的请求将根据请求的操作(内容)进行分区,并转发给相应的实例进行处理;这意味着二进制运算请求将被转发到 ComplexNumberCalculator
服务的第一个实例进行处理,而一元运算请求将被转发到第二个实例进行处理。
根据我们 ComplexNumberCalculator
服务的設計(有关详细信息和类图,请参阅上一篇文章),第一类最终用户将使用 IUnaryOperation
服务协定,第二类最终用户将使用 IUnaryOperation
服务协定,而第三类最终用户将使用 IComplexNumber
服务协定,以便通过中间 RoutingService
与 ComplexNumberCalculator
服务进行通信。
因此,为了模拟我们场景的负载均衡,我配置了以下三个终结点(每种最终用户类型一个):
<client>
<endpoint address="https://:8080/RoutingService/Router"
binding="basicHttpBinding" contract="IUnaryOperation" name="firstTypeEndUsers" />
<endpoint address="https://:8080/RoutingService/Router"
binding="basicHttpBinding" contract="IUnaryOperation" name="secondTypeEndUsers" />
<endpoint address="https://:8080/RoutingService/Router"
binding="basicHttpBinding" contract="IComplexNumber" name="thirdTypeEndUsers" />
</client>
让我们重新配置 RoutingService
以实现我们的基于内容的负载均衡器。因此,我首先重新定义了以下两个目标终结点(每个 ComplexNumberCalculator
服务实例一个):
<client>
<endpoint address="https://:8081/ComplexNumberCalculator1/binary" binding="basicHttpBinding"
contract="*" name="binaryOperationInstance" />
<endpoint address="https://:8081/ComplexNumberCalculator2/unary" binding="basicHttpBinding"
contract="*" name="unaryOperationInstance" />
</client>
下一步将是定义筛选器,然后使用它们来配置路由表,以便我们的基于内容的负载均衡器能够满足我们描述的场景。但在执行此操作之前,让我们通过浏览服务 WSDL 定义来检查 ComplexNumberCalculator
服务每个服务协定支持的操作值,您会发现其中有三个 <portType> 元素。
以下是 IBinaryOperation
portType 的操作值:
http://tempuri.org/IBinaryOperation/Add
http://tempuri.org/IBinaryOperation/Subtract
http://tempuri.org/IBinaryOperation/Multiply
http://tempuri.org/IBinaryOperation/Divide
以下是 IUnaryOperation
portType 的操作值:
http://tempuri.org/IUnaryOperation/Modulus
http://tempuri.org/IUnaryOperation/Argument
http://tempuri.org/IUnaryOperation/Conjugate
http://tempuri.org/IUnaryOperation/Reciprocal
以下是 IComplexNumber
portType 的操作值:
http://tempuri.org/IBinaryOperation/Add
http://tempuri.org/IBinaryOperation/Subtract
http://tempuri.org/IBinaryOperation/Multiply
http://tempuri.org/IBinaryOperation/Divide
http://tempuri.org/IUnaryOperation/Modulus
http://tempuri.org/IUnaryOperation/Argument
http://tempuri.org/IUnaryOperation/Conjugate
http://tempuri.org/IUnaryOperation/Reciprocal
您会观察到,IComplexNumber
服务协定的操作值中包含 IBinaryOperation
和 IUnaryOperation
服务协定,而不是 IComplexNumber
服务协定本身。对于二进制运算,使用 IBinaryOperation
服务协定;对于一元运算,使用 IUnaryOperation
服务协定。为什么?因为 IComplexNumber
是一个空的服务协定,它分别实现了 IUnaryOperation
和 IBinaryOperation
服务协定(接口继承);它本身不定义任何操作。实际上,所有二进制和一元运算都是 IBinaryOperation
和 IUnaryOperation
服务协定的成员,而不是 IComplexNumber
服务协定的成员。因此,它在操作值中被省略了。实际上,IComplexNumber
服务协定的作用仅仅是为二进制和一元复杂数运算提供访问级别。
现在,在检查了操作值之后,您可以轻松地使用操作值或XPath 表达式为路由服务定义筛选器。但我更倾向于使用不同的方法。我将为我们的基于内容的负载均衡器创建一个自定义消息筛选器,该筛选器将检查每个传入消息的操作值,用于特定的服务协定(IBinaryOperation
或 IUnaryOperation
),并相应地返回 Boolean
结果。
因此,接下来我创建了一个自定义消息筛选器 ServiceContractMessageFilter
,如上所述。以下是其代码:
namespace CustomMessageFilters
{
public class ServiceContractMessageFilter : MessageFilter
{
string _serviceContractName;
public ServiceContractMessageFilter(string serviceContractName)
{
if (string.IsNullOrEmpty(serviceContractName)) { throw new ArgumentNullException("serviceContractName"); }
this._serviceContractName = serviceContractName;
}
public override bool Match(Message message)
{
return message.Headers.Action.Contains(_serviceContractName);
}
public override bool Match(MessageBuffer buffer)
{
return buffer.CreateMessage().Headers.Action.Contains(_serviceContractName);
}
}
}
接下来,我使用自定义消息筛选器 ServiceContractMessageFilter
重新定义了以下筛选器:
<filters>
<filter name="serviceContractFilter1" filterType="Custom" customType="CustomMessageFilters.ServiceContractMessageFilter, CustomMessageFilters"
filterData="IBinaryOperation"/>
<filter name="serviceContractFilter2" filterType="Custom" customType="CustomMessageFilters.ServiceContractMessageFilter, CustomMessageFilters"
filterData="IUnaryOperation"/>
</filters>
最后,我将每个筛选器映射到路由表 'RoutigTable
' 中相应的目标服务终结点,如下所示:
<filterTables>
<filterTable name="RoutingTable">
<add filterName="serviceContractFilter1" endpointName="binaryOperationInstance"/>
<add filterName="serviceContractFilter2" endpointName="unaryOperationInstance"/>
</filterTable>
</filterTables>
让我们实现基于内容的负载均衡器。只需再次将 ConsoleClient 和 ConsoleHostRouter 项目设置为启动项目,然后按 Ctrl+F5 键运行项目。现在,最小化 RoutingService
控制台窗口,然后从管理员模式下的Visual Studio Developer Command Prompt运行 WCFRoutingPart3\ComplexNumberServices\StartTwoServices.cmd 文件,以启动 ComplexNumberCalculator1
和 ComplexNumberCalculator2
服务。
接下来,在控制台客户端窗口中按任意键;您可以通过中间的 RoutingService
验证第一类最终用户的请求(复杂的二进制运算)已被路由到 ComplexNumberCalculator
服务的第一个实例(ComplexNumberCalculator1
)进行处理。
接下来,在控制台客户端窗口中按任意键;您可以通过中间的 RoutingService
验证第二类最终用户的请求(复杂的一元运算)已被路由到 ComplexNumberCalculator
服务的第二个实例(ComplexNumberCalculator2
)进行处理。
接下来,在控制台客户端窗口中按任意键;您可以通过中间的 RoutingService
验证第三类最终用户的请求已被分区。一元运算请求已路由到 ComplexNumberCalculator
服务的第一个实例(ComplexNumberCalculator1
),而二进制运算请求已路由到 ComplexNumberCalculator
服务的第二个实例(ComplexNumberCalculator2
)进行处理。
因此,这就是使用 RoutingService
实现的简单的基于内容的负载均衡器。您可以根据自己的需求构建基于内容的负载均衡器,其中包含服务(或多个服务)的多个实例。
轮询法实现负载均衡
在本节中,我将演示基于轮询法的 RoutingService
负载均衡。但在那之前,让我们先尝试理解什么是基于轮询法的算法负载均衡?在轮询法中,请求分配器会以轮换方式将传入的请求(消息)分配给一组服务器(服务)。第一个传入的请求(消息)会从参与组(服务列表)中随机选择一个服务器(服务)进行分配,后续的请求(消息)将由请求分配器按照循环顺序进行重定向。一旦一个服务器(服务)被分配了一个请求(消息)进行处理,该服务器(服务)将被移到服务器(服务)列表的末尾。这样可以确保服务器(服务)的分配是均等的。
让我们详细讨论一下。假设组中有三个服务,顺序为:{service1, service2, service3}。假设第一个传入的消息由请求分配器分配给 service1。那么下一个传入的消息,例如第二个、第三个、第四个、第五个……将由请求分配器按照循环顺序依次分配:service2, service3, service1, service2……。
让我们使用 RoutingService
来模拟基于轮询法的负载均衡。我将使用 ComplexNumberCalculator
服务的三个实例作为参与组,以便按顺序处理传入的消息,从而在高峰负载下提供高速消息处理。请注意,在基于轮询法的负载均衡器中,RoutingService
将充当“请求分配器”。
因此,我首先使用以下三个目标终结点(组的参与者)重新配置了 RoutingService
:
<client>
<endpoint address="https://:8081/ComplexNumberCalculator1" binding="basicHttpBinding"
contract="*" name="firstInstance" />
<endpoint address="https://:8081/ComplexNumberCalculator2" binding="basicHttpBinding"
contract="*" name="secondInstance" />
<endpoint address="https://:8081/ComplexNumberCalculator3" binding="basicHttpBinding"
contract="*" name="thirdInstance" />
</client>
接下来,我们需要重新定义 RoutingService
的筛选器,以便以轮换方式将传入的消息分发给服务组。由于 WCF 中没有内置的筛选器可以遵循轮询法,因此我们需要为其创建一个自定义筛选器。但是,在 msdn 上已经有一个基于轮询法的自定义筛选器示例,我将在此演示中使用它。
因此,接下来我使用自定义轮询消息筛选器重新定义了以下筛选器:
<filters>
<filter name="roundRobinContractFilter1" filterType="Custom" customType="CustomMessageFilters.RoundRobinMessageFilter, CustomMessageFilters" filterData="roundRobinGroup"/>
<filter name="roundRobinContractFilter2" filterType="Custom" customType="CustomMessageFilters.RoundRobinMessageFilter, CustomMessageFilters" filterData="roundRobinGroup"/>
<filter name="roundRobinContractFilter3" filterType="Custom" customType="CustomMessageFilters.RoundRobinMessageFilter, CustomMessageFilters" filterData="roundRobinGroup"/>
</filters>
最后,我将每个筛选器重新映射到路由表 'RoutigTable
' 中相应的目标服务终结点,如下所示:
<filterTables>
<filterTable name="RoutingTable">
<add filterName="roundRobinContractFilter1" endpointName="firstInstance"/>
<add filterName="roundRobinContractFilter2" endpointName="secondInstance"/>
<add filterName="roundRobinContractFilter3" endpointName="thirdInstance"/>
</filterTable>
</filterTables>
就这样。让我们运行我们的演示。只需将 ConsoleClient 和 ConsoleHostRouter 项目设置为启动项目,然后按 Ctrl+F5 键运行项目。接下来,最小化 RoutingService
控制台窗口,然后从管理员模式下的Visual Studio Developer Command Prompt运行 WCFRoutingPart3\ComplexNumberServices\StartThreeServices.cmd 文件,以启动 ComplexNumberCalculator1
、ComplexNumberCalculator2
和 ComplexNumberCalculator3
服务。
接下来,在控制台客户端窗口中按任意键;您可以通过中间的 RoutingService
(请求分配器)验证第一类最终用户请求是否已在组内按轮换方式分配给服务列表。
再次在控制台客户端窗口中按任意键;您可以看到,这次第二类最终用户请求已通过中间的 RoutingService
(请求分配器)在组内按轮换方式分配给服务列表。
再按一次控制台客户端窗口中的任意键;您可以看到,这次第三类最终用户请求已通过中间的 RoutingService
(请求分配器)在组内按轮换方式分配给服务列表。
因此,您已经看到,在每种情况下,请求都以有序的方式在可用服务器之间平均分配。
结论
因此,您已经看到了如何轻松地使用 RoutingService
实现故障转移或高可用性以及负载均衡功能。这些是非常重要的功能,应根据您的需求仔细考虑。在下一部分系列文章之前,祝您编码愉快。
历史记录
- 2014 年 6 月 7 日 -- 文章更新(在“所有文章”部分为系列的第四部分添加了新条目)
- 2014 年 5 月 29 日 -- 文章更新(添加了目录部分)
- 2014 年 5 月 28 日 -- 发布了原始版本