作者简介
Kevin,携程后端开发专家,追求通过深入业务来简化系统,对底层算法、数据分析有浓厚兴趣。
一、引言
1.1 背景
微服务架构下,产研分工精细,需求迭代频繁,随着需求的不断迭代,应用数、代码量及测试用例越积越多;需求迭代(尤其是有新人加入)的过程中,产品经理需要通过开发了解现状和历史逻辑,开发人员翻阅历史代码花费的时间和精力越来越大,测试人员上线前需要回归的用例也越来越多,严重影响了需求迭代的效率。
1.2 现状分析
目前携程旅游BG的后端开发人均应用数超过4个,人均维护的代码行近20万行;每月平均需求迭代的发布超过2千次,其中核心应用数占比及其发布次数占比都超过8成。
为了提高需求迭代的效率,旅游技术团队设计开发代码分析平台,对应用的现状(主要是源代码和测试用例)进行综合分析发现:生产应用中高达三分之一的代码属于dead代码(没有被引用,也没有任何生产流量),严重影响开发效率;日常迭代中68%的自动化回归用例与当前迭代无关,但是为了保障上线质量,测试人员需要对每条失败用例进行分析排查,不仅影响当前的交付进度,而且随着需求的快速迭代,自动化测试的可持续性堪忧。
二、代码分析规划
本文主要基于后端Java应用介绍如何实现代码分析平台化,并借助平台工具实现精准测试和应用瘦身。平台可以帮助开发人员识别无效代码,在短时间内以最小的风险完成应用瘦身,极大的提高研发的效率;同时通过平台的用例知识库进行精准测试,在需求迭代过程中只执行本次改动相关的用例,极大的提高自动化回归的效率和可持续性。如下图:
图1 代码分析与精准测试、应用瘦身
2.1 分析应用现状
通过对应用系统综合分析,形成知识库。分析的对象包括源代码(不含第三方引用)、对外提供的服务(包括api、任务以及消息)、自动化用例、日常迭代变更以及方法维度的生产流量等。知识库包含应用基本信息、统计信息(例如代码规模、方法规模、用例规模)、方法链路信息、用例链路等。
2.2 工具化及流程闭环
利用分析得到的知识库,针对特定场景进行工具化和流程闭环,辅助应用治理。
例如精准测试场景,平台可以与发布流程结合起来,开发提测后自动识别变更内容,并智能推荐自动化用例并执行,将执行结果实时同步给开发和测试人员,实现变更→发布→用例推荐、执行、反馈→修复变更的闭环。
应用瘦身场景,从开发角度来看,平台需提供多视角的辅助分析工具,帮助开发人员确定方法/类是否可以安全的删除;从管理角度来看,平台需划定应用治理前的基线,并将无效代码比例作为应用长期治理对象,从而实时评估下属治理的进度和效果,形成治理过程的闭环。
三、代码分析原理
代码分析的基本单元是方法,主体是应用的整个生命周期,从应用的代码仓库建立以及研发完成代码开发,到测试发布,再到生产运行,我们对不同阶段方法的关联信息进行分析,最终得到一个完整的知识库,分析流程及定义如下图:
图2 代码分析原理
3.1 静态分析
通过源代码解析工具解析出所有的方法声明及调用关系。
针对Java语言常见的解析工具及原理如下:
推荐使用java-callgraph2,理由是java-callgraph2专注于类和方法之间调用关系的分析,解决了很多常见问题,例如thread、lambda、stream使用场景的调用关系缺失等,并且在git上开源,引入源码可实现定制化。
3.2 半动态分析
通过字节码增强技术(如下图)和用例回放相结合,获取用例执行的方法链,再基于静态分析进行方法映射,达到半动态的效果。半动态分析工具推荐使用携程的开源平台AREX。
图3 字节码增强技术
3.3 动态分析
动态分析是一种代码运行时的采集分析,主要方式是收集生产环境方法的执行次数,以确认方法是否有效。目前主要有两种做法,一种是通过打桩的方式,类似于半动态分析。该方式虽然能够获取准确完整的运行时信息,但考虑到存在代码入侵并且可能对生产服务器性能产生影响,不建议采用这种方法。
另一种方法是利用Java虚拟机(JVM)的方法计数器,我们知道JVM采用的是JIT(Just-In-Time)编译机制,方法执行过程如下图:
图4 JVM-JIT方法编译执行流程
这种方式对代码无入侵,缺点是访问JVM方法计数器需要attach虚拟机进程导致STW(Stop The World),并且方法计数并不代表真正流量,只能反映方法有没有被执行以及执行的频度(幸运的是这对我们的场景已经足够了)。
综合考虑,推荐使用第二种方式,另外为了最大程度的降低采集流量期间STW对业务的影响,需要选取最适合采集的实例并提前停止对外服务(集群部署可以通过实例拉出实现)。
四、代码分析平台化
确定了代码分析平台化的目标,并阐述了代码分析的基本原理,接下来我们重点剖析平台化的三个关键步骤。
4.1 步骤一:建立知识库
建立知识库是代码分析平台化的基础,知识库可以将需求迭代的流程串连起来,并为后续分析数据(用例、流量等)的落地提供载体。
4.1.1 获取应用入口
应用入口指的是应用对外提供的服务,通常包括对外提供的api、应用定时调度job、消息(例如qmq)的消费者;应用入口一般都是通过注解标记并自动注册上线,原理如图所示,运行时主动向注册中心注册实例和服务,被动接受调度和请求。
图5 微服务注册发现流程
获取应用入口的最简单方式是通过代码分析根据注解识别。另外,多团队协作场景的api契约往往采用集中管理模式,应用通过第三方包引入api契约定义,为了避免大量的第三方引用解析,建议通过注册中心获取应用入口。
4.1.2 获取源代码
镜像指的是源代码经过编译、打包、检测验证后得到的容器加载对象,镜像是静态分析的主要输入。获取源代码则是为了得到准确的源码统计信息及变更信息。
考虑到开发人员在特定需求迭代过程中会多人协作、多次提交代码,因此获取源代码及镜像的时机建议在集群部署完成后、对外提供服务前,这样可以减少不必要分析、节约资源、简化分析流程以及减少对开发和测试的干扰。
4.1.3 静态分析及存储
通过静态分析可以得到方法间的调用关系,以及对方法进行标记(api、job、consumer、属性等)和染色(重写、继承、引用、可达等)。静态分析流程如下图所示:
图6 静态分析流程
实体数据建议使用关系数据库存储,考虑到方法间的调用关系复杂多变且层级深,推荐使用图数据库存储方法调用关系,不仅检索的复杂度更低、性能更好,而且能够比较直观的反映系统现状(通过Nebula-Graph存储并检索举例如下图)。
图7 方法调用链路图展示
4.2 步骤二:完善知识库用例信息
在建立知识库的基础上,测试作为需求上线前的必备步骤,对测试用例的分析并融入知识库至关重要。这个步骤我们主要通过用例回放收集用例经过的内部方法和对外api,结合源码对比得到的变更方法分析出需求改动直接、间接影响的入口和用例。
4.2.1 用例回放
用例回放指的是在用例执行的同时收集代码执行信息。执行用例回放需要满足两个前提条件,一是需要有一套自动化用例测试平台,能够维护并调度执行自动化用例;二是需要在系统运行时进行打桩,能够在用例执行的过程中识别用例和方法调用信息,并对外输出。
大多数互联网企业都有自建的自动化测试平台,这里不做展开;系统运行时打桩的实现推荐使用开源AREX,不需要修改业务代码,仅需系统镜像打包时加载代理服务,对系统运行时的影响安全可控。
4.2.2 分析流程
通过半动态分析(流程如下图),获取用例执行过程中途经的方法链路,补充知识库中通过用例建立起来的方法关联关系。
图8 半动态分析流程
基于方法调用关系在图中的存储,用例和方法的关系也采用图数据库的存储,只需要再补充新类型的点(用例)和边(用例调用方法)即可,其表现方式更为直观(如下图)。
图9 用例方法调用链图展示
4.3 步骤三:完善知识库流量信息
对源代码、用例的分析是建立在冷数据加载的基础上,应用代码的质量、测试的有效性最终体现在应用对外提供服务的过程中,运行时的数据不可或缺。这个环节我们主要介绍基于动态分析的原理,如何进行生产流量采集,如何将采集数据跟知识库结合起来,为后续的工具化和流程闭环提供数据支撑。
4.3.1 生产流量采集
生产流量采集主要包含两部分内容,入口流量采集和应用内部方法流量采集。
入口流量主要指api(job任务/消息处理)被外部调度的情况。作为日常排查问题和监控的重点,这部分数据通常作为微服务架构的基础能力,可以直接通过公共基础服务获取,这里不做展开。
应用内部方法流量采集的原理(动态分析)前面已经介绍过,这里重点介绍集群部署的场景下,采集实例选取的三个基本原则。
首先是保障采集对生产影响最小。主要基于采集需要暂停实例服务的考量,实例拉出前要做集群服务能力评估,确保服务能力不能下降过多(例如集群实例数少于3个的情况下不建议自动拉出),拉出后要给未完成的业务线程保留一定的处理时间,采集异常或者时间超过一定时长能够及时中断恢复拉入。另外针对job类应用建议owner选择合适时机手工采集。
其次是确保采集内容有效。方法流量采集本质上是JVM底层方法计数信息,因此如果实例创建时间过短(例如自动扩容)或者集群本身只针对特定场景服务(例如操作路由),很多场景都没有被执行到,采集的意义就不大。
最后是保障采集过程可持续。随着业务快速迭代,生产流量是不断变化的,因此流量采集需要周期性的持续进行。
4.3.2 分析流程
通过动态分析(流程如下图),将方法的流量信息补充到图数据库的点(方法)上,可以动态的反映方法被执行情况,间接的反映方法及自动化用例的有效性。
图10 动态分析流程
五、应用场景
在知识库的基础上,结合精准测试和应用瘦身两个具体的应用场景,实现工具化和流程闭环,最终完成代码分析平台化建设。
5.1 精准测试
5.1.1 用例执行现状
自动化用例回归作为应用发布生产前的必经环节,有两项重要的评估标准:用例执行成功率和新增代码行的覆盖率。在没有用例推荐之前,一般采用人工选取执行和全量执行两种方式。
人工选取的优点是用例有针对性,缺点是不仅效率低而且容易漏选;全量执行虽然可以避免用例选取缺漏,同时可以提高增量代码行的覆盖率,但是用例执行成功率无法保障,往往需要对执行失败的任务一一排查,花费大量的时间和精力,大多数情况下失败的用例与迭代的需求并不相关,更令测试同学头疼的是随着需求迭代自动化用例在不断增加,费力度逐渐升高。
5.1.2 用例推荐
基于代码分析平台,应用生产发布前,可以通过对源代码进行对比分析获取变更的具体方法,获取变更通常需要代码版本管理工具和对比组件,目前互联网企业应用比较广泛的代码仓库管理工具是git,对比组件推荐使用code diff。
方法声明不变的变更(例如修改、删除),知识库中已经收集了方法调用链、用例方法链,并且对方法进行了入口标记和染色,我们可以准确的识别其关联的入口及用例;方法声明变化的变更(例如新增、增加入参),我们利用实时静态分析可以通过调用链追溯到影响的入口,通过入口找到关联的用例。用例推荐功能如下图,每次只执行代码变更相关的用例。
图11 用例推荐流程
5.1.3 流程闭环
解决了如何准确的推荐用例,接下里需要结合需求迭代的基本流程和应用发布流程将代码变更、用例推荐、用例执行以及结果反馈串连起来(如下图),实现流程的闭环,才能更好的发挥代码分析平台的作用,最终提高需求迭代效率和质量。
图12 精准测试流程闭环
5.1.4 度量方法及效果
前面提到自动化用例执行效果的评估标准主要是用例执行成功率和增量行覆盖率,成功率主要靠用例本身的质量来保障,增量行覆盖率可以作为衡量精准测试准确性的度量方法之一。理论上精准测试的增量行覆盖率只要不能接近全量执行的增量行覆盖率,就说明推荐用例存在缺失。
经过验证,目前我们精准测试的增量行覆盖率可以达到全量执行的99.2%(偏差主要来自于环境依赖),全面推广后平均减少了68%与当次需求迭代无关的用例回归。随着自动化用例的不断增加,精准推荐已经成为自动化回归不可或缺的一环。
5.2 应用瘦身
5.2.1 应用代码现状
应用经历一段时间的需求迭代之后无效代码就会开始累积,究其原因主要有三个方面,一是需求变更后部分分支不会再被执行到,由于种种原因没有来得及重构;二是项目上线初期的临时检测对比的分支,项目上线后没有及时清除;三是上游场景变化导致下游部分场景不再被执行,但下游自身又无法识别。
过高比例的无效代码不仅影响系统的编译速度,而且严重影响开发的效率,尤其是对于新接手的同学,需要了解大量的代码历史背景才能熟悉系统现有的逻辑,而且代码量越大逻辑越复杂,修改的风险也就越高,即使经验丰富的工程师也不敢轻言重构。
5.2.2 工具化
基于代码分析的知识库信息,以方法为基本单位进行引用、入口以及生产流量的多维度分析(如下图),并提供应用分析工具,辅助开发人员快速的识别无效代码,实现应用瘦身。
图13 方法可达分析
5.2.3 流程闭环
从研发的角度看,删除代码存在一定的风险,如果能够便捷的通过工具获取代码的生产流量情况以及外部依赖情况,将极大的降低这种风险,增强其应用瘦身(包括代码重构和删除)的信心;从团队管理的角度来看,如何衡量治理的效果、把控治理过程的风险以及长远地评估无效代码的合理范围也同样重要,因此平台从研发和管理两个角度实现了闭环(如下图)。
图14 应用瘦身流程闭环
5.2.4 试点效果及经验
完成工具化和流程闭环,我们拿团队内部10%的应用(100+)经过1个月的试点,轻松实现了零故障删除百万级代码行的目标。其中15%的大规模应用(代码行大于20万行)经过瘦身后系统镜像的生成时长从几十分钟级降至到几分钟(耗时包括编译、UT执行、合规扫描等)。代码链路复杂度也明显降低,如下图所示。
图15 应用瘦身前后方法链对比
经过试点,我们总结了应用瘦身需要严格遵守的三个原则,一是瘦身工具只是辅助,代码删除一定要由对应用背景有一定了解的同学进行;二是删除代码一定要经过合作伙伴或leader的review;三是应用瘦身是一个长期治理的过程,不能急于一时或一次了事。
六、总结
本文主要基于微服务架构下为了提高需求迭代效率,通过代码分析形成知识库,针对精准推荐和应用瘦身两个场景进行工具化和流程闭环,初步完成代码分析平台化建设。
目前已支持的场景还需要进一步细化(例如应用瘦身可以支持本地开发工具的插件化),结合当前的知识库,后续还可以支持更多的场景(例如工程复杂度、用例质量等等)。本文只是抛砖引玉,为应用治理提供了一种全新的思路,代码分析平台化是一个长期持续的工程,需要走的路还很长。
参考文献
- java-callgraph2:
https://github.com/Adrninistrator/java-callgraph2 - arex-agent-java:
https://github.com/arextest/arex-agent-java - OpenJDK: https://openjdk.org/groups/hotspot/docs/Serviceability.html
- JavaParser: https://javaparser.org/