当前位置: 首页 > Web前端 > JavaScript

百万QPS前端性能监控系统设计与实现

时间:2023-03-26 23:15:57 JavaScript

作者:腾讯云前端性能监控负责人李真什么是前端性能监控(RUM)腾讯云前端性能监控(RUM)是一站式前端监控解决方案,用户只需将SDK安装到自己的项目中,通过简单的配置即可实现对用户页面质量的全方位保护,真正做到了低成本使用和非侵入式监控。前端性能监控主要关注Web、小程序等大前端领域。主要关注用户页面性能(页面测速、界面测速、CDN测速等)、质量(JS错误、Ajax错误等),通过与腾讯云应用性能监控联动实现开放前后端监控一体化。前端性能监控技术架构历史前端性能监控主要是对日志和指标进行综合处理,主要开发功能为SDK+接入层+API层+可视化。这个看似简单的功能有什么难点呢?前端性能监控是如何实现的?前端性能监控技术难点:高维数据的处理和高并发请求的处理。高并发想必大家都不陌生。我们面对的是无数C端用户的数据采集和上报。业务数据的保障也会影响我们服务的稳定性。高维数据大家可能比较陌生,那么什么是高维数据呢?比如你要计算某个页面在某个地区、某个机型、某个运营商的平均耗时,如果使用离线计算,就需要提前计算好每个维度的值,所以easy就是几十亿到几百亿的维度分布!前端性能监控的初始架构首先选择了前端同学最熟悉的Node+MongoDB+MySQL模型进行开发。底层是指BadJS(一种在线收集JavaScript错误信息的插件)的实现,对其进行无状态处理和容器化。主持。这种架构的主要问题是性能瓶颈明显,指标没法计算。性能瓶颈主要在MongoDB。写入并发超过一定程度后,即使做了读写分离,MongoDB也无法承受数据的写入。计算瓶颈主要存在于架构设计上,主要实现过程如下:这个架构支撑了我们很长时间,中间做了无数的优化,比如通过“节点分布式定时任务”来提升计算能力》,但是面对前端监控这种维度的计算是没有用的,动辄几十亿、上百亿。如果你很穷,你就想改变。由于现有的架构不能满足我们的需求,我们将寻求新的技术解决方案。分析日志存储问题前端性能监控收集了很多用户质量日志,包括:JS错误、Promise错误、AJAX异常等,这些日志可以帮助开发者快速定位问题和分析页面质量。MongoDB存储日志的好处是读写方便,查找速度快。但日志上报量大,使用MongoDB成为性能瓶颈。解决使用MongoDB的问题我们在使用MongoDB的时候首先遇到的就是满容量的问题。当用户日志报告量较大时,日志已满。解决方法:将每个项目的数据单独存放在一个Collection中,并将Collection设置为“capped”为一个固定的collection。每个集合的数量是固定的,只允许用户保存几千万条日志,可以解决容量满的问题,并且可以保证MongoDB的存储增长不会突然增加或异常,但是毕竟对于大项目来说,会存在找不到历史日志的情况。此外,还存在很多运维问题。除了容量满,还有连接池满、会话数异常、CPU异常等一系列问题。而且,随着业务逐渐增多,MongoDB这种数据库存储方式已经不能满足前端性能监控的业务需求。介绍腾讯云日志服务(CLS)经过仔细分析,我们介绍了腾讯云日志服务。在使用了一段时间后,果断将所有日志迁移到腾讯云日志服务。切换到腾讯云日志服务后,也遇到了一系列的问题,比如最常见的“CLS找不到日志”,但是在CLS背后团队的支持下,稳定的迭代和运维,问题很快就解决了。优化指标的计算,解决了日志中的一个大问题,指标计算的问题显得有点“孤单”。在前端性能监控中,我们会帮助用户计算各个维度的平均值和分位数。指标应该如何优化?指标计算不同于日志。除了阅读和写作,还涉及到对业务的一些了解。我们一直卡在自建的麻烦中,试了很多方法,都没有用。主要原因是无法摆脱旧有的技术框架。他一次次闭门造车,却一次次失败。偶然听说内部团队有成熟的指标方案。使用腾讯云流计算Oceanus+腾讯云时序数据库CTSDB,从此为前端性能监控指标的处理开辟了一条新路径。改进后的前端性能监控的整体架构是整体的。SDK采集数据后,通过腾讯云API网关+腾讯云负载均衡,将数据负载均衡发送给已经部署在K8S集群上的日志接收层和测速。层。这两个模块对用户数据进行限制和采样,然后调用微服务补全字段,比如城市,运营商等,然后通过API接口将日志数据写入腾讯云日志服务CLS,再通过Telegraf传递性能数据(采集上报指标和数据的agent)上报至腾讯云监控平台,最终存储在Clickhouse(一个开源的高性能列式OLAP数据库管理系统)中。其中,我们主要专注于SDK和日志接收层、测速处理层、查询数据的API层、数据可视化部分的开发。测速的逻辑有点复杂,下面我会详细讲解设计和实现。测速架构介绍首先,我们通过StatsDSDK采集数据,将用户上报的数据聚合成我们想要的指标数据,主要包括:count:累计值,包括PV、自定义事件访问量。set:去重值,包括UV,自定义事件访问用户数。histogram:直方图,用于计算性能指标数据。summary:统计数据,用户计算性能和指标数据(类似直方图,虽然也是上报,但我们主要用直方图)StatsD收集指标数据后,通过部署在Sidecar上的Telegrafagent上报给云监控中心.Telegraf:数据采集代理,可以编写插件连接监控平台,上报鉴权,限流等。使用Sidecar模式的优势:通过将与功能相关的公共基础设施抽象到不同的层,降低了复杂度可以减少微服务代码,减少微服务架构中的代码重复和代码耦合。微服务的重复使用从上面的架构图也可以看出,我们使用Golang创建了一些微服务,包括Ipcity:一个将用户ip信息解析成城市信息和运营商信息的服务。Restful:提供独立的剪枝微服务。kafka:一种将数据绕过到用户提供的Kafka的服务。当然,这样的微服务架构不是一蹴而就的,也是我们经过长期的摸索和失败后得出的经验。以ipcity为例。有一段时间发现服务器内存总是很高,时不时的服务崩溃,通过heapdump抓取服务崩溃时的内存转储。发现内存中有一个超大的Object,来自ipcity。我们单独运行ipcity进程,发现它已经占用了1.38G的内存,而且这个内存会随着数据源的演进而不断增长。这对于Node多进程模型来说几乎是致命的,因为每个进程都会开启一个ipcity服务。我们使用pm2在8核pod上启动8个Node进程。这样光是ipcity就占用了12G内存,内存的增加也带来了CPU的增加。所以考虑到我们应该把ipcity服务分离出来,接入层通过rpc调用,而不是在每个进程中引入ipcity模块。最后将ipcity服务放到trpc-go中,接入层通过rpc通信完成ip转化为region和operators的工作。这样做的效果非常显着:数据访问层功能更加纯粹,数据转发、Node服务各司其职;彻底解决mode多进程模型重复消耗内存的问题;将一些密集的CPU计算放在trpc-go中完成;Ipcity独立升级维护,不影响整个接入层。因为Golang语言本身的性能要比Node好很多,而且微服务改造也减少了一些接入层CPU抢占的问题。所以这个优化给我们带来了巨大的成本增加。广州集群Pod数量下降65.75%。欣喜之余,之前困扰我们的Kafka连接池问题也进行了同样的改造。在业务方面,我们让用户在平台上输入一个Kafka主题,可以绕过原始数据给用户。由于整个服务是无状态的,并且会随着业务上报量的增加而并行扩展,用户上报的数据会随机分配到任意一个Node节点上,所以我们每个Node节点都需要和用户提供的Kafka保持连接.显然,这对用户Kafka的连接数是一个挑战。解决方案也是通过微服务改造,将所有数据集中到几个节点上,然后绕开给用户。完美解决,后续陆续改造告警服务和剪枝服务。RUM发展中遇到的问题及解决流量激增的思路随着业务的发展,我们面临的第一个挑战就是用户上报的数据流量激增。业务请求在一段时间内激增650%。想必这种情况对于大多数企业来说都是非常致命的。鉴于之前的经验,我们采用单机限速+项目令牌桶采样的方式来解决突增流量的问题。单机限流:准确的说是Node单进程内存限流,主要是防止一些非常突然的异常流量进入我们的数据后台。令牌桶采样:主要针对测速数据,按项目和接口层级进行采样控制,可以保证每个项目和每种接口的最大存储量不超过一定的预设值。流量整形的效果也非常显着。无论是我们服务部署的stke节点,还是用来限流的Redis,资源都下降很明显。Stke广州节点数量下降40%,RedisCPU从57%下降到17%。早期的UV计算是使用Redishash-set来计算uv的。根据规则,用户的唯一值(援助)被建模并分布在60个不同的Redis键中。随着业务的增长,特别是“小程序搜索”的加入,UV数据明显增多,hash-set占用内存大,计算消耗大的问题凸显。老版本的大口径UV大概每天几十亿,这样的UV数据的hash-set最多占用很大的内存。这个方案显然行不通。重新梳理一下需求,UV的计算是基于一天用户的去重,本质上就是DAU。我们需要一个可以实现重复数据删除算法的解决方案。当时考虑了Bloomfilter和HyperLogLog算法,还有业界流行的离线计算方案。什么是HyperLogLog?HLL是一种近似的重复数据删除算法。HLL使用很少的存储空间来计算唯一元素的数量。可以重新聚合由HLL计算的多个维度基础。HyperLogLog比较Redishash和HLL算法和离线计算。相比之下,离线计算和hash方法可以提供高精度的UV计算方法,但是hash方法带来了巨大的内存占用,而且离线计算方法需要我们引入额外的计算框架,成本很高。因此最终选择了HLL算法。HLL算法的优点是可以高效计算去重元素的个数,占用空间很小,缺点是误差小。下面是我们存储1亿个不重叠的辅助信息时,hash和HLL的内存使用差异。使用hash方式占用4.37G内存,使用HLL方式只占用12k内存,数据对比惊人。当然,HLL算法的误差并不总是那么大。随着内部桶数量的不同,内存大小和错误也会发生变化。而Redis中的HLL默认选择14个bucket,所以显示误差为0.81%。有什么办法可以减少误差吗?唯一的办法就是自己实现HLL算法。目前我们的解决方案是通过Clickhouse实现HLL算法,使用16个桶,精度更高,存储空间的增加可以接受。页面自动剪枝页面剪枝主要处理Restful风格页面和API的分析统计功能,如下图所示。从截图可以看出,单一的页面地址和单一的API测速是没有意义的,无法聚合。即使服务器有足够的计算能力,网页的显示仍然是个问题。那么Restful风格的页面地址和API如何聚合呢?我们给出的方案是在服务端自动为用户聚合数据,将上述URL中的变量聚合成一个*.先把整个URL地址按照/画成一棵树,这样变量节点就会变成一个有超多兄弟节点的叶子节点。那么我们只要判断某个节点的子节点数量超过了某个阈值,就可以断定这个URL可能是属于Restful风格的发散节点。所以这个问题就简化为,当某个节点的子节点数大于N时,这个子节点可能是一个变量,我们可以把它的子节点聚合成*来满足我们的需求。唯一的变量变成了N,我们只需要确定N的值即可。这是一个典型的将数据分析模型转化为数学公式的问题。最后,根据经验和真实用户数据,我们给出了一个近似的N值来解决这个问题。当然,这个算法并不完美。例如,构建修剪后的树需要大量内存。当新节点添加到剪枝数中时,您需要比较每个节点。因为剪枝后的树需要放到Redis中,所以也存在Redis高并发读写的问题。但是随着我们计算能力的增强,特别是引入ClickHouse作为性能数据的存储引擎之后,我们基本上已经不会再遇到高维问题了。###使用动态网关解决流量骤增问题。使用动态网关,我们提到流量整形是用来解决突发流量的问题。这是基于流量的增长是在一段时间内完成的。现实中,我们遇到过一些流量会瞬间暴增到一定值的情况,比如过年红包活动、游戏开奖、抢购、热门视频上线等(这些都是血泪的教训)。如果不及时扩大这种情况,服务就会崩溃。对于这种情况,解决办法只有一个,就是在网关层进行限制,但是我们使用的网关不能满足这个自定义限制。面对突如其来的流量,一般的自动扩容架构无论如何都不能完全满足需求。唯一的解决办法就是平时裁员。比如我们接入层的CPU占用率一般在30-50%左右。当CPU占用率达到70%时,界面会出现一些异常。可以想象,如果某个时间点的并发请求数超过原来的两倍,服务器就会在扩容前报大量错误。但是让整个接入层冗余的成本太高,所以我们还是需要一层网关,所以我们选择自己开发一套业务网关。在此背景下,动态网关项目应运而生,希望将所有与流量相关的请求都放在网关中,减轻业务压力。目前动态网关的基础功能已经上线,细心的用户可能还会发现前端性能监控的上报接口更加稳定高效,基本可以在30-50ms内完成响应。动态网关的架构如下。动态网关的核心功能是收到用户请求后直接返回204给用户,然后异步请求真正的接入层,将用户数据转发给接入层。因此,无论后台服务部署在哪里,并发度如何,都可以保证不影响用户上报端。只要在异地激活网关服务,就可以保证用户上报接口的性能,进而充分利用边缘计算节点部署网关服务,效果更加显着。总结随着流量的不断增加,整体架构面临着流量和维度爆炸的双重压力。以下是解决方案的总结:服务无状态化后,整个服务的瓶颈就变成了依赖第三方服务;通过流量整形解决突发流量和异常流量问题;降低整体Service负载,解决连接池占用问题;优化数据上报方式结构,解决高维问题;使用RedisHyperloglog优化UV计算;通过剪枝算法对页面地址和接口地址进行降维;引入openrestry作为业务测试网关,异步处理用户请求,加快服务器响应速度,解决突发流量问题。点击了解腾讯云前端性能监控(RUM)