在 Team Foundation Server 2008 的 Team Build Agent 之间进行负载均衡






4.81/5 (26投票s)
在 Team Foundation Server 2008 的 Team Build 2008 Agent 之间进行负载均衡
引言
在 Team Build 2008 中,在编写生成定义时,我们需要将一个生成代理硬编码到此生成定义中。在生成根据其触发器自动排队的情况下,它会被排队到这个硬编码的生成代理。这引发了一个主要问题:有多个可用的/非繁忙的生成代理专用于团队项目,但仍有多个生成排队在一个生成代理上,等待其他生成完成后才能开始。因此,我们需要一种在生成代理之间进行负载均衡的机制,这在 TFS 2010 中在技术上称为生成控制器。但 Team Build 2008 没有直接的方法来解决这个挑战。本文展示了如何在 Team Build 2008 中均衡生成代理之间的负载。
背景
我们在 2009 年第三季度末期在使用 Team Build 2008 时面临了这个挑战,并必须提出解决方案。肯定还有许多其他人仍在 M 2008 版本,并面临这个问题。所以,这里是解决方案。
可视化问题并确定期望行为
在 Teambuild 2008 中,我们可以为单个团队项目设置多个生成代理。但是,当具有相同生成代理(设置为其默认生成代理)的生成排队时,它们会在生成服务器资源管理器中形成一个队列,等待依次处理。因此,对于 2008 版本来说,解决方案必须是一个应用程序,它可以停止繁忙生成代理上的排队生成,并将它们排队到其他可用的生成代理(如果存在)。
如果您在上图所示,我的团队项目有 3 个生成代理:teambuild01
、teambuild02
和 teambuild03
。在某个时间点,有 4 个生成被排队到同一个默认生成代理 teambuild03
,并在队列中等待其状态从“排队中”->“进行中”->“已完成”。现在为了提供解决方案,我期望的是,一旦队列中的第一个生成(Main.Continuous.BIFBuilderGenerator
)完成,它就会检查是否有可能进行负载均衡。发现 teambuild03
的同一队列中有 3 个生成,而其他两个生成代理 teambuild01
和 02 完全空闲,因此它们每个都可以分配一个生成。这个应用程序应该然后停止 teambuild03
上的其他 2 个生成,并将它们分别排队到 teambuild01
和 teambuild02
。这在下图所示。
实现解决方案
现在,我将讨论如何实现上述期望行为作为解决方案。这个解决方案可以分两步完成。
第一步:编写业务逻辑代码,以确定是否需要负载均衡,如果需要,则从哪个生成代理停止哪些生成,并将它们排队到其他哪些可用的生成代理。我们将使用 TFS 客户端 API 来实现这一点。下面是如何从一个代理停止生成并将其排队到其他可用代理的代码(附带内联注释的详细解释)。现在调用 LoadBalancingTeamBuild2008
类的 OptimizeBuilds
方法将真正完成我们的工作,但它需要两个输入:团队基础 URL 和团队项目名称。在下一步中,我们将看到如何获取这些输入并执行这段代码。
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;
namespace RA.TeamFoundation.Build.Events
{
public class LoadBalancingTeamBuild2008
{
//
// This function finds the builds which are in queue, the agents which are free
// and reassigns these builds to the free build agents
//
public void OptimizeBuilds(string strTFSServerUrl, string strTeamProjectName)
{
// get an instance of the tfs and build server
TeamFoundationServer tfsServer = new TeamFoundationServer(strTFSServerUrl);
IBuildServer buildServer =
tfsServer.GetService(typeof(IBuildServer)) as IBuildServer;
IQueuedBuildSpec queueBuildSpec =
buildServer.CreateBuildQueueSpec(strTeamProjectName);
queueBuildSpec.Status = QueueStatus.Queued;
IQueuedBuildQueryResult queueBuildQueryResult =
buildServer.QueryQueuedBuilds(queueBuildSpec);
// get all the builds queued for the given team project
IQueuedBuild[] queuedBuildsArr = queueBuildQueryResult.QueuedBuilds;
// get all the build agents for the given team project
IBuildAgent[] buildAgentArr =
buildServer.QueryBuildAgents(strTeamProjectName);
// iterate through all the build agents of this team project
foreach (IBuildAgent buildAgent in buildAgentArr)
{
buildAgent.Refresh();
// check if this build agent is free, i.e., ok to be considered
if (IsOkToConsiderThisBuildAgent(buildAgent))
{
// iterate through all the queued builds and
// find out which build is perfect to be switched to other agent
foreach (IQueuedBuild queuedBuild in queuedBuildsArr)
{
if (IsOkToConsiderThisQueuedBuild(queuedBuild))
{
if (!AreBuildAgentsSame(queuedBuild.BuildAgent, buildAgent))
{
// confirm that the build is waiting in queue
// and is ok to be switched
if (IsQueuedBuildWaitingForExecution(queuedBuild))
{
// perform the job of cancelling the build from one
// build agent and queueing it in another build agent.
ChangeBuildAgentAndQueueTheBuildAgain
(queuedBuild, buildAgent, buildServer);
break;
}
}
}
}
}
}
}
//
// This function decides if it's ok to consider a given build agent
// for queueing new builds
//
private bool IsOkToConsiderThisBuildAgent(IBuildAgent buildAgent)
{
// returns true if build agent is enabled, i.e. reachable / available
// and there are no builds queued on this agent, i.e. its free and NOT busy
return ((buildAgent.Status == AgentStatus.Enabled) &&
(buildAgent.QueueCount == 0));
}
//
// This function decides if it's ok to switch the given build
// to any other build agent
//
private bool IsOkToConsiderThisQueuedBuild(IQueuedBuild queuedBuild)
{
// returns true if the build definition name does not end
// with pinned / exclude.
// This logic may change depending upon the requirements
// as there are builds we never want to change their agent.
if (queuedBuild.BuildDefinition.Name.ToLower().EndsWith(".pinned") ||
queuedBuild.BuildAgent.Name.ToLower().EndsWith("exclude"))
{
return false;
}
return true;
}
//
// This function decides if the given two build agents are actually the same
//
private bool AreBuildAgentsSame
(IBuildAgent queuedBuildAgent, IBuildAgent newFreeBuildAgent)
{
// returns true if the build agents full path are the same, i.e. both are one.
return queuedBuildAgent.FullPath.Equals(newFreeBuildAgent.FullPath);
}
//
// This function decides if the given build is actually in queue
// i.e. waiting for execution
//
private bool IsQueuedBuildWaitingForExecution(IQueuedBuild queuedBuild)
{
// returns true if the given build is actually waiting in queue
// for its turn to come
// such builds are ready to be switched to other agents.
return queuedBuild.QueuePosition > 1;
}
//
// This function, actually performs the action of stopping a
// queued build and queueing it in another build agent.
//
private void ChangeBuildAgentAndQueueTheBuildAgain
(IQueuedBuild queuedBuild, IBuildAgent buildAgent, IBuildServer buildServer)
{
// create a new build request for this queued build
IBuildRequest newBuildRequest =
queuedBuild.BuildDefinition.CreateBuildRequest();
newBuildRequest.CommandLineArguments = queuedBuild.CommandLineArguments;
newBuildRequest.BuildAgent = buildAgent;
newBuildRequest.RequestedFor = queuedBuild.RequestedFor;
// actually queue the build to the other free build agent
IQueuedBuild newQueueBuild = buildServer.QueueBuild(newBuildRequest);
buildAgent.Refresh();
// stop the queued build from the existing build agent
queuedBuild.Cancel();
queuedBuild.Refresh(QueryOptions.All);
}
}
}
第二步:如何、何时以及谁来执行上述代码:这段负载均衡代码可以在每次生成完成时被调用。因此,每次生成完成后,它都可以检查是否有任何其他生成在队列中需要负载均衡。所以我们基本上需要一种方法来触发代码在 Team Build 的 BuildCompletionEvent
上的执行。在这里,我们将讨论可用的多种方法中的一种。
编写一个具有指定语法的 Web 服务来接收 tfs 事件通知,检索所需数据并执行负载均衡。
namespace RA.TeamFoundation.Build.Events
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class BuildCompletionEventService : System.Web.Services.WebService
{
[SoapDocumentMethod("http://schemas.microsoft.com/TeamFoundation/2005/06/
Services/Notification/03/Notify", RequestNamespace =
"http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03")]
[WebMethod]
public void Notify(String eventXml, String tfsIdentityXml)
{
if (!String.IsNullOrEmpty(eventXml))
{
// Load the XML event string and parse the required information.
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(eventXml);
// retrieve the team foundation URL
XmlNode xmlTFSNode = xmlDoc.SelectSingleNode
("BuildCompletionEvent/TeamFoundationServerUrl");
string strTFSServerUrl = xmlTFSNode.InnerText;
// retrieve the team project name
XmlNode xmlTeamProjectNode = xmlDoc.SelectSingleNode
("BuildCompletionEvent/TeamProject");
string strTeamProjectName = xmlTeamProjectNode.InnerText;
// initiate the load balancing mechanism
LoadBalancingTeamBuild2008 loadBalancing =
new LoadBalancingTeamBuild2008();
loadBalancing.OptimizeBuilds(strTFSServerUrl, strTeamProjectName);
}
}
}
}
这个 Web 服务必须托管在团队基础服务器机器上存在的 WebServices 目录中。例如,在团队基础服务器机器上,路径为 D:\Microsoft Visual Studio 2008 Team Foundation Server\Web Services\,与其他 Web 服务在一起。完成此操作后,我们的 Web 服务也将列在其他团队基础服务器服务中,如下所示。
现在,使用 BisSubscribe 工具向团队基础服务器注册,以便在任何生成的 BuildCompletionEvent
上调用此 Web 服务,如下所示。
D:\Microsoft Visual Studio 2008 Team Foundation Server\TF Setup>BisSubscribe.exe
/eventType BuildCompletionEvent
/address https://:8080/RA.TeamFoundation.Build.Events/
BuildCompletionEventService.asmx
/deliveryType Soap /server my-server-name
BisSubscribe - Team Foundation Server BisSubscribe Tool
Copyright (c) Microsoft Corporation. All rights reserved.
TF50001: Created or found an existing subscription. The subscription ID is 39.
结论
因此,执行上述步骤并使用代码将确保团队基础服务器在每次生成完成时引发 BuildCompletionEvent
,并执行您托管的 BuildCompletionEventService
,该服务负责生成代理之间的生成负载均衡,即,找出是否有任何生成代理是空闲的,并且是否有任何生成在队列中等待处理,然后将这些生成排队到空闲的生成代理,并从繁忙的代理中取消它们。