基于大数据平台构建移动端APM实践
作者:王思宇
嘉宾介绍:王思宇,七牛云高级研发工程师,2015 年加入七牛,曾在 客户端团队担任 iOS 负责人,先后主导过直播推流 SDK,播放器 SDK,连麦 SDK 等客户端 SDK 的设计和实现,积累了大量客户端架构设计及性能优化经验,所推出的直播及连麦 SDK 服务于包括熊猫,美拍等在内的百余家客户。后参与 APM 产品设计和研发,主要负责后端及大数据平台和数据处理相关工作,致力于将大数据平台应用于更多的业务场景,在大数据平台应用方面有丰富的实战经验。
七牛APM产品的初衷
“移动直播”可以说是去年最为火爆的一个关键词,200多家直播平台,3.25亿的用户规模让2016年被称为“直播元年”,而七牛直播云其实 是一个站在风口之前产品 ,七牛从2014年就开始做直播产品 。对直播来说,性能一直是比较重要的问题,怎么从宏观和微观的角度去知道直播的质量怎么样,怎么去排障,这涉及到一系列的数据收集、展示和分析相关的工作。所以从直播的项目里面,我们就延伸出了直播 APM的产品,后续我们开始深耕移动端性能监控领域,陆续 推出了更为通用的 崩溃拦截、HTTP监控 等相关的功能。
大家都知道,七牛是做云存储起家的,在这个基础架构上又做了直播和点播的富媒体、 容器云、大数据平台以及AI等产品。也 是因为有这么强大的基础能力,我们才能快速地构建出APM产品。
今天,我的演讲将分为三个方面:APM简介、产品整体架构介绍和 iOS 实现细节 。首先是APM的介绍,APM指的是应用性能管理,它 主要分为三个模块,第一个是数字化体验监控,主要是体现 在监控这个方面,把一些性能做量化、采集和展示;第二个是应用程序发现跟踪和诊断,这个主要偏向服务端应用的监控 ;第三个是应用程序分析,这个更高级一些 ,主要 包括模式的发现和诊断,以及机器 学习和AI相关的东西,以期 更加 智能化地进行性能管理 。
说到应用程序性能监控,我们到底该监控哪些指标呢?常见的一些痛点,第一个就是我们最常见的 cra sh ,我们希望能够快速地暴露、发现、定位和修复问题,能够统计错误,最好是长时间的保存,便于我们做一个后续的聚合分类,能够把同类的问题做跟踪。第二个是首开交互响应比较慢,我们希望首开时间能量化,能够方便我们从宏观和微观两个角度对问题进行排查。第三个是网络问题,网络最复杂也是问题最多的,连接超时、网络错误、CDN等等。最后,从宏观上没有办法解决问题的话,很多时候可能是单个APP,即单个用户、特定机型、特定情况、特定场景下发生的问题,这时你让客户给你报日志是不可能的,所以需要一个机制能把特定用户的日志采集起来,方便我们做更深层次、更细节的定位。
今天我主要是结合上面的这些痛点,来讲解一下我们是怎么来做监控的,大致的总结一下需求: 首先我们希望有一个实时的监控,另外的话第一层次是基于阈值的告警,再深层次是能够做一些基于异常模式发现的告警。再就是对数据归类聚合,最后是离线的分析和报告。
图1
要实现这样的功能,市面上能够选择的开源框架非常多,至少有100多种(图1只展示了一部分),从监控到存储、集群调度、批量/实时计算、数据可视化,所以到底选择什么样的开源框架去搭建我们的系统 也是个很纠结的问题 。
图2
最开始我们做了上图这样的一个版本,从 Mobile App 开始,由网关 flume 做一个收集,然后再到 kafka 实时和非实时的topic,通过spark streaming到下游的hdfs,以及 mongo db、influx db 等下游数据库,最后用 kibana 以及自己做的 portal 去做展示。但是这样存在什么问题呢?其实大家可以看到整个架构非常的复杂,首先在搭建系统的过程当中可能会遇到各种各样的版本及兼容问题,其次这个架构运维起来非常复杂,另外infuxdb的集群方案早已闭源了,我们怎么自己去实现集群部署,我们怎么去保证高可用,最后还要去做技术组件的性能调优,这整个的工作量是非常大的。但是其实我们想把精力专注在业务层面,并不想做这些基础的东西,怎么办呢?这个时候七牛的Pandora大数据平台推出来了,我们觉得它很符合我们的需求,从数据存储到数据可视化都有,它主要通过Workflow的方式,把一些开源组件进行整合,另外在集群方案、运维、性能调优上都做了很多的工作。它整个的架构图如下图所示。
图3
数据进来后到消息队列,能够做很多的计算,然后导出到相应的下游,如 HTTP、MongoDB、时序数据库、日志检索服务,最后做展示,另外 report studio 还提供了报表的支持,XSpark做离线的分析。有了七牛大数据平台pandora后,我们APM产品最后的架构变成什么样子了呢?如图 4 所示,最后我们只需要关注红色框框的那几块了。
图4
首先我们 SDK 的开发逻辑是要在客户端把数据收集上来,然后在网关做数据的聚合,然后到pandora 的 pipeline 我们的数据就OK了,然后只需要拖拖拽拽,相应的流程很快就出来,最后做portal的展示,另外配置下监控告警就可以了。这个时候,我们不需要关注那么多运维的东西,不需要关注业务组件如何去优化,只需要关注业务怎么做就可以了。今天是主要是偏移动端的东西,所以主要是讲APP这方面的东西。
经过仔细分析,总体来说我们希望Mobile SDK实现这些目标:
- 兼顾数据的充分性与带宽占用
- 与业务解耦,实现无埋点嵌码
- 不影响宿主APP的性能和稳定性
- 尽可能不出现数据截断或漏报错报的情况
用户基础体验监控
第一个功能是用户基础体验的监控,支持首开时间和页面跳转时间的监控采集,因为首先你得把数据量化起来,才能看到进一步的优化。我们最终是选择面包屑+时间戳的方式去做采集,因为通过面包屑我们能很方便的完成首开时间采集、点击后跳转页面需要的时间、 崩溃复现步骤 这三个功能。
图5
图6
第一个选择的面包屑是在启动的时候去添加,这里我主要讲iOS,安卓也是差不多的原理。sdk 在启动的时候,会撒一个面包屑。然后通过iOS这边的runtime的方式把IOS系统的 sendAction:to:from:forEvent: 方法,给 hook 掉了,这个方法是干嘛的呢,这些按钮去点击的时候,都是会有这个事件,你要用户去点击这个按钮的时候,总会对 UIApplication 发送这个事件,如果我把这个事件给 hook 掉,那用户点击按钮的这个行为,我就能够知道了,所以我能够把用户点击的事件给记录下来,然后把相应的时间记录下来,所以这个地方,我可以撒一个面包屑。下面是 ViewDidAppear 方法,做 iOS 的都非常清楚,我的每一个窗口出来的时候,都会撒一个面包屑。
那么通过这些面包屑,我们怎么去计算我们需要的一些时间呢?第一个是首开时间,假设我们定义 vc1 是首屏,那么 vc1 对应的面包屑的时间戳与start的面包屑时间戳之间的差值便是首开时间。另外就是页面跳转的时间监控,实际上页面跳转的时间就是用户在前一个页面进行点击操作到后一个页面完全渲染出来的时间差,因此只需要找到相关页面的 ViewDidAppear 事件及相应点击事件求时间差即可。
APP 崩溃日志收集
APP 崩溃日志收集这个功能的需求如下:
- 支持收集包括 Objective-C, Swift,C/C++ 在内的crash信息
- 支持 dump crash 时的调用堆栈
- 支持收集发生在 crash 之前的用户操作细节
- 支持 crash 堆栈的符号化
首先是crash采集内核的选择,业界比较好用的两个框架是 KSCrash 和 plcrashreporter,用的比较多的是plcrashreporter,很多硅谷大厂包括微软和 twitter 等也对它做了规模化验证,所以我们这边也选择了plcrashreporter 这个框架,同时我们希望提供一个中间层方便做切换。
图6
Crash 收集的架构大致是这样的,首先是在进入 App 的时候,注册一个handler,在crash发生时,能够抓住当前的crash,dump 当前的堆栈。符号化有2种方案,第一种是客户端符号化,但是这个会存在一些问题,iOS 在加载动态库的时候,很多符号不是存在于内存里面而是做本地文件保存,但是这个文件由于权限原因我们无法拿到,所以这里的符号话我打了一个虚线,想要更精准地进行符号化,服务端进行符号化的方案也是必不可少的。另外就是在本地做存储,在下次启动的时候做检查上传,会把metadata即一些描述信息上传到pandora的logdb。大家如果对服务端有所了解的话, logdb是ELK方案中的一部分。然后把总的 Crash的堆栈情况,上传到对象存储,最后通过portal对它做展示。
图7
图8
具体来说我们这边实现的方式是在应用启动时,调用 plcrashreporter 的 startManager 方法,可以看到有一个OnDeviceSymbolicationEnabled,这个就是本机符号化的一个选项,我这里是做了默认开启的选项。下面是做 report 的 handle,每次启动的时候,会去 handle 上一次的 Crash。大家知道 debugger 在连接的时候,Crash的信号是没有办法做拦截的,所以我们做了一个判断。下面就是开启了plcrashreporter。这里有一个坑:plcrashreporter 没有办法收集 c++ exception,所以我们自己添加了c++ exception的handler。下面看下 c++ exception的handler是如何做的,我这边加了一个 terminate 的回调,然后 terminate 这个回调会把当前的堆栈dump下来。最后展示出来是这样的一个情况,上面是一些描述信息,包括APP的一些本机信息,下面是Crash的堆栈。
图9
图10
HTTP和Webview的请求分析
HTTP和Webview的请求分析需求如下:
- DNS 解析、服务器响应、请求结束耗时
- 服务器 ip
- 下载资源的数据量
- 网络错误码及错误描述
- 服务器响应状态码
网络错误和服务器响应错误的区别是,网络错误是指网络连接超时这一类的错误,就是请求根本没有到达服务器,服务器根本不知道请求来了,网络错误或者根本连不上网、服务器挂了等类似的问题都体现为网络错误。然后服务器响应错误主要是一些后端的问题, 要么是path 不对,要么是后端服务挂了等,但是 gate 是能连接上的。
我们这边是如何实现网络监控的呢?首先我们记住一个原则,就是要实现无埋点监控,所以肯定不能封装一个网络库让用户去调,这样的话用户迁移起来是非常麻烦的,我们这边选择的是使用系统的 NSURLProtocol 类,NSURLProtocol 类是苹果用来让 NSURLSession 和NSURLConnection 能够支持更多网络协议的一个系统。它的大致用法是这样的,苹果的 NSURLSession 只支持 HTTP,但是如果你想做自定义的协议,例如你要做UDP、quic、RTMP等各种协议,而你又想直接用苹果的网络接口来进行访问,而不是自己提供接口,这种场景下你就可以使用 NSURLProtocol 来给系统的网络库添加自定义协议的支持, 首先实现一个 NSURLProtocol 的子类 A,然后通过 NSURLProtocol 的 registerClass 方法注册 A,系统会在外部调用 NSURLSession 的时候询问 A,要不要代理这个请求,另外你还可以在这个子类的 `canonicalRequestForRequest` 方法中对这个request做一些操作,比如去进行 HTTP DNS。然后系统会对每个请求初始化一个 A 的实例,去进行网络请求的代理,另外 NSURLProtocol 也提供了给请求动态添加属性的方法,可以用于添加一些协议相关的属性。
图11
具体来说我们是这样实现的,首先在 sdk 启动时直接把我们自己实现的 NSURLProtocol 子类进行了注册。
图12
在过滤是否需要代理的请求时,我们选择只代理了 HTTP 和 HTTPS,在非 HTTP 或 HTTPS 时会返回 NO。因为我们不想自己发送的一些请求也被代理掉,所以设置了一个名为 `PREDInternalRequest` 的 property 用于识别内部请求,并过滤掉。
图13
这个地方是自己先做了一个 HTTP DNS 的请求,将 host 替换为请求到的 ip,这里使用的是我们七牛自己的 http dns 的库,叫happy dns,如果大家需要用,可以很方便地在我们的 github 上找到,它也是非常好用的一个库。为什么这个地方要判断的一个HTTP呢?主要是因为同一个服务器可能有多个域名,域名不同,证书就不同。一台服务器有多个证书,我要怎么来告诉服务器来我要哪一个证书呢?所以需要先去请求一下,告诉你我要请求的是哪一个域名,可以通过谷歌提出的SSL的扩展协议告诉服务器,我需要是哪个域名的证书。但是苹果的 NSURLSession 的底层是不提供对这个扩展协议自定义化的,所以本来我们当时想选择 ASIHTTPRequest,它是基于 CFNetworking 做的一个库,比较底层,但是这个库在2014年就停止维护了。为了稳定性考虑,我们还是直接用了苹果自己的 NSURLSession 和 NSURLConnection,就没有去支持HTTPS。
图14
然后这里在startLoading 的时候,会去采集包括请求开始时间、结束时间以及DNS查询时间等,然后生成一个新的HTTP请求去替代原始请求。在这个请求返回的时候,我会告诉NSURL Session这个请求已经结束或者失败等,从而完成一个整个代理的过程。整个过程用户是完全无感知的,他不知道自己的请求是被代理的,他以为直接是从系统返回的。
图15
然后讲一下刚才没有提到的地方,在图11中加了一个 Swizzler,这是做什么的呢?其实实现NSURLProtocol 也是有很多坑的,如果你用 NSURLSession 的 sharedSession 实例,发出的请求是无法正常被 NSURLProtocol 代理的。因此我们选择通过替换 NSURLSessionConfiguration 的 __NSCFURLSessionConfiguration 方法来实现对 sharedSession 的代理。
下面是我们最后得到的效果,这边是展示的下载速度、网络请求错误等,我们可以分各种不同的维度做整体的展示。
图16
UI卡顿监控
UI卡顿监控和我们刚才提到的基本性能监控是有区别的,基本性能监控强调的是用户从点击开始到下一个页面跳出来的时间,会更偏向于做时间的监控,我要知道每一个动作对应这个到下面的一个页面花了多长时间。
但是一些例如滑动操作的场景下,虽然我们知道滑动比较卡顿,但是没法定位是在哪里卡顿了,这样就会带来卡顿监控的需求。另外安卓的ANR的问题也可以通过这种方式来进行监控。
总结一下我们的需求,首先是需要准确获取UI卡顿事件,获取到这个事件后,我希望能够获取到当前卡顿的调用堆栈的环境信息,这样看堆栈就能知道卡顿的位置,另外符号化也是必须要做的。
图17
图18
图19
下面看一下我们是怎么实现这个功能的,先简单介绍一下 iOS 的 runloop 的概念,runloop 在底层其实就是一个 while 循环,在这个 while 循环当中会执行一些系统及用户自定义的代码块,以主线程的 runloop 为例,系统启动之后,iOS 的主线程会起一个主线程的 runloop,这个 runloop 首先会负责用户事件的监听和分发,比如点击、摇手机、敲键盘等。另外是 timer 的检查和事件分发,如果你将一个 timer 放到某一个 runloop 中,那么这个 runloop 就会不断轮询检查 timer 是否到期,如果到期便会进行事件的分发。另外就是用户自定义的代码块,我们可以将自定义代码块放入 runloop 当中等待被执行,主线程的 runloop 主要负责 UI 刷新以及事件的分发,如果你在主线程当中执行非常重的操作,那么就会出现 UI 卡顿以及用户行为得不到响应。因此,首先,我们启动了一个后台线程,等待信号量被出发,然后我们在主线程 runloop 中放了一个 observer,runloop 每跑一圈,就会调用一次 observer,observer 会触发这个信号量,从而刷新子线程的超时时间,进入下一个轮回,当主线程被卡住的时候,信号量便无法被正常刷新,因此子线程会在超时时间到达之后收集当前的调用堆栈并进行发送。
图20
网络诊断信息
图21
当某个用户报了这个问题,但是我们并不清楚用户网络的情况,我们希望能够提供一个上报当前网络情况的功能。比如他的ping值,到服务器通不通,他的http request 是否正常,以及 DNS 的信息是否正常,也就是网络的一些 metadata 信息进行上报。这方面的功能实现比较简单,直接用的七牛的网络诊断库 HappyDNS,这个库是开源的,可以到七牛的 github 上找到直接用。
图22
日志上报
我们思考一个直播 app 的场景,直播时整体状况都没有出问题,但是某一个大主播一直报问题,该怎么办呢?这时候我们就需要客户端具有日志上报的功能,这样用户的问题,就能够通过日志做一些非常细致的分析了。
图23
下面我讲解一下我们这个功能实现的方式,从日志的打印接口进去,当日志进来之后会有两路输出,一个是 console,是调试用的,另外是文件系统,保存之后做上传,这两个是可以独立来设置打印级别的,比如说我 console 打印需要 debug,而只需要 error 或者 warning 才去做上传,都可以自定义的做一个设置。另外这里需要提供一个start 和stop的接口。不是每一个用户的日志都需要来保存,一般都是vip用户,需要他上传日志的时候才上传,所以是 start之后才上传。后面的大致逻辑和crash差不多,把 data 传到 logodb 里,把真正的日志传到对象存储,然后通过 portal 做展示。
图24
图25
实现上我们并没有重复造轮子,直接采用的iOS上最好用的一个打印框架 cocoalumberjack 。在开始采集日志的时候,会初始化一个 logger ,然后每一个日志生成之后,会把文件目录返回给你,对相应的文件做保存和上传。
自定义上报
自定义上报功能是希望采集一些SDK没有直接采集到事件方便用户做自定义的分析,用户把这个自定义的数据打过来,直接传到 pandora 平台,自己可以做自定义的一些功能。其实我们之前的功能,也都是通过同样的方式实现的, 图26展示了一个典型的 pipeline 结构,可以通过简单的拖拽以及 sql 满足包括漏斗模型,用户转化率 等绝大部分需求。
图26
最后我们的SDK是完全开源的:
安卓地址: https://github.com/pre-dem/pre-dem-android
iOS地址: https://github.com/pre-dem/pre-dem-objc
希望大家也能参与进来,和大家一起成长,谢谢大家!
End.
转载请注明来自36大数据(36dsj.com): 36大数据 » 基于大数据平台构建移动端APM实践