在组件化的浪潮下,公司引入多仓开发对工程架构进行解耦、跨业务技术能力复用,并辅以组件(混合)二进制化进行编译提速。不过随着工程规模增长、业务复杂度提升,多仓二进制的弊端日益凸显:
诚然,多仓方案很好的落实了组件化思想,但为解决上述多仓体系的问题,公司衍生了大量的优化和效率工具,其从结果看仅聚焦于解决局部问题,难以进一步提升优化空间。更多新挑战的出现使我们意识到当前模式下无法对这些问题进行收敛。对于头条、抖音、飞书这些超级 APP 在多仓中进行开发已经陷入困境。
为保证公司客户端项目长期稳定的发展、从根源上解决问题,我们围绕单仓提出了一套完整的解决方案。
Monorepo(Mono Repository)指的是将多个相关项目或模块的代码集中管理于一个仓库的软件开发模式。具体来说,Monorepo 将所有的源代码和配置文件等存储在一个版本控制仓库中,并使用相同的构建和部署系统来管理和交付代码。
全源码(Full Source)是指将整个软件系统的源代码均存放于一个仓库中,源码为单一可信源。
全源码的概念通常与 Monorepo 联系在一起,将概念定义为 Monorepo 全源码解决方案,是区分于当前流行的组件二进制化开发模式。下图中 MULTI-REPO 为前多仓开发模式,每个组件独立 git 存储;MONO-REPO 为单仓模式,组件同主仓合并,只保留一个 git。
BitSky 是公司内 Monorepo 全源码项目代号,旨在建设客户端 Monorepo 基础设施,提升客户端开发效率和体验。业务方通过 bitsky 工具套件进行集成使用。
合仓套件帮助业务工程平稳迁移 Monorepo;构建服务保证 Monorepo 下高效、稳定的构建;工程架构模块为工程去除 Cocoapods 等依赖管理工具,提供统一依赖分层服务;Developer Tools,提供统一、便捷的 GUI 工具,让研发人员更高效进行本地开发工作。(该章节主要介绍图示中软件服务部分)
项目的首要任务是将现有仓库平滑迁移到 Monorepo。迁仓因涉及多个团队通力协作,故面临较大的业务挑战。为此,项目早期便确定了清晰的目标和计划,采用灰度逐步迁移的方式,避免过度干预业务的正常运转。同时,为了确保迁移的顺利进行,进行全面的风险评估和管理,并采用工具支持迁移的过程。
围绕上述原则,我们执行时分三个阶段进行:
迁仓技术上挑战主要来自 Git ,仓库变大后带来的的性能问题通过配置优化进行有效缓解。上线当天采用 Git 流量预热的削峰策略减缓 GitLab 的流量压力。
Xcode 内置构建系统因自身缓存能力薄弱、代码封闭、不支持分布式,导致其构建性能一般、上限低。在 Monorepo 全源码项目中,全量编译时编译单元总数在 30,000 上下,很明显不能支撑这样的场景。在对比市面上构建系统后,Bazel 作为我们的迁移选择 {Fast,Correct} Choose Two!
多仓体系下通过 xcodeproj、podspec 描述构建规则,而 Bazel 体系构建配置以 BUILD、WORKSPACE 形式呈现,这个环节我们做了两个事情:
BazelGenerator:构建系统能正常运行取决于调用编译器、链接器等工具链时编译参数以及构建依赖的正确性,通过该研发工具将构建规则迁移至 Bazel 体系。
BUILD DSL:定制 bitsky 的构建配置使 Bazel 的构建配置语义化,降低研发同学迁移学习成本。
bitsky_library(
name = "Common",
hdrs = [":Module_hdrs"],
srcs = [":Module_srcs"]
)
Bazel 提供了非常优秀的分布式能力。我们在工程上落地了分布式缓存及分布式构建基础能力的同时,也设计开发了依赖发现、多级缓存、边缘节点、预缓存等策略,提升了构建效率。
产物一致性校验(Universal deterministic builds)指的是在软件构建过程中,在任何平台上构建、任何构建系统、编译器、链接器,最终生成的构建产物(如可执行文件、库等)都是相同的,不会因为平台、工具等因素导致构建产物的差异。优势在于可以确保构建产物的一致性,降低了出现因环境差异导致的问题的风险。更重要的是,提高构建的可重复性和可验证性,为新技术上线做保障。
为达成该目标,我们开发了产物一致性校验工具,从可执行文件级别来进行资源、符号级别对齐。并对 Application 包内图片、文本 等资源文件级别进行校准。
此外,单元测试、集成测试、回归测试也是上线中必要的环节。
CocoaPods 在多仓模式下除去自身的依赖管理能力外,承担了非常多的非自身功能,如组件二进制集成、hmap 的编译优化,混编能力的支持。迁移 Bazel 构建系统后这些优化能力名正言顺的通过构建系统来承担。
而在 Monorepo 全源码体系下,二三方组件已经在工程内,CocoaPods 的实际能力已经不再适合 Monorepo,我们开发了轻量依赖管理能力来完全替代它。去除 CocoaPods 后我们彻底解决了下面的几个大问题:
移除 CocoaPods 不代表放弃组件化,在 Monorepo 生态中我们也设立了相应的组件依赖关系验证及组件模块分层服务。
Xcode 是苹果开发者本地开发主要的 IDE 工具,不仅帮助开发人员创建各种类型的应用程序,还拥有丰富的工具和功能,使得开发过程更加便捷高效。但 Xcode IDE 对外置构建系统支持并不友好,加上大型 App 源码数量级别普遍在 1w~10w ,Xcode 在这种情况下表现不佳,其响应慢、定制性差的缺点更加明显。
在本地开发中通过开源工具 Tulsi、BuildService、IndexImport 的定制化开发,补齐了 Xcode 体系中索引、高亮、日志、进度条等语言能力,使 Xcode 体验与之前无异。通过这些工具我们把 Xcode 的优化牢牢把握在自己的手上,也很好的提升了 Xcode 易用性。
除去上述模块外,BitSky 打通集团内 DevOps 平台服务以及基础服务并协同编译监控分析平台 Hummer 打造完善的指标数据、全链路监控、预警防劣化重要能力,提供完整的 monorepo 研发体验及研发效率。
研发流程上,头条、飞书完成仓库合并后 ,收益数据(以飞书工程为例):
构建效率上,头条完成构建系统迁移后,编译耗时 PCT50 降低 20%,PCT 90 降低 50%。构建整体流程(含 Pod)耗时降低 50%,效果对比图:
应用实践章节概述了解决方案核心模块,而相较于落地执行中的问题和细节还只是冰山一角。这些问题有些是通用的比如说新在 Monorepo 下如何保持组件化/模块化、工程怎么进行进行依赖分层治理、如何建设落实防劣化方案,而有些问题也会因项目的不同而不可借鉴。千里之行始于足下,未来我们也在向着 VFS、多构建系统、多端同仓的方向做进一步探索。
Monorepo 全源码方案并不适用所有移动端项目,大部分项目的架构变革不已个人的意志为转移的,康威定律第一条:“设计系统的架构受制于产生这些设计的组织的沟通结构” 很好的诠释了这个观点。但任何大型技术的迁移在一个不断发展的 App 中都是必要的,只有通过迁移才能不断地推进技术的进步和发展,并持续提升研发效率、保证业务的竞争力。
Monorepo 全源码方案为大型移动端开发提供了全面可行性验证以及宝贵经验。未来,我们会将现有的功能回馈到 bazel 社区,并且推出一系列文章来讲述 BitSky 套件的工作流程及原理。
参考文章:
https://engineering.atspotify.com/2022/11/strategies-and-tools-for-performing-migrations-on-platform/
https://en.wikipedia.org/wiki/Conway%27s_law