虽然持续集成(CI)在降低项目风险方面极为有效,但是它对日常编码活动有更多要求。在这份由两个部分组成的持续集成反模式文章的第二部分,自动化专家兼
Continuous Integration: Improving Software
Quality and Reducing Risk 一书的合著者 Paul Duvall 继续介绍持续集成的反模式,并重点演示如何避免它们。
在这篇共两部分的文章的
第一部分,我描述了以下六种持续集成反模式:
- 签入不够频繁,这会导致集成被延迟
- 破碎的构建,这使团队无法转而执行其他任务
- 反馈太少,这使开发人员无法采取纠正措施
- 接收垃圾反馈,这使开发人员忽视反馈消息
- 所拥有的机器缓慢,这导致延迟反馈
- 依赖于膨胀的构建,这会降低反馈速度
这些反模式延迟或者阻碍了使用持续集成能够得到的好处。在第二部分中,我要介绍其他五种同样具有欺骗性的实践:
- 等到一天结束时才提交修改,造成瓶颈提交,通常造成破碎的构建,使开发人员备受挫折。
- 构建中只包含很少的自动过程,结果导致构建总是成功,造成持续忽视(Continuous Ignorance)和集成延迟问题。
- 没有频繁地通过代码修改构建软件,而倾向于定时构建,从而不利于构建修复。
- 相信代码能够在自己的机器上工作,因此只能在其他环境中发现问题。
- 没有清除旧的构建工件,造成环境混乱,引起误判和漏判错误。
再次强调,要得到持续集成的诸多好处,那就应该理解这些反模式 — 并避免它们。
名称:瓶颈提交
反模式:开发人员在当日工作结束前提交代码修改,引起集成构建错误,妨碍团队成员正常下班。
解决方案:全天频繁地签入代码。
瓶颈提交是签入不够频繁 反模式的一种变体。它认为每天只要至少签入一次代码即可。但是,问题在于每个人都在同一时间
签入。在 CM Crossroads 的一篇文章中,Slava Imeshev 描述了为什么大多数构建失败都发生在晚上 5 点到
8 点之间(范围再小一些,还包括午餐时间)。在 “五点钟签入(Five-O'Clock Check-In)” 中,Imeshev
将这种情况的发生与开人员倾向于每天结束之前签入他们的代码修改这种习惯联系在一起(请参阅 参考资料)。
五点钟现象处处可见
如果某个团队的规定是在所有修改都提交而且运行了构建之前不能下班,那么五点钟签入 将使您很快失去很多朋友。等待代码提交是件非常无聊的事情。想想如果构建出问题会怎么样?我想会有许多人打电话回家去解释为什么又得晚回家。
图 1 演示了软件开发团队签入的典型时间线。请注意库提交放在晚餐时间前后,在下班时间之前。
图 1. 瓶颈提交
当然,这个问题的解决方案就是频繁地提交修改。通过频繁提交,就会拥有更小但更频繁的集成构建,而且在出现构建错误时,错误会较少,而且更容易修复。
五点钟的言外之意很清楚,所以为了自己和团队都应该频繁地签入代码,让五点钟成为多数人的快乐时光。
名称:持续忽略
反模式:在构建成功时,每个人都认为生成的软件工作正常。但是,只有有限的构建中包含编译和少量单元测试。
解决方案:在版本控制库的每个修改上都要运行完整的集成构建。
有些时候团队会陷入一钟 “构建一个接着一个成功” 的安全错觉。实际上,当很少发生构建失败时,几乎可以确定开发团队可能正在应用持续忽略
反模式。 如果集成构建永远不失败,实际可能是对代码没有进行足够的校验和验证!构建所做的工作越少,对于软件实际是否按预期工作的了解
就越少。
全面构建的启示
图 2 左侧的堆栈演示的集成构建除了编译源代码、将类打包为二进制文件、在操作系统上部署软件之外,什么也没做。当然,这比永远不做集成构建要好。但是,请看右侧的堆栈,比较两个堆栈,就能看出像右边那样运行多个过程可以揭示的问题,例如测试和和数据库修改。
图 2. 全面构建可以揭示问题
流线化构建
通过引入额外的过程,例如数据库集成、开发人员测试(单元测试、组件测试、功能测试等)、自动代码检查(例如遵守代码标准的情况、圈(cyclomatic)复杂度、以及代码重复情况的检测)以及安装分布,就能更好地在开发周期中尽早确定软件是否能够真正工作。但是,请记住:在集成构建中添加的过程越多,反馈就越慢。所以,可能需要考虑创建一个构建管道,在运行完初次提交构建之后再运行那些比较慢的过程
— 这样做能够得到更快的反馈,并提供更灵活的软件验证机制。
名称:定时构建
反模式:构建每天运行、每周运行,或者按照其他安排运行,但是没有对每个修改都运行构建。
解决方案:对源代码库上应用的每个 修改都运行构建。
持续集成就是经常地集成软件资源 — 所谓经常,我的意思就是 任何发生代码修改的时候。据我所知,这绝对是及早发现问题的最快途径。好处一是节省资金,好处二是它可以经常发布更好的代码。
定时构建的问题在于它运行的时候并不考虑是否对代码库中提交了修改。这意味着运行这个构建可能毫无价值,因为从上次构建之后可能没有发生任何变化,也可能出现了太多修改,以至于很难理清出现的问题。
有效地执行持续集成需要具有主动性 — 在构建失败时,首要工作就是立即修复问题。定时构建的性质不鼓励这种主动性,所以会造成 “等出现问题时再修补”
的作法,而这正与持续集成的主张相违背。
将定时构建改成更加频繁的操作很容易,只要正确地配置一台持续集成服务器即可。例如,清单 1 演示的 CruiseControl
脚本每两分钟轮询一次版本控制库。如果发现修改,CruiseControl 就会运行构建。
清单 1. 每个修改都构建
...
<schedule interval="120">
<ant anthome="${cc.ant.dir}" buildfile="build-${project.name}.xml"/>
</schedule>
...
|
请不要误会 — 在某些情况下,在定时基础上运行的构建可能有用。例如,负载和性能测试就可以在夜间进行,因为运行这些构建要求的时间比较长。但一般的原则是:如果所有构建运行的频率低于每个代码修改都构建这一频率,那就限制了自己尽早发现问题的机会。
名称:在我的机器上能工作
反模式:只能在自己机器上运行的私人构建,造成日后才发现修改不能在其他环境下工作。
解决方案:团队使用集成构建计算机,在该计算机上对提交给版本控制库的每个修改运行构建。
假设做了一个代码修改并运行了构建(通过 IDE 或 Ant),在一切按预期运行之后,将修改提交到版本控制库。几天之后,有人将这部分代码部署到其他环境,发现所做的修改不能工作。然后您在自己的工作站上启动软件应用程序,结果一切正常,于是您辩解道
“但是,在我的机器上能工作啊!”
您遇到过这类情况么?如果还没有,请做好准备,很可能会发生在您身上。出现这种奇怪的情况会有许多原因,但典型的原因是忘记了将新文件加入版本控制库或者其他环境中不具备您机器上的特定配置。
超越桌面局限
名称:只用 IDE 进行构建。
反模式:在自己的工作站上使用 IDE(只在本地使用)运行私人构建,日后发现在其他环境中不能运行。
解决方案:创建构建脚本,并将脚本提交到版本控制库。对每个修改都运行这个构建脚本。
使用 IDE 编写代码或者创建构建脚本并没有错。IDE 能让工作变得更有效率。但是,如果构建过程与 IDE 耦合得过于紧密,以致于在部署环境下如果不安装
IDE 就无法运行集成构建,那么问题就大了。这是因为运营团队通常不在阶段环境和生产环境中安装 IDE。如果他们需要在这些环境上安装
IDE,那就可能需要对 IDE 进行手工配置(这会造成环境之间的不一致,造成构建错误)才能运行集成构建。而且,让构建依赖
IDE 会将配置问题的发现推迟到再次进入开发过程的时候,因为阶段环境和生产环境的 IDE 中清除了依赖项。
要解决 “只用 IDE 构建” 这一问题,要做的就是创建一个构建脚本(依然可以方便地通过 IDE 使用这个脚本)。然后可以将这个构建脚本作为集成构建的主控机制
— 不论什么环境。
开发人员目光短浅
名称:环境近视症。
反模式:认为构建在一个环境下工作,就能在所有环境下工作。
解决方案:在构建目标中定义构建行为;而且,将依赖环境的数据放在外部的 .properties 文件内。
环境近视症反模式反映了开发人员错误地认为自己的代码能在一个环境工作,自己的工作就算完成这种想法。因此,为了克服这种意识,持续集成系统不应该有任何先入为主的想法(在合理的限度内)。要减少这种想当然的想法并不难,只要删除特定于平台的约束并将它们变成可替换的,例如通过属性文件进行设置。
如果构建既要在 Windows® 环境又要在 Linux® 环境下运行,并且如果有对 C
驱动器的引用,那么就能推断出代码在 Linux 上会出故障。如果构建引用了依赖于机器的环境变量(例如
GLOBUS_LOCATION ),那么如果在另一个环境内没有设置这些变量,构建就会失败。在这些情况下,要做的就是将这些引用换成能够在构建的时候替换的变量。
清单 2 的 Ant XML 代码演示了如何包含拥有环境值的 .properties 文件:
清单 2. 在构建脚本中引用环境属性
<property file="${basedir}/TEST.properties" />
|
清单 3 中的属性显示特定于某个部署环境的数据。请注意这个文件通过将相关信息(例如数据库连接值、Web 容器的位置、主机名称、认证信息)放在外部,减少了构建本身的假设。例如:
清单 3. 在属性文件中定义数据
database.host.name=integratebutton.com
database.username=myusername
database.password=mypassword
database.port=3306
jboss.home=/usr/local/jboss/server/default
jboss.temp.dir=/tmp
jboss.server.hostname=integratebutton.com
jboss.server.port=8080
jboss.server.jndi.port=1099
|
所有构建行为都应该放在目标内(例如 Ant 目标)。在同一构建脚本中任何引用不止一次的数据都应该定义成属性。任何随着机器变化的数据都必须放在外部
.properties 文件内。如果遵循这个简单的建议,那么就会得到最大的灵活性,遇到的问题也会减少。
名称:染污的环境
反模式:运行递增式构建是为了节省时间。但是,旧的工件(来自前一个构建)会形成误判(或漏判)构建。
解决方案:在运行构建之前清除以前生成的工件。制作服务器和配置信息的基准。
清理旧的工件
没有比发现运行构建失败的原因仅仅是由于存在以前构建的工件这件事更让人郁闷的了。这类工件可能包括最近部署的 WAR 文件、不正确的
JAR 版本或数据库更新。更糟的情况是:可能因为在构建环境中存在以前生成的工件,因此使构建成功,从而让人产生误判。
在进行任何相关的构建活动之前要将环境置于已知状态,这一点再怎么强调也不过分。实际上,在构建软件的时候,我喜欢执行所谓焦土政策(scorched
earth)的策略。清单 4 这个简单的示例使用 Ant 的 delete 任务清除以前的日志目录、分发文件和报告文件。这样可以大大减少误判或漏判的机会。
清单 4. 通过 Ant 删除目录
<target name="clean">
<delete dir="${logs.dir}" />
<delete dir="${dist.dir}" />
<delete dir="${reports.dir}" />
<delete file="cobertura.ser" />
</target>
|
上面的代码很简单 — 清除环境的工作可能还包含清除旧的类文件和以前部署的 EAR/WAR 文件,将数据库恢复到已知的状态(例如,删除并重新生成数据库表),重新初始化类路径,避免使用环境变量。
制作部署环境的基准
“焦土策略” 让人想到可以采用更高级的技术,例如设置运行集成构建的操作环境的状态。要制作这些环境的基准,需要自动删除并重新应用每个组件和配置项。比起简单地删除旧文件,根据环境复杂度进行删除可能更加困难。不过,它可以逐步应用。组件可以是数据库、Web
或文件服务器,也可以是某些专有软件。配置项可以是环境变量或者数据库配置修改,例如数据库的内存分配。关键是保持一致。运行构建的每个环境都应该有类似的配置。通过制作部署环境的基准,就能更好地找到特定问题的原因。图
3 演示了一些可能需要在每个部署环境中清除并以类似方式重新应用的组件。
图 3. 清理部署环境
以我的经验而言,清理部署环境最大的好处就是提供了更快排除问题故障的途径。为可以比较的环境制作基准,就能对它们逐个进行比较,所以在环境转换之间出现问题时,就能更快地进行修补。
我希望您能了解到,虽然持续集成是在开发项目中使用的优秀实践,但是如果能够避免某些反模式,还能享受到更多持续集成的好处。开发团队使用其中一些实践是有其理由的,但是这些实践会造成反模式。例如,有些时候运行定时构建是合适的。此外,经常提交代码的有效实践实际上会造成瓶颈,但这并不意味着频繁提交是一种坏的实践。请记住,反模式从本质上讲并不是坏实践,但是在某些情况下,它们并不是良好的方法。
学习
获得产品和技术
讨论
|