本文分析了持续集成系统的功能点,比较Jenkins,Teamcity这两款持续集成工具的优缺点,最后总结了持续集成的最佳实践要点。

阅读对象:研发以及研发管理人员,研发工具研究爱好者

持续集成(Continuous integration),是随着敏捷开发一起成长起来的理念,逐渐发展,当前持续集成的外延基本已经涵盖了持续测试,持续部署,持续反馈,逐渐和持续交付等同了,只是二者的关注点略有区别。

引用《Continuous Integration: Improving Software Quality and Reducing Risk》中的一句话:

持续集成的核心是减少缺陷引入,发现和修复之间的时间间隔。

所以持续集成的关键在于持续

假设时间回退,我们回到还没有持续集成工具的计算机远古时代,遇到了以下场景:

你辛辛苦苦,撸啊撸呀,撸完了功能代码,高兴提交代码到仓库里。结果你的同事更新代码后发现程序编译不通过,后来发现是你少提交了一个代码文件或者依赖配置,导致大家的工作中断。

于是你们商量,找了一台专用电脑,每次代码提交都触发一个hook,调用编译器在服务器上编译,如果错误就通知对应的提交者。这个可能就是最初的持续集成了。

但后来发现只编译还解决不了问题,所以逐渐增加了代码风格检查,静态代码分析,单元测试调用,测试覆盖率检查等等增强功能。

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

然而好景不长,你们发现即便是经过了完整的单元测试,严格的代码分析,也不能保证程序正常运行,更不能保证接口实现符合需求定义。于是你们增加了自动部署测试环境的流程,编写了集成测试用例,每次提交先进行代码build,然后触发测试环境部署和集成测试。

这是持续集成的第二进阶,集成workflow,这才基本实现了真正的持续集成。

你们继续欢乐的进行编程,后来发生一次部署故障,经过集成测试打包后的系统部署到生产环境后出现了错误。原因可能是生产环境和测试环境的异构,也可能是二者配置差异导致的错误。于是你们又改进一次,在workflow中增加了部署预生产环境进行灰度集成测试的流程,还进一步将生产环节的部署和持续集成系统整合,通过持续集成系统来触发生产环境的部署,同时做线上环境部署后的回归测试。

至此,是持续集成的第三进阶,持续部署与交付

随着你们项目的发展,团队成员越来越多,用户也越来越多,对稳定性的要求也越来越高。原来你们的修改直接提交到代码仓库的主分支上,然后触发持续集成。人多了后发现这样有问题,于是使用了feature分支模式,比如典型的Github workflow。

借用一张微博@TimYang发的gitlab workflow交付流程图

  • 分支的每次提交是否需要触发CI流程?
  • 每次merge request或者pull request,是否要触发CI,进行预验证,然后再进行合并?

如果回答是,则需要进一步改进CI,CI的workflow要支持分支并行,并且需要考虑并行到哪一步,只是代码级别,还是同时要进行测试环境集成(当然分支的测试不建议直接跑到灰度环境)。另外如果代码量和单元测试越来越多,开发者本地执行太慢,是否要支持本地未提交代码在服务器端进行CI?

至此,是持续集成的第四进阶,并行多workflow集成以及个性化集成

假设我们需要自己设计一个持续集成系统,现在总结一下持续集成系统需要设计的功能点:

  1. User & AuthManager 这个是任何一个系统的基本需求,同时和持续集成的提醒反馈机制,以及第四进阶的个性化集成都相关。
  2. Project 权限控制一般是以项目为粒度的,同时持续集成的Build configure都是关联在项目之下。
  3. Build Runner 因为各种语言的build工具以及项目定义都不相同,需要允许扩展。为什么不直接用命令行调用呢?Build Runner需要做的事情包括日志收集,错误收集,变量注入等,用脚本比较困难。
  4.  Source configure  项目关联的源码配置
  5.  Build configure  Builder Runner需要的参数等设置,一般都需要支持能配置多个step配置。
  6.  Workflow (pipeline)  这个到第二进阶的时候就需要。因为持续集成系统执行的最小单元是Build configure,同一个configure的step不能独立执行,如果没有workflow机制,直接将所有的流程都放到同一个Build configure的不同的step,导致的结果就是如果后面的step失败,则必须从头开始。比如灰度测试的时候,部署脚本出现了错误,导致失败。如果有workflow,修复脚本后再次运行灰度测试这个Build configure即可,否则必须从头构建,非常浪费时间。
  7.  Build agent  这个不同的CI工具实现的机制不一样,在这里指真正执行Build Runner的单元。Build agent要能支持库扩充部署,方便实现并行Build。
  8. Notification 通知和反馈对持续集成非常重要,最开始和就说到,持续集成包含的一层含义就是持续反馈。只有Build的结果及时通知到开发,测试,运维人员,才能形成持续集成环。

当然,当前CI工具这么多,我们完全没必要自己设计开发。既然我们总结出来CI工具需要包含的功能点,那么我们就从这几个方面进行比较选择一下。由于笔者主要使用Jenkins和Teamcity,所以这里只比较这两个工具,其他的工具也可以按照这几个角度进行比较。另外选择这两个工具比较的原因是二者都可以免费获取到,Jenkins是开源的,Teamcity小规模使用(3个agent,20个Build configure)免费。

  Teamcity Jenkins 比较说明
User & AuthManager 5 5 二者差距不大,都支持ldap等认证扩展,Jenkins胜在认证扩展插件比较多,Teamcity胜在权限管理比较方便(这个和项目配置有关,下面会说到)。
Project 5 3 Teamcity的Project是树型结构的,而不是并列的。这样的好处是配置可以继承,子项目继承父项目的配置。另外的好处是权限容易管理。即使项目非常多也可以展示出来。Jenkins下如果项目太多就必须通过插件来解决了。
Build Runner 5 5 二者基本扯平,Jenkins依然胜在插件较多,Teamcity胜在Builder Runner的抽象比较好,配置项定制化比较强。
Source configure 5 4 主流的SCM二者都支持,Teamcity胜在Source configure是独立的配置对象,关联在Project上,可以通过继承机制多个项目或者多个Build configure共享Source configure。另外就是Teamcity可以直接在后台查看本次build变更的diff,不需要依赖SCM的支持(不确定Jenkins是否有插件支持)。
Build configure 5 3 Jenkins没有明确抽象Build configure,同一个Project只能有一个Build configure。二者的Build configure都支持多个step。Jenkins的这种抽象方式带来的问题是配置不够灵活,实现Workflow比较困难。
Workflow 5 3 Jenkins是通过Build Pipeline插件来实现这个功能,Teamcity内置Build Chains机制。前面也说了,由于Jenkins的同一个项目只支持一个Build configure,Pipeline是通过多个项目的trigger机制实现的。这种方式导致pipleline之间共享资源比较困难。而Teamcity 的Build Chains是通过Build configure之间的依赖关系来实现的,配置比较方便。另外Jekins也有Workflow插件,通过脚本定义workflow,但笔者未深度使用过,不好评价。
Build agent 5 3 Jenkins没有Build agent抽象,集群是通过master-slave机制实现的,slave基本上是master的全功能复制。好处是部署后不依赖agent即可工作,缺点是不方便扩容。而Teamcity的Build agent是独立的,Build runner是运行在agent上的,通agent pool统一管理。Teamcity还支持cloud-agent功能,监测Build queue中的任务,动态伸缩agent的数量。这个使用云服务时非常方便,也能节省资源。
Notification 5 5 二者都支持各种Notification机制以及插件。Jenkins优点是插件多,Teamcity优点是各种Notification工具都开箱即用。

综合评价,由上表可以看出,Teamcity在易用性以及抽象设计上胜出,开箱即用,不用太多自定义即可满足大多数场景,但由于是非开源应用,不便于自定义扩展。建议初创团队或者没有太多研发精力自定义CI工具的团队选择。Jenkins胜在开源,插件丰富,可完全自定义,建议有专门的CI研发人员的团队选择。

Teamcity的cloud-agent有个bug,不能支持amazon aws的中国区Region,因为没有源码我只能提bug等待他们修复,有兴趣的可以上去帮忙给投个票。TW-42235

再说说持续反馈问题

前面说了,如果CI的反馈不能及时通知到相关人员,或者相关人员响应不及时,最后导致的结果就是CI形同虚设。所以CI都有各种Notification插件,还有人弄了个硬件的报警器,build失败就报警,据说效果不错。

  • 邮件 默认都是通过邮件通知的。但邮件的缺点是不及时,同时大家为了避免打扰,往往会设置过滤器过滤掉。
  • Jabber等内部IM 前提是必须团队内有搭建使用内部Jabber服务的习惯。
  • 桌面客户端以及IDE通知 需要所有成员都安装插件或者桌面agent。
  • Slack等团队通讯SaaS Slack的集成可以将build结果通知到channel内,团队共享通知,也可以及时讨论,但缺点是会打扰到不相干的人。
  • Grouk Grouk 是笔者团队研发的团队通讯工具。在CI通知集成上尝试进行改进,在build失败的消息中 @ 导致build失败的提交者,这样大家把提醒设置为 @ 就可以避免打扰了(此功能尚未上线,敬请期待)。

创业团队是否需要持续集成

前一段时间微博上关于技术工具,架构,团队的争论非常火热,有一种观点是创业团队关键是要生存发展,对生存发展没有帮助的花架子都不要,办法土点也没关系。

技术人创业失败后对自信心打击是非常大的,因为会发现自己优雅的代码架构,完善的单元测试覆盖率,良好的持续集成,竟然也没干过对手找外包撸的烂系统,于是就有了上面的观点。笔者也遭遇过创业团队失败,对这种心情深有所感。

但创业失败和成功的因素太多了,很难说优雅的代码架构,完善的单元测试覆盖率,良好的持续集成能有多大作用。我个人后来想明白了,这些只是作为一个技术人专业态度的体现,与成功与否关系不大。

持续集成最大的作用是让研发团队更优雅的进行功能开发,快速迭代,而不是忙于扑火,填坑。具体值得花多大代价去维护,是磨刀和砍柴的关系。如果只是砍棵树,磨磨斧头就行了,如果是片林子,你可能需要弄台伐木机了。

持续集成最佳实践要点

还是引用@TimYang上一个配图的说明:

当然这里面[持续集成]最容易的就是画流程,难改变是流程之外的东西。

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

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

总结,持续集成是最能体现一个团队的DevOps氛围以及水平的一个场景,因为整个流程需要开发,测试,运维的紧密协作,缺一不可。