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

如何使用 WCF 4.0 路由服务和发现服务创建可伸缩的服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (40投票s)

2011 年 3 月 5 日

CPOL

17分钟阅读

viewsIcon

124232

downloadIcon

2343

本文展示了如何使服务能够扩展到一台机器之外,并使用 WCF 路由服务和 WCF 发现服务实现负载均衡和容错。

引言

我之所以写这篇文章,是因为有人要求我创建一个需要满足特殊可伸缩性要求的服务。我意识到,这些要求可以帮助到任何服务,以及你们编写的任何服务。因此,我决定花时间将所有内容都写在这篇文章中。让我们先列出我需要为我的服务满足的规范清单。

  • 服务需要能够扩展到一台机器之外;我需要能够在多台机器上运行相同的服务,并提供容错能力,即如果一台机器宕机,另一台运行服务的机器将处理请求。
  • 服务应该在不使用负载均衡器的情况下实现负载均衡。通常,我会说这里应该使用负载均衡器,但我将展示一种软件解决方案。其思想是,如果服务托管在多个位置,则让不同的位置来平衡负载。
  • 如果托管了更多服务,请确保它们被自动添加到负载均衡器中,并用于容错,而无需重新启动任何内容。如果我们添加新服务,客户端应该能够向它们发送消息,并且它们应该与其他现有服务进行负载均衡(所有服务必须共享相同的契约)。
  • 确保客户端不知道这一切,并只与一个地址通信。

例如,假设您有一个服务用于处理数据库查询。您知道您的服务可能会受到来自多个客户端的数百万次点击。该服务只需在数据库上运行查询,然后返回结果集。假设我们希望使用上述要求来支持可伸缩性。我会将 10 个名为 CustomerQueryService 的服务托管在 10 台机器上。我会在它们前面放一个路由器,该路由器足够智能,可以找到该服务的 10 个实例。例如,假设我启动了第 11 台机器,运行该服务的第 11 个实例;现在我的超级路由器应该能够检测到新版本服务的启动,并能够将其路由到它。没有容错和负载均衡的这些实例是有点无用的。因此,如果我们继续我们的例子,如果机器 3 宕机,或者可能因为它太忙而无法处理更多请求,路由器应该将消息重新发送到服务的另一个实例。即使一切都在运行,比如我只想在所有机器之间分发负载,那么我认为路由器不应该总是发送到列表中的第一台机器,它应该尝试通过应用轮循机制来平衡负载。

总的来说,这些要求你们中的大多数人都会说 IT 可以满足。通过部署路由器/负载均衡器,可以做到这一点。然而,我想提供一个纯软件解决方案作为备选方案,供那些无法承担网络设置复杂性和成本的人使用。

环境

本文中的所有服务都托管在 IIS 7.5 中,使用 WAS (Windows Activation Service) 在 Windows 2008 R2 上运行。此外,还安装了 AppFabric 以增加额外的日志记录(以及发现服务所需的自动启动功能)。所有代码都使用 Visual Studio 2011 和 .NET 4.0 进行编译。通过使用不同的应用程序池并重置它们来模拟服务的关闭和重新启动,对服务进行了测试。代码和示例使用一台机器上的两个托管站点来模拟多台机器环境。

总体设计

解决方案的总体设计分为三个部分:客户端、路由器和服务。通常,会有许多客户端和许多服务,但只有一个路由器。客户端将与路由器通信,路由器将消息发送到服务的一个实例。为此,路由器有一个路由表,其中包含所有服务的地址,它使用这些地址来处理容错和负载均衡。客户端不直接与服务通信,它们只与路由器通信,因此客户端配置很简单。我知道有一个路由器会成为单点故障,所以我在文章后面有一个关于这个问题的部分。

服务必须可伸缩

为了使服务可伸缩,我们必须保持服务无状态,这意味着它不能在调用之间保留状态。这是使任何服务可伸缩的关键要求,因此我在此声明它,以便您立即知道您的服务是否适合可伸缩。

最佳方法是使您的服务使用 PerCall 模式并支持多个并发调用。

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,
      InstanceContextMode=InstanceContextMode.PerCall)]
public class Service1 : IService
{
  public string DoWork(string value)
  {
     Trace.WriteLine("Called From Service1");
     return string.Format("You entered: {0}", value);
  }
}

正如您所见,这是一个简单的服务,它所做的就是返回输入的原始数据。调用之间没有保留状态,服务创建一个 Service1 实例,调用方法,然后销毁该实例。这是任何服务最佳的可伸缩选项。

但是如果您的服务有状态怎么办?如果您在调用之间保留了一些信息怎么办?我的建议是将其存储在其他地方,而不是服务本身。数据库是不错的选择,但如果太慢,您可以使用 AppFabric 缓存。

路由器如何知道服务在哪里运行?

考虑到我们有一个不保留状态且使用 PerCall 实例激活的服务,我们已经准备好使用新的 WCF 4.0 路由器支持来使其可伸缩。但如上所述,我们即将构建的路由器必须是动态的,这意味着它不使用静态的服务位置配置。它使用 WCF 4.0 中的一项新功能,即服务发现。为了使路由器完全动态,我们将需要使用两种类型的服务发现:托管发现和即席发现。对于托管发现,我们将使用一个新的发现服务来跟踪所有正在运行的服务及其位置。托管发现服务将用于在路由器首次加载时初始化路由器。然后,即席发现将接管,并在添加新服务和关闭服务时通知路由器。即席发现提供关于新服务启动和现有服务关闭的“通知”。尽管如此,由于时序问题,我们首先需要使用已运行服务的列表来初始化路由器。我们无法在服务先于路由器启动时收到通知,这就是为什么在设计中包含托管发现的原因。

我知道这可能听起来有点复杂,所以我们将逐步了解每个部分如何使用,然后将它们组合成一个示例来展示它是如何工作的。

首先,让服务可发现

为了使服务与托管发现配合使用,我们需要一个服务来注册所有启动和关闭的服务。这种服务称为发现代理。为了使代码简单,我使用了 MSDN 提供的发现代理示例(在此 )。我只想提一下,此示例不应用于商业软件;主要原因是找到的服务列表保留在内存中。这违反了我们无状态服务的原则。这意味着这个发现代理示例无法扩展到单个实例。要解决这个问题,您需要重新编写服务以使用数据库,然后您可以根据需要扩展您的发现代理。

// The following are helper methods required by the Proxy implementation
void AddOnlineService(EndpointDiscoveryMetadata endpointDiscoveryMetadata)
{
    lock (this.onlineServices)
    {
        this.onlineServices[endpointDiscoveryMetadata.Address] = endpointDiscoveryMetadata;
    }

    PrintDiscoveryMetadata(endpointDiscoveryMetadata, "Adding");
}

void RemoveOnlineService(EndpointDiscoveryMetadata endpointDiscoveryMetadata)
{
    if (endpointDiscoveryMetadata != null)
    {
        lock (this.onlineServices)
        {
            this.onlineServices.Remove(endpointDiscoveryMetadata.Address);
        }

        PrintDiscoveryMetadata(endpointDiscoveryMetadata, "Removing");
    }
}

为了进行测试,您可以设置断点来查看服务何时被添加到发现代理注册表中以及何时被删除。但是,还有一项更重要的任务要做:我们需要配置我们的服务,使其在托管或终止时向发现代理注册。这可以通过将以下配置添加到 web.config 来实现

<behaviors>
   <serviceBehaviors>
       <behavior>
           <serviceMetadata httpGetEnabled="true"/>
           <serviceDebug includeExceptionDetailInFaults="false"/>
           <serviceDiscovery>
           <announcementEndpoints>
                  <endpoint kind="udpAnnouncementEndpoint" />
                  <endpoint
                      name="MyAnnouncementEndpoint"
                      kind="announcementEndpoint"
                      address="net.tcp:///DiscoveryProxy/DiscoveryProxy.svc"
                      bindingConfiguration ="NetTcpBindingConfiguration"
                      binding="netTcpBinding"/>
           </announcementEndpoints>
      </serviceDiscovery>
</behavior>

托管发现的使用仅用于获取网络上的服务初始列表;但是,要检测新服务何时启动,我们不想轮询托管发现代理。相反,我们希望收到通知。因此,我们的服务还使用基于 UDP 的即席发现。为了让服务在启动或关闭时广播,我们将 `endpoint kind="udpAnnouncementEndpoint"` 端点添加到其配置中。

基于以上配置,我们完成了以下工作:

  • 将默认服务行为添加到配置中,通过放置一个没有名称的服务行为;所有服务默认使用此行为(这是 WCF 4.0 的一项新功能)。
  • 添加了一个指向发现代理的端点,告知服务将其地址注册到运行在 “net.tcp:///DiscoveryProxy/DiscoveryProxy.svc” 的发现代理。
  • 一个端点告知服务通过包含 `endpoint kind="udpAnnouncementEndpoint"` 来使用即席发现。此端点告知服务在服务启动和关闭时使用 UDP 进行广播。路由器将需要监听这些广播以了解何时添加或删除了新服务。这将在下一节介绍,届时我将解释如何编写路由器。

此时,我们已经完成了几件事:我们有了服务。我们配置了服务使用发现。托管发现用于维护所有正在运行服务的注册表,即席发现用于广播服务是启动还是关闭。我们使用 MSDN 示例代码运行了一个托管发现代理。总的来说,我们现在已经准备好编写动态路由器了。

使路由器动态化

当我研究 WCF 路由器时,我立刻爱上了它们。它们立即为我解决了许多问题。首先,它们允许我在访问服务之前引入一个拦截层。这给了我在调用发送到服务之前进行日志记录、安全检查和其他操作的机会。路由器还可以用作 Web 网关,一个位于 DMZ 区域的服务,将消息路由到防火墙后面的服务。路由器可以监听 HTTP,但路由到运行 net.tcp 的服务,使其成为完美的 Web 代理。容错是路由器的另一项功能。我们可以为路由器提供一个目标服务端点的列表,如果其中一个不可用,路由器会将消息发送到列表中的下一个服务。客户端甚至不知道是否有备用服务响应。

总而言之,WCF 路由器允许我们将服务扩展到一台机器之外,让客户端与路由器通信,让路由器与网络上运行的服务通信。为了使这一点更有趣,我希望路由器能够动态发现这些服务,并在添加新服务或删除现有服务时自动更新配置。

创建 DiscoveryRouterBehavior

为了实现这一切,我们需要改变路由器的工作方式,所以我们向路由器添加了一个新的自定义行为。让我们快速看一下这个新行为。

public class DiscoveryRouterBehavior : BehaviorExtensionElement, IServiceBehavior
{
  public DiscoveryRouterBehavior()
  {

  }
  void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, 
       ServiceHostBase serviceHostBase, 
       System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, 
       BindingParameterCollection bindingParameters)
  {

  }

  void IServiceBehavior.ApplyDispatchBehavior(
       ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  {
     ServiceDiscoveryExtension discoveryExtension = new ServiceDiscoveryExtension();
     serviceHostBase.Extensions.Add(discoveryExtension);
  }

  void IServiceBehavior.Validate(ServiceDescription serviceDescription, 
                                 ServiceHostBase serviceHostBase)
  {

  }

  public override Type BehaviorType
  {
     get { return typeof(DiscoveryRouterBehavior); }
  }

  protected override object CreateBehavior()
  {
     return new DiscoveryRouterBehavior();
  }
}

我选择使用宿主扩展的原因是因为宿主只在服务开始宿主时使用一次。这让我们有机会在宿主过程中添加代码,并允许路由器在加载时动态配置自身。

此行为的实际作用不大,它只是向宿主进程添加了一个名为 ServiceDiscoveryExtension 的新扩展。让我们看一下扩展类。

服务发现扩展

internal class ServiceDiscoveryExtension : IExtension<ServiceHostBase>, IDisposable
{
  private ServiceHostBase owner;
  private RoutingConfiguration mRouterConfiguration = new RoutingConfiguration();
  private List<ServiceEndpoint>  mEndpoints = null;


  public ServiceDiscoveryExtension()
  {
     // holds the list of endpoints
     mEndpoints = new List<ServiceEndpoint>();
     mRouterConfiguration.FilterTable.Add(new MatchAllMessageFilter(), mEndpoints);
  }
  void IExtension<ServiceHostBase>.Attach(ServiceHostBase owner)
  {
     this.owner = owner;
     PopulateFromManagedDiscovery();
     ListenToAnnouncements();
  }

  void IExtension<ServiceHostBase>.Detach(ServiceHostBase owner)
  {
     this.Dispose();
  }

  public void Dispose()
  {
  }

  private void PopulateFromManagedDiscovery()
  {
     // Create a DiscoveryEndpoint that points to the DiscoveryProxy
     Uri probeEndpointAddress = 
       new Uri("net.tcp:///DiscoveryProxy/DiscoveryProxy.svc");

     var binding = new NetTcpBinding(SecurityMode.None);

     DiscoveryEndpoint discoveryEndpoint = 
       new DiscoveryEndpoint(binding, new EndpointAddress(probeEndpointAddress));

     DiscoveryClient discoveryClient = new DiscoveryClient(discoveryEndpoint);
     var results = discoveryClient.Find(new FindCriteria(
                                   typeof(Service.Api.IService)));

     // add these endpoint to the router table.
     foreach (var endpoint in results.Endpoints)
     {
        AddEndpointToRoutingTable(endpoint);
     }
  }

  private void ListenToAnnouncements()
  {

     AnnouncementService announcementService = new AnnouncementService();

     // Subscribe to the announcement events

     announcementService.OnlineAnnouncementReceived += 
       new EventHandler<AnnouncementEventArgs>(ServiceOnlineEvent);
     announcementService.OfflineAnnouncementReceived += 
       new EventHandler<AnnouncementEventArgs>(ServiceOffLineEvent);

     // Host the AnnouncementService
     ServiceHost announcementServiceHost = new ServiceHost(announcementService);

     try
     {
        // Listen for the announcements sent over UDP multicast
        announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
        announcementServiceHost.Open();
     }
     catch (CommunicationException communicationException)
     {
        throw new FaultException("Can't listen to notification of services " + 
                                 communicationException.Message);
     }
     catch (TimeoutException timeoutException)
     {
        throw new FaultException("Timeout trying to open the notification service " + 
                                 timeoutException.Message);
     }
  }

  private void ServiceOffLineEvent(object sender, AnnouncementEventArgs e)
  {
     // service went offline, remove it from the routing table.
     Trace("Endpint offline detected: {0}", e.EndpointDiscoveryMetadata.Address);
     RemoveEndpointFromRoutingTable(e.EndpointDiscoveryMetadata);
  }

  private void ServiceOnlineEvent(object sender, AnnouncementEventArgs e)
  {
     // a service is added, add it to the router table.
     Trace("Endpint online detected: {0}", e.EndpointDiscoveryMetadata.Address);
     AddEndpointToRoutingTable(e.EndpointDiscoveryMetadata);

  }

  private void AddEndpointToRoutingTable(EndpointDiscoveryMetadata endpointMetadata)
  {

     // set the address, for now all bindings are wsHttp
     WSHttpBinding binding = new WSHttpBinding();
     binding.Security.Mode = SecurityMode.None;

     // set the address
     EndpointAddress address = endpointMetadata.Address;

     // set the contract
     var contract = ContractDescription.GetContract(typeof(IRequestReplyRouter));

     ServiceEndpoint endpoint = new ServiceEndpoint(contract, binding, address);


     mEndpoints.Add(endpoint);

     mRouterConfiguration.FilterTable.Clear();
     mRouterConfiguration.FilterTable.Add(new MatchAllMessageFilter(), 
            new RoundRobinList<ServiceEndpoint>(mEndpoints));

     this.owner.Extensions.Find<RoutingExtension>().ApplyConfiguration(
                           mRouterConfiguration);

     Trace("Endpint added: {0}", endpointMetadata.Address);
  }

  private void RemoveEndpointFromRoutingTable(
          EndpointDiscoveryMetadata endpointMetadata)
  {
     // a service is going offline, take it out of the routing table.
     var foundEndpoint = mEndpoints.Find(e => e.Address == endpointMetadata.Address);
     if (foundEndpoint != null)
     {
        Trace("Endpint removed: {0}", endpointMetadata.Address);
        mEndpoints.Remove(foundEndpoint);
     }

     mRouterConfiguration.FilterTable.Clear();
     mRouterConfiguration.FilterTable.Add(new MatchAllMessageFilter(), 
       new RoundRobinList<ServiceEndpoint>(mEndpoints));

     this.owner.Extensions.Find<RoutingExtension>().ApplyConfiguration(
                mRouterConfiguration);
  }

  private void Trace(string msg, params object[] args)
  {
     System.Diagnostics.Trace.WriteLine(String.Format(msg, args));
  }
}

首先,这个类有很多操作,所以我将把它分成几部分,我们将在其中详细审查每个部分。首先要注意的是,这是一个宿主扩展(使用 IExtension),这意味着这段代码在宿主启动时执行一次。因此,让我们看一下 Attach 方法。

void IExtension<ServiceHostBase>.Attach(ServiceHostBase owner)
{
 // set the owner of this service host extension
 this.owner = owner;

 // populate the routing table from managed discogery
 PopulateFromManagedDiscovery();

 // listen to notifications of new services
 // coming up or existing services shutting down.
 ListenToAnnouncements();
}

这是路由器的核心;在这里,我们构建路由表,并按以下顺序进行:

  • 首先,使用托管发现获取要路由到的服务列表。
  • 监听新服务的启动或服务的关闭。这是我们将监听 UDP 通知的部分。

从托管发现获取服务列表

为了从托管发现获取服务列表,我们使用以下代码:

private void PopulateFromManagedDiscovery()
{
 // Create a DiscoveryEndpoint that points to the DiscoveryProxy
 Uri probeEndpointAddress = 
     new Uri("net.tcp:///DiscoveryProxy/DiscoveryProxy.svc");

 var binding = new NetTcpBinding(SecurityMode.None);

 DiscoveryEndpoint discoveryEndpoint = new DiscoveryEndpoint(binding, 
                   new EndpointAddress(probeEndpointAddress));

 DiscoveryClient discoveryClient = new DiscoveryClient(discoveryEndpoint);
 var results = discoveryClient.Find(new FindCriteria(typeof(Service.Api.IService)));

 // add these endpoint to the router table.
 foreach (var endpoint in results.Endpoints)
 {
    AddEndpointToRoutingTable(endpoint);
 }
}

我们使用发现代理地址(net.tcp:///DiscoveryProxy/DiscoveryProxy.svc)来创建一个发现客户端代理类。我们发出一个查找请求,以查找与契约 Service.Api.IService 匹配的所有服务,并获取所有找到的端点的结果(results.Endpoints)。使用托管发现时有几个重要注意事项。服务仅在运行时注册;在 WAS 中部署服务时,服务可能在调用它之前不会运行。这对 WAS 来说是正常的,因为它只在有客户端访问时才激活服务。但是,这对发现来说是个问题,因为如果您不知道服务在哪里,就无法调用它,而直到有人调用它,它才会被注册。为了解决这个问题,我使用了 AppFabric 中的一项新功能,即自动启动服务的功能。您可以在 MSDN 上这篇关于自动启动服务的文章 了解更多信息。

对于找到的每个端点,我们都会调用一个方法将其添加到路由表中。我们将在稍后详细介绍这个方法。

基于 UDP 发现获取通知

让我们看看路由器如何监听新服务启动的通知。这段代码可以在以下方法中找到:

private void ListenToAnnouncements()
{
    AnnouncementService announcementService = new AnnouncementService();

    // Subscribe to the announcement events

    announcementService.OnlineAnnouncementReceived +=
       new EventHandler<AnnouncementEventArgs>(ServiceOnlineEvent);
    announcementService.OfflineAnnouncementReceived +=
       new EventHandler<AnnouncementEventArgs>(ServiceOffLineEvent);

    // Host the AnnouncementService
    ServiceHost announcementServiceHost = new ServiceHost(announcementService);

    try
    {
        // Listen for the announcements sent over UDP multicast
        announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
        announcementServiceHost.Open();
    }
    catch (CommunicationException communicationException)
    {
        throw new FaultException("Can't listen to notification of services " + 
                                 communicationException.Message);
    }
    catch (TimeoutException timeoutException)
    {
        throw new FaultException("Timeout trying to open the notification service " + 
                                 timeoutException.Message);
    }
}

您会记得,我们为服务设置了两种类型的发现:托管发现和基于 UDP 通知的一种。在这里,我们告诉路由器监听通知并设置两个事件处理程序:一个用于服务上线(ServiceOnlineEvent),另一个用于服务下线(ServiceOffLineEvent)。为了实际监听事件,我们需要托管一个服务。我们要托管的服务是 AnnoucementService,它是发现框架的一部分。我们只需创建一个常规的服务主机并对其进行托管。我们确保主机具有正确的端点,即 UdpAnnouncementEndpoint,它也是发现框架的一部分。最后,这段代码会根据事件处理程序告诉我们服务何时启动或关闭。这里也有一个重要提示:即席发现仅在服务位于同一子网掩码时才有效,并且需要允许 UDP 在机器之间通信。

动态修改路由表

为了使路由器表动态化,我编写了两个方法来从路由表中添加和删除端点。由发现提供的端点信息如下所示:

private void AddEndpointToRoutingTable(EndpointDiscoveryMetadata endpointMetadata)
{
  // set the address, for now all bindings are wsHttp
  WSHttpBinding binding = new WSHttpBinding();
  binding.Security.Mode = SecurityMode.None;

  // set the address
  EndpointAddress address = endpointMetadata.Address;

  // set the contract
  var contract = ContractDescription.GetContract(typeof(IRequestReplyRouter));

  ServiceEndpoint endpoint = new ServiceEndpoint(contract, binding, address);


  mEndpoints.Add(endpoint);

  mRouterConfiguration.FilterTable.Clear();
  mRouterConfiguration.FilterTable.Add(new MatchAllMessageFilter(), 
         new RoundRobinList<ServiceEndpoint>(mEndpoints));

  this.owner.Extensions.Find<RoutingExtension>().ApplyConfiguration(mRouterConfiguration);

  Trace("Endpint added: {0}", endpointMetadata.Address);
}

private void RemoveEndpointFromRoutingTable(EndpointDiscoveryMetadata endpointMetadata)
{
  // a service is going offline, take it out of the routing table.
  var foundEndpoint = mEndpoints.Find(e => e.Address == endpointMetadata.Address);
  if (foundEndpoint != null)
  {
     Trace("Endpint removed: {0}", endpointMetadata.Address);
     mEndpoints.Remove(foundEndpoint);
  }

  mRouterConfiguration.FilterTable.Clear();
  mRouterConfiguration.FilterTable.Add(new MatchAllMessageFilter(), 
         new RoundRobinList<ServiceEndpoint>(mEndpoints));

  this.owner.Extensions.Find<RoutingExtension>().ApplyConfiguration(mRouterConfiguration);
}

这里确实有很多内容,让我慢慢解释。首先,我将要路由到的端点列表保存在一个简单的列表 mEndPoints 中。当我们使用发现时,我们在 EndpointDiscoveryMetadata 类中获取服务实例的位置。但是,使用发现时,我们只获取端点的地址和契约,而不是绑定。因此,在这种情况下,我将绑定硬编码为 WSHttpBinding,如下面的代码所示:

WSHttpBinding binding = new WSHttpBinding();
binding.Security.Mode = SecurityMode.None;

但是,我们可能不喜欢仅仅硬编码绑定,所以我写了一篇文章,介绍如何动态获取绑定,作为发现元数据的一部分。不过为了保持简单,我决定在这篇文章中不使用这种技术。然而,契约被修改为 IRequestReplyRouter,因为这是我们想要的路由类型。真正的契约在消息的头部,在这种情况下,我们使用预定义的过滤器 MatchAllMessageFilter 来路由所有消息。一旦我们得到了端点,我们就使用以下代码将其添加到路由表中:

mRouterConfiguration.FilterTable.Clear();
mRouterConfiguration.FilterTable.Add(new MatchAllMessageFilter(), 
       new RoundRobinList<ServiceEndpoint>(mEndpoints));

暂时忽略 RoundRobinList,将其视为一个常规列表。我稍后会解释它。这两行确实会重置路由表以使用新条目;但是,直到我们调用这行代码,路由器才知道新的配置。

this.owner.Extensions.Find<RoutingExtension>().ApplyConfiguration(mRouterConfiguration);

删除逻辑类似,只是它在列表中查找要删除的服务,然后将列表应用为路由表。

添加负载均衡支持

我必须承认这真的不是负载均衡,更像是以轮循方式调用服务。尽管如此,它允许在机器之间分配负载。通常,在将服务列表添加到路由表时,我们使用常规的 List;但是,这将导致路由器总是调用列表中的第一个服务,而只有在第一个服务无响应时才调用第二个服务。通过将 List 更改为 RoundRobinList,我们跟踪了返回的最后一个项目,并返回下一个项目。RoundRobineList 的概念不是我发明的,我在这个 链接 上找到了它。

单点故障

设计中有一个主要问题:我们有两个单点故障:一个是路由器,另一个是发现代理。发现代理在此处用于以最高效的方式获取正在运行的服务列表,但您不需要使用它,您可以使用 UDP 扫描来完成同样的事情并获取服务列表。虽然速度较慢,但它不会引入单点故障。

然而,对于路由器,我将提出的解决方案是拥有多个路由器。要么客户端现在需要知道两个路由器并确保如果一个调用失败,就使用另一个(不理想),要么我们使用 Windows 内置的网络负载均衡器 (NLB) 并为指向两个或多个物理路由器的路由器使用虚拟 IP。

如果您计划使用 NLB,那么您也可以将其用于您的发现代理,以防您不想进行 UDP 扫描。

摘要

虽然此演示中有许多不错的方面,但也有您应该了解的限制。UDP 通常不是 IT 人员喜欢在网络中开放的协议。因此,请记住防火墙可能会禁用 UDP。我尝试使用 WCF 4.0 的点对点通信来实现相同的目标,但出于某种原因,在 WAS 中托管服务时,点对点通信不起作用(我仍然在示例中保留了点对点主机类,也许有人能让它工作)。代码也不是生产级别的,我保持代码精简简单,但它缺少错误处理和清理。所以只看代码来学习如何使用这些功能,但不要将其用于生产目的。

为了测试服务,我在解决方案中添加了一个 MsTest 项目。

string routerAddress = @"https:///DynamicRouter/Router.svc";
EndpointAddress endpointAddress = new EndpointAddress(routerAddress);
WSHttpBinding binding = new WSHttpBinding(SecurityMode.None);

ServiceProxy proxy = new ServiceProxy(binding, endpointAddress);
string results;
for (int i = 0; i < 10; i++)
{
    results = proxy.DoWork("Test");
}

测试只调用服务 10 次,我们可以看到两个实例以轮循方式使用。

请确保您的服务已启动(使用自动启动或手动启动),以便它被注册到托管发现。如果服务未启动(首次运行时),则托管发现的列表中没有它,路由器将无内容可路由。为了测试模拟服务关闭,我使用了托管服务的站点的“停止应用程序”功能。不过,我确信服务可以在不发送关闭通知的情况下关闭,您可能想添加清理代码,如果服务无响应,则将其从路由列表中删除。尽管如此,我认为此代码中有许多不错的方面,我相信您可以在您的项目中加以利用,至少这是一个如何动态配置路由器、实现负载均衡以及基于 WCF 发现填充其路由表的有效示例。

© . All rights reserved.