|
海神平台崩溃监控(iOS)实践
海神平台崩溃监控(iOS)实践
赵晓萌@贝壳找房
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2020年07月02日 20:17
命运对勇士低语,你无法抵御风暴。勇士低声回应,我就是风暴!1. 前言崩溃率作为客户端质量的一项最为核心的指标,是评估产品质量及影响用户体验和留存的重中之重。那么如何快速暴露、发现、定位并快速修复崩溃,成为项目组人员最为关注的问题。海神平台是贝壳自建的一个移动端监控平台,为贝壳系各应用提供了包括崩溃、异常、网络、自定义错误等监控功能,同时单设备检索功能能够串联各个维度监控信息以及各平台信息,提供更全面的故障现场资料。本文将从iOS崩溃监控出发,讲解海神平台崩溃监控模块的系统设计、数据流处理以及客户端SDK设计及实现原理。2. 项目背景在海神之前,贝壳系产品使用的是第三方的监控平台,如谷歌的Fabric、腾讯的Bugly等。在使用过程中,存在dsym文件上传困难、报警系统滞后、bug分配不明确、历史问题堆积等等问题,严重影响工作效率,导致问题定位不明确,产品质量下降。同时使用第三方监控平台,一方面信息属于对外暴露,不利于产品信息安全,另一方面对于定制化需求,第三方平台是无法提供服务的。为解决这些问题,并与各个业务线沟通,结合项目的特殊应用场景与定制化需求,我们最终确定了海神的崩溃监控模块应具备的能力:崩溃分流到具体业务方自动预警埋点、设备信息以及系统日志等辅助信息收集非崩溃类型的自定义异常以及错误的上报缺陷管理3. 系统设计如图所示,海神平台的崩溃系统可分为以下几个功能组:客户端:崩溃监控、堆栈收集、系统日志、埋点信息、设备信息、附加信息、存储、上传持续集成平台:dsym文件、组件信息、分流表、业务线信息海神后台文件系统:业务线信息、聚合白名单、崩溃文件存储系统、分流系统、系统基础库海神后台数据处理系统:高性能接口、数据队列、资源抽象、数据处理、报警、解析聚合、分流、分配到人、报警系统等等联动公共服务平台:keOnce平台、大数据平台、员工管理平台等本章节针对其中几个关于客户端方面的技术难点,展开阐述。3.1 数据采集及配置持续集成平台提供dsym文件和符号、库、业务线信息对应关系信息(即分流表)。海神平台的配置模块提供各业务线、Crash聚合策略及白名单等配置信息。App内嵌LJBaseCrashReporter基础库,负责收集崩溃堆栈信息、埋点信息,上下文信息以及设备信息等,上传至文件系统。3.2 解析与聚合首先持续集成平台产出偏移量、符号表、各个二进制库的UUID信息。而后Crash SDK上报崩溃信息,崩溃堆栈中包含每个UUID、基地址和目标地址。海神平台通过对比可查到对应符号,利用配置模块里的白名单以及聚合策略确定目标崩溃堆栈,从而聚合出一类崩溃。基于Google的EP思想,可根据崩溃系统提供的资料开发一些工具,提高我们的生产力。系统通过原始的崩溃日志,结合崩溃上下文以及埋点信息聚合崩溃类型,产出回归测试以及自动化测试用例。3.3 分流分流系统将Crash通过堆栈自动化分配至各业务线,并通过报警系统通知各个业务线负责人。精准的分流为快速定位与解决问题提供了强有力的保障。由于苹果使用了ASLR技术,高低地址需要重新映射,所以我们所有的地址关联与确认都是使用UUID以及偏移量来确认的。3.4 Web页面展示如下图所示,海神系统-崩溃监控页面提供了从多维度查询的功能。如版本、时间、业务线、Crash信息以及自定义异常等维度。展开内容则包括业务线、崩溃量、设备信息、用户信息、埋点、崩溃堆栈、系统日志等信息。4. 客户端设计4.1 综述客户端SDK包含如下功能:调试面板支持配置异常捕捉开关,崩溃信息本地查询以及手动上传日志。当前SDK基于第三方开源库KSCrash,上报异常类型包括:CPP Exception、Zombie、Mach、NSException、User(自定义的异常)、Signal。自定义辅助信息包括:设备信息、埋点查询信息、系统日志,网络日志。可添加代理自定义添加信息,获取日志,更改操作方式。提供自定义异常以及错误监控的功能。启动崩溃检测,以及崩溃的同步上传。4.2 基础组件的subspec设计为保证基础组件的通用性并减轻特定业务方的接入成本,采用了如图所示的subspec设计。Test只在自动化测试中引入,其中包括对各功能的触发单元测试,保证基础组件在多线程等极限情况下的稳定性。4.3 SDK核心架构设计SDK流程如下图所示,在注册配置阶段,一方面是各种崩溃类型的注册,另一方面是各种状态下对外代理的注册。为了增加SDK的扩展性以及鲁棒性,中间监控与数据部分由SDK封装处理。崩溃时,写文件为注册代理提供入口方法。最后上报阶段,同样为代理提供了信息添加、审查或转接的机会。几个Protocol则是这个SDK对外的暴露数据的接口,同时也是外部可以访问SDK的唯一途径。同时通过这个对外接口,实现各类辅助信息的SDK进行插拔录入信息。添加ConfigSetting,实现ExtroInfo协议,在崩溃时添加自定义信息,此时运行时环境已挂起,所以不建议使用runtime。添加UploadSetting,实现UploadEmbarkation协议,可在上传前查看并更改信息。自定义异常上报,当创建NSException时,会自动触发一条异常的上报。目前SDK还支持自定义错误的上报,可提供多平台语言的异常上报,以及业务方的手动打点上报。自定义错误的架构设计如下:4.4 监控崩溃目前SDK可捕获到NSException异常、C++异常、Mach级的内核异常、信号异常以及僵尸对象。4.4.1 NSException异常的捕获注册:使用NSGetUncaughtExceptionHandler()获得原始handler,使用NSSetUncaughtExceptionHandler()设置自己的block。触发:当发生NSException时,触发block,执行自定义操作。同时这里有一个需要注意的点,就是在注册使用这个方法后,一定要将这个exception用原始的handler将其继续抛出,以便其他监听者使用。若想保证自己可获得NSException的handler,可以将自己置于所有监控的最后再注册。方法名功能官方解释NSGetUncaughtExceptionHandler()获取原始handlerA pointer to the top-level error-handling function where you can perform last-minute logging before the program terminates.NSSetUncaughtExceptionHandler()设置自己的handlerSets the top-level error-handling function where you can perform last-minute logging before the program terminates.4.4.2 C++异常的捕获与NSException操作同理,使用标准库中的std::set_terminate()将自己处理信息的block设置好,在异常发生时block被terminate()触发,再使用std::terminate_handler将原始异常抛出,供其他监控正常使用。若不设置set_terminate以及set_unexpected(),unexpected()默认调用terminate(),最后调用abort函数退出。C++的异常进一步了解可参考Itanium C++ ABI:Exception Handling(https://refspecs.linuxfoundation.org/abi-eh-1.22.html)4.4.3 Mach异常的捕获Mach是一个XUN的微内核核心,Mach异常也是最底层的内核级异常。Mach有部分API暴露给用户可以来设置thread、task以及host的异常端口数组。通过task_get_exception_ports()方法获得目标task的异常端口的权限,申请一个新的端口的权限用来监控目标task,将新的端口插入,使用task_set_exception_ports设置新端口为处理异常的端口,创建异常处理线程,添加此线程。其中有一个循环,一直等待这个端口将异常抛出,当崩溃发生循环退出,获取所有线程并挂起,处理信息后,再将环境释放,使得程序正常退出。4.4.4 Signal信号异常的捕获Mach信号是最底层的内核,当Mach异常没有使程序退出,它会进一步转化为UNIX信号。Darwin是基于FreeBSD开发的系统,如下图所示,BSD层在Mach层之上,它对Mach内核进行了进一步的封装和扩展,兼容POSIX和更多的API。UNIX Security模块提供syscall支持,BSD Process模块(包括Process的ID和Signal),各种FreeBSD内核以及POSIX的API、POSIX Threads。各类信号通过BSD层转换为UNIX信号来兼容更为流行的SUS规范。Mach异常在host层被ux_exception转换为UNIX信号,同样需要替换处理信号的函数栈来捕获异常。需要注意的是有一些信号是无法被捕获的,如SIGKILL或SIGSTOP,它们是提供给管理员操作进程的信号,进程内部无法捕获。造成这种崩溃的原因,如启动加载资源爆内存造成长时间无法启动、卡死等,这时系统管理员会将此进程直接杀掉。这种情况KSCrash是没有解决的,感谢FaceBook以及手Q通过他们的实践,给出了相对可行的解决方案。可利用排除法,对退出原因进行筛选从而唯一确定为SIGKILL的退出,后期我们的SDK也会将这个方案加入,提供更全面的崩溃监控。创建一个处理异常的sigaction,使用sa_sigaction设置处理信息的block。POSIX标准的信号处理接口是sigaction()函数,将刚创建的异常处理sigaction替换原有的几个异常信号处理的函数栈。被触发后的处理与Mach异常处理相似,最后需要手动调用raise()使得程序正常退出。4.5 堆栈捕获函数的调用与执行就是对内存的各种操作,每个未执行完成的函数都会占用着一段连续区域,这就是Stack Frame。而函数的调用往往都是嵌套的,这时候就需要一些指针来标记它的位置。X86以及ARM64使用的指针寄存器稍有不同,以下以ARM64为例。栈是从高地址到低地址,栈底是高地址,栈顶是低地址。fp指向当前Frame的栈底,sp指向栈顶。ARM寄存器详细列表如下:由此我们不断回溯,便可以获得所有的调用堆栈信息。SDK上报的堆栈信息包含对应库的UUID,以及对象地址。通过组件库的起始地址,便可获得对应堆栈的偏移量了。4.6 启动崩溃检测为检测启动崩溃,需要将监控SDK在第一时间加载进程序。可使用“动态库”来解决这个问题,苹果的官方名称为Embedded Framework,它不同于系统库中的动态库可被所有的APP加载,只可被自己的App所加载。在iOS8之后,由于一系列Extension的出现,允许开发者在自己的单一进程下几个“APP”共同使用同一个“动态库”。静态库加载过程(苹果官方图):动态库加载过程(苹果官方图):那么以贝壳为例,海神是如何实现启动崩溃检测的呢?创建LJShellLaunch动态库(其中包含崩溃监控SDK),设置LJShell的启动依赖。Mach-O是iOS上的可执行文件,描述代码以及内存信息。其中Load Commands中包含了区域位置、符号表、动态符号表等信息。查看Mach-O文件结构,可发现LJShellLaunch已添加为动态库。当程序启动时,系统将Shell的Mach-O推进栈,启动dyld,开启缓存加载各个依赖库,这时便会将LJShellLaunch加载。随后runtime初始化类,dyld返回main()函数地址,完成程序启动。4.7 同步上传与补传NSURLSession提供Background Session(iOS12系统以上使用),可将一个网络任务在后台委托给系统进程执行,我们利用此功能,实现了崩溃发生后的立即上报,保证崩溃监控的及时性。这是一个动态链接库,会使用dlopen函数启动这个动态文件,并返回一个句柄给调用进程,将此动态库加载进程序进程中。但在崩溃时,整个线程状态悬挂,句柄无法返回,造成死锁。在不安全不稳定的状态下,应使用C语言,而不是运行时语言。iOS11及其以下系统使用cURL开源库来实现网络功能。cURL作为命令行工具大家并不陌生,它可实现数据通过url的上传下载,还可携带cookie等信息,更多详细信息可以登录它的官网查阅(https://curl.haxx.se/)。补传会在每次程序启动以及前后台切换时检查,手动调用上传接口,进行补传的操作,保证数据准确上传。5. 写在最后稳定性监控是移动端的一个需要持续性投入的方向,还有很多功能值得我们去探索和实践。我们会陆续将海神平台建设过程中的经验和沉淀分享给大家。如有任何问题或者想法,欢迎联系人店平台业务架构组进行深入讨论。
预览时标签不可点
大前端69移动端37大前端 · 目录#大前端上一篇webpack loader开发下一篇直播基础技术-原理篇关闭更多小程序广告搜索「undefined」网络结果
|
|