背景
自研工具是为了解决内部问题而生,希望通过这些问题引起大家的共鸣:
是否知道重要的业务,该页面是可以正常服务于用户的?
能否在问题还没有大规模爆发之前,快速的感知到业务的异常?
怎么不去用户的电脑上就能直观的看到问题所在,从而俯瞰项目全局;能否从宏观到微观一路下钻快速的定位线上告警信息?
在跨部门沟通时拿出合理的证据,来告诉他这个时间段该接口就是无法访问的,并告知我们的参数传的很正确,帮助服务端反查问题。
产品和设计同学想要提升用户体验,研发不断迭代功能版本。那这些我们以为的优化点,效果究竟如何?怎么去衡量?
哪个广告位,哪个资源位更有价值?怎么能更为精准的触达用户痛点,为提升业务赋能?
我们看到这些疑问,都需要数据指标的支撑。从解决这些问题的角度出发,把反复出现或无法跟其他部门交代的问题,打造成可以帮助我们解决问题的产品。
所以在这种场景下,易车·前端监控应运而生。
它主要是多场景多维度实时的监控大盘,实现浏览器客户端的全链路监控,方便团队事后追查和整改,转变为事前预警和快速判定根因。
经过详细的规划以后,我们把前端监控分为四期,分别为:异常监控(一期)、性能监控(二期)、数据埋点(三期)、行为采集(四期),于 2020 年 6 月 23 号正式启动研发,目前处于二期阶段。
关键结构
为实现上述需求,监控系统主要分为四个阶段来实现;分别是:指标采集、指标存储、统计与分析、可视化展示。
指标采集阶段:通过前端集成的 SDK 收集请求、性能、异常等指标信息;在客户端简单的处理一次,然后上报到服务器。
指标存储阶段:用于接收前端上报的采集信息,主要目的是数据落地。
统计与分析阶段:自动分析,通过数据的统计,让程序发现问题从而触发报警。人工分析,是通过可视化的数据面板,让使用者看到具体的日志数据,从而发现异常问题根源。
可视化展示阶段:通过可视化的平台;在这些指标(API 监控、异常监控、资源监控、性能监控)中,追查用户行为来定位各项问题。
整体架构图
随着统计需求的增加以及前端应用的上线,数据量由早期的每天 100 多万条数据;到现在的每天约 7000 万条数据。架构上也经历了三次版本的迭代。这是最新版的架构图,主要经过 6 层处理。
采集层:PC 和 H5 使用了一套 SDK 监听事件采集指标,然后将监听到的指标通过 REST 接口往 Logback 推送数据。Logback 以长连接的方式,会把这些不同类型的指标数据推送到 Flume 集群当中。Flume 集群会将这些数据,分发到 Kafka Topic 进行存储。
处理层:由 Flink 去实时消费;Flink 会消费三种类型,分别是:离线数据落地、实时 ETL+图谱、明细日志。
存储层:离线数据会存储到 HDFS 中;实时 ETL+图谱数据会存储到 MySQL 中;明细数据会落入到 ES 中。
统计层:离线(DW、DM)、实时(分钟级->十分钟级->小时级)的方式,对指标进行汇总和统计。
应用层:最后由接口去汇总表和明细 ES 里查询数据。
展示层:然后前端输出图表、报表、明细、链路等信息。
技术方案
数据采集
采集最初的愿景是希望对业务无侵入性,业务系统无需改造,只需要嵌入一段代码即可。所以这些采集,都是 SDK 自动化的处理。SDK 会全局监听几个事件,分别为:错误监听、资源异常的监听、页面性能的监听、API 调用的监听。
通过这几项监听,最终汇总为 3 项指标的采集。
异常采集:调用 error/unhandledrejection 事件,用于捕获 JS、图片、CSS 等资源异常信息。**
性能采集:调用浏览器原生的 performance.timing API 捕获页面的性能指标。
接口采集:通过 Object.definePropety 代理全局的 XHR 用于捕获浏览器的 XHR/FETCH 的请求。
采集端 SDK 架构
SDK 主要分为两部分:
第一部分:SDK 主要是 SDK 的驱动,包含:入口、核心工具以及通用类型的推断。
第二部分:也叫做插件部分(蓝色区域),主要实现上面的三项数据指标的采集。接下来主要会详细的介绍第二部分,各项指标的采集方案。
异常采集方案
通过监听 error 错误,即可捕获到所有(JS 错误、图片加载、CSS 加载、JS 加载、Promise 等)异常;它也支持 InternalError、ReferenceError 等 7 种错误捕获。以下是关键性代码。
监听事件
/**
* 监听 error、unhandledrejection 方法处理异常信息
*
* @param {YicheMonitorInstance} instance SDK 实例
*/
export default function setupErrorPlugin(instance: YicheMonitorInstance) {
// JS 错误或静态资源加载错误
on('error', (e: Event, url: any, lineno: any) => {
handleError(instance, e, url, lineno);
});
// Promise 错误,IE 不支持
on('unhandledrejection', (e: any) => {
handleError(instance, e);
});
}
判断异常类型
/**
* W3C 模式支持 ErrorEvent,所有的异常从 ErrorEvent 这里取
*
* @param {MutationEvent} error 资源错误、代码错误
*/
function handleW3C(event: any) {
switch (event.type) {
// 判断脚本错误,还是资源错误
case 'error':
event instanceof ErrorEvent
? reportJSError(instance, event)
: reportResourceError(instance, event);
break;
// Promise 是否存在未捕获 reject 的错误
case 'unhandledrejection':
reportPromiseError(instance, event);
break;
}
}
捕获异常数据
/**
* 上报 JS 异常
*
* @param {YicheMonitorInstance} instance SDK 实例
* @param {ErrorEvent} event
*/
export default function reportJSError(
instance: YicheMonitorInstance,
event: ErrorEvent,
): void {
// 设置上报数据
const report = new ReportDataStruct('error', 'js');
const errorInfo = event.error
? event.error.message
: `未知错误:${event.message}`;
// 设置错误信息,兼容远程脚本不设置 Script error 导致的异常
report.setData({
det: errorInfo.substring(0, 2000),
des: event.error ? event.error.stack : '',
defn: event.filename,
deln: event.lineno,
delc: event.colno,
rre: 1,
});
}
处理 IE 兼容问题
捕获异常时处理下 IE 的兼容性问题即可,IE 的方案如下:
/**
* IE 8 的错误项,所以针对于 IE 8 浏览器,我们只需要获取到它出错了即可。
*
* 1. 错误消息
* 2. 错误页面
* 3. 错误行号(因为文件通常是压缩的,所以统计 IE8 的行号是没有任何意义的)
*
* @param {string} error 错误消息
* @param {string | undefined} url 异常的 URL
* @param {number | undefined} lineno 异常行数,IE 没有列数
*/
export function handleIE8Error(
error: string,
url?: string | undefined,
lineno?: number | undefined,
) {
return {
colno: 0,
lineno: lineno,
filename: url,
message: error,
error: {
message: error,
stack: `IE8 Error:${error}`,
},
} as ErrorEvent;
}
/**
* IE 9 的错误,需要在 target 里面获取到
*
* @param { Element | any } error IE9 异常的元素
*/
export function handleIE9Error(error: any) {
// 获取 Event
const event = error.currentTarget.event;
return {
colno: event.errorCharacter,
lineno: event.errorLine,
filename: event.errorUrl,
message: event.errorMessage,
error: {
message: event.errorMessage,
stack: `IE9 Error:${event.errorMessage}`,
},
} as ErrorEvent;
}
性能采集方案
浏览器页面加载过程
性能指标获取方式
我们借助于浏览器原生的 Navigation Timing API 能够获取到上述页面加载过程中的各项性能指标数据,用于性能分析,它的时间单位是纳秒级。
当然也借助于 PerformanceObserver API 等用于测量 FCP、LCP、FID、TTI、TBT、CLS 等关键性指标。
详细的计算公式
网络请求采集方案
网络请求,通过 Object.definePropety 的方式对 XHR 做的代理。关键性代码如下。
重写 XMLHttpRequest
这部分可以直接参考 ajax-hook 的实现原理。
export function hook(proxy) {
window[realXhr] = window[realXhr] || XMLHttpRequest
XMLHttpRequest = function () {
const xhr = new window[realXhr];
for (let attr in xhr) {
let type = "";
try {
type = typeof xhr[attr]
} catch (e) {
}
if (type === "function") {
this[attr] = hookFunction(attr);
} else {
Object.defineProperty(this, attr, {
get: getterFactory(attr),
set: setterFactory(attr),
enumerable: true
})
}
}
const that = this;
xhr.getProxy = function () {
return that
}
this.xhr = xhr;
}
return window[realXhr];
}
拦截所有请求
正常的情况下一个页面会请求多个接口,假如有 20 个请求;我们期望在阶段性的所有请求都结束已后,汇总成一条记录合并上报,这样能有效减少请求的并发量。
关键性代码如下:
/**
* Ajax 请求插件
*
* @author wubaiqing <wubaiqing@vip.qq.com>
*/
// 所有的数据请求,以及总量
let allRequestRecordArray: any = [];
let allRequestRecordCount: any = [];
// 成功的数据,200,304 的数据
let allRequestData: any = [];
// 异常的数据,超时,405 等接口不存在的数据
let errorData: any = [];
/**
* 监听 Ajax 请求信息
*
* @param {YicheMonitorInstance} instance SDK 实例
*/
export default function setupAjaxPlugin(instance: YicheMonitorInstance) {
let id = 0;
proxy({
onRequest: (config, handler) => {
// 过滤掉听云、福尔摩斯、APM
if (filterDomain(config)) {
// 添加请求记录的队列
allRequestRecordArray.push({
id,
timeStamp: new Date().getTime(), // 记录请求时长
config, // 包含:请求地址、body 等内容
handler, // XHR 实体
});
// 记录请求总数
allRequestRecordCount.push(1);
id++;
}
handler.next(config);
},
// 失败时会触发一次
onError: (err, handler) => {
if (allRequestRecordArray.length === 0) {
handler.next(err);
return;
}
for (let i = 0; i < allRequestRecordArray.length; i++) {
// 当前的数据
const currentData = allRequestRecordArray[i];
if (
currentData.handler.xhr.status === 0 && // 未发送
currentData.handler.xhr.readyState === 4
) {
errorData.push(
JSON.stringify(handleReportDataStruct(instance, currentData)),
);
allRequestRecordArray.splice(i, 1);
}
}
sendAllRequestData(instance);
handler.next(err);
},
onResponse: (response, handler) => {
// 没有请求就返回 Null
if (allRequestRecordArray.length === 0) {
handler.next(response);
return;
}
for (let i = 0; i < allRequestRecordArray.length; i++) {
// 当前的数据
const currentData = allRequestRecordArray[i];
// 只要请求加载完成,不管是成功还是失败,都记录是一次请求
if (currentData.handler.xhr.readyState === 4) {
// 正常的请求
if (
(currentData.handler.xhr.status >= 200 &&
currentData.handler.xhr.status < 300) ||
currentData.handler.xhr.status === 304
) {
allRequestData.push(
JSON.stringify(handleReportDataStruct(instance, currentData)),
);
} else {
if (currentData.handler.xhr.status > 0) {
// 具备状态码
// 错误的请求
errorData.push(
JSON.stringify(handleReportDataStruct(instance, currentData)),
);
}
}
// 删除当前数组的值
allRequestRecordArray.splice(i, 1);
}
}
// 发送数据
sendAllRequestData(instance);
handler.next(response);
},
});
}
function sendAllRequestData(instance) {
if (
allRequestData.length + errorData.length ===
allRequestRecordCount.length
) {
// 处理正常请求
if (allRequestData.length > 0 || errorData.length > 0) {
handleAllRequestData(instance);
}
// 处理异常请求
if (errorData.length > 0) {
handleErrorData(instance);
}
// 所有的数据请求,以及总量
allRequestRecordArray = [];
allRequestRecordCount = [];
// 成功的数据,200,304 的数据
allRequestData = [];
// 异常的数据,超时,405 等接口不存在的数据
errorData = [];
}
}
探针加载方案
探针加载有两种方式,他们分别有一些优缺点:
同步加载:采集 SDK 放到所有 JS 请求头的前面;因为加载顺序的问题,如果放在其他 JS 请求之后,之前的 JS 出现了异常,就捕获不到了。因为要提前加载 JS 资源,会对性能有一定影响。
异步加载:采集 SDK 通过执行 JS 后注入到页面中;如果能保障首次的 JS 无异常,也可以使用异步的方式加载 SDK,对首屏优化有好处。
目前我们采用的是第一种同步加载的方式。
产品部分截图
首页
首页会展示所有应用的情报,在首页可以直观的发现各应用的异常数据。
大盘页面
如果想对某个应用细项的排查,会进入到应用的大盘页面;
主要会展示该应用,前端的重要性指标,近一个小时内的数据状况。
目前主要有页面性能、资源异常、JS 异常、API 接口成功率等重要指标作为衡量。
详情页
详情页,就可以看到该应用某项指标的数据细项。方便团队进行事后的追查、整改,提前预警和快速判定根因所用。
遇到的问题
SDK 采集到指标以后对数据进行上报时,会做一些过滤性的前置操作,如:
屏蔽掉一些黑名单。
指标的削峰填谷。
应用信息的转换。
客户端 IP 获取。
Token 的验证。前置处理有一个弊端,因为服务器会经过解析转换环节;当数据量达到每日 7000 万左右,上报的服务器就扛不住了。
所以我们把数据前置处理,变为数据落地后置处理;后置处理就是在数据清洗的过程中,在过滤掉黑名单以及异常指标。这样就减轻了上报服务器的压力。
并且仓库也会保留所有的原始数据,如果出现异常的时,也方便我们溯源,对数据进行恢复。
整体规划
我们分为了四期,目前还处于二期性能监控阶段。
其它
自研 APM 系统方便与内部进行的打通和整合;比如应用发布后就可以直接推送 SourceMap 文件;并且能实现线上发布以后自动进行页面性能的分析等工作。
如果目前发展阶段还不需要自建一个这样的系统,但业务需要这样的能力,也可以考虑第三方的一些产品。
商业产品分析
重要性指标对和阿里 ARMS 对比
易车·前端监控和阿里云 ARMS 做了一些重要性的指标对比,均值的浮动在上下在 5%-8% 左右;
本文经由作者同意授权发表 出自: https://www.cnblogs.com/wubaiqing/p/14430908.html
最后
最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,相关 文档资料 已上传至 React 中文社区交流群, 请关注公众号,点击菜单栏 入群学习,添加 入群小助手 后入群领取。陆续更新前端超硬核文章。
推荐阅读
(点击标题可跳转阅读)
[面试必问]-你不知道的 React Hooks 那些糟心事
[面试必问]-全网最全 React16.0-16.8 特性总结
[架构分享]- 微前端qiankun+docker+nginx自动化部署
[架构分享]-石墨文档 Websocket 百万长连接技术实践
觉得本文对你有帮助?请分享给更多人
关注「React中文社区」加星标,每天进步
点个赞👍🏻,顺便点个 在看 支持下我吧!
易车前端监控系统旨在解决内部问题,提供多场景实时监控,包括异常、性能、数据埋点和行为采集。系统通过SDK自动收集请求、性能、异常等指标,通过四阶段实现:指标采集、存储、分析和可视化。异常采集监听错误事件,性能采集利用performance.timing API,接口采集代理全局XHR。目前处于二期性能监控阶段,未来规划涵盖四期。系统面临数据量增长挑战,通过数据处理减轻上报服务器压力。

322

被折叠的 条评论
为什么被折叠?



