与几年前相比,现在的 CI 平台要强大得多。总的来说,这是一件好事。借助强大的 CI 平台,软件公司和开发人员可以更频繁地发布更可靠的软件,这对软件用户或客户来说是有利的。一些集中式 CI 平台(如 GitHub Actions、GitLab Pipelines 和 Bitbucket)带来了规模效益,互联网提供了有关如何使用它们的信息。只要搜索一下如何在 CI 平台 Y 上执行 X 操作,就可以找到一些可以直接复制和粘贴的代码。毕竟,没有人愿意为了 CI 配置问题浪费太多时间,他们只是想快速发布产品。
现代的 CI 系统太复杂了
CI 平台的进步是以增加复杂性为代价的,我越来越觉得现代 CI 系统太复杂了。
从根本上讲,CI 平台是一种远程代码执行服务,执行代码是为了构建、测试和发布软件。因此,CI 平台通常会提供一系列增值特性,让你能更轻松地发布软件。这里有很多不同的方法和商业模式,其中一种常见的增值特性是使用某种类型的配置文件(通常是 YAML),它本身提供了常见的功能,例如配置版本控制系统的检出,并指定要运行的命令,而我遇到的问题就是从这里开始。
现代 CI 平台的 YAML 配置很强大。以下是 GitHub Actions 工作流 YAML 的一些特性:
一个嵌入式模板系统,可以将源 YAML 扩展为最终可用的 YAML 文档,包括一种自定义的迷你表达式语言。
作业触发器
命名变量
根据条件执行作业
作业之间的依赖关系
定义基于 Docker 的运行时环境
加密秘钥
构成每个作业的步骤以及这些步骤应该执行哪些动作
如果我们稍微扩展一下范围,把 GitHub 提供的 Actions 包含进来,就会有:
执行 Git 检出
存储工作流/作业使用的工件
缓存工作流/作业使用的工件
安装通用编程语言和环境(如 Java、Node.js、Python 和 Ruby)
还有更多
当然还有第三方 Actions,而且有很多!
这里有很多特性是必需的,我很难说出哪一个是多余的。所有这些特性对于足够强大的 CI 产品来说似乎都是必需的。如果你的产品不提供其中某些特性,就没有人会用它。
那么,我想要抱怨的是什么呢?
我假定一个 CI 系统复杂到与构建系统变得难以区分。那么,你能说服我或你自己:GitHub Actions、GitLab CI 和其他 CI 系统都不是构建系统吗?那些基础元素都有了,GitHub Actions 工作流由作业组成,作业由步骤组成,就像 Makefile 由规则组成,规则由命令组成。CI 系统和构建系统之间主要的区别在于形式和执行模型(传统上看,构建系统是在本地,是单机的,而 CI 系统是在远程,是分布式的)。
然后,我们反过来想:一个构建系统复杂到与 CI 系统变得难以区分。前面我说过,CI 系统是一种远程执行代码的服务。虽然从传统上看,构建系统是在本地运行(因此不是服务),但现代的构建系统(如 Bazel、Buck、Gradle)完全不一样。Bazel 将远程执行和远程缓存作为内置特性,而这些也是现代 CI 系统的内置功能!如果我用 Bazel 建立了一个构建系统,然后定义一个服务器端 Git 推送钩子,让远程服务器触发 Bazel 进行构建、运行测试并将结果发布到某处,那么这就变成了一个 CI 系统吗?我想是的!虽然很粗糙,但我认为它就是一个 CI 系统。
如果你有仔细阅读,就会得出这样的结论:足够复杂的 CI 系统和足够复杂的构建系统在我看来是一样的。两者都提供了一个服务器池,提供了通用的计算/执行功能和构建/交付软件的特性,如任务间工件交换、缓存、依赖关系和用于定义任务的迷你语言。
现代 CI 系统让我感到困扰的地方是:我觉得自己是在重新创造一个构建系统,并将构建系统的逻辑碎片化了。CI 配置不可避免地会转化为一堆复杂的 YAML,其中包含各种缓存和依赖关系优化,以便保持较短的执行时间和可靠性——就像构建系统一样。你会发现,你的构建系统有了 CI 系统的味道,反之亦然。你最终需要管理两个复杂的平台/系统,而不是一个。
因为构建系统比 CI 系统更为一般化(我认为一个足够高级的构建系统可以做的事情是一个足够复杂的 CI 系统的超集),这意味着如果构建系统足够高级,那么 CI 系统就是冗余的。所以,这篇文章的标题可以进一步:CI 系统不是太复杂了,而是说它们就不应该存在。CI 特性应该作为构建系统的扩展。
除了冗余问题,我认为对系统进行统一对用户来说更为友好。将 CI 系统集成到构建系统中(作为常规开发工作流的一部分),可以更容易地将 CI 系统的全部功能暴露给开发人员。请想象一下,你可以在不将变更推到远程服务器的情况下直接运行 CI 作业,就像在本地进行构建或测试一样。这样可以极大地缩短变更周期。
但请不要误解我的意思,CI 系统的某些功能在构建系统中是找不到的(比如集中式结果报告和用于触发作业的 UI/API),它们绝对是有必要存在的。当然,远程计算和作业定义对于构建系统来说是完全冗余的。
现代 CI 产品的方向跑偏了
如果你假设构建系统和 CI 系统之间很相似,就会发现很多现代 CI 产品(如 GitHub Actions、GitLab CI 和其他产品)的方向跑偏了:它们被定义成用来运行 CI 系统的特定领域平台。实际上,它们应该退后一步,被定位成构建系统(可能还包括批处理作业,比如数据仓库/数据管道中常见的那些)所需的更广泛的通用计算平台。
在这个层面上,每一个 CI 产品都是不一样的。我甚至认为 GitHub Actions 是一个 CI 产品,而不是一个平台。下面我来解释一下为什么。
在我看来,在一个理想的 CI 平台上,我能够要求执行一组特别的任务。我能够使用 API 来定义任务,让平台运行它们、上传工件、报告任务结果以便执行其他依赖任务,等等。
GitHub Actions 有一个API,可以用来与服务发生交互,但有一个关键的特性无法实现,就是用它来定义特定的工作单元:远程执行服务。定义特定工作单元的唯一方法是将工作流 YAML 文件提交到代码库中。
GitLab Pipelines 要好一些。GitLab Pipelines 支持父子管道(不同管道之间的依赖关系)、多项目管道(不同项目/代码库之间的依赖关系)和动态子管道(在定义新管道的管道作业中生成 YAML 文件)等特性。动态子管道是一种重要的特性,它们通常将提交的 YAML 配置与远程执行服务分离开来。这里缺少的是一个无需通过父管道/ YAML 就可以实现该功能的 API。如果存在这种 API,你就可以在 GitLab Pipeline 之上构建自己的构建/CI/批处理系统,减少 GitLab Pipeline 的 YAML 配置文件及其创建者的预期对你带来的约束。
像 GitHub Actions 和 GitLab Pipelines 这样的 CI 产品与其说是平台,不如说是产品,因为它们都是基于一个通用的远程执行服务,将一个自成体系的配置机制(YAML 文件)和 Web UI(以及相应的 API)紧密耦合在一起。对于我来说,要将这些产品视为平台,它们需要提供通过 API 来调度计算的能力,不受内置 YAML 配置机制的限制。GitLab 几乎已经实现了,目前还不清楚 GitHub 是否(或是否有兴趣)朝这个方向发展。
我想顺便提一下 Taskcluster,作为 GitHub、GitLab 等 CI 产品的反例。这一小节的内容对整篇文章来说并不是最重要的,可以随意跳过。但如果你想知道为工程师打造的 CI 平台应该是什么样子的,或者你是 CI 平台的开发者,想要了解一些值得借鉴的想法,那就读下去吧。
Mozilla 的Taskcluster最初是为了开发 Firefox 而构建的通用 CI 平台。在 2014 年和 2015 年推出之时,它是独一无二的,它的一些原始功能至今还找不到能够与之媲美的。或许 Mozilla 申请了什么专利,但在开源领域,没有可以与之匹敌的产品,就连我所知道的那些非开源 CI 平台也常常无法提供 Taskcluster 的所有功能。
据我所知,Taskcluster 是目前唯一一个适用于大型项目的开放 CI 平台。
Taskcluster 让我很喜欢的一点是它提供了用来定义执行单元的核心原语。任务是 Taskcluster 的核心执行原语,多个任务被连接在一起形成 DAG(这与构建系统的工作方式差不多)。
我们通过向队列服务发出 API 请求来创建任务,这个 API 请求实际上就是在调度这个工作单元。
定义好的任务实际上就是带有元数据的计算单元,这些元数据包括任务依赖关系、任务拥有的权限/范围,等等。如果你使用过 GitHub Actions、GitLab Pipelines,你就会看到很多你熟悉的基本元素:要执行的命令列表、要在 Docker 映像中执行的命令、构成工件的文件路径、重试设置,等等。
Taskcluster 所提供的特性远远超过了 GitHub、GitLab 等产品。
Taskcluster 提供了 IAM(身份识别与访问管理)风格的作用域特性来实现访问控制。作用域控制你可以执行什么操作、可以访问什么服务、可以使用哪些 Runner 特性(例如是否可以使用 ptrace)、可以访问哪些秘钥,等等。例如,Firefox 的 Taskcluster 设置是这样的:不可信任务是无法访问 Firefox 构建任务的签名密钥的。Taskcluster 是我所知道的唯一一个提供了足够多保护措施的 CI 平台,这些保护措施可以降低 CI 平台作为远程代码执行服务的风险。Taskcluster 的安全模型让 GitHub Actions、GitLab Pipelines 和其他常用的 CI 服务看起来更像是数据泄露和软件供应链漏洞工厂。
Taskcluster 支持使用 YAML 文件来定义任务,不过它已经提供了一个通用的调度 API,所以你不需要这么做。你可以使用自己的配置或前端来定义任务,不过 Taskcluster 并不关心这些,因为它是一个真正的平台。事实上,Firefox 在很大程度上避免使用 YAML,而是构建了自己的任务定义功能。Firefox 代码库里有很多代码,在运行时将生成数千个离散的任务,这些任务构成了 Firefox 的构建和版本 DAG,并将其中的一些子图注册为 Taskcluster 任务。这个功能就是它的一个迷你构建系统,Taskcluster 平台承担了执行/评估机制的角色。
Taskcluster 的模型和能力远远超过了 GitHub Actions 或 GitLab Pipelines,有很多伟大的想法值得借鉴。
Taskcluster 是一个非常强大的用户 CI,但现在还没有可供所有人使用的集中式实例(比如像 GitHub 或 GitLab 那样),而且学习曲线也相当陡峭。所有这些能力都是以复杂性为代价的。我不能随意向普通用户推荐 Taskcluster。但如果其他 CI 产品无法满足你的需求,你想建立自己的 CI 平台,又负担得起请几个人来支持你的 CI 平台,那么 Taskcluster 就值得考虑。
未来展望
在我的理想世界里,存在着一种远程代码执行服务平台,其目的是为近实时和批处理/延迟执行的任务提供服务。它可能是为软件开发而量身定制的,因为这些领域的特定特性将其与其他作为服务工具的通用计算(如 Kubernetes、Lambda 等)区分开来。
DAG 的概念被融入到执行模型当中,你可以将执行单元定义成图来获得依赖关系。你可以定义独立的、特别的工作单元,也可以定义一组单元,但不像构建系统那样,需要在整个执行过程中运行代理来协调任务的执行。
在我的理想世界里,只需要一个 DAG 来指定所有的构建、测试和发布任务。没有 N+1 系统或配置需要管理,也没有额外的平台需要维护,因为一切都是统一的。通过合并实现了规模经济,提高了整体效率。
平台由 worker 池子组成,这些 worker 运行负责执行任务的代理。有些池子用于近实时/同步 RPC 风格的调用,有些池子用于调度/延迟/异步任务。你可以定义自己的 worker 池和 worker。高级客户可能会使用由临时 worker(如 EC2 Spot 实例)组成的自动伸缩组对容量进行伸缩,以相对低的成本满足需求。当不再需要这些容量时就终止 worker,以此来节约成本(Firefox 的 Taskcluster 实例已经这样做至少 6 年了)。
对于最终用户来说,一个本地构建包含了驱动或调度用以生成所需构建组件的完整任务图的子集。一个 CI 构建/测试由实现该目标所必需的任务图的子集组成(它可能是本地构建图的一个超集)。版本的发布也一样。
至于如何配置前端和定义执行单元,平台只需要提供一个东西:一个可以用来调度/执行作业的 API。但是,为了让这个产品对用户友好,它也应该提供 YAML 配置文件(就像现在的 CI 系统那样)。这样,大部分用户可以继续使用简化的 YAML 界面,而高级用户可以使用底层的调度/执行 API 来开发他们自己的驱动程序。人们为他们的构建系统开发插件,并集成到这个平台上。有人会将现有的可扩展构建系统(如 Bazel、Buck 和 Gradle)中的节点转换为平台的计算任务,这样就可以实现构建系统和 CI 系统(可能还有数据管道之类的东西)的统一。
最后,因为我们讨论的是为软件开发量身定制的系统,所以我们需要有健壮的结果/报告 API 和界面。如果人们看不到这些奇特的分布式远程计算在做什么,那谁会知道它们究竟给我们带来了哪些好处?这可能很专业,因为如何跟踪结果与特定的领域有关。高级用户可能想要构建自己的结果跟踪服务,但平台至少应该提供一个通用的服务(就像 GitHub Actions 和 GitLab Pipelines 那样)。因为这是一项巨大的增值特性,如果没有这项特性,很少人会使用你的产品。
但是,我所愿景的统一化世界并不会解决上面提到的 CI 复杂性问题:一个足够大的构建/CI 系统总是具有内在的复杂性,可能需要专门的人来维护。不过,由于复杂的 CI 系统几乎总是附加在复杂的构建系统上,因此通过合并构建系统和 CI 系统可以缩小复杂性的表面积(比如,你不需要操心构建/CI 互操作性问题)。
我愿景中的所有组件现在都以某种形式存在着。Bazel、Gradle Enterprise 和其他现代构建系统都有用于远程执行和缓存的 RPC。它们甚至是可扩展的,你可以开发自己的插件来改变构建系统的核心功能(当然是在不同的程度上)。Taskcluster 和 GitLab Pipelines 支持任务的 DAG 调度。一些批处理作业执行框架(如 Airflow)看起来非常像是特定领域的特别版 Taskcluster。我们缺少的是一个可以将所有这些功能捆绑在一起的单一的产品或服务。
我确信,我所愿景的不是能否实现的问题,而是我们是否应该实现以及谁来实现的问题。
这可能就是问题的所在。我不想这么说,但除了少数公司之外,我真的怀疑这种服务能否在短期内成为一种广泛可用的服务。
我的愿景的价值在于统一离散的系统(构建、CI,也许还有一些临时的系统,如数据管道,这些系统本身就足够复杂)。出于业务/效率方面的原因,你需要将它们统一起来。毕竟,如果它们没有那么复杂或低效,你可能就不会关心如何让它们变得更简单或更快。从这方面讲,我们可能已经过滤掉了 90%的市场,因为他们的系统还不够复杂。
要实现这个愿景,需要采用足够先进的构建系统,充当统一 DAG 远程执行服务的大脑。一些公司和项目将采用先进的构建系统(如 Bazel),因为他们有资源、技术知识和效率激励机制,但其他很多公司不会这么做。相对于简单的构建系统,高级的构建系统所提供的额外好处往往是微不足道的。很多公司将构建和 CI 支持视为产品开发成本。如果你能够在一个并不先进的构建系统上取得成功,并且在没有过多困难的情况下只花费一小部分成本就取得足够好的成效,那将成为很多公司和项目相继效仿的榜样。人们并不关心有关构建系统和 CI 系统的争论是一个怎样的结果:他们只想发布产品。
在我看来,这个想法的总体目标市场太小了。在未来几年内,没有任何有技术专长的公司能够实现和提供这样的服务。我在这个领域工作了 10 年,见证了 Taskcluster 模式的潜力,看到了以前的、现在的和潜在的雇主都在为此不同程度地挣扎着。我知道这个想法对某些人来说是非常有价值的。尽管这对一些公司来说很重要,但我的直觉是,他们只代表了整个潜在市场的一小部分,对于 GitHub 或 GitLab 这样的现有 CI 产品来说,这块蛋糕太小了,目前还不足以引起他们的注意。
我不认为在这个领域创业是个好主意:因为获取客户太难了。而且,由于很多核心技术已经存在于现有的工具中,在专利知识产权方面并没有什么护城河可以阻止那些财力雄厚的模仿者。你最好的退出方式可能是被微软/GitHub、GitLab 或像亚马逊/AWS 这样想在这个领域发展的公司收购。
我认为,我们最希望的是看到现有的 CI 平台能够实现这个愿景,并向全世界发布,或者作为开源项目或服务提供出来。GitHub、GitLab 和其他代码托管提供商是理想的候选者,因为它们的社区有助于推动行业的采用。
我不确定是什么时候,但我打赌 GitHub/微软会率先采取行动。他们在更广泛的市场/产品捆绑方面有更强烈的动机(想想他们已经在 Visual Studio 或 GitHub Workspaces 中集成构建和 CI 系统)。此外,他们面临着一些巨大的构建系统和 CI 挑战(特别是 Windows)。很明显,微软正在 GitHub 上做开发,甚至是公开的。微软工程师将感受到离散的构建和 CI 系统给他们带来的痛苦和限制。最终,GitHub 上至少需要有一个构建系统远程执行服务。我希望 GitHub(或其他公司)将其作为一个统一的平台/服务/产品而不是离散的服务来实现,因为正如我所说的,它们实际上是相同的问题。但提供统一的产品并非阻力最小的途径,所以谁知道会发生什么。
结论
如果我有打响指的魔力,可以让离散的构建、CI(或许还有批处理系统,如数据管道)系统向前快速发展 10 年,那么我会:
让 Taskcluster 成为最好的远程执行服务平台。
添加对实时同步执行 API(如 Bazel 的远程执行 API)的支持,作为对现有批处理/异步能力的补充。
定义 Starlark 方言,这样就可以像 Bazel 等构建工具中的原语一样定义 CI/发布任务。
迫使其他构建工具(如 Bazel)做出改进,缩短构建时间。
提供用于平台交互、查看结果报告的高级 Web UI。
向全世界发布。
这个梦想会很快成为现实吗?可能不会,但梦想还是要有的。或许,一些读者可能会自己去追逐这个梦想。
原文链接:
评论