OpenShift中的持续交付

持续交付

如果要打造一个持续交付的流水线,首先要考虑多环境的问题。一般一个应用程序会有多个环境,比如开发环境、集成测试环境、系统测试环境、用户验收测试环境、类生产环境、生产环境。如何在OpenShift中隔离并建立对这些环境的部署流程有多种方案可以选择。

  1. 同一个project中使用label和唯一名称来区分不同的环境;
  2. 集群中的不同project来隔离环境;
  3. 跨集群来隔离环境。

我们以第二种方式为例,演示下多环境管理问题。

在上图中,我们有一个build project。build project包含了一组相互依赖性比较强的应用,每个应用对应一个build config,产生的Image Stream存放在image register中。而每个环境各对应一个project,其中包含了该应用的deployment config,其镜像输入是build config产生的Image Stream。之所以这样做,有以下几点考虑:

  1. 不同的环境分布在不同的project中,可以很好的借助project的特性进行环境隔离。比如sys project的容器只能部署在label为sys的node上,prod project的容器只能部署在label为prod的node上。
  2. 不同的project可以分别定义权限访问和控制。比如只有QA才能操作sys project中的资源,运维工程师才能操作prod project中的资源。
  3. 不同的环境共用一个Image Stream,保证了应用程序镜像在不同环境中的是完全一致的,防止由于测试环境和生产环境不一致而引入缺陷。

那么大家共用同一个Image Stream,如何实现应用的promotion呢?解决方案就是使用tag。

如上图所示,一个image stream里面有多个版本的镜像,而OpenShift可以为版本添加自定义tag。在不同的project里面,我们配置image的来源为”ImageStreamTag”,名称为”applicationName:environmentName”。比如sys project的镜像名为”App1:sys”,prod project的镜像为”App1:prod”。如果想将version 3的镜像推送到sys环境,只需要简单的给version 3的镜像打上sys的tag,这样部署sys环境时就会自动使用version 3的镜像。

oc tag App1:latest App1:sys

如果在Deployment Config里面配置了自动监听tag的变动的操作,那么一旦你修改了ImageStream的tag,就会自动触发对应环境的部署。

由于应用程序镜像在不同的环境中是一致的,那么变动的部分都被抽取到了外部配置中。如何根据不同的环境来加载对应的外部配置呢?实现方式有很多种,这里介绍了使用Spring Cloud Config的方案。

首先我们将针对不同环境的配置放置在一个git仓中,然后通过Spring Cloud Config Server将其转换为http服务。而我们在应用中嵌入Spring Cloud Config Client,其会接收一个环境变量来拉取指定环境的配置。而该环境变量可以通过Deployment Config来进行注入。

oc env dc/sys PROFILE=sys

使用Spring Cloud Config给予了我们更多的灵活性。我们可以选择在应用程序第一次启动的时候拉取配置,也可以设置每隔一段时间自动更新配置,从而实现热更新。

OpenShift虽然提供了构建和部署的能力,我们有时也需要使用Jenkins之类的工具来可视化以及编排整个流水线。

既然OpenShift是个容器化的管理平台,那么我们完全也可以将Jenkins作为一个应用纳入到OpenShift中来托管,这样Jenkins的Master和Slave都是容器化的。OpenShift官方提供了一个Jenkins2.0的镜像,其预装了OpenShift pipeline插件,可以很方便地进行构建、部署等操作。

生产环境的部署

OpenShift在产品环境的部署默认是rolling的方式。

每次部署时,它会启动一个新的Replica Controller,部署一个pod,然后削减旧的Replica Controller的pod,如此往复,直到旧的Replica Controller中的所有pod都被销毁,新的Replica Controller的所有pod都在线。整个过程保证了服务不宕机以及流量平滑切换,对用户是无感知的。

而有的时候部署场景要负责些,比如我们想在产品环境对新版本做了充分的PVT(product version testing)才切换到新版本。那么就可以使用蓝绿部署的方式。

蓝绿部署方案的关键点在于一个Router对应两个Service。而Route作为向外界暴露的服务端口是不变的,两个Service分别对应我们的生产蓝环境和生产绿环境。同时只有一个Service能接入Router对外服务,另一个Service用来进行PVT测试。切换可以简单的修改Router的配置即可。

port:
  targetPort: app-blue-http
to:
  kind: Service
  name: app-blue

结语

OpenShift在应用的构建以及部署方面为我们提供了大量开箱即用的功能和解决方案,所以实现持续交付不再那么艰难。我们可以将更多的精力花费在提升应用程序质量以及架构方面,交付更好的产品。


更多精彩洞见,请关注微信公众号:思特沃克

Share

对于持续集成实践的常见问题的解答

我之前总结了一下自己在做咨询时辅导团队时遇到的问题,并且给出了相应解答。

Q1:什么是持续集成?

集成,就是一些孤立的事物或元素通过某种方式集中在一起,产生联系,从而构成一个有机整体的过程。知识经济的社会,集成已经成了很重要的一个名词。各行各业基本都会用到集成。比如汽车行业,那么复杂的一台跑车愣是通过一大堆零件组装起来。对于这些传统行业,它们在研发成功以后,可以通过流水线的方法批量生产进行集成。而在软件行业中,集成并不是一个简单的“搬箱子”的过程。因为软件工业是一个知识生产活动,其内在逻辑非常复杂,需求又很难一次性确定,完成的产品与最初的设计往往相差很远。敏捷宣言中就有一条是说响应变化重于遵循计划。而且由于软件行业的迅猛发展,软件变的越来越复杂,单靠个人是根本无法完成。大型软件为了重用及解耦,往往还需要分成好几个模块,这样集成就成了软件开发中不可或缺的一部分。

持续集成这个词语最早是由大名鼎鼎的Martin Fowler。他在早期进行软件行业实习的时候就发现一个问题,即集成是项目中的一个大难题。当他在英国一家软件公司实习时,项目经理亲口告诉他一个项目已经开发了好几年了,现在正在做集成,集成已经进行了好几个月了,每个人都很疲惫,并不知道集成什么时候才能结束。其中一个很重要的原因就是项目集成发生的频率太低,导致大家对项目很没有信心。

在《持续集成》一书中,对持续集成的定义如下:持续集成是一种软件开发实践。在持续集成中,团队成员频繁集成他们的工作成果,一般每人每天至少集成一次,也可以多次。每次集成会经过自动构建(包括自动测试)的检验,以尽快发现集成错误。自从在团队中引入这样的实践之后,Martin Fowler发现这种方法可以显著减少集成引起的问题,并可以加快团队合作软件开发的速度。

Q2:持续集成能给团队带来什么好处?

如果想要谈持续集成的好处,那么我们应该先谈谈没有采纳持续集成,项目会出现什么问题。总体来说,没有采用持续集成的项目一般会面临下面四个问题:

  1. 没有一致的可部署的软件。只有在完成集成测试、系统测试后,才能得到可用的软件,整个过程中只有最后时刻才能拿到可运行软件。集成活动不一定在一个标准的构建机器上生成,而是在某个开发人员的机器上构建的,那么可能存在在其他机器上无法运行的问题。
  2. 很晚才发现缺陷,并且难以修复。实践证明,缺陷发现的越晚,需要修复的时间和精力也就越大。从上一个可工作的软件到发现缺陷之间可能存在很多次提交,而要从这些提交中找出问题并修复的成本会很大,因为开发人员需要回忆每个提交的上下文来评估影响点。
  3. 低品质的软件。 由于集成时每次包含的代码很多,所以大家的关注点主要都是如何保证编译通过、自动化测试通过,而往往很容易忽略代码是否遵守了编码规范、是否包含有重复代码、是否有重构的空间等问题。而这些问题又反过来会影响今后的开发和集成,久而久之集成变得越来越困难,软件的质量可想而知。
  4. 项目缺少可见性

而通过持续集成的活动,我们可以实现以下价值:

  1. 减少风险。缺陷的检测和修复变得更快。软件的健康程度可以测量。
  2. 减少重复过程。让人们有时间做更多的需要动脑筋的、更高价值的工作。通过对重要过程自动化,克服项目中某些成员对实现改进的抵制。
  3. 在任何时间、任何地点生成可部署的软件。对客户来说,可以部署的软件是最实际的资产。
  4. 增强项目的可见性。集成就像我们项目的一面镜子,通过这面镜子能够快速的了解项目目前的状况、存在的问题。
  5. 对开发团队的软件产品建立起更强大的信心。它能够帮我们有效的决策,注意到项目进展的趋势。

Q3:持续集成都包括哪些要素?

一个最小化的持续集成系统需要包含以下几个要素:

  1. 版本管理系统:项目的源代码需要托管到适合的版本管理系统中,一般我们使用git作为版本控制库,版本管理软件可以使用github、gitlab、stash等。
  2. 构建脚本:每个项目都需要有构建脚本来实现对整个项目的自动化构建。比如Java的项目就可以使用gradle作为构建工具。通过构建工具实现对编译、静态扫描、运行测试、样式检查、打包、发布等活动串起来,可以通过命令行自动执行。
  3. CI服务器:CI服务器可以检测项目中的代码变动,并及时的通过构建机器运行构建脚本,并将集成结果通过某种方式反馈给团队成员。

Q4:持续集成的全景图是什么样子的?

以下是持续集成的一个全景图。从中可以看到我们需要版本管理系统、构建脚本、CI服务器、CI构建机器、反馈机制。

Q5:持续集成一般都包含哪些任务?

持续集成并不是说只要代码能编译通过就是集成成功,我们已经把每次集成都看做一次完整的测试。任何迁入到代码库中的代码都应该是可以部署到产品环境的。拿一个Java项目为例,持续集成一般执行的任务有:

  1. 代码静态扫描:通过静态扫描确定代码的一些潜在bug,比如未被使用的变量等。
  2. 代码样式检查:团队一致定义出需要遵循的编码规范,并通过一些插件对迁入的代码进行样式合规性检查,防止不守规范的代码进入版本库。比如方法名首字母小写、类的大字母大写、if关键字后面需要加空格等问题都可以纳入到样式检查中。
  3. 单元测试、集成测试、系统测试:通过运行自动化的单元测试、集成测试、系统测试可以有效的保证迁入代码的质量。一旦有测试失败,开发人员就需要快速反应进行修复。
  4. 测试覆盖率检查:一般项目会设置一个测试覆盖率指标(比如80%),如果代码达不到这样的测试覆盖率,就不会允许代码迁入。这样可以保证开发人员在新增功能时也要为新加入的功能编写自动化测试。
  5. 编译打包:确保没有任何语法错误,生成构建产出物。
  6. 发布: 将通过完整构建的产出物放置到产出物仓库,以便进行后续部署。

这些任务都必须是能通过命令行自动完成的,不同类型的项目任务略有不同。

Q6:持续集成这些任务应该遵循什么顺序?

其中有一个重要的原则就是fail fast,即快速失败。一般会把运行时间短的、价值大的任务放在前面,而运行时间长的任务放置到后面。因为构建成功的标准是所有的验证都能够通过,那么执行时间短的任务放在前面能更快的得到反馈。

Q7:为什么我们组搭建了持续集成服务器,并且还派专人看守CI,但是感觉项目并没有明显的改善?

并不是说搭建了持续集成服务器就说明团队能成功运行持续集成了。持续集成是一个实践,所以大家要遵循一些原则。

大家可以先思考一下问题:

  1. 在CI服务器上多久会看到一次集成?
  2. CI服务器的集成结果是绿色居多(指构建成功)还是红色居多(指构建失败)?
  3. 当构建失败后,团队成员有没有第一时间修复构建,有没有在构建失败的情况下依然在提交代码,在提交代码之前有没有进行本地的私有构建?

从这些问题可以引申出持续集成中需要遵循的一些原则:

  1. 经常提交代码
  2. 不要提交无法构建的代码
  3. 立即修复无法集成的构建
  4. 编写自动化的开发者测试
  5. 必须通过所有测试和审查
  6. 执行私有构建
  7. 避免迁出无法构建的代码

Q8: 本地提交代码应该经过哪些步骤?

本地提交可以采用经典的七步提交法


更多精彩内容,请关注微信公众号:软件乌托邦

Share

避免CI成为一个安全隐患

背景

最近临时交接了一个客户测试环境和产品环境的维护工作。交接的客户资产包含:代码库、生产环境主机、测试环境主机、搭建在测试环境主机上的持续集成服务器以及对应的账号密码。这个持续集成服务器采用Jenkins搭建,并且可以用来部署测试环境和生产环境的应用。

不久,接到了客户的一个维护请求:把最新的生产环境数据同步到测试环境里。

这个维护任务需要通过SSH登录到测试环境主机上进行操作。测试主机是通过authorized_keys进行SSH认证的,只要你自己的ssh-key被添加到了主机上,就可以实现无密码登录。这样有两个好处:一方面维护人员无需使用密码,避免了生产环境密码的泄露。另一方面可以按需吊销不再使用的客户端,及时回收权限。所以我需要把自己的sshpublickey交给管理员,让他把我的key加到可访问列表里。

悲剧的是,前管理员告诉我,他的key因为更换电脑的关系没有及时更新。所以,他也无法登录主机。而且之前参与维护的其它管理员的key也都失效了,这意味着我们失去了对主机的控制。此时,我手上只有登录Jenkins的的用户名和密码,于是一个邪恶的想法就诞生了:

既然Jenkins可以执行脚本,那么我是否可以通过Jenkins把我的key注入进去?

于是我把ExecuteShell的Job变成了我的命令行,通过运行日志得知了宿主用户的文件目录信息。然后把自己的sshpublickey加到了登录列表里(此处省略敏感信息):

sudo sh -c“cp~/.ssh/authorized_keys~/.ssh/authorized_keys.bak”

sudo sh -c"echo‘{我的sshpublickey}’>>~/.ssh/authorized_keys"

It works !

我成功的登录了机器,但这却暴露了一个问题:持续集成服务器成为了一个安全隐患。

首先,持续集成服务器可以执行代码。这就意味着它有可能执行有害代码。

其次,持续集成服务器缺乏足够的用户鉴权,这很有可能导致未授权用户访问。

无权限控制的服务器+可以执行代码=裸奔的肉鸡

那么,如何构建一个更安全的持续集成服务器服务器?

rootless原则

“神操纵着万物,你感觉得到他,但永远看不见他。”——《圣经·希伯来书11:27》

在服务器的世界里,root用户就是神,拥有至高的权力和力量。如果有人获得了“神之力”,后果可能不堪设想。

无论是Web服务器、数据库服务器还是持续集成服务器。都是这个世界里的二等公民,权限和力量都应该受到约束。执行的时候应该受到控制。

此外,应该极力避免sudo的滥用,尤其是对那些从外部访问的用户。很多情况下,为了操作方便,很多用户都有sudo的权限。但这恰恰造成了低权限用户通过提升自己的访问权限进行有害操作。

在上述的故事里,因为没有对Jenkins的主机用户做有效隔离,导致了我可以用sudo注入自己的key获得机器的访问权限。

沙盒隔离原则

因为持续集成服务器会执行脚本或运行程序,而这些程序和脚本有可能是存在恶意代码的。所以,对应的任务应该在隔离的安全沙盒中执行,例如:受限的用户,受限的权限,受限的空间。

在上述的故事里,我就通过CI执行了一段不安全的脚本成功获得了登录主机的权限。

如果这些任务在隔离并受控的Docker容器里执行,那么会安全得多。

当然,也可以考虑采用TravisCI这样的第三方持续集成服务来保证安全性。

备份和备份核查原则

在上述故事里,因为缺乏有效的备份机制,导致了所有人都无法访问主机。此外,我在修改authorized_keys的时候先进行了备份。这样,如果我注入失败,还可以还原。

这里的备份,不光是对配置、数据的备份,还有岗位的备份。

如果管理员有备份,完全不会出现无法登陆的事情。

如果有备份QA服务器,完全可以不需要当前的QA服务器。

在做任何变更前,都应该做好备份以及还原的准备。因为任何变更都会带来“蝴蝶效应”。

但是,光备份是不够的。如果备份不能有效还原,那和没有备份没有什么区别。所以,要定时的进行备份恢复测试。确保备份在各种情况下可用。

多重要素身份验证原则

上述的持续集成服务器是暴露在互联网中的,任何一个人访问到这个站点,通过一定程度的密码破解,就可以获得这个持续集成服务器的访问控制权限。从而可以做出上述的操作。

所以,有了用户名和密码,并不一定是可信用户。还需要通过更多的手段,诸如手机短信验证码或者第三方认证集成来验证用户的身份。

关键操作手动验证原则

试想一下,如果在上述的例子中我并没有服务器的访问权限。而是通过提交未经审查的代码自动运行测试脚本。实际上也会造成同样的效果。

有时候我们会为了方便,让持续集成服务器自动触发测试。但是,恰恰是这种“方便”带来了额外的安全隐患。而这样的方便,不光方便了自己,也方便了恶意入侵者。

所以,不能为了方便而留下安全隐患。在关键操作上设置为手动操作,并通过一定机制保证关键操作的可靠性才是最佳实践。

构建安全CI的几个实践:

采用Sibling的方式在Docker里运行任务。

账户密码管理统一采用LDAP认证,如果过期则从外部修改。

CI的登录权限和其它的认证方式(比如GitHub、Okta等)集成起来。并用组限制登录。

对于生产环境的CI,通过更加细粒度的权限限制来隔离一些危险操作。

官方的安全指南

不少持续集成工具的官方都提供了最佳实践以及安全指南帮助我们构建持续集成服务器。请务必在构建持续集成服务器前阅读并理解这些安全实践和措施,并遵照安全最佳实践构建持续集成服务器:

Jenkins最佳实践:https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Best+Practices

Jenkins官方安全指南:https://wiki.jenkins-ci.org/display/JENKINS/Securing+Jenkins

如果没有这些如果

上面提到了太多的如果。如果这些“如果”能发生在事前,这些问题就不会产生。持续集成本身是开发的最佳实践,但如果缺乏安全的意识,一味的追求方便和高效,则会带来很大的安全隐患。通过一些简单而基础的措施和手段,我们就能大大的降低风险。


更多精彩洞见,请关注微信公众号:思特沃克

Share

遗留系统流水线的改进

持续集成(Continuous Integration)是一种软件开发实践,它倡导开发团队频繁地进行系统集成,每一次的集成都可以通过流水线(Pipeline)快速验证。

(图片来自:http://t.cn/Rajfxek)

和传统的集成方式相比,持续集成可以有效地缩短反馈周期、提高软件质量、降低开发成本。这种开发实践也越来越为更多的开发者所接受。对于一个有七年历史的项目,非常幸运的是我们在项目刚开始就使用了持续集成,这也是我们可以长期、稳定地给客户交付高质量软件的保障之一,但是有时我们在项目中也经常会听到一些这样的声音:

  • 每次想提交代码的时候都没有机会, 我们是不是要考虑引入提交“令牌”机制,拥有“令牌”的人才能提交
  • 这个Pipeline已经挂了这么久了,今天估计是无包可测了
  • 这个是测试“随机挂”,重新触发一下Pipeline就好了
  • 我的提交Break了Pipeline了吗?我确认一下
  • ….

通过分析我们发现,这些声音背后的真相更是残酷:

  • 约20对Pair依赖的核心Pipeline构建时间超过1个小时,开发的反馈周期长,大量的半成品积累在本地开发环境和Pipeline上
  • 代码提交之后大约需要2个小时才能出包,每周平均每天可供QA测试的包数量不足1个,平均每个Story的周期时间长
  • 上线前冻结代码、回归测试的时间大约需要2周左右,冻结期间产生的代码无法集成、验证
  • Pipeline不稳定测试导致某些Pipeline构建至少需要重新触发2到3次左右
  • 依赖关系复杂,牵一发而动全身,优化不知从何做起

有时“正确地做事情”比“做正确的事情”还要困难,在项目一开始便在项目中尝试实施TDD等敏捷开发实践,但是随着项目的规模的增加,功能越来越丰富,单元测试在增加、基于UI的功能性测试也在增加,流水线的构建速度却变得越来越慢。微服务架构具有易扩展,技术选择灵活和部署独立等特性,于是我们把应用拆分为不同的微服务,但是同时也带来了流水线的数量和微服务之间集成测试的增加,Pipeline的依赖关系也变得越来越复杂。

在去年,项目中的DevOps小组,在前人的基础上和团队大刀阔斧地开始了Pipeline的改进工作,希望可以通过一些必要的措施,优化流水线,保证QA每天有包可测、缩短开发期间的反馈周期。

改进什么?

这是我们在改进刚开始就面临的一个问题,本地Pipeline数量众多,每一个Pipeline平均有3-4个构建阶段,每一个阶段又有2-3个并行执行的任务,如此众多的Pipeline和任务,应该从什么地方着手?面对Pipeline构建时间长、测试不稳定、代码冻结时间长,依赖关系复杂等问题,应该如何决定改进的优先级?根据高德拉特的约束理论(ToC),所有在非约束点的改进都是假象,我们可以把整个构建流水线看作是一个完整的系统,如果我们改进的是约束点的上游,就会增加约束点的负担;如果我们改进的是约束点的下游,由于通过下游的工作量主要由约束点决定,所以任何在这个位置的改进都是徒劳无获,无益于整个系统产出的提高还造成了浪费。

现状问题树是寻找约束点的方法之一,借助于这个思维过程(Thinking Process)可以帮助我们梳理“不良效果”(Undesirable Effects )之前的因果关系,最后找到需要解决的核心问题,解决了根本问题之后,由其衍生的各种“不良效果”也大多会消失。通过内部的讨论和演练,我们最终把问题定位在以下几个方面:

  • 核心Pipeline构建时间过长
  • 集成环境测试成功率低
  • 缺少必要的监控预警机制

如何改进?

降低资源的占用时间

在零件制造车间中,每一个零件都需要按照既定工序通过车、镗、铣、磨、刨等车床,每一个阶段都需要占用车床资源进行特定的加工工作。和零件加工类似,来自客户的每一个功能性需求也同样要经过类似处理流程,从需求分析到编码开发,从构建打包到部署测试,每一个环节都需要占用一定的资源。如果在单位时间内,资源占用的比率越高,就会产生比较严重的排队现象。如下图所示,在单位时间内如果资源占用大于百分之七十,队列的长度也会呈指数型增长。

Pipeline作为整个软件交付流程重要的一环,如果每次构建资源占用比例过高,会导致大量的代码积压在开发环境等待构建、验证和打包,“在制品”数量也越来越多,而过多的“在制品”恰恰就是软件交付延期的隐形杀手之一。解决对Pipeline资源占用比例过高的途径只有一个:加速处理“在制品”的流程。在改进的过程中我们总结出了以下几种主要的加速手段:

1.并行化

假设Pipeline单元测试需要30分钟才能运行结束,可以通过切分单元测试多进程并发执行,如下图所示,可以节省近20分钟的运行时间:

同样也可以把并行化用于优化Pipeline结构,如下图所示,通过减少不必要的Pipeline依赖关系,让不同的Pipeline并行执行,可以减少大约五分钟的端到端的构建时间。

优化前: 端到端构建时间20分钟

优化后:端到端构建时间15分钟

2.使用Mock或者Stub,隔离真实服务

对于有数据库依赖的单元测试,如果在运行期间连接真实的数据库,读写速度会比较慢,除了IO操作之外,为了保证不同测试之间的隔离性,往往还需要考虑测试运行之后的数据清理工作,而这也会带来一部分的性能损耗。针对这种情况,可以考虑使用内存数据库替代真实的数据库,在提升IO操作的同时,数据清理工作也变得很简单。

为了保证代码的修改没有破坏现有的功能,一般我们会增加基于UI的回归测试,在测试运行之前部署当前应用以及其所依赖的各个服务。对于应用依赖的服务的部署和API调用,也会消耗部分时间。这是可以考虑使用SinatraMoco等工具隔离部分第三方服务,从而缩短部署时间和API的调用时间。

但是隔离真实服务的同时也掩盖了测试替身和真实组件之间的差异性,比如我们在API测试中使用Sqlite替代SQL Server,但是SQLite并没有datatime字段类型,需要在测试代码中需要做额外的映射配置,这种差异性同样也会导致潜在的产品缺陷。所以我们在选择Mock或者Stub时需要权衡利弊,如果使用则需要额外的手段来验证这种差异性

3.优化基础设施和运行环境

增加硬件配置如CPU,内存、替换固态硬盘等也可以一定程度地降低Pipeline的构建时间。对于由于语言或者框架本身带来的性能约束,也可以通过升级到新版本来解决,比如把Ruby从1.8.7版本升级到2.0版本。

提高构建成功率

团队在改进的过程中发现可以通过下面的公式大致估算出平均每天Pipeline产出的可用包的数量:

根据这个公式,如果要增加平均每天出包的数量,除了降低每次构建的时间之外还需要提高Pipeline的构建成功率,而影响构建成功率最常见的问题就是“非确定性”测试。在项目的Pipeline上曾经出现过下面这些情况:

  • 一些UI测试每次至少需要被重新执行一次才能通过
  • 部分单元测试在特定的时间段会稳定失败
  • 构建结构和测试被执行的顺序有关

Martin Fowler在Eradicating Non-Determinism in Tests中指出了这种非确定性测试存在的两个问题:首先它们属于无用测试,由于测试本身的不确定性,它们已经无法用来描述、验证对应的功能。测试运行的结果也无法给开发人员提供正确的反馈,如果测试失败,开发人员无法直接判断这个测试是由于产品缺陷导致还是由于非确定行为导致。其次这些测试就像“致命的传染病菌”一样,降低正常测试的存在价值。假设一个测试套件中有100个测试,其中10个测试为非确定性测试,这些非确定测试会给开发团队带来很多的“噪音”,团队对于Pipeline失败会觉得司空见惯、习以为常,剩余90个测试的作用也会大打折扣。

1.保证隔离性

在Pipeline的结构方面,由于非确定性测试的“传染性“,在着手解决非确定性测试之前可以考虑从测试套件中隔离这种类型的测试,这种隔离一方面可以保证正常的测试可以继续提供正确的反馈,另一方面也方便开发人员解决非确定性的测试问题,如果被隔离的测试失败,只需要重新执行部分测试而不是整个测试套件,很大程度地缩短了修复过程中的反馈周期。

从测试代码级别也需要保证不同测试之间的隔离性,构建的结果不应该依赖于测试被执行的顺序。在优化过程中我们遇到过这样的情况:基于UI的功能性测试依赖于一部分用户基础数据,其中测试T1在运行过程中需要修改特定用户的角色,在测试T2需要使用该用户完成其他的业务操作。如果T1在T2之前执行可以构建成功,反之则会构建失败。解决这类问题通常有两种做法,测试运行之前创建不同的用户或者测试运行结束之后恢复用户数据。对于第二种方法,如果当前测试没有正确地清理数据会导致下一个执行测试失败,增加了定位问题的难度,所以更推荐使用前者来保证不同测试之前数据隔离。

2.增加必要的等待

在UI测试中很多操作都依赖于页面元素出现的时间、位置等,在不同的网络环境、机器性能不同,页面的加载速度也不一样,测试运行的结果也会有所不同。通常web driver会提供一系列的方法来帮助开发者判断元素是否已经加载完成、是否可见、页面是否已经加载完成等(比如Watir的when_present, wait_until_present等),在测试代码中合适的地方使用这些方法可以让测试代码更加健壮,从而提升Pipeline构建的成功率。

3.正确测试异步行为

系统中的异步操作可以为用户提供更好的使用体验,系统不需要等待当前操作完成就可以继续处理其他操作,但是异步操作也增加了测试的复杂度。在项目的集成测试代码中我们发现类似这样的等待操作:sleep 10, 这种原始的等待策略不够稳定,对于网络状况、机器性能、数据量等外部因素依赖较大。回调(callback)和轮询(loop)是两种推荐的测试异步的方法,回调不会有任何尝试任何多余的等待时间,但是使用场景比较有限;轮询通用性更高但是会产生一定的多余等待时间,对于轮询操作,建议使用更小的等待时间间隔(interval)和重试(retry)上限。

调整测试结构

不合理的测试结构也是影响Pipeline性能的重要因素,根据测试金字塔理论,就测试数量来说,从低层级到高层级的测试需要保证金字塔状的结构。测试的运行时间呈现的却是一个倒金字塔状,测试的层级越高测试运行的时间越长,对应Pipeline的构建时间也越长。所以改进Pipeline也可以从调整测试层级结构开始。

1.梳理业务流程,简化测试结构

新的功能在不断增加,已有的需求也不断在变动,产品本身也不断接受来自最终用户和市场的反馈,现有的测试有可能并没有覆盖那些最有价值的场景而已经覆盖的场景也许在真正的产品环境下使用率很低;有些场景已经在低层级的单元测试覆盖,在高层级测试中出现了很多重复的用例。

调整测试结构可以和领域专家一起,重新梳理业务流程,把测试的重点放在那些最有价值的业务场景上,在高层级增加适当的UI测试保证核心功能没有被破坏。对于出现重复或者价值不大的测试,可以考虑删除高层级的测试,用更多的单元测试来替代,从而降低测试的运行时间。关于自动化测试更多的优化手段可以参考一个遗留系统自动化测试的七年之痒

除此之外还需要构建有效的反馈回路,通过Google Analytics等网站分析平台收集来自于最终用户和市场的数据、用户使用习惯、时区语言等地域性信息对应地调整现有的测试结构,让测试环境下的业务场景更加接近真实的产品环境。

2.用契约测试替代集成测试

“Integrated tests are a scam. A self replicating virus that threatens the very health of your codebase, your sanity, and I’m not exaggerating when I say, your life.” – JB Rainsberger

JB Rainsberger的这个说法一点也不夸张,在项目中总是有关于集成测试的各种“吐槽”,构建时间慢、问题难以复现和定位、修复难以验证、不稳定等。而契约测试就是那个可以拯救你,让你脱离“苦海”的利器。

契约测试是“单元级别”的集成测试,基于消费者驱动的契约测试把契约测试分为了两个阶段:消费者(Consumer)生成契约和提供方(Provider)验证契约,在生成契约时通过Mock隔离真实的服务提供方,运行单元测试生成用JSON描述的契约文件;服务端验证只需要部署自身就可以验证契约文件的正确性。

契约测试有很多优点,首先它不依赖于完整的集成环境,部署成功率高,其实在测试运行期间无真实的API调用和模拟的UI操作,测试运行的速度快,成功率高;而且在本地开发环境就可以验证契约测试,问题容易定位,修复的反馈周期短。引入契约测试不但给Pipeline性能带来大幅度的改善,还可以提升整个团队的开发效率。

如何保护改进成果

Pipeline是软件交付的命脉,为了保证每一个功能需求可以长期稳定、源源不断地通过,在优化过程中我们引入了关于Pipeline性能的监控机制,我们基于ThoughtWorks的开源产品GoCD 提供的API开发了一个监控工具,每一次构建之后可以自动统计该次构建的时间和成功率,如果超过这两个指标超过了阈值,则让该次构建失败,提醒代码提交者检查是否引入了影响Pipeline性能的变更,避免性能的进一步恶化。考虑到网络和机器性能的问题,在设置实际的阈值的时候可以稍大于期望值。

我们还构建了基于邮件通知的预警机制,每个工作日的下午发送出包数量通知,提醒团队解决影响出包的问题,同时我们把Pipeline的性能可视化并纳入每周的周报中。

写在最后

优化Pipeline除了Pipeline结构、测试策略和监控可视化手段之外,还需要关注软件架构和团队组织结构,下面是我们项目架构的局部依赖图:

核心的微服务OrderAPI依赖复杂,和RatingSrv之间甚至出现了双向依赖,领域上下文(Bounded Context)的不合理切分使得业务逻辑散落在不同的服务之间,不管我们增加集成测试还是契约测试,这种依赖关系也同样会体现在Pipeline之间的依赖关系上,增加Pipeline的复杂度。

在ThoughtWorks技术雷达上A single CI instance for all teams目前处于Hold状态,在一个组织中多个团队共享一个臃肿的CI会导致很多的问题,比如上文中提到的构建队列过长,构建时间长等,一旦这个共享的Pipeline出现问题会造成多个团队工作 的中断。技术雷达建议在具有多团队的组织中由各个团队分布式地管理自己独立的CI。这种分布式的CI同样也依赖于整洁的软件架构和与之相契合的团队组织形式。

在项目的DevOps小组解散之后,我们成立的项目内部的DevOps Community,以保证产品交付为目标,同时肩负着项目提高内部DevOps技能的职责。项目内部的成员有跨多个开发团队的不同角色组成,DevOps community产生的相关Task,最后都会分配到不同的开发团队中。DevOps是一种文化而不应该是一个单独的小组,DevOps主旨在于构建整个团队中的责任共享的文化,改进现有的流水线是每一个开发团队都需要具有的技能之一。


更多精彩洞见,请关注微信公众号思特沃克

Share

持续集成理论和实践的新进展

之前雷镇同学将Martin Fowler先生的著名论文《持续集成》第二版翻译成中文并发布出来,掀起了国内对于持续集成理论和实践讨论的新高潮。笔者在本文中将全面对比持续集成论文前后两版的异同,分析并展示ThoughtWorks在持续集成领域的理论和实践方面的研究成果,以期对国内企业实施持续集成起到参考和借鉴作用。需要说明的是,本文所介绍的内容毕竟限于笔者的水平,并且主要是ThoughtWorks内部开发和对外咨询实践的总结,所以未必对读者所遇到的情况是适用的,请自行甄别。

《持续集成》第二版虽然是最近才翻译出来(本文写于2015年初),但是实际上Martin Fowler先生完成此文是在5年前的事情。这五年恰好是ThoughtWorks中国公司快速成长的五年。在这五年内ThoughtWorks中国在持续集成领域也有很多发展,这包括:著名的持续集成工具Cruise主要是由中国公司负责开发1; 中国公司帮助国内很多大中型企业完成持续集成实施和相关的流程改进;2009年中国公司的很多同事对于持续集成的度量进行了深入的讨论并且最终由胡凯将其实现为一款软件iAnalysis;2010年至2011年成功的交付了从需求提供方到多个技术服务提供商的持续集成方案,以及企业级自动化中心方案。所以,本文主要包括两部分内容,一部分是通过对比第一版与第二版的异同介绍2000年到2006年之间持续集成领域的主要发展,另一部分则是介绍第二版发表之后持续集成领域的新进展。读者如果之前没有阅读过《持续集成》论文的第二版,建议将本文第一部分一同阅读,因为本文并非对论文的重述,所以很多地方还需要参考原文中的内容。

第一部分 《持续集成》第一版与第二版

《持续集成》第一版由ThoughtWorks首席科学家Martin Fowler和Matthew Foemmel共同完成2,第二版由Martin Fowler更新。

《持续集成》的第一版中并没有给出比较正式的定义,虽然作者在文中说是借鉴了XP实践中的术语,但是目前能看到的XP实践中对持续集成的定义实际上大多数都是指向了Martin的文章。那么我们还是来看看第二版中给出的定义。

持续集成是一种软件开发实践。在持续集成中,团队成员频繁集成他们的工作成果,一般每人每天至少集成一次,也可以多次。每次集成会经过自动构建(包括自动测试)的验证,以尽快发现集成错误。许多团队发现这种方法可以显著减少集成引起的问题,并可以加快团队合作软件开发的速度。3

第二版相对于第一版增加了不少内容,其中最重要的几点包括:

  1. 详细介绍了使用持续集成进行软件开发的工作流程。
  2. 突出了配置管理在持续集成实践中的作用。
  3. 提出分阶段构建的概念。
  4. 增加了持续集成报告的内容。
  5. 增加了持续部署的内容。
  6. 给出了引入持续集成的建议。

持续集成的流程

在持续集成领域,我们经常会用到的一个术语就是“构建(Build)”。很多人认为“构建=编译+链接(Build=Compile+Link)”,Martin在第一版中指出一次成功构建包括:

  • 所有最新代码从配置管理工具中取出(check out或者update)。
  • 所有的代码从干净的状态开始编译。
  • 将编译结果链接并部署,以备执行。
  • 执行部署的应用并运行测试套。
  • 如果上述所有操作没有任何错误,没有人工干预,并通过了所有测试,我们认为这才是一次成功的构建。

实际上,目前很多团队对成功持续集成构建的定义基本上是符合上述定义的。这个定义的特点在于它是相对独立的,它是一个从干净状态的源代码最终获得可运行的通过验证的软件的过程。

Martin在第二版中则在成功构建的基础上给出了成功集成的定义。成功集成关注的不是一次“编译+链接+部署+验证”的过程,而是从开发流程的角度介绍一次完整的在持续集成约束下的代码提交过程4

  • 将已集成的源代码复制一份到本地计算机。
  • 修改产品代码和添加修改自动化测试。
  • 在自己的计算机上启动一个自动化构建。
  • 构建成功后,把别人的修改更新到我的工作拷贝中。
  • 再重新做构建。
  • 把修改提交到源码仓库。
  • 在集成计算机上并基于主线的代码再做一次构建。
  • 只有这次构建成功了,才说明改动被成功的集成了。

下图展示了Martin对成功集成的定义:

当然在第一版的“代码提交”这一节,Martin也提到了本地构建的概念,只是不如第二版这么明确。

配置管理

Martin在第一版中有两处提及配置管理,分别是:单一代码源(Single Source Point)和代码提交(Checking In)。第二版中则包括:通过持续集成构建特性(Building a Feature with Continuous Integration)、只维护一个代码仓库(Maintain a Single Source Repository)、每人每天都要向主线提交代码(Everyone Commits To The Mainline Every day)、每次提交都应在集成计算机上重新构建主线(Every Commit Should Build the Mainline on an Integration Machine)。不仅条目数量上增加明显,作者提出的很多实践都是基于配置管理来讲的。

工具

配置管理是持续集成的输入。在第一版中作者所推荐的配置管理工具是CVS,到第二版中作者推荐的配置管理工具已经换成了SVN5(参见第二部分中的配置管理工具部分)。

分支策略

实现进度与质量的平衡是配置管理的重要目的。Martin在第二版中对滥用分支给出了警告:

尽量减少分支数量。典型的情况是保持一条主线,……,每个人都从这条主线开始自己的工作(对之前发布版本进行Bug修正或者临时性的实验都是创建分支的正当理由)。

但是这里给出的建议对于大型团队来说并不十分合适。我们将在第二部分对于配置管理的分支策略进行详细描述。

内容

Martin在第一版中给出的原则是:

任何人都可以找到一台干净的机器,连上网,通过一个命令就可以取得要构建所开发的系统需要的所有源文件。

第二版中的原则增加了对构建的支持6

任何人都可以找到一台干净的机器,做一次取出(checkout)动作,然后对系统执行一次完整的构建。

分阶段构建(Staged Build)

分阶段构建是Cruise(已经更名为Go)引入的重要概念。其主要的意义在于:

  • 分离关注度不同的验证阶段,比如Commit Build和Regression Tests,团队会对不同的验证阶段采取不同的策略
  • 构建流程可视化
  • 通过分阶段并发构建来缩短反馈周期

当构建的时间过长时,我们通常会要求开发人员只运行速度较快的价值较高的构建阶段就可以继续自己的开发任务,而不必等待漫长的次级构建完成。这里作者提到ThoughtWorks不同的团队有很多有趣的实践,我们将在第二部分向读者介绍其中的一部分。

报告

作者在第二版中专门拿出一节“每个人都能看到进度(Everyone Can See What’s Happening)”来介绍有关持续集成报告的内容。因为:

持续集成的目的是为了沟通。

这是第二版相对于第一版来说一个非常明显的变化。在第一版中通知的手段还主要是电子邮件,实际上在作者撰写第二版的时候,ThoughtWorks已经不赞成将电子邮件作为主要的持续集成通知工具了。更好的沟通工具包括音乐、熔岩灯、显示器等。

对于沟通的重视从工具的角度也可以体现出来。Cruise Control最主要做的事情是任务调度,在报告部分做的相对来说非常粗糙,比较有价值的报告大部分是从Cruise移植过去的。Cruise在从一开始就非常重视这一点,通过Cruise你可以非常清晰地知道,代码发生了什么变化、正在进行的构建的状态和历史构建的状态。网页的形式对于分布式团队来说具有不可替代的优势。

正如我们前面所说的,音乐、熔岩灯等物理手段,具有更强的信息辐射能力。站起来往周围看一看就知道哪个团队的构建成功了,哪个失败了。

持续部署

持续集成实践有一个基本的思想就是:越是痛苦的事情,越要经常做。集成之后更令人心惊胆颤的事情就是——部署。部署到生产环境的流程通常要严格得多,然而所有的工作必须经历了生产环境的验证才算是成功的,所以——持续部署才是王道。Martin在第二版中建议:

你应该有一个脚本帮助你很容易地将系统部署到生产环境中去。……同时要特别考虑的是要能够自动回滚。

引入持续集成的建议

作者在第二版中特别给出了逐步引入持续集成的建议。包括:

  1. 引入版本控制。
  2. 实现自动化构建。
  3. 添加自动化测试。
  4. 加快提交构建。
  5. 寻找帮助。(比如ThoughtWorks)

第二部分 持续集成领域的新进展

正如前文所说,ThoughtWorks中国公司在过去的几年里面对于持续集成实践和帮助客户实施持续集成都积累了很多的经验,同时在理论体系方面也更加丰富完整。这也使ThoughtWorks在这个领域继续保持了行业领先的位置。

正如我们在第一部分讲到的,持续集成不应该只作为一个孤立的实践来应用。我们的经验表明如果只把持续集成作为一个孤立的实践应用很难从持续集成长期受益。持续集成往往进入“长红”或者“长绿”的不正常的状态。长红意味着系统长期无法集成;长绿则往往意味着缺少足够的验证。为了术语上的澄清,我们明确地将持续集成的定义区分为狭义的持续集成和广义的持续集成。

狭义的持续集成:基于某种或者某些变化对系统进行的经常性的构建活动。> 广义的持续集成:软件开发团队在上述活动的约束下所采用的开发流程。

狭义的持续集成

一般来说,狭义的持续集成包括如下几个方面:持续检查、持续编译(链接)、持续验证、持续部署、持续基础设施、持续报告等6个方面。

持续检查

持续检查的目的是保证代码风格一致,主要关注于代码的静态质量和内部质量,比如变量命名方式、大括号位置等等。大部分的现代集成开发环境(IDE)都具备实时检查代码质量的功能。为了保证主线上的代码质量能够达到一致的标准,应当在持续集成脚本中加入静态检查阶段。比如,Java的PMD、 FindBugs等等。

持续编译

持续编译是一个很朴素的想法,就是保证主线上的代码始终处于可编译的状态。但是这对于很多大中型团队来说却并非想当然的简单。这是因为很多团队并未采用集体代码所有权策略,导致存在依赖的团队的代码无法编译。针对这样的问题,一方面我们建议采用集体代码所有权;另一方面,对于确实因为安全原因需要隔离的代码应该边界、明确接口,很少存在大部分代码需要对大部分人保密的情况。

持续检查和持续编译是持续集成中最基本的验证手段。

持续验证

持续验证的目的是检查主线上的代码是否能够实现所要求的功能,或者已有的功能是否被破坏。在大部分的构建中,验证部分是耗时最长、成本最高的部分,但同时也是收益最大的部分。在这个阶段,我们看到的主要问题是验证不充分和验证时间过长。针对这样的问题,我们通常采用分层分级的持续集成策略。后面有详细的描述。

持续部署

对于大型软件应用来说,部署往往是一个费时费力又容易出错的步骤,因为这里面涉及到数据迁移、版本兼容等各种棘手的问题。持续部署的思想是将这些工作标准化自动化,使其能够可靠地、自动地、快速地运行。持续部署是当前DevOps运动中的热门话题之一。

持续基础设施集成

现代大型软件开发,尤其是互联网应用开发中经常依赖于一些常见的基础设施——比如Spring、Tomcat、Database等等。这些基础设施发生变化的时候,我们应当及时地触发持续集成,以确保我们的系统是能够与新的基础设施一起工作的。

持续报告

报告是将持续集成的状态以适当的形式展现给干系人的基本手段。报告是持续集成的晴雨表,所以它必须直观、易懂,而且对关注点不同的角色展现不同的内容和粒度。比如,开发人员可能更关心错误的日志;项目经理可能更关心测试覆盖率;产品经理可能更关心持续集成通过率的趋势等等。

广义的持续集成及持续集成策略

当要把持续集成实践应用到团队的时候,有很多额外的技术或者非技术因素需要考虑。

组织结构

持续集成是一个重要的沟通工具,而开发过程中两个最需要紧密沟通的角色就是开发和测试。在我们常见的组织结构中开发和测试往往隶属于不同的部门,甚至这些部门隶属于不同的高级经理。这往往会给持续集成的推广带来很大的阻力。这是因为持续集成从环境搭建到运行维护都需要两种角色的通力合作。我们的经验是这类涉及到人力资源的事情除非某一级“共同的大老板”出面,否则是很难协调的。“借调”这样的方式往往不能保证效果。

流程

放到团队的角度看待流程应当更加关注于各个成员之间的配合。每个开发人员提交代码之前应当确保是经过本地构建的;开发人员在提交之前应该确认主线上的代码是通过了持续集成的;测试人员测试的版本应该是通过了某次持续集成的,并且有相应的具体版本信息。

为了保障流程的顺利执行,我们还经常采用持续集成看板、提交令牌等辅助手段。

环境

环境是指持续集成运行时所依赖的软件和硬件的集合。我们经常遇到的一个问题是,软件在一台机器上能够通过持续集成的验证,而在另一台机器上则不能通过。这通常是因为我们对持续集成环境的定义不明确造成的。所以在搭建持续集成和在组织内推广持续集成的时候,我们需要特别注意持续集成环境的标准化,明确指出持续集成运行时依赖哪些第三方库,机器配置如何,端口和网络状况如何等等。

我们经常采用将持续集成环境加入配置管理的方式来解决环境标准化的问题。

分层

在大型团队(超过100人)中,扁平的开发组织结构是运行起来是比较困难的。常见的做法是按照特性,将团队划分为10人左右的小团队。一般来说,如果团队数量超过10个,还会再增加一层架构。这时候,配置管理的策略也应当做出调整。常见的做法是为每个团队拉出一个分支,设置一个集成分支用于将各个特性分支的内容整合在一起。需要注意的是,这里每个分支都应该具备所有代码的访问权限,也就是所有分支是同根的、等价的。

图片来自:http://damonpoole.blogspot.com/2008/01/multi-stage-continuous-integration-part_05.html

测试分级

在实施持续集成的时对于测试的类型应该有比较明确的定义。一般来说,我们经常把测试分为三级——单元测试、集成测试和系统测试。这是一个很大的话题,这里只是说明此处的单元测试并不是指针对函数的测试。虽然单元测试主要是函数基本的测试,但是每个单元测试应该针对的是特性或者对应代码在实现该特性上所发挥的作用。

单元测试的运行速度通常非常快,应该在数秒到数分钟。单元测试应该覆盖绝大部分的特性需求。集成测试单个测试的运行速度通常会比单元测试慢一个数量级,比如存在文件读写或者其他的IO和网络操作。集成测试主要用于保证系统的各个组件之间的调用是工作的。系统测试的运行速度一般会比集成测试慢,通常需要将整个系统运行起来,比如Web开发中的selenium测试。系统测试主要用于测试系统的正确路径(Happy Path)可以工作。有的团队会开发很多基于整个系统的回归测试,也属于系统测试。就场景的覆盖而言,单元测试>集成测试>系统测试,从运行时间来看则相反。

持续集成的成熟度评估

在帮助客户实施和推广持续集成的过程中我们逐渐总结出一些原则,帮助客户评估现状,分析和设计未来的目标。该评估方法借鉴了ThoughtWorks敏捷成熟度模型中有关配置管理、测试、构建等内容,并做了适当的简化。

构建:

级别 描述
3+:对外防御的 团队能够根据自己的需要,协调其他团队的持续集成,当依赖的其他团队的代码和组件或者第三方库和基础设施发生改变时自动进行构建。团队对于外部依赖的可靠性有信心。
3:对内防御的 构建是自动的。由测试和检查来保证团队内部的开发质量。持续集成的修复具有最高的优先级。团队对持续集成的结果有信心。
2:频繁的 构建是自动的,而且构建的速度较快。构建的触发条件是明确的,通常每次代码提交都会触发构建。团队中的每个人都会触发构建,并且了解构建的状态。
1:重复执行 构建是自动的,但是执行的不够频繁。构建的触发是随机的或者频率是非常低的(低于每天一次)。构建的速度通常非常慢,比如一次构建超过半个小时。
0:可重复的 主要依赖于手动的方式构建软件,但是每次构建的方式都是相同的或者相似的。通常有相关的文档的指导。经常团队指定某个人负责构建软件,虽然大部分人都能够做这件事情。
-1:手动的 主要依赖于手动的方式集成软件。每次集成的方式可能不一样。经常团队中只有个别人能够将软件集成起来。

测试:

级别 描述
3+:全面集成的 全团队对测试负责。测试驱动整个开发过程。测试与构建完全集成。
3:测试驱动的 业务分析人员和开发人员均参与测试。测试在构建过程中自动执行。开发人员实践测试驱动开发。
2:集成的 开发人员参与测试。部分测试集成在构建过程中执行。大部分测试在软件开发过程中执行。
1:共享的 开发人员参与测试。测试并未集成在构建过程中。部分测试在软件开发过程中执行,大部分测试在软件开发结束后执行。
0:审查的 测试由专门的测试人员负责。有部分测试是在软件开发过程中执行。但大部分测试在软件开发结束后执行。
-1:独立的 测试由专门的测试人员负责。仅在软件开发结束后执行。

配置管理:

级别 描述
3+:企业级的 企业有统一的配置管理策略。
3:跨项目的 配置管理在多个项目之间协调。
2:自动的 配置管理策略与持续集成策略紧密结合,团队成员有频繁提交的意识。一般采用乐观锁策略,原子提交。
1:集成的 版本管理下的内容是受控的。通常在版本管理中的代码是可以编译的。开发人员能够访问到自己工作所需要的代码。开发人员按照一定的规则访问配置管理工具和提交代码。一般采用悲观锁策略。版本管理工具和构建过程集成在一起的。
0:基本的 有基本的版本管理。但版本管理下的内容不全面,或者不足以支撑团队的开发。
-1:无配置管理 没有配置管理。或者使用方式完全错误。

常用实践和工具

持续集成看板7

问题:

我们需要让整个团队能够方便快捷的了解持续集成的状态。

上下文:

在完成了基本的构建基础设施的搭建之后,我们需要让团队成员及时获得持续集成的状态信息。传统的邮件方式可能会使人厌烦,或者错过重要的构建信息。

解决方案和实现:

安装一个显示器,将构建的状态信息以简明的方式展示在显示器上。将显示器放置在团队所有成员都能够很容易看到的地方。

个人构建中心

问题:

在某些情况下,构建环境的成本很高,而我们需要每一个开发人员提交之前完成一次个人构建。

上下文:

有些测试是依赖于设备的,而这些设备非常昂贵,并且配置复杂,所以无法给每个开发/测试人员一套构建环境。我们发现根据本地构建的理论模型,每个人的提交应该是串行的,这样我们实际上可以做到让这些人共享一套环境,但是从逻辑上就像是每个人有一套自己的环境一样。

解决方案:

在运行个人构建的时候将本地的代码同步到一台共享的机器上执行构建,构建完成后结果反馈给提交这次个人构建的人。

实现:

个人构建中心的实现有很多种方案。这些方案的区别主要在于如何将代码同步到个人构建中心服务器上。两种常见的方式:一个是使用rsync或者类似方式同步;另一个是使用分布式配置管理工具如git/hg同步。其拓扑结构如下:

第一种方式相对独立灵活;第二种方式稳定、高效,但是对于配置管理工具有依赖。

后果:

个人构建中心节省了大量的计算资源,同时也容易使得中心服务器成为单点失败的源头。一旦中心服务器出现问题,可能会导致团队的流程受到较大影响。

提交令牌

问题:

在实施本地构建的时候,向目标分支的提交应该是串行的,以避免构建被破坏后难以定位问题来源。但是团队往往缺乏一种有效的机制来保证这种串行。

上下文:

有些团队试图通过技术的手段来解决这个问题,比如通过配置管理上的锁机制,这种方式和乐观锁模式有较大冲突。有些团队通过团队内部沟通的方式解决,比如谁提 交之前都会通知别人,或者通过持续集成监视器来了解当前的构建状态,以决定自己是否可以提交。这些方式各自有各自的适用情形,较容易理解。

解决方案和实现:

使用一个实物作为令牌,准备提交的代码的人首先取得令牌,当代码提交完成(包括相应的提交构建)之后,将令牌交还。令牌要醒目,并且移动方便。小型奖杯、毛绒玩具、较大的头饰(如下图)都是不错的令牌。

分阶段构建

问题:

在某些团队中完整构建所花费的时间可能很长,如果每次提交都运行完整的构建会浪费很多时间。

上下文:

随着持续集成的日益完善,我们往往会发现验证所花费的时间越来越长,而大部分验证趋于稳定,失败的情况很少见。通过技术手段缩短构建时间是解决问题的根本办法,但是缩短构建时间是一个耗时耗力的工作,很难短期内见效。

解决方案和实现:

将构建分为几个阶段执行,在本地构建中仅执行速度比较快、可信度比较高、出错概率比较大的验证。利用晚上或者其他合适的时间执行全面的验证——我们这次构建称为全量构建。需要注意的是,这种情况下仍然要保证提交构建和本地构建的一致性。

iAnalysis

iAnalysis是一款轻量级的持续集成报告工具。该工具的核心思想是将持续集成构建过程中产生的数据以趋势和对比的方式展示出来。正如前文所说,我们在2009年的ThoughtWorks Away-Day上讨论了敏捷度量的话题,大家最后一致认为,数据有两种最基本的用法——横向对比和纵向对比。横向对比就是不同的人、不同的团队之间对比;纵向对比就是现在和过去对比。iAnalysis正是这种思想的体现。

关于作者

肖鹏,ThoughtWorks资深咨询师,程序员,敏捷过程教练。拥有7年以上软件开发实践经验,多次担任大中型企业敏捷流程改进咨询和培训,服务对象涉及通信设备制造、通信运营、互联网行业等。他关注于设计模式、架构模式、敏捷软件开发等领域,并致力于软件开发最佳实践的推广和应用。他曾参与翻译《Visual Studio 2005技术大全》,主持翻译《面向模式的软件架构》第四卷和第五卷等图书。


1 目前Cruise的开发任务已经不在中国公司了。

2 实际上这篇文章介绍的是Matthew所在团队在2000年早些时候已经在使用的实践,ThoughtWorks全球CEO郭晓先生当时也在这个团队。

3 为了与ThoughtWorks常用的术语保持一致,部分术语与雷镇和熊节同学所译略有差别,下同。

4 笔者对其格式略作处理,与原文稍有出入。

5 Martin最近在自己的一篇博客上对几种流行的配置管理工具做了对比。

6 注意,本文并非为指出第一版的缺陷,只是通过对比来说明作者论文中重点的变化。

7 这里只是借用“看板”这个词的字面含义,与精益中的看板有区别。


感谢张凯峰对本文的审校。

本文原文发表于InfoQ:http://www.infoq.com/cn/articles/ci-theory-practice

Share