对CI/CD与分支模型的理解

Posted by 王天一 on 2017-06-13

最近从前项目上刚下来,经历了项目中分支策略的重大变更,完成了对部署策略替换的实现,现在想把自己了解到的这方面知识记录以及总结一下,深入自己对其的理解程度。

#CI/CD
##概念
CI (Continuous Integration)和CD (Continuous Delivery)是敏捷中非常重要的概念,它以持续、敏捷的思想对项目开发、发布流程有一个具体的指导性实践。

###持续集成

具体来说,Martin Fowler的定义是:持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。

###持续交付

持续交付是一个软件开发原则,使软件随时可以发布到生产中。强调的是应用程序保持在可以部署到生产中的状态。图中是持续交付会涉及到的持续部署的过程,持续部署意味着每一个变化都会通过管道自动投入生产,每天都会有许多次部署。

###我的理解
持续集成是持续交付的基础,提前了代码融合的时机,同时提前了发现错误的时间点,也方便根据小步提交来定位错误;促进了人与人之间的交流;大大减少了合并代码产生冲突的可能性;但同时引入功能开关的问题。持续交付站在一个比较高的角度提供了一个软件开发与交付时的原则,是一个提高我们应用程序部署过程安全性、高效性、有效性的方式。

##前项目的应用
上面都是概念的东西,一定需要至少一个实践来验证或者展示概念对应的实体是否是我们想要的。并且当理论付诸实践的时候,你才会发现嘴上说说是多么的容易

我们项目上的CI/CD流程如图,省略掉具体测试与发布使用的AWS服务和测试工具。首先,代码被提交之后会触发一条新的流水线(Pipeline),经过自动化测试之后会部署到持续集成服务器中,然后跑User Journey测试,之后部署到UAT和PROD需要手动触发。

现状:

  • 对于持续集成。刚来项目的时候,我们的项目并不是采用持续集成或者说是非常低效的持续集成,因为我们当时没有规定每个成员集成的频率,而是按照每个人做卡的速度,当卡做完之后再进行提交,所以经常性的两天甚至五天进行一次提交。
  • 对于持续交付。即使持续集成没有做到很好,但是依然可以实现持续交付,因为我们有在提交代码之前对Dev的约定:本地测试通过并且通过Desk Check才能提交;以及每次提交都会体现在Pipeline的记录中,所以在经过QA测试之后的版本是可以随时挑选特定的代码版本发布到生产环境中。使用GoCD之后我们搭建一个属于我们自己项目的Pipeline,保证了代码中的每个变化都会自动化部署到生产管道中让GoCD帮我们跑测试、部署到QA服务器,等待QA测试以及部署到产品环境中。

###问题来了

  • 持续集成没有做到带来的问题。举一个最明显的例子。因为没有做到持续集成,我们经常碰到合并代码的时候冲突太大,特别是样式之间的冲突,因为对于样式,代码比较分散,重构不同的样式必定会影响到使用到此样式的组件,所以对于前端样式的卡,每次合并冲突的时候非常令人头疼的。
  • 即使做到之后,持续集成本身存在的问题。如果一天完成不了一张卡的开发、测试的话,但是为了持续集成,需要将代码提交到代码库,这时需要功能开关来将自己为完成的代码包裹起来,对外不可见。功能开关可能导致添加功能开关的难度(前端样式太过分散、重构期间的代码)、代码逻辑复杂、控制功能开关生命周期的effort。
  • 将代码合入到主干分支的安全性。即使跑过测试,经过Desk Check这样的初步测试,不是一个比较严格的代码审校流程。这个问题会放到另一个项目上的应用里讲到。

###如何改进
那么我们就开始严格的持续集成。约定:每人每天至少提交一次代码

那么更多的问题来了:跑测试的时间太长直接阻碍测试代码的编写与持续集成的速度、维护功能开关的effort、开发人员的习惯导致的遗忘。这些问题其实都不大,比如第一个问题可以通过改进测试来提高跑测试的速度(优化代码或实现并行测试)。这些就是我们为了实现持续集成的过程中需要面临和解决的问题

##另一个项目上的应用
在一些大公司中,存在一种比较完善、规范化、对代码安全有一定要求的的持续集成模型。

虚线右侧是我们接下来准备看到的策略。

  1. Dev在本地分支进行开发
  2. 完成之后将主干分支的代码拉下来合并到本地
  3. 将本地代码推到Gerrit的repo中
  4. 触发GoCD/Jenkins将代码部署到CI服务器中
  5. Team Lead或专人人工检测代码以及服务器运行状态
  6. 同意合并到主干分支/不同意,让Dev继续在该分支上开发,无视此次提交

这种策略与上个策略的区别在于对代码控制方面进行的比较严格,使用第三方工具来帮助我们代码审查,对提高代码库质量是非常好的一个实践。但同时增加了整个持续集成流程的复杂度和增加了DevOps实施难度。
#分支模型
很巧的是,上个项目使用过的两种分支模型恰好是老马这篇文章提到的前两种,顺便盗了两张图过来。
##功能分支模型

模型:

  • 为每个小功能(每张卡)开一个分支,在分支上提交时记得经常拉远端主干代码及时解决冲突,功能完成后合并到主分支。
  • 部署时选择主干上的某一次提交进行部署,Hotfix会从主干最新节点再加上一个提交。

提交到主干分支上的频率:

  • 2~5天/次

优点:

  • 所开发的功能之间可以保持相对独立,直到拉远端代码为止与他人无代码上冲突的可能
  • 一般不需要使用功能开关,因为功能之间(分支之间)已经独立开来
  • 结构简单,所有的分支都是基于卡的,并且全部围绕着一条主分支

缺点:

  • 当一个功能无法拆分成独立的卡时,卡之间的关联太多,可能会需要功能开关
  • 合并代码的时候冲突过多。如上图,P1-5提交到主干上之后,会是Green分支的噩梦

##单分支模型

模型:

  • 始终在主干分支上开发,实践小步提交(比如以方法为单位进行提交),遇到冲突及时与冲突代码所有者协商,如果有未完成的代码但需要提交则使用功能开关隐藏起来。
  • 部署时选择主干上的某一次提交进行部署,Hotfix会从主干最新节点再加上一个提交。
  • 如果一张卡包含前后端,那么最好先开发后端再前端,因为功能开关在前端加最方便

优点:

  • 小步提交(持续集成),经常解决冲突避免一次性解决大冲突
  • 将冲突提前,及早的发现并解决问题

缺点:

  • 必须使用功能开关,存在一定的effort去维护它
  • Dev与QA需要保证开关两种情况下应用程序能正常使用
  • 一段时间后功能开关太多太杂难以移除、代码混乱不清晰

提交到主干分支上的频率:

  • 每天/次

##Git Flow模型

没有使用经验,请参看文章1文章2

#总结
以上总结了几种不同的持续集成方案、分支模型,但在具体项目中应当按具体需求确定使用哪一种方案或模型。就像如果你去别的国内外包公司想要他们使用TDD可能会碰到的尴尬一样,不要盲从某种方案,世界上没有银弹。

另外想说一句,吐槽的人多,行动的人少,想做你什么都不是,做了一部分但不彻底人们还是会瞧不起你,就像当你杀掉一百万人的时候你已经是国家领袖而不是当初的杀人犯了。
#注
以上只是个人总结以及个人理解,如有偏差,欢迎指正。
#参考资料
https://martinfowler.com/articles/continuousIntegration.html
https://martinfowler.com/delivery.html
https://martinfowler.com/bliki/FeatureBranch.html
http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html
http://www.mindtheproduct.com/2016/02/what-the-hell-are-ci-cd-and-devops-a-cheatsheet-for-the-rest-of-us/

号外号外

最近在总结一些针对Java面试相关的知识点,感兴趣的朋友可以一起维护~
地址:https://github.com/xbox1994/2018-Java-Interview