预测:多云

对辅助角色上的专用端点进行负载平衡

Joseph Fultz

Joseph Fultz
在一月初,我和 David Browne 致力于开发对 Windows Azure 辅助角色上的内部服务点进行负载平衡的解决方案。 一般来说,辅助角色中的服务端点已发布,因此,负载平衡器可以跨实例进行调用的平衡。 然而,我们面对的客户需要不可公开寻址的端点。 另外,他们还不希望承受某种排队操作的延迟。 如何满足这种要求?

一个内部事件对我们来说意味着探索各种技术和解决方案,在此期间,我和 David 提出了两种不同的应对挑战的方法。 在本月的专栏中,我将阐述我在构建其中一种方法的原型时的设计考虑和所用代码段。

我们不希望给最终解决方案意外造成瓶颈,因此排除了软件代理样式的解决方案。 作为替代,我选择的软件机制将为服务调用提供有效的 IP,而调用节点将在给定持续时间内缓存端点以降低端点解析的开销。 我考虑的三个主要策略是:

• 静态分配:将服务端点分配给每个调用节点

• 集中式控制:一个节点跟踪并控制各个调用节点的分配

• 协作控制:允许任何给定节点指示是否可用于服务调用

这些选项都各有优缺点。

静态分配易于实现,这是它的优点。 如果调用方与工作线程之间的工作单元映射等同,这应该是可行的平衡方法,因为 Web 角色的负载平衡解决方案将进一步平衡辅助角色的调用。

它有两个主要缺点,一个是无法解决服务的高可用性问题,另一个是无法解决调用方与服务节点之间的任何负载矛盾。 如果尝试对静态分配解决方案执行 Morph 以解决问题,该解决方案将几乎肯定开始趋向集中式控制或协作控制。

集中式控制

典型的负载平衡器利用的是集中式控制,它接收运行状况信息,并根据此类信息平衡服务请求。 它收集有关节点的信息、它掌握的所执行分配的信息以及任何检测信号信息,并且将对虚拟 IP (VIP) 所做的请求定向到给定节点。

在此方案中,中心点执行的操作基本相同,但它不充当请求的代理,而是由调用节点要求中央控制器接收合适的地址来执行调用,并且控制器将根据其掌握的信息分配地址(参见图 1)。 调用节点将缓存端点并在预定时限内使用它,到期后,将重复解析过程。

集中式控制

图 1 集中式控制

智能全部位于中央控制器,它必须跟踪确定将调用分配到哪个节点所需的所有必备信息。 这可以像轮询一样简单,即它可能执行完整的数据收集和运行状况分析。 它也可能因各种服务端点具有不同的可用性确定标准而变得复杂,这意味着中央控制器必须掌握工作进程池中服务实现的全部信息。

此实现的最大弊端是:如果中央控制器停机,则系统也会停止。 这意味着必须实现完全独立的解决方案才能解决中央控制器的高可用性问题。

在一些机器人和矩阵系统中,辅助节点将选择一个主控制器,如果检测信号丢失,它们仅需选择一个新主控制器即可。 这种解决方案结合了集中式控制和协作控制,是个不错的设计,但是,它同时显著增加了负载分配机制的实施负担。

协作控制

任何具有为某人分配任务的管理经验的人都知道,实际让某人执行任务存在现实障碍。 经验证明,最好直接询问某人是否有时间完成任务,假定他能正确估计自己的工作能力,则这是确定他是否确实有时间完成任务的最佳方法。 我将就此模型继续说明。

我的想法是:每个调用节点都从其当前分配的服务端点启动,并询问它是否仍然可用(参见图 2)。 如果不可用,节点将继续轮询可用池,直到得到正面的响应(参见图 3)。 此后,将使用与上述相同的到期缓存机制来降低端点解析开销。

协作控制

图 2 协作控制

平衡到另一个节点

图 3 平衡到另一个节点

此设计的优点是:高可用性在设计阶段解决,并且确定可用性的节点与实际能够为调用方服务的工作线程之间应有高保真度。 每个服务节点都应使智能融入其实现,以便感知特定于服务的情况,而该服务决定了其可用性。 此智能超出了 CPU 以及类似情况的范畴,可以是诸如节点访问的下游系统的可用性等。 因此,如果节点返回负面信息、错误或超时,调用节点将查询下一个可用服务节点,如果后者可用,将使其服务调用指向该端点。

此解决方案的较大弊端在于:它要求在防御系统的两侧实现,才能在调用方与端点之间提供可用性服务和调用协议,以便确定端点可用性。

原型

在此示例中,将执行下列操作:

  • 设置标准机制以确定可用性
  • 调用方将短暂缓存可用节点
  • 我可以在设定的时限内禁用节点,在此时限内,所有调用将平衡到单个节点上
  • 当节点再次变得可用时,调用方应该能够返回到上一个节点

一些需要注意的问题:首先,我没有执行任何智能化地确定可用性的工作,前提是我仅设置平衡机制,而不考虑决策背后的智能。 另外,我没有处理错误和超时,不过,错误和超时的处理方式与从可用性查询获得负面结果时的处理方式相同。 最后,我仅捕获部署中的所有辅助角色,但在真正的实现中,可能需要更智能的方法(例如注册表机制)来确定所有可用服务端点;或者仅尝试命中每个端点上的服务,并将成功的调用标记为可能端点。 代码甚至可以询问特定的专用端点。此外,如果每个服务的代码都不同,则可以将代码用作区分方式。

首先要做的是从部署中的辅助角色获取 IP 列表。 要完成该目标,我需要配置角色。 对于辅助角色,我将打开“配置”窗口,然后添加内部服务端点,如图 4 所示。

将内部服务端点添加到辅助角色

图 4 将内部服务端点添加到辅助角色

我还在部署中将辅助角色标记为专用服务。 通过使用 RoleEnvironment 对象的 API 和标签,很容易提取节点:

if (_CurrentUriString == null) {
  System.Collections.ObjectModel.ReadOnlyCollection<RoleInstance> 
    ServiceInstances = null;
  System.Collections.ObjectModel.ReadOnlyCollection<RoleInstance> 
    WebInstances = null;

  ServiceInstances = 
    RoleEnvironment.Roles["PrivateServices"].Instances;
  WebInstances = 
    RoleEnvironment.Roles["ServiceBalancingWeb"].Instances;

我将使用节点的序号匹配开始节点以检查可用性。 如果 Web 角色数多于辅助角色数,我将使用 mod 函数来匹配开始节点。 有了实例和用于测试可用性的开始节点,我就可以开始执行循环操作并测试端点了(参见图 5)。

图 5 测试端点

while (!found && !Abort) {
  string testuri = 
    ServiceInstances[idxSvcInstance].InstanceEndpoints[
    "EndPointServices"].IPEndpoint.ToString();
  found = CheckAvailability(testuri);
  if (found) { 
    ServiceUriString = testuri; 
  }
  else {
    idxSvcInstance++;
    if (idxSvcInstance >= ServiceInstances.Count) { 
      idxSvcInstance = 0; 
    }
    loopCounter++;
    if (loopCounter == ServiceInstances.Count) { 
      Abort = true; 
    }
  }
}

请注意,有一个对 CheckAvailability 函数的调用(参见图 6)。 我在该函数中创建了一个绑定,由于端点仅供内部使用,因此绑定的安全模式为“None”。 我将服务客户端实例化,设置合理的超时并返回调用的值。

图 6 CheckAvailability

static public bool CheckAvailability(string uri) {
  bool retval = true;
  Binding binding = new NetTcpBinding(SecurityMode.None);
  EndPointServicesRef.EndPointServicesClient endpointsvc = 
    new EndPointServicesRef.EndPointServicesClient(binding, 
    new EndpointAddress(@"net.tcp://" + uri));
  endpointsvc.InnerChannel.OperationTimeout = 
    new System.TimeSpan(0,0,0,0, 5000);

  try {
    retval = endpointsvc.IsAvailable();
  }
  catch (Exception ex) {
    // Todo: handle exception
    retval = false;
  }
  return retval;
}

如果调用期间出错,我将返回“false”,从而允许循环移至下一个节点并检查其可用性。 不过请注意,为了确定代码当前在其下执行的 Web 角色实例编号,我解析了实例 ID。 要完全达到此目的,我必须打开任意内部端点(可能曾经为外部端点)。 如果我没有这样做,ID 将不会递增,解析也将无用,因为每个节点都与唯一节点相似。

另一种创建节点列表的方法是循环访问所有节点,在列表中标识当前正在执行的节点的顺序位置,或者仅按 IP 的最后一位八进制数对其进行排序。 后两种方法可能更可靠一点,但对于此特定示例,我仅使用实例 ID。

另一个需要注意的地方是:ID 的结构在实际部署中与在开发结构中不同,因此,我必须在解析代码中处理它,如下所示:

string[] IdArray = 
  RoleEnvironment.CurrentRoleInstance.Id.Split('.');
int idxWebInstance = 0;
if (!int.TryParse((IdArray[IdArray.Length - 1]), 
  out idxWebInstance)) {
  IdArray = RoleEnvironment.CurrentRoleInstance.Id.Split('_');
  idxWebInstance = int.Parse((IdArray[IdArray.Length - 1]));
}

这段代码将返回用于在静态变量中缓存的合适端点 IP。 接下来,我设置一个计时器。 在时间事件触发时,我将端点设置为 null,从而导致代码再次查找要用于服务的有效端点:

System.Timers.Timer invalidateTimer = 
  new System.Timers.Timer(5000);
invalidateTimer.Elapsed += (sender, e) => 
  _CurrentUriString = null;
invalidateTimer.Start();

我在此处使用了 5 秒的短暂时限,因为我想确保下面这一点:在短暂的测试执行期间,一旦禁用了一个服务端点,我至少可以将一个 Web 角色弹到另一个端点。

运行演示

现在,我将修改默认页及其隐藏代码,以便仅显示它建立了关联的节点。 我还将添加一个按钮以禁用节点。 两段代码都相当简单。 例如,“禁用”按钮将禁用与请求平衡到的 Web 页关联的服务端点。 因此,对于此测试示例,它会带来一点古怪 UI 的感觉。

我将在 UI 中添加一个标签和一个命令按钮。 在标签中,我将输出已分配端点的 ID,而通过此按钮可以禁用节点。这样,我可以查看与单个端点关联的所有 Web 角色,直到节点重新联机。 在隐藏代码中,我在页面加载中添加一些代码以获取端点(参见图 7)。

图 7 演示页代码

protected void Page_Load(object sender, EventArgs e) {
  string UriString = EndpointManager.GetEndPoint();
  LastUri=UriString;
            
  Binding binding = new NetTcpBinding(SecurityMode.None);
            
  EndPointServicesRef.EndPointServicesClient endpointsvc = 
    new EndPointServicesRef.EndPointServicesClient(binding, 
    new EndpointAddress(@"net.tcp://" + UriString));
  lblMessage.Text = "WebInstacne ID: " + 
    RoleEnvironment.CurrentRoleInstance.Id.ToString() + 
    " is Calling Service @ " + UriString + " & IsAvailable = " + 
    endpointsvc.IsAvailable().ToString();
  cmdDisable.Enabled=true;
}

由于我确实只想说明协作平衡,我没有实现其他服务方法或界面,因此,我仅重用 IsAvailable 方法来说明这一点。

图 8 显示了正在运行的原型应用程序。 首先,您会看到 ID(此 ID 来自开发结构)、IP 以及可用状态。 刷新页将导致请求平衡,因此,端点的显示也可能会不同。 如果单击“禁用”按钮,将运行一小段代码来为当前的端点设置调用 DisableNode:

protected void cmdDisable_Click(object sender, EventArgs e) {
  Binding binding = new NetTcpBinding(SecurityMode.None);
  EndPointServicesRef.EndPointServicesClient endpointsvc = 
    new EndPointServicesRef.EndPointServicesClient(binding, 
    new EndpointAddress(@"net.tcp://" + LastUri));
  endpointsvc.DisableNode();
}

运行演示

图 8 运行演示

DisableNode 方法仅设置布尔值,然后设置计时器以重新启用它。 将计时器的时间设置为稍长于缓存端点的到期时间,以方便在测试运行时进行说明:

public void DisableNode() {
  AvailabilityState.Enabled = false;
  AvailabilityState.Available = false;

  System.Timers.Timer invalidateTimer = 
    new System.Timers.Timer(20000);
  invalidateTimer.Elapsed += (sender, e) => EnableNode();
  invalidateTimer.Start();
}

禁用节点后,来自其他 Web 服务器的后续请求应该都平衡到同一个辅助端点。

深入说明

很显然,本示例对于阐释这一点显得有点简单,我只是希望突出说明对于实际实现需要考虑的一些事项。 我还希望简要介绍 David 解决此问题的实现方法,因为他解决了我没有解决的问题领域。

在此示例中,我有意使调用节点在角色启动过程中运行端点解析代码。 调用节点将在静态成员中缓存端点,或者根据缓存到期时间在实际缓存刷新时缓存端点。 不过,可以将调用节点整合到服务实现过程中,从而允许进行精细控制;与之相对的是,单元处于 IP 和端口组合的级别。 我可能根据要解决的实际问题和服务结构的设计对样式进行取舍。

为了在生产环境中运行,这里列出了一些要考虑以及可能解决的事项:

  • 决定可用性的智能。 这不仅意味着可能检查的事项(CPU、磁盘、后端连接状态等),还意味着应该在是否可用时用于翻转位的阈值。
  • 处理全部返回不可用值情况的逻辑。
  • 有关缓存端点时限的决定。
  • EndpointManager 中的一些附加方法,用于更改设置、从池和常规运行时维护中删除节点。
  • 通常包含在服务实现中的所有典型异常处理和诊断。

虽然我知道这些事项是显而易见的,但我还是坚持“无猜测”的原则。

下面简要概述 David 的方法:他在故障域与升级域之间设置了一个矩阵,尝试通过首选同一域中的端点来确保调用方可用性与端点可用性匹配。 我认为这是一个非常棒的想法。 如果有可能,将我俩的实现结合在一起,就可以确保辅助角色按相同的服务级别协议为您的 Web 提供服务。但如果二者都不可用,您的 Web 能够平衡到任何其他节点。

结论

从配置的角度而言,我希望 Windows Azure 平台不断发展,以实现专用端点的负载平衡。 迄今为止,对于需要执行的任务(原始套接字几乎总是需要内部级别的保护),代码解决方案很可能是最方便的途径。 通过将端点解析调用从实际服务调用中分离并将其加入启动过程,应该可以保持增值代码清晰,不与基础代码混淆。 因此,一旦可以配置这样的功能,就可以在保持服务正常工作的情况下允许禁用平衡代码。

Joseph Fultz 是达拉斯 Microsoft 技术中心的架构师,协助企业客户和 ISV 设计和制作软件解决方案以满足商业和市场需求。他在 Tech·Ed 及类似的内部培训活动中做过讲座。