人工处理崩溃报告的过程重复性高、过程繁琐,令人十分痛苦。来自京东的王永杰老师带来了京东手机京东 crash 自动分析处理系统的实践经验,并展望了利用机器学习实现的智能化崩溃信息分析的未来。
此外,ArchSummit 全球架构师峰会深圳站 将于 2016 年 7 月 7 日~8 日在深圳·华侨城洲际酒店召开,京东商城运营研发部总架构师者文明策划了《低延迟系统架构设计》专题,将会为大家分享目前各大互联网企业在低延迟系统架构设计上都有哪些新思路,欢迎关注。
去年 618 活动中手机京东的单量占比已经超过了 80%,一度超过 85%。今年 618 活动才刚刚开始,很有可能会再创新高,毋容置疑现如今已是一个移动的时代。
手机客户端是移动时代最为重要的载体之一,而质量又是客户端的生命线。如果一个手机客户端总是崩溃或者卡顿的话,用户的体验就会很差,转化率等一系列核心数据指标就会收到严重影响。然而这种情况却十分普遍,特别是面对安卓系统的千差万别,厂商的定制乃至各种山寨机,不同的系统版本时,保证客户端质量是一项非常严峻和有挑战的事情。
很多因素会影响到 App 的质量,比如性能与稳定性。而稳定性的一个最重要的指标就是 crash 率。本文的主题就是围绕着如何将 crash 的分析处理进行自动化甚至于智能化。
我现在主要负责手机京东开放平台,本文分享的实践来自于京东开放平台的支撑体系的一部分,我们需要保证业务部门在这个平台上进行业务开发的质量、性能以及开发效率等各个方面。
业内对于 crash 率的定义一般情况有两个。一个叫”崩溃率“,通常来说定义是每天 0 点到 24 点的 crash 的数量除以启动次数。还有一个比较重要的指标是”去重崩溃率“,大多数公司会更看重这个指标,它一般是影响用户数除以 DAU。
有的应用的平均崩溃率在百分之几, 游戏类的应用可能达到 9% 乃至更高,有兴趣的可以看看腾讯 Bugly 年初发布的 2016 年度数据报告。
京东做了这么多年,最开始的时候 Crash 率指标也曾经到过百分位。因为京东的用户量比较大,最开始的时候崩溃报告非常庞大,甚至需要分段分多次才能导出完整的数据。不过经过努力,崩溃率在之后减少到了千分位,而目前京东已经迈入了崩溃率万分位的时代。
保持产品稳定性,降低崩溃率,是蛮痛苦的一个过程。因为它不仅涉及到崩溃信息要尽可能多收集的同时,还要去分析崩溃信息、定位问题并找出解决方案 。今天本文重点是崩溃的分析和处理过程。
首先来看一下人工处理崩溃报告的过程。这是比较遥远的处理方式,在当时来说这种过程十分令人痛苦。
简单来讲 crash 首先会通过用户主动提交,收集存储到服务器。我们主要来看一下虚线框里的环节, 工程师需要从服务器中导出数据,对这些数据进行去重、排名,根据重要性筛选分析;经过初步分析后,定位相应的模块,然后通过邮件分发给指定的工程师;工程师会具体分析和解决问题,最后将代码提交到代码仓库。这个过程需要有人工来负责崩溃的统计、分析以及 Bug 的分派。还有一个没提到的事是,问题处理过程不好跟踪。如果没有一个系统去管理的话,就不容易知道 crash 问题到底处理没有处理,以及问题处理到哪个阶段了。
有时候 crash 表面上起来看起来是属于 A 模块,但实际上它属于 B 模块,这种类似的问题很多。同时开发过程,一个是有灰度的阶段;还有日常的阶段,需要针对不同的阶段使用不一样的方式针对 crash 进行处理。
总结一下人工处理 crash 流程的问题。首先它比较繁琐,特别在灰度期间,工程师可能需要每天把这数据导出一遍,然后去处理排名、分析,整个流程特别繁琐,重复率高,而且效率比较低。处理的实时性不高,毕竟工程师不可能一直盯着 crash 报告的产出,一般情况都是一天一维度导出一份 crash 报告,处理完之后然后去分发给相应责任人。
人工处理可能会出现遗漏,比较典型的情况是在在线上运行的这个阶段。这个阶段的话可能有一个业务的 UV 并不太高,它有一些崩溃而且甚至是必现的问题,但是由于线上收集到的总的 crash 数量特别大,导致这个问题可能占比比较小,就被掩藏在其他的崩溃里边,导致遗漏。 再一个是刚才提到的,我们如何去做 crash 的跟踪与统计。
接下来讨论下怎么去改进 crash 的处理过程。我们都有什么资源可被利用呢?我们有工程师还有机器,工程师擅长创新,而机器不知疲倦,擅长重复性的工作。我们知道,人类进化的标志之一是直立行走,那人跟动物最大的区别是什么呢? 制造并使用生产工具,提高生产效率和生产力。所以我们思考如何让工程师发挥创造性,把精力聚焦到使用创造性解决疑难问题上,而繁琐的问题交给机器,因为它擅长执行又不知疲倦。
首先看一下目标的自动化流程是什么样子。整个环节除了最后的环节需要人去处理这些疑难问题,比如去定位问题根源,修改代码之外,希望其他环节尽可能做到自动化流程。
从 crash 提交开始,系统就能做自动分析,把问题聚类、分类、排名;之后自动创建 Bug 报告,提交到 Bug Tracker,使我们更方便的追踪 crash 的 bug 是什么时候出现的,它的状态是什么样子的,怎么在系统中流转。整个流程在相关系统上全部打通之后,信息就会流动起来。
接下来讲一下结果的分析过程。京东有一个 crash 展示的系统,可以很方便的查看 crash 排名、责任人、处理的状态、具体的信息,模块的情况等,然后根据这些信息进行详细分析。每一条 crash 的上报,系统都会自动分析,定位到这个问题是谁,然后分发给责任人。
对于比较明显的问题、或者问题能定位清楚的话,系统会直接就告诉我们这个问题是什么,代码的哪一行出错了,代码修改的提交时间和 commit ID 是什么,将问题交给相关开发人员。这种信息十分受开发者欢迎,他看一下那个邮件,就知道是大概是哪出问题了,然后解决问题,提交代码。对于一些比较简单的问题,系统甚至可以做到自动化修改和代码提交,不需要经过人工干预。
代码提交之后,系统会自动在 Bug Tracker 中修改 bug 的状态,然后把 crash 展示系统里边的状态也改掉。信息的打通,会自动触发 CI 系统编译打包发布到灰度系统,再之后升级推送到用户。
这个环节也是可以做到自动化的,包括打通 Hotfix。比如说有些用户经常出现 crash,在处理完之后就针对性的自动生成一个 Hotfix 包,定向灰度分发,然后目标用户升级之后就把这个问题解决掉了,而其他用户也不会受到影响。
这个目标流程看起来很美好,那怎么去实现它呢?
简单介绍一下整体系统架构设计, 设计时考虑的自动处理分析系统其实不止可以处理 crash,对于其他问题的思路是一样的,比如用户的意见反馈。中间的虚线部分是自动分析处理系统;而下面的编译系统、代码管理系统、Hotfix 系统、Bug 管理系统以及灰度系统都可以通过这个架构有机的结合在一起。
自动分析处理系统的总体架构
自动分析处理系统可以划分成四大部分。
上面提到的前端展示部分,可以展示用户感兴趣的个性化内容、分模块汇总的数据情况和分析报告等。
后台管理系统,用于实现各种自动化配置。比如代码里有很多的嵌套关系与各种模块,同时也支持插件化,总体大概有五六十个插件。它们之间的关系与配置,以及相关的维护人员,以及各大小版本和灰度版本,都需要主动去完善配置。
中间层主要负责数据同步。如 crash 信息收集系统的同步,包括原始数据的格式化,以及分析、汇总的邮件发送等。Crash 详情分析一般都会发给责任人,抄送相关,以便具体问题具体分析。Crash 分析也会有汇总邮件,发送给相关负责人,用以了解总体的情况。
其中最重要的部分是 crash 的分析处理的这个环节。对于安卓系统收集的报告来说代码都有混淆过;对于 iOS,包括安卓的 native code 话,线上 Crash 没有符号表,需要把混淆后的代码对应回去。我们希望把这份工作全交给机器来去做,比如解混淆、解析过滤,还原去定位到具体文件某一行,这次修改的提交的相关信息等。深度分析未来计划会继续深入做,比如代码库的分析,报告的生成等。
整个架构涉及的环节特别多,由于篇幅关系,分享实践的过程中一些关键点。关键问题如果搞定的话,其他的问题都对于各位资深工程师,甚至对于普通的工程师的话应该都都不会有太大的难度。这些难点首先需要你要敢想,然后真正的去分析去做,一切都是可以实现的。
首先看邮件中的崩溃分析结果。每当遇到了一个 crash,系统就会分析出一些信息,汇总到邮件发出来。
现在看的是一个崩溃分析的结果。由于交给机器来处理崩溃信息,可以通过聚类分析把客户端所有历史数据进行分析,去重等,比如我们可以知道这个 crash 之前出现过多少次。上面这个结果包含四个部分,崩溃类型、崩溃次数,相关代码的上下文以及提交版本 ID、修改日志和历史,这些信息附在邮件正文里发送给相关责任人。
这个是崩溃信息的预览。可以看到崩溃的异常类型、描述、版本号、build 号、代码的文件对应的行数和 tag 这都有,还有复原过的具体导致出错的崩溃堆栈,函数名及返回值和参数等。堆栈本来是被混淆过的,这里会通过机器复原回来,方便工程师分析查看。他如果觉得这信息啊不够,觉得机器分析的有问题,他可以通过原是崩溃信息链接去查看。
通过代码信息可以看出当时出问题的代码行的上下文共有二十行左右的代码,用颜色进行语法格式化,错误的代码会被高亮,左侧还附有行号。这种风格贴近于工程师会比较喜欢的代码样式,函数和变量什么都很清楚。 整个工具脚本主要是用 python 写的,代码高亮应用 pygments 实现,一般的工程师都可以做。即使有些关键点和难点,克服一下,就能解决。
还有一个比较重要的功能,就是要把 crash 跟 Bug Tracker 关联起来。不管是开发者还是 QA 或者是其他人,都可以方便的跟踪 crash 的状态。
Bug Tracker 使用的 Jira(下图左侧)。收到 crash 并完成分析之后,系统就会在 Jira 上面创建一条 bug,把它的责任人、所属模块和崩溃的堆栈以及其他相关的信息全都给贴到 bug 详情中,以供查看。责任人既会收到 crash 的通知邮件,也会收到 Jira 的通知邮件。他可以上 Jira 查看和处理 Bug 的状态。整个 crash 的处理和流转过程都是透明的,如果有工程师轻易的做不负责任的流转的话,QA 就会找他的麻烦,这样就没人会轻易的踢皮球了 。
右侧是展示系统,跟 Bug Tracker 是关联起来的。第一列是 crash 的占比,占比排名靠前的是我们最需要关注的, 比如占比前三名的问题 。右边是它的类型,还有它所所属的模块,责任人是谁以及一些基本信息。最重要的是这里跟 Jira 里边同步的状态,是已解决,还是新建还是说是流转的状态。这样汇总起来的信息就会使得 QA 很方便地查看和跟踪 。
之前讲了下自动化系统如何与 Bug Tracker 关联。大家肯定也很关注,京东是如何还原混淆过的代码,而且做到了多库的还原。这里面包含主工程,几十个的插件,还有一些 Jar 包、AAR 等。现在为了降低 Git 代码仓库的体积,我们还把几乎所有的二进制文件,包括 Jar 包和这个 AAR 都放到 Maven 上。不管是哪种方式,我们都需要把出错的代码还原到具体代码的哪个文件,并定位到 具体行和具体 commit ID。
为了实现这个多库还原,我们定义了一个配置表。其实别看刚才那么多的类型,其实主要的把它给分成两类即可。一类是主工程或者是已经混淆过了的,有 mapping 文件的;另一类是没有混淆过的。他们最大的区别就是有没有 mapping 文件。
红框里面,这个目录第一层是那个模块的名字,每个模块下面一级以版本号和 build 号为子目录,在这个目录里有一个 mapping 文件和 json 文件。红框内的定义就是混淆过的代码,有 mapping 文件,跟下面蓝色框的主要差别就是多了这个映射表。
接下来看一下如何通过映射表恢复混淆的代码。
映射表最关键的是要建立起来一个映射树,这个树是从无数的 mapping 文件生成出来的。这里可以大概分成三类。可以先说 com.jingdong.cc,它本来没有被混淆过,只在最后混淆了一下,只有一个映射关系,对应到 com.jingdong.Test3,在恢复出来的映射树中代表一个叶子节点。对于主工程内的也比较简单,com.jingdong.aa 对应 com.jingdong.test1。
最麻烦的是 com.jingdong.bb,在混淆中对应的是 com.jingdong.test2,在主工程内可能又被混淆。简化一点,你可以设定已经混淆过的代码不能再次被混淆。这样去建映射表的时候会有个顺序,要先去用主工程的映射,依赖别人的先去建,被依赖的放在后边,最后建出来的就是一个正确的树。
如果这个树又被混淆了一次就比较麻烦了,需要引入有多个树,因为 com.jingdong.test1 可能会影射成 com.jingdong.bb,然后又混淆一次,映射成一个 com.jingdong.dd 或者一个什么别的,然后你只能多次建树。
最终这个映射树建出来的话,多库代码还原就容易了。
还有一个比较大的问题,是这个代码追踪,同一个工程的多代码库,为什么会有多个仓库呢?
我不知道其他公司有没有遇到这个情况。因为客户端体量大,每个版本 commit 越来越多,仅日志信息就特别大,很快就会涨到超过 1G,甚至几个 G,Git 服务随之会越来越慢。为了提高访问速度,每当膨胀到一定量级的时候,就会拉出一个快照再建一个库,然后基于新库继续开发 。
这样虽然可以提升效率,但带来一个问题。同一个工程,但有很多历史仓库,出错的代码最后一次提交有可能是在 A 这个仓库里边,有可能在 B 仓库里面,有可能在历史的 Z 仓库里边。怎么把这个关系去找回来呢?在此之前, 让工程师手工去找是特别麻烦的。
另外还有拆分仓库的情况。一个仓库在发展的过程中,难免需要解耦分拆子库,这样也会面临有多仓库的问题。
更常见的问题是这个文件路径的变更。不管是类名还是 package 的修改,在重构的过程中都是难以避免的,怎么去找到它的历史。不是到这就断了,找不到责任人了,这些其实都是问题。
为了解决多代码库的追踪问题,也需要有一个配置的信息。大家可以看到,这里边主要分为两种,一种是嵌套的工程,一种是单仓单代码的仓库。单代码仓库的配置(红色框)比较简单,它有它这个 project、name,还有它是跟谁一块编译的,也就是嵌套工程标识,就可以了。
最重要的就是这个嵌套工程的历史,这里边会有不光有 project 和 name,还有它的 sources,甚至会有多个 sources。source 的 id 可以简单的理解为历史顺序,先在最新的 id:3 里找代码,如果没有继续找 id:2,还没有,继续 在 id:1 里找,这样逐级地往前找,直到找到。
解混淆后就有了复原后的堆栈 、package、类名和方法名以及行号。通过这些信息和 映射表就可以找到文件和出错行的上下文。用 git blame 可以找到这一行的最后修改人及 commit ID 等信息,同时也可以找到 diff 信息进行输出。但是由于有可能有文件更名的情况,还可能需要计算偏移量从而找到原始行号,否则会出现错位的清凉。另外针对代码库迁移的情况,也需要不停的切换代码库,直到找到最后一次修改的 commit 为止。追踪的流程如下图所示。
整个过程就描述了完整的代码复原和追踪过程,不管是多代码仓库, 还是仓库拆分、迁移或是文件移动, 都可以根据这一套方案进行原始修改信息的复原和追踪 。
上面就介绍完了整个 Crash 自动分析处理系统,目前系统运行效果非常好,尤其是在灰度阶段,近四成的 crash 可以较准确定位。未来发展的方向我们定位由自动化向智能化发展, 这也是人工智能发展的大势所趋。 未来 crash 等问题不仅能自动分析和处理,而且还能智能分析定位,给出解决方案,甚至直接给出修复 patch 包 。
简单举个例子,比如可以对 crash 进行深度的分析, 向工程师提供一些辅助建议,提高解决问题的效率。首先可以用代码、patch、还有崩溃信息等数据参数化,利用机器学习平台进行训练,建立代码相关度的模型。 之后利用建好的模型对 crash 进行辅助定位和分析,并在日常运行中继续学习和训练,提升准确度。
作者简介
王永杰,国防科大学士,北交大硕士,首届 GMTC 大会特邀嘉宾,曾担任盛大创新院高级研究员。2013 年加入京东,任平台产品研发部技术专家、架构委员会主任架构师、手机京东开放平台负责人,是最早一批投身 Android 研究和开发的工程师。目前重点负责手机京东开放平台的建设,将各环节进行工具化、自动化、系统化,提升研发效率和质量,降低业务开发门槛。