「桌面端」|Electron 崩溃自行归因分析

造轮子是一辈子的事

背景

在上文中 「桌面端」|Electron 崩溃自行上报实现 我们在之前建设了桌面端崩溃上报的能力,只是为了看一下线上的崩溃率,但对于后续如何归因问题、分析问题、解决问题,并没有在前期设计方案,导致崩溃虽然产生了,但很难去排查:

  1. 抓不到大头崩溃问题:没有问题归因,甚至没有 Dump 文件解析,导致最起码的问题原因也无法直观显示,需要团队成员自行使用符号表解析工具自己解析。
  2. 解决不了崩溃问题:崩溃问题排查都是极为耗时的,如果不能锁定大概模块范围,那也没办法进行人员排期进行工作分工。

从上得知,我们需要从零开始构建桌面端崩溃分析监控链路,目标上是能力对齐友盟等三方分析平台,提供基本的崩溃归因分析能力。

分析

在上文方案中,崩溃记录在 ELK 日志平台,崩溃信息上传在 OSS 文件存储。

那后续需要做:

  1. 读取 ELK 的崩溃记录。
  2. 从 OSS 把崩溃具体信息 Dump 文件获取下来。
  3. 解析 Dump 文件,生成可以阅读的文本格式。
  4. 聚合归因可观测,直观显示每个版本的不同崩溃的数量级,并持续监控。

选型

基于公司现有基建设施,做如下选择:

  1. 选择 Python 脚本做自动化流程。
  2. 选择 Teamcity 作为脚本触发平台。
  3. 选择公司自建的质量分析平台作为数据展示。

具体方案

读取崩溃记录

从 ELK 中获取崩溃记录,用的是一个比较取巧的办法,从网站 URL 中分析出,获取数据列表接口(接口略)。

通过去请求这个接口,传入合适的 params 和 header 即可获取到一段时间内的崩溃记录列表。

这里可以看到我有一个重试 10 次的注解,原因是这个接口不太稳定,不仅慢,而且经常字段上会会缺斤少两(一脸懵逼)。

下载崩溃信息

在 ELK 日志平台上存储的是 OSS 上的文件地址,具体的崩溃信息是存储在这个 Dump 文件中。

想要分析它,首先需要把它下载到本地。

这只需要提供一个批量下载的方法即可:

这里简单实现即可,毕竟脚本每隔一段时间只执行一次,不会有什么性能瓶颈。

分析 Dump 文件

解析方式

Dump 文件是一个二进制格式的文件,需要使用 breakpad 进行解析。

当然,解析成文本的文件只有简单的错误说明和内存地址,想看具体堆栈还需要符号表进行转换。

这里有两种方式:

一种直接使用 mimdump 库,从 (github.com/electron/el...)找到当前 Electron 发布的符号表地址:

当然是要用符合我们项目的 Electron 版本。

附代码示意:

第二种是直接使用封装好的第三方库(electron-minidump),它是基于 mimdump 的二次封装。

好处是它自身已经包括通用的符号表库引用:

问题是它使用的是最新的 Electron 版本进行符号表转换,但我们项目版本比较低,解析不出来,需要找到 electron-minidump 的安装地址,把我们项目版本的符号表放进去即可:

bash 复制代码
npm root -g  # 找到 npm - g 的安装目录

找到 electron-minidump 安装地址下的 /cache 目录:

从中找到 electron 的缓存 pdb 包:

把我们下载适应版本的放进去即可。

批量解析

同样,我们需要对多个 Dump 文件进行解析,所以需要构造一个批量方法:

把二进制 dump 文件批量转换成 txt 文本文件:

执行后,就可以看到具体的崩溃原因及崩溃堆栈了。

当然整个过程是耗时的,每个 dmp 文件的解析都大概需要 10+ 秒,不过对于崩溃归因这个场景来说也不影响,我们每天执行一次即可。

归因聚合

我们有了这些可分析的崩溃信息后,还需要做崩溃类型汇总,对崩溃问题进行聚合,把高优问题筛选出来优先处理。

与 App 有些不同的是,App 上不同的崩溃地址,可能也还是同一个问题,所以友盟上还有分析堆栈,模糊聚合的能力。

而桌面端除了内存溢出外(内存比较特别,崩溃的点只是最后一棵稻草,而不是发生崩溃的位置,所以没有什么意义,这个需要额外的建设内存监控机制)。

其他崩溃地址是清晰的,所以这阶段根据地址做聚合即可。

这一段附一下代码:

Python 复制代码
class Ascribe:

    @staticmethod
    def ascribe_to(list: list[ElkModel]):
        """
        归因分析
        """
        if list.__len__() == 0:
            return []

        folder_name = Define.analysis_dir
        sort_list = sorted(list, key=lambda x: x.create_time)

        data_list = []
        for model in sort_list:
            file_name = folder_name + "/" + os.path.basename(
                model.id) + ".dmp.txt"
            reason = Ascribe.analysis(file_name)
            data = Ascribe.format_data(
                model,
                reason['os'],
                reason['crash_reason'],
                reason['crash_keyword'],
                reason['content'],
            )
            data_list.append(data)
        result = Ascribe.statistics(data_list)
        return result, data_list

    @staticmethod
    def analysis(file_name: str) -> dict:
        """
        分析文件
        """
        content = ""
        with open(file_name, 'r') as f:
            content = f.read()
        # 根据正则表达式获取操作系统信息
        os_pattern = r"Operating system:\s*(.*?)\n"
        os_result = re.findall(os_pattern, content)
        os = "unknown"
        if os_result.__len__() > 0:
            os = os_result[0].strip()

        # 根据正则表达式提取崩溃原因信息
        crash_reason_pattern = r"Crash reason:\s*(.*?)\n"
        crash_reason_result = re.findall(crash_reason_pattern, content)
        crash_reason = "unknown"
        if crash_reason_result.__len__() > 0:
            crash_reason = crash_reason_result[0].strip()

        # 根据正则表达式提取崩溃地址信息
        crash_address_pattern = r"Crash address:\s*(.*?)\n"
        crash_address_result = re.findall(crash_address_pattern, content)
        crash_address = "unknown"
        if crash_address_result.__len__() > 0:
            crash_address = crash_address_result[0].strip()

        crash_keyword_pattern = r"\(crashed\)\n*(.*?)\n"
        crash_keyword_result = re.findall(crash_keyword_pattern, content)
        crash_keyword = "unknown"
        if crash_keyword_result.__len__() > 0:
            crash_keyword = crash_keyword_result[0].strip()

        return {
            "crash_reason": crash_reason,
            "crash_address": crash_address,
            "crash_keyword": crash_keyword,
            "content": content,
            "os": os,
        }

    def statistics(data_list: list) -> list:
        """
        统计
        """
        dict = {}
        for data in data_list:
            key = data["keyStack"]
            if data["exceptionName"] == "Out of Memory":
                key = "Out of Memory"
            item = dict.get(key, None)
            if item == None:
                dict[key] = {
                    "total": 1,
                    "devices": set([data["deviceId"]]),
                    "data": data,
                    "list": [data],
                }
            else:
                devices: set = item["devices"]
                devices.add(data["deviceId"])
                list: list = item["list"]
                list.append(data)
                dict[key] = {
                    "total": item["total"] + 1,
                    "devices": devices,
                    "data": data,
                    "list": list
                }
        result = []
        # 遍历每一个分组
        for _, value in dict.items():
            # 统计每个崩溃的数量及崩溃的设备数量
            crash_count = value["total"]
            device_ids: set = value["devices"]
            device_count = device_ids.__len__()
            last_crash_info = value["data"]
            last_crash_info['crashNum'] = crash_count
            last_crash_info['imeiCount'] = device_count
            for data in value["list"]:
                data["aggregateIssueKey"] = last_crash_info["issueKey"]
            result.append(last_crash_info)
        return result

这样我们就在脚本上构建了一个归因聚合能力,可以统计出每类崩溃的崩溃总数和崩溃设备数。

平台观测

这里采用质量管理平台(qa.gaoding.com/h5/qacenter...)来进行查看及任务汇总。

采集策略

由于 ELK 为了降本,只保留了 7 天数据,且质量管理平台存储数据的话也需要额外开发工作(当前 App 处理上,质量平台也只是调用友盟的接口)。

所以策略上:

  1. 脚本一次执行采集 7 天数据。
  2. 每半小时执行一次脚本。
  3. 质量管理平台只存储最新数据。

上报 API

  1. 清理数据
  2. post 统计数据。
  3. post 明细数据。

具体效果

归因查询

明细查询

监控预警

总结

崩溃归因分析到此就结束了,希望能给大家一些启发,在 Electron 这个丐版客户端上,造轮子造的更舒服一点 ...

但如何解决问题,也是很头疼的一件事 ...

后续,笔者还会写几篇关于 Electron 的文章,需要的同学可以关注下。


感谢阅读,如果对你有用请点个赞 ❤️

相关推荐
高山我梦口香糖11 小时前
[electron]预脚本不显示内联script
前端·javascript·electron
weixin_5051544613 小时前
数字孪生在建设智慧城市中可以起到哪些作用或帮助?
大数据·人工智能·智慧城市·数字孪生·数据可视化
捷码小编2 天前
数据可视化大屏案例落地实战指南:捷码平台7天交付方法论
低代码·数字孪生·数据可视化
捷码小编2 天前
如何选择专业数据可视化开发工具?为您拆解捷码全功能和落地指南!
低代码·数字孪生·数据可视化
卸任3 天前
Electron自制翻译工具:增加中英互译
前端·react.js·electron
十三画者3 天前
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
python·机器学习·数据挖掘·数据分析·r语言·数据可视化
程序员老刘4 天前
20%的选择决定80%的成败
flutter·架构·客户端
皓子4 天前
海狸IM桌面端:AI辅助开发的技术架构实践
前端·electron·ai编程
搏博4 天前
将图形可视化工具的 Python 脚本打包为 Windows 应用程序
开发语言·windows·python·matplotlib·数据可视化
@HNUSTer6 天前
Python数据可视化科技图表绘制系列教程(一)
python·数据可视化·科技论文·专业制图·科研图表