开源应用程序架构(第 1 卷)
持续集成

C. Titus Brown 和 Rosangela Canino-Koning

持续集成 (CI) 系统是自动定期构建和测试软件的系统。虽然它们的主要优势在于避免构建和测试运行之间出现较长时间间隔,但 CI 系统还可以简化和自动化许多原本繁琐的任务的执行。这些任务包括跨平台测试、定期运行缓慢、数据密集型或难以配置的测试、验证旧平台上的正常性能、检测很少发生的测试失败以及定期生成最新的发布产品。此外,由于构建和测试自动化对于实施持续集成是必需的,因此 CI 通常是迈向 *持续部署* 框架的第一步,在这个框架中,软件更新可以在测试后快速部署到实时系统中。

持续集成是一个及时的话题,尤其因为它在敏捷软件方法中占据突出地位。近年来,各种语言的开源 CI 工具呈爆炸式增长,这些工具在各种架构模型的背景下实施了广泛的功能。本章的目的是描述持续集成系统中实现的常见功能集,讨论可用的架构选项,并检查在选择架构的情况下哪些功能容易或不容易实现。

下面,我们将简要描述一组系统,这些系统体现了设计 CI 系统时可用的架构选择的极端情况。第一个是 Buildbot,它是一个主从系统;第二个,CDash 是一种报告服务器模型;第三个 Jenkins 使用混合模型;第四个 Pony-Build 是一个基于 Python 的分布式报告服务器,我们将用它作为进一步讨论的参考。

9.1. 概览

持续集成系统架构的空间似乎被两种极端情况所支配:主从架构,其中中央服务器指导和控制远程构建;以及报告架构,其中中央服务器聚合来自客户端的构建报告。我们所知的所有持续集成系统都选择了来自这两种架构的某些功能组合。

我们以 Buildbot 为例,它是一个集中式架构,由两部分组成:中央服务器或 *构建主服务器*,它在连接的一个或多个客户端之间调度和协调构建;以及客户端或 *构建从服务器*,它们执行构建。构建主服务器提供一个中央位置来连接,以及有关哪些客户端应该按什么顺序执行哪些命令的配置信息。构建从服务器连接到构建主服务器并接收详细的指令。构建从服务器的配置包括安装软件、识别主服务器以及为客户端提供连接凭据以连接到主服务器。构建由构建主服务器调度,输出从构建从服务器流向构建主服务器,并保存在主服务器上,以便通过 Web 和其他报告和通知系统进行展示。

在架构谱的另一端是 CDash,它由 Kitware, Inc. 用于可视化工具包 (VTK)/洞察工具包 (ITK) 项目。CDash 本质上是一个报告服务器,旨在存储和展示从运行 CMake 和 CTest 的客户端计算机接收的信息。使用 CDash,客户端启动构建和测试套件,记录构建和测试结果,然后连接到 CDash 服务器以存储信息以进行集中式报告。

最后,第三个系统 Jenkins(在 2011 年更名前称为 Hudson)提供了两种操作模式。使用 Jenkins,构建可以独立执行并将结果发送到主服务器;或者,节点可以从属 Jenkins 主服务器,然后主服务器调度和指导构建的执行。

集中式模型和分布式模型都有一些共同的功能,并且正如 Jenkins 所示,两种模型可以在单个实现中共存。但是,Buildbot 和 CDash 彼此形成鲜明对比:除了构建软件和报告构建结果的共性之外,实际上所有其他架构方面都不同。为什么?

此外,在多大程度上架构的选择似乎使某些功能更容易或更难实现?某些功能是否从集中式模型中自然地出现?现有的实现的可扩展性如何?它们能否轻松修改以提供新的报告机制,或扩展到多个软件包,或在云环境中执行构建和测试?

9.1.1. 持续集成软件的功能

持续集成系统的核心功能很简单:构建软件、运行测试并报告结果。构建、测试和报告可以通过从计划任务或 cron 作业运行的脚本执行:此类脚本只需从 VCS 中签出源代码的新副本,进行构建,然后运行测试。输出将记录到文件中,并存储在规范的位置或在构建失败时通过电子邮件发送出去。这很容易实现:例如,在 UNIX 中,整个过程可以用七行脚本为大多数 Python 软件包实现

cd /tmp && \
svn checkout http://some.project.url && \
cd project_directory && \
python setup.py build && \
python setup.py test || \
echo build failed | sendmail notification@project.domain
cd /tmp && rm -fr project_directory

图 9.1 中,未阴影的矩形表示系统内的离散子系统和功能。箭头显示各个组件之间的信息流。云表示构建过程的潜在远程执行。阴影矩形表示子系统之间潜在的耦合;例如,构建监控可能包括监控构建过程本身以及系统运行状况的各个方面(CPU 负载、I/O 负载、内存使用量等)。

[Internals of a Continuous Integration System]

图 9.1:持续集成系统的内部结构

但这种简单性具有欺骗性。现实世界的 CI 系统通常做得更多。除了启动或接收远程构建过程的结果外,持续集成软件还可能支持以下任何其他功能

所有这些潜在的 CI 系统组件的概述如 图 9.1 所示。CI 软件通常实现这些组件的某个子集。

9.1.2. 外部交互

持续集成系统还需要与其他系统交互。存在几种类型的潜在交互

9.2. 架构

Buildbot 和 CDash 选择了相反的架构,并实现了重叠但不同的功能集。下面我们将检查这些功能集,并讨论在选择架构的情况下功能的实现难易程度。

9.2.1. 实现模型:Buildbot

[Buildbot Architecture]

图 9.2:Buildbot 架构

Buildbot 使用主从架构,有一个中央服务器和多个构建从服务器。远程执行完全由主服务器实时编写脚本:主配置指定要在每个远程系统上执行的命令,并在每个先前命令完成后运行它们。调度和构建请求不仅通过主服务器进行协调,而且完全由主服务器 *指导*。没有内置的配方抽象,除了基本的版本控制系统集成(“我们的代码在此存储库中”)以及区分在构建目录上操作的命令与在构建目录中操作的命令。操作系统特定的命令通常直接在配置中指定。

Buildbot 保持与每个构建奴隶的持续连接,并在它们之间管理和协调作业执行。通过持久连接管理远程机器会给实现带来重大的实际复杂性,并且一直是长期存在的 bug 来源。保持稳健的长期网络连接运行并不简单,并且测试与本地 GUI 交互的应用程序通过网络连接具有挑战性。OS 警报窗口尤其难以处理。但是,这种持续连接使资源协调和调度变得简单,因为奴隶完全受主控的支配来执行作业。

Buildbot 模型中设计的那种严格控制使得资源之间的集中式构建协调变得非常容易。Buildbot 在构建主控上实现了主控和奴隶锁定,以便构建可以协调系统级和机器级的资源。这使得 Buildbot 特别适合运行系统集成测试的大型安装,例如与数据库或其他昂贵资源交互的测试。

但是,集中式配置会导致分布式使用模型出现问题。每个新的构建奴隶都必须在主控配置中明确允许,这使得新的构建奴隶无法动态地连接到中央服务器并提供构建服务或构建结果。此外,由于每个构建奴隶完全由构建主控驱动,构建客户端容易受到恶意或意外错误配置的影响:主控在客户端操作系统安全限制内,实际上完全控制着客户端。

Buildbot 的一个限制功能是,没有简单的方法可以将构建产品返回到中央服务器。例如,代码覆盖率统计信息和二进制构建保存在远程构建奴隶上,并且没有 API 可以将它们传输到中央构建主控进行聚合和分发。目前尚不清楚为什么缺少此功能。这可能是由于与 Buildbot 一起分发的命令抽象集有限导致的,这些抽象集专注于在构建奴隶上执行远程命令。或者,这可能是由于决定将构建主控和构建奴隶之间的连接用作控制系统,而不是用作 RPC 机制。

主控/奴隶模型和这种有限的通信通道的另一个后果是,构建奴隶不报告系统利用率,并且无法配置主控以了解高奴隶负载。

构建结果的外部 CPU 通知完全由构建主控处理,并且需要在构建主控本身内实现新的通知服务。同样,新构建请求必须直接传达给构建主控。

9.2.2. 实现模型:CDash

[CDash Architecture]

图 9.3:CDash 架构

与 Buildbot 相反,CDash 实现了一个报告服务器模型。在此模型中,CDash 服务器充当远程执行构建信息的中央存储库,其中包含与构建和测试失败、代码覆盖率分析和内存使用相关的报告。构建在远程客户端上按照自己的时间表运行,并以 XML 格式提交构建报告。构建可以通过“官方”构建客户端提交,也可以通过非核心开发人员或用户在自己的机器上运行发布的构建过程提交。

这种简单的模型之所以能够实现,是因为 CDash 与 Kitware 构建基础设施的其他元素(CMake、构建配置系统、CTest、测试运行程序和 CPack、打包系统)之间紧密的概念集成。该软件提供了一种机制,通过该机制,可以在相当高的抽象级别以操作系统无关的方式实现构建、测试和打包配方。

CDash 的客户端驱动过程简化了客户端 CI 过程的许多方面。决定运行构建是由构建客户端做出的,因此客户端条件(时间、高负载等)可以在客户端启动构建之前由客户端考虑。客户端可以随意出现和消失,轻松实现志愿者构建和“云中”构建。构建产品可以通过简单的上传机制发送到中央服务器。

但是,为了换取这种报告模型,CDash 缺少 Buildbot 的许多便利功能。没有集中协调资源,也无法在具有不可信或不可靠客户端的分布式环境中简单地实现这一点。进度报告也没有实现:要做到这一点,服务器必须允许增量更新构建状态。当然,没有办法同时全局请求构建并保证匿名客户端响应签入执行构建——客户端必须被视为不可靠。

最近,CDash 添加了功能以启用“@Home”云构建系统,其中客户端向 CDash 服务器提供构建服务。客户端轮询服务器以获取构建请求,根据请求执行构建并将结果返回到服务器。在当前实现中(2010 年 10 月),构建必须在服务器端手动请求,并且客户端必须连接才能让服务器提供其服务。但是,将此扩展到更通用的计划构建模型非常简单,在这种模型中,只要有相关的客户端可用,服务器就会自动请求构建。 “@Home”系统在概念上与后面描述的 Pony-Build 系统非常相似。

9.2.3. 实现模型:Jenkins

Jenkins 是一个广泛使用的持续集成系统,用 Java 实现;直到 2011 年初,它被称为 Hudson。它能够充当具有本地系统执行的独立 CI 系统、远程构建的协调器,甚至充当远程构建信息的被动接收器。它利用 JUnit XML 标准进行单元测试和代码覆盖率报告,以集成来自各种测试工具的报告。Jenkins 起源于 Sun,但被广泛使用,并且拥有与之相关的强大的开源社区。

Jenkins 在混合模式下运行,默认情况下使用主控服务器构建执行,但允许使用多种方法执行远程构建,包括服务器启动的构建和客户端启动的构建。但是,与 Buildbot 一样,它主要设计用于中央服务器控制,但已适应支持各种分布式作业启动机制,包括虚拟机管理。

Jenkins 可以通过主控通过 SSH 连接启动的连接或通过客户端通过 JNLP(Java Web Start)管理多个远程机器。此连接是双向的,并支持通过串行传输进行对象和数据的通信。

Jenkins 具有强大的插件架构,该架构抽象出了此连接的细节,这使得能够开发许多第三方插件来支持返回二进制构建和更重要的结果数据。

对于由中央服务器控制的作业,Jenkins 具有一个“锁定”插件,以阻止作业并行运行,尽管截至 2011 年 1 月,该插件尚未完全开发。

9.2.4. 实现模型:Pony-Build

[Pony-Build Architecture]

图 9.4:Pony-Build 架构

Pony-Build 是一个用 Python 编写的概念验证的去中心化 CI 系统。它由三个核心组件组成,如图 9.4 所示。结果服务器充当包含从各个客户端接收的构建结果的集中式数据库。客户端独立包含所有配置信息和构建上下文,以及轻量级的客户端库,以帮助访问 VCS 存储库、管理构建过程以及将结果传达给服务器。报告服务器是可选的,它包含一个简单的 Web 界面,既用于报告构建结果,也可能用于请求新的构建。在我们的实现中,报告服务器和结果服务器在单个多线程进程中运行,但在 API 级别是松散耦合的,可以轻松地更改为独立运行。

此基本模型装饰了各种 webhooks 和 RPC 机制,以促进构建和更改通知以及构建自省。例如,不是将代码存储库中的 VCS 更改通知直接绑定到构建系统,而是将远程构建请求定向到报告系统,该系统将它们传达给结果服务器。同样,不是直接在报告服务器中构建对新构建的推送通知到电子邮件、即时消息和其他服务,而是使用 PubSubHubbub (PuSH) 活动通知协议控制通知。这允许各种消费应用程序通过 PuSH webhook 接收“有趣”事件的通知(目前仅限于新构建和失败构建)。

这种高度解耦模型的优势是巨大的

不幸的是,与 CDash 模型一样,也有许多严重的缺点

Pony-Build 提出关于 CI 的两个其他方面:如何最好地实现 _recipes_,以及如何管理 _trust_。这两个问题是相互交织的,因为 recipes 在构建客户端上执行任意代码。

9.2.5. 构建 recipes

构建 recipes 添加了一个有用的抽象级别,特别是对于使用跨平台语言或多平台构建系统构建的软件。例如,CDash 依赖于一种严格的 recipes;大多数,或者可能是所有使用 CDash 的软件都是使用 CMake、CTest 和 CPack 构建的,而这些工具是为处理多平台问题而构建的。从持续集成系统的角度来看,这是理想的情况,因为 CI 系统可以简单地将所有问题委托给构建工具链。

然而,并非所有语言和构建环境都是如此。在 Python 生态系统中,围绕 distutils 和 distutils2 构建和打包软件的标准化程度不断提高,但目前还没有出现用于发现和运行测试以及整理结果的标准。此外,许多更复杂的 Python 包通过 distutils 扩展机制将专门的构建逻辑添加到其系统中,该机制允许执行任意代码。这是大多数构建工具链的典型特征:虽然可能有一套相当标准的命令要运行,但总会有例外和扩展。

用于构建、测试和打包的 recipes 因此存在问题,因为它们必须解决两个问题:首先,它们应该以平台无关的方式指定,以便单个 recipes 可以用于在多个系统上构建软件;其次,它们必须可以自定义以适应要构建的软件。

9.2.6. 信任

这引发了第三个问题。CI 系统广泛使用 recipes 引入了一个必须由系统信任的第三方:不仅软件本身必须可信(因为 CI 客户端正在执行任意代码),而且 recipes 也必须可信(因为它们也必须能够执行任意代码)。

这些信任问题在严格控制的环境中很容易处理,例如,构建客户端和 CI 系统是内部流程的一部分的公司。然而,在其他开发环境中,感兴趣的第三方可能希望提供构建服务,例如为开源项目提供服务。理想的解决方案是在社区级别支持在软件中包含标准构建 recipes,这是 Python 社区使用 distutils2 所采取的方向。另一种解决方案是允许使用数字签名的 recipes,以便受信任的个人可以编写和分发签名的 recipes,而 CI 客户端可以检查是否应该信任这些 recipes。

9.2.7. 选择模型

根据我们的经验,用于持续集成的松散耦合的 RPC 或 webhook 回调模型非常容易实现,只要忽略任何涉及复杂耦合的紧密协调要求。无论构建是在本地还是远程驱动,远程检出和构建的基本执行都有类似的设计约束;有关构建的信息收集(成功/失败等)主要由客户端需求驱动;而按架构和结果跟踪信息则涉及相同的基本要求。因此,可以使用报告模型非常轻松地实现基本 CI 系统。

我们发现松散耦合模型也非常灵活且可扩展。添加新的结果报告、通知机制和构建 recipes 很容易,因为组件清晰地分离且相当独立。分离的组件具有明确委派的要执行的任务,并且也易于测试和修改。

在类似 CDash 的松散耦合模型中,远程构建的唯一挑战在于构建协调:与实现的其余部分相比,启动和停止构建、报告正在进行的构建以及协调不同客户端之间的资源锁定在技术上要求很高。

很容易得出结论,松散耦合模型在各方面都“更好”,但显然,只有在不需要构建协调的情况下才如此。应根据使用 CI 系统的项目的需要做出此决定。

9.3. 未来

在思考 Pony-Build 的过程中,我们想出了几个希望在未来的持续集成系统中看到的功能。

9.3.1. 总结

上面描述的持续集成系统实现了适合其架构的功能,而混合 Jenkins 系统从主/从模型开始,但添加了来自更松散耦合的报告架构的功能。

人们很容易得出结论,架构决定功能。当然,这是无稽之谈。相反,架构的选择似乎引导或指导开发走向特定的功能集。对于 Pony-Build,我们惊讶于我们最初选择 CDash 式报告架构在多大程度上推动了后续的设计和实施决策。一些实施选择,例如在 Pony-Build 中避免使用集中式配置和调度系统,是由我们的用例驱动的:我们需要允许远程构建客户端的动态附加,这在 Buildbot 中很难支持。我们没有实现的其他功能,例如 Pony-Build 中的进度报告和集中式资源锁定,是理想的,但如果没有令人信服的要求,添加起来太复杂了。

类似的逻辑可能适用于 Buildbot、CDash 和 Jenkins。在每种情况下,都有一些有用的功能缺失,可能是由于架构不兼容造成的。然而,从与 Buildbot 和 CDash 社区成员的讨论以及阅读 Jenkins 网站可以看出,所需的功能很可能是首先选择的,然后使用允许轻松实现这些功能的架构来开发系统。例如,CDash 为一个核心开发人员数量相对较少的社区提供服务,他们使用集中式模型开发软件。他们最主要的考虑是让软件在核心机器组上正常运行,其次是接收来自技术精通用户的错误报告。同时,Buildbot 在越来越多的复杂构建环境中使用,这些环境中有多个客户端需要协调才能访问共享资源。Buildbot 更灵活的配置文件格式及其用于调度、更改通知和资源锁定的众多选项比其他选项更适合这种需求。最后,Jenkins 似乎旨在易于使用和简单的持续集成,并提供完整的 GUI 用于配置它以及在本地服务器上运行的配置选项。

开源开发的社会学是将架构与功能联系起来的另一个令人困惑的因素:假设开发人员根据项目的架构和功能是否适合他们的用例来选择开源项目?如果是这样,那么他们的贡献通常会反映出已经适合项目的用例的扩展。因此,项目可能会被锁定在特定的功能集中,因为贡献者是自我选择的,并且可能会避免架构不适合他们自己所需功能的项目。在我们选择实现一个新的系统 Pony-Build 而不是为 Buildbot 做贡献时,这当然对我们来说是正确的:Buildbot 的架构 simply 不适合构建数百或数千个包。

现有的持续集成系统通常构建在两种截然不同的架构之一周围,并且通常只实现部分所需的功能。随着 CI 系统的成熟和用户群的增长,我们预计它们会增加额外的功能;然而,这些功能的实现可能会受到基本架构选择的限制。该领域的演变将很有趣。

9.3.2. 致谢

感谢 Greg Wilson、Brett Cannon、Eric Holscher、Jesse Noller 和 Victoria Laidler 关于 CI 系统(特别是 Pony-Build)的有趣讨论。多名学生为 Pony-Build 开发做出了贡献,包括 Jack Carlson、Fatima Cherkaoui、Max Laite 和 Khushboo Shakya。