序
从2012年决定研发ZCIP(ZSmart Continuous Integration Platform)开始,持续集成实践已经走过5个年头。
回顾这5年的历程,找个合适的词来描述的话,就是“结硬寨,打呆仗”,这是曾国藩打仗的套路,用最简单的方式逐步推进,让开发人员慢慢养成习惯。可以说现在ZCIP已经是研发线最重要的生产系统之一,所有研发人员都养成使用ZCIP的习惯,对质量的提升起到潜移默化的效果,最近已经连续几个月没有外场严重故障发生了。
1. 决定自行研发ZCIP
2012年在公司的管理干部读书班上,母公司的讲师介绍了持续集成的概念,故事中一个普通开发人员摆脱了加班查故障的噩梦的美好前景激励着我们也开展持续集成。
引入一场变革,如果对于其效果并不确认,可以先试点导入,验证有效后再全面推广;如果确信是正确有效的事情,也可以跳过试点,直接全面推广,比如Salesforce的敏捷推行。
持续集成是应用最广泛的敏捷实践,其效果也早已得到验证,所以就没有必要再试点了,领导拍板直接在研发线全面推行。
问题是,依托于什么工具进行?市面上的持续集成工具有很多,有开源的也有商业的,比如hudson(现在更名为jenkins),ThoughtWorks的Go(现更名为GoCD,并且也已经开源)等。
jenkins的优点是开源,插件丰富;GoCD的管道式持续集成是其主要特点,但是是商业工具,不免费。并且如果遇到定制需求,都需要进行一定的定制开发,同时在度量数据、报表展示方面都存在一些弱项。综合比较并结合我们自己全面推广的诉求,结合我们的体量(1000多人的软件公司在国内也不算是个小公司了)工具组(当时还不是支撑工具组,而是部门里的测试精英团队中的一个小组)决定开发一套自己的持续集成平台。
当时做出这个决定,因为要有一些研发投入,所以还是受到了研发线不少质疑的,其实当时我心里也是有点打鼓,好在领导支持工具组的调研和决定,给予了这部分研发预算,工作得以展开。
2. 可用的产品是衡量进度的唯一标准
回想ZCIP的研发过程,其实它是遵循了敏捷原则的,先做一个最小可用产品,投入使用,不断收集反馈和改进。
工具组的热情高涨,很快(印象中是一个月左右)就做出了第一个可用产品并投入使用。
持续集成平台的原理其实比较简单,就是一个任务调度平台。第一版做出了一个框架,明确了server, agent, 节点等概念。server是主控机器,所有接入到ZCIP中的机器(计算节点)需要在ZCIP中注册,并安装agent;节点是在agent上运行的任务类型。随着持续推进的要求,我们就不断地扩充节点类型。节点区分标准节点和非标准节点,标准节点是结合推进规范(后来制定的)制定的强制要求,会有对应的审计;非标准节点是结合研发线同事的具体定制要求添加的,不做审计要求。
节点类型有很多,包括编译(C++、Java)、代码静态检查(cppcheck,CheckStyle/PMD/findbugs等)、单元测试(gtest/JUnit),数据库脚本更新执行、版本打包、自动部署等。最初支持的节点,应该只是编译,这样就可以支撑DailyBuild了。在推行过程中,业务团队根据自己的需求也扩充了一些节点,比如检查多语言资源文件一致性的资源文件检查节点等。
3. 勿以善小而不为
因为遇到过太多次现网重大故障,程序崩溃是由于一些低级错误引发的,而这些错误早已在编译时由编译器报告了。虽然当时已经组织规范小组制定了统一的代码规范,但没有有效的工具对所有研发团队的版本进行检查。持续集成推进的最初动力其实来自于把编程规范真正落地。
作为C++程序员,最初自己只知道可以利用编译器的高级选项来检查错误,在《C++编程规范,101条规则、准则与实践》,《Effective C++》,《Writing Solid Code》, 《Pragmatic Programmer》,《Code Complete》, 《高效程序员的45个习惯》等多本编程书籍中都强调重视编译告警,没有什么比花费了半天时间跟踪一个故障,最终发现编译器早就已经告诉你原因更让人沮丧的事情了。编译时先消除所有告警,确保所有已知的故障都已经规避了,在这个坚实的基础上再进行下一步,这是我自己形成的习惯。在一个小团队中通过持续的宣传让大家都具备相同意识和习惯是可能的,但是面对一个几百人的部门,这种方式就很难奏效了,这时依托于工具推进是有效的方法。
2012年时从客户处得知已经有一系列的代码静态扫描工具可以帮助提升质量,将软件内部质量揭露出来,于是开始在实际工作中应用这些工具。我们选择了一系列软件(开源的居多),包括cppcheck(C++), CheckStyle/PMD/findbugs(Java), FxCop(.net)。使用这些工具对代码进行扫描,发现了成千上万的告警,其中有很多严重告警。
当然编译器检查也是一种很有效的方式。规范小组组织讨论形成了标准化后的编译选项,也是依托于ZCIP进行相应的检查。
信任,但要检查。上述每个工具在ZCIP中都被封装为一个单独的标准节点,在部门中强制推行,要求各个团队自行制定计划进行整改。
4. 及早采取措施,但措施的力度要小
推行这件事情是有阻力的,因为表面上是增加了开发人员的工作。
对于一些历史版本,系统已经稳定运行了很多年,即便存在一些严重告警和隐患,研发团队也不愿意投入时间和精力去修复这些故障,大多数研发团队信奉的原则是:只要系统没有坏,就不要去碰它。这当然是有一定道理的,
推行初期为了让大家形成质量意识,采取的是运动方式,目标是希望能够短期内把已知的告警消灭掉,但未能达成预期效果,反而因为批量修改代码引发过几起一级故障,其实和当时代码走查机制不健全也有关系。有好几位项目的客服人员跟我说:听说开发部在推什么CheckStyle,导致我这个项目发生故障了,我这个项目能不能不要搞CheckStyle了?
随后更新了推进原则,本着质量不恶化的原则,要求所有的代码检查节点告警不能增加,新增告警会导致持续集成失败。推行统一的规则就体现出ZCIP的好处了,支撑工具组修改标准节点的逻辑,就实现了对所有流程的的统一要求。同时推行的阻力也小了很多。
这个规则推行一段时间,开发人员对规范逐渐养成习惯之后,调整规范要求,符合《精益软件度量》中提到的紧箍咒原则:每次构建时告警数量必须比前一次减少。这要求开发人员不仅保证自己修改的文件没有引入新的告警,还要消除之前代码的至少一个告警。只要告警数呈现向下的趋势就好。
上述规则再次推行一段时间后,要求再次收紧:对于产品类项目,因为暂时没有面向生产的压力,要求要高一些,在研发期间要求达到0告警;对于交付类项目,仍然沿用每次构建要求告警下降的原则。
1年之后,因为低级错误(比如字符串越界)造成的现网程序崩溃的故障数量明显下降了,开发人员的质量意识都得到了增强。
除了这些语法告警之外,ZCIP中还检查重复代码和圈复杂度超标的告警,这两项检查也是遵循要求逐渐收紧的原则。重复代码的检查阀值从200逐渐降低到20,节点的规则要求阀值不超过20,少数有更高要求的团队甚至把阀值设置为7;圈复杂度的阀值,起初是19,后来降低到15,现在要求是10,少数接受了CleanCode理念的团队有更高的要求,希望降低到5或者3。后续的推进目标就是CleanCode了。
这些小步的持续推进,都是依托着ZCIP的定制研发实现的。
5. 速度,速度
持续集成要求给开发人员快速反馈,让人在计算机前等上几十分钟才看到结果是不可接受的,所以持续集成推进过程中想了很多办法提升速度,力求在10分钟内让开发人员得到反馈,底线是15分钟。对于构建版本并进行全量验收测试的持续交付流程,要求的执行时间是在8小时内完成,这样就可以在每天晚上运行,第二天早上来查看结果。
更换更强劲的机器
这是最简单其实也是最经济的提速方案,因为人力始终比机器贵。
起初各个业务单位各自申请更好的机器资源用于编译,后来逐步推行统一机房管理,虚拟化,现在在向云CI方向努力。编译速度最慢的是flex代码,在HTML5还在争论标准化的时候,一次产品技术选型我们选择了flex,所以现在有不少版本都是用flex做前端。不过flex编译速度是很大的一个弊端,如果flex没有停止演进,估计在编译性能提升上会有比较好的表现,现在停止演进了,目前能想到的最好办法就是通过加强机器来提升性能。通过更换机器,增加并行编译等方式,一个项目的flex源码编译从90分钟缩短到30分钟。
增量编译和检查
用增量方式替代全量方式,显然也是能提升速度的。
用scons替换make,scons判断文件变更的机制和make不同,能够更好的支持增量编译。为了提升代码检查的速度,我们对cppcheck, CheckStyle, PMD这几个工具都进行了修改,实现了增量检查,大大提升了速度。
优化测试用例,提升系统可测试性。公司有一个自研的自动化测试平台ZTP,可以基于它进行接口级和系统级的自动化验收测试。高效的测试用例高度依赖于系统的可测试性设计,如果系统没有提前做相应的设计考虑,那么测试用例就往往只能采取忙等的策略,执行结果不稳定,同时还消耗大量时间。通过推行自动化测试,逐步的改进系统以更好的支持自动化测试。
6. 持续是一种坚持
持续集成中最难推行的是单元测试,尤其是针对有十几万甚至几十万行代码的遗留系统,记得曾经有位同事和我说过:“你让我搞单元测试,你先找一个能做起来的项目给我看看。”
如同ZCIP的口号:“持续是一种坚持”。虽然艰难,但是在单元测试推进的道路上我们始终在努力前行。
单元测试的推进经过了如下几个阶段:
首先是在平台框架产品上推行,因为平台框架产品相对独立,和其他产品的耦合性低,单元测试相对容易进行。
在平台框架产品有了单元测试的标杆(代码行覆盖率达到75%)之后,再找一个业务产品推进,形成标杆,然后再大规模推广。目前我们正处在这个阶段中。
当然在推行过程中不能只看数字指标,还要关注测试用例设计的质量,包括测试命名,测试代码的可读性,测试有效性等。让团队写出有效的测试用例而不是虚假的测试用例,不能把单元测试变成维护负担。
同时要关注分层测试中各层用例数的比例。剥离对数据库、IO等的依赖,实现传统意义上的单元测试,这样执行速度是最快的,但是往往接口级的集成测试用例是性价比最高的,在持续集成推行过程中,需要在这两者之间寻求一个平衡。
单元测试节点执行的成功标准也是随着推进过程不断在提高中,这当然也是依托于ZCIP的快速定制开发实现的。
7. 绩效 = 能力 * 纪律
提供了基础设施,但如果不遵循严格的纪律要求,CI仍然难以发挥作用。
CI的纪律要求是团队把CI失败作为第一优先级的事情处理,体现了精益的自働化原则,但并不是所有团队成员都理解这一点。面对几百人的团队,难以逐个说服教育,还是要依托于ZCIP工具和相应的度量审计来推行CI纪律。
该纪律的指标是构建失败一小时修复率,当CI失败暴露了问题时,修复的难度和软件的规模相关,集成的产品越多修复时间越长,所以这个指标对研发团队还是造成了一些压力,对于促进研发习惯的变化起到了较好的作用。
跋
《持续交付》译者乔梁乔帮主曾经说过:所谓敏捷,把持续集成持续交付这些工程实践做到炉火纯青,自然就敏捷了。
我们正走在持续交付和DevOps的路上,路漫漫其修远,不过方向对了就不怕路远。下一站是云CI,让开发人员摆脱机器资源申请和维护的烦恼。
有人说过“没有自动化测试的敏捷都是假敏捷”,诚然工程技术实践是敏捷不可或缺的重要组成部分,不过敏捷推行中也不能忽略人的因素,参与者的积极性是更为关键和基础的,个体和互动还是最重要的。