Jenkins 并行构建高级操作(以及 Jenkins 分布式构建)

Blog
Author:
Joseph SibonyJoseph Sibony
Published On:
7月 31, 2024
Estimated reading time:
1 minutes

Jenkins 是一个在灵活性方面非常优秀的持续集成工具。每一个简单的事情都可以通过十种不同的方法来完成,包括并行构建。

本文将通过在 Jenkins 的并行阶段指令中引入一些命令式逻辑,来扩展其灵活性。

一些 Jenkins 的基础

在进入并行化和所有那些复杂的内容之前,让我们先打下基础,非常简单但十分重要。

本质上,Jenkins 是一种非常复杂的运行脚本的方法。它有广泛的插件,基本上可以运行任何由执行作业的代理支持的语言,并且有一个丰富的 DSL 来构建作业以运行脚本。

然而,真正改变游戏规则的是 2016 年引入的流水线插件,它允许灵活的作业执行。突然之间,可以使用舒适的声明式 DSL 定义一个作业,将一个作业分解为多个阶段,甚至可以在不同的代理上运行每个阶段。

结果,Jenkins CI/CD 流水线的引入最终成为并行化作业的推动者,确实,在某个时候,“parallel” 指令被引入到 Jenkins 流水线中。

parallel” 指令允许通过将 想要并行运行的阶段包裹在一起来并行运行多个阶段,如下例所示。

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’)
                }
            }
        }
    }
}

上述示例展示了多个项目的并行构建,只需将我们想要并行化的阶段包裹在一个 “parallel” 范围内。

如果 想要更扎实地了解如何在 Jenkins 和其他 CI 系统中并行化构建,可以查看这篇深入介绍并行指令基础的文章。

那么这篇文章将讨论什么?

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

那么,并行阶段功能的缺陷在哪里?

主要是在动态决定哪些阶段应该并行运行时缺乏。

Jenkins 的并行构建可以基于静态信息和决策,在这种情况下,声明式方法效果很好。但是,当需要动态决策时,就需要一种新方法来进行更高级的 Jenkins 并行构建。

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

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

每当有更改推送到该仓库的任何分支时,就会触发一个构建每个项目工件的 Jenkins 流水线。

这是定义该 pipeline 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. 生成要构建的项目列表,我们将使用 Pipeline Utility Steps 插件的 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 并行构建。

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

这可以通过使用代理指令轻松实现。让我们通过添加代理符号来详细说明前面的示例。为了演示一个简单的例子,我们将使用主代理来处理所有阶段。

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
                }
            }
        }
    }
}

上面的示例演示了如何动态选择构建运行的代理。每个阶段的节点可以根据传递给它的变量在不同的机器上运行,这真的让可能性无穷无尽。

动态并行化的新功能

现在我们已经引入了动态并行运行阶段的逻辑,我们可以做各种新事情来应对其他挑战。

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

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

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

Jenkins 并行构建的最终结果

通过将 Groovy 引入其中,我们现在可以动态组合声明式流水线,以这种方式更灵活地构建我们的项目,并使构建多个项目的过程更快。

需要注意的是,Groovy 可以在 Jenkins 中用于解决广泛的挑战,而并行化只是其中受益的一个领域。

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

通过并行运行多个项目构建,我们使所有项目的整体构建速度更快,但我们没有使每个项目的构建速度更快。更快构建项目的能力,特别是对于 C++(但不仅限于此),基于分解构建步骤并并行化它们的能力,而这既不能通过 Jenkins 并行构建实现,也不能通过 Jenkins 分布式构建实现。

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

在本文提供的示例中,构建命令使用的是 ‘make’ 来构建 C++ 项目。如果我们想加快每个 ‘make’ 命令的速度,我们可以增加它运行的节点的资源,或者将作业分配给较少繁忙的节点,但我们不能使用 ‘parallel’ 指令分解 ‘make’ 命令本身。

换句话说,如果一个项目需要 45 分钟来构建,当它与另一个项目并行运行时,仍然需要 45 分钟来构建。

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

在这里,诸如 Incredibuild 之类的工具可以帮助并行化单个构建的多个阶段。随着项目的增长和构建时间的延长,允许快速开发迭代依赖于在合理的时间内构建项目的能力,因此我们需要进一步加快构建过程。

加速 Jenkins CI/CD pipeline

总结一下,并行化 Jenkins 流水线的各个阶段。使用 Jenkins 并行构建和 Jenkins 分布式构建,可能会非常有用,但它不会使每个单独的构建速度更快。

为此,我们需要其他工具的帮助,例如 Incredibuild。这些解决方案也可以一起使用,例如, 可以并行运行多个构建,并且并行化每个构建的编译阶段。

关于 Jenkins CI/CD 的常见问题

什么是 CI/CD Jenkins

CI/CD 代表持续集成和持续开发。它是一种方法,允许团队在没有停机时间的情况下处理始终在线的应用程序,并且进行更有效的跨团队协作。Jenkins 是一种帮助团队管理其 CI/CD 流水线并提供对运营过程更大控制的工具。

Jenkins CI/CD 的阶段是什么?

Jenkins CI/CD 流水线的四个阶段是检出、构建、测试和部署。第一个用于查找源代码,构建是指编译代码,测试用于查找错误和潜在错误,部署则是将新代码发布到生产环境中。

Jenkins 的用途

Jenkins 是一种 CI/CD 工具,用于管理和控制软件发布过程的几个步骤,从构建到文档甚至测试。

Jenkins 2024 年仍然有用吗?

Jenkins 2024 年仍然有用吗?当然。Jenkins 仍然是市场上最受欢迎的 CI/CD 流水线工具之一,并且仍被各行业的数千个开发团队使用。