前端监控平台系列:服务端功能设计与实现

avatar
@智云健康

本文作者: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天的签到记录:

  1. 用数据库来存储,建一张包含时间、员工id,每次有人签到就直接在对应表中insert缺点:如果人数很多并且同时签到,会造成数据库连接池爆炸,不能实时更新。优点:存有明细数据,可以查询某一天的某个时刻签到的
  2. 用每天的日期当做bitmapkeyvalueuserId,一个userId在内存中占了二进制位(bit),比如连续自增userId10万个人签到在内存中占了(12500B ≈ 12K,如果换成hash表,相当与用word来存储,内存至少大了16倍),判断一个人是否签到只需要判断当前userId所处的二进制位是否被占。缺点:需要自己在内存中额外建一个map来对应明细数据,比如用户在某天签到的具体时间。优点:运行内存速度快、支持高并发,实时查询、插入、更改

sdkToEnd

员工签到-bitmap

场景二:就以本系统的错误标签为例,一个项目会有多个error,一个error会有多个eventevent与error的关系),如果想存每个错误的标签集合,后续用来做标签可视化报表或者用来搜索,一个错误是由哪些浏览器版本报的、ip集合、自定义的标签集合等等,至少会有10个以上的标签,用bitmap实现:

使用bitmap来存储每个错误的标签集合:比如一个错误可能有很多浏览器版本和很多IP,这样做的后果是,一个错误要对应12~15个bitmap,一个bitmap对应一个标签集合,比如IP可能有几十万个,在bitmap中仅仅占了几k的内存,而且还能去重,但是我需要展示这个ip值,所以就需要另建一个表来专门存储唯bitmap对应的值与IP值进行映射,不太好维护,所以最终还是选择了mysql作为存储方式

总结:

  1. 需要对明细数据进行大量操作的话建议放入数据库

  2. 需要实时统计并且数据量较大时使用bitmap进行快速查找,判重与删除

参考链接

mysql

用来存储用户、团队、项目、错误、错误级别的标签、项目级别的标签的结果数据

sls代替ES

由于某些原因所以没有采用Elasticsearch,采用是阿里云的日志服务(sls),但是由于sls不支持更新已经插入的数据,所以部分表:比如标签集合表还是放入mysql来处理

功能点

流程概览

sdkToEnd

流程概览

表结构设计

sdkToEnd

表结构概览

如上图所示,总共有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

sdkToEnd

sdkToEnd

获取错误标签

  1. 用户真实IP:利用nginx反向代理,在请求头中添加x-real-ip字段,nginx配置:proxy_set_header x-real-ip $remote_addr;,根据ip获取获取地理位置以及运营商
  2. 根据user-agent字段可以拿到浏览器版本、系统版本、设备等等
  3. 根据SDK上报的数据可以拿到当前错误的类型、sdk版本、自定义标签、trackerId(用户唯一标识)、traceId(请求接口唯一标识)等等

收集错误标签

上面获取了这么多标签,那为什么要收集这些标签呢?有以下几种好处:

  • 更好的搜索:可以用多重搜索更精确的筛选出部分错误,如下图所示:

front_multiple_search

多重搜索
  • 查看错误详情时有更多标签信息

    错误标签信息
  • 更好的统计标签集合

标签集合统计

批量更新入库

批量入库概览

比如3分钟的定时任务从redis.list中获取前面100条数据,在服务器处理的时间内,可能错误会再次被推上来,所以需要结束时再进行一次计算。

取出数据

裁剪redis

缓存与定时任务的作用:减少高峰期对数据库频繁读写导致连接池爆满,定时任务中批量处理的数据可以适当提高,比如三分钟的定时任务可以处理10000条应该是绰绰有余,这个数值可以根据项目的数量自定义。

告警规则和实现

错误收集完,然后就是通知开发者及时解决错误,不管是更新错误状态或者是让开发看到某个错误的存在,但又不能频繁的通知开发者,所以需要制定一套告警规则,这套告警规则还可以根据不同项目来自定义

告警规则

http告警规则

每个错误进来会有错误等级区分,比如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等级,也是说每次传进来这个错误都会通知对应负责人

MITO.log上报p1错误

多重标签搜索

front_multiple_search

多重标签数据显示

需要做到标签数据下拉框,需要用到project_tag表,这个表收集了项目级别的所有标签,具体怎么批量收集上面已经讲过(批量更新入库),每次切换项目时,对应的标签数据也是不同的。拿到标签后需要到sls中搜索并去重得到errorId数组,然后通过查询errors表进一步对当前errorId数组进行过滤、排序、分页,最终返回给前端

front_multiple_search

多重标签搜索流程

错误详情(前端)

下面是部分错误详情的组件前端展示:

用户行为栈

用户行为栈是MITO-SDK收集的信息,可以配置栈的个数。主要是作用是:查看某个错误触发时的上下文,比如某个接口报错,通过用户行为栈可以看出来在这个接口触发前用户做了哪些事。

sourcemap还原

利用source-map进行还原操作,将打包后的js还原成开发环境下的 . vue或者.js文件。

sourcemap还原前

sourcemap还原后

示例参考noerror:用sourcemap包还原打包后的js

其他标签

结尾

开源监控-SDK:将会支持react、小程序、更多hook

后续可能用放出saas服务免费使用

期待下一篇:页面性能监控的实践

点击关注,不迷路!!!每周都会翻译或原创高质量文章哦!!!