易车前端监控系统#研发解决方案

易车前端监控系统旨在解决内部问题,提供多场景实时监控,包括异常、性能、数据埋点和行为采集。系统通过SDK自动收集请求、性能、异常等指标,通过四阶段实现:指标采集、存储、分析和可视化。异常采集监听错误事件,性能采集利用performance.timing API,接口采集代理全局XHR。目前处于二期性能监控阶段,未来规划涵盖四期。系统面临数据量增长挑战,通过数据处理减轻上报服务器压力。

背景

自研工具是为了解决内部问题而生,希望通过这些问题引起大家的共鸣:

  • 是否知道重要的业务,该页面是可以正常服务于用户的?

  • 能否在问题还没有大规模爆发之前,快速的感知到业务的异常?

  • 怎么不去用户的电脑上就能直观的看到问题所在,从而俯瞰项目全局;能否从宏观到微观一路下钻快速的定位线上告警信息?

  • 在跨部门沟通时拿出合理的证据,来告诉他这个时间段该接口就是无法访问的,并告知我们的参数传的很正确,帮助服务端反查问题。

  • 产品和设计同学想要提升用户体验,研发不断迭代功能版本。那这些我们以为的优化点,效果究竟如何?怎么去衡量?

  • 哪个广告位,哪个资源位更有价值?怎么能更为精准的触达用户痛点,为提升业务赋能?

我们看到这些疑问,都需要数据指标的支撑。从解决这些问题的角度出发,把反复出现或无法跟其他部门交代的问题,打造成可以帮助我们解决问题的产品。

所以在这种场景下,易车·前端监控应运而生。

它主要是多场景多维度实时的监控大盘,实现浏览器客户端的全链路监控,方便团队事后追查和整改,转变为事前预警和快速判定根因。

经过详细的规划以后,我们把前端监控分为四期,分别为:异常监控(一期)、性能监控(二期)、数据埋点(三期)、行为采集(四期),于 2020 年 6 月 23 号正式启动研发,目前处于二期阶段。

关键结构

为实现上述需求,监控系统主要分为四个阶段来实现;分别是:指标采集、指标存储、统计与分析、可视化展示。77ac30a283ca7a6ba28f9eb334cc5f57.png

  • 指标采集阶段:通过前端集成的 SDK 收集请求、性能、异常等指标信息;在客户端简单的处理一次,然后上报到服务器。

  • 指标存储阶段:用于接收前端上报的采集信息,主要目的是数据落地。

  • 统计与分析阶段:自动分析,通过数据的统计,让程序发现问题从而触发报警。人工分析,是通过可视化的数据面板,让使用者看到具体的日志数据,从而发现异常问题根源。

  • 可视化展示阶段:通过可视化的平台;在这些指标(API 监控、异常监控、资源监控、性能监控)中,追查用户行为来定位各项问题。

整体架构图

随着统计需求的增加以及前端应用的上线,数据量由早期的每天 100 多万条数据;到现在的每天约 7000 万条数据。架构上也经历了三次版本的迭代。这是最新版的架构图,主要经过 6 层处理。0f53da9290aa93209b7ea430a498e4b6.png

  • 采集层:PC 和 H5 使用了一套 SDK 监听事件采集指标,然后将监听到的指标通过 REST 接口往 Logback 推送数据。Logback 以长连接的方式,会把这些不同类型的指标数据推送到 Flume 集群当中。Flume 集群会将这些数据,分发到 Kafka Topic 进行存储。

  • 处理层:由 Flink 去实时消费;Flink 会消费三种类型,分别是:离线数据落地、实时 ETL+图谱、明细日志。

  • 存储层:离线数据会存储到 HDFS 中;实时 ETL+图谱数据会存储到 MySQL 中;明细数据会落入到 ES 中。

  • 统计层:离线(DW、DM)、实时(分钟级->十分钟级->小时级)的方式,对指标进行汇总和统计。

  • 应用层:最后由接口去汇总表和明细 ES 里查询数据。

  • 展示层:然后前端输出图表、报表、明细、链路等信息。

技术方案

数据采集

采集最初的愿景是希望对业务无侵入性,业务系统无需改造,只需要嵌入一段代码即可。所以这些采集,都是 SDK 自动化的处理。SDK 会全局监听几个事件,分别为:错误监听、资源异常的监听、页面性能的监听、API 调用的监听。9106934590b1fec61f9668ee72ca9e5a.png通过这几项监听,最终汇总为 3 项指标的采集。

  • 异常采集:调用 error/unhandledrejection 事件,用于捕获 JS、图片、CSS 等资源异常信息。**

  • 性能采集:调用浏览器原生的 performance.timing API 捕获页面的性能指标。

  • 接口采集:通过 Object.definePropety 代理全局的 XHR 用于捕获浏览器的 XHR/FETCH 的请求。

采集端 SDK 架构

65adf64c4bb36611b379eef8ea6d74e5.pngSDK 主要分为两部分:

  • 第一部分: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;
}

性能采集方案

浏览器页面加载过程

a26ab322d0e951d34186c8d842b7d7d1.png

性能指标获取方式

我们借助于浏览器原生的 Navigation Timing API 能够获取到上述页面加载过程中的各项性能指标数据,用于性能分析,它的时间单位是纳秒级。e43aa2527e67f048417d12231266425c.png当然也借助于 PerformanceObserver API 等用于测量 FCP、LCP、FID、TTI、TBT、CLS 等关键性指标。

详细的计算公式

b40633b1d2ccb13036a3ac42997a7080.png

网络请求采集方案

网络请求,通过 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,对首屏优化有好处。

目前我们采用的是第一种同步加载的方式。

产品部分截图

首页

首页会展示所有应用的情报,在首页可以直观的发现各应用的异常数据。

0526a262510baea7c7377047af155dd3.png

大盘页面

如果想对某个应用细项的排查,会进入到应用的大盘页面;

主要会展示该应用,前端的重要性指标,近一个小时内的数据状况。

目前主要有页面性能、资源异常、JS 异常、API 接口成功率等重要指标作为衡量。

3ead6e0c74057acc2a7951cbed679563.png

详情页

详情页,就可以看到该应用某项指标的数据细项。方便团队进行事后的追查、整改,提前预警和快速判定根因所用。

c92233be890a4696428e51b1f337dd95.png

遇到的问题

SDK 采集到指标以后对数据进行上报时,会做一些过滤性的前置操作,如:

  • 屏蔽掉一些黑名单。

  • 指标的削峰填谷。

  • 应用信息的转换。

  • 客户端 IP 获取。

  • Token 的验证。前置处理有一个弊端,因为服务器会经过解析转换环节;当数据量达到每日 7000 万左右,上报的服务器就扛不住了。

所以我们把数据前置处理,变为数据落地后置处理;后置处理就是在数据清洗的过程中,在过滤掉黑名单以及异常指标。这样就减轻了上报服务器的压力。

并且仓库也会保留所有的原始数据,如果出现异常的时,也方便我们溯源,对数据进行恢复。

整体规划

我们分为了四期,目前还处于二期性能监控阶段。

392a1f3920dad953bdcfeebd5d603365.png

其它

自研 APM 系统方便与内部进行的打通和整合;比如应用发布后就可以直接推送 SourceMap 文件;并且能实现线上发布以后自动进行页面性能的分析等工作。

如果目前发展阶段还不需要自建一个这样的系统,但业务需要这样的能力,也可以考虑第三方的一些产品。

商业产品分析

5a42573b7a1395fc173c4657da99032f.png

重要性指标对和阿里 ARMS 对比

易车·前端监控和阿里云 ARMS 做了一些重要性的指标对比,均值的浮动在上下在 5%-8% 左右;

本文经由作者同意授权发表 出自: https://www.cnblogs.com/wubaiqing/p/14430908.html

68c6fd9297403c3780e8b7742e7c720a.png

最后

最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,相关 文档资料 已上传至 React 中文社区交流群, 请关注公众号,点击菜单栏 入群学习,添加 入群小助手 后入群领取。陆续更新前端超硬核文章。

推荐阅读

(点击标题可跳转阅读)

[极客前沿]-你不知道的 React 18 新特性

[极客前沿]-写给前端的 K8s 上手指南

[极客前沿]-写给前端的Docker上手指南

[面试必问]-你不知道的 React Hooks 那些糟心事

[面试必问]-一文彻底搞懂 React 调度机制原理

[面试必问]-一文彻底搞懂 React 合成事件原理

[面试必问]-全网最简单的React Hooks源码解析

[面试必问]-一文掌握 Webpack 编译流程

[面试必问]-一文深度剖析 Axios 源码

[面试必问]-一文掌握JavaScript函数式编程重点

[面试必问]-阿里,网易,滴滴,头条等20家面试真题

[面试必问]-全网最全 React16.0-16.8 特性总结

[架构分享]- 微前端qiankun+docker+nginx自动化部署

[架构分享]-石墨文档 Websocket 百万长连接技术实践

[自我提升]-Javascript条件逻辑设计重构

[自我提升]-页面可视化工具的前世今生

[大前端之路]-连前端都看得懂的《Nginx 入门指南》

[软实力提升]-金三银四,如何写一份面试官心中的简历

觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

0101190d72a5bfb6ea3b0a777748c679.png   点个赞👍🏻,顺便点个 在看 支持下我吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值