“天下事头绪纠缠,兴一利必也生一弊。”
一句话,道破了改进难点所在。最近在项目中围绕持续集成做改进的时候,对这一点感受颇深。跌跌撞撞的一路走来。我们的持续集成的过程已经变得有些“个性化”,反过头来看我们一路的变化,非常有意思。
从项目的技术架构说起,我们的项目是采用的 J2EE+Flex 的方式进行开发的。在我进入项目组的时候,一个比较健壮的持续集成环境已经搭好了。工程分为两个,一个是 Java 后端的工程,一个是 Flex 前端的。我们的持续集成服务器是 CC。整个开发工作是围绕着持续集成展开的。一周为一个迭代。
那个时候,我们采用的是比较标准的方式:
- 后台采取 TDD 的方式开发。
- 每次提交代码之前更新所有代码,然后运行所有测试用例,全部为绿色的时候才提交。
- 前台 Flex 比较麻烦,所以采取了用功能测试覆盖单元测试的方式。用基于 Ruby 的 FunFx 写单元测试。工作方式与后台差不多,每次前台功能测试全部通过了才提交。
- 持续集成的流程是每隔 5 分钟检测一边代码库,有更新就 build。
- build 的流程是先编译后台,跑单元测试,单元测试通过了,再编译 Flex,将 swf 和 html 以及后台的文件打成 war 包,部署到 tomcat 上去,跑功能测试。
- build 成功之后发布到特定的目录,形成发布列表。有 war 包供人下载。
那个时候,build 一次大概是 15 分钟,因为 Check In Gate 环节是按照标准流程走的,所以 build 出错的几率也小。CC 大多数时候是绿的。哪怕偶尔出问题红了,也很快被修正了。
随着项目的开发,代码规模越来越庞大。功能测试越来越慢,比起自动执行脚本那种速度,开发人员更乐意手动点两下,加之上面对工作进度的压力。更改了一些工作方式:
- 后台的工作方式不变。
- 前台,将功能测试脚本的工作交给几个测试人员编写。几个测试人员也坐在附近,基本可以看作团队成员(到后来编制也是我们团队的成员了)。 开发人员人肉测试一下保证没有问题了再提交。
- 测试人员写脚本的流程是拿到上一次 build 成功的 war 包,在本地写脚本,本地测通过了再提交。
- 持续集成服务器上功能测试不通过的时候,由测试人员提交 BUG,开发人员修改。
- 持续集成的流程依旧。
这样,从后来收集的数据看,工作效率是提升了,因为参照以前的统计,开发人员的工作一下减轻了 1/3。以进度来衡量的速度自然很轻易就可以让上级满意了。
新的解决方案总会产生新的问题,测试人员在测试方面的专业性,使得他们写出来的脚本测的更细。功能测试的时间占耗,增长的更快了,短短半个月,就增长到了 1 个小时。每当出现问题,作出反应之后,要在 1 个多小时以后才能知道结果。而且,持续集成方面并没有做到,一旦出错,谁也不能提交代码这么严格。模块化的设计所带来的假想的安全感和进度的压力,使得开发人员修正问题的激励不高。于是修正问题的速度不如产生问题的速度快。持续集成服务器在那两个个礼拜里只有两头是绿的,周一早晨和周五下午。
最早,我们发觉,由开发人员重构造成的脚本失败占大多数,而测试人员每次拿到的上一个版本是没有错误的。所以会出现自动化脚本本地跑得过,服务器上跑不过的情况发生。于是我们修改了发布的逻辑,在后台单元测试通过、flash 编译完成的情况下打的那个 war 包,复制一份,放到某特定目录指定为 current build。供测试人员写测脚本使用。
过程改进之后,测试人员可以快速的修正脚本了,虽然对于开发人员重构造成测试人员工作的返工无疑是一种浪费,但是毕竟自动化的测试省了回归测试的不少时间,还是可以接受。
脚本的修正速度解决之后,工作似乎有了些起色,但很快,问题的本质就暴露了出来–build 的时间太长了,修得速度还是跟不上问题产生的速度。尤其是中间缺少当 build 失败时强制阻止代码提交的环节。这之后依然是周一和周五两头绿,中间都是红的。于是,我们觉得问题还是出在 build 速度上。我们人工的将功能测试脚本分到四个 suite 里去,然后以多线程的方式运行。速度被提高了 4 倍。于是又消停了两天。
好景不长,多线程的测试似乎不太稳定。很多本地可以跑通的测试用例,到了服务器上就失败。险些一个礼拜都没有 build 出一个版本。最后不得不改回单线程。这时,build 一次已经占到了 100 分钟。第一期的产品 Backlog 还没有完成 1/3。
持续集成走到这里已经进入一个困境,有必要做一些更深一步的改进。经过多次讨论,归纳出了几套方案:
- 分冒烟测试和 all test 两套测试用例集是我们当中呼声最高的一种方案,当我的代码提交之后在跑完所有单元测试和基本的冒烟测试之后就发布 beta 版,由测试人员接到 beta 版,进行更细致的自动化测试并带一些人肉测试。但是反对的声音认为,不跑完全部的测试用例就失去了持续集成的意义。而且会更降低开发人员修正 Bug 的积极性。于是作为修正,支持的声音则提出,在 Check-In Gate 处把关,恢复每个人提交代码之前跑测试用例的实践。可这明显会给开发人员带来更大的工作负担,估计以此时的进度压力,开发人员的安全感肯定会大幅下降。很可能会推行不下去。
- 另一个方案是从细节处调优,把 WEB 应用部署到另外一台机器上去,或许就会稳定一些了。但是反对的声音认为,以测试用例的这个增长速度,他早晚会不稳定的,而且可能撑不过两周。作为修正,想考虑分布式,但是我们所有人的知识储备中,并没有一个人清楚 CC 有没有分布式能力。所以想的是购买 Cruise,但是价格的障碍就摆在眼前了,在项目前景还不是很明朗的情况下,估计很难申请到资金,但也不是不可能,只要我们敢于冒这个风险。
- 第三,便是更为高级的分支式开发,将版本库划分一下分支,以分支来搭配持续集成,以分支合并来触发自动构建。这样做,开发的过程就更加有板有眼,粒度可以划分的更细。可是分支的划分,一时想不清楚。但是假设想清楚了,似乎这也使得我们的工作流程更加复杂了,做如此之大的改变,风险有多大?效果有多大?成本有多大?到底是值不值得?一时也想不清楚。
方案有了,听起来都很有道理,也都有问题。该如何做出决策,是个问题。现在大家众说纷纭,都有理,就变得难以抉择。而且到现在了是不能随便尝试的,这种尝试也是一种风险,一旦出问题造成的成本上升都会加大我们身上的压力。
迷茫之下到 AgileChina 上跟大家讨论了一下。非常高兴的收集到了几方面的建议:
Jeff Xiong 觉得可以将测试分级,并将 build 分为两个环节,一个跑基本的用例,一个跑全部的用例。这跟我们的第一套方案的思路吻合。但是这种行为是不是失去了持续集成的意义呢?他也不是很确定,他说:
我也不确定……不过,不全面的持续集成至少比不能用的持续集成要好。哪怕一个 quick build(只?)能抓到 80%(70%?60%?50%?)的 defect,如果它只要很少的时间就能跑一遍,似乎值得这样做。 不过同时就需要(可能是专门的)人来关注 slow build 的健康状况,不然 broken functional tests 可能被忽视并累积。
因为是在论坛上,互相之间的交流容易造成理解上的偏差,我便阐述了一下我的理解:
嗯。。。也就是说分一个 fast build 和一个 slow build 然后有专人关注 slow build。 那我的理解,这个 fast build 应该是反映了后台的健康和前台与后台的基本集成的健康。主要用来完成保障集成的角色。
而 slow build 则是反映了从用户接口来看的软件的健康状况,定期回归,防止发生过的错误再次发生。
这样逻辑上看着很清晰,但是两者之间的同步。。。会不会有什么问题呢?
Jeff Xiong 认可了我的理解,并提出了更进一步的解释和建议:
slow build 实际上是运行完整的回归测试套件。当然理想的情况是 slow build 基本上不出错,因为 * 逻辑 * 用单元测试都覆盖到了,功能测试只是在描述 * 表现形式 *。那么因为 slow build 基本上不出错,就没有价值每次都去运行它,让它在后台慢慢的跑着,过一段时间(半天或者两小时)去关注它一下,没问题就好,偶尔出了问题就马上解决并且加上对应的单元测试。这样你既节约了时间又不会严重降低对质量的保障力度。 实际上的情况可能比较难这样理想,但是和所有好的环境一样,这个环境不是说一下子规划好就万事大吉的。你可能大概的分一分,然后不断的维护,在两组 build 之间交换测试案例,一些覆盖到大量功能的、经常出错的案例也许要换到 fast build 的冒烟套件里面,一些看起来永远不会出错的案例也许可以换到 slow build 去。一直琢磨这个事,它才会变得越来越好。
(题外话:最近我觉得稍微大一点的项目应该有比较专注的 build master,developers 往往并不是特别认真的考虑 build 和 CI 的持续改进。)
而胡凯则提出了一些基于 CC 采用分布式的解决思路:
CruiseControl 是支持一个叫做 Distribute 的 Contrib,它的 Idea 是:因为 CruisControl 支持叫做 Composite 的 build 方式,那么你可以起多个 build server 一起来 build 同一个项目,当且仅当所有的子 build 通过,整个 build 才算成功。 对于每个 build server,你是在单线程测试,对于整个项目,你却是并发测试,因为有多个 server 同时在跑测试。
但是他也说到 CC 毕竟是开源的东西,配置起来十分麻烦,于是最后也提出了采用 Cruise 的方案 ^_^:
或者你也可以尝试一下 ThoughtWorks 新发布的 Cruise, 基本的理念很相似, 都是把测试分布到不同的机器上执行。 在试用期内可以同时跑 6 个 Agent. 你可以在每个 Agent 上执行不同的 Target 比如
Agent1 : ant ft.suite.1
Agent2 : ant ft.suite.2
Agent3 : ant ft.suite.3 你可以拿来玩儿下,根 Open Source 的那个比起来,应该容易设置很多。缺点是:
- 你必须使用 hg 或者 svn 作为 SCM
- 试用期过后,你只有 2 个免费的 Agent
另外,糖醋鼻子还提到了分支式开发,大家围绕这个还展开了激烈的讨论,不过我考虑到分支式开发对我们的利可能要远小于弊,最终还是放弃了这个方案。
在吸取了两方面的建议之后,经过一番思考,决定开始两方面的准备,一方面,对测试用例的分级方面做了一些工作,重构了一部分用例的结构。另一方面我去调研分布式构建的实现手法。CC 的配置果然非常麻烦,调研期间发现了有 RemoteAnt 这个东西,试了一下基本满足我们的需求,考虑到一个 Agent 不用做的太过重型,于是就采用了这个方法。在分布式的技术调研已经完成的情况下,测试分级要不要做成了一个问题,但考虑到目前需求只作了 1/3,如果这个都抗不住要分级的话,后面的工作就没法做了,所以分级的事情,虽然做了,但是也暂缓实行。
现在实践已经采用,会不会产生新的问题呢?前文说过“兴一利必也生一弊”,这个事情是肯定的。那么问题就来了,既然弊端肯定会滋生,我们怎么知道作出的决策是正确的呢?
其实,倒回去看这一路走来的过程,除了一个可以运行的过程以外,还有一个很重要的收获,那就是:如何进行决策。而所谓决策,并不是在黑白分明的事情之间做出选择,而是在都有理的事情中做出选择。就像我们都知道做软件设计的时候,设计是没有好坏之分的,只有适不适应你的具体情况之别。所以当我们面临几套解决方案的时候,就好象面对几套设计方案一样,真的是很难选择。如何做?各自就有各自的思路了。像我们就吸收了敏捷开发的思想中所强调得不做过度设计。选择立杆见影的改进去做。同时,像前文所说,抱着拥抱变化的态度,相信兴一利必生一弊并不是坏事。相反,他可以从一定程度上,带领我们找到真正的问题。
作者简介:仝键,网名咖啡屋的鼠标,06 年大学毕业,普通程序员,专注于 Java、Flex 方面的开发、Agile 等软件开发方法论的学习。爱好参加社区活动。
志愿参与 InfoQ 中文站内容建设,请邮件至 editors@cn.infoq.com 。也欢迎大家到 InfoQ 中文站用户讨论组参与我们的线上讨论。
评论