其他
百万 QPS 前端性能监控系统设计与实现
什么是前端性能监控(RUM)
前端性能监控技术架构历史
前端性能监控最初架构
分析日志存储问题
解决 MongoDB 使用问题
引进腾讯云日志服务(CLS)
优化指标计算
改进后的前端性能监控整体架构
测速架构介绍
count :累计值,包括 PV,自定义事件访问量。
set :去重值,包括 UV,自定事件访问用户数。
histogram : 直方图,用来计算性能指标数据。
summary :统计数据,用户计算性能和指标数据(与 histogram类似,虽然也上报了,但是我们主要使用 histogram)
StatsD 收集完指标数据后,通过部署在 Sidecar 上的 Telegraf agent 把数据上报给云监控中台。Telegraf:数据收集 agent,可编写插件对接监控中台协议上报鉴权,流量限制等。
使用 Sidecar 模式的优势:
通过抽象出与功能相关的共同基础设施到一个不同层降低了微服务代码的复杂度
能够降低微服务架构中的代码重复度和代码耦合度
节约 Node 多进程模型对 telegraf 的重复使用
微服务抽离
从上面的架构图中还可以看到,我们使用 Golang 做了一些微服务,其中包括
Ipcity:把用户ip信息解析为城市信息和运营商信息的服务。
Restful:提供独立的剪枝微服务。
kafka:把数据旁路到用户提供的 Kafka 中的服务。
当然这样的微服务架构也不是一蹴而就的,也是我们经过长时间的摸索和失败得出来的经验。拿 ipcity 举例。有一段时间发现服务器内存总是非常高,而且服务时不时崩溃,通过 heapdump 抓取了服务崩溃时候的内存 dump。
发现内存中存在一个超大的 Object,来自 ipcity。我们把 ipcity 的进程单独跑起来,发现就已经占有了 1.38G 的内存,而且这个内存会随着数据源进化不断变大。
这对于 Node 多进程模型来说,几乎是致命的,因为每个进程都会启用一个 ipcity 的服务。
最后把 ipcity 的服务放在 trpc-go 里,接入层通过 rpc 通信完成 ip 转化为地区和运营商的工作。
这样做的效果非常显著:
数据接入层功能更纯粹,数据转发,Node 服务做自己擅长的事情;
彻底解决 mode 多进程模型重复耗费内存的问题;
把一些密集型 CPU 计算放在 trpc-go 中完成;
Ipcity 独立升级,独立维护,不影响整个接入层。
由于整个服务是无状态的,并且会随着业务上报量平行扩容,用户上报的数据会随机分配到任意一个 Node 节点上,因此需要我们每个 Node 节点都跟用户提供的 Kafka 保持连接。显而易见,这样对用户 Kafka 的连接数是一个挑战。解决办法也是通过微服务改造,把全部数据集中在几个节点上,再旁路给用户。完美解决,后续又陆续改造了告警服务,剪枝服务。
RUM 发展遇到的问题以及解决思路
流量激增
随着业务的发展,我们首先面临的挑战是用户上报数据流量激增。
业务请求一段时间内突增 650%。想必这种情况对于大多数企业来说都是非常致命的。鉴于之前的经验,我们使用单机限流 + 按项目令牌桶抽样解决流量突增问题。
单机限流:准确来说,是 Node 单进程内存限流,这个主要为了防止一些非常突发和异常的流量进入我们的数据后台。
令牌桶抽样:主要针对测速数据,实行按项目和接口级别的抽样控制,可以保证每个项目,每种类型的接口最大入库不超过某个预设值。
流量整型后的效果也非常显著,无论是部署我们服务的 stke 节点,还是用来限流的 Redis ,资源都大幅下降。Stke 广州节点数下降40%,Redis CPU 从 57% 下降到 17%。
UV 计算
早期使用Redis hash-set 计算 uv ,根据规则对用户唯一值(aid)取模后分布在 60个不同的 Redis key 中,随着业务增长,尤其是 “小程序垂搜” 加入,UV数据显著上涨,hash-set 占有内存大,计算消耗大的问题凸显。旧版本当时宽口径 UV 约几十亿每天,用这种方式 UV 数据使用的 hash-set 最大占有较大的内存。
这种方案显然是行不通的,重新整理需求,UV 计算是基于一天用户去重的,本质上是 DAU,我们需要一个可以实现去重算法的方案。当时考虑的有布隆过滤器和 HyperLogLog 算法,还有业界比较流行的离线计算的方案。
HyperLogLog 是什么?
HLL 是一种近似的去重算法
HLL 使用极少存储空间计算不重复元素的个数
多个使用 HLL 统计出的维度基数可以重聚合
对比来说,离线计算和 hash 的方式,可以提供一个高精度的 UV 计算方式,但是 hash 的方式带来极大的内存占用,离线计算的方式需要我们引入额外的计算框架,成本较高。所以最后选择了 HLL 算法,HLL 算法优势是可以高效计算去重元素个数,占有空间极少,缺点是存在少量误差。下面是我们用 1亿 条不重合 aid 存储使用 hash 和 HLL 中内存占用的区别。
使用 hash 的方式,占用了 4.37G 的内存,使用 HLL 的方式,只占用了 12k 的内存,数据对比惊人。当然 HLL 算法的误差也不是永远这么大的,随着其内部分桶个数的不同,占用内存大小和误差也在变化。
不过 Redis 中 HLL 默认选择了 14 分桶,因此显示误差是 0.81%。那有没有办法减少误差呢?唯一的办法就是自己实现 HLL 算法,目前我们的方案是通过 Clickhouse 实现了 HLL 算法,并且使用 16分桶,这样精度更高,而且存储空间的增长也在可接受范围内。
页面自动剪枝
页面剪枝主要面对的是具有 Restful 风格页面和 API 的分析和统计功能,以下图为例。
从截图中可以看出来,单个页面地址和单条 API 测速无意义,也无法聚合。即使服务端有充足的算力,Web 侧页面展示也是一个问题。那么 Restful 风格 的页面地址和 API 如何聚合呢?
我们给出的方案是在服务端自动帮用户把数据做汇聚,把上面 URL 里面的变量汇聚成为一个 *。
首先根据 / 把整个 URL 地址绘制成为一个树,这样的话,变量节点就会变成一个有超级多兄弟节点的叶子结点。那我们只需要判断某个节点的子节点个数超过一定的阈值就可以得出,这个 URL 可能是属于 Restful 风格的发散节点。
所以这个问题就简化成了,当某个节点的子节点数大于 N,子节点可能就是一个变量,我们就可以把其子节点汇聚成为 * 以满足我们的诉求。唯一的变量就变成了 N,我们只需要确定 N 的值就可以了。这是一个典型的数据分析模型转化为数学公式的问题。最终根据经验和真实用户数据,我们给了一个 N 的大概值用来解决这个问题。
当然这个算法并不是完美的,比如构建剪枝树需要消耗比较多内存,剪枝数有新的节点加入的时候需要对各个节点做对比,因为剪枝树需要放在 Redis 中,所以还有 Redis 高并发读写的问题。
但是随着我们计算能力的增强,尤其是引入 ClickHouse 作为性能数据的存储引擎后,基本没有再遇到高维度的问题。
使用动态网关解决流量瞬间突增
使用动态网关
前面我们讲到通过流量整型来解决突发流量的问题,这个是建立在流量的增长是在一段时间内完成的。而真实情况我们遇到了一些流量会在一瞬间增长到某个值的情况,比如新年的红包活动,游戏的抽奖,购物的抢购,热门视频的开播等(这些都是血泪的教训)。这种情况若不及时扩容,服务将会崩溃。
对于这种案例,解决方案只有一种,就是在网关层限制,可是我们使用的网关却又无法满足这种自定义的限制。
面对突发流量,一般性自动扩所容的架构无论如何都无法完全满足需求,唯一的解决方案就是在平时就做冗余。比如我们接入层平时 CPU 占用大概在 30 - 50% 左右,当 CPU 达到 70% 的时候,接口就会有一些异常。那可想而知,如果某个时间点有超过原并发 2 倍请求量时候,服务器在扩容之前就已经大量报错了。但是将整个接入层做冗余成本太高了,于是我们还是需要一层网关,于是选择自研一套业务网关。
在这个背景下,动态网关项目应运而生,希望把所有跟流量相关的请求都放在网关中进行,以减少业务的压力。目前动态网关基础功能已经上线,细心的用户可能也发现了,前端性能监控的上报接口更稳定更高效了,基本可以在 30-50ms 内完成响应。
动态网关的核心功能在于拿到用户请求后,直接返回了 204 给用户,然后再异步请求真正的接入层,把用户数据转发给接入层。所以无论后台服务部署在哪里,并发如何,都可以保证用户上报侧不会受到影响。只要网关服务异地多活,就可以保证用户上报接口的性能,随后也可以充分利用边缘计算节点来部署网关服务,效果更加显著。
总结
随着流量不断递增,整体架构面临着流量和维度爆炸的双重压力。下列为处理方案总结:
服务无状态化处理后,整个服务的瓶颈就变成了依赖的第三方服务; 通过流量整型解决突发流量和异常流量的问题; 通过把部分服务微服务化降低整个服务负载,并且解决连接池占有的问题; 对数据上报方式进行架构优化,来解决高维的问题; 通过使用 Redis Hyperloglog 优化 UV 计算; 通过剪枝算法来降低页面地址和接口地址的维度; 引入 openrestry 做业务测网关,把用户请求异步处理,加快服务器响应速度,也解决突发流量的问题。