我们使用 Azure DevOps Server 2020(本地)。
基本原理
我们的 PR 构建需要很长时间才能构建。使用 msbuild/m:4
标志从干净构建大约需要 45 分钟。管道配置为清理输出并在构建之前运行 git clean 以用于我的测试。
(实际的 PR 构建只清理输出,但不清理存储库)
我试图找出 msbuildmaxCpuCount
参数和我们可以在同一构建服务器上运行的并发 PR 构建数量(即构建代理的数量)的最佳组合。
我有一个没有人使用的专用构建服务器,并在其上配置了 6 个代理。这是我的主题测试。接下来我有一个驱动程序构建在另一个构建服务器上运行,它使用 REST API 在 SUT 上对构建进行排队。
我知道用任意maxCpuCount
值排队 PR 构建。问题是 - 如何对并发构建进行排队?从表面上看 - 只需执行相同的 REST API,SUT 池中的下一个可用代理就会选择并运行它。但是,驱动程序构建也会输出一些进度信息。它实际上不会将一个构建排队,而是 N 一个接一个地构建,同时等待前一个构建完成(使用 REST API)。它会在构建失败后清理并重新启动它们,等等......驱动程序构建输出用于对相同的驱动程序构建进行故障排除(这是代码,因此会受到错误的影响)。
所以我放弃了运行多个 Powershell 作业或 PS 运行空间或线程的选项,因为理解它们的组合输出将是非常有问题的。
相反,我选择在驱动程序构建本身中定义多个作业。因此,如果同时运行 5 个驱动程序构建作业,这意味着我正在 SUT 上同时运行 5 个 PR 构建。
最后,为什么首先要构建驱动程序?为什么不长时间运行控制台脚本?几个原因:
- 我的台式机经常在晚上重启
- 服务器机器有 15 分钟无活动注销。我不想安装鼠标跳动工具 - 不确定我是否被允许。
- 将驱动程序代码安排为 Windows 服务或计划任务不能提供与 Azure DevOps 相同的便利级别来监控其进度。
问题
我的测试有两个维度:
- Max Degree of Parallelism 或 MaxDOP - 映射到 msbuild
maxCpuCount
参数 - Agent Count - 参与测试的 SUT 构建代理的数量
由于我设计它的方式(如上所述),代理计数定义了驱动程序构建中同时运行的作业的数量。
我想实现以下测试矩阵:
MaxDOP X AgentCount = { 4,3,2,1 } X { 3, 4, 5, 6 }
例如,如果MaxDOP = 3 and AgentCount = 5
,那么我将在 SUT 上将 5 个 PR 构建与/m:3
. 总共有 6 个代理意味着这 5 个 PR 构建将同时运行。
我想为4 >= MaxDOP >= 1
和的所有组合做到这一点3 <= AgentCount <= 6
所以我想出了以下驱动程序构建:
parameters:
- name: ctx
type: object
default:
- agentCount: 3
maxDOP: 4
dependsOn: Prepare
- agentCount: 3
maxDOP: 3
dependsOn: MaxDOP_4x3_Agents
- agentCount: 3
maxDOP: 2
dependsOn: MaxDOP_3x3_Agents
- agentCount: 3
maxDOP: 1
dependsOn: MaxDOP_2x3_Agents
- agentCount: 4
maxDOP: 4
dependsOn: MaxDOP_1x3_Agents
- agentCount: 4
maxDOP: 3
dependsOn: MaxDOP_4x4_Agents
- agentCount: 4
maxDOP: 2
dependsOn: MaxDOP_3x4_Agents
- agentCount: 4
maxDOP: 1
dependsOn: MaxDOP_2x4_Agents
- agentCount: 5
maxDOP: 4
dependsOn: MaxDOP_1x4_Agents
- agentCount: 5
maxDOP: 3
dependsOn: MaxDOP_4x5_Agents
- agentCount: 5
maxDOP: 2
dependsOn: MaxDOP_3x5_Agents
- agentCount: 5
maxDOP: 1
dependsOn: MaxDOP_2x5_Agents
- agentCount: 6
maxDOP: 4
dependsOn: MaxDOP_1x5_Agents
- agentCount: 6
maxDOP: 3
dependsOn: MaxDOP_4x6_Agents
- agentCount: 6
maxDOP: 2
dependsOn: MaxDOP_3x6_Agents
- agentCount: 6
maxDOP: 1
dependsOn: MaxDOP_2x6_Agents
jobs:
- job: Prepare
steps:
- Some preparation work
- ${{ each ctx in parameters.ctx }}:
- job: MaxDOP_${{ ctx.maxDOP }}x${{ ctx.agentCount }}_Agents
variables:
AgentCount: ${{ ctx.agentCount }}
strategy:
parallel: ${{ ctx.agentCount }}
timeoutInMinutes: 60000
dependsOn: ${{ ctx.dependsOn }}
steps:
- Queue the PR build on the SUT using ${{ ctx.maxDOP }} for maxCpuCount. The parallel strategy takes care to scale it ${{ ctx.agentCount }} times
- job: Cleanup
dependsOn:
- MaxDOP_1x6_Agents
condition: always()
steps:
- Some cleanup work
挑战在于同步作业。这是因为我不希望具有不同MaxDOP
值的作业同时运行。例如,接受这份工作MaxDOP_3x4_Agents
:
- 最大DOP = 3
- 代理计数 = 4
- 必须在 MaxDOP_4x4_Agents 之后运行
而且当然:
- 第一个作业
MaxDOP_4x3_Agents
必须在Prepare
作业之后运行 - 该
Cleanup
作业必须在最后一个作业之后运行MaxDOP_1x6_Agents
如果不对所有 16 个测试矩阵单元进行明确编码,我就无法找到表达这种语义的方法。忽略dependsOn
要求,2个嵌套for
循环很容易完成这项工作:
parameters:
- name: agentCounts
type: object
default: [3, 4, 5, 6]
- name: maxDOPs
type: object
default: [4, 3, 2, 1]
...
- ${{ each agentCount in parameters.agentCounts }}:
- ${{ each maxDOP in parameters.maxDOPs }}:
- job: MaxDOP_${{ maxDOP }}x${{ agentCount }}_Agents
variables:
AgentCount: ${{ agentCount }}
strategy:
parallel: ${{ agentCount }}
timeoutInMinutes: 60000
dependsOn: ????
steps:
...
唉,我不知道如何在dependsOn
这里指定。
驱动程序构建的一些快照(忽略持续时间,它目前是空运行,没有调用 SUT 上的实际 PR 构建):
使用一个小的测试矩阵: