之前分享过《版本分支管理标准 - Git Flow》,不过在实际使用过程中, 因为其有一定的复杂度,使用起来较为繁琐,所以一些人员较少的团队并不会使用这个方案。
在这基础上,一些新的分支管理标准被提出。这里转发一下这个标准:《Trunk Based Development 主干开发模型》。
Preface
在之前的博文中我们介绍了 Git Flow 分支模型,正如文中所说,Git Flow 偏向于控制管理,使用了较多的分支,流程颇为复杂。大量的团队在实践过程中也遇到了颇多问题,其中大部分来自长期存在的分支。随着软件开发模型的演进,GitHub Flow、Trunk Based Development 等模型也应运而生,也已被 Google、Facebook、TW 等企业实践。本文主要介绍 TBD 模型。
Git Flow的问题
- 合并冲突,合并冲突在使用 Git Flow 是非常常见的。原因很简单:如果你有多个并行功能分支,他们长时间存在,那么很可能代码库的相同部分在两个功能分支中被分别更改。合并冲突不仅对于需要手动解决的开发人员来说是令人沮丧的,也增加了在代码中破坏某些功能的风险,因为当你不得不决定使用哪个版本代码时,很容易犯错。
- 功能分离,在合并到同一个分支之前,你不能测试两个功能的组合。当你在单独的分支中开发几天甚至几周的功能时,当合并回主分支后,可能也会发生两个功能的相互作用影响了你的代码。
- 并没有做到持续交付,在 Git Flow 分支模型下,发布是非常有计划的,一个 feature 必须要经过一系列步骤才能到达生产环境,在时间上平均一个 feature 都要等待 两周时间才能长线,这样的等待并非是需求上的“按计划发布”,而是从技术上就造成了发布瓶颈,显然难以达到持续交付的要求。
- 与持续集成相悖,你会发现,在坚持持续集成实践的情况下,feature 分支是一件非常矛盾的事情。持续集成鼓励更加频繁的代码集成和交互,让冲突越早解决越好。feature 分支的代码隔离策略却在尽可能推迟代码的集成。
GitHub Flow
GitHub Flow 是一个更轻量级的软件开发模型,示意图如下。它摒弃了 Git Flow 中繁杂的分支, 只保留一个主分支 master 。开发新功能时从 master 分支上拉取 feature 分支,开发完成后发起 Pull-Request ,小组内进行评审和反馈,此时也进行 Code Review 。测试通过后合并回主分支。
相比于 Git Flow,这种方式因为省去了一些分支而降低了复杂度,同时也更符合持续集成的思想,以一张故事卡为集成的最小单位,相对来说集成的周期短,反馈的速度也快,能够及早的遇到问题并及早解决。
顺着持续集成的思想,如果我们把 GitHub Flow 分支模型做得再极致一点,我们不要 feature 分支,或者把 feature 分支只留在本地;不需要使用 Pull-Request 而是直接 Push 到远程 master 分支,我们就做到了 Trunk based Development。
TBD
Trunk based Development,又叫 主干开发 ,是一套代码分支管理策略,开发人员之间通过约定向被指定为 主干 的分支提交代码,以此抵抗因为长期存在的多分支导致的开发压力。此举可 避免分支合并的困扰,保证随时拥有可发布的版本 。“主干”这个词隐喻了树木生长的场景,树木最粗最长的部位是主干,分支从主干分离出来但是长度有限。
使用主干开发后,我们的代码库原则上就只能有一个 Trunk 分支即 master 分支了,所有新功能的提交也都提交到 master 分支上,保证每次提交后 master 分支都是可随时发布的状态。没有了分支的代码隔离,测试和解决冲突都变得简单,持续集成也变得稳定了许多,但也有如下几个问题:
- 如何避免发布引入未完成 Feature,答案是使用 Feature Toggle 。在代码库里加一个特性开关来随时打开和关闭新特性是最容易想到的也是最容易被质疑的解决方案。Feature Toggle 是有成本的,不管是在加 Toggle 时的代码设计,还是在移除 Toggle 时的人力成本和风险,都是需要和它带来的价值进行衡量的。
- 如何进行线上 Bug Fix,答案是在发布时打上 Release Tag,一旦发现这个版本有问题,如果此时 master 分支还没有其他提交,那可以直接在 master 分支上 Hot Fix 然后合并至 release 分支;如果 master 分支已经有了提交就需要做以下三件事:
- 从 Release Tag 创建发布分支。
- 在 master 上做 Fix Bug 提交。
- 将 Fix Bug 提交 Cherry Pick 到 release 分支。
- 为 release 分支打上新的 Tag 并做一次发布。
说明
- 主干开发是助力实现 持续集成 和 持续交付 的关键因素。开发团队的成员一天多次地将代码提交到主干分支,满足了持续交付的必要条件。团队的工作在 24 小时内就可以被整合,这保证了代码版本随时处于可发布状态,使得持续交付成为可能。
- 你可以选择直接向主干分支提交代码的方式(适用于小团队)或者采用 Pull-Request 的方式,只要保证特性分支不能长期存在,并且产品是独立存在的。
- 根据团队规模和提交频率, 特性分支可用于合并到主干分支前的代码审查和持续集成 。这些特性分支可以让开发人员在代码合并到主干分支之前进行持续审查,而对于较小规模的团队,则可以直接向主干分支提交。
- 根据预期的发布频率,你的团队或许需要实时从主干分支创建 发布分支 以确保发布版本不会有新的提交,这些分支应该在发布完成后一段时间内删除。另一方面,你的团队也可以选择从主干分支发布而不需要发布分支,并采用“ 修复前进(fix forward) ”的策略进行 bug fix,这种发布策略适用于高吞吐量的团队(high-throughput teams)。
Trunk Based Development
顺着持续集成的思想,如果我们把上一种分支模型做得再极致一点,我们不要 Feature 分支,或者把 Feature 分支只留在本地;不需要使用 Pull-Request 而是直接 Push 到远程 Master 分支,我们就做到了 Trunk based Development。(关于从 GitFlow 到 TBD 的论述,TW 同事尚齐在洞见上有一篇Gitflow有害论值得一读,另外想要了解得更细致可以去到官网(中文版),里面详细列举了各种实践和反模式。)本文主要就项目上落地过程中遇到的一些问题做个简要的说明。
使用主干开发后,我们的代码库原则上就只能有一个 Master 分支了,所有新功能的提交也都提交到 Master 分支上,没有了分支的代码隔离,测试和解决冲突都变得简单,持续集成也变得稳定了许多,问题也接踵而至,主要有以下三个:
- 如何避免发布的时候引入未完成的 Feature
- 如何进行线上 Bug Fix
- 如何重构
如何避免发布引入未完成 Feature
答案是: Feature Toggle。
既然代码要随时保持可发布,而我们又需要只有一份代码来支持持续集成,在代码库里加一个特性开关来随时打开和关闭新特性是最容易想到的也是最容易被质疑的解决方案。
Feature Toggle 是有成本的,不管是在加 Toggle 的时候的代码设计,还是在移除 Toggle 时的人力成本和风险,都是需要和它带来的价值进行衡量的。事实上,在我们做一个前端的大特性变更的时候,我们确实没有因为没办法 Toggle 而采用了一个独立的 Feature 分支,我们认为即使为了这个分支单独做一套 Pipeline,也比在前端的各种样式间添加移除 Toggle 来得简单。但同时,团队商议决定在每次提交前都要先将 Master 分支 Merge 到 Feature 分支,以此避免分支隔离久以后合并时的痛苦。
如何进行线上 Bug Fix
在发布时打上 Release Tag,一旦发现这个版本有问题,如果这个时候Master分支上没有其他提交,可以直接在 Master 分支上 Hot Fix,如果 Master 分支已经有了提交就要做以下三件事:
- 从 Release Tag 创建发布分支。
- 在 Master 上做 Fix Bug 提交。
- 将 Fix Bug 提交 Cherry Pick 到 Release 分支。
- 在Release 分支再做一次发布。
线上 Fix 通常都比较紧急。看完这个略显繁琐 Bug Fix 流程,你可能会问为什么不在 Release 分支直接 Fix,再合并到 Master 分支?
这样做确实比较符合直觉,但事实是,如果在 Release 分支做 Fix,很可能会忘了 Merge 回 Master,试想深夜两点你做完 Bug Fix 眼看终于上线成功,这时的第一反应就是“终于可以下班了。什么,Merge 回 Master? 明天再来吧“ 等到第二天你早已把这个事忘得一干二净。而问题要等到下一次上线才会被暴露出来,一旦发现,而这个时候上一次 Release 的人又不在,无疑增加了很多工作量。
如何重构
这里指的是比较大规模的重构,无法在一次提交完成,TBD 要求每一次提交都是一个可上线的版本,所以这同时还意味着这个重构无法再一个上线周期内完成。
这种情况,需要在代码设计中增加一个抽象层,保证在重构过程中先不动原来的代码,也不破坏既有功能,类似于蓝绿部署中的负载均衡器的作用,这样的流程就是:
- 在将要被重构的代码逻辑附近引入抽象层然后提交,对所有人可见。如果有需要可以是多个提交,这些提交都不能破坏 build,然后依次 push 到共享代码库。
- 为将要被引入的代码写抽象层的第二次实现,然后提交。但在主干上由于关闭状态所以其他开发人员暂时不依赖于它。如果需要的话,这可能像上面那样需要多次提交。第一步的抽象层也可能偶然被调整,但必须遵循同样的原则:不能破坏build。
- 切换使用重构后的代码,然后 Push。
- 删除原有的旧实现(被重构代码)
- 删除抽象层
这个流程和汽车换轮胎有那么点类似,新旧轮胎代表重构前后代码,抽象层就好比千斤顶。
一点感受
TBD 还因为被 Google,亚马逊这样的公司采用而闻名,可以参照阮一峰的另篇文章:谷歌的代码管理,但并不因此意味着 TBD 就适用于所有场景。即使是是 CICD 已经被广泛接受,也不能称持续交付为软件开发的银弹。技术用的对不对,还是要看上下文。