65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (26投票s)

2014年5月28日

CPOL

13分钟阅读

viewsIcon

57216

downloadIcon

1808

本文介绍了使用 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> 

下面是此文章提供的解决方案的屏幕截图:

现在,将 ConsoleClientConsoleHostRouter 项目设置为启动项目,然后按 Ctrl+F5 键运行项目。接下来,从管理员模式下的Visual Studio Developer Command Prompt运行 WCFRoutingPart3\ComplexNumberServices\StartThreeServices.cmd 文件,以启动 ComplexNumberCalculator1ComplexNumberCalculator2ComplexNumberCalculator3 服务。

现在,最小化 RoutingService 控制台窗口,然后在控制台客户端窗口中按任意键;您可以通过中间的 RoutingService 验证复杂的二进制运算消息已路由到 ComplexNumberCalculator1 服务。

如果您再次在控制台客户端窗口中按任意键;您将看到传入的消息仍然路由到相同的 ComplexNumberCalculator1 服务,正如中间的 RoutingService 所预期的那样。

现在,只需关闭正在运行 ComplexNumberCalculator1 的控制台窗口,然后在控制台客户端窗口中再次按任意键;您会注意到,复杂的二进制运算消息现在已由中间的 RoutingService 路由到 ComplexNumberCalculator2 服务。

接下来,关闭正在运行 ComplexNumberCalculator2 的控制台窗口,然后再次按任意键;您可以验证复杂的二进制运算消息这次已由中间的 RoutingService 路由到 ComplexNumberCalculator3 服务。

最后,关闭正在运行 ComplexNumberCalculator3 的控制台窗口,并最后一次在控制台客户端窗口中按任意键,这次您将遇到一个错误,因为备份列表中没有更多可用的备份服务来路由传入的消息。实际上,这是最糟糕的情况,我们应该在备份列表中有足够的备份服务。

因此,您已经看到 RoutingService 在每次故障转移时都将传入的消息路由到下一个可用的服务。

负载均衡

除了高可用性和可靠性之外,性能对于关键服务来说也至关重要。性能是指服务完成请求所需的时间。即使在高峰负载下,服务也应能够为最终用户提供高速请求处理。可以通过负载均衡技术来提高服务的性能。可以在分布式环境中的各种机器上部署服务的多个实例,以维持可接受的性能。当收到多个并发请求时,通常使用算法(例如,轮询法随机法加权轮询等)将它们分发到可用机器(服务)之间。算法的作用是确定处理活动请求最少的机器(服务)。

我们也可以使用 RoutingService 来实现负载均衡。让我们以我们的 ComplexNumberCalculator 服务为例。假设该服务的最终用户有三种类型;第一种类型只能执行二进制运算,第二种类型只能执行一元运算,第三种类型可以执行二进制运算一元运算ComplexNumberCalculator 服务的一个实例可以轻松处理这三种类型的最终用户请求。但在高峰负载下,性能可能会成为一个问题(本演示中的假设)。因此,为了在高峰负载下提供高速处理,我们可以基于内容或使用某些算法(例如,轮询法随机法加权轮询等)来平衡传入的请求。

在接下来的章节中,我将通过使用基于内容的路由技术和轮询法来演示我们的 ComplexNumberCalculator 服务场景中的负载均衡

基于内容的路由实现负载均衡

让我们考虑 ComplexNumberCalculator 服务的两个实例,而不是一个,以便基于消息的内容(操作类型)来平衡传入的请求。第一个实例将仅处理二进制运算,而第二个实例将仅处理一元运算。因此,第一类最终用户的请求将由 ComplexNumberCalculator 服务的第一个实例处理,而第二类最终用户的请求将由第二个实例处理。那么第三类最终用户呢?他们的请求将如何处理?请注意,第三类最终用户可以访问两种类型的操作:二进制一元。嗯,第三类最终用户的请求将根据请求的操作(内容)进行分区,并转发给相应的实例进行处理;这意味着二进制运算请求将被转发到 ComplexNumberCalculator 服务的第一个实例进行处理,而一元运算请求将被转发到第二个实例进行处理。

根据我们 ComplexNumberCalculator 服务的設計(有关详细信息和类图,请参阅上一篇文章),第一类最终用户将使用 IUnaryOperation 服务协定,第二类最终用户将使用 IUnaryOperation 服务协定,而第三类最终用户将使用 IComplexNumber 服务协定,以便通过中间 RoutingServiceComplexNumberCalculator 服务进行通信。

因此,为了模拟我们场景的负载均衡,我配置了以下三个终结点(每种最终用户类型一个):

<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 服务协定的操作值中包含 IBinaryOperationIUnaryOperation 服务协定,而不是 IComplexNumber 服务协定本身。对于二进制运算,使用 IBinaryOperation 服务协定;对于一元运算,使用 IUnaryOperation 服务协定。为什么?因为 IComplexNumber 是一个空的服务协定,它分别实现了 IUnaryOperationIBinaryOperation 服务协定(接口继承);它本身不定义任何操作。实际上,所有二进制和一元运算都是 IBinaryOperationIUnaryOperation 服务协定的成员,而不是 IComplexNumber 服务协定的成员。因此,它在操作值中被省略了。实际上,IComplexNumber 服务协定的作用仅仅是为二进制和一元复杂数运算提供访问级别。

现在,在检查了操作值之后,您可以轻松地使用操作值XPath 表达式为路由服务定义筛选器。但我更倾向于使用不同的方法。我将为我们的基于内容的负载均衡器创建一个自定义消息筛选器,该筛选器将检查每个传入消息的操作值,用于特定的服务协定(IBinaryOperationIUnaryOperation),并相应地返回 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>  

让我们实现基于内容的负载均衡器。只需再次将 ConsoleClientConsoleHostRouter 项目设置为启动项目,然后按 Ctrl+F5 键运行项目。现在,最小化 RoutingService 控制台窗口,然后从管理员模式下的Visual Studio Developer Command Prompt运行 WCFRoutingPart3\ComplexNumberServices\StartTwoServices.cmd 文件,以启动 ComplexNumberCalculator1ComplexNumberCalculator2 服务。

接下来,在控制台客户端窗口中按任意键;您可以通过中间的 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> 

就这样。让我们运行我们的演示。只需将 ConsoleClientConsoleHostRouter 项目设置为启动项目,然后按 Ctrl+F5 键运行项目。接下来,最小化 RoutingService 控制台窗口,然后从管理员模式下的Visual Studio Developer Command Prompt运行 WCFRoutingPart3\ComplexNumberServices\StartThreeServices.cmd 文件,以启动 ComplexNumberCalculator1ComplexNumberCalculator2ComplexNumberCalculator3 服务。

接下来,在控制台客户端窗口中按任意键;您可以通过中间的 RoutingService请求分配器)验证第一类最终用户请求是否已在组内按轮换方式分配给服务列表。

再次在控制台客户端窗口中按任意键;您可以看到,这次第二类最终用户请求已通过中间的 RoutingService请求分配器)在组内按轮换方式分配给服务列表。

再按一次控制台客户端窗口中的任意键;您可以看到,这次第三类最终用户请求已通过中间的 RoutingService请求分配器)在组内按轮换方式分配给服务列表。

因此,您已经看到,在每种情况下,请求都以有序的方式在可用服务器之间平均分配。

结论

因此,您已经看到了如何轻松地使用 RoutingService 实现故障转移高可用性以及负载均衡功能。这些是非常重要的功能,应根据您的需求仔细考虑。在下一部分系列文章之前,祝您编码愉快。

历史记录

  • 2014 年 6 月 7 日 -- 文章更新(在“所有文章”部分为系列的第四部分添加了新条目)
  • 2014 年 5 月 29 日 -- 文章更新(添加了目录部分)
  • 2014 年 5 月 28 日 -- 发布了原始版本
© . All rights reserved.