CI 系统并行功能对比

Blog
Author:
Ori HochOri Hoch
Published On:
6月 24, 2021
Estimated reading time:
3 minutes

上一篇博文中,同样围绕这个主题,我写了一些独立于 CI/CD 平台的并行化解决方案。紧接其后,在本篇文章中,我将回顾常见 CI/CD 平台的并行化功能,其中一些功能我们在以前的文章中已提到:Jenkins、 BambooGitLabCircleCI Bitbucket。这些 CI 系统使用简单的声明性语法,并用Web UI 进行状态报告并生成日记,帮助团队突破单机的限制,进行并行化开发。

为什么需要多个机器并行化?

随着项目的增长和复杂化(这里更多指的是代码膨胀),构建过程中的需求也会随之增长。运行集成测试可能需要重新设置以支持基础架构,并执行其他耗时的任务。虽然单一机器的性能已相当强大,但慢慢地还是会触到极限。此外,我们可能也需要在不同的操作系统或不同硬件的机器中运行某些进程(例如,用于机器学习的 GPU)。种种原因,总之,摆脱单一机器限制的并行化需求非常普遍。

大多数 CI/CD 平台都能完美地解决这些需求,无论是自托管的解决方案,如Jenkins,可与其他解决方案集成,以设置额外的机器(例如,使用 Kubernetes AWS)。或者,使用 CI/CD SAAS 提供所需基础设施的解决方案。

Jenkins 管道,让工作同时同步运行

jenkins

Jenkins 支持许多不同的定义作业的方法,借助插件,方式甚至更多。我将重点介绍最新的,以及较受欢迎的声明性语法管道的方法,这些方法不需要额外的插件。

让我们从简单的并行指令开始,这个指令可出现在管道中的任何阶段(该阶段已使用并行指令除外,不支持嵌套并行指令)。在并行指令下,可以使用所有可用的标准指令、定义阶段和步骤,包括嵌套阶段(只要本身不是并行指令)。

在并行阶段中,你可以根据各个阶段的需要,指定代理标签,将测试分配于不同的服务器中。

 

下面的示例预设了一个 Makefile,其默认任务为编译成二进制文件,附加任务包括发布到工件存储,或从工件存储下载,并在二进制文件上运行测试:

pipeline { 
    agent any 
    stages { 
        stage('Compile & Publish Binary') { 
            steps { 
                make && make publish 
            } 
        } 
        stage('Run Tests') { 
            when { 
                branch 'master' 
            } 
            failFast true 
            parallel { 
                stage('Test A') { 
                    agent { 
                        label "for-test-a" 
                    } 
                    steps { 
                        make download && make test_a 
                    } 
                } 
                stage('Test B') { 
                    agent { 
                        label "for-test-b"
                    } 
                    steps { 
                        make download && make test_b 
                    } 
                } 
            } 
        } 
    } 
} 

在前面的示例中,只有在编译和发布完成后,并行测试才会启动。“failFast true”指令也包含在其中,因此当其中一个测试失败时,构建则立即失败,运行中止。如果 “failFast true”指令没有包含在内,那么无论测试成功与否,Jenkins runner 都需要等待两个测试完成后,才结束构建。适时,下一个阶段也才会开始。

Matrix 指令有点复杂,但对于不同平台上的测试非常实用。在 matrix 指令下,你需要定义一组轴,每个轴包含名称和一系列的数值。轴值的所有不同组合都被合并,同时并行运行。许多指令(如 agent 指令)可以在 matrix 指令下设置,并使用扩展的轴值。

在下面的示例中,我们扩展了前面的示例,以便在不同的操作系统上运行测试:

pipeline { 
    agent any 
    stages { 
        stage('Compile & Publish Binary') { 
            steps { 
                make && make publish 
            } 
        } 
        stage('Run Tests') { 
            when { 
                branch 'master' 
            } 
            failFast true 
            matrix { 
                agent { 
                    label "${PLATFORM}-for-test-${TEST}" 
                } 
    axes { 
        axis { 
            name 'PLATFORM' 
            values 'linux', 'mac', 'windows' 
        } 
        axis { 
            name 'TEST' 
            values 'a','b'
        } 
    } 
    stages { 
                    stage('Test') { 
                        steps { 
                            make download && make test_${TEST} 
                        } 
                    } 
                } 
            } 
        } 
    } 
} 

在本例中,测试将运行 6 次,分别针对不同的平台和测试组合。我们使用 agent 指令为每个组合选择不同的服务器,并在执行阶段使用 TEST 变量。

诚然,matrix 功能对于可自然并行的任务(如多平台测试)颇有成效,但许多其他任务很难进行分割。比如,C++ 应用程序的编译进程中会产生一个二进制,这个进程不能轻易地分割并行。也因此,Incredbuild 与 Jenkins 集成,成功解决了这一痛点,帮助团队突破构建进程的固有限制,将并行进一步优化。

CircleCI 并行步骤

CircleCI 的工作流(workflows)功能,提供了一种相对简单的方法来配置多项作业及其运行顺序。将工作流功能与工作并行(job parallelism)属性结合使用,单项作业可并行分配于多个执行器中,每个执行器处理该作业的部分进程。

使用工作流功能,第一步,先设定需要运行的作业,以及各个作业的运行步骤。接着,需要配置工作流,工作流是所有并行作业的嵌套列表。你可以选择将作业之间的依赖关系包含在内,以规范作业的运行顺序。

首先,我们将定义作业,这些是我们在工作流中使用的基本构建块: 

jobs: 
  compile_and_publish: 
    docker: 
      image: circleci/buildpack-deps:focal 
    steps: 
      - checkout 
      - run: 
          command: make && make publish 
  make: 
    parameters: 
      task: 
        type: string 
        default: "" 
    docker: 
      image: circleci/buildpack-deps:focal 
    steps:
      - checkout 
      - run: 
          command: make << parameters.task >>

然后,我们设定执行顺序,将这些作业的工作流排列组合:

workflows: 
  version: 2 
  compile_publish_and_test: 
    jobs: 
      - compile_and_publish 
      - make: 
          task: test_a 
          requires: compile_and_publish 
      - make: 
          task: test_b 
          requires: compile_and_publish 

在上面的示例中,compile_ 和 _publish 作业先运行,只有这两步成功了,测试作业才会启动,且并行运行。无论成功与否,CircleCI 需要等待两个测试作业都完成,才能继续下一个作业。但是,作业状态是实时汇报的,CircleCI UI可以帮助取消运行或重试特定作业,这个步骤不需要等待整个工作流完成。CircleCI 提供了更多功能来配置作业的依赖关系,请查看文档以了解更多详细信息:when 属性Background 命令从步骤中结束作业when 步骤requires在工作流中使用 when

我们可以使用 matrix 作业进一步扩展工作流,以给定的参数组合运行作业:

workflows: 
  version: 2 
  compile_publish_and_test: 
    jobs: 
      - compile_and_publish 
      - make: 
          requires: compile_and_publish 
          matrix: 
            parameters: 
              test: [test_a, test_b] 

如果 make 任务有一个额外的平台参数,我们可以运行以下命令来执行给定参数的所有组合:

matrix: 
            parameters: 
              test: [test_a, test_b] 
              platform: [windows, mac, linux] 

CircleCI 还有一个非常有用的功能,可以在单个作业中支持并行化。假设有一个作业运行文本文件中定义的 make 任务,每行一个make任务:

jobs: 
  tests: 
    docker: 
      image: circleci/buildpack-deps:focal 
    steps: 
      - checkout 
      - run: 
          command: while read TASK; do make $TASK; done < tests.txt 

使用 CircleCI 并行化属性可以设定启动的执行器数量。然后借助 CircleCI CLI工具,在多个执行器上运行同一任务。这样,每个执行者都可以分担任务的一部分进程。值得注意的是,此选项不允许拆分现有 make 任务,因为我们依然受到构建系统的限制。不过这个功能确实有助于一组任务自动拆分为多个独立任务,且并行运行:

jobs: 
  tests: 
    docker: 
      image: circleci/buildpack-deps:focal 
    parallelism: 4    
    steps: 
      - checkout 
      - run: 
          command: for TASK in $(circleci tests split tests.txt); do make $TASK; done 

上面的示例将启动 4 个执行器,每个执行器将运行 tests.txt 文件中定义的部分任务。CircleCI tests split命令有不同的选择,其中最实用的是基于计时数据进行拆分的功能,详细信息,请参阅 CircleCI 并行化文档。

CircleCI 并行化功能多样且实用,在 CI 审查系统功能最为丰富。其中,基于时间数据进行分割的功能特别有用,但相当复杂,因此我建议大家从文件中了解更多内容。如上所述,尽管这些功能很好,但对于解决构建系统的固有局限,还是无能为力。对于拆分单一的任务,如 C++ 编译,甚至进一步并行化,也是鞭长莫及。开发大型 C++ 项目,深度并行至关重要,这也是  Incredibuild 发挥价值的地方。

Atlassian Bamboo 

Bamboo logo

Bamboo 中设置 CI/CD 管道有多种方法,我们将重点介绍 Bamboo specs yaml。以此类推,这些设置步骤和概念也适用于其他方法。

Bamboo 只支持基础并行,可同步运行并行定义下的所有作业,因此不需要任何特殊的设置来支持并行化:

version: 2 
plan: 
  project-key: MYPROJECT 
  name: My Project 
  key: BUILD 
 

stages: 
  - Compile and publish: 
      jobs:  
        - Compile and publish 
  - Test: 
      jobs: 
        - Test A 
        - Test B 
 

Compile and publish: 
  tasks: 
    - script: 
        - make && make publish 
 

Test A: 
  tasks: 
    - script:  
        - make test_a 
 

Test B: 
  tasks: 
    - script:  
        - make test_b

在本例中,只有在编译和发布完成后,测试 a 和测试 b 才会并行运行。Bamboo 没有任何指令来控制任务之间的依赖关系,因此这两个并行任务将一直运行,直到完成。Final 任务的概念是,无论任何构建任务或其他最终任务是否失败,Final 任务都将始终执行。即使手动停止构建,Final 任务也将执行。详情请阅读文档

不过,与其他 CI 系统相比,Bamboo 的并行化功能非常有限,因此我们必须依靠构建系统,或 DevOps 工程师来进一步优化构建进程。再一次强调,对于 C++ 构建,Incredibuild 的解决方案可帮助实现所需并行优化,让开发人员专注开发,减少漫长的等待时间。

Bitbucket 管道并行步骤

Bitbucket 管道使用并行指令,步骤简单、直观:

pipelines: 
  default: 
    - step: 
        script: 
          - make && make publish 
    - parallel: 
        - step: 
            script: 
              - make test_a 
        - step: 
            script: 
              - make test_b 

与前面的示例相同,在本例中,只有在构建步骤成功完成后,测试才会并行运行。无论构建是否成功,这两个测试都将在构建完成时才并行启动。BitBucket  没有配置任务之间依赖关系的功能。更多详细信息,请参阅并行步骤的完整文档

Bamboo 类似,Bitbucket 并行化功能非常有限,(这两种产品都归属于Atlassian)。这意味着大多数并行化和构建优化工作都必须由 DevOps 工程师完成(针对 C++ 构建,我们建议使用 Incredibuild 解决方案)。

GitLab CI 并行化

Gitlab

GitLab 并行化无需任何特定指令。同一阶段中的所有作业并行运行,这与任何管道体系结构的工作方式相同:

stages: 
  - build 
  - test 
 

image: ubuntu 
 

build_and_publish: 
  stage: build 
  script: 
    - make && make publish 
 

test_a: 
  stage: test 
  script: 
    - make test_a 
 

test_b: 
  stage: test 
  script: 
    - make test_b 

在上面的示例中,测试 a b 属于同一阶段,因此只有在 build_ _publish  完成后,测试才会并行运行。GitLab 具有处理任务和阶段依赖关系的高级配置功能,如需了解更多详细信息,请阅读相关文档以:有向无环图父子管道

GitLab 还支持单个作业内的并行化。Parallel 属性设定启动的作业实例数。每个作业实例将接收环境变量信息,包含已启动实例总数和每个作业实例索引:

test: 
  stage: test 
  parallel: 10 
  script: 
    - echo Running job instance $CI_NODE_INDEX out of a total of $CI_NODE_TOTAL jobs 

上述示例将并行启动 10 个作业实例,每次运行的作业实例将在

CI_NODE_INDEX 环境变量中显示不同的作业索引,CI_NODE_INDEX 环境变量的总值为 10,即作业总数为 10

使用作业并行属性的另一个可用功能是定义数值矩阵,而后并行作业实例开始处理所有值的组合:

test: 
  stage: test 
  parallel: 
    matrix: 
      - PLATFORM: windows 
        OS: ["8", "8.1", "10"] 
      - PLATFORM: linux 
        OS: [ubuntu, debian, fedora] 
      - PLATFORM: mac 
        OS: [mojave, catalina, bigsur] 
  script:  
    - echo test $CI_NODE_INDEX out of $CI_NODE_TOTAL 
    - echo platform: $PLATFORM  OS: $OS 

上面的示例中,总计 9 个作业并行运行,每个作业具有不同的环境变量值,如脚本 echo 所示。

触发下游管道时,你可以使用include属性,代替脚本:

deploy: 
  stage: deploy 
  parallel: 
    matrix: 
      - PROVIDER: aws 
        STACK: [monitoring, app1] 
      - PROVIDER: ovh 
        STACK: [monitoring, backup] 
      - PROVIDER: [gcp, vultr] 
        STACK: [data] 
  trigger: 
    include: path/to/child-pipeline.yml 

上述示例将对所有值组合触发child-pipeline.yml  6次。

有关如何使用下游管道的详细信息,请参阅触发器文档

GitLab 的解决方案功能非常丰富,下游管道的使用让代码可重用,这极大地帮助提升构建时性能。不过,与前面工具一样,GitLab 仍然无法拆分现有任务,将并行贯彻到底,而 Incredibuild 的解决方案,可与任何 CI 系统(包括GitLab)完美集成,将单个 Make 任务拆分、并行执行。

CI并行产品对比

虽然大部分主流的 CI/CD 系统都支持某种形式的并行化,但是功能参差不齐,一些系统只有基础的并行化选择,而另一些却提供了更加丰富、实用的独特功能。下表总结了这些差异: 

  Jenkins  CircleCI  Bamboo  Bitbucket  GitLab 
并行阶段/步骤          
基于数值矩阵的并行          
可跨操作系统/服务器并行          
启动同一作业的多个实例          
根据不同的数据自动分配工作          
控制任务依赖关系          

总结

读完这篇文章后,希望能让大家更好地理解如何使用 CI 系统的并行功能。这项功能,可以帮助突破单个计算机的限制。另外,还可享受 CI 系统 UI 的附加功能,构建作业状态在 UI 中清晰展示,同时只需简单的声明性语法定义管道。

需要注意的一点,是构建进程的固有局限性。如,构建进程在单个 Makefile 任务中编译了单个二进制文件,CI/CD 平台将无法进行并行处理。这已超出了 CI 系统的能力,因为 CI 系统不了解内部构建执行。为了进一步优化,我们需要一些与 C++ 构建工具紧密结合的技术,如 Incredibuild 解决方案。

另一个我没提到的要点,是工件处理。在这些示例中,构建二进制文件之后,我们希望该文件可用于文件中运行测试的所有并行实例。这个问题,我们将在后续的文章中心详细讨论。每个 CI 系统都有不同的功能和工件用法,但无论你的 CI 系统是怎样的,都能找到合适的方案。

speed up c++