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


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

相关推荐
2501_915373881 天前
Electron 从零开始:构建你的第一个桌面应用
前端·javascript·electron
2501_915373881 天前
Electron 架构详解:主进程与渲染进程的协作机制
electron
Acaibird.1 天前
腾讯元宝桌面客户端:基于Tauri的开源技术解析
ai·electron·tauri·跨平台开发
qq_589568102 天前
Electron学习+打包
前端·javascript·electron
XuX032 天前
MATLAB制作散点图:从基础到进阶的三种类型讲解
数学建模·matlab·数据可视化
2501_915373884 天前
Electron读取本地文件
前端·javascript·electron
半块橘子5 天前
Electron-vite中ELECTRON_RENDERER_URL环境变量如何被设置的
前端·javascript·electron
whistle哨子5 天前
electron-vite 应用打包自定义图标不显示问题
javascript·electron
朝阳395 天前
Electron Forge【实战】自定义菜单 -- 顶部菜单 vs 右键快捷菜单
前端·javascript·electron
前端没钱6 天前
在Electron中爬取CSDN首页的文章信息
前端·javascript·爬虫·electron