一、背景介绍
有赞早期业务跑在一个单体php工程上,随着业务发展,性能拓展性已经满足不了需求,为了后续发展,底层开始微服务化,整体转向dubbo框架。从单体转向分布式框架,测试也面临着一系列问题,如下:
- 对于分布式系统中的绝大部分应用,随着业务发展,自身应用代码复杂度会不断增加,如何准确、全面判定代码修改影响范围会越来越重要;
- 一些领域设计不太合理的业务架构,会发现任一应用接口变动会使多个应用受影响。测试过程中会发现只是自身应用代码一个修改,会导致对外暴露的接口逻辑发生很大变动,此时测试人员需要判定出这个对外暴露的接口对上层应用到底有多大影响;
- 业务快速迭代导致测试时间不断压缩,全量回归是一个很困难的事情,那么测试范围需要开发测试人员根据代码和业务熟悉程度精确把控,风险容易失控;
基于上述背景,我们研发了精准测试工具,作为应用上线质量的参考维度之一,集成到测试工具平台上供技术部门所有同事使用。
二、整体方案设计
对于上面的痛点,可以分为三步走;第一步修改过的代码如何识别,第二步分析出自身应用有哪些接口受到影响;第三步获取上层业务方受到的影响;设计要点如下:
- 识别变更的代码:上线代码和master代码采用抽象语法树分析,去除噪音后,比对方法体即可获取到新增/修改/删除的方法;
- 分析影响的自身应用对外暴露的接口,采用动静结合。静态分析采用字节码分析,同时补充了桥接来解决部分多态问题;动态分析采用了和主流调用链技术一致的javaagent来对代码进行织入,为了防止大量织入导致性能变差,只在qa环境进行织入;
- 对于应用间链路查询,由于有赞内部很早就有一个调用链系统,可以实时查看应用接口之间调用详情,借助这个系统,使用大数据spark或者MR进行离线任务,汇总处理所有的链路信息即可获取应用间所有链路信息,上层业务方影响范围只要查询链路即可获得。
PS:对于没有现成调用链的公司可以参考成熟的开源工具skywalking,github地址如下:https://github.com/apache/skywalking
三、重点模块
重点模块包含了代码比对,静态分析,动态分析,动静结合,应用间影响分析。
3.1 代码比对
设计思路:影响分析,首先需要判定哪些方法发生变动。传统的git/svn会把增加的注释、空白字符、空行等非业务代码认为是代码变动,实际上这类变动对于我们业务来说没有任何影响;而单纯判断编译后的class文件,是可以避免这些误判,但判断哪些方法发生变动,需要比对方法体的指令,同时还要处理各种内部类的问题,难度不小;为了解决这个问题我们采用了语法树分析,流程如下:
3.2 静态分析逻辑
设计思路:
对于java代码,分析字节码可以发现,调用方法是通过invokestatic,invokespecial,invokeinterface,invokevirtual,invokedynamic这五个指令,扫描每个方法体指令中的invoke指令,获得应用内部调用链中的一系列父子节点。每个应用对外暴露出去的接口都可以认为是一棵内部调用链的根节点,从根节点出发遍历可达的所有节点,那么内部调用链即可生成。要点如下:
- 对于字节码分析,有很多字节码操作工具,ASM/bcel/Javassist都可以,使用方法都类似,随便选择一个就行;
- 对于invokedynamic指令,单纯按照字节码指令指向的是一个引导方法(Bootstrap Method),需要判定真正执行的方法,进而获取真正的调用链;
- 为了加快速度,减少后期处理,需要剔除掉不感兴趣的父子节点,比如调用三方包/jdk的API/get方法/set方法;
- 调用接口指令是invokeinterface方法,但实际上真正执行的是接口的实现类代码,如果接口只有一个实现类,那么我们就可以判定执行的就是这个实现类,从而可以进行桥接;
- 匿名内部类编译过程中会生成一个类似A$1的class文件,根据字节码文件中的EnclosingMethod字段可以判定上层调用方的类名和方法名,从而可以完成方法和匿名内部类方法的桥接;
动态分析&动静结合
动态分析:
对于代码中存在的AOP和多态,静态分析无法很好的解决,采用动态分析将会很好的解决这个问题。使用javaagent对内部方法进行代码织入,当执行自动化或者功能测试,可以记录一次请求经过的所有内部方法,这样形成的内部方法调用链将会记录aop和多态执行的真正的方法,静态的弱点会得到很大的补充。要点如下:
- 性能问题:大量织入会导致性能损耗,首先判定当前环境,是否是qa环境,qa环境再织入,不要对线上有影响;
- 织入范围:只对com.youzan的包进行织入且排除掉二方包(二方包包名一般也为com.youzan.*),排除掉所有的get/set方法,排除掉private方法(子类重写不了父类私有方法),排除掉这些会大大加快代码织入速度,且对分析无影响;
- 对于每次请求到结束返回,整个调用过程可以看作是不断入栈出栈的过程,调用一个方法是入栈,方法结束为出栈,当栈为空,即表示请求结束,出入栈的顺序反映了代码的调用逻辑,从而形成内部调用链;
动静结合:
动态分析会存在样本不足,内部调用链不能完全反映内部方法调用情况;静态分析存在多态和AOP的问题,存在孤立的节点,无法串联起来;为了尽可能分析出受影响的范围,以及避免动静分析两种方式的弊端,采用动静结合,要点如下:
- 根据动态分析和静态分析,分别获取了一系列内部调用链,把这些内部调用链的节点打散后重新组合得到包含动态和静态数据的内部调用链;
- 根据新增/修改的方法名称和方法入参类型,匹配出包含此方法的内部调用链,内部调用链根节点就是改动点影响的对外暴露接口;
3.3 应用间影响分析
应用间链路采集采用的是sdk+javaagent,整体方案类比skywalking,可以参考skywalking进行二次开发。此处主要介绍离线分析思路,应用间调用链的数据都是每个应用分批上报,一次请求在各个节点上报的都会包含最上层调用方接口、上层调用方接口以及本接口信息,汇总去重后将反映出整体应用间调用详情。(PS:由于一些异常情况,实时上报的链路数据不一定完整,故离线统计入库之前需要判定是否为一棵完整的调用关系树)
3.4 效果
应用内影响的接口效果如下,主要包含了汇总信息/比对页面/影响点对应的接口:
一个接口可能被多个调用方调用,对于开发和测试人员一般最关注的是接口直属调用方和入口调用方以及整体的拓扑图,如下所示:
某些情况下,单条链路调用链详情也需要展示出来:
3.5 不足
- 对于新增代码的影响面大部分都是依靠字节码分析,而字节码分析在多态和AOP方面存在天然短板,影响面会有所丢失
- 应用内链路跟踪存在大规模代码织入,对性能和内存资源会造成一定损耗,对于代码量很大的工程,损耗尤其严重;
- 对于大规模代码重构或者底层公用方法的变动,影响面分析会覆盖很多接口,此时依然需要人工评估是否可以缩小测试范围;
- 目前应用内代码分析只支持java语言,缺少其他语言的范围评估。