mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
del
This commit is contained in:
139
极客时间专栏/geek/持续交付36讲/配置管理/04 | 一切的源头,代码分支策略的选择.md
Normal file
139
极客时间专栏/geek/持续交付36讲/配置管理/04 | 一切的源头,代码分支策略的选择.md
Normal file
@@ -0,0 +1,139 @@
|
||||
<audio id="audio" title="04 | 一切的源头,代码分支策略的选择" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/52/ce/52565d5de9b66f1f25c64ccc8b68d4ce.mp3"></audio>
|
||||
|
||||
记得大概是一年前吧,我与好友老吴喝茶聊天时,讨论到:高效的持续交付体系,必定需要一个合适的代码分支策略。
|
||||
|
||||
我告诉老吴:“采用不同的代码分支策略,意味着实施不同的代码集成与上线流程,这会影响整个研发团队每日的协作方式,因此研发团队通常会很认真地选择自己的策略。”
|
||||
|
||||
老吴是一名有多年开发经验的资深架构师,当时正好要接手一个框架团队,从个人贡献者向团队管理者转型。他个人对代码管理工具可谓熟之又熟,甚至连“老古董”的CVS都可以跟你聊半天。但他在为团队制定代码分支管理策略时,还是慎之又慎,足见其重要性。
|
||||
|
||||
最后我们发现,要确定选用哪种代码分支管理策略,需要先假设几个问题,这几个问题有了答案,也就代表你找到了适合的方向。
|
||||
|
||||
你需要思考的几个问题如下:
|
||||
|
||||
<li>
|
||||
Google和Facebook这两个互联网大咖都在用主干开发(Trunk Based Development,简称TBD),我们是不是也参照它俩,采用主干开发分支策略?
|
||||
</li>
|
||||
<li>
|
||||
用Google搜索一下,会发现有个排名很靠前的分支策略,叫“A successful Git branching model”(简称 Git Flow),它真的好用吗?团队可以直接套用吗?
|
||||
</li>
|
||||
<li>
|
||||
GitHub 和 GitLab 这两个当下最流行的代码管理平台,各自推出了 GitHub Flow 和 GitLab Flow,它们有什么区别?适合我使用吗?
|
||||
</li>
|
||||
<li>
|
||||
像阿里、携程和美团点评这样国内知名的互联网公司,都在用什么样的分支策略?
|
||||
</li>
|
||||
|
||||
今天,我想再沿着当时的思考路径,和你一起回顾和总结一下,希望能够带你全面了解代码分支策略,帮助你做出合适的选择。
|
||||
|
||||
## 谈谈主干开发(TBD)
|
||||
|
||||
**主干开发是一个源代码控制的分支模型,开发者在一个称为 “trunk” 的分支(Git 称 master) 中对代码进行协作,除了发布分支外没有其他开发分支。**
|
||||
|
||||
Google和 Facebook都是采用“主干开发”的方式,代码一般直接提交到主干的头部,这样可以保证所有用户看到的都是同一份代码的最新版本。
|
||||
|
||||
**“主干开发”确实避免了合并分支时的麻烦,因此像Google这样的公司一般就不采用分支开发,分支只用来发布。**
|
||||
|
||||
大多数时候,发布分支是主干某个时点的快照。以后的改Bug和功能增强,都是提交到主干,必要时 cherry-pick (选择部分变更集合并到其他分支)到发布分支。与主干长期并行的特性分支极为少见。
|
||||
|
||||
由于不采用“特性分支开发”,所有提交的代码都被集成到了主干,为了保证主干上线后的有效性,一般会使用特性切换(feature toggle)。特性切换就像一个开关可以在运行期间隐藏、启用或禁用特定功能,项目团队可以借助这种方式加速开发过程。
|
||||
|
||||
特性切换在大型项目持续交付中变得越来越重要,因为它有助于将部署从发布中解耦出来。但据吉姆 · 伯德(Jim Bird)介绍,特性切换会导致代码更脆弱、更难测试、更难理解和维护、更难提供技术支持,而且更不安全。
|
||||
|
||||
他的主要论据是,将未经测试的代码引入生产环境是一个糟糕的主意,它们引发的问题可能会在无意间暴露出来。另外,越来越多的特性切换会使得逻辑越来越混乱。
|
||||
|
||||
**特性切换需要健壮的工程过程、可靠的技术设计和成熟的特性切换生命周期管理**,如果不具备这三个关键的条件,使用特性切换反而会降低生产力。
|
||||
|
||||
根据上面的分析,主干开发的分支策略虽然有利于开展持续交付,但是它对开发团队的能力要求也更高。
|
||||
|
||||
主干开发的优缺点如表1所示。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/59/5e/59096cdc19f9e0a5fefbe99f9b57445e.png" alt="">
|
||||
|
||||
## 谈谈特性分支开发
|
||||
|
||||
和主干开发相对的是 “特性分支开发” 。在这个大类里面,我会给你分析Git Flow、GitHub Flow和 GitLab Flow这三个常用的模型。
|
||||
|
||||
**第一,Git Flow**
|
||||
|
||||
我们在Google上查关键词“branch model”(也就是“分支模型”),有一篇排名比较靠前的文章“A successful Git branching model”,它介绍了Git Flow模型。
|
||||
|
||||
Git刚出来的那些年,可参考的模型不多,所以Git Flow模型在2011年左右被大家当作了推荐的分支模型,至今也还有项目团队在使用。然而,Git Flow烦琐的流程也被许多研发团队吐槽,大家普遍认为hotfix和 release 分支显得多余,平时都不会去用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/0f/4bc4c4035857479b2126fed5af4ce90f.png" alt="">
|
||||
|
||||
**第二,GitHub Flow**
|
||||
|
||||
GitHub Flow 是 GitHub 所使用的一种简单流程。该流程只使用master和特性分支,并借助 GitHub 的 pull request 功能。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7a/14/7ae8a5fe4f5fcdae7ba9cfaf758f1a14.png" alt="">
|
||||
|
||||
**在 GitHub Flow 中,master 分支中包含稳定的代码,它已经或即将被部署到生产环境。任何开发人员都不允许把未测试或未审查的代码直接提交到 master 分支。对代码的任何修改,包括Bug 修复、热修复、新功能开发等都在单独的分支中进行。不管是一行代码的小改动,还是需要几个星期开发的新功能,都采用同样的方式来管理。**
|
||||
|
||||
当需要修改时,从 master 分支创建一个新的分支,所有相关的代码修改都在新分支中进行。开发人员可以自由地提交代码和提交到远程仓库。
|
||||
|
||||
当新分支中的代码全部完成之后,通过 GitHub 提交一个新的 pull request。团队中的其他人员会对代码进行审查,提出相关的修改意见。由持续集成服务器(如 Jenkins)对新分支进行自动化测试。当代码通过自动化测试和代码审查之后,该分支的代码被合并到 master 分支。再从 master 分支部署到生产环境。
|
||||
|
||||
GitHub Flow 的好处在于非常简单实用,开发人员需要注意的事项非常少,很容易形成习惯。当需要修改时,只要从 master 分支创建新分支,完成之后通过 pull request 和相关的代码审查,合并回 master 分支就可以了。
|
||||
|
||||
**第三,GitLab Flow**
|
||||
|
||||
上面提到的GitHub Flow,适用于特性分支合入master后就能马上部署到线上的这类项目,但并不是所有团队都使用GitHub或使用pull request功能,而是使用开源平台GitLab,特别是对于公司级别而言,代码作为资产,不会随意维护在较公开的GitHub上(除非采用企业版)。
|
||||
|
||||
GitLab Flow 针对不同的发布场景,在GitHub Flow(特性分支加master分支)的基础上做了改良,额外衍生出了三个子类模型,如表2所示。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/34/0efbe615c847aac070fdb9b8a588a434.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/1e/8e29bb4314de4492977e919abe91191e.jpg" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/c8/bd6218d57b6d522844cf080d9ca9a7c8.jpg" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1b/58/1bf962fe8b0523fefac348529ffc0d58.jpg" alt="">
|
||||
|
||||
GitLab Flow的特性分支合入master用的是“Merge Request”,功能与GitHub Flow的“pull request”相同,这里不再赘述。
|
||||
|
||||
通过Git Flow、GitHub Flow和 GitLab Flow(3个衍生类别) 这几个具体模型的介绍,我给你总结一下特性分支开发的优缺点。如表3所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/b1/a517f2b1c48c8de54e500df3996628b1.png" alt="">
|
||||
|
||||
## 选出最适合的分支策略
|
||||
|
||||
上面我跟你讲到的分支模型,都是IT研发领域比较流行的。虽然有些策略带上了代码平台的标识,如GitHub Flow,但并不意味着该策略仅限于GitHub代码平台使用,你完全可以在自己搭建的代码平台上使用这些策略。
|
||||
|
||||
接下来,我就总体归纳一下什么情况下应该选择什么样的分支策略。如表4所示。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/d9/ae29f0b5a96306f49ce485c1823b23d9.png" alt="">
|
||||
|
||||
## 国内互联网公司的选择
|
||||
|
||||
GitLab作为最优秀的开源代码平台,被多数互联网大公司(包括阿里、携程和美团点评等)所使用,这些大厂也都采用特性分支开发策略。当然,这些大公司在长期持续交付实践中,会结合各自公司的情况做个性化的定制。
|
||||
|
||||
比如,携程公司在GitHub Flow的基础上,通过自行研发的集成加速器(Light Merge)和持续交付Paas平台,一起完成集成和发布。
|
||||
|
||||
再比如,阿里的AoneFlow,采用的是主干分支、特性分支和发布分支三种分支类型,再加上自行研发的Aone协同平台,实现持续交付。
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我主要给你介绍了各种代码分支策略的特性。
|
||||
|
||||
你应该已经比较清晰地理解了“主干开发”和“特性分支开发”两种策略的各自特性:
|
||||
|
||||
<li>
|
||||
“主干开发”集成效率高,冲突少,但对团队个人的开发能力有较高要求;
|
||||
</li>
|
||||
<li>
|
||||
“特性分支开发”有利于并行开发,需要一定的流程保证,能保证主干代码质量。
|
||||
</li>
|
||||
|
||||
相信在没有绝对自信能力的情况下,面对绝大多数的场景,企业还是会选择“特性分支开发”的策略。所以,我给你介绍了几种主流的特性分支方法,并对比了各类策略的优劣,以及它们适用的场景。
|
||||
|
||||
接下来,你就可以根据自己所在项目的具体情况,参考今天的内容,裁剪出最适合自己团队的分支策略了。
|
||||
|
||||
## 思考题
|
||||
|
||||
<li>
|
||||
开源性质的项目,为什么不适合用主干开发的分支策略?
|
||||
</li>
|
||||
<li>
|
||||
如果你所在的团队只有5人,而且迭代周期为1周,你会采用什么样的分支策略?
|
||||
</li>
|
||||
|
||||
欢迎你给我留言。
|
||||
|
||||
|
||||
177
极客时间专栏/geek/持续交付36讲/配置管理/05 | 手把手教你依赖管理.md
Normal file
177
极客时间专栏/geek/持续交付36讲/配置管理/05 | 手把手教你依赖管理.md
Normal file
@@ -0,0 +1,177 @@
|
||||
<audio id="audio" title="05 | 手把手教你依赖管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/7f/836151335836db3e6e2c02bc77a37a7f.mp3"></audio>
|
||||
|
||||
软件工程是多人合作的结果,我们在开发软件的时候经常会使用一些别人编写好的,比较成熟的库。
|
||||
|
||||
比如,早期的前端开发用到了 jQuery 库,那么通常的做法是去官网下载一个最新版本的 jQuery,然后放在自己本地的项目中。对于简单的前端项目来说,这样可以简单粗暴地达到目的。
|
||||
|
||||
但当项目越来越庞大,除了 jQuery 之外,你还会依赖一些其他的第三方库。比如 Bootstrap 与 Chosen,这两个流行的前端库也都依赖 jQuery,如果这些第三方库依赖的jQuery 版本一致还好,但大多数情况并没有这么乐观:
|
||||
|
||||
>
|
||||
你的项目依赖的 jQuery 版本是 1.0.0 ,Bootstrap 依赖的版本是 1.1.0,而 Chosen 依赖的版本是 1.2.0,看上去都是小版本不一致,一开始并没有发现任何问题,但是如果到后期发现不兼容,可能就为时已晚了。
|
||||
|
||||
|
||||
所以,你需要在确定依赖之前,就把整个系统的依赖全部梳理一遍,保证每个依赖都不会有冲突问题。
|
||||
|
||||
你可能会质疑,这个前端工程师一定是初级的,事先都不会确认 Bootstrap 和 Chosen 依赖的版本吗,直接选择依赖 jQuery 1.0.0 版本的不就行了?
|
||||
|
||||
这么说有一定道理,但是手工维护这些依赖是相当麻烦且容易出错的。随便找一个比较流行的开源软件,你都会发现它依赖了大量的第三方库,而这些第三方库又依赖着其他的第三方库,形成了一条十分复杂的依赖链。靠人工去解决这个依赖链一定会让你怀疑人生,因此你需要一些工具去管理项目的依赖。
|
||||
|
||||
## 你见过几种依赖管理工具?
|
||||
|
||||
其实,各大平台早已有一套自己的手段来解决上述的问题,仔细看看你常用的软件,你会发现其实工作当中已经充斥着各种各样的依赖管理工具,没有它们你将寸步难行。
|
||||
|
||||
>
|
||||
<p>操作系统的依赖管理工具,比如CentOS 的 yum,Debian 的 apt,Arch 的 Packman,macOS 的 Homebrew;<br>
|
||||
编程语言的依赖管理工具,比如Java 的 Maven, .Net 的 nuget,Node.js 的 npm,Golang 的 go get,Python 的 pip,Ruby 的 Gem 。</p>
|
||||
|
||||
|
||||
这些平台的解决思路都是将依赖放到共同的仓库,然后管理工具通过依赖描述文件去中央仓库获取相应的包。
|
||||
|
||||
一个典型的依赖管理工具通常会有以下几个特性:
|
||||
|
||||
<li>
|
||||
统一的命名规则,也可以说是坐标,在仓库中是唯一的,可以被准确定位到;
|
||||
</li>
|
||||
<li>
|
||||
统一的中心仓库可以存储管理依赖和元数据;
|
||||
</li>
|
||||
<li>
|
||||
统一的依赖配置描述文件;
|
||||
</li>
|
||||
<li>
|
||||
本地使用的客户端可以解析上述的文件以及拉取所需的依赖。
|
||||
</li>
|
||||
|
||||
接下来我以 Maven 为例,跟你一起探究一下 Maven 会如何管理 Java 项目的依赖。
|
||||
|
||||
## Maven 如何管理依赖?
|
||||
|
||||
Maven 是 Java生态系统里面一款非常强大的构建工具,其中一项非常重要的工作就是对项目依赖进行管理。
|
||||
|
||||
Maven 使用 XML格式的文件进行依赖配置描述的方式,叫作 POM(Project Object Model ),以下就是一段简单的 pom.xml 文件片段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/b5/b37b7cb7e75517bf64695bcfa63529b5.png" alt="">
|
||||
|
||||
在POM中,根元素 project 下的 dependencies 可以包含一个或多个 dependency 元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:
|
||||
|
||||
<li>
|
||||
groupId、artifactId、version: 依赖的基本坐标;
|
||||
</li>
|
||||
<li>
|
||||
type: 依赖的类型,默认为jar;
|
||||
</li>
|
||||
<li>
|
||||
scope: 依赖的范围;
|
||||
</li>
|
||||
<li>
|
||||
optional: 标记依赖是否可选;
|
||||
</li>
|
||||
<li>
|
||||
exclusions: 用来排除传递性依赖;
|
||||
</li>
|
||||
|
||||
要想用好Maven管理依赖,你必须理解每一项的含义,而新手通常傻傻分不清楚。举个例子,依赖范围这一项,Maven 在不同的时期会使用不同的 classpath :
|
||||
|
||||
- 比如,junit 只有在测试的时候有用,那么将其设为 test scope 就可以;
|
||||
- 再比如 ,servlet API这个 jar 包只需要在编译的时候提供接口,但是实际在运行时会有对应的 servlet 容器提供,所以没必要打到 war 包中去,这时候只需要指定在 provided scope 就可以了。通过指定 provided scope的方式可以让每个依赖各司其职,不用弄成“一锅粥”。
|
||||
|
||||
包管理工具还解决了依赖传递的问题,比如你的项目 A 依赖了 B,而 B 依赖了 C和D,那么在获取依赖的时候会把 B、C、D 都一起拉下来,这样可以节省大量的时间。
|
||||
|
||||
再让我们回到最开始的问题:依赖不一致该如何处理?通常,每个包管理工具都有一套自己的原则,Maven 的依赖仲裁原则如下。
|
||||
|
||||
**第一原则: 最短路径优先原则。** 比如,A 依赖了 B和C,而 B 也依赖了 C,那么 Maven 会使用 A 依赖的 C 的版本,因为它的路径是最短的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/38/a64e8f8816eb5f91538c70b117e8e938.png" alt="">
|
||||
|
||||
**第二原则: 第一声明优先原则。** 比如,A 依赖了 B和C,B 和 C 分别依赖了 D,那么 Maven 会使用 B 依赖的 D 的版本,因为它是最先声明的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/75/58/75c0189d4c6d3f28074b315928a36d58.png" alt="">
|
||||
|
||||
**根据这两个原则,Maven 就可以确定一个项目所有依赖的列表,但它处理依赖的方式还是有些简单粗暴。有时 Maven 的决定结果并不是你想要的,所以我们在使用 Maven 的时候还是要多加小心。**
|
||||
|
||||
## Maven 最佳实践
|
||||
|
||||
接下来,我跟你分享下,我平时使用Maven 时总结的一些经验。
|
||||
|
||||
<li>
|
||||
生产环境尽量不使用 SNAPSHOT 或者是带有范围的依赖版本,可以减少上线后的不确定性,我们必须保证,测试环境的包和生产环境是一致的。
|
||||
</li>
|
||||
<li>
|
||||
将 POM分成多个层次的继承关系,比如携程的POM继承树一般是这样:
|
||||
</li>
|
||||
|
||||
```
|
||||
corp pom
|
||||
ctrip pom/qunar pom
|
||||
bu pom
|
||||
product pom
|
||||
project parent pom
|
||||
project sub module pom
|
||||
|
||||
```
|
||||
|
||||
这样做的好处是每一层都可以定义这一级别的依赖。<br>
|
||||
其中 ctrip pom/qunar pom 我们叫它为公司的 super-pom,每个项目必须直接或间接的继承其所在公司的 super-pom。这样做的好处是每一层都可以定义这一级别的依赖,便于各个层次的统一管理。
|
||||
|
||||
<li>
|
||||
在父模块多使用 dependencyManagement 来定义依赖,子模块在使用该依赖时,就可以不用指定依赖的版本,这样做可以使多个子模块的依赖版本高度统一,同时还能简化子模块配置。
|
||||
</li>
|
||||
<li>
|
||||
<p>对于一组依赖的控制,可以使用BOM(Bill of Materials) 进行版本定义。一般情况下,框架部门有一个统一的BOM 来管理公共组件的版本,当用户引用了该BOM后,在使用框架提供的组件时无需指定版本。即使使用了多个组件,也不会有版本冲突的问题,因为框架部门的专家们已经在BOM中为各个组件配置了经过测试的稳定版本。<br>
|
||||
BOM是一个非常有用的工具,因为面对大量依赖时,作为用户你不知道具体应该使用它们的哪些版本、这些版本之间是否有相互依赖、相互依赖是否有冲突,使用BOM 就可以让用户规避这些细节问题了。</p>
|
||||
</li>
|
||||
<li>
|
||||
对于版本相同的依赖使用 properties 定义,可以大大减少重复劳动,且易于改动。上面的 pom.xml 片段,就是使用了 properties 来定义两个一样的版本号的依赖。
|
||||
</li>
|
||||
<li>
|
||||
不要在在线编译环境中使用 mvn install 命令,否则会埋下很多意想不到并且非常难以排查的坑:该命令会将同项目中编译产生的jar包缓存在编译系统本地,覆盖mvn仓库中真正应该被引用的jar包。
|
||||
</li>
|
||||
<li>
|
||||
禁止变更了代码不改版本号就上传到中央仓库的行为。否则,会覆盖原有版本,使得一个版本出现二义性的问题。
|
||||
</li>
|
||||
|
||||
归根结底,这些经验都是为了两件事:减少重复的配置代码,以及减少不确定的因素发生。
|
||||
|
||||
有时候,你会听到来自业务开发部门同事传来报障的声音:“为什么我本地可以编译通过,而你们编译系统编译通不过?”难道 Maven 在工作的时候还看脸? 当然不是!
|
||||
|
||||
遇到这样的情况不要急,处理起来通常有如下“三板斧”:
|
||||
|
||||
<li>
|
||||
确认开发操作系统,Java 版本,Maven 版本。通常情况下操作系统对 Java 编译的影响是最小的,但是偶尔也会遇到一些比如分隔符(冒号与分号)之类的问题。Java 和 Maven 的版本应尽量与生产编译系统保持一致,以减少不必要的麻烦。
|
||||
</li>
|
||||
<li>
|
||||
如果确认了开发操作系统没问题,那么你可以把用户的项目拉到自己的本地,并且删除本地依赖的缓存,也就是删除 .m2 目录下的子目录,减少干扰,执行编译。若编译通不过,说明用户本地就有问题,让他也删掉自己本地的缓存找问题。如果可以编译通过,说明问题出在编译系统,进入第3步。
|
||||
</li>
|
||||
<li>
|
||||
使用 mvn dependency 命令对比生产编译系统与本地依赖树的区别,检查编译系统本地是否被缓存了错误的 jar 包,从而导致了编译失败。有时候这种错误会隐藏得比较深,非常难查,需要很大的耐心。
|
||||
</li>
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我跟你聊了聊依赖的问题。你可以从中:
|
||||
|
||||
<li>
|
||||
了解到依赖管理的复杂度是如何产生的;
|
||||
</li>
|
||||
<li>
|
||||
学习到依赖管理的一些常规思路;
|
||||
</li>
|
||||
<li>
|
||||
初步掌握通过Maven进行依赖管理的方式方法,及一些最佳实践。
|
||||
</li>
|
||||
|
||||
同时我也与你一起分享了一些我的实际经验,希望能够对你在实际工作中有所帮助。
|
||||
|
||||
## 后续
|
||||
|
||||
理想是美好的,然而现实却很骨感,在实际过程中我们也遇到了一些问题,比如用户不遵守我们推荐的命名规则,或者不继承公司提供的 Super POM,或者框架组件升级而用户不愿意升级等等。
|
||||
|
||||
为了能够统一管理,我们在构建系统上增加了一些强制手段来做统一的约束,使用 Maven Enforcer 插件以及其他方式对构建过程实行大量检查,欲知详情,请听下回分解。
|
||||
|
||||
## 思考题
|
||||
|
||||
你可以对比一下 Maven 其他语言的依赖管理工具,如 npm、pip、gem 等等,看看它们各自都有什么样的优缺点。
|
||||
|
||||
欢迎你给我留言。
|
||||
|
||||
|
||||
205
极客时间专栏/geek/持续交付36讲/配置管理/06 | 代码回滚,你真的理解吗?.md
Normal file
205
极客时间专栏/geek/持续交付36讲/配置管理/06 | 代码回滚,你真的理解吗?.md
Normal file
@@ -0,0 +1,205 @@
|
||||
<audio id="audio" title="06 | 代码回滚,你真的理解吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/bc/926b17ecd2e841a914c26b96a90910bc.mp3"></audio>
|
||||
|
||||
## 什么是代码回滚?
|
||||
|
||||
在我正式开始今天的分享前,先给你讲两个核心概念:
|
||||
|
||||
<li>
|
||||
包回滚是指,线上运行的系统,从现在的版本回滚到以前稳定的老版本。
|
||||
</li>
|
||||
<li>
|
||||
代码回滚是指,Git分支的指针(游标),从指向当前有问题的版本改为指向一个该分支历史树上没问题的版本,而这个版本可以是曾经的commit,也可以是新建的commit。
|
||||
</li>
|
||||
|
||||
## 你是不是也遇到了问题?
|
||||
|
||||
在日常的代码管理中,困扰开发工程师最多,也是他们向我咨询得最多的问题就是:代码回滚的问题。这些问题,有的只是影响个人开发,而有的涉及了整个团队。我把这些问题进行了整理汇总,你可以看看是否也遇到过类似的问题?
|
||||
|
||||
<li>
|
||||
<p>今天上午我在自己的开发环境上拉了一条新分支,提交了5个commit,最新提交的3个commit 我不想要了,那我该怎么退回到这3个commit之前的那个commit?<br />
|
||||
答:参考我在下面即将分享的“个人分支回滚”的内容。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>我本地的分支通过 reset --hard 的方式做了代码回滚,想通过push的方式让远端的分支也一起回滚,执行push命令时却报错,该怎么办?<br />
|
||||
答:如果不加 -f 参数,执行 reset --hard 后,push会被拒绝,因为你当前分支的最新提交落后于其对应的远程分支。push 时加上 -f 参数代表强制覆盖。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>线上产品包已经回滚到昨天的版本了,我清清楚楚地记得昨天我把发布分支上的代码也reset --hard到对应的commit了,怎么那几个有问题的commit今天又带到发布分支上了?真是要命!<br />
|
||||
答:集成分支不能用 reset --hard 做回滚,应该采用集成分支上新增commit的方式达到回滚的目的。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>我刚刚在GitLab上接纳了一个合并请求(Merge Request),变更已经合入到master上了,但现在我发现这个合并出来的commit有较大的质量问题,我必须把master回滚到合并之前,我该怎么办?<br />
|
||||
答:可以在GitLab上找到那<!-- [[[read_end]]] -->个合并请求,点击revert按钮。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>刚刚线上A产品V6.2的包有问题,我已经把A的产品包回退到V6.1 版本了,请问发布分支上的代码也要回滚到V6.1对应的commit吗?<br />
|
||||
答:你可以在下文“哪些情况下需要回滚代码?”和“哪些情况下包的回滚无需回滚代码?”中找到答案。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>产品包的回滚可以在我们公司持续交付云平台上执行,平台能不能也提供代码一键回滚的功能?这样我们回滚代码能相对轻松一些。<br />
|
||||
答:针对已上线发布的版本,我认为持续交付平台提供一键回滚的方式还是有必要的。这么做可以规范集成分支上线后代码回滚的行为,也能减少人为失误。具体做法可以参考我在下面给你分享的“集成分支上线后回滚”的内容。</p>
|
||||
</li>
|
||||
|
||||
上面这六个问题,除了前两个问题外,剩下的四个问题都可能影响到整个团队,因此回滚代码时须站在团队的立场,采用合适的方式进行回滚。
|
||||
|
||||
接下来,我就一一为你解答这些问题。
|
||||
|
||||
## 哪些情况下需要回滚代码?
|
||||
|
||||
在代码集成前和集成后,都有可能需要回滚代码。
|
||||
|
||||
**第一种情况:开发人员独立使用的分支上,如果最近产生的commit都没有价值,应该废弃掉,此时就需要把代码回滚到以前的版本。** 如图1所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/5c/2c8109c6e46be8137cb4bf7fdaacce5c.png" alt="" />
|
||||
|
||||
**第二种情况:代码集成到团队的集成分支且尚未发布,但在后续测试中发现这部分代码有问题,且一时半会儿解决不掉,为了不把问题传递给下次的集成,此时就需要把有问题的代码从集成分支中回滚掉。** 如图2所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/33/d9947d8f6f7f746414a18b81be5f3733.png" alt="" />
|
||||
|
||||
**第三种情况:代码已经发布到线上,线上包回滚后发现是新上线的代码引起的问题,且需要一段时间修复,此时又有其他功能需要上线,那么主干分支必须把代码回滚到产品包V0529对应的commit。** 如图3所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/e5/1db632b6217ed01a9ebb7c640ae09ee5.png" alt="" />
|
||||
|
||||
## 哪些情况下包的回滚无需回滚代码?
|
||||
|
||||
<li>
|
||||
线上回滚后,查出并不是因为源代码有问题。
|
||||
</li>
|
||||
<li>
|
||||
下次线上发布,就是用来修复刚才线上运行的问题。
|
||||
</li>
|
||||
|
||||
## 代码回滚必须遵循的原则
|
||||
|
||||
集成分支上的代码回滚坚决不用 reset --hard 的方式,原因如下:
|
||||
|
||||
<li>
|
||||
集成分支上的 commit 都是项目阶段性的成果,即使最近的发布不需要某些 commit 的功能,但仍然需要保留这些 commit ,以备后续之需。
|
||||
</li>
|
||||
<li>
|
||||
开发人员会基于集成分支上的commit 拉取新分支,如果集成分支采用 reset 的方式清除了该 commit ,下次开发人员把新分支合并回集成分支时,又会把被清除的 commit 申请合入,很可能导致不需要的功能再次被引入到集成分支。
|
||||
</li>
|
||||
|
||||
## 三种典型回滚场景及回滚策略
|
||||
|
||||
在上面的内容中,我给你提到了个人分支回滚、集成分支上线前的回滚,以及集成分支上线后的回滚,这三种需要代码回滚的场景,它们具有一定的代表性。
|
||||
|
||||
现在,我就先以表1 的形式,针对不同场景为你归纳不同的处理策略。后面的章节中,我再为你具体介绍每种场景的处理步骤。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/26/72/26f06b78c368b7717d06ab03f3bb4872.png" alt="" />
|
||||
|
||||
**第一,个人分支回滚**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/5c/2c8109c6e46be8137cb4bf7fdaacce5c.png" alt="" />
|
||||
|
||||
针对图1的情况:
|
||||
|
||||
<li>
|
||||
feature-x分支回滚前HEAD指针指向 C6 。
|
||||
</li>
|
||||
<li>
|
||||
在个人工作机上,执行下面的命令:
|
||||
</li>
|
||||
|
||||
```
|
||||
$ git checkout feature-x
|
||||
$ git reset --hard C3的HASH值
|
||||
|
||||
```
|
||||
|
||||
如果feature-x已经push到远端代码平台了,则远端分支也需要回滚:
|
||||
|
||||
```
|
||||
$ git push -f origin feature-x
|
||||
|
||||
```
|
||||
|
||||
**第二,集成分支上线前回滚**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/33/d9947d8f6f7f746414a18b81be5f3733.png" alt="" />
|
||||
|
||||
针对图2中集成分支上线前的情况说明:
|
||||
|
||||
<li>
|
||||
假定走特性分支开发模式,上面的commit都是特性分支通过merge request合入 master 产生的commit。
|
||||
</li>
|
||||
<li>
|
||||
集成后,测试环境中发现C4和C6的功能有问题,不能上线,需马上回滚代码,以便 C5 的功能上线。
|
||||
</li>
|
||||
<li>
|
||||
团队成员可以在 GitLab 上找到C4和C6合入master的合并请求,然后点击 revert 。如图4所示。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/a1/7734d20cb2ddf59f2237584f7c382fa1.png" alt="" />
|
||||
|
||||
回滚后master分支变成如图5所示,C4’是revert C4产生的commit,C6’是revert C6产生的commit。通过revert操作,C4 和 C6 变更的内容在 master 分支上就被清除掉了,而 C5 变更的内容还保留在 master 分支上。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/f0/00a1d5aa42c53d4d355e297ca5f221f0.png" alt="" />
|
||||
|
||||
**第三,集成分支上线后回滚**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/e5/1db632b6217ed01a9ebb7c640ae09ee5.png" alt="" />
|
||||
|
||||
我先跟你说明一下图3中的具体情况:
|
||||
|
||||
<li>
|
||||
C3打包并上线,生成线上的版本V0529,运行正确。之后C6也打包并上线,生成线上版本V0530,运行一段时间后发现有问题。C4和C5并没有单独打包上线,所以没有对应的线上版本。
|
||||
</li>
|
||||
<li>
|
||||
项目组把产品包从V0530回滚到V0529,经过定位,V0530的代码有问题,但短时间不能修复,于是,项目组决定回滚代码。
|
||||
</li>
|
||||
<li>
|
||||
C4和C5没有单独上过线,因此从线上包的角度看,不能回滚到C4或C5,应该回滚到C3。
|
||||
</li>
|
||||
<li>
|
||||
考虑到线上包可以回滚到曾发布过的任意一个正确的版本。为了适应线上包的这个特点,线上包回滚触发的代码回滚我们决定不用 一个个revert C4、C5和C6的方式,而是直接创建一个新的commit,它的内容等于 C3 的内容。
|
||||
</li>
|
||||
<li>
|
||||
具体回滚步骤:
|
||||
</li>
|
||||
|
||||
```
|
||||
$ git fetch origin
|
||||
$ git checkout master
|
||||
$ git reset --hard V0529 # 把本地的master 分支的指针回退到 V0529,此时暂存区(index)里就指向 V0529里的内容了。
|
||||
$ git reset --soft origin/master # --soft使得本地的master 分支的指针重新回到 V05javascript:;30,而暂存区(index)变成 V0529的内容。
|
||||
$ git commit -m "rollback to V0529" # 把暂存区里的内容提交,这样一来新生成的commit的内容和 V0529 相同。
|
||||
$ git push origin master # 远端的master也被回滚。
|
||||
|
||||
```
|
||||
|
||||
回滚后如图6所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/46/1c05958045f89f8dae034fd2f5461f46.png" alt="" />
|
||||
|
||||
C3’的内容等于 C3,master分支已清除C4、C5和C6的变更。
|
||||
|
||||
现在master又回到了正确的状态,其他功能可以继续上线。
|
||||
|
||||
如果要修复C4、C5和C6的问题,可以在开发分支上先revert 掉C3’ ,这样被清除的几个commit的内容又恢复了。
|
||||
|
||||
## 总结
|
||||
|
||||
代码回滚在持续交付中与包回滚一样,也是不可缺少的一项活动。但它并不是简单地去执行Git的reset或revert命令就可以搞定的事情。
|
||||
|
||||
除了开发的个人分支上存在回滚的情况外,我们还会遇到集成分支上需要回滚的情况;对于集成分支的回滚,又可以分为上线前和上线后两种情况;因为紧急程度和上线情况的不同,我们必须采用不同的回滚策略。
|
||||
|
||||
我围绕着开发工程师在代码管理中,最常遇到的6个问题,分别为你介绍了代码回滚的概念,梳理了需要回滚及不需要回滚的情况,分析了回滚的类别及其不同的回滚策略,提炼了回滚原则,希望能对你的实际工作有所帮助,保持正确的回滚姿势。
|
||||
|
||||
## 思考题
|
||||
|
||||
那么,接下来就是留给你的思考题了。
|
||||
|
||||
<li>
|
||||
集成分支上线前,如果发现新提交的5个commit有3个需要回滚,请问,除了点击合并请求中的revert按钮这种方法外,还可以怎么做?
|
||||
</li>
|
||||
<li>
|
||||
采用特性分支开发的一个项目,每个特性分支合入到 master 时都会产生一个合并的commit,而且该项目是禁止直接向master做push操作的。可是该项目的master分支却存在多个非合并产生的commit,请问这些commit很可能是怎么产生的?
|
||||
</li>
|
||||
<li>
|
||||
持续交付平台如果要提供一键代码回滚的功能,每次回滚都要生成一个新的commit吗?即使以前已经产生过同内容的commit了,也要重建新的commit么?
|
||||
</li>
|
||||
|
||||
欢迎你给我留言。
|
||||
|
||||
|
||||
112
极客时间专栏/geek/持续交付36讲/配置管理/07 | “两个披萨”团队的代码管理实际案例.md
Normal file
112
极客时间专栏/geek/持续交付36讲/配置管理/07 | “两个披萨”团队的代码管理实际案例.md
Normal file
@@ -0,0 +1,112 @@
|
||||
<audio id="audio" title="07 | “两个披萨”团队的代码管理实际案例" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f2/b3/f22236b6b7458739171fda6d0fbcccb3.mp3"></audio>
|
||||
|
||||
在亚马逊内部有所谓的“两个披萨”团队,指的是团队的人数不能多到两个披萨饼还不够吃的地步。也就是说,**团队要小到让每个成员都能做出显著贡献,并且相互依赖,有共同目标,以及统一的成功标准,这样团队的工作效率才会高。**
|
||||
|
||||
现在有很多互联网公司喜欢采用“两个匹萨”团队的模式,你可能很好奇,这些团队通常是如何实施代码管理的?
|
||||
|
||||
当前国内互联网公司通常采用特性分支开发的模式,我在第四篇文章《一切的源头,代码分支策略的选择》中,为你详细介绍了这种模式,下面我就以这种模式为例,为你解开困惑。
|
||||
|
||||
以迭代周期为一周的项目为例,我将按照从周一到周五的时间顺序,通过整个团队在每天的工作内容,跟你分享项目任务分配,分支创建、集成与分支合并、上线,包括分支删除的关系。你可以从中了解互联网公司研发团队日常代码管理的真实情况,体会团队为了提高研发效率,在代码管理上做出的创新与改进。
|
||||
|
||||
## 背景
|
||||
|
||||
周一上午 11:30,“复仇者” 团队的周会结束,会议室里陆续走出了6名工程师:
|
||||
|
||||
- “钢铁侠”:5年一线开发经验,现任“复仇者”项目经理及产品负责人;
|
||||
- “美国队长”:6年开发经验,负责“复仇者”项目的技术架构,兼开发工作;
|
||||
- “绿巨人”:3年开发经验,全栈开发;
|
||||
- “雷神”:3年开发经验,全栈开发;
|
||||
- “蜘蛛侠”:1年开发经验,负责几个成熟模块的维护;
|
||||
- “黑寡妇”:资深测试工程师,负责系统集成与测试。
|
||||
|
||||
其他同事泡咖啡喝茶的时候,“钢铁侠”在公司的GitLab 上已经把 issue 分配给了团队成员,预示着忙碌又充实的一周要开始了。
|
||||
|
||||
## 周一下午
|
||||
|
||||
“美国队长”“绿巨人”“雷神”“蜘蛛侠”这4名开发人员早已熟悉团队的工作流程,午休之后,他们纷纷打开 GitLab 界面,在待办事项上找到自己的 issue,查看无误后,直接根据 issue 建好了新的特性分支。
|
||||
|
||||
每个新分支代表了一个具体的任务,待四人建好新分支后,“钢铁侠”不由得微微一笑,心想:哈哈,任务都被大伙儿认领了,看样子,他们下午就要开工啦。这4名开发人员新建的4个分支,如图1所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/66/4d/6637c70899f06eba1d85e384c9e3404d.png" alt="" />
|
||||
|
||||
这时,资深测试工程师“黑寡妇”也没闲着,开始查看起本周计划完成的issue,整理出功能点、性能要求和粗粒度的接口列表,基本明确了测试范围。随后,她在公司GitLab平台上为本周迭代设置好了“Smart Merge”,如图2所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/28/913cdf0e0217a8f139e45e3efa8d1528.png" alt="" />
|
||||
|
||||
要是有新人加入团队,“黑寡妇”肯定会向他推荐这个“Smart Merge”([Smart Merge](https://github.com/gitlab-extra/smart_merge)是我和几个好友一同开发的GitLab插件,其作用是高效地解决分支合并的一系列问题)。高效的工作习惯使得“黑寡妇”早已整理好了说明文档,以便随时分享给其他同事。
|
||||
|
||||
有了Smart Merge,任何一个分支的变更会自动触发合并,一旦出现冲突,开发人员就会立刻收到邮件通知。
|
||||
|
||||
周一下班前,4位开发人员分别把各自的本地分支push到了 GitLab 平台。集成后没有冲突,大家开开心心回家了。
|
||||
|
||||
## 周二
|
||||
|
||||
“美国队长”起了个大早,9点半就到公司了,昨天他已经实现了核心功能,今天要完善这些功能并升级 API。他忙了个把小时,本地开发自测完成,并把本地 feature/captain 分支push 到了 GitLab 服务器。
|
||||
|
||||
一分钟不到,“美国队长”的邮箱收到了 GitLab 发来的通知,告诉他刚提交的某两个文件和 feature/hulk 分支发生了冲突。
|
||||
|
||||
“美国队长”知道肯定是黑寡妇创建的 Smart Merge 帮助自己快速发现了冲突,他直接用 GitLab 的 compare 功能对比了 feature/captain 和 feature/hulk 这两个分支,找到了冲突所在的行。
|
||||
|
||||
通过分析,“美国队长”判断出feature/hulk的变更是合适的,这个冲突应该由他解决掉。
|
||||
|
||||
“美国队长”选择在本地对自己的分支执行 git rebase -i ,把引入冲突的commit进行了变更,自测通过后,再次把 feature/captain 分支push到了 GitLab 。为了确保冲突的问题已经被解决,他打开了Smart Merge,发现状态是“已合并”(Merged) ,这才端起杯子泡咖啡去了。
|
||||
|
||||
上午10:00前后,“绿巨人”等人也陆陆续续到公司了。团队已经约好了协作节奏:每周四下班前完成一个迭代的上线。
|
||||
|
||||
通常周二下午开发人员要把每个 issue 的基本功能开发好,“黑寡妇”周二下午会给 Smart Merge 配置好持续交付的环境,一旦某个分支 push 后,自动完成分支合并,然后自动编译、打包,并部署到测试环境。
|
||||
|
||||
在测试环境上,除了跑自动化测试外,“黑寡妇”也会手工做一些集成测试和性能测试。
|
||||
|
||||
周二下午,“美国队长”开始 review 大家的代码,他把本周开发的4个分支,在GitLab上分别创建了4个merge request,目标分支都是 master 。
|
||||
|
||||
“美国队长”觉得 GitLab 的 review 功能很完善,交互也很便捷。这时,其他3名开发人员,忙着写代码和自测。“黑寡妇”除了搭建测试环境外,还补充了自动化测试的用例。
|
||||
|
||||
## 周三
|
||||
|
||||
经过周一和周二的努力,本周的基本功能均已实现,“黑寡妇”开始对系统实施集成测试,并做一些压力测试。
|
||||
|
||||
上午测试时,“黑寡妇”发现在某些场景下系统存在较大的延迟,这个问题在上周的版本中并不存在。她判断是本周新引入的功能导致了这个问题,但一下子又很难确定是怎么引起的。
|
||||
|
||||
于是,“黑寡妇”决定修改 Smart Merge 的配置,把嫌疑最大的分支剔除掉后再打包测试。通过这样的方式,最后查出是 feature/thor 这个分支引入的问题,她把测试情况详尽地告诉了“雷神”。
|
||||
|
||||
大半个下午雷神都在查问题,到下午四点钟时,问题终于被“雷神”修复了,他把 feature/thor 分支做了 push,然后向“黑寡妇”求助,请她合入自己的分支后再帮忙做测试。
|
||||
|
||||
“黑寡妇”把“雷神”的分支重新加回到 Smart Merge 中,并把编译包重新部署到了测试环境。经过测试验证:延时大的问题真的不见了。
|
||||
|
||||
下班前,“黑寡妇”召集项目组开了个简短的质量会议,大家商量后认为本周计划内的四个开发任务集成后没有大的质量问题,周四可以一起上线。
|
||||
|
||||
会后,“黑寡妇”看了看本周的四个合并请求,“美国队长”对四个请求意见都是赞成合入 master,Sonar 检查也都合格,加上自己测下来质量也过关,于是,她果断地接受了四个合并申请。
|
||||
|
||||
在回家前,master 对应的最新 commit 已经顺利地编译、打包后被发到用户验收测试环境,“黑寡妇”对这个环境启动了自动化测试服务。
|
||||
|
||||
至此,测试加修复Bug,忙碌了一整天,大家终于可以回家休息了。
|
||||
|
||||
## 周四
|
||||
|
||||
“黑寡妇”一早上班时,首先查看了自动化测试的结果,显示 master 分支构建出的包符合质量要求。于是,她又对没有设计自动化测试用例的部分,进行了手工测试,发现几个界面上存在文字描述的问题,随后通知开发做修复。
|
||||
|
||||
开发在本地分支上修复问题后 push 到 GitLab,再次发起合并请求,“黑寡妇”逐个接受了这几个 Fix 的请求。
|
||||
|
||||
到中午时分,用于上线的产品包终于生成了。
|
||||
|
||||
等到发布窗口开启时,“黑寡妇”通过公司的发布系统把合格的产品包发布到了线上。观察一段时间,线上运行都正常。
|
||||
|
||||
对应本次上线,“黑寡妇”及时给 master 打了tag,然后把本周成功发布的消息通知到项目组,并向“钢铁侠”做了汇报。
|
||||
|
||||
“钢铁侠”看大伙儿忙碌了这么多天,豪爽地请大家喝果汁,并告诉大家他又有几个紧急的用户需求,嘱咐大伙下周继续努力。
|
||||
|
||||
## 周五
|
||||
|
||||
通常在这一天,项目组会一起清理过期的分支,删除本周已合并到 master 的分支。而对于下周开发的新分支,项目组约定统一从 master 上拉取。另外,利用这一天,项目组也会召开回顾和改进会议,以讨论解决目前的一些已有问题的方案,这些讨论即包含工作流程问题,也包含代码和系统等问题。
|
||||
|
||||
## 总结
|
||||
|
||||
我介绍了由6人组成的“两个披萨”团队代码管理的实践,通过周一到周五的具体活动,你可以看到采用特性分支开发的团队是如何创建分支、集成分支和删除分支的,希望能对你的日常工作也有所帮助。
|
||||
|
||||
## 思考题
|
||||
|
||||
假设有A、B、C三个功能依次被合并到master并准备上线,此时发现A功能有问题,不能上线,而B和C则必须上线,此时你会采取什么办法来解决?
|
||||
|
||||
欢迎你给我留言。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user