Skip to main content

性能监控

如果让你来实现一个前端监控系统,应该考虑什么?如何去实现?

一、为什么要做前端监控

更快的发现问题 做产品决策依据 提升前端开发的技术深度和广度 为业务扩展提供更多可能性

二、前端数据分类

前端的数据其实有很多,从大众普遍关注的 PV、UV、广告点击量,到客户端的网络环境、登陆状态,再到浏览器、操作系统信息,最后到页面性能、JS 异常,这些数据都可以在前端收集到。

2.1 访问相关的数据

PV/UV:最基础的 PV(页面访问量)、UV(独立访问用户数据量)

页面来源:页面的 referer,可以定位页面的入口

操作系统:了解用户的 OS 情况,帮助分析用户群体的特征,特别是移动端、iOS 和 Android 的分布就更有意义了

浏览器:可以统计到各种浏览器的占比,对于是否继续兼容 IE6、新技术(HTML5、CSS3 等)的运用等调研提供参考价值

分辨率:对页面设计提供参考,特别是响应式设计

登录率:登陆用户具有更高的分析价值,引导用户登陆是非常重要的

地域分布:访问用户在地理位置上的分布,可以针对不同地域做运营、活动等

网络类型:wifi/3G/2G,为产品是否需要适配不同网络环境做决策

访问时段:掌握用户访问时间的分布,引导消峰填谷、节省带宽

停留时长:判断页面内容是否具有吸引力,对于需要长时间阅读的页面比较有意义

到达深度:和停留时长类似,例如百度百科,用户浏览时的页面到达深度直接反映词条的质量

2.2 性能相关的数据

白屏时间:用户从打开页面开始到页面开始有东西呈现为止,这过程中占用的时间就是白屏时间

首屏时间:用户浏览器首屏内所有内容都呈现出来所花费的时间

用户可操作时间:用户可以进行正常的点击、输入等操作 页面总下载时间:页面所有资源都加载完成并呈现出来所花的时间,即页面 onload 的时间 自定义的时间点:对于开发人员来说,完全可以自定义一些时间点,例如:某个组件 init 完成的时间、某个重要模块加载的时间等等

2.3 点击相关的数据

页面总点击量

人均点击量:对于导航类的网页,这项指标是非常重要的

流出 url:同样,导航类的网页,直接了解网页导流的去向

点击时间:用户的所有点击行为,在时间上的分布,反映了用户点击操作的习惯

首次点击时间:同上,但是只统计用户的第一次点击,如果该时间偏大,是否就表明页面很卡导致用户长时间不能点击呢?

点击热力图:根据用户点击的位置,我们可以画出整个页面的点击热力图,可以很直观的了解到页面的热点区域

2.4 异常相关的数据

这里的异常是指 JS 的异常,用户的浏览器上报 JS 的 bug,这会极大地降低用户体验

异常的提示信息:这是识别一个异常的最重要依据,如:e.src 为空或不是对象

JS 文件名

异常所在行

发生异常的浏览器

堆栈信息:必要的时候需要函数调用的堆栈信息,但是注意堆栈信息可能会比较大,需要截取

2.5 其它数据

除了上面提到的 4 类基本的数据统计需求,我们当然还可以根据实际情况来定义一些其他的统计需求,如用户浏览器对 canvas 的支持程度, 再比如比较特殊的-用户进行轮播图翻页的次数,这些数据统计需求都是前端能够满足的,每一项统计的结果都体现了前端数据的价值

三、性能指标

FP(First Paint):首次绘制时间,包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻。

FCP(First Content Paint):首次内容绘制。是浏览器将第一个 DOM 渲染到屏幕的时间,可能是文本、图像、SVG 等。这其实就是白屏时间

FMP(First Meaningful Paint):首次有意义绘制。页面有意义的内容渲染的时间

LCP(Largest Contentful Paint)。最大内容渲染。代表在 viewport 中最大的页面元素加载的时间。

DCL(DomContentLoaded):DOM 加载完成。当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发。无需等待样式表,图像和子框架的完成加载。 L(onload):当依赖的资源全部加载完毕之后才会触发。

TTI(Time to Interactive):可交互时间。用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点。

FID(First Input Delay):首次输入延迟。用户首次和页面交互(单击链接、点击按钮等)到页面响应交互的时间。

四、前端监控目标(监控分类)

4.1 稳定性(stability)

JS 错误,JS 执行错误或者 Promise 异常

资源异常,script、link 等资源加载异常

接口错误,ajax 或 fetch 请求接口异常

白屏,页面空白

4.2 用户体验(experience)

加载时间,各个阶段的加载时间

TTFB(Time To First Byte 首字节时间)。是指浏览器发起第一个请求到数据返回第一个字节所消耗的时间,这个时间包含了网络请求时间、后端处理时间。

FP(First Paint 首次绘制)。首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时间。

FCP(First Content Paint 首次内容绘制)。首次内容绘制是浏览器将第一个 DOM 渲染到屏幕的时间,可以是任何文本、图像、SVG 等的时间。

FMP(First Meaningful Paint 首次有意义绘制)。 首次有意义绘制是页面可用性的量度标准。

FID(First Input Delay 首次输入延迟)。用户首次和页面交互到页面响应交互的时间。

卡顿。 超过 50ms 的任务。

4.3 业务

PV:page view 即页面浏览量或点击量 UV:指访问某个站点的不同 IP 地址的人数。

页面停留时间:用户在每一个页面的停留时间。

五、前端监控流程

数据埋点

数据上报

分析和计算,将采集到的数据进行加工总结

可视化展示,将数据按照各种维度进行展示

监控报警,发现问题后按一定的条件触发报警

六、常见的埋点方案

6.1 代码埋点

代码埋点,就是以嵌入代码的形式进行埋点,比如要监控用户的点击事件,会选择在用户点击时插入一段代码,保存这个监听行为或者直接将监听行为 以某一种数据格式直接传递给服务器端。

优点是可以在任意时刻,精确的发送或保存所需要的数据信息。

缺点就是工作量大

6.2 可视化埋点

通过可视化交互的手段,代替代码埋点。

将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义 的增加埋点事件等等。最后输出的代码耦合了业务代码和埋点代码

可视化埋点其实是用系统来代替手工插入埋点代码。

6.3 无痕埋点

前端的任意一个事件都被绑定一个标识,所有的事件都被记录下来。

通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析

无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象

缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构

七、编写监控采集脚本

7.1 监控错误

错误分类

JS 错误

Promise 异常

资源异常

监听 error

7.2 数据结构设计

jsError

let info = {
title: "前端监控系统", // 页面标题
url: "http://localhost:8080", // 页面url
timestamp: "1212121212121212", // 访问时间戳
userAgent: "chrome", // 用户浏览器类型
kind: "stability", // 大类
type: "error", // 小类
errorType: "jsError", // 错误类型
message: "uncaught TypeError:blablabla", // 错误详情
filename: "http://localhost:8080/", // 访问的文件名
position: "0:0", // 行列信息
stack: "btn Click (http://localhost:8080)", // 堆栈信息
selector: "HTML BODY #container .content INPUT", // 选择器
};
接口异常数据结构设置
let info = {
title: "前端监控系统", // 页面标题
url: "http://localhost:8080", // 页面url
timestamp: "1212121212121212", // 访问时间戳
userAgent: "chrome", // 用户浏览器类型
kind: "stability", // 大类
type: "xhr", // 小类
eventType: "load", // 事件类型
pathname: "/success",
status: "200-0k",
duration: "5", // 持续时间
response: "hahah", // 响应内容
params: "参数", // 参数
};

白屏 screen 返回当前 window 的 screen 对象,返回当前渲染窗口中和屏幕有关的属性

innerWidth 只读的 window 属性。innerWidth 返回以像素为单位的窗口的内部宽度

innerHeight 窗口的内部高度(布局视口)的高度

layout_viewport

elementsFromPoint 方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素。

let info = {
title: "前端监控系统",
url: "http://localhost:8080/",
timestamp: "1239404040404044",
userAgent: "chorme",
kind: "stability",
type: "blank",
emptyPoints: "0", // 空白点
screen: "2049 * 1152", // 分辨率
viewPoint: "2048 * 994", // 视口
selector: "HTML BODY #container", // 选择器
};

整体大致可以分四个阶段:信息采集、存储、分析、监控。

采集阶段:收集异常日志,先在本地做一定的处理,采取一定的方案上报到服务器。

存储阶段:后端接收前端上报的异常日志,经过一定处理,按照一定的存储方案存储。

分析阶段:分为机器自动分析和人工分析。机器自动分析,通过预设的条件和算法,对存储的日志信息进行统计和筛选,发现问题,触发报警。人工分析,通过提供一个可视化的数据面板,让系统用户可以看到具体的日志数据,根据信息,发现异常问题根源。

报警阶段:分为告警和预警。告警按照一定的级别自动报警,通过设定的渠道,按照一定的触发规则进行。预警则在异常发生前,提前预判,给出警告。

性能监控: 使用 Resource Timing API 和 Performance Timing API,可以计算许多重要的指标,比如页面性能统计的起始点时间、首屏时间等。

异常监控: 前端捕获异常分为全局捕获和局部捕获。局部捕获作为补充,对某些特殊情况进行捕获,但分散,不利于管理。所以,我会选择全局捕获的方式,即通过全局的接口,将捕获代码集中写在一个地方。具体在实现项目中,我应该会采用 badjs-report,它重写了 window.onerror 进行上报异常,无需编写任何捕获错误的代码。

前端埋点: 埋点的方案有手动埋点,即在需要监控的地方插入监控逻辑,但是工作量可能会很大;还有无埋点,前端自动采集全部事件,上报埋点数据,但是缺点是服务器压力会很大。我可能倾向于采用声明式埋点,将埋点代码和具体的业务逻辑解耦,只用关心需要埋点的控件,并且为这些控件声明需要的埋点数据即可,主要是为了降低埋点的成本吧。在 dom 元素上增添埋点信息,比如

// key表示埋点的唯一标识;act表示埋点方式
<button data-stat="{key:'buttonKey', act: 'click'}">埋点</button>

监控告警: 这里我认为最便捷、高效的方式,就是接入内部的告警组了吧,尤其是在阿里,似乎什么轮子都有,那可能需要考虑就是触发告警的阈值和时机了。

性能:使用 Performance API,可以得到许多重要的指标,如页面性能统计的起始点时间、首屏时间等

报错:使用 onerror 和 onunhandledrejection,甚至是 try catch

操作行为:对事件触发函数做 patch,或者添加特定的事件监听

PV/UV:利用浏览器存储方法或 Cookie、IP 等储存相应用户信息,随请求发送

设备信息:获取 navigator.userAgent

PV、UV 属于增长数字类型,可以用 Redis 等记录,如果有需要,定时入库。其他属于大量文字信息,可以用成熟的消息队列来消费。因为有大量写,所以可以考虑做读写分离。

技术难点:

可能整个系统比较复杂的就是如何高效合理的进行监控数据上传。除了异常报错信息本身,还需要记录用户操作日志,如果任何日志都立即上报,这无异于自造的 DDOS 攻击。那就需要考虑前端日志的存储,日志如何上传,上传前如何整理日志等问题。

前端在收集的过程中可能会影响用户体验。

后端对于收到的日志要使用合适的工具进行收集,数据量大时选择如何取舍。

可能会采取的方案

indexDB 存储日志,因为容量大、异步!不用考虑阻塞页面问题。 在一个 webworker 中对日志进行整理,比如对每一条日志打上标签,进行分类等操作。 上报日志也在 webworker 中进行,可以按照重要紧急度区分,判断是否延时或者立即上报。

实现一个系统,统计前端页面性能、页面 JS 报错、用户操作行为、PV/UV、用户设备等消息,并进行必要的监控报警。方案如何设计,用什么技术点,什么样的系统架构,难点会在哪里?

一、需求背景

1.1 为什么要做

更快的发现问题

做产品决策依据

提升前端开发的技术深度和广度

为业务扩展提供更多可能性

为了实现收集功能,我们需要一个前端监控平台,它能够收集数据、处理数据、存储数据、查询数据。其实就有很多现成的平台或者开源项目我们可以直接使用。

1.2 行内通用方案

前端技术发展至今,相信大家已经对前端监控的这件事情非常熟悉,或多或少都会在我们的项目中用上它。比如搭建使用开源项目 sentry、付费平台阿里的 ARMS、甚至小程序配套的前端监控服务。

sentry,sentry 主要提供的功能是收集错误。支持大多流行语言的客户端和服务端,不支持小程序,但是目前有大公司根据 sentry 的上报数据结构,自己实现了小程序 SDK 并开源,目前关注度和流行度都偏低。除开错误,它的其它类型的前端监控能力相对来说很弱。

阿里 ARMS,ARMS 提供的功能与支持的客户端比较齐全,小程序也支持。只是需要付费。总体来说提供的功能还是比较全面、符合国内的环境。

小程序自带监控 微信小程序不断的在完善内部的监控,各方面的功能也慢慢丰富了起来,但是只能支持小程序本身。

在使用这些开源或者平台前端监控服务的时候,始终有一些不足。比如:

系统分散

很难满足增加一些自定义数据和查询的需求

特性一直不更新、Bug 解决周期长

二次开发难度大

1.3 定制化

如果完全从 0 到 1 来打造一套前端监控系统,成本也是很高的。甚至在早期,都可能没人愿意用,系统是否能立项或者持续发展下去也是一个问题。于是从一些开源项目中去寻找,去找一个方便改造也有一定功能模块的项目。可以基于它的代码,进行长期的改造和迭代。慢慢的改造成为一个更适合公司内部环境的一个前端监控系统。

二、系统架构

2.1 基本构成

客户端 SDK

web

服务端 node + EggJs

数据库 Redis Mongo+mongooseJs(orm)

管理台 Vue + ElementUI

为了实现前端监控,第一要素是要收集客户端数据,为了方便客户端集成监控系统、我们需要首先开发封装一个统一的 SDK、去帮助我们收集数据。

SDK 收集了数据,我们还需要通过服务端接口来接收,在服务端,使用 node+EggJs,node 适合 i/o 密集型场景,符合前端技术栈。eggjs 简单易用、文档友好,大部分使用 node 的前端程序员都应该能很快上手。

服务端收集到数据并进行一些处理之后,我们需要存储到我们的数据库之中。在数据库方面,使用 mongo 做持久化存储,mongo 文档模型数据库,数据扩展方便,类 json 结构方便和 node 配合使用,天生适合日志系统。使用 redis 做数据缓存,redis 简单易用的高性能 key-value 数据库,市场上占据主流,被大部分人都熟知。

最后,还需要一个管理台,做数据查询与管理。管理台使用 Vue+ElementUI,简单快速。

客户端 SDK 收集数据上报,node 服务端获取到数据后,先存在 redis 中,node 服务会根据消费能力去拉取 redis 数据处理分析后存储到 mongo 之中,最后我们通过管理后台展示处理好的应用数据。

2.2 技术的可能的一些难点

可能整个系统比较复杂的就是如何高效合理的进行监控数据上传。除了异常报错信息本身,还需要记录用户操作日志,如果任何日志都立即上报,这无异于自造的 DDOS 攻击。那就需要考虑前端日志的存储,日志如何上传,上传前如何整理日志等问题。

对于日志的处理上报有这样的处理方案

前端存储日志 我们并不单单采集异常本身日志,而且还会采集与异常相关的用户行为日志。单纯一条异常日志并不能帮助我们快速定位问题根源,找到解决方案。但如果要收集用户的行为日志,又要采取一定的技巧,而不能用户每一个操作后,就立即将该行为日志传到服务器,对于具有大量用户同时在线的应用,如果用户一操作就立即上传日志,无异于对日志服务器进行 DDOS 攻击。因此,我们先将这些日志存储在用户客户端本地,达到一定条件之后,再同时打包上传一组日志。

那么,如何进行前端日志存储呢?我们不可能直接将这些日志用一个变量保存起来,这样会挤爆内存,而且一旦用户进行刷新操作,这些日志就丢失了,因此,我们自然而然想到前端数据持久化方案。

目前,可用的持久化方案可选项也比较多了,主要有:Cookie、localStorage、sessionStorage、IndexedDB、webSQL 、FileSystem 等等。那么该如何选择呢?我们通过一个表来进行对比:

存储方式 cookie localStorage sessionStorage IndexedDB webSQL FileSystem

类型 key-value key-value NoSQL SQL

数据格式 string string string object

容量 4k 5M 5M 500M 60M

进程 同步 同步 同步 异步 异步

检索 key key key, index field

性能 读快写慢 读慢写快

综合之后,IndexedDB 是最好的选择,它具有容量大、异步的优势,异步的特性保证它不会对界面的渲染产生阻塞。而且 IndexedDB 是分库的,每个库又分 store,还能按照索引进行查询,具有完整的数据库管理思维,比 localStorage 更适合做结构化数据管理。但是它有一个缺点,就是 api 非常复杂,不像 localStorage 那么简单直接。针对这一点,我们可以使用 hello-indexeddb 这个工具,它用 Promise 对复杂 api 进行来封装,简化操作,使 IndexedDB 的使用也能做到 localStorage 一样便捷。另外,IndexedDB 是被广泛支持的 HTML5 标准,兼容大部分浏览器,因此不用担心它的发展前景。

日志上报

按照上报的频率(重要紧急度)可将上报分为四类

即时上报

收集到日志后,立即触发上报函数。仅用于 A 类异常。而且由于受到网络不确定因素影响,A 类日志上报需要有一个确认机制,只有确认服务端已经成功接收到该上报信息之后,才算完成。否则需要有一个循环机制,确保上报成功。

批量上报

将收集到的日志存储在本地,当收集到一定数量之后再打包一次性上报,或者按照一定的频率(时间间隔)打包上传。这相当于把多次合并为一次上报,以降低对服务器的压力。

区块上报

将一次异常的场景打包为一个区块后进行上报。它和批量上报不同,批量上报保证了日志的完整性,全面性,但会有无用信息。而区块上报则是针对异常本身的,确保单个异常相关的日志被全部上报。

用户主动提交

在界面上提供一个按钮,用户主动反馈 bug。这有利于加强与用户的互动。

或者当异常发生时,虽然对用户没有任何影响,但是应用监控到了,弹出一个提示框,让用户选择是否愿意上传日志。这种方案适合涉及用户隐私数据时。

即时上报 批量上报 区块上报 用户反馈

时效 立即 定时 稍延时 延时

条数 一次全部上报 一次 100 条 单次上报相关条目 一次 1 条

容量 小 中 – –

紧急 紧急重要 不紧急 不紧急但重要 不紧急

即时上报虽然叫即时,但是其实也是通过类似队列的循环任务去完成的,它主要是尽快把一些重要的异常提交给监控系统,好让运维人员发现问题,因此,它对应的紧急程度比较高。

批量上报和区块上报的区别:批量上报是一次上报一定条数,比如每 2 分钟上报 1000 条,直到上报完成。而区块上报是在异常发生之后,马上收集和异常相关的所有日志,查询出哪些日志已经由批量上报上报过了,剔除掉,把其他相关日志上传,和异常相关的这些日志相对而言更重要一些,它们可以帮助尽快复原异常现场,找出发生异常的根源。

用户提交的反馈信息,则可以慢悠悠上报上去。