自己动手打造前端性能监控系统

  • 作者:ericzzhang
  • 时间:2018-08-19
  • 951人已阅读
导语:了解前端性能监控的方式以及如何打造监控系统

自己动手打造前端性能监控系统

背景

为什么要做监控页面性能?

一个页面性能差的话会影响到很多方面。在公司层面,页面性能会影响公司收益,如用户打开页面等待的太久,用户可能会直接关掉页面,或者下次不再打开了,特别是在移动端用户对页面响应延迟容忍度很低。

除此之外,页面的加载速度还将直接影响页面的SEO,网页加载速度太慢,用户会直接关掉,这直接增加页面的跳出率,当搜索引擎发现页面跳出率高,搜索引擎会认为该网站对用户的价值不高,从而降低排名。2018年7月谷歌公司新规定,页面访问时间比较长,谷歌公式将会降低该页面的搜索排名。

虽然性能很重要,但在开发迭代中,开发会有所忽略,性能会随着版本迭代而有所衰减,所以我们需要一个性能监控系统,持续监控,评估,预警页面性能的状况,发现瓶颈,从而指导优化工作。

页面性能的评估与监控有很多成熟优秀的工具 ,比如gtmetrix 网站,他可以同时查多个分析工具的的结果,会提供许多的建议。

但这种方式与真实情况偏离,无法反馈某个地区的整体速度,慢速用户多少,无法反映性能的波动情况,另外除了白屏之类的,我们还有一些功能性的测速,比如页面可点击时间,广告展示的时间等等,这些都是无法模拟监控的。

为了持续监控不同网络环境下用户访问情况与页面各功能可用情况,我们选择在页面上植入js来监控线上真实用户数据。具体做法就是用一段代码将用户的数据上报到我们的服务器,通过一个系统将数据汇总,处理,最后图形化数据,方便我们查看各个页面等性能。

测速系统的设计

测试系统分三个部分,如下

  • 前端上报
    • 如何记录测速时间点。
    • 如何上报。
    • 数据的采样。
  • 数据处理,入库。
  • 数据展示

###前端上报

在前端植入一段前端js代码,通过这些代码来上报页面性能数据,那一般哪些指标能够更好的反馈用户的体验呢?

用户最大感受就是,页面为什么打开要等那么久,为什么图片加载那么慢,页面加载半天也不能点击。这些用户的感受对于程序员来说就是重要的页面性能指标。根据用户上述痛点抽象出指标,白屏时间,首屏时间,可交互时间。那么这个时间我们是如何统计的捏?

确定统计起始点

起始点时间,应该是我们输入网址后,点回车作为起始点,这样才是用户真正开始等待的时间。如果是高端的浏览器,我们可以直接使用Navigation Timeing接口来获取统计起点。

Navigation Timeing接口是一个在web中精准测量性能的javascript API,这个接口提供了一系列详细的时间状态。

在Chrome中打开控制台,在命令行中输入performance,点开并查看它的timing属性,你会看到如下代码

1.png

每一个performance.timing属性都表示一个页面事件(例如页面发送了请求)或者页面加载(例如当DOM开始加载),测量以毫秒的形式从1970年1月1日的午夜开始。结果为0表示该事件未发生(例如redirectEnd或者redirectStart等)

这里有一张从 Navigation Timing draft 弄来的 performance.timing 事件的顺序图。

2.png

其中navigationStart表示当浏览器请求的时间点,通俗地就是你在url输入栏里按下回车间的时间点,或则页面按F5刷新的时间点。

其他时间点的详细解释请点击 https://www.w3.org/TR/navigation-timing/,或则google一下,有多篇文章做了解释,这里就不再累述。

此接口大部分浏览器都已经支持,除了pc端ie9以下的浏览器。

白屏时间

用户看到页面展示出现一个元素的时间。很多人认为白屏时间是页面返回的首字节时间,但这样其实并不精确,因为头部资源还没加载完毕,页面也是白屏。

真正白屏结束的时间分为三种。

第一种没有靠js渲染的普通页面,白屏时间应该是在头部外链资源加载完,因为浏览器只要加载头部资源才会真正的渲染页面。所以白屏时间点最好是打印在头部末尾的位置(这里可能也不精确,但尽量保证接近),如代码所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <!--头部资源-->
    <link href="style.css">
    <title>Document</title>
    <scirot>
        // 白屏幕结束时间
        var time = +new Date() - performance.timing.navigationStart;
    </scirot>
</head>
<body>
</body>
</html>

第二种,是使用了一些前端框架,比如vue,reacjs,他们需要执行完js后,才会将内容渲染到页面上,或者异步拉取数据,在数据拉回来在显示页面。这种情况下我们一般会在页面价格loading态。那么白屏结束时间在这个loading加载的后面。

首屏时间

首屏时间是指页面第一屏所有资源完整展示的时间。这个时间每个页面都不一致。比如一个页面的首屏是4张图片,那么我们应该在四张图片加载完成后才算首屏时间,或则页面是异步拉取数据的,首屏时间应该是将数据插入到浏览器的时间。总之找到首屏资源最后加载完成的时间点就是首屏时间。

上报方式

测量好时间后,就需要将数据发送给服务端。测速数据对丢失率要求比较低,且测速应该尽量在不影响主流程的逻辑和页面的性能前提下进行。使用的img标签get请求来上报数据,主要有以下原因。

  • 不存在ajax跨域问题,可做不同源的请求
  • 很古老的标签,没有浏览器兼容性问题
 var i = new Image();
 i.onload = i.onerror = i.onabort = function () {
 	i = i.onload = i.onerror = i.onabort = null;
 }
 i.src = url;
            

一些高级浏览器还支持 navigator.sendBeacon方法。这个方法可以用来发送一些小量数据,该方法是异步的,且在浏览器关闭请求也照样能发,特别适合上报统计的场景。

navigator.sendBeacon(url, data ? $.param(data) : null)

最终方案:当浏览器支持sendBeacon方法,优先使用该方法,不支持时使用img的方式上报。

采样

测速上报数据是海量的,由于数据太大,入库处理时间也会增加,且服务器性能有限,为了避免资源的浪费,在上报过程中进行数据采样处理。采样的粒度由用户端自己控制,如果采样为1/10, 也那么上报数据要加上rate=10, rate为采样率。

数据收集和入库

我们在一台机器上起了一个nginx服务器,nginx服务器可以记录访问,将用户的访问记录写进日志,这个日志可以记录请求的所有信息请求头,例如请求参数,请求ip。日志可以按照自己设定的格式来生成日志。

当页面的测速发送请求过来,nginx记录这个请求,将该请求写进日志中。

我们并没有用到nginx的logrotater(日志定时轮询)。由于logrotater最小颗粒度是1天,但我们希望日志是按5分钟一个文件来存储(原因是文件可以分批处理,避免一次性处理文件太大,且我们查询的测速点一天的走势的测速点颗粒度也是5分钟)。

nginx配置如下:

if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{1})[0-4]")
{
    set $logname $1-$2-$3-$4-$50;
}
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{1})[5-9]")
{
    set $logname $1-$2-$3-$4-$55;
}

生成该日志:
access_log logs/stat.y.qq.com.sp.access.$logname.log spdata;

直接在日志文件生成的时候,就分割好。但这样有个缺点不能使用logrotate定时删除日志文件的功能,要额外写一个定时删除日志的脚本,避免文件过度沉淀,浪费磁盘资源。

log存的数据格式为:

log_format  spdata  '$time_local ~|^ $http_x_forwarded_for ~|^ $request ~|^ $http_referer ~|^ $status ~|^ $http_user_agent ~|^ $cookie_ptisp ~|^ $cookie_uin';

其中 ~|^ 为分隔符

统计的字段有

  • 时间,按5分钟
  • IP,用户的ip地址
  • data,页面上报的数据
    • appid 产品id产品id (比如QQ音乐, 全民K歌)
    • pid 项目id (比如PC客户端, YQQ, QQ音乐手机客户端, 其他H5)
    • pageid 页面id (项目下面的具体某个页面)
    • points 测速点  1=xx&2=xx&3=xx...
    • r 采样率 0~1
  • referer 页面referer
  • ua 解析出 分平台 分系统 分版本 分app 网络类型
  • isp 运营商
  • uin 用户的id

为了减少服务器的压力,上报机器与入库服务器分离。入库时,入库服务器定时从上报机器上拉取日志,进行数据入库。

数据的入库

数据的处理是该系统一大难题,全平台每天的pv上亿。为了避免数据过于庞大,我们将收集的数据按日期建立新表。

即使按日期建立新表,查询的数据也有上千万,直接查询表的数据也是非常耗时的。为了解决数据查询耗时的问题,我们建立了三个表,数据统计表,原始数据表,原始数据索引表

数据统计表

统计表是记录5分钟内某个页面所有点的平均耗时。在解析数据的时候,程序将一天分为多个5分钟,计算每个测速点的5分钟平均速度,并写进数据统计表,在查询某个测速点的一天的走势,我们可以直接查询统计表,无需将所有点再重新便利一遍。使用统计表可以大大减少查询的数据量,从而提高查询速度,查询mysql是毫秒级别。

原始表 & 索引表

数据统计表,可以解决大部分数据查询需要,但如果增加几个复合条件查询(查询条件有,国家,省份,运营商,网络类型,操作平台),显然统计表是满足不了的, 如果把每个条件组合都建立一个统计表,那会产生很多额外的表,而且复合查询使用率也不是很高,付出的却很大,这种方案是不可行的。

我们将原始数据分别存到不同的表里,通过索引表里面的索引来查原始数据。一个表的数据不宜过大,数据超过一定的数量级,查询速度会非常慢,为了保证Mysql的性能,这里建议单表记录数不超过1千万。通过索引来查询各个分表的数据。

阈值告警

在某个数据接口返回太慢而导致页面打开速度变慢,这个时候我们需要一个预警,来通知开发人员,在处理数据入库时,某个节点5分钟平均用时超过预设阈值,或者默认阈值10秒,系统会将这个信息以某种方式来告诉开发人员。页面通过告警能够非常敏感发现问题,从而及时解决,不让问题继续扩大。

数据展示

系统提供主要展示一个页面各个测速节点耗时的柱状图,单个测速点单天走势,和一段时间走势图。多维分析列表。

页面整体概况

3.png

来展示整个页面所有测速点耗时情况。

使用柱状图的原因,也是为了方便了页面开发人员,观察到底是哪个地方最为耗时,页面的瓶颈在哪。

除了查看页面的整体耗时,还可以查看单个测速点详细情况。

测速点详情

4.png

测速点将展示下面几个方面的信息

  • 平均耗时
  • 请求量
  • 慢速用户比例
  • 速度的正态分布

为了方便挖掘性能可能的瓶颈,需要从多维度对数据进行分析,比如移动端比较看重网络类型,需要我们根据网络类型对数据进行分析。

常见的维度的还有

  • 国家
  • 省份
  • 运营商
  • 网络类型
  • 操作系统

5.png

异常数据处理

6.png

在看观察点的走势图中,走势图会有一个很长的突刺。造成突刺的原因是这个点的延时要比周围点的数据高很多。为了搞清楚突刺的原因,查询了原始表,大多数上报点都是正常的,但会有一次上报的耗时是30多分钟,目前不知道为什么会上报这么长的渲染时间,可能跟用户机器有关,也可能跟当时网络情况有关。这些数据是用户端上报的,具体很难定位问题,这些点对算出来的图表平均值影响较大,为了保证数据整体正常,数据不受某一个异常节点影响太大,我们将大于10分钟的点过滤掉直接过滤掉。

总结

我们从三个各方面,前端上报,数据收集和入库,数据展示来介绍了如何打造一个测速系统,性能优化是我们需要持续关注,为了打造流畅的使用体验,测速系统是必不可少的工具。

参考

https://fex.baidu.com/blog/2014/05/build-performance-monitor-in-7-days/

https://www.qcloud.com/community/article/655542

http://javascript.ruanyifeng.com/bom/performance.html

Top