BGM!基于错误日志进行分析和告警!

4,009 阅读21分钟

前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。


第十四届|前端成长晋升专场,8-29 即将直播,9 位讲师(蚂蚁金服/税友等),点我上车👉 (报名地址):


正文如下

本文为第五届 - 前端监控体系建设专场讲师 - 能翔 的分享 - 简要讲稿版(完整版请看录播视频):

大家好,我是贝贝集团的周能翔,今天给大家带来分享:如何基于错误日志进行分析和告警。我目前在贝贝集团大前端架构,致力于通过技术产品提升开发人效以及对客户端底层架构进行维护。

总览

前面 Allan 已经给大家讲了我们怎样从零到一搭建错误监控平台,我这边会给大家说一下在已有的错误监控基础上,我们遇到的一些实际问题和针对这些问题做的优化。接下来我会从数据日志清洗,辅助分析能力,用户界面展示以及错误告警追踪四个方面讲一讲我们做的具体优化方案。

遇到的问题和演进

数据清洗

下面我们就来看下我们遇到的问题和解决方案。先来看我们在数据清洗阶段遇到的问题和解决方案。

首先我们就遇到了 iOS 错误日志可读性差的问题。这个就是我们获取到的 iOS 原始错误日志里的一段堆栈信息,它实际上是一个基地址加上一个地址偏移量,面对这样的错误堆栈开发去排查问题是很麻烦的,同时因为不同应用版本因为代码有变化都会有不同的偏移量,导致相同的错误在不同版本会有不同的偏移量,完全没有办法进行错误聚合。
所以我们会将每个 iOS 版本的符号表上传,通过符号化的任务,将上面的基地址和偏移量转化为具体的文件和方法以及行号,这样我们的开发就可以比较方便的定位到错误的原因了。同时这样也让不同版本之间相同的错误可以有一样的错误堆栈,为后续的错误聚合奠定了基础。

讲到错误聚合,下面我们再来看看我们在做错误聚合阶段遇到的各种坑,我们的错误聚合经历了从最初的对全内容进行散列聚合到后续进行一些通用性优化来提升聚合度,再到针对一些特别问题进行一些针对性的优化。

这里我们以一条安卓的错误日志为例,这就是我们实际上报的一条安卓错误日志,包含 3 个部分:错误名称,错误描述,错误堆栈。错误名是获取了捕获的错误的类名,错误描述是错误的具体描述,一般包含了本次错误发生的原因,以及一些相关的特性信息,最后是错误堆栈,这块也是我们排查问题的关键。

最基础的聚合方式就是对上报的全部内容进行一个散列,求个 MD5 值,然后把所有散列值相同的错误聚合成同一类错误,简单粗暴。这样做的问题就是,我们发现这样的聚合度很低,很多明明是同一种错误都被聚合成了不同的错误。

所以我们针对这种情况进行了一些优化,这是一个同类错误的另一条日志,我们发现错误描述这部分内容是不同的。这是一条索引超出范围错误,他的具体错误描述里面包含了发生错误时的非法索引的值,还包含了发生错误的一些视图以及页面信息,这些特性内容可能对我们单次分析错误是有用的,但对于错误聚合来说他们是我们不需要关注的信息。我们发现他们相同的部分是错误名称以及错误堆栈,于是我们可以进行一下聚合优化,将错误名称和错误堆栈加起来进行散列,获取 MD5 值,用这个值来进行聚合就可以有效的减少错误描述信息里面特性信息不同导致的聚合失败。

然后我们继续看这条错误日志,现在我已经把错误描述去掉了,现在只留下错误名称和错误堆栈,是不是就可以完美的聚合了呢?很遗憾,并不是。我们抓取到了另一条错误日志,我们人工判断这就是同一个错误,但是错误堆栈却不一样了!原因是客户端的代码最终是运行在不同的系统上的,我的 APP 可能运行在安卓 5,安卓 6,安卓 7 上等等,而不同的系统版本之间,系统类的代码是不一样的,我的错误方法调用链路还是一样的链路,但这些系统类的代码行号却发生了改变。这就导致了我明明是同一个错误,在不同的系统版本上确实不一样的错误堆栈。处理方法就是我们可以把所有系统类的堆栈的行号去除,通过正则表达式找到系统类的行号的部分,去除。现在我们将错误名和去除了系统行号的堆栈加起来做一个散列,以这个 MD5 值进行聚合就可以有效减少不同系统版本导致的聚合失败问题。

那是不是去除了系统行号之后不同系统的错误日志就都可以完全聚合了呢?其实也并不是,来看这个例子。有时候不同系统版本之间有些方法的调用堆栈不止是行号是有不同,甚至调用的方法链路都有差别,这个时候就不能很好的聚合了。针对这种情况我们发现如果错误堆栈中有我们的业务代码,那业务调用链路是相同的,我们可以针对性的将错误堆栈中包含业务代码堆栈的日志通过错误名加上业务代码堆栈来计算散列,这样就可以进一步降低这种情况下不同系统版本带来的聚合影响。当然这样做会一定程度上牺牲一些聚合精准度,有低概率将非同类错误聚合到一起。可以通过带上业务代码最相邻的一条系统代码堆栈来进行进一步的优化。同时我们发现在实际使用的过程中,这类情况很少发生,偶尔有发生其实最终对我们发现和解决问题的影响也远远小于同类错误不能聚合的影响。我们的最终目的毕竟是更好的发现和解决线上问题,所以大可放心使用这类相对激进的聚合算法。

在错误日志中还会有一些比如安卓的内存溢出类型的错误。这类错误发送的时候很可能错误堆栈的方法并没有什么实际问题,只是因为别的地方存在内存泄露等问题导致了在执行到这行代码的时候内存溢出了,如果要是拿它的错误堆栈进行聚合,那 10 个 OOM 错误给你聚合出 10 种不同的错误都是有可能的。这显然不是我们想要的结果,也不利于后续的排查和分析。所以我们会把所有内存溢出的错误根据错误名称全部聚合在一起,通过统计错误日志的特性指标,比如大量发生在哪个页面,大量发生在哪些机型,哪些系统版本来进行后续的排查和优化。不能拘泥于我们前面定义的聚合算法,拿没有标识度的堆栈来聚合。

数据持久化

我们会在错误大量发生时进行削峰处理。避免我们的监控系统像之前用的 Sentry 一样,每次都在错误大量爆发的时候挂掉,在最需要它的时候掉链子。

我们的削峰机制是数据处理的轮询任务每次会从 ES 中拉取最新的 10000 条错误日志,同时聚合过后,同一类型的错误只取 200 条写入具体的事件数据库,其他的我们会增加该类型错误的发生数量。

这样做的原因是我们最关心的永远是现在最新发生的错误,不能因为前面有大量还没处理完的错误就按时间顺序一点一点处理存量数据,优先拉取处理最新发生的错误。然后我们认为 200 条具体的错误事件已经足够我们进行相关的错误分析了,所以当错误大量发生时,一次我们只录入 200 条具体错误事件,既能缓解db的压力,避免大量数据读写,也能让数据处理任务更快完成,尽快触发告警通知到相关负责人。

辅助错误分析

针对前端,我们记录了前端用户在页面上的发的 Ajax 请求,点击事件,以及控制台错误日志,当发生错误的时候,我们会把这些行为日志和错误日志关联起来,让开发在排查前端错误的时候可以通过这些用户行为日志来更好的分析问题发生的原因。

而这类需求在分析客户端问题时其实更为重要。我们发现在分析客户端问题时,仅靠错误日志上报的信息,很多时候是不够的,因为缺少了用户浏览路径,操作行为等信息,而客户端很多错误是在特定触发条件下产生的,仅有错误堆栈很难复现问题,这就导致了开发排查问题的体验很差,要么需要去猜测错误是怎么发生的,要么需要自己从海量的行为日志中去查找用户行为日志,费时费力。

于是我们给客户端建立了一套日志链路,从客户端一次冷启动开始,我们会用 uuid 生成一个链路 id,后续的所有行为日志,网络日志和崩溃日志都会带上这个链路 id,我们会记录一些关键节点,页面调整,网络环境变化,错误的网络请求,一些用户操作行为等。这样我们可以通过链路 id 直接关联到我们的相关行为日志,方便后续的问题排查。举个例子,我们的客户端发生过一次 weex 错误,错误内容只有 weex 的 JS 内容不合法,无法渲染展示,我们完全不知道这个错误是怎样发生的,也无法复现。随后通过排查具体的行为日志,我们发现发生这个 weex 错误之前,用户都进入过 H5 页面,我们又拉取了发生这个错误的所有用户的日志链路,确定了发生错误前,用户都进入了 H5 页面,并且这些页面还都是 Http 协议的,同时我们还拉取到了一个广告请求,于是我们确认了发生这个错误是因为发生了网络劫持,加载了广告 JS,其中一个跳转链接正好被我们的 webview 认为是我们的 weex 页面并进行了跳转,而广告页面内容不能被我们的 weex 页面识别所以发生了这样的错误。我们当时仅通过 bugly 上的信息是没有办法复现和解决这种问题的,所以日志行为链路还是很好的帮助我们进行错误分析和解决的。

数据可视化


错误列表 可检索 可排序 错误信息
错误详情 错误堆栈 用户行为 特征信息 可检索
趋势 错误趋势 事件趋势

错误监控平台可视化的需求主要是这三块:对于错误列表我们希望能支持多条件的检索,可以按各种维度排序,透露出关键的错误信息;对于错误详情我们希望可以方便的查看错误堆栈,能够关联到用户行为,确认错误发生的特征信息,并对聚合后的错误也提供一定的检索能力;然后我们希望对于全部错误和聚合后的错误都提供趋势图表,方便我们确认是否有错误波动发生。

错误列表页 错误趋势 检索区域 错误列表
错误详情页 检索区域 事件趋势 事件信息 特征信息 事件列表

于是我们把这些功能划归到两个页面,错误列表页承载整体的错误趋势,比较全面的检索能力以及错误列表。错误详情页承载相对简单的检索能力,事件趋势,具体事件信息,错误的特征信息以及聚合错误下的具体事件列表

这就是我们的天网页面,我们在页面左侧放了一整列的检索区域提供比较全面的检索能力;我们把错误趋势图表放在显眼的位置方便开发迅速定位一段时间的错误趋势;剩下页面最大的部分,我们留给了错误列表,我们在表头提供了排序能力,包括按错误数,错误用户数,时间排序,同时对于 24 小时内的错误还提供了按选定时间范围的错误数排序,这样可以避免一些长期累积错误数较多的错误霸占前排导致最近发生的错误无法透出的问题;最后是列表,列表中基本能看到错误原因,还有错误出现的 url。错误的总数量,影响到的用户数量。

这是从列表页点击进入的详情页。页面大致是三栏布局的结构,从左往右供五个区域,分别是:检索区域,这里的检索主要是提供时间版本等简单的检索条件;事件列表,聚合后错误的每一个具体错误事件;事件趋势图表,展示这类错误的发生趋势;最中间的大片区域留给了错误详情,包含开发最关心的堆栈详情、用户行为、事件处理的时间线;最后是错误详情外的特征信息,用于帮助开发者获得错误特征,方便排查。

错误监控平台的目的是及时发现和解决错误,我们希望错误处理链路不能停留在发现问题上,而是能串联后续的问题处理流程。我们通过告警或是开发主动查看我们的监控平台发现错误,在平台指派跟进人之后,会在我们的需求管理平台上创建一个需求给跟进人,跟进人完成错误分析和处理后通过持续发布系统发布代码上线后,新的代码又被我们的错误监控平台持续监控,这样就形成了一个完整的错误闭环,持续监控集团的线上代码质量。

监控告警

这里是一个比较基础的告警机制,监控平台接收到错误后,判断如果错误连续上报超过一定时间,且期间每分钟错误
数量超过一个阈值,我们就触发普通告警,触发普通告警后,如何没有被标记已处理,且期间每分钟错误数量超过一个更高的阈值,我们会触发告警升级。普通告警相对触发阈值较低,相对容易触发,同时我们普通告警的方式也较为保守,仅使用邮件通知,主要触达对此高度关注的开发,同时邮件也便于用作留档进行后续排查。而告警升级后,我们就会通过钉钉,短信,电话等越来越激进的方式尝试通知相关责任人,确保问题能被及时响应解决。

前面说的很简单,数据清洗后,近 1 分钟的错误超过阈值就告警,没超过阈值就忽略。但大家有没有想过近 1 分钟的阈值是什么?是按服务器时间取近 1 分钟上报的错误,还是按客户端时间取近 1 分钟发生的错误呢?如果是按照近1分钟上报的错误,我们发现端侧的错误是累积上报的,可能因为之前网络环境不好客户端累积了 3 天的错误一次性上报,导致近 1 分钟错误量激增触发告警,但实际上当前并没有错误发生,上报的错误都是之前发生的。如果是按照近 1 分钟发生的错误,我们又会发现端侧的错误上报存在延迟,近 1 分钟的错误可能会过几分钟才成功上报,如果我们只取近 1 分钟内发生的错误,那数量会比实际错误少很多,很难触发告警阈值,而如果为此降低告警阈值又会导致告警准确度下降。

怎样解决这样的问题呢?我们先看按上报时间的方案,我们可以先确定一个告警有效时间,例如一个小时,表示在 1 个小时内发生的错误都是我们告警需要关心的。在每次轮询拉取的错误中,我们对于实际发生时间在一个小时之前的日志,只清洗入库,但不将它计算为近 1 分钟上报的有效错误。我们只判断近1分钟上报的错误中有效错误数量是否超过阈值,这样就可以减少因累计上报导致的很久之前我们并不那么关心的错误引发告警的情况。

再来看按照发生时间的方案,我们同样先确定一个告警有效时间,还是举例一个小时。每次轮询任务拉取的错误我们全部按照发生时间清洗入库,然后我们检查过去 1 个小时是否存在某 1 分钟内的错误数量超过了阈值,而不是仅仅看近 1 分钟内的告警是否触发阈值,如果存在超过阈值的情况我们就进行告警。

下面来看具体的例子:先看按照上报时间来计算的方案,这是我们拉取到的近1分钟的错误,总共 5 个错误,都是 10 秒前上报的,但有些错误是两三天前发生的,我们把这些实际发生时间在一个小时之前的错误,清洗入库,但不计算为近1分钟的有效错误,而剩下两个错误分别是 5 分钟前和 20 秒前发生的,都在一个小时的有效告警时间范围内,我们就会记录近 1 分钟发生的有效错误是 2 次。

再来看按照发生时间的例子:我们分别在 9 点整、20 分、40 分、10 点整、10 点 20 时接收到了 1 条实际发生时间为 8 点 59 的错误日志。如果按照以前的逻辑只有 9 点整的时候,那条 8 点 59 的日志才是 1 分钟内发生的,而到了 9 点 20,我们只会计算 9 点 19 分到 9 点 20 分发生的错误日志,这条 8 点 59 的日志就会被忽略。按照新的逻辑,我们在 9 点整、20 分、40 分时收到的 8 点 59 的日志都属于1个小时之内的日志,我们会在收到时给 8 点 59 这个时间点的日志数量 +1,到 9 点 40 时,我们会统计到 8 点 59 分发生了 3 次错误。等到 10 点时,再收到 8 点 59 的错误,因为发生时间已经在一个小时之前,我们就不再关心这个时间点的错误数量了,也不会继续累加触发告警。

除了日志时间处理上的问题,我们还发现有些清洗任务相对较慢,比如 iOS 符号化,这就导致了每分钟能统计到的错误数量受限于任务处理速度。假设我们 1 分钟可以处理 20 条日志的符号化,而告警阈值设置的是 1 分钟 30 条,那无论这一分钟发生了多少错误,都会因为清洗任务较慢导致只能统计到 20 条,永远没法触发告警。针对这样的问题,我们可以在轮询任务的时候同步读取原始错误数量,根据错误数量直接进行内部告警,通知到相关责任人,再通过异步任务进行日志数据清洗,补全清洗后的错误信息。这样就不会因为耗时错误阻塞了告警,等开发去查看时,我们也会及时将已经处理完的日志详情同步到数据库进行展示。

成果展示

我们的天网监控平台已经完成了多端的支持,同时支持 Web 的 PC 端,H5,Node,小程序,安卓,iOS,Weex。接入线上工程 80+,截止目前累计收集错误日志超 2 亿条,极大的支持了集团线上工程的质量监控。

总结

最后我们再来总结一下前面讲的内容。我们优化聚合算法,实现更精准的聚合,同时也不局限于理论意义上的准确,已更好发现和解决问题为目标。在发生大量错误时,通过削峰处理来保持监控平台的稳定,同时也保留了最重要的信息方便开发进行排查和解决问题。根据开发排查问题的需要来设计可视化,方便开发更快捷的定位和解决问题。打通内部系统形成完整的监控闭环,不让发现的错误停留在监控平台无人问津。最后我们通过优化告警来兼顾错误告警的时效性和准确性,更及时的让相关责任人了解到发生的问题。

谢谢大家参与本次的分享。

本文使用 mdnice 排版