程序员

持续集成进阶篇

在前一篇文章持续集成入门篇中我大概介绍了下持续集成的概念及工具(抱歉,在前一篇文章中我查的资料不够与时俱进,工具介绍的都比较老,目前流行的工具应该就属JenkinsTravis CI 了)。

这篇文章我将就持续集成的话题继续深入地探讨一番。

持续集成的四个步骤

代码级别的集成

这个级别的集成不依赖独立的持续集成工具也可以实现,一般语言的build工具基本内置,比如java的maven,gradle,go内置的build工具。

集成 Workflow

单一的编译-构建工具逐渐地不能满足产品快速交付的需求。

整个开发流程的重心从『代码级别的集成』转移到了更自动化地编译和更完美的测试验证,致力于在最短的时间内发现问题,缩短开发周期,提高软件质量。比较常见的一个场景,某个团队先进行代码 Build,触发单元测试、集成测试,打包测试完毕后再自动部署到测试环境,循环往复,形成「编译-构建-测试-集成-部署到测试环境」的 Workflow.

持续部署与交付

在上个阶段中,产品是自动部署在测试环境,手动部署在生产环境。之所以这样选择,是因为产品在从需求到部署的过程中,会经历若干种不同的环境,例如 QA 环境、各种自动化测试运行环境、生产环境等。这些环境的搭建、配置、管理,在不同环境中的具体部署是比较复杂的。经常会遇到这么一种场景:明明在测试环境已经部署成功,但线上环境又出现部署故障。这种情况很可能是生产环境和测试环境的异构造成的。

这时候需要改进你的 CI 系统,建立标准化的环境部署顺序,在 Workflow 中增加部署预生产环境并进行灰度集成测试的流程,做好线上环境部署后的回归测试。到这里,已经真正做到了持续交付。

持续交付并不是指软件每一个改动都要尽快部署到产品环境中,它指的是任何的代码修改都可以在任何时候实施部署。而『持续部署』,即自动部署到生产环境中而无需手工干预:得到一个版本后,自动部署该版本到生产环境中。实践证明,相对独立快速地部署新功能是一个核心竞争力,可以减轻大规模功能变更的风险。

gitlab-workflow交付流程图

并行多workflow集成以及个性化集成

随着项目和团队规模增长,模块之间依赖关系变得复杂,如何确保代码质量的同时,保证代码构建的一致性和稳定性,成为一大挑战。Docker 可以方便地以『容器化』的方式部署,它就像集装箱一样,打包了所有依赖,在其他服务器上部署很容易,不至于换服务器后发现各种配置文件散落一地,这样就解决了编译时依赖和运行时依赖的问题。

还有一个问题,开发的分支越来越多,每个活跃分支都进行环境部署和集成测试,对持续集成环境的维护成本也就越高。Docker 的快速启动和镜像仓库是天生为 CI/CD 设计的,以前启动一个虚拟机需要几分钟,而启动 Docker 只需要几秒钟,让并行的持续集成才能成为可能。

目前,比较常见的基于 Docker 进行持续集成的流程如下:

  • 开发者提交代码
  • 触发镜像构建
  • 构建镜像上传至私有仓库
  • 镜像下载至执行机器
  • 镜像运行

持续集成工具

传统的CI工具

第一个正规的持续集成工具是于2001年所推出的CruiseControl,这是一个基于Java开发的开源软件。除了持续构建流程之外,它还提供了邮件通知、Ant以及对各种源代码控制系统的支持,并推出了支持.NET与Ruby的移植版本。尽管Jenkins后来居上,成为第一个得到广泛应用的CI工具,但CruiseControl已经具备了一个CI工具的基本功能,为CI过程的推广做出了很大的贡献。

Jenkins的出现与发展颇有传奇色彩,它的前身是由一位来自Sun公司的开发者川口浩介(Kohsuke Kawaguchi)于2004年开发的一个基于Java的CI工具Hudson。经过三/四年的发展后,它逐渐超越CruiseControl成为了最流行的CI工具。但自从Oracle收购了Sun之后,希望将Hudson作为收费的商业工具进行开发。以川口为首的开发者社区则决定以Jenkins的名义继续免费版本的开发。有趣的是,Hudson与Jenkins的开发者各自将对方视为自己的分支版本,而将自身视为正统。在2013年后,Jenkins的发展势头已有超越之势,它的每日提交次数远远地超越了Hudson,如今已成为市面上最流行的CI工具。

早期的Jenkins与其他传统CI一样,只支持本地托管。而现在已经有一些云计算平台推出了基于Jenkins的SaaS方案。这方面比较突出的有CloudBees,它所提供的方案是一种集成了CI与CD的混合方案,可通过Docker Pipeline插件提供对Docker容器的支持。

除了Jenkins之外,其他一些流行的CI工具还包括由JetBrains推出的TeamCity,以及由Atlassian推出的Bamboo等等。这些CI工具基本都提供了以下功能:

  • 对源代码控制系统的支持,例如Git、Subversion、TFS等等。可以在代码控制的主线发生代码提交时自动触发后续的一系列步骤,例如构建、测试与部署等等。
  • 对依赖管理工具的支持,如Java的Maven、NodeJS的NPM、Ruby的Gem,以及.NET的Nuget等等。
  • 对各种类型测试的支持。早期的CI只支持单元测试,即单个对象或组件的功能验证。随后加入了对集成测试的支持,即对组件之间的通信与交互进行难。尽管如此,这还不足以验证系统确实按照用户期望的方式进行工作。因此现代化的CI工具开始支持功能性测试,将原先的手工测试替代为基于Selenium等工具的自动化测试。

云计算环境中的CI工具

曾在大规模企业中尝试过CI实践的开发者非常了解:代码的构建与测试的执行是一种非常消耗资源的操作,如果有多个团队使用同一个CI平台,那么这种情况将进一步加剧。近几年来,软件团队逐渐厌倦了本地托管的CI系统对时间与精力的要求。而基于云计算平台的SaaS解决方案的出现快速地弥补了这方面市场的缺失。

Travis CI是一个基于GitHub API所打造的托管CI服务,使用Travis CI有一个先决条件,即源代码需要在GitHub进行托管。Travis CI通过webhook对GitHub代码仓库中的各种变化进行响应,例如代码提交或pull request等等。Travis CI也依赖GitHub提供的服务对用户和组织进行认证。

使用基于云环境的CI系统让开发者得以从对本地CI系统的安装、配置过程中解脱,不必再关注于基础设施和用户认证与授权方面的问题。此外,由于大多数SaaS方案都提供了对应的API,因此整个工作流都可以实现API驱动。

基于云环境的CI系统还有另一大优势,他们通常会提供更多的测试功能,例如对不同浏览器与操作系统组合条件的测试。例如Travis就支持在Linux、Mac和Windows系统上的测试,并支持PHP、NodeJS、Go和C等各种语言。

用于移动应用的CI工具

随着智能手机的日益普及,移动应用的数量也在不断增长。但由于移动应用与Web应用相比有一些特别之处,例如它的测试与发布方式,以及完全不同的依赖管理机制,因此移动应用对于构建、测试及部署流程提出了完全不同的要求,这是传统的CI工具力所不及的。好在如今已经有几家主流的CI提供商实现了支持移动应用的CI工具,例如CircleCI已经提供了对iOS应用的支持。

移动应用的测试与Web应用具有很大的差别,Web应用的客户端多数集中在一些主流的浏览器与操作系统上,而移动应用的客户端往往是千差万别的,特别是在Android平台上。某些测试框架,例如Espresso以及Appium能够自动替你解决许多困难。而像CrashlyticsHockeyApp这样的工具除了内置的CI功能之外,还能够自动生成应用崩溃的报告,为开发者进行问题诊断提供充分的上下文。

而由于移动客户端的多样性,以集中化的方式进行所有测试的方式是不太实际的。因此,移动开发社区更推崇beta测试的方式,通过TestFairyTestFlight等工具将潜在的新版本发布给beta测试人员。

移动应用的另一个独特之处在于它的发布方式,通常需要经过漫长而繁琐的审核流程才可发布至对应的应用商店。这不仅降低了持续交付的速度,还不得不在流程中引入各种人工步骤,使全自动化的流程无法实现。

为此,像Fastlane这样的工具可实现将应用审核流程中的大部分元素实现自动化,例如为新应用进行屏幕截图及处理认证等信息。可结合Jenkins等CI工具以完善整个工作流。

CI工具的未来

CI与CD过程如今已成为现代化应用开发中一个并不可少的元素,绝大多数开发团队在软件项目中都需要设计一个完善的CI与CD工作流。

而CI的发展并不会停下脚步,它仍处于高速的发展中。在对Web及移动项目支持的基础上,未来几年之内,我们将看到CI在其他类型的开发中的应用,例如智能手表、智能汽车以及物联网中,甚至是在虚拟现实与生物科技项目中的应用。

CI过程目前所面临的一个挑战在于在开发环境中执行的自动化测试与生产环境之间总是存在着或多或少的差别。随着近来年以Docker为代表的容器化技术在(微)服务系统中的广泛应用,CI过程也从容器的使用中受益匪浅。Docker的高可移植性使多个CI提供商开始拥抱Docker。举例来说,CircleCI就支持基于容器的应用,而CodeShip近期也推出了Jet,这是一个对Docker应用进行测试与部署的解决方案。

持续集成最佳实践要点

持续集成无论是工具使用还是流程定义,其实都不难,难的是如何形成这样的习惯与文化。笔者通过自己的实践经验总结了以下持续集成的最佳实践要点,可以帮助让这个改进更容易些:

  • 首先要选择一个可以脱离IDE进行build的语言以及项目定义工具。这个很明显,CI是要在服务器上跑的。如果你的团队进行build还完全依赖IDE,这事情就没法搞。
  • CI工具越早引入越好,最好是写第一行代码的时候就先弄个CI,但配置不用一步到位,可以按照上面的进阶一步步完善。这样才容易形成围绕CI进行开发的习惯。
  • 集成测试用例最好使用项目本身开发语言编写和单元测试类似,至少是团队开发人员都熟悉的语言。并且项目代码要和集成测试用例在同一个源码仓库里。如果你的团队有专门的QA人员写测试用例,那最好让QA和开发人员共享同一个代码仓库。如果你的集成测试系统是通过配置实现的,那也请将测试用例作为配置文件放到代码仓库中,而不是通过web编辑器放到数据库中。这样做的最大好处是项目代码和集成测试代码共享同一个分支,同一个build number,只有这样才能做多分支的并行测试。否则如果测试用例单独维护,代码的分支如何和测试用例对应起来?最后的结果就是,自动化测试用例都是上线后补充的,上线前还是依赖人肉测试。
  • 编写测试用例和功能开发最好是同一个人,如果做不到,编写测试用例的开发也要有权限修改业务代码。因为要做自动化测试必须在系统中留一些后门来给自动化测试提供便利。比如提供用户的批量生成和销毁,比如对测试的请求不记录到统计日志中等等。
  • 部署的脚本或者配置最好和项目在同一个源码仓库。只有这样,自动化部署才方便实施。因为项目的改进以及重构,往往伴随着依赖资源以及部署机制上的改造。
  • 服务最好不依赖外部容器,可以独立运行。这个专指java的容器,其他的如go,nodejs都没有这个问题。java的容器是企业应用为了降低部署成本带来的习惯,但当前虚拟化,docker等技术这样成熟的情况下,应用容器已经完全没必要了。如果非要用,也最好直接和应用打包在一起,让应用可以直接运行,这对开发效率以及集成测试,都非常有帮助。
  • 最好提供一种直接可以单进程运行整个系统而不依赖外部资源的配置,外部资源都用内存版的库进行mock。这样做的好处是可以非常快速的进行初步的集成测试验证,同时也非常方便统计集成测试覆盖率(通过单元测试覆盖率工具即可实现)。
  • 如果公司有多个研发团队,最好共享CI池,这样成本最小。有的公司为了省钱,避免超过免费限制,部署多套CI。其实算下来这样成本比购买商业版更高。

总结

持续集成是最能体现一个团队的DevOps(关于DevOps将在接下来的文章中进行讲解)氛围以及水平的一个场景,因为整个流程需要开发,测试,运维的紧密协作,缺一不可。

参考文章

参考书籍