阅读 1137

用开源软件打造企业级 DevOps 工作流(二):版本控制

前言

本文章为系列文章的第二篇,之前已经写了一篇 《用开源软件打造企业级 DevOps 工作流(一):概述》,主要介绍了DevOps的基本概念以及一些组成要素。在这篇文章中,我们将介绍 版本控制系统 / VCS(Version Control System),除了介绍版本控制的基础概念以外,我们还将介绍如何使用开源的 GitLab 来实现版本控制系统。

版本控制系统

版本控制系统主要是针对软件开发过程中对代码变更的管理,保证了代码的可回溯、可审查、可管理等功能需求,而最终的目的,是可维护性。在上一篇文章中,我们举例阐述了没有版本管理工具会出现什么问题,因此不会在本文赘述。其实主要问题就是没有版本管理工具导致的可维护性的下降,从而导致各种不可预测的bug的出现。

版本的主要内容包括三部分:

  1. 检入检出控制(Check-In / Check-Out Control)

  2. 分支与合并(Branch / Merge)

  3. 历史记录(History)

检入检出控制

关于检入检出,我们可以理解为代码与 版本控制系统数据库(VCS Database) 的同步与交互过程(如下图)。

检出(Check-out) 相当于用户将 版本控制系统数据库(VCS Database) 的代码同步了一份到本地,如果与本地代码发生了冲突,会比对本地代码并做相应处理(后面我们会讨论合并)。

检入(Check-in) 相当于是检出的逆操作,也就是将本地代码同步到远端版本管理系统,其实是对代码数据库的一种更新,相当于一次升级,因此,VCS 需要对将要更新的代码进行版本审查,并会拒绝将不合法的代码升级(例如非最新升级)更新到数据库中。

检入检出的设置保证了代码数据库的原子操作,不会因为两个人同时提交代码而造成冲突。更改代码通常在本地进行,每一次更改完毕后会将最新的代码提交到代码数据库,同时更新代码仓库,保证远端代码是最新的。

分支与合并

代码的 分支(Branch) 概念是指同一“祖先“的代码在不同的方向上各自进行更迭,互不干扰。不同分支的代码经过不断的迭代,虽然来自同一个“祖先”节点,但很可能演变成完全不同的功能和结构。

一个分支相当于是对某个节点的拷贝,同时也可以自己发展成新的版本。版本控制系统中有分支的概念是为了方便开发多个 功能(Feature) 时防止互相干扰,是一种解决代码冲突的方式。

例如,某开发团队需要开发 功能 A功能 B ,而这两个功能都需要在 文件 M 上进行修改。如果同时开发 功能 A功能 B,则会很难协调,因为开发人员需要同时操作文件 M。这就跟要求两个猴子吃同一根香蕉一样困难,那么我们为什么不分成两根香蕉、一只猴吃一根呢?

因此,我们创建了两个分支 AB,两个开发者分别开发,互不影响,互不干扰,这样操作不会造成代码冲突,开发起来非常和谐。

虽然我们解决了同时修改一个文件的问题,我们还需要将两个开发好的功能整合起来,这就产生了 合并(Merge) 的概念。

如上面那张图,当 A 功能B 功能 都开发好了之后,分别为版本 A2 和版本 B2,需要将两者整合在一起,产生新的版本 M2,这个整合的过程就是合并。

当然,合并的过程会产生 冲突(Conflict),也就是两个分支同时修改了同一份代码。一般的版本控制系统(例如 GitSVN)会尝试自动将代码进行合并,例如非常明显的增删操作。然而,也有不能自动合并的情况,通常要求开发人员手动合并代码,这叫 解决冲突(Resolve Conflict)

开发过程中,我们的分支管理会有很多种策略,一般开发团队会根据项目的需要和团队的情况选择合适的分支管理策略,这个后面会讲。

历史记录

这个其实很好理解,代码的所有变动,变动了什么(What)、何处变更的(Where)、何时变的(When)、由谁提交的变更(Who)、为什么变更(Why,提交注释),这些都会体现在 历史记录(History) 中。

如果代码出了问题、或者需要参考历史功能等,开发者可以根据历史记录来回溯代码,找到 Bug 发生的原因,以及理解历史代码的设计等等。这有助于帮助后续的开(jie)发(pan)者(xia)更加轻松地掌握需要管理的代码。

分支管理策略

我们在前面着重介绍了一下分支与合并,这里我们会介绍一下其衍生出来的分支管理策略,这对日常开发来说非常重要,因为不同的分支策略对项目开发来说有着深远的影响。

分支管理策略可以看作是一种开发模式:团队成员之间如何通过分支与合并操作来协同工作,将不同的功能整合在一起,开发测试环境如何隔离,生产环境上线后如何做相应更改等等。

下面我们将介绍几种常见的分支管理策略。

主干开发(Trunk Based Development)

主干开发(Trunk Based Development) 简称为 TBD,是一种常见的分支管理策略,也是 Google 和 Facebook 等大型互联网巨头经常采用的策略之一。

主干开发要求所有代码都提交到 主干(Master) 分支上,从而避免了开发者们看到的代码是过时代码的情况。只有当需要 发布(Release) 的时候,主干才创建一个当前节点的分支,作为发布用。

对于主干开发来说,开发者要求每天开发前都同步最新代码,如果有与新提交代码的冲突,需要在本地自己解决后重新提交到主干上。这样的好处在于,由于开发者的本地代码基本是跟主干同步的,因此合并时并不会有重大的变更,合并代码的时候相对比较容易,不会花很多时间。

而这样做的缺点也很明显,如果很多人在项目上进行开发,会导致源源不断的更新代码提交到主干,这会导致发布的时候存在众多提交而导致出现了 bug 难以追溯,进而难以修复,容易出现“一颗耗子屎坏了一锅粥”的情况。

我们接下来要介绍的 Git Flow 分支管理策略就是来解决这个缺点的。

Git Flow

Git Flow 是一种 特性分支(Feature Branch) 策略。特性分支策略的概念跟主干开发策略的概念是相对的,意思是不同的功能拉出一个分支单独开发,开发好后再合并到主干,保证功能之间互不影响和干扰。

Git Flow 是特性分支策略的一种,是 Vincent Driessen 在 《A Successful Git Branching Model》(nvie.com/posts/a-suc…) 中提出的一种分支模型(如下图)。

简单来说,Git Flow 要求有 Master(主干)、Develop(开发)、Release(发布)和 Hotfix(热修复)几个基础分支。

每次需要开发新功能时,在 Develop 分支下拉出一个 Feature 分支(特性分支)来进行单独开发,开发好后合并到 Develop 分支。当 Develop 分支开发到一定程度的时候,再将其合并到 Release 分支。

Release 分支是在生产环境的 Master 分支前的起 UAT 测试缓冲作用的预备分支。当 Release 分支准备好之后,就将其合并入 Master 主干分支,这样就相当于在生产环境上发布了新版本了。

当线上版本出了 Bug 需要修复的时候,我们会在直接在 Master 分支上拉出一个 Hotfix 分支,在这个分支上直接做修复,然后合并回主干。同时 Hotfix 上的几次修复还会合并到 Develop 上的某个节点,以保存这次修复,这样 DevelopMaster 就基本保持了一致。

这是经典意义的 Git Flow,但在实际操作的时候我们并不一定会原封不动地完全照搬这个模式。例如,很多时候我们其实用不到 Release 分支,只存在 MasterDevelop,这对于中小型项目来说比较灵活。有时候 Release 也被称为 Test 测试分支。另外还有些变种,例如主干开发与 Git Flow 的部分结合,在 Develop 分支上做主干开发,每次需要部署到生产环境的时候就将其合并入 ReleaseMaster 分支,以发布正式版本。

Git or SVN?

GitSVN(Subversion) 是两个比较受欢迎的版本控制工具。两者之间最大的区别是:SVN 是集中式的,而 Git 是分布式的(如下图)。

SVN 要求代码仓库只有一个中心仓库,所有开发者在做提交代码的操作前,必须保证自己本地代码与中心仓库的代码完全同步;而 Git 相对来说就灵活得多,Git 不要求所有开发者的本地代码完全一致,在提交时只要求被 push 的仓库分支与本地的一致就可以。

另外,SVN 的分支合并复杂且不健壮,因为 SVN 无法区分合并是人工操作还是自动合并的,因此将不会创建一个合并记录节点;而 Git 则相反,会在合并后创建一个合并记录节点,这增加了可回溯性。而且,用 Git 创建分支的成本很低,只需要 git branch <branch_name> 就可以了。

由于分支合并的复杂性,SVN 通常不适合用作特性分支策略,而适合主干开发模式(Google App Engine 就是 SVN 管理的)。

而 Git 则既适合主干开发模式,又天生支持特性分支策略。所以,我们一般都采用更灵活的 Git 作为版本控制工具,Git 也是现在更为主流的选择。

开源工具 GitLab

GitLab 简介

GitLab 是一个开源的版本控制系统,使用 Git 作为代码管理工具,并在此基础上搭建了 Web 服务,它有着精美的 Web UI 界面,方便用户操作使用。

GitLab 是用 Ruby 编写的开源项目,有非常自由的 MIT 版权,允许二次开发并投入商业使用。

GitLab 支持 Git 代码仓库、权限管理、合并请求、Issues、Wiki、CI/CD 等非常多的强大功能。

GitLab 非常类似 GitHub,支持代码仓库、代码合并、代码审核等基础功能,区别在于:GitHub 是一款云端产品,而且项目大多为开源项目(私有仓库有限制);而 GitLab 是开源产品,可以非常轻松的部署在任意一台服务器上。

我们用 GitLab 作为我们 DevOps 工作流的原因主要在其强大的可视化界面和权限管理,相当于加强版的 Git 仓库。因为人是视觉动物,可视化之后可以更加高效的处理各种复杂信息,GitLab 可以帮助我们做到可视化操作。

而对于企业开发来说,通常会有管理多个项目的需求,不同的项目也会有不同的开发者参与进来,因此有效地管理这些权限会是个需要注意的问题,而 GitLab 本身就支持相关的权限管理。

安装 GitLab

安装 GitLab 非常简单,我们推荐的方式是用容器化工具 Docker 来进行安装。如果您对 Docker 不熟悉,可以去网上查找一下相关资料(我相信会非常多),或者关注本系列即将介绍的 DevOps 容器篇,在这篇文章中,我们将着重介绍 Docker。

在安装之前,确保您已经在本机或者服务器上安装了 Docker,能够执行基本的 Docker 操作,例如docker ps

sudo docker run --detach \ # --detach 表示是后台运行
  --hostname gitlab.example.com \ # GitLab 中引用的 hostname,需要设置为服务器域名
  --publish 443:443 --publish 80:80 --publish 22:22 \ # 映射端口
  --name gitlab \ # 容器名称
  --restart always \ # 容器挂了之后会自动重启
  --volume /srv/gitlab/config:/etc/gitlab \ # 配置持久化
  --volume /srv/gitlab/logs:/var/log/gitlab \ # 日志持久化
  --volume /srv/gitlab/data:/var/opt/gitlab \ # 数据持久化
  gitlab/gitlab-ce:latest # 镜像名称
复制代码

这是官方文档中的 Docker 启动命令,只需要在命令行中输入以上命令,就可以启动 GitLab 了。

稍微等待几十秒,在浏览器中输入 http://localhost 就可以看到 GitLab 的登陆页面了。

使用 GitLab

这里我们不打算详细介绍 GitLab 的所有功能,我们只会简单介绍 DevOps 工作流中的重要部分,主要是 Git 的版本控制部分:克隆仓库、创建分支、合并分支。剩下的功能交给读者阅读官方文档或者自己安装体验。

克隆仓库

在代码项目中,复制下面 SSH 或者 HTTP 的地址,例如 ssh://localhost/user/project1 ,在本地命令行中输入如下内容。

git clone ssh://localhost/user/project1
复制代码

然后会在当前目录创建一个该项目的目录,并将文件从远端拷贝下来。这里会创建一个originremote记录。可以通过如下方式看到。

git remote -v
复制代码

如果我们通过 merge request 的方式来进行版本控制管理,我们将还会用到 git remote 的操作。

创建分支

我们可以在界面中创建分支,但我们也可以在本地创建。

在本地项目中输入如下内容创建 develop 分支。

git branch develop
git checkout develop

# or

git checkout -b develop
复制代码

然后更改一些代码,并通过git commit提交代码。接下来,将代码推送到远端服务器。

git push origin develop
复制代码

这样,远端就创建了一个 develop 分支,并且将本地 develop 分支上的commits更新到服务器上了。

合并分支

当我们想通过合并的方式将 develop 的更新同步到主干 master 分支时,我们需要合并操作。

在本地,可以这样操作。

git checkout master
git merge develop
git push origin master
复制代码

这样就完成了主干的合并。

然而,有些时候特别是比较大型的项目时,我们并不希望在 master 分支上进行修改,我们想强制要求通过合并代码的方式来更新主干分支。因此,我们需要加一个保护操作。我们可以在项目中的 Settings -> Repository -> Protected Branch 中设置保护主干分支,也就是说,不允许 pushmaster (读者请自行了解如何设置,这里不赘述)。

如果做了这样的保护限制,我们需要通过 merge request 的方式来合并分支。在 GitLab 中的项目首页,点击左侧菜单的 Merge Requests,点击 New merge request,选择 Source branchdevelopTarget branchmaster,点击 Compare branches and continue,进入提交合并请求页面。在这个页面中,你可以看到一些合并信息,包括这次合并有哪些 commits,哪些代码有更改、更改了什么,等等。点击 Submit merge request,一个 merge request 就创建好了。然后,如果你是有权限的角色的话,你可以在这个合并请求页面中点击 Merge 同意这次请求,master 分支也就合并好了。

GitLab 在合并分支时加入的代码审阅功能对于 Code Review 来说非常方便。通常来说有权限同意合并请求的开发者都是 Reviewer,需要在合并之前审阅提交的代码,并根据审阅的结果同意或拒绝合并请求。

总结

本篇文章主要介绍了版本控制系统的主要概念,包括检入检出、分支合并、历史记录,以及两种分支开发策略(主干开发和 Git Flow),还比较了 Git 和 SVN。另外,本文还着重介绍了开源版本控制系统 GitLab,介绍了其概念、安装和基本使用。这些知识,对于我们后面讲 DevOps 工作流是非常重要的基础。下一篇,我们将介绍持续集成(CI),这与版本控制系统息息相关,因为诸如 Jenkins 或者 GitLab CI/CD 可以用 GitLab 代码仓库作为源码来源来自动构建产品。没有版本控制系统,CI 的优势会受到局限。因此,版本控制系统是 DevOps 工作流中非常重要的模块。

在后面的文章中,我们将继续介绍 DevOp 的其他内容,包括持续集成、容器化、编排、网络、以及如何将这一切串联起来协同组成企业级的 DevOps 工作流。敬请期待后面的内容。