有赞移动性能监控平台(一)

2021-03-04 14:51:43 浏览数 (1)

作者:洪恩涛

部门:零售移动

前言

随着移动端业务复杂度的提升,开发同学在编写业务的时候往往容易忽略性能问题,虽然有赞移动端自研了 APM ,但是 APM 采集的都是线上的数据,无法在 QA 与开发阶段提前发现问题,为了保障软件的稳定性,需要补齐线下监控能力,避免性能问题上线对商家经营过程造成影响。

一、架构设计

整体基于 APM 现有框架迭代线下监控能力,并在端上开发 AWACS 可视化工具,通过全局悬浮窗,并结合提醒能力(弹窗与 Toast 提示)实时通知测试人员进行问题查看,同时后台也会定时分析测试环境采集的性能数据,进行管理与分配。

QA 同学基于 Appium 补齐了 UI 主流程 case ,通过自动化测试最大程度确保每次应用发版的稳定。而设备自动化回归流程可以与线下监控完美的结合起来,首先每个版本可以确保运行在同一款设备上,其次每个版本运行的主流程用例基本上保持一致,这样就为应用“前后”版本性能数据分析对比提供了两个参照条件。性能问题上报后,除了微信Robot通知相关干系人解决问题外,还基于移动端 mPaaS 搭建了问题管理与分配平台,便于跟进与追踪。

二、监控指标分析

性能监控目前对阶段、流量、页面耗时、 ANR 、慢方法、 fps 等数据做了实时监控,本篇文章只会对阶段、流量、页面耗时进行归纳分析,后面“有赞移动性能监控平台系列文章“会对 ANR 、慢方法、 fps 等监控数据进行总结。

2.1 阶段数据

移动端每个业务流程都可以统称为“阶段”,比如 App 启动、商品加购、商品查询等,业务方可以对自身需要关心的业务阶段进行监控,结合“数据分析”与“告警能力”快速协助业务方排查问题。阶段分析包括“方法耗时分析”与“网络状况分析”两个部分,下面会具体介绍。

2.1.1 方法耗时分析

在 App 编译期会对每个方法进行前后打点,确保运行过程中每个阶段方法耗时都可以被自动统计出来,节省手动打点统计成本。

原理

开发 Gradle Plugin 插件,在 App 编译 Transform 阶段( .class 转换为 .dex 过程),对字节码进行操作,在方法的开始执行( methodEnter )与结束执行( methodExit )通过 ASM 工具分别插入 MethodBeat 的 i 与 o 方法,对每个方法进行首尾打点,运行时自动统计方法耗时。

分析详情

应用所有版本产生的阶段数据都会上传到后端,后端会通过定式任务对阶段数据进行分析,分析角度分为新增、新减、陡增、陡降4个维度,协助开发综合对问题进行排查。启动阶段举例:

2.1.2 网络状况分析

业务方可以定义是否需要监控阶段的网络状况,比如启动阶段,除了要监控启动方法耗时之外,网络状态也需要进行监控( App 启动时,硬件负载比较高,过多的网络 IO 请求会拖累启动速度)。

原理

有赞零售 App 网络通过 OkHttp 进行请求,通过自定义拦截器对网络进行统一拦截,统计每个阶段网络链接数量与请求耗时, intercept 方法实现如下:

代码语言:javascript复制
public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = null;
        String url = getRequestUrl(request.url());
        if (!TextUtils.isEmpty(url)) {
            AppSegmentCache.INSTANCE.setRequestStart(url);
            long startNs = System.nanoTime();
            try {
                response = chain.proceed(request);
            } catch (Exception e) {
                throw e;
            }
            long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
            AppSegmentCache.INSTANCE.setRequestEnd(url, tookMs);
        }
        return chain.proceed(request);
    }
分析详情

分为“全部调用”与"重复调用"两个统计维度,“全部调用”会统计当前版本该阶段网络请求总数,且会与上个版本请求总数进行比较,计算出升降趋势(提升/下降了 x%),且列出“新增”与“新减”两个数据维度,协助对网络状况进行分析。“重复调用”会统计所有重复调用的接口状况,包括网络重复请求的总数,还会全部列出重复调用链接内容,方便业务方进行排查优化。

2.2 流量

App 运行过程中主要涉及到接口、文本、视频、图片等各种流量请求,往往在开发过程中不太会注意流量消耗这个指标,最近也经常有商家反馈 App 流量消耗比较大,但目前并不能准确的定位流量消耗主因。

2.2.1 原理

分别对 HttpUrlConnection 和 OkHttp 做 Hook ( .class -> .dex transform 流程插桩),所有的请求都得经过 Hook 层,这样就能统计每个请求的流量大小与 response 内容,便于业务方进行分析。

OkHttpHook

App 编译期往 OkHttp 中配置 GlobalNetworkInterceptor 拦截器,统计 response length(流量大小)、response content(请求返回内容,如果是 gzip 需要解压),然后将流量内容写入文件中(非线上包才会采集),便于分析。

代码语言:javascript复制
internal object OkHttpHook {
    @JvmField
    public val globalNetworkInterceptor = Interceptor { chain ->
    ... ...
    // 计算repsonse length(流量大小)
    // 读取response content(如果是gzip需要解压)
    // 流量内容写入文件中
    val fileUrl = File(file, URLEncoder.encode(SimpleDateFormat("yyyy-MM-dd-HH:mm:ss-SSS").format(Date())   "-"   netPackInfo.url))
    fileUrl.writeText(netPackInfo.toString())
    ... ...
}
HttpUrlConnectHook

编译期对 HttpUrlConnection 进行 Hook ,底层网络请求其实是代理到 OkhttpClient 上,这样就能保证 HttpUrlConnection 所有网络请求也能通过 OkHttp 进行处理,这样流量拦截器( GlobalNetworkInterceptor )就可以进行复用了。

代码语言:javascript复制
public object HttpUrlConnectHook {
    @JvmStatic
    fun proxy(httpUrlConnection: URLConnection): URLConnection {
        try {
            return hookOkHttpURLConnection(httpUrlConnection)
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return urlConnection
    }
}

@Throws(Exception::class)
private fun hookOkHttpURLConnection(httpUrlConnection: URLConnection): URLConnection {
    val builder = OkHttpClient.Builder()
    val mClient = builder
            .retryOnConnectionFailure(true)
            ... ...
            .build()
    val strUrl = httpUrlConnection.url.toString()
    val url = URL(strUrl)
    val protocol = url.protocol.toLowerCase(Locale.ROOT)
    if (protocol.startWith("http", ignoreCase = true)) {
        return HttpUrlFactory.OkHttpURLConnection(url, mClient)
    } else urlConnection
}
2.2.2 分析详情

通过展示网络各个统计指标数据详情,包括每个接口的请求流量大小、请求次数、请求内容,便于技术人员对流量问题进行分析。

2.3 页面耗时

有赞零售面向B端的产品,适配了很多低端收银机,在页面流畅性有严格要求,通过页面耗时监控,统计每个页面( Activity | Fragment )的耗时,当页面耗时超过阈值时,会生成问题,分配给相应的处理人进行修复。

2.3.1 原理

监控 Activity 与 Fragment onCreate() 方法开始执行作为页面绘制开始时机,页面 onDraw() 方法第一次回调时机作为页面绘制结束时机,两个时机做减法,算出页面渲染耗时。

页面监控开始时机

在 ActivityLifecycleCallbacks 全局监听 Activity 生命周期,在 onActivityCreated() 方法中调用 watchActivity() 方法,watchActivity 除了统一对 Activity 页面预埋开始时机外,还会区分 Activity 类型,对 Activity 内嵌的 Fragment 注册 FragmentLifecycleCallbacks 监听,同样在 onFragmentViewCreate() 回调中对Fragment页面开始时机进行预埋。

代码语言:javascript复制
public void watchActivity(Activity activity) {
    watchWithMonitorView(activity.getClass().getName(), activity.getWindow().getDecorView()); 
    ... ...
    if (activity instanceof android.support.v4.app.FragmentActivity) {
            ((FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentLifecycleCallbacks() {
                    public void onFragmentViewCreated(android.support.v4.app.FragmentManager fm, final android.support.v4.app.Fragment f, View v,
                                                      Bundle savedInstanceState) {
                        watchWithMonitorView(f.getClass().getName(), v);
                    }

                }), true);
    }
    ... ...
}
页面监控结束时机

监控页面根布局 onDraw() 第一次回调,定为页面绘制结束时机。

代码语言:javascript复制
public void watchWithMonitorView(final String className, final View view) {
        final long startTime = System.currentTimeMillis();
        final WeakReference<View> viewWeakReference = new WeakReference<>(view);
        final ViewTreeObserver.OnDrawListener onDrawListener = new ViewTreeObserver.OnDrawListener() {
            Boolean first = true;
            @Override
            public void onDraw() {
                if (startTime != 0 && first && viewWeakReference.get() != null) {
                ... ...
            }
        };
        view.getViewTreeObserver().addOnDrawListener(onDrawListener);
    }
2.3.2 分析详情

在设备自动化回归过程中一个页面会被多次调用,在线下监控环境中,只有一个页面3次超过耗时阈值( 200ms )才会算成有效的页面卡顿问题,防止硬件不稳定造成问题误报。

三、后台问题分析

设备自动化回归过程中产生的性能数据存在一定的波动性,后台需要对批量性能数据进行算法校验,评估出合理的有效问题,减少问题误报。

分析工作流程(阶段数据分析举例):

设备自动化回归过程采集到阶段数据后,上传到移动网关,晚上7点启动定时任务,在后台拉取当前应用版本各个阶段最近n条数据,进行数据聚合分析,计算出合理的阶段耗时平均值,再同样拉取当前应用上个版本各个阶段的最近n条数据,同样算出阶段耗时平均值,与当前版本阶段耗时平均值进行对比,算出涨跌幅度,如果超出阈值就会当成有效问题,进行分配与告警。

四、线下AWACS工具

在 QA 与开发过程中, App 上会悬浮告警 ICON ,开发者可以点击告警 ICON 打开性能监控中心进行数据查看。性能监控中心会展示阶段、 ANR 、慢方法、流量、 FPS 等性能数据,便于开发对问题进行排查。

五、问题管理与分配平台

后台对问题进行分析后,如果是有效问题会落到后台 db 中,前台在 mPaaS 搭建一套问题查看与分配 UI 看板,方便业务方对问题进行处理与状态跟进。

5.1 问题列表

APM 监控的所有性能指标都可以在性能面板中进行切换筛选, tab 选中后再结合应用、状态、环境筛选器列出问题列表。

5.2 问题详情

点击问题列表后跳转到问题详情,问题详情中包含问题发生次数、进度、详细信息、设备基本信息等,协助开发定位问题。

点击问题详情右上角“变更状态”按钮,可以变更问题状态,并可选择负责人对问题进行分配与跟进。

六、未来规划

1:监控更多维度的数据,包括 cpu 、线程、子线程更新 UI 等。

2:增加更多的自动化测试用例,针对特定的性能场景进行独立测试(现在测试用例基本上都是主流程,覆盖度还不太够)。

3:补齐自动化设备的数量与型号,通过多机型综合分析性能问题。

4:推广到公司内部使用,协助解决有赞其他应用端性能问题。

0 人点赞