使用功能开关更好地实现持续部署(2)

发表于:2013-12-04来源:InfoQ作者:崔力强点击数: 标签:部署
在得到了GoogleMapService这个抽象层之后,就可以实现新的GoogleMapV3Service,并且使用它替换原有的GoogleMapV2Service。最后如果有必要的话再将接口去除掉。 这时

  在得到了GoogleMapService这个抽象层之后,就可以实现新的GoogleMapV3Service,并且使用它替换原有的GoogleMapV2Service。最后如果有必要的话再将接口去除掉。

  这时候就可以使用上面提到的方法,在IOC中使用功能开关基础设施来控制SearchLocationService 使用的到底是V2的还是V3的服务。具体的代码如下:

  type="com.service.GoogleMapService"

  code="feature.googleV3Service.isActivated">

  其中,ns:runtime-conditionl利用了Spring的自定义命名空间技术来侵入到bean的装配过程中。整个Tag其实是定义了一个名为googleMapService的bean。在这个自定义Tag的实现中,它同样会去读取功能开关 (feature.googleV3Service.isActivated)的状态,然后根据这个状态来把正确的bean赋给 googleMapService这个bean。别人只需要直接使用googleMapService这个bean就可以了。使用这种方式,我们就可以轻易而安全的在两个bean之间切换功能。

  功能开关如何支持持续集成

  上面两个例子给大家展示了在一个Spring应用程序中如何使用功能开关。通过这种方式我们所有的代码,不管有没有完全做完,都存在与一个分支上了,从而也就解决了上面所提到的各种与合并相关的问题。同时还可以方便的通过改变配置文件的方式来更改应用程序的状态,如果在线上发现了问题,也可以快速的关闭该功能。

  但是这里面有个问题,在某次应用程序启动的时候,某功能的配置项的值必须是确定的,要么是开启,要么是关闭。那么在持续集成服务器上跑自动化验收测试的时候到底应该如何设置配置项的值呢?

  假设我下次发布期望该功能是关闭的,那么我每次跑测试的时候就让该功能关闭,这样可以保证本次发布是安全的。但是在开关开的时候才生效的那些功能是没有办法被自动化测试覆盖的。如果该功能出了问题,持续集成服务器也没法发现。那么在未来准备发布该功能的时候,我们还要再打开开关,做一些自动或者手动的测试,出现bug(很有可能,因为持续集成服务器并没有保护到我这个功能不被破坏)了再集中修复。这样还是没能完全解决功能分支所带来的问题。

  为了解决这个问题,我们引入了两个CI Job:Regression和Progression

  针对开关开的时候写一系列的自动化测试,使用加tag的方式标记它们为@feature_on。

  当开关打开的时候,因为系统行为变化了,所以之前存在的某些相关的自动化测试就没法再通过,我们把这些因为开关打开而失败的测试标记为@feature_off,意为只有该开关关掉的时候这些测试才有效。

  创建一个叫做Regression Tests的CI Job,即上图黄色的那个圈。其包含了加@feature_off tag的测试和没有tag的测试。启动应用程序的时候把功能开关关闭,然后跑这个圈里面所有的测试。

  创建一个叫做Progression Tests的CI Job,即上图灰色的那个圈。其包含了加@feature_on tag的测试,同样也包含了没有tag的测试。启动应用程序的时候把功能开关打开,然后跑这个圈里面所有的测试。

  每次代码提交都会触发Regression和Progression两个Job的构建。

  使用这种方式,我们可以保证应用程序的不同状态在每次提交都是被测试到的,如果有任何问题,可以第一时间发现、修复。增强了对应用程序正确性的信心,也为在产品环境切换开关状态提供了足够的信心。

  功能开关的陷阱

  上面我们提到的是只有一个功能开关的情况,那么如果有两个,会是什么情形呢?假设两个功能分别叫A,B。那么理论上来说我们应该测试到A_on, B_on; A_on, B_off, A_off, B_on; A_off, B_off这四种情况。也就是说需要有四个CI Jobs与之对应,如果有3个功能就需要8个Jobs。这显然是不可接受的。

  这里就涉及到使用功能开关的一个很重要的原则,功能开关之间不要有依赖。也就是说不管别的开关状态如何,只要A的开关是开的,那么所有的 @featureA_on的测试就一定可以通过,所有@featureA_off的测试就一定不能通过。如果保证每个功能都是相互独立的,我们就不需要测试所有的组合,还是只需要测试两种就可以了。比如这次发布需要的状态时A_on, B_off,那么在Regression中就按照这个状态起应用程序,并跑相关的测试;在Progression中按照A_off, B_on的状态起应用程序,并跑相关的测试。

  因此需要限制项目中功能开关的数量,并且严格避免开关之间的依赖,才能真正从功能开关中获益。

  总结

  可以看到,功能开关相比功能分支确实能给我们带来很多的好处:减少合并,真正持续集成,在产品环境可以打开或者关闭某个功能,等等。但是这些好处也不是免费得到的。总结一下使用功能开关需要做的:

  创建使用功能开关的基础设施。在本例中就是在JSP中访问资源文件,通过自定义的Spring namespace来动态确定bean的值等等。

  在代码中根据功能开关的状态来决定代码的行为。很有可能会引入比较多的if/else在Java代码,JSP代码中。

  需要跑两个CI Jobs。不过在时间上其实并没有太多的损耗,因为我们可以通过加一个CI slave并行跑就可以了。

  在某功能已经稳定地在产品环境下跑了一段时间之后,要及时拆掉跟这个开关相关的配置,if/else判断等等代码,减少系统中并存的开关数量。

  设计一个开关的时候要非常小心,不要和其他开关有依赖关系。

  总的来说,功能开关对于持续发布,平稳发布,甚至提高代码质量都有着积极的作用,是一个值得尝试的实践。

原文转自:http://www.ltesting.net/deltestingadmindd/