【大前端攻城狮之路】百度爱番番前端性能监控体系方案设计

一、背景

爱番番大前端整体面临以下问题:

  1. Metrics:URL的RED指标不全。URL不全,ERROR缺失,Duration分位置缺失。整体实效性为T+1。无法及时感知问题。只对基本页面级别的读操作进行了监控。
  2. Tracing:Trace无法全端串联,直接影响具体case的跟进。无前端Trace。
  3. Logging:无Log。Sentry的Error信息目前仅PC接入。且未进行实际使用;和trace无法打通。
  4. 报警:对于异常数据缺乏有效报警。

二、目标

  1. Metrics目标:RED指标,自定义Metrics
  2. Tracing目标:全端单动作追踪
  3. Logging目标:Error级别Log具备。单Traceid和Log能够实现串联

2.1 核心目标

从全局问题出发,能够洞察统计性的页面url真实RED指标,以及可以做操作流任意阶段之间的统计性耗时分析。从个案问题出发,能够基于用户id进行任意一次全端调用链追踪。
具体地:

  1. 页面级别性能监控。可包含条件检索和正常页面刷新。包含RED
  2. 能够基于用户id进行任意一次全端调用链追踪。
  3. 可分析一个操作流任意阶段之间的统计性耗时。
  4. 准实时呈现。数据延时小于5min。
  5. 可区分来源、地域、设备等核心信息
  6. 写请求性能监控。包含新建、编辑、删除。包含RED

2.2 目标抽象

本质上,抽象为AggrEvent、Event、Trace、Span 四个概念。Transaction用来做纯埋点,此处暂不需要。

  1. 带TraceID(可被覆盖)和TimeStamp信息的Event
  2. 全端Trace下的调用链。本质上是前端的Event链路 + 服务端的Span链路串联而成。
  3. 考虑到批量传输性能。采用AggrEvent进行聚合发送。

三、 名词解释

  1. RED: RED方法是Weave Cloud在基于Google的"4个黄金指标"的原则下结合Prometheus以及Kubernetes容器实践,细化和总结的方法论,特别适合于云原生应用以及微服务架构应用的监控和度量。主要关注以下三种关键指标:
    1. (请求)速率:服务每秒接收的请求数。
    2. (请求)错误:每秒失败的请求数。
    3. (请求)耗时:每个请求的耗时。
  2. optid(operate-id):一个操作唯一对应的一个id。比如一次刷新,一次修改。
  3. reqid(request-id):单次请求唯一对应的一个id。如果重试,则reqid设置为新id。一个optid可能会存在多个reqid。
  4. tid(tracing-id):贯穿服务端端调用链的唯一id。
  5. optType: 某一类操作定义。比如一次刷新,一次修改。

四、目标拆解

4.1 Why

4.1.1 为什么要做大前端监控体系?

获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产研优化的方向。

4.1.2 收益是什么?

  1. Metrics:对各端各页面各场景RED指标准实时分析、可视化呈现,打破前端监控黑盒
  2. Tracing:打通前后端调用链,能够针对任意case进行前后端链路分析,让前端性能优化有的放矢
  3. Logging:对各端运行时错误进行实时监控报警,能通过日志最大程度还原用户现场并定位问题,提升前端页面稳定性

4.2 What

4.2.1 监控什么端?

爱番番现有产品各端:web,h5(浏览器、webview),Android,iOS,客户端,小程序

4.2.2 监控什么方向

(1) 数据监控

数据监控,顾名思义就是监听用户的行为。常见的数据监控包括:

  • PV/UV:PV(page view),即页面浏览量或点击量。UV:指访问某个站点或点击某条新闻的不同IP地址的人数
  • 用户在每一个页面的停留时间
  • 用户通过什么入口来访问该网页
  • 用户在相应的页面中触发的行为

统计这些数据是有意义的,比如我们知道了用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送等等。
该方向主要为业务服务,目前职责定位更符合神策埋点系统。

(2) 性能监控

性能监控指的是监听前端的性能,主要包括监听网页或者说产品在用户端的体验。常见的性能监控数据包括:

  • 不同用户,不同机型和不同系统下的首屏加载时间
  • 白屏时间
  • http等请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 页面交互动画完成时间

这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,比如兼容低版本浏览器的动画效果,加快首屏加载等等。

(3) 异常监控

产品的前端代码在执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,可以避免线上故障的发上。虽然大部分异常可以通过try catch的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获;
爱番番目前采用sentry进行错误日志采集。

4.2.3 监控什么内容

1.web

|----------|-------------------------------------------------------------------------------------------------------------------------------------|
| 类别 | 内容 |
| 页面分析 | 具体丰富的页面指标,提供如服务器端响应时间、网络延时、DOM解析和页面渲染时间等性能指标 帮助研发更快捷的定位服务端、客户端的页面问题 |
| Ajax请求 | 获取用户访问过程,页面发出的所有Ajax请求URL、引用页面URL,监控某一Ajax请求的响应时间、回调时间、上传数据量、下载数据量以及响应过程中服务器返回的错误 |
| JS error | 代码级定位出错页面或者脚本URL,引用页面URL,出错的行列信息、堆栈等信息,通过sourceMap定位源码文件,通过pageId,operateId以及tid最大限度还原上下文 监测Web App中JS错误的数量,各浏览器出错百分比和JS错误率随时间变化的趋势 |
| 浏览器监测 | 统计不同版本浏览器和浏览器类型的平均页面加载时间和吞吐率 提供基于多平台浏览器性能分析,兼容性分析 |
| 慢页面追踪 | 抓取加载时间超过设定阈值的页面上的元素信息,及每个元素的TCP建连、首包及剩余包等所需时间 详细定位页面上的哪些元素的加载拖慢了页面的响应,为优化用户体验提供依据 |

2.其他端:
h5(浏览器、webview)
统计从端点击开始,到指定页面渲染全流程时间分布,包括(容器耗时,框架耗时,网络耗时,渲染耗时等)。
统计NPJS框架自身稳定性,以及各个阶段耗时。
统计任意多个操作各个时间断耗时。
iOS,Android
现在使用百度移动端性能中台,功能基本满足爱番番监控、报警功能;
移动端监控指标主要为 卡顿和崩溃,目前有崩溃堆栈信息日志;暂无交互时长分析(xray平台有,手百性能中台没有)
http://performance.baidu.com/
PC客户端
electron监控方案参考WEB版

四、方案调研

自建or接入其他平台?

业务埋点采买神策。
性能监控平台:
厂内:
性能中台 http://performance.baidu.com/
日志中台 http://app.baidu-int.com/
目前日志中台可以接入日志,但是需求是前端页面的性能指标,属于性能平台范围,目前性能平台无前端性能指标建设。
性能中台的定位主要是针对native,当前通用能力主要是崩溃、卡顿、端异常、Flutter异常、日志回捞等,其他的一些能力主要在手百、或者相关SDK上,还未对外输出。经沟通暂不考虑支持前端性能监控。
厂外:性能监控主流收费平台:

  1. ONEAPM https://www.oneapm.com/solutions/qualityacceptance.html
  2. 听云 https://www.tingyun.com/
  3. 性能魔方 http://www.mmtrix.com/imonitor
  4. 监控宝 https://www.jiankongbao.com/

收费平台功能大同小异,均能满足前端基础性能监控需求。
优点:有较为成熟的解决方案,能快速满足多端性能监控基本需求;
缺点:

  1. 收费
  2. 无法与部门现有后端APM体系打通
  3. 无法满足特定case分析需求

结论:复用神策埋点SDK存储,上报能力及通路,进行SDK二次封装;自建日志服务及展示系统。

五、方案设计 (web)

1、采集

(1)埋点SDK
基于神策SDK进行二次封装
统一封装埋点SDK,通过npm包形式进行版本管理;使用方在公共模块对埋点SDK进行初始化。
增加无侵入性能采集能力,提供采样率配置等可配置扩展能力。
参考 接口文档
(2)通用统计指标(参考听云)

|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 指标 | 统计方式 |
| #### 白屏 | 用户浏览器输入网址后至浏览器出现文字或1px图片所花费时间。计算规则:优先使用Chrome、IE提供的firstPaintTime,没有获取到计算head中link、script脚本下载的最长时间。 |
| #### 首屏 | 用户浏览器首屏内所有的元素呈现所花费时间。计算规则:寻找首屏区域内的所有图片,计算最长加载时间得到首屏时间。 |
| #### 可交互 | 功能可以使用的时间,也指domready时间。计算公式:可交互=Navigation Timing API domContentLoadedEventStart -- fetchStart。 |
| #### 完全加载 | 页面完全加载总时间。指从NavigationStart事件开始到LoadEventEnd事件结束,计算公式:LoadEventEnd-NavigationStart |
| #### HTML加载 | 指主HTML文件从DNS解析到加载完且不包含排队时间和应用服务器响应时间,即包含DNS,TCP建连,Request和Response,计算公式:responseEnd-domainlookupStart-排队时间-应用服务器响应时间 |
| #### 页面渲染 | 指从responseEnd事件开始到loadEventEnd结束,包含DOM解析和资源加载,计算公式:LoadEventEnd-responseEnd |
| #### DOM解析 | 指从responseEnd事件开始到DomContentLoadedEventEnd事件结束,计算公式:DomContentLoadedEventEnd-responseEnd |
| #### 资源加载 | 指从DomContentLoadedEventEnd事件开始到loadEventEnd事件结束,计算公式:loadEventEnd-DomContentLoadedEventEnd |
| #### JS错误率 | 出现JS错误的比例。JS错误包含Javascript错误代码和位置信息。 |
| #### 服务端响应时间 | 服务器响应时间是指应用服务器处理请求所消耗的时间,即应用响应时间,等于请求到达应用服务器到应用代码执行完成并输出响应信息的时间。(需要通过Server探针自动注入方式嵌码,否则服务器响应时间为零) |
| #### AJAX请求响应时间 | 所有Ajax请求时间在时间轴的投影合并的总耗时 |
| #### unload | 卸载当前页面的耗时,计算公式:unloadEnd-unloadStart |
| #### Redirect | 页面重定向操作所消耗的时间,计算公式:redirectEnd-redirectStart |
| #### Cache | 取缓存数据的耗时,计算公式:domainLookupStart-fetchStart |
| #### DNS | 通过域名解析服务(DNS),将指定的域名解析成IP地址的消耗时间。 |
| #### TCP建连时间 | 浏览器和WEB服务器建立TCP/IP连接的消耗时间。当元素下载完成后,浏览器可能会根据服务器返回的结果保持此连接,而不是完全关闭此连接。当监测节点再次和相同的服务器建立连接时,会复用此连接,对应消耗时间可能为0。此指标即为TCP/IP连接三次握手的前二次握手的时间(从IE发送TCP包SYN到收到服务器返回的TCP包SYN ACK的时间),第三次握手时间(从IE发送TCP包ACK到服务器接收此TCP包的时间)不计算在内。 |
| #### 排队时间 | 排队时间指服务器端的请求阻塞时间,即请求从Web前端服务器(例如Apache, nginx或F5负载均衡设备)到达应用服务端的时间。 |
| #### 首包时间 | 从开始页面请求到浏览器开始接收到HTML代码的时间,不包括排队时间和服务器端的时间,计算公式:responseStart-connectEnd -排队时间-服务器响应时间 |
| #### 剩余包时间 | 从responseStart事件开始到responseEnd事件结束,计算公式:responseEnd-responseStart |
| #### 首次渲染时间 | 从导航到页面首次渲染消耗的时间,计算公式:firstPaintTime-navigationStart(又名:白屏时间,firstPaintTime) |
| #### 首次交互时间 | 从用户的第一个动作发生时间 -- navigationStart,其中动作包括:点击,按键,滚动鼠标。 |
| #### 自定义加载时间 (用户可感知时间) | 每个页面都可以设置一个用户自定义的加载时间性能指标。计算方式为路由切换时间至主动调用sdk ready方法时间差值 |
| #### AJAX平响时间 | 平均每次AJAX请求的响应时间 |
| #### AJAX传输数据量 | 单位KB,平均每次AJAX请求的数据传输量(上传+下载字节数) |
| #### AJAX回调时间 | 平均每次AJAX请求的回调时间(回调时间是指当数据从服务器传到客户端之后,本地代码调用这些数据做相应的处理,可以理解为本地执行时间) |
| #### 客户端时间 | 从请求某资源到下载完过程中,没有出现网络传输的时间片段之和,比如DNS-TCP建连,之间的切换需要消耗CPU来调度,这就可能会产生很短的时间空隙 |
| #### 事件平均响应时间 | 操作请求完成时间。 |

统计方式,通过performance API
https://www.w3.org/TR/navigation-timing/

Core Web Vitals(https://web.dev/vitals/

核心指标

|---------|---------------------------------------------------------|---------------------------------------------------------------------------|
| metrics | 描述 | 含义 |
| TTFB | time to first byte | 从请求到数据返回第一个字节所消耗时间 |
| TTI | Time to Interactive (TTI) | DOM树构建完毕,可绑定事件 |
| DCL | DOMContentLoaded | HTML文档完全加载解析完成 |
| L | onLoad | 依赖资源全部加载完毕 |
| FP | first paint | 第一个像素点绘制完成时间 |
| FCP | First contentful paint (FCP) | 首次绘制非空白节点时间 |
| FMP | first meaningful paint | 首次有意义绘制(需要自定义) |
| LCP | lLargest contentful paint (LCP) | 在视口中最大的页面元素加载时间 |
| FID | First input delay (FID) | 用户首次和页面交互到页面响应的时间 https://github.com/GoogleChromeLabs/first-input-delay |
| CLS | Cumulative layout shift (CLS) | 度量在页面开始加载到其生命周期状态更改为隐藏之间发生的所有意外布局更改的累积分数 |

使用谷歌Web Vitals进行获取
https://github.com/GoogleChrome/web-vitals

复制代码
interface Metric {
  // The name of the metric (in acronym form).
  name: 'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB';

  // The current value of the metric.
  value: number;

  // The delta between the current value and the last-reported value.
  // On the first report, `delta` and `value` will always be the same.
  delta: number;

  // A unique ID representing this particular metric that's specific to the
  // current page. This ID can be used by an analytics tool to dedupe
  // multiple values sent for the same metric, or to group multiple deltas
  // together and calculate a total.
  id: string;

  // Any performance entries used in the metric value calculation.
  // Note, entries will be added to the array as the value changes.
  entries: (PerformanceEntry | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[];
}

(3)前后端调用打通

  1. pageGuid,重新定义pageGuid生成规则,暂定为 router+orgId+timestamp+random ? 进行加密处理
  2. requestId,由网络库统一生成,生成方式参考BFE_logId生成规则,在请求header中携带,用于skywalking tid进行映射
  3. operateId,需要使用者明确【操作】起止时机,并手动调用SKD API。
    1) 在使用方初始化【操作】时 ,调用 SDK.initOpt('操作名 '),该方法返回本次操作operate Name**,用于后续传递;**
    1) 在使用方明确【操作】开始时,调用SDK.startOpt('operateName'),SDK生成唯一operateId,存于localStorage.pageGuid..operateName.operateId(示例);
    2) 为了保证上下文独立性,在该操作涉及到的请求options中配置{operateName: 'operateName'},网络库在发送请求时,主动获取operateId并携带,取值方式为:localStorage.pageGuid..operateName.operateId || '';
    3) 在使用方明确【操作】结束时,需调用SDK.endOperate(操作名);SDK将进行该次操作相关操作时间,页面渲染时长等统计,存储在localStorage.pageGuid.bucket中,通过上报策略时机进行统一上报。

(4)错误日志报警
现有sentry在采集错误日志时,增加pageGuid,requestId,operateId相关信息,通过(3)映射方式与Skywalking 调用链进行链路打通。
鉴别有用报错信息分类,确定报警阈值及报警人配置,进行HI,邮件,短信,电话报警方式;增加报警升级策略。
增强报表展示能力,通过pageGuid等维度查看页面级别报错,以便于进行下一步排查和跟进。

2、上报

直接复用神策SDK上报机制。
1.上报时机(备选)
页面性能数据(数据量较少)

  • 页面加载和重新刷新
  • 页面切换路由

前后端trace数据(数据量较大)

  • 存储于 localStorage.pageGuid.bucket中;
  • bucket中数量超过阈值,触发上报;
  • 页面卸载unload,触发上报;

2.上报方式
  如何上报性能数据,我们第一反应就是通过ajax请求的形式来上报前端性能数据。这种方法有一些缺陷,比如必须对跨域做特殊处理以及如果页面销毁后,相应的ajax方法并不一定发送成功等问题。
  其中跨域的问题比较好处理,最难解决的问题是 如果页面销毁,那么对应的ajax方法并不一定能成功发送。
   根据google analytics(GA)中的方法,根据浏览器的兼容性以及url的长度,来采用不同的方法上报性能数据:
**  通过动态创建img标签的方式,在img.src中拼接url的方式发送请求,不存在跨域限制。如果url太长,则才用sendBeacon的方式发送请求,如果sendBeacon方法不兼容,则发送ajax post同步请求。**

(1)、sendBeacon方法

解决在文档卸载或者页面关闭后无法完成异步ajax请求的问题,很多情况下我们会把异步变成同步。在页面卸载的unload或者beforeunload事件中执行同步方法调用。
但是同步方法调用存在一个问题,就是会推迟A页面切换进入B页面的时间。而sendBeacon方法解决了该问题,简单来说:
sendBeacon方法在页面销毁期,可以异步的发送数据,因此不会造成类似同步ajax请求那样的阻塞问题,也不会影响下一个页面的渲染
sendBeacon的调用方式为:

复制代码
function sendBeacon(url,data){
  //判断支不支持navigator.sendBeacon
  let headers = {
    type: 'application/x-www-form-urlencoded'
  };
  let blob = new Blob([JSON.stringify(data)], headers);
  navigator.sendBeacon(url,blob);
}

(2)动态创建img标签的形式

通过动态创建img标签的形式,指定src属性所指定的url来发送请求,首先不受跨域的限制,其次img标签动态插入,会延迟页面的卸载保证图片的插入,因此可以保证在页面的销毁期,请求可以发生。

复制代码
function imgReport(url, data) {
   if (!url || !data) {
       return;
   }
   let image = document.createElement('img');
   let items = [];
   items = JSON.Parse(data);
   let name = 'img_' + (+new Date());
   image.onload = image.onerror = function () {
      
   };
   let newUrl = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');

   image.src = newUrl;
}

(3)同步ajax post请求

动态创建img标签的方法,拼接url的时候存在一定的问题,因为浏览器对url的长度是有限制的。而sendBeacon方法兼容性不是很好,最后兜底的处理方式就是发送同步的ajax请求,同步的ajax请求前面说过,会在页面销毁期之前执行,虽然会有一定程度的阻塞下一个页面的渲染。

复制代码
function xmlLoadData(url,data) {
  var client = new XMLHttpRequest();
  client.open("POST", url,false);
  client.setRequestHeader("Content-Type", "application/json; charset=utf-8");
  client.send(JSON.stringify(data));
}

(4)综合解决方案

首先拼接携带参数的完整的url,判断url的长度,如果url的长度小于浏览器允许的最大长度内,那么通过动态创建img标签的形式来发送前端性能数据,如果url太长,则判断浏览器是否支持sendBeacon方法,如果支持,则通过sendBeacon方法来发送请求,否则发送同步的ajax请求。

复制代码
function dealWithUrl(url,appId){
      let times = performanceInfo(appId);
      let items = decoupling(times);
      let urlLength = (url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&')).length;
      if(urlLength<2083){
        imgReport(url,times);
      }else if(navigator.sendBeacon){
        sendBeacon(url,times);
      }else{
        xmlLoadData(url,times);
      }
 }

3、方案设计(Hybrid H5)

3.1总体架构

3.2.上报流程

3.3.流程时序图

4.服务端采集及分析

搭建OAP服务,进行数据清洗加工及展示