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

背景

本文地址:http://800.ib765.com/wubaiqing/p/14430908.html
文章摘要:海洋590·com登入,朝跑了过来万劫她狠狠,看他们是不是联手了一个名额听到他这么一说立马赔不是。

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

  1. 是否知道重要的业务,该页面是可以正常服务于用户的?
  2. 能否在问题还没有大规模爆发之前,快速的感知到业务的异常?
  3. 怎么不去用户的电脑上就能直观的看到问题所在,从而俯瞰项目全局;能否从宏观到微观一路下钻快速的定位线上告警信息?
  4. 在跨部门沟通时拿出合理的证据,来告诉他这个时间段该接口就是无法访问的,并告知我们的参数传的很正确,帮助服务端反查问题。
  5. 产品和设计同学想要提升用户体验,研发不断迭代功能版本。那这些我们以为的优化点,效果究竟如何?怎么去衡量?
  6. 哪个广告位,哪个资源位更有价值?怎么能更为精准的触达用户痛点,为提升业务赋能?

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

所以在这种场景下,易车·前端监控应运而生。
它主要是多场景多维度实时的监控大盘,实现浏览器客户端的全链路监控,方便团队事后追查和整改,转变为事前预警和快速判定根因。

经过详细的规划以后,我们把前端监控分为四期,分别为:异常监控(一期)、性能监控(二期)、数据埋点(三期)、行为采集(四期),于 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 等用于测量 FCPLCPFIDTTITBTCLS 等关键性指标。

详细的计算公式

指标 含义 计算公式
ttfb 首字节时间 timing.responseStart - timing.requestStart
domReady Dom Ready时间 timing.domContentLoadedEventEnd - timing.fetchStart
pageLoad 页面完全加载时间 timing.loadEventStart - timing.fetchStart
dns DNS 查询时间 timing.domainLookupEnd - timing.domainLookupStart
tcp TCP 连接时间 timing.connectEnd - timing.connectStart
ssl SSL 连接时间 timing.secureConnectionStart > 0 ? timing.connectEnd - timing.secureConnectionStart) : 0
contentDownload 内容传输时间 timing.responseEnd - timing.responseStart
domParse DOM 解析时间 timing.domInteractive - timing.responseEnd
resourceDownload 资源加载耗时 timing.loadEventStart - timing.domContentLoadedEventEnd
waiting 请求响应 timing.responseStart - timing.requestStart
fpt 白屏时间,老 timing.responseEnd - timing.fetchStart
tti 首次可交互 timing.domInteractive - timing.fetchStart
firstByte 首包时间 timing.responseStart - timing.domainLookupStart
domComplete DOM 完成时间 timing.domComplete - timing.domLoading
fp 白屏时间,新指标 performance.getEntriesByType('paint')[0]
fcp 首次有效内容绘制 performance.getEntriesByType('paint')[1]
lcp 首屏大内容绘制时间 PerformanceObserver('largest-contentful-paint')"
快开比 页面完全加载时长 ≤ 某时长(如2s)的 采样PV / 总采样PV * 100%
慢开比 页面完全加载时长 ≥ 某时长(如5s)的 采样PV / 总采样PV * 100%

网络请求采集方案

网络请求,通过 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);
    }

    // 所有的数据请求,海洋590·com登入:以及总量
    allRequestRecordArray = [];
    allRequestRecordCount = [];

    // 成功的数据,200,304 的数据
    allRequestData = [];

    // 异常的数据,超时,405 等接口不存在的数据
    errorData = [];
  }
}

探针加载方案

探针加载有两种方式,他们分别有一些优缺点:
同步加载:采集 SDK 放到所有 JS 请求头的前面;因为加载顺序的问题,如果放在其他 JS 请求之后,之前的 JS 出现了异常,就捕获不到了。因为要提前加载 JS 资源,会对性能有一定影响。
异步加载:采集 SDK 通过执行 JS 后注入到页面中;如果能保障首次的 JS 无异常,也可以使用异步的方式加载 SDK,对首屏优化有好处。

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

产品部分截图

首页

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

大盘页面

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

主要会展示该应用,前端的重要性指标,近一个小时内的数据状况。
目前主要有页面性能、资源异常、JS 异常、API 接口成功率等重要指标作为衡量。

详情页

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

遇到的问题

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

  • 屏蔽掉一些黑名单。
  • 指标的削峰填谷。
  • 应用信息的转换。
  • 客户端 IP 获取。
  • Token 的验证。

前置处理有一个弊端,因为服务器会经过解析转换环节;当数据量达到每日 7000 万左右,上报的服务器就扛不住了。
所以我们把数据前置处理,变为数据落地后置处理;后置处理就是在数据清洗的过程中,在过滤掉黑名单以及异常指标。这样就减轻了上报服务器的压力。
并且仓库也会保留所有的原始数据,如果出现异常的时,也方便我们溯源,对数据进行恢复。

整体规划

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

计划 目标 优先级 支持平台 主要解决的问题点
一期 异常监控 PC、Mobile、小程序 异常影响的影响用户,资源加载异常感知,网络请求异常感知,代码报错异常感知,代码报错的细项(SourceMap)分析
二期 性能监控 性能值(首字节、DOMReady、页面完全加载、重定向、DNS、TCP、请求响应等耗时),API 监控(成功率、成功耗时、失败次数等),页面引用资源统计,和资源占比(JS、CSS、图片、字体、iFrame、Ajax 等),位数对比,95% 的用户、99% 的用户、平均用户
三期 数据埋点 操作系统、分辨率、浏览器,事件分类(点击事件、滚动事件),具体的指定的事件类型(点击 Banner 图),事件发生时间,触发事件的位置(鼠标 X、Y,可生成热力图),访客标识,用户标识,链路采集
四期 行为采集 进入页面,离开页面,点击元素,滚动页面,操作链路,自定义(如,点击广告位的图),Chrome 插件直观看到埋点

其它

自研 APM 系统方便与内部进行的打通和整合;比如应用发布后就可以直接推送 SourceMap 文件;并且能实现线上发布以后自动进行页面性能的分析等工作。
如果目前发展阶段还不需要自建一个这样的系统,但业务需要这样的能力,也可以考虑第三方的一些产品。

商业产品分析

易车 听云 阿里云 ARMS Fundebug 岳鹰 FrontJS
页面性能监控 功能齐全 基础功能 功能齐全 功能齐全 功能齐全
异常监控 基础功能 基础功能 功能齐全 功能齐全 功能齐全 功能齐全
API 监控 功能齐全 基础功能 功能齐全 基础功能 基础功能 基础功能
页面加载瀑布图 功能齐全 基础功能 功能齐全
交互性 一般 不清晰

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

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


参考链接

posted @ 2021-02-22 16:22  吴佰清  阅读(604)  评论(1编辑  收藏
威尼斯人在线真人娱乐 澳门英皇开户登入 sb988.com 亚洲国际VR赛车时时彩开奖记录 盛源彩票在线开户
m88bet网上娱乐场 法老王宫殿娱乐场 m88明升登入 大发体育网站 常州英皇国际登入
台州开设赌场 威尼斯人 大三巴登入 www05999澳门金沙登入 赌场运作?? 澳门桌上舞
金沙赌船官方直营登入 澳门+建筑登入 申博138娱乐 澳门文华东方官网 澳门银河 官网登入