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


    前言

    随着移动端业务复杂度的提升,开发同学在编写业务的时候往往容易忽略性能问题,虽然有赞移动端自研了 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 方法实现如下:

    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 需要解压),然后将流量内容写入文件中(非线上包才会采集),便于分析。

    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 )就可以进行复用了。

    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页面开始时机进行预埋。

    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() 第一次回调,定为页面绘制结束时机。

    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:推广到公司内部使用,协助解决有赞其他应用端性能问题。

    end

    
    


    ‍‍

  • 相关阅读:
    1月10日 TextView
    1月9日 布局2
    30 Adapter适配器
    29 个人通讯录列表(一)
    28 ListView控件
    27 登录模板
    26 Activity的启动模式
    25 Activity的生命周期
    24 得到Activity返回的数据
    23 Activity的传值2(bundle)
  • 原文地址:https://www.cnblogs.com/finer/p/14488252.html
走看看 - 开发者的网上家园