「桌面端」|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 的文章,需要的同学可以关注下。


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

相关推荐
techdashen18 小时前
拆开任意 Electron 应用:从 Windows 安装包到 Discord 的私有更新协议
javascript·windows·electron
三声三视20 小时前
Electron 窗口状态保存,我在鸿蒙 PC 上放弃了 electron-store
electron·arkts·鸿蒙
zyk_5202 天前
大屏数据可视化解决方案
数据可视化·大屏端
极光代码工作室2 天前
基于Spark的电商用户点击流分析系统
大数据·python·数据分析·spark·数据可视化
FIT2CLOUD飞致云2 天前
加强安全防护,图表与仪表板功能优化,DataEase开源BI工具v2.10.23 LTS版本发布
数据分析·开源·数据可视化·dataease·bi
sTone873753 天前
Electron 进程架构模型
前端·electron
哈撒Ki3 天前
快速入门 Electron
前端·面试·electron
ayqy贾杰3 天前
我同事,40了,他vibe coding了个App
前端·ios·客户端
AI科技星3 天前
维度原本——基于超复数谱系的全域维度统一理论
c语言·前端·javascript·网络·electron
SAP_奥维奥科技3 天前
从产品合规到体系出海:中国医疗器械企业经营底座重构白皮书
sap·数据可视化·复杂供应链管理·sap医疗器械·sap生命科学