ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Bazel 构建体系解析:任务型构建系统(Task-Based Build Systems)的工作原理与三大困境

Bazel 构建体系解析:任务型构建系统(Task-Based Build Systems)的工作原理与三大困境 Bazel 构建体系解析任务型构建系统Task-Based Build Systems的工作原理与三大困境【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel本篇技术指南以 Bazel 官方文档《Task-Based Build Systems》为核心系统讲解任务型构建系统的定义、工作原理以 Ant 的build.xml为实例及其在并行化、增量构建与脚本维护三方面的根本性缺陷并结合本仓库源码说明 Bazel 为何以工件artifact而非任务task作为构建的核心抽象。读完本文你将能够准确区分任务型与工件型构建系统的本质差异理解 Bazel 设计决策背后的动机并为评估迁移 Bazel 提供理论依据。理解任务型构建系统任务型构建系统task-based build system是最基础的构建抽象之一其基本工作单元是任务task。每个任务本质上是一个可以执行任意逻辑的脚本并且任务可以声明它所依赖的其他任务——这些依赖必须在它之前运行。今天主流的大部分构建系统——如 Ant、Maven、Gradle、Grunt、Rake——都是任务型的。与直接编写 shell 脚本不同这些现代构建系统要求工程师创建描述如何执行构建的构建文件buildfile。在 Bazel 的文档体系中任务型构建系统被视为 shell 脚本之后的下一逻辑进化它在脚本的基础上引入了结构化的任务定义与依赖关系管理但并未从根本上改变构建 执行一串命令的本质。这一演进脉络在 docs/basics/build-systems.mdx 中有完整铺垫当组织规模扩大后单纯依赖编译器或 shell 脚本会迅速遇到可维护性、可复现性与性能问题。一个经典实例Ant 的 build.xml以 Ant 官方手册中的经典示例为例其构建文件用 XML 编写既定义了构建的元数据也定义了一组任务XML 中的target标签project nameMyProject defaultdist basedir. description simple example build file /description !-- set global properties for this build -- property namesrc locationsrc/ property namebuild locationbuild/ property namedist locationdist/ target nameinit !-- Create the time stamp -- tstamp/ !-- Create the build directory structure used by compile -- mkdir dir${build}/ /target target namecompile dependsinit descriptioncompile the source !-- Compile the Java code from ${src} into ${build} -- javac srcdir${src} destdir${build}/ /target target namedist dependscompile descriptiongenerate the distribution !-- Create the distribution directory -- mkdir dir${dist}/lib/ !-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -- jar jarfile${dist}/lib/MyProject-${DSTAMP}.jar basedir${build}/ /target target nameclean descriptionclean up !-- Delete the ${build} and ${dist} directory trees -- delete dir${build}/ delete dir${dist}/ /target /project注意 Ant 的术语与直觉相反Ant 用target表示任务用task表示命令。上述构建文件定义了三类核心任务任务target依赖执行内容init无生成时间戳、创建${build}目录compileinit将${src}下的 Java 源码编译到${build}distcompile创建${dist}/lib目录并打包 JARclean无删除${build}与${dist}目录每个任务执行的是 Ant 预定义的一系列命令创建/删除目录、运行javac、创建 JAR 等且可以通过用户编写的插件无限扩展以覆盖任意逻辑。任务通过depends属性声明依赖从而形成一张无环图acyclic graph如下图所示图 1任务依赖形成的有向无环图来自 Bazel 官方文档ant dist的执行过程用户通过向 Ant 命令行工具提供任务名来执行构建。当用户输入ant dist时Ant 会依次执行以下步骤加载当前目录下名为build.xml的文件并解析构建出图 1 所示的依赖图结构查找命令行指定的名为dist的任务发现它依赖名为compile的任务查找compile任务发现它依赖名为init的任务查找init任务发现它没有依赖执行init任务中定义的命令在其全部依赖执行完毕后执行compile任务中定义的命令在其全部依赖执行完毕后执行dist任务中定义的命令。最终Ant 执行dist任务所运行的代码等价于下面这段 shell 脚本./createTimestamp.sh mkdir build/ javac src/* -d build/ mkdir -p dist/lib/ jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*剥去语法外壳后我们获得了什么去掉语法外壳之后构建文件与构建脚本其实差别不大。但任务型抽象已经带来了实实在在的收益可以在其他目录创建新的构建文件并将它们链接在一起可以轻松地以任意复杂的方式添加依赖既有任务的新任务只需向ant命令行工具传入一个任务名它就能自动确定所有需要运行的步骤。Ant 是 2000 年发布的老牌软件。此后 Maven、Gradle 等工具在 Ant 基础上做了改进并基本取代了它例如自动管理外部依赖、用更简洁的非 XML 语法等。但这些新系统的本质并未改变——它们依然是让工程师以任务为单元、以规范化且模块化的方式编写构建脚本并提供执行这些任务、管理任务间依赖关系的工具。任务型构建系统的阴暗面由于这类工具本质上允许工程师把任意脚本定义为任务它们极其强大几乎可以做任何你能想象的事。但力量伴随代价当构建脚本日益复杂时任务型构建系统会变得难以驾驭。其根本问题在于——任务型系统把过多的权力给了工程师而给系统的权力不足。由于系统对脚本内部在做什么一无所知性能受损系统在调度和执行构建步骤时必须非常保守正确性无保障系统无法确认每个脚本是否在做它应该做的事于是脚本会不断膨胀最终变成又一个需要调试的负担。困境一构建步骤难以并行化现代开发工作站的性能相当强大拥有多个可以并行执行构建步骤的核心。但任务型系统即使看起来应该能并行时也常常无法并行执行任务。假设任务 A 依赖任务 B 和任务 C而 B 与 C 互不依赖——那么同时运行 B 和 C 以更快到达 A 是安全的吗可能安全如果它们不触碰任何相同资源可能不安全例如两者都用同一个文件记录状态同时运行就会产生冲突。系统在一般情况下无法得知这一点因此它只有两个选择要么冒险并行导致罕见但极难调试的构建问题要么把整个构建限制在单进程单线程中执行。这既是对强大开发机的巨大浪费也彻底排除了将构建分发到多台机器的可能性。困境二增量构建难以实现一个好的构建系统应当支持可靠的增量构建——一次小改动不应触发整个代码库的全量重建。在构建系统慢且无法并行的前提下这一点尤其重要。但任务型构建系统在此同样举步维艰因为任务可以做任何事系统无法一般性地判断某个任务是否已经完成很多任务只是拿一组源文件跑编译器、产出一组二进制文件理论上源文件没变就无需重跑但系统没有额外信息无法断言这一点——任务可能下载了一个会变化的文件也可能每次运行都写入不同的时间戳为保证正确性系统通常必须在每次构建时重跑每一个任务。部分构建系统尝试让工程师自行指定任务需要重跑的条件但这往往比看起来棘手得多。例如在 C 这类允许文件被其他文件直接#include的语言中不解析输入源文件就无法确定需要监视变化的完整文件集合。工程师往往采取捷径而捷径会导致任务结果被错误复用这类罕见又恼人的问题。当此类问题频繁发生时工程师会养成每次构建前先clean的习惯——这完全违背了增量构建的初衷。结论很明确判断一个任务何时需要重跑这件事出人意料地微妙更适合交给机器而不是人类。困境三构建脚本难以维护与调试任务型构建系统强加的构建脚本往往很难伺候。构建脚本虽然受到的审查较少但它们与被构建的系统一样是代码而且是 bug 极易藏身的地方。以下是在任务型构建系统中非常常见的几类 bug隐性文件契约被破坏任务 A 依赖任务 B 产出某个特定文件任务 B 的负责人没意识到其他任务依赖它把输出改到了别的位置。这个问题只有等有人运行任务 A 失败时才能被发现。传递依赖的连锁失效任务 A 依赖任务 B任务 B 依赖任务 C而任务 A 需要的文件正是由任务 C 产出的。B 的负责人决定不再依赖 C——于是任务 A 失败了尽管任务 B 根本不在乎 C机器假设泄漏新任务的开发者无意中假设了运行机器的环境比如某个工具的安装位置或特定环境变量的值。任务在他的机器上正常换一个开发者就失败。不确定性任务包含非确定性成分例如从互联网下载文件或向构建中加入时间戳。于是每次运行可能得到不同结果工程师无法复现和修复彼此的失败自动化构建系统上的失败也无法复现。竞态条件多依赖任务可能产生竞态。如果任务 A 同时依赖 B 和 C而 B、C 都修改同一个文件任务 A 的结果将取决于 B 和 C 谁先完成。为什么无法在任务框架内修补在上述框架内这些性能、正确性与可维护性问题不存在通用解法。只要工程师还能在构建期间编写任意代码系统就不可能拥有足够的信息来始终快速且正确地执行构建。要解决这个问题必须从工程师手中收回一部分权力交还给系统并把系统的角色从运行任务重新定义为产出工件artifact。出路以工件为中心的重构这一思路催生了工件型构建系统artifact-based build system——Blaze 及其开源版本 Bazel 正是这一范式的代表。在本仓库中你可以直接对照任务型与工件型两种构建文件的差异任务型构建文件如上文 Ant 的build.xml是一段命令式描述用图灵完备的脚本语言罗列如何产出输出的命令序列工件型构建文件Bazel 的BUILD文件则是一份声明式清单声明要构建的工件集合、它们的依赖以及一组有限的影响构建方式的选项。以本仓库 examples/cpp/BUILD 为例Bazel 的构建文件长这样cc_library( name hello-lib, srcs [hello-lib.cc], hdrs [hello-lib.h], ) cc_binary( name hello-world, srcs [hello-world.cc], deps [:hello-lib], )这份文件不再描述先做什么、后做什么的命令序列而是声明hello-lib是一个由hello-lib.cc与hello-lib.h构成的库hello-world是一个依赖hello-lib的可执行文件。工程师只告诉系统构建什么what而如何构建how——配置、运行、调度编译步骤——完全由 Bazel 负责。为什么工件抽象能解决问题工件型系统把构建视作一个数学函数输入源文件以及编译器之类的工具输出二进制文件。由此带来的能力与任务型系统形成鲜明对比安全并行Bazel 知道每个目标只会运行预定义的编译器而非任意用户脚本因此可以安全地并行执行无依赖关系的构建步骤在多核机器上可能获得一个数量级的性能提升可靠增量Bazel 知道每个目标的输出只取决于其声明输入输入未变即可复用缓存输出从而只重建最小必要集合而绝不产生过期构建工具即依赖每个java_library隐式依赖一个已知位置的 Java 编译器工具变化会像普通依赖一样触发其下游工件的重建。Bazel 解决任务型系统痛点的完整机制工具作为依赖、自定义规则扩展、沙箱隔离环境、外部依赖的确定性管理等在 docs/basics/artifact-based-builds.mdx 中有详细展开。更大的图景并行与复用之上的分布式构建并行化 输出复用这两个能力正是构建系统走向分布式与超大规模的基础分布式构建在超大代码库中单个二进制可能依赖数万个构建目标单机物理极限无法突破只有将工作单元分散到任意可扩展数量的机器上远程缓存与远程执行才能让任意规模的构建快速完成。详见 docs/basics/distributed-builds.mdx封闭性Hermeticity任务型系统常见的非封闭来源——任意处理、非确定性产出时间戳、构建 ID、跨主机差异的系统二进制、构建期间写源树——正是工件型系统通过工具即源码输入源身份等机制着力消除的对象这也是远程缓存/远程执行可行的前提。详见 docs/basics/hermeticity.mdx依赖管理大规模下的依赖复杂性传递依赖、版本冲突、外部依赖的确定性也是 Bazel 设计考量的重要维度详见 docs/basics/dependencies.mdx。小结任务型构建系统Ant、Maven、Gradle、Grunt、Rake 等是构建脚本之后的重要一步它以任务为基本单元让工程师得以模块化、有原则地组织构建逻辑并借助依赖图自动确定执行顺序。但其根本缺陷在于把过多权力交给了工程师、过少权力交给了系统——系统对脚本内部行为一无所知导致并行化困难无法判断并行是否安全只能保守串行浪费多核资源并阻断分布式构建增量构建困难无法一般性判断任务是否已完成只能全量重跑或依赖工程师手写脆弱的重跑条件脚本维护困难隐性文件契约、传递依赖失效、机器假设泄漏、非确定性、竞态条件等 bug 层出不穷且极难调试。这些问题的根源是同一个只要构建期间可以运行任意代码系统就不可能获得足够信息来快速且正确地构建。Bazel 的答案是换一种心智模型——不再运行任务而是产出工件工程师声明构建什么what系统决定如何构建how。正是这一转变使得安全并行、可靠增量、工具版本化管理、环境隔离与确定性外部依赖成为可能并最终支撑起远程缓存、远程执行与超大规模仓库的分布式构建。想要继续深入可以从 docs/basics/artifact-based-builds.mdx 开始按 docs/basics/index.mdx 中构建基础系列构建系统、任务型、工件型、分布式、依赖管理的顺序系统学习 Bazel 的设计哲学。【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表