当Dev遇上CI与CD

简述

从刚开始的时候两眼一抹黑,到磕磕碰碰的终于让测试包可以在代码提交的时候顺利的自动构建,团队终于向CI和CD的开发生活迈出了历史性的一步,中间踩了不少坑,也有不少收获,写一篇东西纪念一下。

感觉这篇东西都是键者不成熟的思绪,满满的水货~

1. 原生之初

公司的研发团队本身是小团队,再经典的划分为前端后端,而当前研发中的应用是处于功能实现阶段,也就是几乎每天都会发布新功能,也几乎每天都有可能有已有的功能发生变动。

于是问题也开始接踵而至……

1.1 完整性

键者简单通过一段对话来还原一下这个场景:

  • 飞哥,你是不是提交漏了代码?(从QQ上发来一张编译报错的截图)
  • 啊,不好意思,等下我现在补。
  • 好了,你去pull一下
  • 还是不行啊(第二张截图)……
  • o_O

相信这个场景在没有完整测试环境的小团队中不会太陌生。

某个正在开发A模块的攻城狮,忽然收到要快速修复B模块的某个bug的需求。

在本地修完以后把涉及到fix的代码提交了,本地继续保留正在dev的代码,继续开发,可是有时候一个还处于开发中的不成熟系统的某个最大特点就是:某些bug其实是牵连很广,开发者用IDE一键修改了很多牵连到的模块,但是提交的时候却会漏了提交,导致代码仓库中的最新版本可能其实并不可编译(就更别提功能是否完成或者可靠了),也就是提交的代码是不完整的。

一种最传统的做法应该是遵循git flow的流程在当前主干上增加一个分支专门修复bug,修复完成后将分支合并到主干,并提交到代码仓库。

传统有时候好就好在可靠,但对于小团队来说,过多的分支处理起来并不轻松,更多时候,我们其实可以接受某个提交的节点其实是不完整的,很多时候缺少什么代码没提交也很容易发现的,只要在一个“干净”的环境编译一次就知道了,然后把欠缺的部分重新提交一下就好。

但提交者本身的环境是包含完整的代码的(至少我们的攻城狮提交代码前肯定都会先在本地测过可以完整使用),只是提交的时候发生了遗漏,而这个遗漏往往是同事更新了新的代码后才发现的。

当然,我们可以要求每个人在本地保留一块干净的开发环境,每次push之前先完整的模拟一次从无到有的编译过程,以确保自己提交了完整的代码。

但有时候这并不简单,还记得那个说做菜实在太难了,因为每次都需要先准备一个干净碟子的冷笑话么……

1.2 可靠性

一个项目的代码可以编译成功了,就算可靠了么?

显然,我们希望是,但可惜,并不能。

一个编译成功的程序,在运行的时候,有太多的可能导致它会崩溃了,哪怕它只是在启动的时候读取一下配置,也有可能因为读到了有问题的配置,导致无法正常工作。

也有可能启动的时候没什么问题,在工作的时候,处理某个输入的时候,产生了异常,导致崩溃😫。

这个异常可能是原来就有的bug,也有可能是为了修复某个bug而牵一发动全身,导致其他地方本来没问题的地方产生的bug

理论上,bug总是存在的,一个存在bug的程序并不一定不能稳定的运行。

就像没有一个人是完美的,但不完美并不是不能很好的生活下去。

例如键者这种上了年纪的老人家,虽然断更了很久但还是偶然会诈尸一下写篇东西吹吹水。

但对于一个程序的来说,至少预期应有的功能都应该提供到位,否则就还不是一个能发布的版本。这个时候我们怎么知道这个程序已经达到了正常工作的地步?找个人专门把所有功能都跑一遍么?骚年,我很看好你哟,事实上,软件测试工程师就是负责这项工作的。

但其实有些功能的bug我们可以直接用代码测试出来,有问题早知道,不是更方便么。

没错,这就是写测试代码的价值,一个项目稍微具有规模后,如果没有足够的测试,哪怕是开发者本人,也很容易陷入下边这种情况:

  • 这段代码你看着“感动”吗?
  • 不敢动不敢动Orz……

所谓泥足深陷,积重难返,莫过于此……

能通过测试的代码未必就是可靠的,但至少,连测试都通不过,那至少这个版本是有问题的。
要么是项目有问题,要么是测试逻辑或者数据没及时更新。

1.3 可用性

在软件可靠的情况下,下一个要考虑的是可用性,先不说真正面向的大众客户的可用性,首先要保证的其实是开发的可用性。

某个功能的开发,往往是前端和后端协同工作的成果,在功能还未稳定的时候,前端直接连接后端的开发机做联调测试是很常见的情况,但是随着功能增加,需求的分化,前端会更多的关注展示(Display)方面的问题,而后端开始关注高并发,高可用,弹性扩展的问题,如果每次联调都需要占用一个后端工程师,有时候未必是有必要的。

如果持续让一个后端工程师运行着一个最新版的Server呢?不是不可以,但毕竟不太方便,而且还是容易产生前文的完整性和可靠性问题,毕竟后端的开发机上跑着的未必是完整的版本,可能有他正在调试或者开发的其他功能,而前端如果依赖于这个版本进行开发,有些场景下可能会产生不一致的问题。

例如后端攻城狮把原来的某段未提交的代码在提交前删了之类……

因此,一个常见的解决方案是把本机当作开发环境,专门跑一台(组)测试环境,把当前最新可用版本(或者最近几个可靠版本)部署到测试服务器,然后前端的日常开发中,特别是不需要与后端联调的开发中,只要连接到这个测试服务器就可以了。

当然该联调的时候还是可以联调的。

2. CI/CD的自动化

其实键者猜想,上边提的问题,对很多小伙伴来说可能并不算真的问题,因为日常团队协作及开发中,各个团伙一般早就行程了自己的开发流程,从设计开发测试部署都会有自己的一套方案。

2.1 不是AI,而是CI

持续集成(Continuous Integration,简称CI),本身并没有跳出这个流程,它只是强调将这个流程的周期变短。

也许短到每次提交代码都执行一次。

但如果项目达到一定规模,这个规模可能是功能、复杂度等达到一定规模,也可能是参与开发的人员达到一定规模,CI的实现就越来越难以用人工维护,因为人力成本是很高的,而且持续而单调(是的,大部分时候这个工作非常单调),单调意味着人工容易出错。

其实,CI本身并没有“自动化”这种含义,事实上,它并不关心执行的是谁,是一个个集成工程师、开发工程师或者运维工程师,抑或是某一个程序,或者某段脚本。

2.2 不是AD,而是CD

而与CI经常一起出现的,是CD。而这里的CD其实有两个解,一个是持续交付(Continuous Dselivery),另一个是持续部署(Continuous Ceployment),后者是前者的下一步。

而键者在这里讨论的是后者,项目实施中一般说的CD也是后者,因为持续部署提倡的是从代码的提交测试构建部署都是自动完成的。

也就是说,每个成功的成功提交的节点应该都是可部署的,哪怕只是部署到测试机。

更详细的概念笔者放在了附录

3 实现思路

自动化这个东西,说起来可能都很好理解,简单讲,就是从提交代码开始,后续所有的流程全部由计算机自动完成,并及时把结果(特别是异常结果)反馈给开发者,以便开发者作出响应。

但是实现的时候其实还是挺多问题的,因为自动化意味着以前可以人工处理的异常都要自动化处理,我们在设计CI/CD方案的时候需要预先考虑到一般场景下可能会出现的问题,并作出合理的响应,是自动修正并继续部署,抑或是打断当前流程并通知开发者跟进处理,都是要预先考量的。

当然,就像很多开发时一样,我们可以先考虑最正常的情况,然后根据各种遇到的异常一点点处理。

3.1 环境(environment)

CD的最终目的应该是最终程序总是可以在预期的环境中按预期的逻辑运行,所以环境虽然并不是CD的流程中的一个环节,但键者在这里还是把这个问题拿出来单独讨论。

就像前文说的,第一步是准备一个干净的碟子。

我们提交的是代码,如果把代码看作原材料,我们要把菜端上桌,就势必要进行一定的加工,而厨房就是我们加工的场所,恰到好处的加工场所,才能确保加工的结果是可重现的。

可能很多时候脚本语言并不需要加工,但也需要一个可重现的运行环境。

而准备环境的重要性在于,一个程序测试时有测试环境构建时有构建环境运行时有运行环境。对于一个可靠的提交代码版本,它应该是能在给定的环境下通过测试,构建成功,并稳定运行的。

怎么样是一个给定的环境?键者认为,至少是当你面对一台新的主机(例如公司新租的一台ECS),你能复现的环境。一个应用的测试、构建、运行等环境可以是一样的,也可以是不一样的。

例如说,构建环境中可能涉及到编译器,涉及到当前项目依赖的第三方包,甚至涉及构建时执行的命令。而运行环境中,你并不需要有编译器,甚至不需要有源代码,你可能只需要一个可执行程序即可。

但在CD中,你最好尽量保证你每次开始一轮新的任务时,初始的环境都是一样的,这样可以最大程度的保证你的代码被别的同事下载,或者上线运行的时候,不会出现不完整的问题。

所谓干净的碟子,不一定是一个全新安装的操作系统,一般更合理的是具备了项目团队公开指定的所有工具及三方库的环境,而且每次运行过后,都应该把这个环境还原到上次运作的状态。

例如,某次测试的时候自动生成了一个文件,生成成功并校验通过表示测试通过。则测试完毕后,应立即删除这个文件,避免下次测试的时候,误以为上次下载的文件是这次下载的,导致放过了可能存在的bug。

为什么说应该是公开指定呢?有时候我们可以在开发的时候引入了新的第三方包,但没有及时同步,可能会导致其他攻城狮编译出错得莫名其妙,此时CD环节就会报错,你就可以及时发现自己没有提交关于新包的配置。

如果严谨的考虑,应至少准备三套环境,一个用于测试,一个用于构建,一个用于运行,在团队中,一次合格的提交不能只满足于能在本地的开发机上完整、可靠、可用的通过。

linux下的应用开发的话,Docker显然是在处理环境问题目前可能是最好用的工具,不过今天只讲理论,就不多说了~

3.2 提交(commit)

就像再厉害的程序,也要被执行(Exec)才能变成进程一样,CD的启动也需要一个触发的契机,除了手动触发的情况之外,我们一般会在每次提交(commit)代码的时候自动触发。

当然,也可以细分为本地提交,以及push到公共仓库的时候。更常见的情况是push到公共仓库的时候。

具体的操作就是通过各种手段(例如git hook,或者写个轮询定时查看等等),监控代码仓库的变动情况,一旦发生变动,就执行一个指定的程序(可能就是个脚本或者触发某个已经在运行的服务),这个程序要接手后续的工作,完成自动部署的后续工作。

所以,提交这个场景下,童鞋们需要关注的是触发时机以及执行者的问题。

关于CD的执行者,有很多现成的工具,例如JenkinsTravisDroneGitlab-Runner等,当然这里也可以是多个程序协同完成工作,不一定是一个程序。所以童鞋们这里一定要抓住重点,是做什么,而不是几个人(程序)做,只要能达成目的,几个人做都可以。

3.3 测试(test)(第一轮)

测试的问题又是要具体情况具体分析的了,很多开发语言都具备一定程度上不用构建就能测试的测试框架,所以第一轮测试瞄准的是具备这一特性的开发场景。

首先当然是要初始化测试环境,然后下载最新的代码,根据项目情况执行测试脚本(或者运行特定的测试工具),根据测试结果决定是应该报警,还是继续。

往往是单元测试……

3.4 构建(build)

对于需要编译的语言来说,可能很多时候构建可能会被看作一个编译的过程,编译成功了,构建通过。

脚本就更别说了……

然而其实我们知道,对于很多应用来说,可执行程序并不是一个完整的应用,一个完整的应用可能还需要包括符合特定目录结构的配置文件、资源文件等等,所以所谓构建在这里是指能把整个程序完整运作的所有应有的资源按合理的方式组织好的过程,其中只是凑巧有些资源是需要通过代码编译出来的而已。

当然,如果你的应用确实只有一个可执行程序,那编译完就算完事了。

3.5 测试(test)(第二轮)

第二轮测试和第一轮测试的核心区别在于,第一轮测试是在构建之前进行的,针对的是未构建之前的代码或者资源,而第二轮测试是在构建之后的,针对的就是一整个应用,或者说,第二轮测试更偏向一轮黑盒测试,测试的是应用能否按预期工作。

当然,无论是第一轮测试还是第二轮测试,都不是必要的,需要根据自己的项目情况具体决定

3.5 部署(deploy)

部署,简单的说就是把构建环节中准备好的应用包,放到指定的运行环境(例如一台服务器的某个目录),并运行的过程。

但部署其实也有很多关注点:

  1. 部署到哪一台服务器。
  2. 部署的时候怎么处理旧版的正在运行的应用程序(关了,还是把新应用当作弹性扩展,还是通过某些策略把旧应用的数据或者资源移到新应用)
  3. 部署的时候,当前应用依赖的其它服务需不需要做什么处理,例如重建数据库?
  4. 部署时使用的配置应该怎么根据部署的情况自动调整。
  5. 如果部署失败了,怎么办?

这些问题其实都要一一考虑清楚,当然,如果是简单处理,也并不是所有问题都必须解决才能工作。

小结:老生常谈的银色子弹

就像很多童鞋知道和经历过的一样,万能的子弹是不存在的,每个问题都要放在具体的场景中,才能产生价值。

键者踩过的坑,主要还是受限于键者的经历以及需求,每次理论到实践的过程都会出现各种难以预料的问题,但键者一直觉得学习一个新的技术,理解理念和实际操作同样重要,一般根据概念理解操作的逻辑,知其然然后知其所以然,另一方面也要边实操边反馈理论的理解。

键者自己经常会遇到这样的情况,某个东西以为看懂了,结果做起来发现完全不是那么回事,很多概念的边界其实是模糊的,然后又要回来扣理论,然后再回头做。

嗯嗯,所以有缘的话键者也许会写一篇干一点的基于Docker+Gitlab的自动化部署实战。

也希望本文能抛砖引玉,帮助第一次接触CI/CD自动化开发的童鞋整理思路。

附录

摘自《持续集成是什么?》(作者:阮一峰)

持续集成指的是,频繁地(一天多次)将代码集成到主干。

(1)快速发现错误。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易。

(2)防止分支大幅偏离主干。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。

持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

Martin Fowler说过,”持续集成并不能消除Bug,而是让它们非常容易发现和改正。”
与持续集成相关的,还有两个概念,分别是持续交付和持续部署。


持续交付(Continuous delivery)指的是,频繁地将软件的新版本,交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。

持续交付可以看作持续集成的下一步。它强调的是,不管怎么更新,软件是随时随地可以交付的。


持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。

持续部署的目标是,代码在任何时刻都是可部署的,可以进入生产阶段。

持续部署的前提是能自动化完成测试、构建、部署等步骤。

地址:http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据