找回密码
 会员注册
查看: 33|回复: 0

从Turborepo看Monorepo工具的任务编排能力

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
74758
发表于 2024-9-30 13:55:24 | 显示全部楼层 |阅读模式
本文部分图片来自互联网前言2021 年 12 月 9 号,Vercel 的官方博客上发布了一篇名为 Vercel acquires Turborepo to accelerate build speed and improve developer experience[1] 的博文,正如其所说,Vercel 收购了 Turborepo[2],以加速构建速度以及提高开发体验。Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。通过增量构建、智能远程缓存和优化的任务调度,Turborepo 可以将构建速度提高 85% 或更多,使各种规模的团队都能够维护一个快速有效的构建系统,该系统可以随着代码库和团队的成长而扩展。博文中已经简明扼要的突出了 Turborepo 的优势,本文则会从现有的实际场景出发,谈谈大型代码仓库(Monorepo)可能会遇到的一些问题,再结合业界现有的解决方案,看看 Turborepo 在任务编排方面做出了哪些创新与突破。一个合格的 Monorepo 应当具备哪些能力?随着业务的发展和团队的变化,业务型 Monorepo 中的项目会逐渐增加,极端一点的例子就是 Google 将整个公司的代码都放到一个仓库中,仓库的大小达到了 80TB。业务型 Monorepo:不同于 lib 型 Monorepo(React、Vue3、Next.js 以及 Babel 等广义上的 packages),业务型 Monorepo 将多个业务应用 App 及其依赖的公用组件库或工具库组织到了一个仓库中。项目数量的增加意味着在享受 Monorepo 优势的同时,也带来了巨大的挑战,一个优秀的 Monorepo 工具可以让开发者毫无负担的享受 Monorepo 的优势,一个不好用的 Monorepo 工具可以让开发者痛不欲生,甚至让人怀疑 Monorepo 存在的意义。列举一些本人遇到的部分场景:依赖版本冲突新建一个项目,该项目由于依赖问题无法启动新建一个项目,其他项目由于依赖问题无法启动依赖安装速度慢初始化安装依赖 20min+新增一个依赖 3min+build/test/lint 等任务执行慢笔者先前有过 Rush[3] 的落地经验,在实践过程中,发现除了最基本的代码共享能力外,还应当至少具备三种能力,即:依赖管理能力。随着依赖数量的增加,依旧能够保持依赖结构的正确性、稳定性以及安装效率。任务编排能力。能够以最大的效率以及正确的顺序执行 Monorepo 内项目的任务(可以狭义理解为 npm scripts,如 build、test 以及 lint 等),且复杂度不会随着 Monorepo 内项目增多而增加。版本发布能力。能够基于改动的项目,结合项目依赖关系,正确地进行版本号变更、CHANGELOG 生成以及项目发布。一些流行工具的支持能力如下表所示:-依赖管理任务编排版本管理Pnpm WorkspaceyesyesnoRushyes(by Pnpm)yesyesLagenoyesnoTurboreponoyesnoLernanoyesyesPnpm[4]:Pnpm 具备一定的任务编排能力 (--filter 参数),故此处也将其列入,同时作为 Package Manager,其自身更是大型 Monorepo 不可或缺的一部分。Rush[5]:由微软开源的可扩展 Monorepo 管理方案,内置 Pnpm 以及类 Changesets 发包方案,其插件机制是一大亮点,使得利用 Rush 内置能力实现自定义功能变得极为方便,迈出了 Rush 插件生态圈的第一步(插件机制由我司同学贡献)。Lage[6]:同样由微软开源,个人认为是 Turborepo 的前身,Turborepo 是 Lage 的 Go 语言版本。Lage 自称为 "Monorepo Task Runner",相较于 Turborepo 的 "High-Performance Build System" 内敛许多,Star 数也相差了一个数量级(Lage 300+,而 Turborepo 5k+),更多可查看该 PR[7]。在后文中 Lage 等同于 Turborepo。Lerna[8]:已经停止维护(2022/02/13),故后续讨论不会将其纳入。依赖管理过于底层,版本控制较为简单且已成熟,将这两项能力再做突破是比较困难的,实践中基本都是结合 Pnpm 以及 Changesets[9] 补全整体能力,甚至就干脆专精于一点,即任务编排,也就是 Lage 以及 Turborepo 的发力点。如何选择合适自己的 Monorepo 工具链?Pnpm Workspace + Changesets:成本低,满足大多数场景Pnpm Workspace + Changesets + Turborepo/Lage:在 1 的基础上增强任务编排能力Rush:考虑全面,扩展性强任务编排可以划分为三个步骤,各工具支持如下:范围界定并行执行云端缓存PnpmRushTurborepo/Lage范围界定:按需执行子集任务该能力在日常开发中具有丰富的使用场景,可以理解为对任务进行 Tree Shaking,按需执行,不应该引入与当前意图无关的任务执行。例如第一次拉取仓库,启动项目 app1 需要构建 Monorepo 内 app1 的前置依赖 package1 以及 package2。而打包生产环境项目 app1 时,需要构建 app1 自身以及 Monorepo 内 app1 的前置依赖 package1 以及 package2。以上两种情况则应该根据需要筛选出需要构建的项目。在不同的 Monorepo 工具中,这一行为有着不同的称呼:1.Rush 中称之为 [Selecting subsets of projects](https://rushjs.io/pages/developer/selecting_subsets/ "Selecting subsets of projects"),选择项目子集,在本示例中应当使用如下命令:#本地启动app1开发模式,app1为依赖图的顶端,但不需要构建app1自身$rushbuild--to-except@monorepo/app1$cdapps/app1&npmstart#SCM打包app1,app1为依赖图的顶端,且需要构建@monorepo/app1自身$rushbuild--to@monorepo/app12.Pnpm 中称之为 [Filtering](https://pnpm.io/filtering "Filtering"),即过滤,将命令限制于包的特定子集,在本示例中应当使用如下命令:#本地启动app1开发模式,app1为依赖图的顶端,但不需要构建app1自身$pnpmbuild--filter@monorepo/app1^...$cdapps/app1&pnpmstart#SCM打包app1,app1为依赖图的顶端,且需要构建@monorepo/app1自身$pnpmbuild--filter@monorepo/app1...3.Turborepo/Lage 中称之为Scoped Tasks[10],但目前(截止 2022/02/13)这一能力过于局限,Vercel 团队正在设计一套与 Pnpm 基本一致的 filter 语法,详情参见 RFC: New Task Filtering Syntax[11]范围界定保证了执行任务的数量不会随着 Monorepo 内无关项目的增加而增加,丰富的参数能够帮助我们在各种场景(package 发包、app 构建以及 CI 任务)去进行 selecting/filtering/scoping。比如修改了 package5,在 Merge Request 的 CI 环境需要保证 package5 以及依赖 package5 的项目不会因为本次修改而构建失败,则可以使用以下命令#使用Rush$rushbuild--to@monorepo/package5--from@monorepo/package5#使用Pnpm$pnpmbuild--filter...@monorepo/package5...在本示例中最终会挑选出 package5 以及 app3 进行构建,从而在 CI 上达到了合入代码的最低要求——不影响其他项目构建。基于工作区所有项目的 package.json 文件,可以方便地得到项目之间的具体依赖关系,每一个 Project 都知晓其 Dependents 以及 Dependencies,配合开发者传入的参数,从而方便地进行子集项目选择。并行执行:充分释放机器性能假设挑选出了 20 个子集任务,应该如何执行这 20 个任务来保证正确性以及效率呢?即:正确的对任务进行拓扑排序任务的粒度不是以项目维度,而是对项目下运行的 script 来做更细粒度的编排Project 之间存在依赖关系,那么任务之间也存在依赖关系,以 build 任务为例,只有前置依赖构建完毕,才可构建当前项目。网上有一道比较流行的控制最大并发数面试题,大致题意是:给定 m 个 url,每次最大并行请求数为 n,请实现代码保证最大请求数。这道题的思路其实与任务编排中的任务并行执行大同小异,只不过面试题中的 url 不存在依赖关系,而任务之间存在拓扑序,差别仅此而已。那么任务的执行思路也就呼之欲出了:初始可执行的任务一定是不存在任何前置任务的任务其 Dependencies 数量为 0一个任务执行完成后,从任务队列中查找下一个可执行的任务,并立刻执行一个任务执行完成后,需要更新其 Dependents 的 Dependencies 数量,从其内移除当前任务(Dependencies 数量 -1)一个任务是否可执行,取决于其 Dependencies 是否全部执行完毕(Dependencies 数量为 0)本文不作代码层面讲解,具体实现可见 Monorepo 中的任务调度机制[12]一文,在代码层面上实现了任务的拓扑序并行执行。打破任务边界本图来自Turborepo: Pipelining Package Tasks[13]之前谈到任务执行时,都是在同一种任务下,比如 build、lint 或是 test,在并行执行 build 任务时,不会去考虑 lint 或是 test 任务。如上图 Lerna 区域所示,依次执行四种任务,每一种任务都被前一种任务阻塞住了,即使内部是并行执行的,但不同任务之间依旧存在了资源浪费。Lage/Turborepo 为开发者提供了一套明确任务关系的方法(见 turbo.json),基于该关系,Lage/Turborepo 可以去进行不同种类任务间的调度和优化。相较于一次只能执行一种任务,重叠瀑布式的任务执行效率当然要高得多。turbo.json{"$schema":"https://turborepo.org/schema.json","pipeline":{"build":{//其依赖项构建命令完成后,进行构建"dependsOn":["^build"]},"test":{//自身的构建命令完成后,进行测试(故上图存在错误)"dependsOn":["build"]},"deploy":{//自身lint构建测试命令完成后,进行部署"dependsOn":["build","test","lint"]},//随时可以开始lint"lint":{}}}Rush 在 20 年 3 月以及 10 月也进行过相关设计的讨论,并于 21 年年底支持了类似的功能特性[rush] Add support for phased commands. #3113[14]Turborepo: Pipelining Package Tasks[15]How does lage work[16][rush] Design proposal: "phased" custom commands#2300[21]云端缓存:跨多环境复用缓存Rush 具备增量构建[17]的特性,使 rush build 能够跳过自上次构建以来输入文件(input files)没有变化的项目,配合第三方存储服务,可以达到跨多环境复用缓存的效果。Rush 在 5.57.0 版本引入了插件机制[18],进而支持了第三方远端缓存能力(在此之前仅支持 azure 与 amazon),可以基于插件机制,轻松地实现将公司内部的存储服务作为云端缓存的存储方案。落地到日常开发场景中,本地开发、CI 以及 SCM 各开发环节都能从中受益。上文有提到,在 CI 环节构建改动项目及其上下游项目可以一定程度上保证 Merge Request 的质量。如上图所示,存在场景修改了 package0 的代码,为了保证其上下游构建不被影响,则在 CI Build Changed Projects 阶段,会执行以下命令:$rushbuild--topackage0--frompackage0基于 git diff 挑选出源文件改动的 projects,此处为 package0经过范围界定,package0 及其上游 app1 会被纳入构建流程,由于 app1 需要构建,作为其前置依赖,package1 至 package5 也需要被构建,但这 5 个 package 实际上与 package0 并不存在依赖关系,也不存在变更,仅为了完成 app1 的构建准备工作。若依赖关系复杂起来,比如某个基础包被多个应用引用,那么类似于 package1-package5 的准备构建工作就会大大增多,导致这一阶段 CI 十分缓慢。实际构建的项目 = 改动项目 + 改动项目的 Dependencies + Dependencies 的依赖 + 改动项目的 Dependents + Dependents 的依赖由于 package1-package5 等 5 个项目与 package0 不存在直接或间接的依赖关系,且输入文件没有改变,故能够命中缓存(如有),跳过构建行为。如此便将构建范围由 7 个 project 降至 2 个 project。实际构建的项目 =改动项目+ 改动项目的 Dependents如何判断是否命中缓存在云端,每一个项目构建结果的缓存压缩包与其输入文件 input files 计算出来的 cacheId 形成映射,输入文件未发生变化,则计算出来的 cacheId 值就不会变化(内容哈希),就能命中对应的云端缓存。输入文件包含以下内容:项目代码源文件项目 NPM 依赖项目依赖的其他 Monorepo 内部项目的 cacheId缓存 hash 的生成比较复杂,除了上述的内容,还可能包括一些其他情况,比如编译使用的环境变量,用来区分测试环境以及线上环境等。若对其原理感兴趣,可以看看\@rushstack/package-deps-hash[19],围绕 git 实现#在控制台敲敲看$gitls-treeHEAD-r#非master分支修改文件后再敲敲看$gitdiff-index--color=never--no-renames--no-commit-idmaster--结语在编写本文过程中笔者也想起了 sorrycc[20] 在 GMTC 上分享的 《前端构建提速的体系化思路》中提到的构建提速三大法宝:延迟处理。基于请求的按需编译、延迟编译 sourcemap缓存。Vite Optmize、Webpack5 物理缓存、Babel 缓存Native Code。SWC、ESBuild作为任务编排工具来讲,Native Code 的优势并不明显(虽然 Turborepo 使用 Go 语言编写,但 Lage 作者认为在现有规模下,任务编排的效率瓶颈并不在编排工具本身),但延迟处理与缓存是有异曲同工之妙的。最后使用精简且务实的 Lage 官网副作为本文主题「任务编排」的结尾:Run all your npm scripts in topological order incrementally with cloud cache-_@microsoft/lage_配合云端缓存,依照拓扑排序增量运行你所有的 npm scripts。参考资料[1]Vercel acquires Turborepo to accelerate build speed and improve developer experience: https://vercel.com/blog/vercel-acquires-turborepo[2]Turborepo: https://turborepo.org/[3]Rush: https://rushjs.io/[4]Pnpm: https://pnpm.io/[5]Rush: https://rushjs.io/[6]Lage: https://microsoft.github.io/lage/[7]PR: https://github.com/vercel/turborepo/pull/370[8]Lerna: https://lerna.js.org/[9]Changesets: https://github.com/changesets/changesets[10]Scoped Tasks: https://turborepo.org/docs/features/scopes[11]RFC: New Task Filtering Syntax: https://github.com/vercel/turborepo/discussions/105[12]Monorepo 中的任务调度机制: https://github.com/worldzhao/blog/issues/11[13]Turborepo: Pipelining Package Tasks: https://turborepo.org/docs/features/pipelines[14][rush] Add support for phased commands. #3113: https://github.com/microsoft/rushstack/pull/3113[15]Turborepo: Pipelining Package Tasks: https://turborepo.org/docs/features/pipelines[16]How does lage work: https://microsoft.github.io/lage/guide/levels.html#level-4-pipelining[17]增量构建: https://rushjs.io/pages/advanced/incremental_builds/[18]插件机制: https://github.com/microsoft/rushstack/pull/2900[19]@rushstack/package-deps-hash: https://www.npmjs.com/package/@rushstack/package-deps-hash[20]sorrycc: https://github.com/sorrycc[21] [rush] Design proposal: "phased" custom commands · Issue #2300 · microsoft/rushstack · GitHub
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-15 07:41 , Processed in 0.578524 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表