资讯详情

资讯详情

自建真实用户监控RUM系统:从数据采集到告警落地的完整指南

1. 为什么我要用一个代号“rea”的项目来盯真实用户访问先说结论在绝大多数业务里线上环境的表现和你在本地、测试环境里看到的完全不是一回事。一年前我把一个内部服务命名为“rea”取的是“Real End-user Access”的意思简单说就是“真实用户访问”核心目标是把每个真实用户打开页面时的性能数据、错误信息、行为路径完整捞回来做成一套自建的真实用户监控服务RUMReal User Monitoring。为什么不自接用现成的商业监控产品原因很现实一是业务方要看的指标维度非常细某些垂直场景比如同一页面在不同地域的加载耗时分布、特定渠道用户的首屏时间趋势在通用工具里要么没这个维度要么要额外开很高的费用二是数据要回传到自己的数据仓库做后续分析配合灰度发布、AB实验做切片对比这部分商业产品很难和内部已有的数据体系打通。所以自己搭一套反而是长期更稳的方案。这套服务能在什么场景帮到你线上版本发布后实时看到新版本在大盘用户里的性能波动而不是等用户投诉、客诉电话打进来才后知后觉定位“用户说页面很卡”到底是网络问题、前端脚本问题还是后端接口问题做性能优化时有一个统一口径的基线数据避免“我觉得快了”和“用户真的觉得快了”之间永远存在争议。适合谁来参考前端工程师、性能优化同学、刚准备搭建监控体系的团队以及想搞清楚“一套RUM系统里到底要采哪些数据、怎么采、采回来怎么用”的读者。这篇文章不是讲概念而是把我从指标选型、SDK设计、上报通道、踩坑修复到最终落地告警的完整过程拆开讲里面每一步都有明确理由也附上了真实踩过的坑。2. 指标选型的底层逻辑不是所有数字都值得采2.1 按用户体验的阶段来定指标而不是按“网上流行什么”我在设计rea的指标模型时一开始也犯过“什么火就采什么”的错。后来梳理清楚了用户访问一个页面的体验本质是一条时间线——浏览器开始发起请求、页面开始绘制、首屏内容出现、可交互、后续元素稳定布局。每一个阶段对应一个或多个指标指标之间是有因果关系的不是一个个孤立的数。这条时间线我分为四段网络与请求阶段从用户发起导航到服务器响应典型指标是TTFBTime To First Byte首字节时间绘制阶段浏览器拿到HTML后开始解析、绘制内容典型指标是FPFirst Paint首次绘制、FCPFirst Contentful Paint首屏有内容绘制可交互阶段页面视觉已基本完整用户可以操作典型指标是LCPLargest Contentful Paint最大内容绘制和INPInteraction to Next Paint交互到下一次绘制的延迟这是取代旧版FID的指标稳定性阶段页面加载完成后布局是否还在跳典型指标是CLSCumulative Layout Shift累计布局偏移。这几个指标是Core Web Vitals的组成部分但我在rea里没有只采这五个而是又补了几个对定位问题特别有帮助的指标DOMContentLoaded时间、页面完全加载时间window.load、资源的加载耗时明细JS/CSS/图片分开统计、接口请求的成功率与耗时分位数。为什么要这样补举个例子你看到LCP变慢了但不知道慢在“图片资源加载慢”还是“接口数据返回慢导致渲染被阻塞”这时候如果只有LCP一个数字能知道它慢了却不知道往哪个方向查。把资源加载和接口数据拆细之后定位范围就大幅缩小。2.2 指标口径不统一数据就是死的这是我认为整个RUM系统里最难的一步没有之一。FP、FCP、LCP这些指标浏览器、实验室工具、不同前端库给出的值经常不一致原因就是口径不同。rea里我做了几件关于口径统一的事第一所有指标明确记录计算起点。我用的是navigationStart导航开始时间作为统一的时间原点而不是performance.timing里的其他字段。这样微服务之间、前后端之间对“耗时”的定义才一致。第二指标归一化处理。CLS是一个无单位的得分它的计算是累计布局偏移量之和LCP是时间戳而错误率是百分比。这三类数据在存储和展示上必须分开建模不能塞进同一个“指标数值”字段里得有专门的类型标识。桶状分布存储是我最终采用的方案比如LCP不存单值而是按“0-1秒、1-2秒、2-3秒、3-5秒、5秒以上”分成若干个桶每个桶存计数。这样不仅天然支持分位数计算还能大幅压缩数据量后面告警判断也很方便。第三剔除异常样本。在统计LCP时如果用户把页面切到后台很久才切回来浏览器会暂停计算这类数据在现场统计里要么丢弃要么标记为“后台标签页样本”单独存放。rea的做法是对这类样本打上visible_state字段值可能是hidden、visible、prerender最后的报表默认过滤掉hidden样本但保留在原始数据里可查。2.3 指标采回来后一定要能“切片”我在搭建初期犯过一个比较严重的错误只看整体均值。结果某次发布后整体LCP均值几乎没有变化但后台一查某省的P90数据恶化明显。如果当时只看均值就发布了那个问题会被掩盖很久。所以rea的每条性能样本在上报时都会带上上下文属性包括用户标识脱敏后的ID页面路由以路由为单位而不是以URL为单位因为URL带参会导致同一页面分裂成无数个条目版本号当前发布的前端版本渠道来源扫码、广告跳转、直接访问等地区IP解析后的省级或城市级运营商移动、联通、电信设备类型手机/平板/PC/其他浏览器及版本有了这些维度才能回答“是新版本影响了大盘还是某个渠道的问题”“是移动网络普及地区的用户更卡还是Wi-Fi用户更卡”这类问题。所有后续的告警预警、报告分析都建立在这些维度之上。如果少了这一步采集上来的数据只是一堆数字无法支撑任何决策。3. 采集端SDK的实操设计PerformanceObserver是核心但不止于此3.1 为什么选PerformanceObserver而不是传统timing APIrea的采集SDK一期用的是performance.getEntriesByType(navigation)这类传统API后来测试中发现两个硬伤一是SPA应用里路由切换时传统Navigation Timing拿不到新页面的导航时间二是如果要监听资源加载完成的事件用轮询的方式不仅损耗性能还容易丢数据。后来换成PerformanceObserver它的事件驱动方式更优雅也是官方推荐的做法。具体做法是先判断浏览器是否支持PerformanceObserver不支持的环境再降级到getEntriesByType轮询兜底。rea在整个SDK里封装了一套名为“能力检测”的工具对FP、FCP、LCP、CLS、INP分别判断支持情况缺哪个就放弃采哪个并在上报时带一个support字段说明当前环境采集了哪些指标。这是一个很划算的取舍不会因为某台老设备浏览器不支持新API就整条数据丢掉而是明确标注缺失指标后照常上报后续分析时能做方法层面的区分。3.2 采集LCP的细节坑LCP看起来简单就是“最大内容绘制的时间”但实际处理时有几个很容易忽略的细节。LCP的候选元素可能是图片、视频、文本块、背景图计算时需要用PerformanceObserver监听largest-contentful-paint条目。关键坑在于LCP的候选值会随着加载过程变化最开始时可能是一个小文本块后面一个更大的图片加载完成后LCP值会更新。所以不能只取第一个条目就直接上报而是要持续观察直到页面加载稳定下来再上报“最终LCP”。rea的SDK里是这样处理的监听所有largest-contentful-paint类型的条目每次回调都记录当前最大值然后在页面进入稳定状态后监听页面visible状态、window load事件、以及一个超时兜底取最终值。稳定状态这个细节很重要——如果用户页面一直在加载资源LCP的最终值可能迟迟不落定。rea的做法是捕获到第一个LCP候选后启动一个计时器连续多次没有新的、更大的候选出现就判定为稳定并上报当前值。3.3 CLS的正确测量窗口CLS这个指标我在内部叫它“页面跳动的罪魁祸首”。它衡量的是页面生命周期内发生的意外的布局偏移。这里两个关键词都容易被忽略一个是“意外”用户主动触发的布局变化不算一个是“生命周期内”不是只计算加载期间。要排除“意外”这个词的干扰就得结合用户交互来判断。比如用户点击了某个按钮展开菜单、下拉刷新、输入文字等这些行为导致的布局偏移标准工具会通过“在用户交互后短暂窗口内发生的偏移不计入”来排除。rea在监听layout-shift条目时会记录entry的hadRecentInput字段。这个字段表示“偏移是否发生在用户输入后的短时间内”如果为true则不计入CLS累计。但要注意hadRecentInput为false时也不一定就计入——需要排除浏览器自身机制导致的合法布局变化。实际处理时我一般直接采用“记录所有不为hadRecentInput的条目再按会话窗口计算最大CLS值”。多次加载页面时CLS算的是整个页面生命周期内的累计表现所以rea在单页应用路由切换时会重置本轮会话的CLS计算起点避免把上一个页面的布局偏移带进当前页面的统计数据里。3.4 数据兜底与采样忘了兜底就会丢数据不管API多现代总有一批老环境不支持。rea的采集SDK里做了基于功能检测的分层方案支持PerformanceObserver的环境用Observer采集FP、FCP、LCP、CLS、INP不支持Observer但支持getEntriesByType(navigation)的环境降级用一次性读取的方式采集TTFB、DOMContentLoaded、load时间不采LCP和CLS两个都不支持的环境极老设备只采集最基础的JS错误和页面URL作为访问量的补充样本。这套分层的价值在于不会因为某个老浏览器缺一个API就把整个页面的监控废掉。同时也能看到不同设备类型的样本覆盖差异。采样方面rea在正式上线后设置的默认采样率是10%——也就是每十个用户里随机选一个上报全量数据。这个比率对大部分产品形态都够用了毕竟要的不是精确值是趋势和分布。但如果某个页面访问量特别大比如首屏点击率极高的核心页面可以单独调低到5%甚至1%反之如果问题是偶发性的可以临时把特定路由的采样率调到100%抓几天数据定位问题再调回来。注意采样策略一定要“动态可配置”并且把采样率的变更记录在案。我曾经因为调整采样率后忘了同步给数据分析那边导致对比数据时出现了明显的样本量差异白白浪费了几天排查时间。4. 上报通道的工程化不卡主线程还要确保数据不丢4.1 sendBeacon是最省事的选择但不是万能钥匙采集到的数据最终要送到后端发送动作如果在主线程做同步XHR会直接拖慢页面响应如果在卸载pagehide时用普通fetch发送大概率丢数据因为页面马上销毁请求来不及发出。rea早期用的是Image对象打点上报也就是把参数拼在URL的query后面让浏览器加载一个1像素的图片来触发GET请求。这种方式能保证90%的送达率但有个问题拼在URL上的长度有限数据稍微大一点就会被截断。后来切到navigator.sendBeacon它能保证只要浏览器支持卸载流程中数据也会正常送达而且不会阻塞页面关闭流程。但sendBeacon有一个明显的坑请求大小限制在很多浏览器里大约在64KB左右。如果采集字段太厚比如把某次加载的全部资源明细都塞进去很容易超限。rea的做法是把单条上报数据控制在10KB以内超过的部分拆包并且设置了“如果sendBeacon不可用降级用XMLHttpRequest同步发送”的逻辑。很多团队会在这一步纠结要不要用上报延迟策略比如积累N条后批量发送我的经验是RUM上报不应该过度批量。因为性能监控数据的实时性是有价值的延迟几分钟就失去了“发布后立刻发现异常”的意义。rea最终采用“即时上报关键指标 延迟上报资源明细”的双通道策略第一类数据核心Web Vitals、JS错误、接口失败记录页面加载完成后立即sendBeacon发送第二类数据资源明细列表、长任务记录、详细堆栈这类数据量大且对实时性要求不高积累到一定数量或页面卸载前再上报。4.2 后端接收器的幂等去重设计SDK一旦跑起来就会面临网络抖动、前端重试导致的重复上报。rea的后端接收器重点做了两件事一是“按事件ID去重”每条上报数据在SDK里就生成一个全局唯一的eventId后端接收后如果发现相同eventId已经处理过就丢弃二是“按时间窗口聚合”把同一个用户在同一个小时内上报的重复数据合并计数。这样设计的原因是前端重试机制不可避免网络超时、服务端返回5xx都可能导致同一条数据被重发。如果后端不做去重报表里的PV数会虚高甚至出现荒谬的“某个页面访问量比真实值高30%”的情况。事件ID的生成规则是用随机数加时间戳加页面会话ID确保同一会话内不同事件不重复同一事件不同时间重试时ID不变。4.3 数据入库与消费链路rea的数据链路是浏览器SDK → 网关接收 → 消息队列 → 数据处理服务 → 写入数据仓库明细表 聚合表。为什么中间要加消息队列因为监控数据的特点是“突发性”。白天线上业务高峰时上报量可能瞬间冲到平时的几十倍如果后端没有缓冲直接把请求打进数据库高峰期查询会直接把库拖垮。有了队列做削峰填谷数据处理服务按稳定的速率消费即使处理速度暂时跟不上数据也只是堆积在队列里延迟处理不会丢失。聚合表是我后来加上的因为用原始数据明细表去做“某个路由昨天的P90 LCP”这类的查询海量样本下会非常慢。rea每隔五分钟跑一次聚合任务按“路由地区运营商浏览器版本号”组合键把桶状分布数据汇总成一张聚合表。日常看板读聚合表秒级出结果深度排查时再去查原始明细表。这套两层结构是监控系统的标配跑了一段时间后非常稳定。5. 上线后的踩坑实录SPA路由、同源iframe、低端机与“假首屏”5.1 SPA路由切换时监控“重置”的边界条件这是rea碰到的第一个大坑。某次上线后报表显示某路由的访问量异常低查了半天发现是SDK在SPA模式下没有正确监听路由变化。SPA单页应用的核心特点是“换页面不刷新浏览器”整个应用只加载一次后面都是前端路由在切换视图。如果SDK在首次window load之后就认为“页面加载完成”那后续所有通过路由切换打开的子页面都不会被记录为一次新的访问。rea最初接入时就是这样只上报了第一个路由的数据后面的页面全部丢失。解决方案是监听History API的pushState/replaceState事件同时监听popstate用户点浏览器的前进后退按钮路由发生变化时重置本轮采集的状态。注意这里有个坑pushState是编程式调用浏览器不会自动触发popstate事件所以SDK必须通过“包装原生方法”的方式在pushState执行时主动触发自己的回调。社区里很多监控方案都默认监听hashchange但现代SPA基本都用History模式如果只监听hashchange一样会漏数据。rea的SDK在路由切换时做的是重新生成会话ID和route字段保留前一个路由的LCP等已上报指标作为前页面的最终数据然后重置LCP、CLS、INP的监听状态开始新页面的数据采集。5.2 同源iframe导致的指标重复计算另一个比较有意思的坑是页面里嵌了同源的iframe在采集资源加载时间时iframe里的资源也被PerformanceObserver捕获到了。虽然理论上iframe里的资源耗时也属于用户真实体验的一部分但它会污染主文档的FCP、LCP计算——如果主文档首屏明明在1.5秒完成但某个后台iframe里有一个巨大的广告图在5秒时才加载LCP就会变成5秒多报表直接“爆表”。rea在选择LCP条目时只采纳“entry是在主文档上下文中触发的”这一部分过滤掉iframe内产生的largest-contentful-paint条目。做法是比对entry的element所属的Document只有它等于当前页面的document时才算。这个处理对其他指标也一样适用——资源监控明细里iframe的资源单独归到一个iframeList字段里不混在主文档资源里方便排查时区分。5.3 低端机和弱网下的采样失真做RUM系统的人最容易忽略的真实情况是低端机的性能数据本身就是一段“动荡曲线”。一台两年前的千元机CPU主频低内存吃紧浏览器在渲染过程中很容易产生长任务导致FCP时间剧烈波动。如果直接用均值去评估优化效果很容易被极端设备拉偏。rea在数据展示层做了两个处理一是区分设备档位用“设备性能等级”字段高/中/低用户侧的Device Memory和硬件并发核数来判断二是所有性能指标在报表里默认展示“P50、P75、P90、P95”四个分位数而不是平均数。为什么是分位数而不是均值因为性能数据的分布根本不是正态分布少数极端样本会把均值拉高而分位数能告诉你看板的人“正常情况下用户是什么体验”。P75是大多数人在中档设备上的真实感受P95是大部分团队要优化的瓶颈区间。每次评比需求也会基于同一组分位数来做这是保住分析口径一致性的关键。5.4 首屏时间的“假数据”陷阱还有一种情况我曾经误判了很久页面FCP数据优化得很好但用户仍然反馈说慢。后来查清楚了——很多SPA的FCP指标是在HTML里只有一块静态骨架屏时就触发了。骨架屏本身确实是页面有内容绘制但它不是一个有用的首屏真正的业务内容还没出来。这就是“假首屏”。rea后来新增了一个自定义指标叫“业务可用首屏时间”以“首屏区域出现首个真实业务组件比如商品卡片、列表项”为准采集方式是显式调用SDK提供的markFirstRender()方法业务方在核心页面的关键组件挂载完成后调用。这个方法能保证FCP这类浏览器层面的指标和“用户觉得页面能用了”的业务指标对齐。这个自定义指标对业务价值远远大于浏览器原生指标因为这能真正回答“用户从打开页面到看到核心内容花了多久”。6. 告警规则怎么定阈值不是一拍脑袋定出来的6.1 先跑两周基线再定告警线监控系统搭好以后最怕的就是从第一天就设一个主观阈值。比如“LCP超过2.5秒就告警”听起来很合理但实际跑出来可能是各地域正常泛型就是P75达到3秒。rea的做法是上线后先“静默采集”两周这段时间只看数据不定告警。两周后通过聚合表算出各指标在“版本正常、业务正常”时期的P90值把这个值作为告警基线的起点。比如LCP在正常时期的P90为2.8秒那告警阈值就设在3.2秒P90×1.15的宽松系数“持续五分钟超出该阈值”才触发告警。这个基线的价值在于它自动包含了业务特性和用户结构的影响——比如你的用户画像整体偏弱网那基线自然就高一些如果整体是纯Wi-Fi的办公类工具那基线就低很多。一旦直接套用别人的通用阈值几乎每周都会被误报淹没。6.2 按“维度切片”设置差异化阈值在rea告警里我强烈建议不要只按全球或全国大盘配置一个阈值。不同地区、不同运营商、不同版本之间的性能差异极大。我实际用的解法是“白名单 优先关注列表”核心版本当前正式发布版本触发全局告警阈值时立刻通知非核心版本、低流量渠道的异常不直接告警而是进“观察列表”做定时扫描后只有连续上升趋势才升级告警。地区差异化阈值也很有用。有些地区的宽带基础差P90天然比其他地区高用平均逻辑很容易造成“某地区天天报警导致告警疲劳”。最后团队把审批规则调成“重点地区覆盖主要用户的地域单独设置阈值其他地区做聚合趋势分析”告警噪音下降了七成以上。6.3 告警之后的分析路径要“留得下痕迹”每次告警触发后团队里都要有一个人能回答“这个告警到底值不值得处理”。如果告警信息里只有“LCP P90超过阈值”那排查者仍然要从零开始查。所以rea的告警后台上自动附带了“快照上下文”触发告警的维度组合、该组合在告警前后的趋势变化、最近一个采集周期里的头部异常资源接口、脚本、图片、涉及版本等。这一段看起来简单实际价值极大——很多告警在带上上下文之后当场就能看出是哪个脚本导致的。如果告警快照里显示某个CDN域名下的JS文件加载时长自某个时间点起突然翻倍那方向就很清晰了不用再从头分析性能瀑布图。7. 这套系统还能扩展出什么从监控到诊断7.1 版本对比用发布前后的数据说话监控系统如果只用来监控价值只发挥了三成。rea最有用的一个场景是版本差异分析某个版本发布后自动对比上一个版本的同维度性能数据输出“P75上涨/下跌幅度”的对比报告并标记出恶化超过5%的维度组合。这种能力让团队在发版后不再靠感觉拍板而是直接看到数据。你可以在rea告警台的“版本对比”模块里看到类似下面这张表的结构示意版本样本数LCP P75LCP P90CLS P75JS错误率v1.0.0上一版8,4322.1s3.0s0.080.4%v1.0.1当前版9,0172.4s3.6s0.121.1%如果发布后看到JS错误率上涨说明大概率是新代码的异常如果LCP上涨但JS错误率持平方向则主要在加载链路或后端接口。告警与版本关联是RUM系统从“被动监控”走向“主动诊断”的关键一步。7.2 从指标到达因接入日志与链路追踪到了后期rea已经不停留在性能指标层面而是开始对接内部已有的日志系统和链路追踪服务。做法是前端SDK在关键接口请求的发生点生成一个traceId这个traceId随同接口请求头传给后端同时写入rea的上报数据里。后端日志系统里也有同样的traceId两边一碰撞就能把“用户感知慢前端指标”和“后端某个接口P99高后端日志”关联起来。这套打通对一个曾经靠猜来定位问题的团队来说是质的飞跃。曾经排查一次白屏问题要花半天前端说是接口慢后端说是前端不调用两边僵持不下。现在只要打开一条数据看traceId对应后端的调用链路和耗时几秒钟就知道卡在哪一步。7.3 长期演化从“采集数据”走向“自动化优化闭环”如果rea这套系统跑上一年数据沉淀足够后我打算做的是“自动化优化建议”根据采集到的资源明细自动找出体积明显偏大且加载耗时高的JS/CSS/图片文件给出“哪些文件该拆包、哪些图片该转格式、哪些接口该加缓存”这类建议。虽然这一步现在还没完全实现但趋势是明确的——RUM不仅仅是监控工具它正在成为前端工程团队最重要的数据基础设施之一。从“我怀疑这里有问题”到“数据告诉我问题就在这里”这个转变是监控体系给研发流程带来的最大价值。我不打算写什么高远的展望只说一点个人体会真实用户监控这个事投入不小但它属于“今天不做明天出事故时追悔莫及”的基础设施。rea这个代号从取名到现在跑了大半年期间数不清多少个晚上因为它揪出了藏在角落的问题。如果你也在考虑搭一套最合适的时机永远是现在。数据不会自己出现采集端一旦铺好后面所有优化工作都会变得有的放矢。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →