构建系统之我见

简介: 翻译稿,来是 https://ruudvanasseldonk.com/2018/09/03/build-system-insights,2018年

译者注

在持续集成的流程中,软件构建是最重要的环节之一,负责生成测试和部署用的软件包。在一个大型软件项目中软件构建是一个很复杂和很耗时的工程,提高构建的效率和准确性对软件开发团队的工程效率和软件质量是至关重要的。在Google的Blaze出现之前,大部分C/C++项目都是使用GNU make作为构建工具。make应付小型的项目还可以,对于大型软件项目来说make的构建速度,准确性和多语言扩展等都是一个巨大的挑战,需要耗费大量的开发资源和服务器资源。Blaze的出现像是开了一扇天窗,受其影响出现了Buck/Pants/Please等一系列新的构建工具,继而Google开源了Blaze(命名为Bazel)。构建系统技术领域一下子活跃了起来。本文是一位构建系统的资深专家写于2018年的文章,我尝试翻译一下,和读者一起学习了解构建系统设计的一些核心要素。里面有小部分内容译者没有完全领会,且感觉不影响本文的核心内容,就选择性跳过了(手动狗头)

原文:https://ruudvanasseldonk.com/2018/09/03/build-system-insights

------ 以下为翻译内容 ------

前言

最近一些新一代的构建系统吸引了很多关注,加入到了本已挺多的构建工具的行列里。尽管这些新的构建系统的来源和设计目标不尽相同,但他们有一些共性的东西。笔者最近尝试使用了多种不同的构建系统,引发了一些思考,慢慢地形成了关于构建系统关键原则的一些浅见。在这里我列一下这些见解,探讨一下构建系统该如何工作。

缓存和增量构建

第一个同时也是最核心的见解是关于缓存。可以把构建步骤看成一个纯函数式编程的结构,output = func(input),在输入固定的情况下输出是固定的,这是缓存工作的基础。在构建系统中,构建步骤的输入决定了其构建步骤的输出,换句话说,构建输出的内容应该存储于可根据输入内容寻址的存储结构里。如果输出内容已经存在,其构建步骤可以跳过,因为即使重复执行其输出应该是始终一样的。理想情况下重复执行同样的构建步骤其输出内容应该是字节相等的,但实际场景中可能因为各种原因只能达到功能等价,这通常不是问题。如果输出内容不存在,这时缓存就能派上用场,输出内容可以从远端缓存直接获取,而不需要执行构建过程。

缓存的特点:

  1. 缓存可以存储一个构建目标的不同制品。同一个构建目标,因为代码版本变化(譬如切换了代码分支)或配置变更(譬如打开或关闭了优化开关)会生成不同的制品。如果同样条件(版本和配置)的制品已经在缓存中存在了,就没必要重复执行构建过程。
  2. 缓存可以安全地在多个不相关的代码仓库间共享。一个共享库如果被两个项目使用,没必要构建两次。
  3. 缓存可以安全地在多个机器间共享。持续集成的工作流或另一个同事构建生成的构建制品可以被从远端的缓存直接获取过来,和本地机器无关。

把构建步骤看做一个纯函数使得缓存的实现相对容易,大多数现代的构建工具都使用某种方式的不可变的根据输入寻址的缓存技术。Nix使用这种缓存技术来做系统包的管理,Bazel和SCons用来管理细粒度的构建目标,Stack用它来实现在不同仓库间共享依赖,Goma则根据输入文件和编译命令的哈希来缓存构建制品。

这里有个比较大的问题:获取一个构建步骤的所有输入可能是很困难的。这里的输入包含所有可能影响构建过程的输入文件,构建命令及参数,已经使用的环境变量等。一些构建步骤所使用的工具链可能隐式地从环境中获取一些状态,譬如从CXX环境变量或默认的头文件路径中获取状态。实际上,Nix和Bazel的实现在不遗余力的防止意外获取这些影响构建结果的输入状态,从而为工具链提供一个受控的和可重现的环境。

构建目标定义

构建目标定义内容应该尽可能和源代码放在一起

相比于在一个中心的仓库定义全局的构建目标(然后被各个模块引用),把构建目标定义在各自的模块源代码中的方式在大型的软件项目中可维护性更好。

我所了解的一些最大的代码仓库的软件项目都在使用这一原则。譬如Chromium的构建系统GN,以及它的前辈GYP。也包括Blaze及其衍生的(Pants和Buck等)构建系统。

构建目标应尽可能细粒度

相比于少量大粒度的构建目标,大量细粒度的构建目标对缓存效率和并行构建更加友好。如果一个构建步骤的输入的内容有变更而需要重新构建,更细粒度的构建目标定义可以有效缩小重新构建的范围。互相没有依赖关系的构建目标可以并行构建,所以细粒度的目标定义可以解锁更大的并行度。再者,在构建过程中一个构建目标需要等到其所有依赖的目标都构建完成后才能开始构建,如果构建目标实际只依赖其中一小部分目标,多余的构建目标将没必要的拉长构建过程的关键路径,增加了非必要的构建时间。在CPU核足够多的情况下,细粒度目标的构建总是能显著的快于粗粒度目标的构建。

我在使用Bazel的时候体会到了细粒度目标的重要性,其实这是为什么Bazel能快速的构建大型依赖图的原因。跟Bazel类似的构建工具Buck曾提及它能内部自动把粗粒度目标图转化为细粒度目标图,这也是Buck为什么快的其中一个原因。

延迟计算构建目标定义

延迟计算只计算真正需要构建的目标,可能大部分构建目标不需要解析,延迟计算能带来更高的性能,对大型仓库亦如是。

Bazel的延迟计算的实现方式是每个模块定义一个BUILD文件,以及使依赖路径和文件系统路径一致。使得没有被依赖的构建目标的文件甚至都不想要被加载。

工具链和依赖

构建工具需要管理运行时和编译器工具链

当工具链或一个依赖需要从外部系统获取的时候,一个简单的构建步骤可能变为一个耗时很长的依赖下载或涉及琐碎的配置问题,给构建过程带来很大的不确定性。构建系统的可重复性将深受其害。

一个真正的具有可重复性的构建需要受控的构建环境。
通过语言包管理器固定其所管理的构建环境的依赖包是可重复性的重要的一步,但不能完全解决问题,只要在构建过程中对构建环境有隐性的依赖(譬如通过系统包管理器安装的一些共享库或工具),”只在我的机器上能工作“的问题就始终存在。

有两种方式可以用来创建受控的构建环境:

  1. 追踪所有的隐含依赖,将它们变为显式依赖。在一个沙箱环境中构建,使得所有未声明的依赖无效,能有效的识别出隐式依赖。举个例子,如果构建步骤没有指定GCC的依赖,PATH中就没有gcc可用。Nix就是用这个方式实现的。
  2. 直接固定整个构建环境,而不是具体的依赖,譬如在Docker容器或虚拟机中构建。不过需要注意环境在初始化后不能被修改,例如,在已有容器中运行apt update将会使得构建环境成为不确定的状态。

工程效率

性能是构建系统的一个重要功能,启动时间很重要

软件开发过程中一个常见的操作是修改一小部分代码然后重新构建系统,在这个场景下最关键的是快速定位哪些构建步骤需要重新执行,这个过程中解释器或JIT编译器的开销可能是比较可观的,构建语言的设计也能影响到构建启动的快慢。

以我使用Bazel的经验来看,虽然Bazel编译大型项目很快,但启动过程比较慢。因为Bazel是在JVM上运行的,有时在一个很小的代码仓库里做一个空操作都需要好几秒,而另一个跟它相似的但是用Go实现的构建工具Please就比它快捷得多。构建目标是否能被高效的计算也是性能的一个影响因素,譬如,虽然make和Ninja都是native的工具,但因为Ninja语法更简洁,能更高效的计算构建目标,构建过程就比make要快。

小结

本文中我列出了一些关于构建系统的见解,一些可能相对深入,一些则比较粗浅。一个共同的主题是函数式编程的一些原则同样适用于构建工具,特别地,把构建步骤比作纯函数,把构建制品看做不可变的,使得高效和正确的缓存技术自然涌现出来。在操作实践方面,把构建目标定义尽可能贴近源代码使得代码仓库更易维护,细粒度的构建目标可以解锁更高的并行度从而使构建更快速。像所有好的想法一样,这些见解可能事后看起来很明显,我仍然希望看到它们能被更多的应用于构建系统的设计当中。

相关文章
|
6月前
|
存储 网络协议 API
「译文」CMDB 最佳实践技术指南 -2- 主流的 CMDB 发现技术
「译文」CMDB 最佳实践技术指南 -2- 主流的 CMDB 发现技术
|
6月前
|
Dubbo 前端开发 Java
让你在组建企业级项目时手到擒来——浅谈各类常用工具和框架概述
让你在组建企业级项目时手到擒来——浅谈各类常用工具和框架概述
|
5月前
|
人工智能 数据可视化 大数据
项目管理软件:从单一工具到全面解决方案的演进
【6月更文挑战第24天】从单一工具到全面解决方案,项目管理软件伴随企业复杂性增长而进化。初期专注任务分配和进度,现整合云、大数据、AI,提供集成、灵活、可视化及智能的全面解决方案。技术革新如云计算增强实时访问和数据分析,大数据支持决策,AI助力预测和自动化。未来,软件将进一步智能化、集成化,优化项目管理效率和满意度。
|
4月前
|
监控 Python
系统工程是一个广泛的领域,它涵盖了多个学科和技术的集成,以实现复杂系统的开发、运行和维护。
系统工程是一个广泛的领域,它涵盖了多个学科和技术的集成,以实现复杂系统的开发、运行和维护。
|
测试技术 API 调度
【老司机平台技术】构建应用级项目集成任务通用实验室
欢迎使用老司机平台,共同推进高效业务测试体验,地址:http://drivers.alibaba.net/背景老司机项目集成任务原计划为每一个项目老司机创建一个实验室,当项目环境部署时,会拉起这个实验室,然后触发老司机的项目集成任务。项目集成任务本身配置可触发的项目标,通过与实验室传递的项目标匹配以判断是否真正执行此集成任务。这里存在几个问题:如果每个项目都创建一个实验室,那么最终同一个应用上存在
431 0
【老司机平台技术】构建应用级项目集成任务通用实验室
|
人工智能 API 区块链
Web3技术丨佛萨奇系统开发技术介绍(代码搭建)原力佛萨奇系统丨MetaForce开发逻辑方案
Web3技术丨佛萨奇系统开发技术介绍(代码搭建)原力佛萨奇系统丨MetaForce开发逻辑方案
224 0
|
区块链
阐述佛萨奇开发源码 佛萨奇系统开发原力方案 佛萨奇2.0版本源码部署技术解决逻辑
阐述佛萨奇开发源码 佛萨奇系统开发原力方案 佛萨奇2.0版本源码部署技术解决逻辑
108 0
|
机器学习/深度学习 自然语言处理 TensorFlow
搜狗开源业内最全「阅读理解工具集合」,助力研发人员快速构建高效模型
搜狗搜索在Github上开源“搜狗阅读理解工具集合”,提供了完整的阅读理解任务组件及10+个复现模型,极大降低了复现相关模型的难度。
1457 0
|
人工智能 大数据
阿里云被集成之我见
阿里云的这个被集成有什么不同呢?
2456 0