高级 Jenkins 并行构建(与 Jenkins 分布式构建)

Blog
Author:
Michael ZionMichael Zion
Published On:
11月 8, 2021
Estimated reading time:
1 minutes

就灵活性而言,Jenkins 是一款非常出色的 CI 工具。

每个简单的事情都可以用十种不同的方式来完成,包括并行化构建。

本文将采用 Jenkins 中的并行阶段指令,并将其灵活性扩展至所需的地方。
我们将通过在管道的定义中引入一些命令逻辑来实现这一点。

一些基础

在开始并行化和所有这些复杂工作之前,让我们先打下一些非常基本但非常重要的基础。

本质上,Jenkins 是一种非常精细的脚本运行方式。它有大量插件,您基本上可以运行受运行作业的 agent 所支持的任何语言,并且您拥有丰富的 DSL 来构建作业,以运行脚本。

然而,真正改变游戏规则的是 2016 年引入的管道插件,它允许灵活执行作业。

突然之间,您可以使用舒适的声明式 DSL 定义作业,将单个作业分解为多个阶段,甚至在不同的 agent上运行每个阶段。

因此,Jenkins 管道的引入最终成为了作业并行化的一个促成因素。事实上,在某个时候,“并行”指令被引入了 Jenkins 管道。

“并行”指令通过包装要与之并行运行的阶段,允许并行运行多个阶段,如下例所示。

Jenkinsfile

pipeline {
    agent any
    stages {
        stage(“Compile & Build Binary”) {
            parallel {
                stage(“Build X”) {
                     sh(‘cd /path/to/proj-1 && make && make publish’)
                }
                stage(“Build Y”) {
                      sh(‘cd /path/to/proj-2 && make && make publish’)
                }
            }
        }
    }
}
上述示例通过简单地将我们想要并行化的阶段包装在“并行”范围内,展示了多个项目的并行构建。

如果您想在使用 Jenkins 和其他 CI 系统并行化构建方面拥有更坚实的基础,您可以点击此处查看这篇深入研究并行指令基础知识的文章。

那么,这篇文章是关于什么的呢?

我们将使用一些命令式逻辑在 Jenkins 中扩展并行化,也就是说我们将使用声明式 Jenkinsfile 来允许 Jenkins 并行构建,并在缺少并行阶段指令的地方引入更多功能。

那么,哪里缺乏并行阶段功能呢?

当涉及到动态决定哪些阶段应并行运行时,最缺乏此功能。Jenkins 并行构建可基于静态信息和决策,声明式方法运行良好。但是当需要动态决策时,需要一种新方法来实现更高级的 Jenkins 并行构建。

示例如下:

假设我们有一个包含多个 C++ 项目的 Git 仓库,我们的目标是并行化所有项目的构建。

  • 每周都会向其中添加新项目,并且每天都会修改现有项目。
  • 对于每个项目,仓库的结构是在其根目录中都有一个目录。
  • 每个项目中都有一个用于构建该项目的 Makefile。
  • 在该仓库的根目录中还有一个 Jenkinsfile,其中定义了仓库的 Jenkins 管道
company-a
├── Jenkinsfile
├── project-j
│   └── Makefile
├── project-x
│   └── Makefile
└── project-y
    └── Makefile

每当将更改推送到该仓库的任何分支时,都会触发构建每个项目失真的 Jenkins 管道。这是定义该管道的 Jenkinsfile 文件的内容。

pipeline { 
    agent any 
    stages { 
        stage(“Compile & Build Binary”) { 
            parallel {
                dir(“project-x”) { 
                    stage(“Build X”) { 
                        sh(‘make && make publish’) 
                    }
                }
                dir(“project-y”) { 
                    stage(“Build Y”) {
                        sh(‘make && make publish’)
                    }
                } 
            } 
        } 
    } 
}

现在,假设开发人员在该仓库中添加了一个新的项目 ‘project-j’:

company-a
├── Jenkinsfile
├── project-j
│   └── Makefile
├── project-x
│   └── Makefile
└── project-y
    └── Makefile

开发人员将更改推送到分支,但看不到他们刚刚添加的新项目的失真。

你能猜到这是为什么吗? 这是因为他们没有为新项目在 Jenkinsfile 中添加阶段!

如果是这样,在添加新项目后,Jenkinsfile 应如下所示:

pipeline {
    agent any 
    stages { 
        stage(“Compile & Build Binary”) { 
            parallel {
                dir(“project-x”) { 
                    stage(“Build X”) { 
                        sh(‘make && make publish’) 
                    }
                }
                dir(“project-y”) {
                    stage(“Build Y”) {
                        sh(‘make && make publish’)
                    }
                }
                dir(“project-j”) { 
                    stage(“Build J”) { 
                        sh(‘make && make publish’)
                    }
                }
            } 
        } 
    } 
}

您可能已经猜到了,声明式管道的主要缺陷在于它不是动态的。

声明式本质上不是动态的,但我们可以改变它。

我们将通过减少管道的声明性,使其变得更强大来解决这个问题(就像我们承诺的那样!)。

我们将根据仓库的结构生成并行、动态运行的阶段来实现这一点。

根据仓库结构选择要动态构建的项目可以分两步来完成:

  1. 生成要构建的项目列表——为此,我们将使用管道工具步骤插件的 findFiles 功能
  2. 根据该列表生成并行阶段

我们将使用 Groovy 代码实施这些阶段:

def parallelStages = [:]
def projectsToBuild = []

pipeline { 
    agent any 
    stages { 
        stage("Compile & Build Binary") { 
            steps {
                script {
                    // Find directories (for simplicity’s sake, all directories)
                    def files = findFiles()
                    files.each { f ->
                        if (f.directory) {
                            projectsToBuild.add(f.name)
                        }
                    }
                    
                    projectsToBuild.each { p ->
                        parallelStages[p] = {
                            node {
                                dir(p) {
                                    stage(p) {
                                        sh('make && make build')
                                    }
                                }
                            }
                        } 
                    }

                    parallel parallelStages
                }
            }
        }
    }
}

因此,上面给出的代码可以定位所有的项目目录,创建一个阶段数组,其中每个阶段构建一个不同的项目,然后并行运行所有阶段。

这意味着无需在每次有新项目时更新 Jenkinsfile,所有项目都将并行构建。

跨机器动态分布 Jenkins 构建

我们以 Jenkins 并行构建作为本文的开头,但是我们也提到了 Jenkins 管道插件公布了许多新功能,其中一个功能就是在不同的节点上运行不同的阶段。

这意味着一个阶段可以在某台机器上运行,而同一管道的另一个阶段可以在另一台机器上运行。因此,我们可以拥有 Jenkins 分布式构建,而不仅仅是 Jenkins 并行构建。

当我们想要并行构建多个项目时,这一事实非常有用,因为它允许我们将构建每个项目的负载分配到单独的机器上,从而加快构建时间,甚至满足构建的特定标准。

这可以通过使用 agent 指令轻松实现。

接下来我们通过添加 agent 符号来详细说明前面的示例。

为了使用一个简单的示例进行演示,我们将在所有阶段使用主 agent。

def parallelStages = [:]
def projectsToBuild = []
def chosenAgent = “master”

pipeline { 
    agent any 
    stages { 
        stage("Compile & Build Binary") { 
            steps {
                script {
                    // Find directories (for simplicity’s sake, all directories)
                    def files = findFiles()
                    files.each { f ->
                        if (f.directory) {
                            projectsToBuild.add(f.name)
                        }
                    }                    

                    projectsToBuild.each { p ->
                        parallelStages[p] = {
                            node(chosenAgent) {
                                dir(p) {
                                    stage(p) {
                                        sh('make && make build')
                                    }
                                }
                            }
                        } 
                    }

                    parallel parallelStages
                }
            }
        }
    }
}

上述示例演示了如何动态地选择要运行构建的 agent。

每个阶段的节点可以基于传递给它的变量在不同的机器上运行,这一事实提供了无限的可能性。

具有动态并行化的新功能

现在我们已经引入了并行动态运行阶段的逻辑,我们可以采取新措施来应对其他挑战。

比如,假设我们不想在每次引入变更时构建所有项目,因为其中一个项目比其他项目花费的时间要多,所以我们希望尽可能避免构建它。

我们现在需要做的就是确保 projectsToBuild 列表只包含在当前分支上工作期间修改的项目。

事实上,我们现在可以引入任何一组条件和规则来动态地构建我们想要的项目列表,并在此基础上生成一组并行运行的阶段。

Jenkins 并行构建——最终结果

通过在等式中引入 Groovy,我们现在可以动态地组合声明式管道,这样可以更加灵活地构建我们的项目,并使多个项目的构建进程更快。

应注意的很重要的一点是,Groovy 可在 Jenkins 中用于解决各种挑战,而并行化只是从中受益的一个领域。

动态并行化不能解决什么问题?

通过并行运行多个项目构建,我们总体上加快了所有项目的构建速度,但并没有加快每个项目的构建速度。快速构建项目的能力,特别是对于 C++(但不仅限于 C++),是基于打破构建步骤且并行化的能力,这是 Jenkins 并行构建和 Jenkins 分布式构建无法实现的。

这是因为每当我们在 Jenkins 中并行化阶段时,我们可以在同一节点或多个节点上并行运行多个阶段(如上所示),但我们不会将构建命令本身分解为多个并行化进程。

在本文提供的示例中,所用构建命令是用来构建 C++ 项目的 ‘make’。

如果我们想加快每个 ‘make’ 命令的速度,我们可以增加它运行的节点的资源,或者将作业分配给一个不太繁忙的节点,但是我们不能使用 ‘parallel’ 指令来分解 ‘make’ 命令本身。

换言之,如果一个项目的构建时间为 45 分钟,那么当它与另一个项目并行运行时,其构建时间仍是 45分钟。

因此,虽然并行化各种构建有助于加快 CI,但加快每个构建仍然是一个挑战,需要采用不同的方法来解决。

此处,诸如 Incredibuild 之类的工具可能有助于并行化单个构建的多个阶段。

随着项目的增长和构建时间的延长,是否能够快速开发迭代取决于是否能够在合理的时间内构建项目,因此我们需要进一步加快构建过程。

综上所述,用 Jenkins 并行构建和 Jenkins 分布式构建并行化 Jenkins 管道的各个阶段可能真的有用,但这不会使每个单独的构建更快。

为此,我们需要其他工具的帮助,如 IncredBuild。

这些解决方案也可以一起使用,例如,您可以并行运行多个构建,并且并行化每个构建的编译阶段。

Pipeline_1200x360