背景
自研工具是为了解决内部问题而生,希望通过这些问题引起大家的共鸣:
-
是否知道重要的业务,该页面是可以正常服务于用户的?
-
能否在问题还没有大规模爆发之前,快速的感知到业务的异常?
-
怎么不去用户的电脑上就能直观的看到问题所在,从而俯瞰项目全局;能否从宏观到微观一路下钻快速的定位线上告警信息?
-
在跨部门沟通时拿出合理的证据,来告诉他这个时间段该接口就是无法访问的,并告知我们的参数传的很正确,帮助服务端反查问题。
-
产品和设计同学想要提升用户体验,研发不断迭代功能版本。那这些我们以为的优化点,效果究竟如何?怎么去衡量?
-
哪个广告位,哪个资源位更有价值?怎么能更为精准的触达用户痛点,为提升业务赋能?
我们看到这些疑问,都需要数据指标的支撑。从解决这些问题的角度出发,把反复出现或无法跟其他部门交代的问题,打造成可以帮助我们解决问题的产品。
所以在这种场景下,易车·前端监控应运而生。
它主要是多场景多维度实时的监控大盘,实现浏览器客户端的全链路监控,方便团队事后追查和整改,转变为事前预警和快速判定根因。
经过详细的规划以后,我们把前端监控分为四期,分别为:异常监控(一期)、性能监控(二期)、数据埋点(三期)、行为采集(四期),于 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% 左右;
结语
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下作者。
如果你想和获得React交流的机会,和数万React 开发者一起交流学习**,获得100本精选电子书籍PDF 点击这里** ---> React沸点
如果你是有其他目的的,别加我,我不想跟你交朋友,我只想简简单单学习前端,不想搞一些有的没的!!!。