learn.lianglianglee.com/文章/网易严选怎么做全链路监控的?.md.html
2022-09-06 22:30:37 +08:00

870 lines
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>网易严选怎么做全链路监控的?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇)</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log)</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count()</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM)</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?</a>
</li>
<li>
<a class="current-tab" href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>网易严选怎么做全链路监控的?</h1>
<h3>引言</h3>
<p>严选全链路基建从 2018 年就开始了2019 年又对多端进行了补充,今年再做完大前端部分的链路就实现了全端全链路的覆盖。我有幸参与了整个建设过程,并作为大前端部分的负责人设计和完成了大前端部分的链路建设。</p>
<p>那么问题来了:</p>
<ol>
<li>数据是怎么收集的?</li>
<li>数据是怎么上报的?</li>
<li>数据是怎么可视化的?</li>
<li>SDK 怎么实现的多端兼容?</li>
</ol>
<p>最后还会分享些做 SDK 的心得和对 APM 的一些展望。</p>
<h3>1. 项目背景</h3>
<p>企业在成长到一定程度以后都会对技术基建产生要求,这时候一般有两种路子可以走,购置大厂基建资源,另一种就是发挥技术团队的“余晖”,结合开源资源开发一套与企业业务域契合度比较高的自建方案。一旦走了第一条路子,就 kill 了大部分程序猿的 KPI同时也保护了他们为数不多的头发。</p>
<p>无论采用哪种方式,需要解决的问题会随着业务体量的增加逐渐明显。</p>
<ul>
<li>挂了的网络请求从前端发出去,通过网关,转发到后端的 Java/Python/Node 服务中,到底落到哪个节点了?</li>
<li>线上环境中出现了偶发性的报错,傲娇的程序猿一定会说,”这玩意不能复现,就修不了的啊?”</li>
<li>技术团队热火朝天的搞了个性能优化专题,最后拿了一波理论数字来唬人,“你们”的那些截图我怎么就那么不信呢,数据呢?</li>
<li>项目完结了,我们引入了高大上的复盘的机制,复盘会胜利转型甩锅大会,复盘的时候数据哪里来?</li>
<li>老板忽然说,我想要个“大屏”,嗯,越美越好</li>
<li>我们有监控的,前端小哥也“实现”了一个 SDK 的收集上报,可,当需要支持小程序的时候,他离职了</li>
<li>老板又出现了,拿着他二舅姥爷三姨夫初中同学小舅子的手机来,说,”你看,我们的 H5 为什么挂了?最重要的 Banner 图没出来,啊?”</li>
</ul>
<p>展开了聊的话,我相信你们故事比我还多,那我们需要的是什么呢?</p>
<blockquote>
<p>实时全端全栈全链路性能异常监控系统</p>
</blockquote>
<p>比较拗口,因为是我刚创造出来了,一般我们把这份基建项目称作 APM他的系统架构图基本是这样的。</p>
<p><img src="assets/49d54370-7f17-11ea-9bbe-b79e71d58acf.png" alt="系统架构图" /></p>
<p>剩下的时间,我们掰开了,揉碎了,细细表一表。</p>
<h3>2. 数据收集</h3>
<p>数据收集是 SDK 的主要功能SDK 从加载到页面的那一刻到从页面离开之前都一直在扮演一个看门狗的角色,不断收集前端发生的各种事故,实时的上报到后端,以此实现我们的监控目标。那 SDK 主要帮我们收集了什么信息,又怎么收集的呢?</p>
<h4>2.1 收集什么数据?</h4>
<p>SDK 想要收集什么数据,是由你的业务场景决定的:</p>
<ul>
<li>需要针对页面性能进行优化或者查看已有的主要页面的性能,那么,需要收集 performance.timing 的信息</li>
<li>需要监控网络请求链路那么需要在网络请求中添加链路标记TraceID</li>
<li>需要收集错误数据,那么,需要监听 error/uncatchexception 事件,为了复现错误当时的情况,有时还需要收集用户当时的操作</li>
<li>如果想要支持一点埋点能力,则需要提供基本的 API 给用户,也可以提供基本的 PV/UV 数据</li>
<li>需要支持小程序wx/dd/my/alipay/……)怎么办?重新各端去写一套?显然不用,兼容方案做起来</li>
<li>需要……</li>
</ul>
<blockquote>
<p>大姐,你这么想要,钱付了么。我们也是有底线的,虽然我们名字很长(实时全端全栈全链路性能异常监控系统),但也不是啥都要支持。向我提需求之前,先想想好自己到底想要什么 :p</p>
</blockquote>
<h4>2.2 性能数据</h4>
<p>说到性能数据,不得不提 W3C 给我们的 performance.timing 了,这玩意是个好东西。官方图拿来凑字数。</p>
<p><img src="assets/b3d4b3c0-7f1a-11ea-aef6-539c3826c714.png" alt="png" /></p>
<p>这张图并不是我们实际的每一次请求的耗时,而是一个页面在加载完成前的不同阶段,而且并不是所有的阶段都会发生。通过我们项目总结,将得到的这些数据分成两大类:</p>
<p>第一类,就是网页的“基础耗时类指标”,简单说,这类指标没啥用,大概看看有这么个东西就行了,这玩意主要用来做可视化的瀑布图,看起来比较酷炫而已。</p>
<table>
<thead>
<tr>
<th align="left">字段</th>
<th align="left">描述</th>
<th align="left">计算方式</th>
<th align="left">备注</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">DNS</td>
<td align="left">DNS 查询耗时</td>
<td align="left">domainLookupEnd - domainLookupStart</td>
<td align="left">-</td>
</tr>
<tr>
<td align="left">TCP</td>
<td align="left">TCP 连接耗时</td>
<td align="left">connectEnd - connectStart</td>
<td align="left">-</td>
</tr>
<tr>
<td align="left">SSL</td>
<td align="left">建立 SSL 安全连接耗时</td>
<td align="left">connectEnd - secureConnectionStart</td>
<td align="left">可用于对比上没上 HTTPS 对你的网站造成了多少影响</td>
</tr>
<tr>
<td align="left">TTFB</td>
<td align="left">Time to First Byte网络请求耗时</td>
<td align="left">responseStart - requestStart</td>
<td align="left">TTFB 的指标计算方式有很多,主要用来看着陆页面的网络响应时间,这么说很抽象啊,下边贴了个图,看一下就知道了</td>
</tr>
<tr>
<td align="left">Content Download</td>
<td align="left">数据传输耗时</td>
<td align="left">responseEnd - responseStart</td>
<td align="left">-</td>
</tr>
<tr>
<td align="left">DOM</td>
<td align="left">DOM 解析耗时</td>
<td align="left">domInteractive - responseEnd</td>
<td align="left">-</td>
</tr>
<tr>
<td align="left">RES</td>
<td align="left">资源加载耗时</td>
<td align="left">loadEventStart - domContentLoadedEventEnd</td>
<td align="left">同步加载的资源耗时,这部分往往是可以优化的部分</td>
</tr>
</tbody>
</table>
<p><img src="assets/567261d0-7f1c-11ea-a918-478f642cdd64.png" alt="TTFB &amp; Content Download" /></p>
<p>第二类,我把它称作“有用指标”,我们做性能分析,主要关注的就是这几个了。</p>
<table>
<thead>
<tr>
<th align="left">字段</th>
<th align="left">描述</th>
<th align="left">计算方式</th>
<th align="left">备注</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">FB</td>
<td align="left">首包时间</td>
<td align="left">responseStart - domainLookupStart</td>
<td align="left">结合 TTFB 可以看出网络请求是否存在问题</td>
</tr>
<tr>
<td align="left">FPT</td>
<td align="left">First Paint Time, 首次渲染时间 / 白屏时间</td>
<td align="left">responseEnd - fetchStart</td>
<td align="left">从请求开始到浏览器开始解析第一批 HTML 文档字节的时间差</td>
</tr>
<tr>
<td align="left">tti</td>
<td align="left">Time to Interact首次可交互时间</td>
<td align="left">domInteractive - fetchStart</td>
<td align="left">浏览器完成所有 HTML 解析并且完成 DOM 构建,此时浏览器开始加载资源</td>
</tr>
<tr>
<td align="left">ready</td>
<td align="left">HTML 加载完成时间, 即 DOM Ready 时间</td>
<td align="left">domContentLoadEventEnd - fetchStart</td>
<td align="left">如果页面有同步执行的 JS则同步 JS 执行时间 = ready - tti</td>
</tr>
<tr>
<td align="left">load</td>
<td align="left">页面完全加载时间</td>
<td align="left">loadEventStart - fetchStart</td>
<td align="left">load = 首次渲染时间 + DOM 解析耗时 + 同步 JS 执行 + 资源加载耗时</td>
</tr>
</tbody>
</table>
<h4>怎么上报的数据很多都是 0 啊,这玩意准不准?</h4>
<p><strong>1. 资源加载的 Size 为 0</strong></p>
<p>Size 数据是通过 PerformanceResourceTiming.transferSize 获取的。transferSize 只读属性表示所提取资源的大小(以八位字节表示)。如果是从本地缓存获取资源,或者如果是跨源资源,则该属性返回的值为 0。</p>
<blockquote>
<p>在 Chrome 浏览器中按 F12 打开开发者工具面板,当 Network 页签上的 Disable cache 未勾选时transferSize 为 0。</p>
<p>勾选 Disable cache 后transferSize 即恢复正常。</p>
</blockquote>
<p><strong>2. 资源加载的 Time 为 0</strong></p>
<p>Time 数据是通过 PerformanceResourceTiming.duration 获取的。在瀑布图中查看静态资源加载情况时,部分情况下 Time 为 0是由于该请求命中了缓存并且是通过 max-age 控制的长缓存。</p>
<blockquote>
<p>在 Chrome 浏览器中按 F12 打开开发者工具面板,取消勾选 Network 页签上的 Disable cache刷新页面后即可看到经过网络过程所耗的时间。</p>
</blockquote>
<p><strong>3. 页面性能指标时间为 0</strong></p>
<p>查看 API 返回的数据时,如果发现很多返回的时间数据为 0是因为受同源策略的影响跨域资源获取的时间点会为 0主要包括以下属性redirectStart、redirectEnd、domainLookupStart、domainLookupEnd、connectStart、connectEnd、secureConnectionStart、requestStart、responseStart。</p>
<p>在资源响应头中添加 Timing-Allow-Origin 配置,例如:<code>Timing-Allow-Origin*</code></p>
<h4>2.3 网络请求数据</h4>
<p>网络请求指的是用户从当前系统中发出去的 Fetch/XHR 请求,如何无侵入的获取到这些请求的数据呢?那就需要 HACK 一下这两个方法了。</p>
<p><img src="assets/a546a300-8635-11ea-bdf2-1776e599fc0a.png" alt="png" /></p>
<p>Hack 的步骤大概有以下几步:</p>
<ol>
<li>把 Windows 中的原生方法另存;</li>
<li>重写原生方法覆盖掉原生的方法,在重写中首先 call 掉原生的方法,保证数据可以直接发送出去;</li>
<li>重写一下 toString 方法。</li>
</ol>
<p>有了这样的能力,在重写原生方法中我们就可以做很多事情了,比如:</p>
<ol>
<li>添加链路跟踪头部x-tracing-id 这种,便于后端将链路串联起来。</li>
<li>添加请求计时,当前请求发出去到接收到请求的正确返回用了多长时间,这个时间我们一般记为网络请求耗时。</li>
<li>添加网络请求快照,可以收集这次请求的 HTTP 请求码,返回的业务 code 等但一般情况下SDK 不要收集网络请求数据的返回,因为那有可能涉密,另外,这些数据量不可控,你报上来也没啥用,所以还是不要收集了。</li>
<li>网络请求错误的时候也可以收集到,记录为错误(异常处理)或者网络请求失败(链路跟踪)都可以,我是按照链路跟踪处理的,因为网络请求错误没有堆栈信息,也没有分析代码的价值。</li>
</ol>
<p>话说回来,我们通过 TraceId是怎么实现分布式链路跟踪的呢这里不得不引入一张 Google 的图。</p>
<p><img src="assets/af8ee760-86a7-11ea-8580-efe30fb23e5b.png.png" alt="img" /></p>
<p>简单说,就是前端打一个标记以后,在后端服务节点传递数据的时候,保留这个头部,同时添加 from/to 的逻辑,来串联请求链路。</p>
<p><img src="assets/db6ff770-86a7-11ea-94dc-e928d61ce86c.png.png" alt="img" /></p>
<h4>网络请求这玩意你 Hack你挂了咋整</h4>
<p>危险的代码都需要 <code>try...catch</code>,对于 SDK 来讲,只是 try…catch 还不够,还需要做两件事:</p>
<ol>
<li>恢复 Hack 之前的函数执行</li>
<li>上报 Hack 失败事件,给 SDK 的开发者</li>
</ol>
<h4>2.4 错误数据</h4>
<p>错误数据的收集一般情况会通过冒泡机制,监听全局的 error、uncatchedException 两个事件收集到之后对错误进行分类收集错误的堆栈信息进行上报。为了在错误分析的时候能够将错误快速的复现还需要收集一下错误发生时候用户操作的上下文环境包括一些浏览器基础信息网络状况、用户的点击、输入事件DOM 节点)等等。</p>
<h4>我自己 catch 了的错误怎么办?</h4>
<p>最佳实践中,我们要求在发起网络请求等异步操作的时候添加 <code>try...catch</code>捕获错误并妥善处理。但这种情况会阻止错误冒泡到全局SDK 就捕获不到这个错误了。但对于业务层面,我们也没有得到想要的结果,这种情况怎么处理呢?</p>
<p>这种情况,我们提供了主动上报错误的接口给到接入方。</p>
<pre><code class="language-js">async getLIst(){
try{
const {result, pagination } = await axios.get(url, params);
this.list = result;
this.pagination = pagination;
}catch(e){
toastr.err(e &amp;&amp; e.msg || '获取列表失败') // 最佳实践的错误处理
sdk.report('error', e) // SDK 提供的错误数据主动上报能力
}
}
</code></pre>
<p>当然还有另外一种形式, SDK 重写 error 方法,接入方在 catch 中,使用 console.error(e) 将 error 打印到控制台。</p>
<pre><code class="language-js">console.prototype.error = function(){
console.error(arguments);
sdk.report('error', e) // SDK 提供的错误数据主动上报能力
}
</code></pre>
<h4>2.5 用户行为数据</h4>
<p>用户数据收集的目的是为了复现错误发生时用户的操作轨迹。以 FunDebug 的效果图为例。</p>
<p><img src="assets/6bedeee0-8637-11ea-9ff7-cf1f0a784843.png.png" alt="img" /></p>
<p>收集用户信息时时刻进行的,这里的收集可以放到 worker 中进行,单独开一个线程处理可以减少对业务的性能损耗。</p>
<p>行为队列是一个固定长度(一般为 5~10 步)的队列,一旦发生错误,则从队列中取出当前内容,并清空队列,将错误信息和队列中的行为信息一同发布到后端。熟悉 HTTP 请求的同学就会发现,这不就是个“滑动窗口”么,对对,就是这个,太专业的!</p>
<p>另外,错误信息需要延迟发送,主要避免什么情况呢,就是重复报错数据的收集。重复报错问题在前端太常见了,一个报错你不管它,相同的操作再来一遍,还报错,再来一遍还是报错。就是这样,但这种场景下我们需要重复收集么,当然不用,只需要记录一下错误发生的次数就行了。</p>
<p><img src="assets/4bfe1f10-8637-11ea-a60a-8194ba77d48b.png" alt="行为队列收集逻辑" /></p>
<h3>3. 数据上报</h3>
<p>数据收集好以后,需要上报到日志收集服务,上报数据的时候就会遇到这么几个问题,数据模型的一致性(便于清洗和聚合),上报的方式和性能,以及自主数据上报能力。</p>
<h4>3.1 定义上报模型</h4>
<p>上报的数据模型采用了通用的 OpenTracing 模型,结合我厂的一部分业务场景进行了改造。</p>
<p><img src="assets/e27d02e0-86a9-11ea-b1ce-c55688cf3f05.png.png" alt="img" /></p>
<h4>3.2 上报方式</h4>
<p>一般上报会采用一个静态文件的形式比如 PNG/GIF 这种,可以规避跨域和异步等待的问题。在我开发 SDK 的时候,我把常见的上报方式都拿出来对比了一下。</p>
<ul>
<li>navigator.sendBeacon兼容性堪忧no-IE</li>
<li>yx.mail.netease.com/a.gif&amp;data_post长度受限</li>
<li>fetch.post(data, {mode: 'no-cors'}) 合并上报</li>
<li>img.srchack 方案兼容 fetch 失效的情况</li>
<li>xhr.post作为降级兼容方案</li>
<li>小程序中使用小程序的 request 方法上报</li>
</ul>
<p>首先会考虑使用 sendBeacon毕竟是官方提供的专门用来做 report 的 API虽然兼容性很挫。</p>
<p><img src="assets/362e8b80-86a4-11ea-bdf2-1776e599fc0a.png.png" alt="img" /></p>
<p>但不影响我们对技术本身的追求嘛。</p>
<p>实在没有,那就得看一下上报数据的长度和方式了。一般情况下上报会分为两种形式。</p>
<ol>
<li>立即上报</li>
<li>延迟上报</li>
</ol>
<h4>3.2 主动上报组件</h4>
<p>主动上报组件跟业务关联度比较高,这里就不详细展开了,其实核心想法还是比较简单。就是在我们不方便去自动收集数据的场景中,由使用者(往往是集团大佬)来主动提供数据场景,并对数据进行脱敏处理(马赛克)通过简单的标识和文字注释,我们可以很容易定位到问题的所在。然后通过上报弹窗的形式,给大佬们更多选择。</p>
<ul>
<li>上报信息多选框</li>
<li>用户信息(可选)</li>
<li>发送给开发者(可选)</li>
<li>上报信息描述输入框</li>
<li>通过 dom-to-img 生成的图片,同时提供图片(标注、马赛克、文字、取消、确定)编辑能力</li>
</ul>
<p>核心截图逻辑可以抽离出来作为 ServerlessCapture as Service。</p>
<h3>4. 数据流向</h3>
<p>数据从 SDK 收集上报以后,通过 HTTP2 通道将数据发送到 Gateway 进行鉴权和过滤,而后通过 Kafka 通道消费到 Collector 做数据清洗、聚合和存储,最后向 NodeBFF 提供数据,由 NodeBFF 对数据可视化平台提供特性数据接口,最终时间数据可视化展示。</p>
<p><img src="assets/0c2ed6e0-86aa-11ea-9ff7-cf1f0a784843.png.png" alt="img" /></p>
<h3>5. 数据可视化</h3>
<p>数据上报上来以后,需要对数据进行可视化展示,展示的数据不外乎性能、异常、链路信息。但这些信息的重心却不一样。</p>
<h4>5.1 异常信息展示</h4>
<p>异常信息的展示分为三层:</p>
<ol>
<li>应用层面,主要展示当前应用的异常概览,错误总数、错误率、错误类型。</li>
<li>错误列表展示,流水展示收集到的错误信息,每一条展示错误名称、所在页面、错误次数、如果打通了公司的组织架构信息,可以直接在系统中指认当前错误的负责人。同时还可以给错误定级。</li>
<li>错误详情页面,详情页面主要展示错误发生时的页面状况,包括页面基本信息,用户信息,用户行为路径(用户数据上报收集所得)错误堆栈(如果收集了 sourceMap 可以做代码映射)错误自主上报截图(如果用户主动上报了也放到这里)。</li>
</ol>
<p>我们进行异常和错误分析的时候,往往没法只通过代码就能定位到问题,还需要链路信息的辅助,所以如果错误发生上下文中有链路信息的时候,还可以通过上报的链路 ID定位到当前错误发生时的网络请求进而跳转到链路查询页面进行详细定位。</p>
<h3>6. SDK 发布</h3>
<p>刀耕火种阶段,我们发布 SDK 一般是将 SDK 发布到 CDN然后让业务方接入以后通过时间戳查询参数来保障每次接入的都是最新版本。但这种方式存在很多问题。</p>
<ol>
<li>SDK 无法回退</li>
<li>多个 SDK 发布时需要接入多次</li>
<li>区分不同开发环境(测试、线上)时需要进行额外配置,甚至加载不同的 SDK</li>
</ol>
<p>这些都是让业务方觉得不靠谱的地方 ,那如何解决这个问题呢?</p>
<p><img src="assets/fee36fc0-86a7-11ea-a330-69dc969564d3.png.png" alt="img" /></p>
<p>简单说就是造一个壳子,这个壳子可以从 CDN 拉取 script 模板而这个模板是可以随时通过管理平台进行配置的。对于业务方而言只需要接入一次盒子或者通过工程化脚本、CI/CD 过程自动化接入)就可以通过配置来接入 SDK 了。</p>
<p>很不幸的是,当我有这个想法之后,抬头看一下,发现 Google/云音乐的大佬们早就已经做出来这样的东西了。最终还是采用共建的形式,引入了他们的方案。怎么说呢。</p>
<blockquote>
<p>英雄所见略同。</p>
</blockquote>
<p><strong>遇到了 Script.error 问题?</strong></p>
<p>当我们接入脚本以后,遇到过这个问题,就是错误收集到的时候,控制台打出了 script.error 问题,这个报错十分诡异,没有堆栈,啥也没有。那为什么会这样呢,其实一艘就有了。</p>
<blockquote>
<p>“Script error.” 有时也被称为跨域错误。当网站请求并执行一个托管在第三方域名下的脚本,就可能抛出 &quot;Script error.&quot; 最常见的情况是采用 CDN 托管 JS 资源。</p>
<p>浏览器只允许同域下的脚本捕获具体的错误信息。</p>
</blockquote>
<p>简单的解决方案呢就是在 script 标签中添加 crossorigin 属性,不用给值。 当然这本身是一个跨域问题,需要 cdn 也配置一下头部允许策略 <code>Access-Control-Allow-Origin: *</code> 不过放心,现在主流 CDN 经这么做了。</p>
<h3>7. 稳定性保障</h3>
<p>稳定性保障主要分为三个角度:</p>
<ol>
<li>SDK 的稳定运行、稳定迭代发布</li>
<li>数据上报接口稳定接收数据,扛得住大流量</li>
<li>为其他管理平台数据的稳定性保障能力(上线前的卡点检查能力)</li>
</ol>
<h4>7.1 SDK 稳定</h4>
<p>SDK 的稳定是重中之重,你发出去的 SDK 一定不能影响业务的正常运行,否则就万劫不复了,轻松领 P0 级 Bug。那如何保障 SDK 不会出现这种问题呢?</p>
<ol>
<li>兼容性优雅降级方案</li>
<li>错误容灾能力</li>
<li>动态管控 SDK 的分发,一旦出现问题,可以立刻下线(回退)掉,避免损失</li>
</ol>
<h4>7.2 上报接口稳定性</h4>
<p>上报接口一般是由后端提供的高可用服务,自动扩容 + 容灾处理,同时会在上报接口中通过 Appollo 来控制接口的采样,降低运维风险。</p>
<h4>7.3 向外输出稳定性保障能力</h4>
<p>这里指的是性能卡点能力,其实也简单,就是提供一个接口调用,这个接口中封装的是固定环境下的 Lighthouse 运行时环境,管理平台将需要进行评测的页面地址发送到服务内部,由 Lighthouse 运行后返回当前给定的页面评分。管理平台可以根据这个评分对页面进行卡点(低于 80 分,不允许上线) 这套方案是参考集团内其他小伙伴开发的,我就不在这里赘述了。</p>
<h3>8. SDK 开发心得</h3>
<p>开发 SDK 是有一段爱恨情仇的,从 0 开始开发,搭建工程化脚手架,到模块化拆分,多端支持持,功能性迭代,踩过了许多坑,印象最深的,莫过于 SDK 的发版落地。</p>
<p>如果你也面临开发 SDK 的任务,我有几个地方跟你唠叨一下。</p>
<ol>
<li>设计先行先把要做的事情全盘想好SDK 没法那么“敏捷”,迭代的版本多会让人觉得不够稳健。优先设计数据模型,结合 TypeScript 可以很方便的组织代码结构。</li>
<li>将代码进行模块化组织,针对不同的端,将可以共用的部分抽离出来(重构一波以后才明白)。</li>
<li>发版之前先想好版本怎么控制,如何更新。</li>
<li>需要暴露使用起来一致的 API 供接入方使用。</li>
<li>尽早收集实际业务方的需求和痛点。</li>
<li>接入过程中要经常进行复盘回访SDK 接入的咋样,有啥问题没?</li>
<li>文档尽量贴近接入方的使用过程和习惯。</li>
</ol>
<h3>9. 功能展望</h3>
<p>APM 现有的能力主要放在了业务层面的性能、异常、错误监控上,收集数据,上报数据,可视化展示数据。除此之外,还能做些啥呢,换句话说:</p>
<blockquote>
<p>你心中的 APM 应该是个什么样子呢?</p>
</blockquote>
<p>我大概做了个脑洞,给这个未来建起来的监控生态取名 hands 取义,上帝之手。你瞧不见他,但他无时不刻不在,当你需要的时候,伸手可得。</p>
<p><img src="assets/00509880-86a1-11ea-94dc-e928d61ce86c.png.png" alt="img" /></p>
<p>功能上,应该有这样一些能力:</p>
<ul>
<li>抽象性能与错误为体验,增加更具表达力的性能模型,赋能体验提升;</li>
<li>聚合多端产品,弱化端之间的概念,给接入方提供洞察全局,概览分析的能力;</li>
<li>通过前置的性能分析和数据积累,预警可能出现的异常。</li>
</ul>
<h3>10. 后记</h3>
<p>本来想要展开的盘子挺大的,但写写发现,要把事情全部讲清楚,能凑够一本书了,恰逢贝壳的陈辰老师出了一本《从零开始搭建前端监控平台》想要深入探讨的,可以先去读一下陈老师这本书,当然也可以凑够 300 人,开个答疑聊天群,我们云聊一下。</p>
<p>最后放一句之前看到的 Slogan切中就里地说明了监控平台的价值</p>
<blockquote>
<p>Prepare for the unpredictable.</p>
</blockquote>
<p>未雨绸缪。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099807419a48b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>