来,跟我一起 ,自研多端错误监控平台

15,412 阅读22分钟

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


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


正文如下

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

前言

视频前的开发者们,大家下午好!

我今天分享的主题是“如何实现一套多端错误监控平台”。先来做一个简单的自我介绍,我是来自贝贝-大前端架构组的 Allan ,目前致利于集团错误监控系统维护以及工程标准化等基建工作。同时,我也是《React+Redux前端开发实战》的作者。

废话不多说,今天我将与大家分享一下贝贝的是如何自研实现这套错误监控平台的。希望本次分享能让大家“听得懂、用得到、带的走"。我今天的分享将会持续1个小时左右,希望大家不赶时间,做好心理准备。

目录

技术的价值,在于解决业务问题。所以我将通过前面两个章节,来结合贝贝这边的实际情况去讲解我们为什么去做这件事情,以及做这套错误监控平台所带来的价值。第三个章节再来讲解技术实现,最后一个章节总结。

第一章:集团现状

接下来,我们正式开始。首先,我先来简单介绍一下贝贝集团的业务现状。贝贝于 14 年成立贝贝网,这是一个母婴购物平台;17 年成立贝店 —— 会员制折扣商城;18 年成立贝贷 —— 金融科技平台;去年上半年成立贝仓,去年下半年成立贝省。同时贝贝还拥有多个创业业务:红人直播、贝仓新零售等。

从时间线上面我们可以看到贝贝集团从一开始的 3 年一个项目,后来一年一个项目,再到后来一年多个项目,贝贝的业务呈现多元化快速发展。好,这就引申出了集团的第一个现状:业务多!

讲完了集团内部的业务情况,我们再来看看整个行业的现状。我本人是从 14 年接触前端开发,15 年从事前端开发的。而移动互联网的全面发展刚好是 2014 年,那么这7年左右的时间内我们前端发生了哪些变化呢?

  • 移动互联网全面发展之前,应用大多停留在 PC 端;
  • 当移动互联网起来后,应用开始从 PC 向移动端转移,也就是我们的 iOS 平台和 Android 平台;
  • 然后企业为了不用一个项目两边开发,H5 出现在了 webview 上;
  • 但 H5 的性能和体验始终没有原生的 App 好,于是出现了 Hybrid、Weex、RN 以及近几年出现的 Flutter;
  • 并且伴随着各类小程序的出现(微信小程序、支付宝小程序、钉钉小程序、抖音小程序);
  • 同时前端的技术栈也从一开始的 jQuery 到后来的 Angular、Vue、React 等等。

所以作为前端开发,我们得要每年都要学习新的技术。这就是我们整个行业的发展,他让我们的端/平台/技术栈不断地在分裂,变得越来越混乱。相信这点大家都感同身受吧!那么这就引申出了我们集团的第二个现状:端多!

集团业务的多元发展导致现状一:业务多;行业端的分裂导致现状二:端多。而业务多和端数量的多最终导致我们集团的工程数量越来越多!据我最近统计,贝贝目前拥有线上业务超过 80 个!好,那这 80 多个业务如果采用第三方解决方案,从企业的角度来看我们需要付出多少成本呢?

我们先来看几款常见的第三方产品:

  • 先来看 Fundebug:付费版 159 每个月,这个版本数据不在我们这里,所以从数据安全来讲我们肯定不会采用,而数据可以存放自己服务器的本地版本需要 30 万起。如果每年去支付这样一笔费用,开销还是挺大的
  • 再来看 FrontJS,FrontJS 高级版 899 每个月,专业版是 2999 每个月
  • 最后看 Sentry,Sentry 是 80 美金每个月。那么我们以 FrontJS 为计费参照,对这 80 个项目算一笔简单的账。80 个工程,12 个月,299 每个月每个工程,一年下来需要的费用是 28 万 7。而我们算过做这样一套系统需要 180 人日,也就是 15万人民币。并且一旦开发完,不需要每年去支付这样一笔费用。

第二章:为什么选择自己研发

当然,我们不能仅仅从价格这个角度,来作为我们要去自己研发这套错误监控平台的衡量标准。毕竟贝贝去年也是有融到 8.6 个亿的融资是吧😁。

所以接下来我们先来简单分析一下竞品,首先是 Sentry。Sentry 不支持 Weex、小程序。而且稳定性不是非常好,当错误大量爆发的时候 100% 挂。

  • 【小故事一】贝贝在使用自己研发的这套错误监控平台之前其实有使用过 Sentry,由于 Sentry 在处理大量的错误,会导致 Sentry 网页打不开,几乎到达某一个错误量时 100% 出现,出现 504 。需要等 Sentry 处理完错误才会恢复正常,延长了错误的响应时间。再来看Fundebug,Fundebug不支持 Weex,统计信息对我们来说不够详细。再来看 Bugly,Bugly 可能客户端同学知道的比较多,前端同学可能不太清楚。Bugly 仅仅支持 Android 平台和 iOS 平台,以及一些游戏场景(Cocos2d、Unity3D),并且不支持拓展。
  • 【小故事二】但通过 bugly 我们可以知道错误数量,错误机型等基本信息,但错误机型/版本/系统版本都很分散,错误信息只有:创建 weex 实例错误、Weex 文件内容格式错误。但仅有的信息是无法定位和解决问题的!最后看一下 FrontJS,FrontJS 仅支持 Web 端和小程序,每分钟监控事件数量受限(高级版),告警通知受限,不支持拓展,筛选问题的历史事件选择也受限。

所以,我们将他们与自己的错误监控平台来做一个对比(业界方案数据统计可能有误,仅供参考)的话。可以发现,业界方案各有各的好,但不能满足贝贝这边的场景。我们需要:历史可回溯时间更久、需要更稳定、需要各端(前端、客户端、Node 端)等等都适用的产品。

所以综合业务解决方案后,最终考虑到稳定性、一致性、可拓展性、安全性和成本这些维度,我们决定自己研发。

第三章:天网技术解析

为了让大家理解我接下来要讲的内容,我们先来看一下天网的整体架构图:

从应用接入层通过 SDK 去捕获错误并将错误上报。上报的数据流经 kafka 到 ES 暂存。再到调度中心执行清洗脚本,数据清洗完后再持久化存储到 MySQL,最后 Node.js 为可视化提供接口,最后错误数据展示。这一整个流程看起来有很多陌生词语,不用害怕不用慌,我们将这张图来抽象一下。

前面的整体架构图也就是由这六个部分组成:从错误收集到错误上报到数据清洗到数据持久化,最后到数据可视化和监控。

看起来还是有点难,对不对?没关系,我们先从实现一个最小的闭环讲起。由于大家都是前端开发,所以我们先实现一个前端的错误监控讲起!

我们先来模拟一个线上的报错:

如上面截图所示,这是一个 vue 的项目,我们在 created 中执行一个没有被声明的方法 this.foo()。线上肯定会报错对不对?好,过了一两分钟,我们来到可视化平台,可以看到这条错误出现在了错误监控平台的首页列表中,上面是错误趋势,横轴是错误时间,纵轴是错误数量,下面红框内是错误列表。点击列表后,我们进入错误详情页,从中间截图我们可以看到错误堆栈信息,这里其实我们一眼可以定位到错误位置。但为了更加方便开发者定位问题,我们还在右侧展示了当前错误发生时所在的环境信息。上面有设备信息和环境信息!

好,那么从线上错误的发生,到最终可视化的展示,中间发生了什么?

带着这个问题,我们从之前我们抽象出来的架构流程图进行详细讲解。我们先从我们前端开发同学最熟悉的 Web 端开始讲起!

一、SDK —— 错误收集/上报

1、SDK 如何设计?

毫无疑问,可视化数据的源头是在错误源头使用 SDK 收集的,那么我们先从 SDK 讲起。


我们将 SDK 分为自动和手动两种。手动一般用于业务的 try/catch 中,我们在手动错误上报中分为 error、warn、info 这三个 level 。在手动上报没有命中的情况下,我们使用自动上报兜底!

2、错误捕获机制

接下来我们来举例分析几个常用的错误捕获机制(下图高亮部分)。

【1】监听 window.onerror

当发生 JavaScript 运行时错误(包括处理程序中引发的语法错误和异常)时,使用接口ErrorEvent 的 error 事件将在 window 被触发,并被 window.onerror() 调用。

【2】监听 unhandledrejection 事件

Promise 被 reject 并且没有得到处理的时候,会触发 unhandledrejection 事件。所以可以对此事件进行监听,将错误信息捕获上报。

【3】跨域脚本错误:Script error.

由于我们一般会将静态资源存放在 cdn 等第三方域名上,所以当前业务域名中的 window.onerror 会将这类错误统一展示为 Script error 。所以为了解决这个问题一般有两种解决方案:

  • 后端配置 Access-Control-Allow-origin、前端在 script 标签配置 crossorigin
  • 劫持原生方法,使用 try/catch 绕过,将错误抛出

这里我们来讲讲第二种,由于浏览器不会对 _try-catch _起来的异常进行跨域拦截,所以我们采用劫持原生方法,将原生方法用 try/catch 的函数包裹来处理。示例代码如下:



这是 AOP(面向切面编程)设计模式,当错误发生的时候,我们会在 catch 中重新 throw 一个错误出来,最后在将原生 addEventListener 抛出执行。为了方便理解,我们将这种解决方案流程展示一下:


如上图所示,假如业务在 a.com 上,静态资源在 b.com 上,此时不做任何处理的时候 b 域名上的 js 报错会统一展示为:Script error.。但我们希望能获得完整的错误堆栈,所以我们采用劫持原生事件,将其进行 try/catch ,然后再抛出异常 throw error,重新抛出异常的时候执行的是同域代码,所以能拿到完整的堆栈错误信息。从而实现错误捕获上报。

【4】其它技术栈——Vue.js

Vue 项目中有自带的错误捕获机制,Vue.config.errorHandler(errorCaptured),这里我们通过劫持Vue.config.errorHandler,当 Vue 的项目中发生错误时,将错误捕获上报。示例代码如下:

这里还是采用了 AOP 模式(不清楚的同学可以去搜索了解下)。先将原生的 Vue.config.errorHandler 方法赋给一个临时变量上,然后在错误发生的时候在里面进行上报,最后再将原有的 errorHandler 继续执行。

【5】其它技术栈 —— React.js

React.js 也有自己的一套错误捕获的机制,我们在 SDK 中先声明一个错误边界的组件,然后在业务中去引用 SDK 中的这个组件,让他去包裹你的 react 标签。当子组件内发生错的时候,错误就会走到 SDK 的这个错误边界的组件中,在里面的 componentDidCatch 声明周期中可以捕获错误直接上报。这个错误边界组件其实就是一个高阶组件(什么是高阶组件?通俗理解:一个包装了另外一个 React 组件的 React 组件)。

3、环境收集

上面 5 张截图来自可视化错误的详情页,我们针对一个错误将它的概要信息、占比信息、特征信息、位置信息和  SDK 版本信息进行展示。这有助于我们更好的知道错误的发生环境。我们将这些错误分类为:业务信息、设备信息、网络信息和 SDK 信息。

3.1 环境信息收集原理

想要获得环境信息,我们先要看他是否命中主动上报,如果有,那就采用就主动上报的环境信息;如果没有,那就看是否命中 hybrid 接口(运用客户端能力捕获环境信息)的上报的环境信息,如果命中,那就采用客户端采集的环境信息;如果没有我们再去看是否命中 UA(UserAgent)收集的环境信息。这就是我们环境信息收集的 3 种方式。

再来看之前对环境的分类,业务信息:通过主动上报、客户端能力上报和UA上报;设备信息:通过客户端能力上报和 UA 上报;网络信息:采用客户端能力上报和 UA 上报;最后 SDK 的版本信息我们直接在 SDK 中去 require('./package.json').version 这种方式获取。

4、为什么行为收集?

其实通过前面的错误信息和环境信息我们已经能定位到错误了,那么为什么我们还要进行行为收集呢?来看下面截图(来自我们的错误监控平台):



通过上面截图,我们可以清晰的认识到 ‘nick’ of undefined 这个错误发生的链路。先从浏览器发送请求行为,再到用户点击行为,再到控制台打印行为,最后到这个错误的展示。我们可以完整复现一个错误的由来。这就是我们为什么要进行!

4.1 行为收集分类

我们简单将行为分类为:用户行为、浏览器行为、控制台打印行为。

其中,用户行为中包含了我们常见的点击、滚动、聚焦/失焦、长按等;浏览器行为包含了发起请求、跳转、前进/后退、关闭、新开窗口等;控制台行为包括了 console.log/error/warn 等。

4.2、行为收集机制讲解

接下来,我们分别对这几种行为的 case 进行讲解。

Case1、点击行为(用户行为)

使用 addEventListener 全局监听点击事件,将用户行为(click、input)和 dom 元素名字收集。
当错误发生将错误行为一起上报。

Case2:发送请求行为(浏览器行为)

监听 XMLHttpRequest 对象的 onreadystatechange 回调函数,在回调函数执行时收集数据。

不管使用的 axios 还是 fetch,底层其实都是走得 XMLHttpRequest,所以不同担心你使用的请求行为捕捉不到!

Case3:页面跳转(浏览器行为)

监听 window.onpopstate,页面跳转的时会触发此方法,将信息收集。

这里用的还是 AOP 模式,大家会发现,我们在日常业务中不太用到的 AOP 在 SDK 中多次被使用!感兴趣的赶紧去了解下吧~🙂

Case4:控制台打印(控制台行为)

这里我们通过改写 console 对象的 **info、warn、error **方法,在 console 执行时将信息收集。

5、数据上报

此时,我们已经得到了错误、环境、行为信息,接下来需要将它们上报,这里我们使用 POST 一个 gif 对其进行上报。

如上面截图所示,我们通过请求一个名为 n4.gif 的图片将错误信息进行上报。

5.1、为什么使用 1 x 1 的 gif ?

原因是:
1、没有跨域问题
2、发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
3、不会携带当前域名 cookie!
4、不会阻塞页面加载,影响用户的体验,只需 new Image 对象
5、相比于 BMP/PNG 体积最小,可以节约 41% / 35% 的网络资源小

6、SDK 小结

我们用一句话对 SDK 进行小结:

监听 / 劫持 原始方法,获取需要上报的数据,在错误发生时 **触发 **函数使用 gif 上报。

为了方便记忆,提炼 3 个关键词:劫持、原始方法、gif!(如果你还记不住,那也别打我)

二、数据清洗

我们先来思考我们为什么不能直接使用 SDK 上报的数据,而需要对数据进行清洗呢?

SDK 上报的原始数据有这么几个特征:

  • 数据量大、体积大:动辄几兆、十几兆、还碰到过几十兆的
  • 没有分类、聚合:同一类型的错误只是时间维度不同,没必要没条都去存储
  • 没有对非法数据进行过滤:无用信息太多,不利于聚合啊,也加重了服务器负担

1、存储介质对比

那么我们是如何对 SDK 上报的这些数据进行清洗的呢?首先我们得找个地方将这些数据进行存储,那么我们采用什么方式进行存储呢?接下来我们先来看市场上几种常见的数据存储方案:
第一个是 MySQL,MySQL 大家应该比较熟悉,日常业务中都有用到。MySQL 支持二级索引、支持事务,虽然对于全文搜索不是很好,但他的使用场景其实也不太用得到全文搜索,所以他比较适合线上核心业务。
第二个是 HBase ,诞生到现在已有 10 年了,这也是一个比较成熟的项目。Hbase 拥有天然分布式,虽然不支持全文搜索、二级索引和事务,但他支持在线扩展,非常适合增长量无法预估、写入量巨大的应用。
第三个是 ES,ES 是近年来很火的开源分布式搜索分析引擎,通过简单部署,就可以对日志进行分析、全文检索、结构化数据分析等。ES 还有统计功能,还支持二级索引,同时 ES 也是天然分布式。相较于其他高大上的数据库产品,ES 的出身比较屌丝。 ES 的创建者以前失业的屌丝程序员,在无事可干的时候为了方便老婆搜索食谱而创建了ES。 Elastic 公司已经获得数亿美金融资,当年的屌丝程序员也早已逆袭成为 CEO 并走上人生巅峰。

好了,那么这几种存储方案对比下来,我们最终可以将 Mysql 将作为数据持久化存储的方案,为可视化提供数据;而 ES 作为数据的临时存储方案,在这个环节,我们可以对这部分数据进行清洗。

2、清洗流程

2.1 清洗流程

我们将清洗流程分为下面 3 步:获取数据、数据预处理、数据聚合。


2.2 获取数据

从 ES 中获取数据非常简单,ES 底层是基于 Lucene 的搜索服务器的,它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。所以我们前端开发只需要想平时开发业务调用接口一样去调用就可以了。

  1. 通过 GET 请求从 ES 获取近一分钟的错误信息(下图)
  1. 设置阈值(削峰机制),由于错误大量爆发的时候为了“不让服务器承受他不该承受的压力”,我们使用了下面两种方法做削峰机制:
  • 每分钟数据获取上限 10000 条,超过就采样入库
  • 同类型错误数量大于 200 条,只统计数量

2.3 数据预处理

如上面ppt截图中的右图所示,这是一个从 ES 中截取的某个错误的 content 字段,由于 ES 中的 data 是 string 格式,并且代码有被转译,所以我们需要将其 JSON.parse()。并且有时候会出现里面还不完全是个字符串包裹的对象,所以我们需要将里面我们需要的字段提取出来。并且,我们还要取出原始数据中的无用信息,减小存储体积。

上面截图就是我们最终存储到 mysql 中的数据。可以看到这里的数据就比较的清晰,没有多余的看不懂的内容。

2.4 数据聚合


先来思考我们为什么需要做数据聚合这件事,目的有两个:1、存储性能:存储小;2、查询性能:查询快。

那么我们要如何对某一类的错误进行聚合呢?

我们从三个维度来做:1、业务名;2、错误类型;3、错误信息

我们以贝贝业务线中的错误举例,beibei 是业务名,SyntaxError 是语法错误,SyntaxError: The string did not match the expected pattern… 是错误信息。我们将他们拼到一起,然后用 md5 得到这么一串东西:ecf9f6d430bea229473782dc63407673。

好,接下来上报的错误都会同样采用这种方式去聚合,看他们的这一串东西是否一样,如果一样,那我们就将他们识别为同一类错误,将他们在 MySQL 中存为同一条:

如上图所示,message_id 是聚合后的那一串东西,event_count 表示同类型错误出现的次数。最后我们将这些数据在可视化中这样展示:

2.5、清洗过程监控

线上错误有了 SDK 进行监控,那么 g't
先看单位时间内数据量和耗时是否正常、忽略输的数据时间轴上数量是否稳定以及每分钟拉取数据量是否是 10k(之前讲过削峰价机制)

先看单位时间内数据量和耗时是否正常、忽略输的数据时间轴上数量是否稳定以及每分钟拉取数据量是否是 10k(之前讲过削峰机制,每分钟执行上线 10k 条数据)。

三、监控


web端错误监控的最后一个小节,我们来看监控。当错误短时间内大量爆发的时候我们需要第一时间将错误信息告知给开发者,让那些小崽子们第一时间处理线上错误。从而让线上错误影响减轻到最小。错误的告警模型比较简单:

就是当某个错误满足一定条件的时候,将错误通过钉钉、邮箱、电话、短信或 webhook 等方式告诉订阅该项目的开发者。而条件我们可以通俗的理解为:某种错误,在连续 2 分钟内,每分钟报错量大于等于 100 条的时候就触发告警。然后将错误信息触达到开发者!



如上图所示,我们将告警分为普通告警和升级告警。升级告警中会在普通告警基础上额外增加短信和工作群,相信视频前的开发者们不管去哪里都会带着个手机,哪怕去上厕所,你们自己说是不是[斜眼笑]?然后这些告警信息上会有标题和内容。标题含有错误来源、错误等级、业务名;错误内容上会有错误描述和影响了多少用户等等。

好,至此我们已经完成了 Web 端 的错误监控,实现了多端错误监控的最小闭环!

接下来思考其他端的错误监控如何实现呢?我们先来根据天网的整体架构图来回顾一下 web 端的错误监控如何实现:

我们来思考上面这个图中,端与端之间哪些流程我们是可以共用的呢?而又有哪些流程是端与端之间是有差别的?

是的,你猜对了。除了错误采集上报是端与端之间不一样的地方之外,其它流程都是可以共用的!我们以此为切入点,接下来分析其他端或平台上的错误是如何采集与上报的。注意这里的核心思路是:差异化采集,格式化上报!后面的讲解中,请记住这句话!

四、node 端 sdk 实现

其它内容我们将在下一篇文章进行讲解。Tobe continue...

本文使用 mdnice 排版