本文作者:cjinhuo,未经授权禁止转载。
背景
接着上一个系列的前端监控平台系列:JS SDK(已开源),这篇的主要目的讲下服务端的功能设计与实现
技术栈
nestjs
nestjs对TS支持较好,有丰富的装饰器以及开箱即用的依赖注入容器
redis
redis.hash
由于错误上报是一个经常性的操作,如果每次都查询数据库比较浪费资源,所以用来存放apikey
与项目间的联系等一些需要常用的数据,全部都存在一个hash
中是为了跟别的项目区分开,相当于一个命名空间的作用(hash
中的key
不能设置过期时间)
redis.string
用来存放用户id信息、项目id信息以及一些高频使用数据,但是切记要设置过期时间,不然一些长期没有用的数据就会残留在redis
中
redis.list
用来实现类似RabbitMQ的功能,用于批量计算入库,下文有介绍
redis.bitmap
bitmap
用来统计某个标签的数量不仅速度快、支持高并发、占的内存特别少,因为是用二进制来存储每个对应标签的值,首先来了解下bit(位)、byte(字节)、word(字)
Bit&Byte&Word
Bit = Binary digIT = 0 or 1
Byte = a sequence of 8 bits = 00000000, 00000001, ..., or 11111111
Word = a sequence of N bits where N = 16, 32, 64 depending on the computer
场景一:比如我现在需要统计全公司所有人(有10万个员工)的一年内365天的签到记录:
- 用数据库来存储,建一张包含时间、员工id,每次有人签到就直接在对应表中
insert
,缺点:如果人数很多并且同时签到,会造成数据库连接池爆炸,不能实时更新。优点:存有明细数据,可以查询某一天的某个时刻签到的 - 用每天的日期当做
bitmap
的key
,value
是userId
,一个userId
在内存中占了二进制位(bit),比如连续自增userId
10万个人签到在内存中占了(12500B ≈ 12K,如果换成hash
表,相当与用word
来存储,内存至少大了16倍),判断一个人是否签到只需要判断当前userId
所处的二进制位是否被占。缺点:需要自己在内存中额外建一个map来对应明细数据,比如用户在某天签到的具体时间。优点:运行内存速度快、支持高并发,实时查询、插入、更改
场景二:就以本系统的错误标签为例,一个项目会有多个error
,一个error
会有多个event
(event与error的关系),如果想存每个错误的标签集合,后续用来做标签可视化报表或者用来搜索,一个错误是由哪些浏览器版本报的、ip集合、自定义的标签集合等等,至少会有10个以上的标签,用bitmap
实现:
使用bitmap来存储每个错误的标签集合:比如一个错误可能有很多浏览器版本
和很多IP
,这样做的后果是,一个错误要对应12~15个bitmap
,一个bitmap
对应一个标签集合,比如IP可能有几十万个,在bitmap
中仅仅占了几k的内存,而且还能去重,但是我需要展示这个ip
值,所以就需要另建一个表来专门存储唯bitmap
对应的值与IP值进行映射,不太好维护,所以最终还是选择了mysql作为存储方式
总结:
-
需要对明细数据进行大量操作的话建议放入数据库
-
需要实时统计并且数据量较大时使用bitmap进行快速查找,判重与删除
参考链接:
mysql
用来存储用户、团队、项目、错误、错误级别的标签、项目级别的标签的结果数据
sls代替ES
由于某些原因所以没有采用Elasticsearch
,采用是阿里云的日志服务(sls),但是由于sls
不支持更新已经插入的数据,所以部分表:比如标签集合表还是放入mysql
来处理
功能点
流程概览
表结构设计
如上图所示,总共有10个表加1个sls(日志服务),下面就来讲讲每个表的作用与关系
events
用来存放用户行为、错误栈、标签信息、错误信息,每条错误上报后最终都会存入events
中,放到sls
中的原因是sls
对TB级数据多重条件搜索也只要几百毫秒。
errors表
用来存放错误状态、错误影响用户量、错误事件数、错误等级、错误所属项目id、错误所属开发人员等等
event与error的关系
一个error
对应多个events
。比如现在有两台电脑同时访问同一页面,这个页面有个接口报错,比如是500服务器内部异常
,MITO-SDK会根据接口地址、错误类型:http
、状态码、请求方法进行hashCode
生成errorId
,如果这些参数是一样的话,那么hashCode
出来得errorId
也应该是一样的,那么这一个error
会被推入到mysql
,这两个event
会分别被推入到sls
,虽然是同一个error
,但是这两个event
的标签不一样:比如ip
、浏览器版本等等
error_tag表与project_tag
了解了event与error的关系,error_tag表
用来收集错误级别下面所有事件的标签种类和数量,用来统计某个错误下的标签集合,project_tag表
用来收集项目级别下的所有错误的所有的事件的标签种类和数量,用来多重搜索的下拉框数据显示
project表
用来存放项目名称、apikey(SDK和project之间的联系)
user_project表
用来关联user表
和project表
的连接表,存放用户id和项目id
team表
存放团队名称、团队通知方式
user_team表
用来关联user表
和team表`的连接表,存放用户id和团队id
sourcemap表
打包后的.js
文件需要有.map
才能还原出生产环境的代码,用来存放.js和.map文件的地址
collect表
存放用户id、错误id,表示用户收藏了哪些错误
错误收集
SDK在客户端负责收集错误并上报到服务端,服务端需提供一个接口用来保存错误信息,由于SDK是存放于客户端,所以并发量会比较高,如果采取单条错误过来后,直接计算影响用户数和一些结果数据并且直接入库,这样会导致数据库连接池崩溃,服务端的压力也会因为并发量的增加逐渐崩溃。
错误收集
错误收集的部分是交给MITO-SDK来完成,通过SDK收集客户端的错误信息,并配置dsn
,然后上报到指定服务端
批量入库
缓存到redis
为了缓解高峰期服务端的压力,所有事件抛上来时先将事件需要的信息提取完然后放入redis.list
中,然后起一个定时任务来慢慢消费redis.list
获取错误标签
- 用户真实IP:利用nginx反向代理,在请求头中添加
x-real-ip
字段,nginx配置:proxy_set_header x-real-ip $remote_addr;
,根据ip
获取获取地理位置以及运营商 - 根据
user-agent
字段可以拿到浏览器版本、系统版本、设备等等 - 根据SDK上报的数据可以拿到当前错误的类型、sdk版本、自定义标签、trackerId(用户唯一标识)、traceId(请求接口唯一标识)等等
收集错误标签
上面获取了这么多标签,那为什么要收集这些标签呢?有以下几种好处:
- 更好的搜索:可以用多重搜索更精确的筛选出部分错误,如下图所示:
-
查看错误详情时有更多标签信息
错误标签信息 -
更好的统计标签集合
批量更新入库
比如3分钟的定时任务从redis.list
中获取前面100条数据,在服务器处理的时间内,可能错误会再次被推上来,所以需要结束时再进行一次计算。
缓存与定时任务的作用:减少高峰期对数据库频繁读写导致连接池爆满,定时任务中批量处理的数据可以适当提高,比如三分钟的定时任务可以处理10000条应该是绰绰有余,这个数值可以根据项目的数量自定义。
告警规则和实现
错误收集完,然后就是通知开发者及时解决错误,不管是更新错误状态或者是让开发看到某个错误的存在,但又不能频繁的通知开发者,所以需要制定一套告警规则,这套告警规则还可以根据不同项目来自定义。
告警规则
每个错误进来会有错误等级区分,比如HTTP_ERROR
,第一个错误进来是个p4
等级,此时不通知,随着数量和影响用户量的提升,不断升级,比如事件数达到30个事件数就升为p3
这时就应该通知到对应项目负责人,达到60个时再升一级并且再次通知对应负责人,当数量达到500时升为p1
级,此时事件数已经足够庞大,我们就可以认定为这个错误必须要解决(或者选择忽视),此时每次错误进来都将会通知对应负责人。当然这个是可以自定义的,每个项目需要根据用户量的大小不同,设置不同的等级制度
不同项目对应等级数量不同
比如项目A的日活(日活跃用户数量)有:50、项目B的日活有:10000,那么需要将FetchRule的调整下:
错误状态总共有以下五种
- 未解决
一个错误上报后默认就是未解决状态。可以被更改成:解决中、已忽视
- 解决中-状态(solving)
如果你在监控平台上看到该错误,并且认为它需要被解决,但是又不想被同一个错误一直通知,这时将错误状态改为解决中,那么你将会有两小时的时间来修正的错误,在这两个小时内,该错误被再次推进来时,不会通知负责人,并且在两小时后服务端会自动将状态改为已解决。如果两小时后被再次触发,那么状态将变成重新打开,并按照正常的告警规则来通知对应开发者。当前状态不可被更改
- 忽视状态-状态(ignored)
如果等级已经达到p1
后,但是你认为这个错误暂时不想解决或者根本没必要解决,那么可以将改错误置为忽视状态
如果置为忽视状态时,那么这个错误升到p1
级也不会通知对应负责人。当前状态可以被更改成:解决中
- 重新打开
如果一个错误被改为已解决后,这时又上报了一个错误,状态就变成重新打开,后续的告警通知还是走正常流程。当前状态可以被更改成:解决中、已忽视
实现
把状态改为解决中,两小时内不会再发通知,两小时后自动把状态改为已解决?怎么实现在两小时内不通知,并且在2小时后自动将状态改为已解决:
在redis中设置一个过期时间为2小时的key,并在设置完成的回调函数里面添加setTimeout
,2小时后就可以处理一些逻辑,但是有个问题,如果此时,服务端重新发版后,这些setTimeout
会消失掉,怎么破?这时需要借助redis.hash
:
借助redis.hash
的持久缓存,hash.key
是错误id,hash.value
是到期时间戳,那么就算发版后,重新读下redis.hash
重新启动这些setTimeout
就可以了。在这两小时中,凡是有错误上报,判断状态是解决中就不告警通知
手动上报-紧急通知
MITO-SDK可以支持手动上报数据的,假如有如下场景:在某个重要支付界面,你在一些重要的代码起前面加了trycatch
,一旦报错了需要第一时间通知到自己,这时就可以用MITO.log()
,这个方法可以传错误等级,下面代码传了level:critical
,在服务端对应的是p1
等级,也是说每次传进来这个错误都会通知对应负责人
多重标签搜索
需要做到标签数据下拉框,需要用到project_tag表
,这个表收集了项目级别的所有标签,具体怎么批量收集上面已经讲过(批量更新入库),每次切换项目时,对应的标签数据也是不同的。拿到标签后需要到sls中搜索并去重得到errorId
数组,然后通过查询errors表
进一步对当前errorId
数组进行过滤、排序、分页,最终返回给前端
错误详情(前端)
下面是部分错误详情的组件前端展示:
用户行为栈
用户行为栈是MITO-SDK收集的信息,可以配置栈的个数。主要是作用是:查看某个错误触发时的上下文,比如某个接口报错,通过用户行为栈可以看出来在这个接口触发前用户做了哪些事。
sourcemap还原
利用source-map进行还原操作,将打包后的js
还原成开发环境下的 . vue
或者.js
文件。
示例参考noerror:用sourcemap包还原打包后的js
其他标签
结尾
开源监控-SDK:将会支持react、小程序、更多hook
后续可能用放出saas
服务免费使用
期待下一篇:页面性能监控的实践
点击关注,不迷路!!!每周都会翻译或原创高质量文章哦!!!