APM全称是 ApplicationPerformanceManagement
,即应用性能管理平台。对于公司而言,应用发展到如今这个阶段,在人员不断扩展、业务不断复杂、新产品不断孵化,拥有一个统一质量、安全管理平台对于应用健康情况全方位实时的把脉,以及对于业务的可持续化发展的保驾护航是尤其重要的。git
在去年其实这样的平台还不多像今年这般被频繁说起。尤记得前几天刚刚参加绿色联盟会议,对于应用的质量、安全不管是阿里、腾讯、新美大仍是360都绕不过这个主题,而你们都知道谷歌更是在今年发力推Vitals监控,彷佛在今年这个时间点,各大公司都使出浑身解数不约而同的加大了这方面的投入。数据库
无独有偶,2018年流利说Android架构组有一个小目标,就是将本来杂乱的交付、监控等流程系统化、体系化,便于横向项目的发展以及确保各业务线、产品线可持续发展,其中APM就是很重要的一块:后端
其实在年初咱们公司组织架构扩大后,流利说总体已经进入了人员与业务的快速增加的快车道,对于几十个模块、业务线与多条产品线的状况下,原有的那套应对中小应用的体系已经显现了一些问题。所以咱们着手从各个阶段入手对其一一击破,今天,咱们与你们分享的就是咱们在搭建APM过程当中,对于大盘搭建与数据整合收集部分,但愿你们都可以有所收获。安全
特别值得一提的是以前在谈到[《客户端持续交付工程实践》这篇文章时,咱们谈到了为了确保质量,在最后出了各种的质量报告,并将其经过邮件的方式落地,在最后的时候甚至提出了如何避免线上裸奔,其实这些最后都在APM大盘中获得了进行的落地,这些特性组合让APM大盘展现实现了经过数据与绿线告知测试完备程度以及应用质量程度的任务,后面咱们会谈到,你们跟上脚步👣,咱们往下看:服务器
首先谈到APM大盘,就必须须要肯定咱们须要监控哪些维度,咱们先来看下最终的决定:架构
之因此选这三个维度,是结合咱们目前的发布流程以及开发节奏的特性来肯定的:框架
最直接的是监控24小时内的全局的实时数据变化 --> 实时变化大盘
函数
而对于每一个版本两两比较也是十分重要的,所以便衍生了横跨一年的版本维度的变化大盘 --> 版本变化大盘
工具
因为咱们每一个版本内的开发周期有 dev
、 alpha
、 beta
,拥有较长的固定的迭代周期,所以咱们须要在版本发布以前对当前版本特别的关照,从而衍生了一个仅仅只包含当前版本的数据大盘 --> 开发周期大盘
性能
在肯定了三大维度以后,咱们要作技术选型的肯定,其实这块在搭建流利说APM平台之初咱们就遇到了一个问题,对于流利说的Android架构团队来讲,咱们还很难像腾讯、阿里、饿了么、爱奇艺那样投入足够的人力创建很是完善的自有的各种监控平台与体系,所以咱们采用了一些现有成熟的体系框架与监控平台来减轻咱们的工做量,将有限的精力聚焦在监控整合以及如何展现测试完备程度与应用质量的核心问题上。所以咱们采用了咱们的云架构团队提供基于开源方案的Promethues数据库与Grafana展现平台,以及在一些监控方面咱们采用了第三方数据平台做为数据依据以及问题处理平台(如bugly)再作二次数据采集整合。这也祭奠了流利说APM大盘的另一个对全部数据的整合与入口的特性,固然这也就影响了咱们主要的技术选型:
而对于各种的自动化收集与大盘维度数据控制的自动化,咱们须要依赖如下实体媒介来搭载咱们的各种服务:
在第一期APM大盘须要监控的数据方面咱们选择了 包大小
、 关键页面打开耗时95线
、 内存泄漏
、 遗留BUG数
、 冷/热启动
、 代码质量状况
以及 ANR
与 CRASH
,再加上权衡测试完备程度的 参加测试状况
与 综合覆盖率
,这样的选择的主要缘由是,第一期其实是APM大盘的从无到有,咱们核心工做是将整个框架搭建起来,所以在数据选择方面咱们主要选择从对用户直观感觉与影响最为明显的维度出发进行选择。
其中的 代码质量状况
相信有同窗会提出疑惑,这块实际上是 FindBugs
、 PMD
、 Lint
这三块扫描出的不须要强制处理 Warning
级别的各种数量状况,而对应的 Error
级别的问题,在代码合并的时候已经被强制处理了,这块若是感兴趣,咱们在《客户端持续交付工程实践》中有详细的进行了阐述。
对于刚刚谈到咱们有依赖第三方数据平台做为数据依据,主要是将其做为问题处理平台,而涉及到数据处理主要涉及这三块: Crash
、 ANR
、 内存泄漏
。对这三个数据而言咱们不得不依赖一个解决问题的平台用于对具体问题的进一步分析、归类,以及对解决进度的标注。其中 Crash
与 ANR
大多数平台都拥有,而内存泄漏咱们采起的方案是基于 LeakCanary
进行收集而后做为错误进行上报,具体后文会提到。
后端的APM服务的开发,对于服务而言主要就是提供各种RESTful接口提供后端数据库的数据写入,一些页面的展现(如综合覆盖率详情页)以及文件上传的支持(如 jacoco
的 ec
文件的收集),这边因为咱们Android技术栈的缘由直接使用了 kotlin
基于 SpringBoot
进行快速开发迭代。
开发周期是跟着版本走的,所以这块是强绑开发流程的,在开发周期不断的迭代中会进行自动的跟进,而且整个大盘数据维度切换时机是根据以小版本的切换来跟进的,好比 6.8.x
发布后,大盘所展现的全部数据就会自动变迁为 6.9.x
,而最后一位的 patch
版本因为只会在 hotfix
分支上出现,周期很短会有其余流程跟进,不会在该开发周期大盘中体现。
可以作到对单独某个版本维度的展现,得益于 Grafana
中可使用 MySQL
做为数据源,并支持灵活的使用各种 SQL
语句对数据进行塞选,所以咱们只须要在CI上建立周期任务,在当前开发分支按期扫描当前版本,当版本发生变更的时候写入到对应的版本数据库中便可,而全部其余数据获取数据时,只须要进行联表查询取最新版本相关的数据便可。
这里提到的开发分支在不一样的阶段会有所不一样,还记得在[客户端持续交付工程实践》中咱们提到的开发流程时的那张图:
这里能够清晰的看到,在版本提测前当前的开发分支是 develop
,接收各类类型的合入,而且合入审核只 OkCheck
跑过,代码 Owner
与另一位同窗 Approve
便可合入;当提测后,当前的开发分支会变迁为 release
,一般只接收代码 fix
,此时也就是只接收 fix/xxx
的分支合入,而且在 beta
后代码合入还须要发布经理的 Approve
。为了减小你们合并代码可能照成的误操做,这边还开发了 lit
工具,因为篇幅限制,这个话题咱们就没有再深刻了(咱们有计划在明年对这个发布流程进行较大幅度的调整,之后有机会也会与你们进行分享的)。
咱们来简单经过一个例子来看下开发周期大盘这块的数据展现的状况,假如咱们拥有一个开发版本的表以下:
此时咱们即可以经过取降序限制 1
个结果直接拿到最新版本的数据,做为当前版本周期:
如上图,其中 $__timeFilter
函数是 Grafana
提供的,因为 Grafana
支持设置当前面板所展现的数据的时间范围,所以这里的 $__timeFilter(date_created)
等价于 date_created
的时间戳在设定范围内的条件。
紧接着咱们尝试拿到当前版本的内存泄漏泄漏个数:
完美,符合预期!其余的数据也是以此类推的获取便可。
对于开发周期大盘,咱们但愿你们在不一样的时间点关注不一样的维度,在开发阶段只须要重点关注 包变化
与 代码质量
状况,而当提测后因为代码趋于稳定,咱们此时应该须要开始关注各种重要的质量指标。
特别是在提测后(当前版本周期进入 alpha0
后),你们须要经过在大盘中根据 参加测试人数
与 综合覆盖率
做为衡量测试完备程度的一个标准,在测试完备程度尽量高的状况下,咱们跟进其余的数据指标对相关问题进行修复,修复后因为代码的修改 综合覆盖率
会降低,此时再经过灰度等方式提升完备程度,再修复问题打磨应用,以此造成一个良性的闭环。
其中咱们经过明确关键发布节点的规则来对这块的落地,在 alpha
阶段测试同窗开始介入对测试完备程度的跟进,在 beta
前测试同窗须要将完备程度提升到咱们在 Grafana
中定义的 SingleStat
图表中的绿线的阈值。而相同的,开发同窗也须要在 beta
前将几个基础指标也提升到对应的绿线阈值之内,在达标并提交 beta
包后,这边须要保持不管是完备程度仍是各项质量数值都在绿线阈值范围内便可。
其实在创建APM时,相信每个团队都会遇到一个问题,那就是咱们如何去权衡里面所罗列出的每一项质量指标数值所表明的实际含义是好,仍是坏。其实对于咱们而言,这个问题十分简单清晰,只须要与本身的上一个版本进行对比,所以就有了版本变化大盘。
版本变化大盘即是收集各个版本的综合表现,以最直观的形式进行呈现。你们从上图能够看到咱们清晰的将版本变化大盘拆解为了5个部分,囊括了 基础性能指标
、 关键页面耗时
、 包状况
、 Qark安全扫描
以及 潜在代码质量问题
。
全部的数据也是一样来自 MariaDB
的数据源,主要缘由是版本周期一般来讲跨度比较大,间隔也会比较长,而且考虑到咱们有不少联表查询的场景,所以这边便没有使用 Prometheus
这种偏向于实时监控数据库做为数据源。哪怕是有些数据是直接打到 Prometheus
的(如关键页面耗时),咱们也会在适当的时候周期性的根据版本拉回一个综合数值塞给 MariaDB
。
这边举一个案例,咱们须要知道每相邻两个包的大小变化状况:
为了让包大小的变动更加直观,咱们实际须要知道的是每一个版本相对于上个版本的变化值,这里利用了 SQL
能够灵活的定义变量并进行先后的比较进行实现。
实时大盘实际上展现的就是最近24小时内,应用的健康状况,以及线上用户的直观感觉,全部数据都是全版本的综合值。
在数据收集方面,这块和前面提到的技术选型有着很大的关系。对于数据的获取,主要采用三种方式:
应用运行时上报: 关键页面耗时
、 运行时Jacocoec
CI周期性扫描上报: 包状况
、 安全报告
、 代码质量
、 AndroidTest与UnitTestec
、 综合覆盖率详情
、
应用API调用: Phabricator上遗留BUG数
服务器爬虫部署: Crash
、 ANR
、 内存泄露
、 冷/热启动耗时
、 参与测试状况
这边使用爬虫而非一些站点的提供的API的缘由是由于第三方的平台提供的API基本上是不符合咱们要求的,而咱们这边没法Push对方新增/修改接口,So...要不本身写平台,要不爬虫,因为人手不足时间有限,咱们选择了后者。
其中 Crash
、 ANR
、 冷/热启动耗时
、 关键页面耗时
是集成在应用中带到线上的,而 内存泄露
、 综合覆盖率相关
是只有在测试包才会带有,其余的是基本上解耦应用自己的数据分析。
这边对于数据存储方面也十分明确,对于实时性很是强的 Crash
、 ANR
、 内存泄露
、 冷/热启动耗时
、 关键页面耗时
、 参与测试状况
、 Phabricator上遗留BUG数
这边是直接打到Promethues上,而对于在持续有 ec
刷新的状况下才会15min刷一次的 综合覆盖率
、以及天天刷新一次的 包状况
、 安全报告
、 代码质量报告
这些便直接打到 MariaDB
便可。不过为了版本变化大盘以及当前周期大盘的联表查询,这边还有另一个周期性半天任务,就是从Prometheus上扫描全部类型的数据,进行综合计算后会刷新到 MariaDB
,也就是说实际上 MariaDB
上拥有全部数据只不过没有Prometheus实时。
P.S. 关于Prometheus的使用以及每一个数据Metric类型的选择直接参看Promethues的官方文档便可,Prometheus的各种文档仍是很是全的。
相信你们在大盘上,可以注意到有一个数据可能不少厂商的APM大盘都没有包含的 --- 综合覆盖率
谈综合覆盖率的数据整合以前,先和你们说说咱们统计这块的缘由,其实对于应用发布而言,如何权衡测试的完备程度一直是一个十分棘手的问题。而综合覆盖率自己其实并无神奇的效果,可是他能够十分明确的作到一件事情,那就是若是综合覆盖率是100%,那说明全部代码都有被执行到,那么剩下的事情就是咱们如何作到只要存在问题的代码被执行到,咱们就可以自动化的将其进行上报。这块其实很好的可以与几个基础指标融合,如 ANR
、 Crash
等,这些原本就是遇到了就会自动上报的。咱们经过结合当发生这些问题的时候,当前的覆盖率 ec
文件将不被上报,来让覆盖率这件事情行之有效的在很大意义上体现测试完备程度而且驱动问题修复造成闭环。固然对于一个体系化的集成测试而言,实际上代码的简单的单次执行并没有法暴露全部问题,更多的是与该次执行的上下文有很强的联系,这块就须要不管是接口测试、功能测试以及Monkey等自动化测试的完善了,不过在这里对于测试完备程度而言一个完善的综合测试的覆盖率机制依然是一个相对可靠的参考纬度。
对于开发大盘而言,咱们须要综合覆盖率与参与测试的人数、启动次数做为测试完备度的一个衡量标准,全部数据都是创建在综合覆盖率达标而且参与人数达标的状况下,才用于其阶段性的意义。在早期咱们尝试推过全量的综合覆盖率,可是因为测试同窗与开发同窗很难就很老的代码花大量的人力去进行覆盖,所以咱们结合咱们的开发周期对jacoco进行了定制,实现了基于版本的差分的综合覆盖率计算与输出。
这里涉及了较多维度的定义,首先简单来讲:
综合覆盖率
= AndroidTest覆盖率
merge UnitTest覆盖率
merge 运行时覆盖率
其实,真正的计算方式是:
综合覆盖率
= 该版本修改代码中被执行过的行数的总和
/ 该版本中所修改代码的总行数
而具体的该版本修改的被执行的行数总和:
该版本修改代码中被执行过的行数的总和
= AndroidTest中执行到被修改的
merge UnitTest中执行到被修改的
+ 运行时中执行到被修改的
虽然看起来公式彷佛挺复杂的,可是实际上基于Jacoco Gradle Plugin的 JacocoMerge
咱们就能够作合并的操做,所以只须要分别获得三种状况下执行的 ec
文件便可,在具体分析以前对其进行合并,而后在使用 JacocoReport
出报告后针对报告根据 git blame
拿到当前版本修改过的代码进行二次标注便可。
针对 AndroidTest
与 UnitTest
较为简单,咱们在 GitLab
上直接建立周期性的任务进行 ec
文件生成便可,而针对 运行时
,咱们在每次应用推到后台时主动将当前的 ec
文件上传到后台,惟一须要注意的是,多进程状况,在调用 org.jacoco.core.runtime.RuntimeData#reset
以前,每次经过 org.jacoco.agent.rt.RT.getAgent#getExecutionData()
取到的 ec
文件都是带有全部的 ec
数据,所以这边能够采用同一个进程使用相同的文件名进行覆盖上传便可,最简单的方法就是在当前进程第一次收集的时候生成一个 UUID
,后续不断复用便可,然后台 RESTful
接口的行为须要确保相同名称文件使用覆盖的措施。
这边咱们在设计综合覆盖率的时候也并不是一路顺风,遇到很多有意思的问题,咱们捡一些与你们分享下:
因为咱们须要收集的是 AndroidTest
、 UnitTest
、 Runtime
这几种环境下的 ec
文件,这里其实有一个颇有意思的问题,就是最终的综合覆盖率报告,咱们须要从报告上获知具体哪些行被覆盖了,这里报告内确定须要非混淆的源码才可读,而这里所产生的 ec
文件倒是来自几个环境, AndroidTest
与 UnitTest
所执行的是未混淆的环境, Runtime
因为须要测试同窗的参与、甚至灰度内部大量同窗的参与,咱们确定是须要给到混淆包的,这样一来理论上来讲,这两种一个来自未混淆包的 ec
,一个来自混淆包的 ec
最终合并而后生成报告确定是对应不上的,后来在咱们来回比对,并解析 ec
文件后发现,经过jacoco注入后的包,jacoco会自动注入一个对当前类的说明:
所以这块问题Jacoco自己就已经给咱们解决了。
而在作针对版本的差量的综合覆盖率时,咱们遇到了一个相对棘手的问题,以下图:
咱们假设当前版本是 6.9.0
,而下一个版本是 6.10.0
,还记得前面提到的咱们目前的发布流程,在应用提测以前你们都是在 develop
分支上进行合并代码,而当应用提测后, 6.9.0
就会迁出 release
分支,而且在 release
分支上进行开发,而 develop
此时的任何代码的合并都将不会在这个版本带上,由于此时 develop
分支已经变为 6.10.0
的开发分支, 6.9.0
的开发分支已经变为 release
,当 6.9.0
发布后会合并回 develop
分支,此时 develop
分支依然是 6.10.0
的开发分支只不过合入了 6.9.0
提测后的代码。
这里就遇到了一个问题,咱们计算当前版本的综合覆盖率时所选取的变动来源,不能是从 A
到 HEAD
,由于这样一来就包含了 6.9.0
提测后的全部代码,也不能是 B
到 HEAD
,由于这样就丢失了 6.9.0
提测期间 6.10.0
并行开发的那部分提交。实际上咱们所须要的是途中绿点的全部变动。这块得益于咱们以前在作变动集时的相关积累,经过 git
提供的方法,计算出从 A
到 HEAD
的Diff中每一行的 Commit
,而后将属于红色这条分支的 Commit
的行过滤掉,这样就获得了全部绿色部分修改的行。
Jacoco还有一些版本的兼容问题,好比低版本的Jacoco生成的 ec
文件跨度一大在高版本上就直接解析失败,好比 0.7.4
与 0.8.2
(这是截止本文发布的最新稳定版本)生成的 ec
是相互解不开的,所以咱们在接收 ec
文件的时候须要注意作好根据版本区分开。还有另一点, Jacoco
从 0.8.2
开始对 kotlin
作了一些支持,不过 0.8.2
版本须要依赖 AndroidGradlePlugin
的 3.2.1
或更高版本。
最后,除了外层的覆盖率方面咱们添加了基于版本的覆盖率,在详情页中,咱们经过左侧的标注来讲明该代码是当前版本修改的,该标注是超连接到Gitlab上的相关Commit的,其余部分保留了 Jacoco
本来的面貌。
这些内容其实在《流利说客户端持续交付工程实践》中咱们就有提到,只不过当时是经过邮件进行通知的,后来咱们将其上报到APM的 MariaDB
上,若是感兴趣能够在那边文章进行查看。
前面也提到了,关于内存泄漏,咱们是直接经过 LeakCanary
注册 RefWatcher
对全部的泄漏进行监听,其中也包含了 isExcluded=true
的系统级别的泄漏,可是会在上报时进行区分,主要缘由是不管是 Framework
层的泄漏仍是咱们上层代码逻辑泄漏,泄漏的都是咱们应用的虚拟机,其对用户的影响也是直接表如今咱们本身的应用上的。
其中存在的问题是,咱们在流程上须要将应用控制在绿线内,所以咱们不能让系统级别Stick级别的泄露阻碍了应用的发布,也不能让Stick级别的泄露被隐藏,所以咱们将一些已知的Stick级别的泄露单独提Task跟进,而直接将这块的问题标注为了解决。
内存泄漏这块还有一个小坑,咱们刚开始是直接将泄漏的堆调用信息做为 message
以错误的形式上报给Bugly,利用Bugly中根据不一样 message
归为不一样的错误的形式,将相同堆调用信息的泄漏合并起来,可是后来发现,对于不一样的泄漏,在上报的错误中虽然使用了不一样的 message
可是因为他们的错误栈可能相同(都是走上报的那个栈),致使不一样的泄漏被合并为了同一个,这个问题最后咱们将内存泄漏的堆调用信息写入到栈中后获得了解决。
对于关键页面耗时这块因为咱们须要计算的是95线的数值,简单来讲就是95%的用户所感知的耗时都是在该数值以内,因为这块数据方面的特色,咱们分享下其在Promethues上的坑点与解决方案。
这边很显然咱们须要使用 Histogram
这个类型,因为咱们除了95线,在作进一步分析中还须要更多的数据纬度,在各方面的权衡下,咱们对每一个版本的每一个页面监控取20个 Bucket
,那么问题就来了,咱们平均发一个版本,整个流程下来 dev
、 alpha0
、 beta0
、 beta1
、 release
,一共5个版本,假定咱们须要监控10个页面,组合下来就是发布一个版本总共就须要 20*5*10
= 1000
个新增,而且版本是随着时间的推移不断新增的,这样累计下去将是至关浪费的。
所以这边咱们修改策略,由于咱们线上在周期中最新的长期只会有一个版本在跑,而且本地的发布流程常年也只会关注正在开发的版本,所以咱们这边再也不采用特定版本,而是规定版本只为两个: 发布版本
、 开发版本
,这么一来总体的数目就被固定了,只须要在版本发布时,将对应的版本告知数据接收方,刷新 发布版本
与 开发版本
的定义,存储关注的综合数据后,清空数据,而且只接收定义的这两个版本数据便可。
今天咱们主要分享了咱们在APM大盘这块的工程实践,而对于APM而言,拥有大盘只是其中必不可少的第一步,更重要的是对于大盘的使用,咱们经过在几个关键节点的流程上进行落地,在应用提测后,测试同窗、架构团队、业务团队分别有轮值的同窗进行跟进相关绿线,测试同窗主要负责完备的测试的绿线,而业务与架构的同窗确保应用质量的绿线,再达成后才可 beta
;另外咱们的Android团队有每周的AWTT&Party会议,其中有一趴就是对APM所展示的质量状况的Review。固然结合APM大盘对测试完备的定义,对应用质量的定义不管是用于咱们如今这套开发流程,仍是将来其余的开发流程对于应用的快速迭代以及可持续发展都是十分重要的,因为篇幅有限,咱们就不作细说了,下次有机会再与你们分享,也十分欢迎你们多多拍砖,评论。