研发效率破局之道07 | 分支管理:Facebook的策略,适合我的团队吗?

你好,我是葛俊。今天,我来跟你聊聊研发过程中的 Git 代码分⽀管理和发布策略。 
在前⾯两篇⽂章中,我们讨论了持续开发、持续集成和持续部署的整个上线流程。这条流⽔线针对的是分⽀,因此代码的分⽀管理便是基础。能否找到适合⾃ ⼰团队的分⽀管理策略,就是决定代码质量,以及发布顺畅的⼀个重要因素。 
Facebook 有⼏千名开发⼈员同时⼯作在⼀个⼤代码仓,每天会有⼀两千个代码提交⼊仓 ,但仍能顺利地进⾏开发,并发布⾼质量的产品。平⼼⽽论, Facebook 的⼯程⽔平的确很⾼,与他们的分⽀管理息息相关。 
所以在今天这篇⽂章中,我会先与你详细介绍 Facebook 的分⽀管理策略,以及背后的原因;然后,与你介绍其他的常⻅分⽀管理策略;最后,向你推荐如 何选择适合⾃⼰的分⽀策略。 
Facebook 的分⽀管理和发布策略 
Facebook 的分⽀管理策略,是⼀种基于主⼲的开发⽅式,也叫作 Trunk-based。在这种⽅式中,⽤于开发的⻓期分⽀只有⼀个,⽽⽤于发布的分⽀可以有 多个。 
⾸先,我们先看看这个⻓期存在的开发分⽀。 
开发分⽀ 
这个⻓期存在的开发分⽀,⼀般被叫作 trunk 或者 master。为⽅便讨论,我们统⼀称它为 master。也就是说,所有的开发⼈员基于 master 分⽀进⾏开发, 提交也直接 push 到这个分⽀上。 
在主⼲开发⽅式下,根据是否允许存在短期的功能分⽀(Feature Branch),⼜分为两个⼦类别:主⼲开发有功能分⽀和主⼲开发⽆功能分⽀。Facebook 做 得⽐较纯粹,在主代码仓中,基本上禁⽌功能分⽀。 
另外,在代码合并回 master 的时候,⼜有 rebase 和 merge 两种选择。Facebook 选择的是 rebase(关于这样选择的原因,我会在后⾯与你详细介绍)。 所以,Facebook 的整个开发模式⾮常简单,步骤⼤概如下。
第⼀步,获取最新代码。 
git checkout master
git fetch
git rebase origin/master
第⼆步,本地开发,然后执⾏  
git add
git commit
产⽣本地提交。
第三步,推送到主代码仓的 master 分⽀。 
git fetch
git rebase origin/master
git push
在 rebase 的时候,如果有冲突就先解决冲突,然后使⽤ 
git add
git commit
更新⾃⼰的提交,最后重复步骤 3,也就是重新尝试推送代码到主代码仓。
看到这⾥,你可能对这种简单的分⽀⽅式有以下两个问题。 
问题 1:如果功能⽐较⼤,⼀个代码提交不合适,怎么办? 
解决办法:这种情况下,第⼆步本地开发的时候可以产⽣多个提交,最后在第三步⼀次性推送到主仓的 master 分⽀。 
问题 2:如果需要多⼈协同⼀个较⼤的功能,怎么办? 
解决办法:这种情况下,Facebook 采⽤的是使⽤代码原⼦性、功能开关、API 版本等⽅法,让开发⼈员把功能拆⼩尽快合并到 master 分⽀。 
⽐如,⼀个后端开发者和⼀个前端开发者合作⼀个功能,他们的互动涉及 10 个 API 接⼝,其中两个是在已有接⼝上做改动,另外 8 个是新增接⼝。 
这两名开发者的合作⽅式是: 
  • 第⼀,后端开发者把这 10 个接⼝的编码⼯作,以⾄少 10 个单独的提交来完成。强调“⾄少”,是因为有些接⼝的编码⼯作可能⽐较⼤,需要不⽌⼀个提交 来完成。 
  • 第⼆,对已有 API 的改动,如果只涉及增加 API 参数,情况就⽐较简单,只需要在现有 API 上进⾏。但如果牵涉到删除或者修改 API 参数,就要给这个 API 添加⼀个新版本,避免被旧版本阻塞⼊库。 
  • 第三,在实现功能的过程中,如果某个功能暂时还不能暴露给⽤户,就⽤功能开关把它关闭。
这就保证了,在不使⽤功能分⽀的情况下,这两个开发者可以直接在 master 分⽀上合作,并能够不被阻塞地尽快提交代码。当然了,这种合作⽅式,可以扩 展到更多的开发者。 
以上就是开发分⽀的情况。接下来,我再与你讲述发布分⽀和策略。 
发布分⽀ 
基于主⼲开发模式中,在需要发布的时候会从 master 拉出⼀条发布分⽀,进⾏测试、稳定。在发布分⽀发现问题后,先在 master 上修复,然后 cherrypick 到发布分⽀上。分⽀上线之后,如果需要⻓期存在,⽐如产品线性质的产品,就保留。如果不需要⻓期存在,⽐如 SaaS 产品,就直接删除。Facebook 采⽤的⽅式是后者。 
具体来说,部署包括 3 种:有每周⼀次的全量代码部署、每天两次的⽇部署,以及每天不定次数的热修复部署。⽇部署和热修复部署类似,我们下⾯详细介 绍周部署和热修复部署。 
每次周部署代码的时候,流程如下所示。 
第⼀步,从 master 上拉出⼀个发布分⽀。 
git checkout -b release-date-* origin/master
第⼆步,在发布分⽀进⾏各种验证。 
第三步,如果验证发现问题,开发者提交代码到 master,然后⽤ cherry-pick 命令把修复合并到发布分⽀上: 
git cherry-pick <fix-sha1> # fix-sha1 是修复提交的 commit ID
接着继续回到第⼆步验证。 
验证通过就发布当前分⽀。这个发布分⽀就成为当前⽣产线上运⾏版本对应的分⽀,我们称之为当前⽣产分⽀,同时将上⼀次发布时使⽤的⽣产分⽀存档或者 删除。
在进⾏热修复部署时,从当前⽣产分⽀中拉出⼀个热修复分⽀,进⾏验证和修复。具体步骤为: 
第⼀步,拉出⼀个热修复分⽀。 
git checkout -b hotfix-date-* release-date-*
第⼆步,开发⼈员提交热修复到 master,然后 cherry-pick 修复提交到热修复分⽀上。 
git cherry-pick <fix-sha1>
第三步,进⾏各种验证。 
第四步,验证中发现问题,回到第⼆步重新修复验证。验证通过就发布当前热修复分⽀,同时将这个热修复分⽀设置为当前的⽣产分⽀,后⾯如果有新的热修 复,就从这个分⽀拉取。 
这⾥有⼀张图⽚,描述了每周五拉取周部署分⽀,以及从周部署分⽀上拉取分⽀进⾏热修复部署的流程。
图 1 Facebook 的代码分支管理和部署流程
以上就是 Facebook 的代码分⽀管理和部署流程。 
需要注意的是,这⾥描述的部署流程是 Facebook 转到持续部署之前采⽤的。但考虑到⾮常多的公司还没有达到持续部署的成熟度,所以这种持续交付的⽅ 式,对我们更有参考价值。 
Facebook 分⽀管理策略的背后原因 
Facebook 采⽤主⼲分⽀模式,最⼤的好处是可以把持续集成、持续交付做到极致,从⽽尽量提⾼ master 分⽀的代码质量。 
解释这⼀好处之前,我想请你先看看下⾯这 3 个措施有什么共同效果: 
  • ⼏千名开发者同时⼯作在同⼀条主⼲; 
  • 不使⽤功能分⽀,直接在 master 上开发; 
  • 必须要使⽤ rebase 才能⼊库,不能使⽤ merge。
其实,它们的共同效果就是:必须尽早将代码合⼊ master 分⽀,否则就需要花费相当⻓的时间去解决合并冲突。所以每个开发⼈员,都会尽量把代码进⾏原 ⼦性拆分,写好⼀部分就赶快合并⼊库。 
我曾经有过⼀个有趣的经历。⼀天下午,我和旁边的同事在改动同⼀个 API 接⼝,实现两个不同的功能。我们关系很好,也都清楚对⽅在做什么,于是⼀边 开玩笑⼀边像在⽐赛⼀样,看谁先写好代码完成⾃测⼊主库。结果是我赢了,他后来花了⼗分钟很⼩⼼地去解决冲突。 
Facebook 使⽤主⼲分⽀模式的好处,主要可以总结为以下两点: 
  • 能够促进开发⼈员把代码频繁⼊主仓进⾏集成检验。⽽这,正是持续集成的精髓。与之相对应的是,很多 20 名开发者的⼩团队,采⽤的也是共主⼲开发⽅ 式,但使⽤了功能分⽀,让⼤家在功能分⽀上进⾏开发,之后再 merge 回主仓。结果是,⼤家常常拖到产品上线前才把功能分⽀合并回主⼲,导致最后关 头出现⼤量问题。 
  • 能够确保线性的代码提交历史,给流程⾃动化提供最⼤⽅便。不要⼩看“线性”,它对⾃动化定位问题意义⾮凡,使得我们可以从当前有问题的提交回溯, 找到历史上第⼀个有问题的提交。更棒的是,我们还可以使⽤折半查找(也叫作⼆分查找)的办法,⽤ O(LogN) 的时间找到那个有问题的提交。
⽐如,在⼀个代码仓中,有 C000 ~ C120 的线性提交历史。我们知道⼀个测试在提交 C100 处都是通过的,但是在 C120 出了问题。我们可以依次 checkout C101、C102,直到 C120,每次 checkout 之后运⾏测试,总能找到第⼀个让测试失败的提交。 
或者更进⼀步,我们可以先尝试 C100 和 C120 中间的提交 C110。如果测试在那⾥通过了,证明问题提交在 C111 和 C120 之间,继续检查 C115;否则就证 明问题提交在 C101 和 C110 之间,继续检查 C105。这就⼤⼤减少了检查次数。⽽这,正是软件算法中经典的折半查找。  
 事实上,Git 本身就提供了⼀个命令 git bisect ⽀持折半查找。⽐如,在刚才的例⼦中,如果运⾏测试的命令⾏是 runtest.sh。那么,我们可以使⽤下⾯的命 令来⾃动化这个定位流程: 
> git checkout master # 使用最新提交的代码
> git bisect start
> git bisect bad HEAD # 告知 git bisect,当前 commit 是有问题的提交
> git bisect good C100 # 告知 git bisect,C100 是没有问题的提交
> git bisect run bash runtest.sh # 开始运行自动化折半查找
Cxxx is the first bad commit  # 查找到第一个问题提交
bisect run success
> git bisect reset # 结束 git bisect。回到初始的 HEAD
很⽅便吧。⽽如果历史不是线性的,也就是说如果提交使⽤了 merge,那么我们就不能⽅便地定位出第⼀个问题提交了,更别说是折半查找了。 
这种快速定位问题的能⼒,可以给 CI/CD 带来巨⼤好处。在持续交付过程中,我们常常没有⾜够的资源对每⼀个提交都进⾏检查。⽐如前⾯提过,Facebook 的持续交付流⽔线就是每隔⼀段时间,对代码仓最后⼀个提交运⾏流⽔线的检查。如果发现问题,就可以通过上⾯这种⽅法⾃动化地找到问题提交,并⾃动产 ⽣ Bug ⼯单,分配给提交者。 
其他主要分⽀⽅式 
除了主⼲开发的分⽀管理策略,还有 3 种常⽤⽅式: 
  • Git-flow⼯作流; 
  • Fork-merge ⼯作流; 
  • 灵活的功能分⽀组合成发布分⽀。
我在⽂中给出了链接供你参考。接下来,我们具体看看这⼏种⽅式。 
Git-flow ⼯作流 Git-flow ⼯作流有两个⻓期分⽀:⼀个是 master,包含可以部署到⽣产环境的代码;另⼀个是 develop,是⼀个⽤来集成代码的分⽀,开发新功能、新发 布,都从 develop ⾥拉分⽀。此外,它还有 3 种短期分⽀,分别是新功能分⽀、发布分⽀、热修复分⽀,根据需要创建,当完成了⾃⼰的任务后就会被删 除。 
Git-flow ⼯作流的特点是规定很清晰,对各种开发任务都有明确的规定和步骤。⽐如: 
  • 开发新功能时,从 develop 分⽀拉出⼀个前缀为 feature- 的新功能分⽀,在本地开发,并推送到远端中⼼仓库,完成之后合并⼊ develop 分⽀,并删除 该功能分⽀。 
  • 发布新版本时,从 develop 分⽀拉出⼀个前缀为 release- 的发布分⽀,部署到测试、类⽣产等环境进⾏验证。发现问题后直接在发布分⽀上修复,测试通 过之后,把 release 分⽀合并到 master 和 develop 分⽀。在 master 分⽀上打 tag,并删除该发布分⽀。
这种⼯作流,在前⼏年⾮常流⾏。它的好处是流程清晰,但缺点是: 
  • 流程复杂,学习成本⾼。 
  • 容易出错,容易出现忘记合并到某个分⽀的情况。不过可以使⽤脚本⾃动化来解决。 
  • 不⽅便进⾏持续集成。 
  • 有太多的代码分⽀合并,解决冲突成本⽐较⾼。
Fork-merge 
Fork-merge 是在 GitHub、GitLab 流⾏之后产⽣的,具体做法是:每个开发⼈员在代码仓服务器上有⼀个“个⼈”代码仓。这个“个⼈”代码仓实际上就是主代 码仓的⼀个 clone。开发者对主代码仓贡献代码的步骤如下: 
1. 开发者产⽣⼀个主代码仓的 fork,成为⾃⼰的个⼈代码仓; 
2. 开发者在本地 clone 个⼈代码仓; 
3. 开发者在本地开发,并把代码推送到⾃⼰的个⼈代码仓; 
4. 开发者通过 web 界⾯,向主代码仓作者提出 Pull request; 
5. 主代码仓的管理者在⾃⼰的开发机器上,取得开发者的提交,验证通过之后再推送到主代码仓。   
看起来步骤繁琐,但实际上和主⼲开发⽅式很相似,也有⼀个⻓期的开发分⽀,就是主仓的 master 分⽀。不同之处在于,它提供了⼀种对主分⽀更严格、更 ⽅便的权限管理⽅式,即只有主仓管理者有权限推送代码。同时,主仓不需要有功能分⽀,功能分⽀可以存在 fork 仓中。所以,主仓⼲净便于管理。 
这种⽅式对开源项⽬⽐较⽅便,但缺点是步骤繁琐,不太适⽤于公司内部。 
灵活的功能分⽀组合成发布分⽀ 
除了上述⽅式之外,还有⼀种⾮常灵活,但对⼯具⾃动化要求很⾼的分⽀⽅式,即基于功能分⽀灵活产⽣发布分⽀的⽅式。这种⽅式的典型代表是阿⾥云效的 “分⽀模式”。 
具体⽅法是⼤量使⽤⼯具对分⽀的管理进⾏⾃动化,开发⼈员在 web 界⾯上⾃助产⽣针对功能的分⽀。编码完成后,通过 web 界⾯对分⽀组合、验证,并上 线,上线之后分⽀再⾃动合⼊主库。 
这种⽅式的好处是: 
  • ⽅便基于功能进⾏开发。也就是说,开发者可以针对每个功能产⽣⼀个分⽀进⾏开发。 
  • 灵活,也就是能够⽅便地对功能进⾏组合,发布到对应环境上测试。出了问题,可以⽅便地添加或者删除功能。
但这种⽅式的问题是,对⼯具的依赖⽐较⾼,没有⼀个系统的⼯具来⾃动化的话,基本做不起来。另外,这种⽅式会⼤量封装底层的实现,使开发⼈员不知道 底层发⽣的问题,⼀旦出现问题就不太容易解决。 
哪⼀种分⽀管理策略更适合我的团队呢? 
要找到适合⾃⼰团队的分⽀管理策略,我们先来对⽐下上⾯提到的⼏种⽅式的优缺点吧。  
图 2 ⼏种常⽤的代码分⽀管理策略对⽐ 
另外,要找到合适的代码分⽀管理策略,你还可以参考以下 3 个问题,根据答案帮助你进⾏选择。 
问题 1:如果提供功能分⽀让成员共享,在哪⾥建⽴这个分⽀? 
如果团队不⼤,可以允许在主仓创建功能分⽀,不过注意定时删除不⽤的分⽀,避免影响 Git 的性能。如果团队⽐较⼤,可以考虑使⽤ Forkmerge ⽅式,在上⾯提到的“个⼈代码仓”⾥创建功能分⽀,从⽽避免污染主仓。
问题 2:要不要使⽤ Merge Commit? 
代码在合并到主干的时候,可以选择 rebase 或者 merge。使用 rebase 的好处是上边提到的方便定位问题。而使用 merge 的好处是,可以清晰地在分支里看到一个功能的所有提交,不像在 rebase 中,一个功能的提交往往是分散的。
问题 3:团队成熟度如何? 
单分支开发集成早,质量比较好,但对团队成员和流程自动化要求高。所以,如果你的团队比较小,或者比较成熟的话,可以考虑使用单分支,否则可以选择多分支开发模式,但要想办法把集成提前,同时逐步向单主干分支过渡。
总结来说,尽量减少⻓期分⽀的数量,代码尽早合并回主仓,⽅便使⽤ CI/CD 等⽅法保证主仓代码提交的质量,是选择分⽀策略的基本出发点。 
⼩结 
⾸先,我分享了 Facebook 使⽤的单主⼲开发分⽀,以及通过临时发布分⽀进⾏部署的分⽀管理策略和部署⽅式。然后,我与你介绍了⼏种常⻅的分⽀管理 策略,并给出了推荐的选择⽅法。 
在 Facebook ⼯作时,我们⼀直使⽤这种主⼲分⽀开发⽅式。它强迫我们把代码进⾏原⼦化,尽量确保每⼀个提交都尽快合⼊ master,并保证代码质量。⼀ 开始我不是很习惯,但习惯后我发现它的确很棒。 
⾸先,因为你和你的合作开发者都需要尽快把代码拆⼩、⼊仓,这就帮助我们提⾼了功能模块化的能⼒。其次,因为 master ⾥⾯的提交⼀般都⽐较健康,并 且是⽐较新的代码,所以很少会被不稳定的因素阻塞。最后,线性提交历史对开发者的⽇常⼯作也很有帮助。我们在开发的时候,常常会碰到⼀个本来⼯作得 好好的 API,在拉取到最新代码之后出现了问题。这时,我就可以使⽤这种⽅法找到第⼀个造成问题的提交,从⽽⽅便定位和解决问题。 
⼀个流程设计、实施得好,对产品来说可以提⾼质量,对团队来说可以提⾼效能,对个⼈来说可以帮助成⻓。这就是⼀举三得。 
思考题 
1. 产品线性质的产品开发,以及 SaaS 产品开发,在选择分⽀管理策略时有不同的考量。你觉得哪种分⽀管理⽅式更适合⼆者呢? 
2. 你知道 trunk-based ⾥⾯“trunk”的意思吗?
感谢你的收听,欢迎你在评论区给我留⾔分享你的观点,也欢迎你把这篇⽂章分享给更多的朋友⼀起阅读。我们下期再⻅!
各种分支管理策略,学习!问下,基于base开发的这种方式,是不是根据时间线,截止某一时间点(或者某个版本号之前的),代码验证过了,就可以上线了,而不是根据业务功能的先后紧急!
作者回复: 对的。这种方式是发布周期与功能解耦。版本火车一列一列发出去。功能开发者自己决定把commit搭乘哪一列。办不上的话,没办法,等下一列。
主干开发模式,对于开发的拆分能力要求很高,同时从测试的角度,又怎么确保整个项目和产品功能的完整性呢?
作者回复: DevOps,测试左移,测试右移都是比较有效的办法。后面的文章,会有比较详细的介绍,敬请期待。

发表评论

您的电子邮箱地址不会被公开。