用户浏览页面生产的数据,会被 SDK 整理成 3+1 类数据,再上报给后端。后端将这些数据进行处理、存储、再加工后,被展示出来被最终的开发者消费。

image-20210506175903213

现在,虽然我们可以使用 node.js 来开发后端部分。但是,要 hold 住集团内部所有的前端监控请求,还是挺难的,要同时解决下面 3 个难点:

  • 流量大: 预计单日请求峰值可达10亿次
  • 数据量大:预计每日产生数据峰值可达5TB
  • 实时性强:需要提供分钟级别的告警、分析功能

前端开发同学是最了解前端监控系统的需求的,但是对于海量数据处理我们的前端团队并没有经验,那么应该怎么应对大数据的挑战呢?

我们给出的答案是,合作。 JavaScript 和 Java 团队合作,发挥各自的优势,用 JavaScript 处理业务,用 Java 处理大数据。 团队整体上分为 3 个小组:

  • SDK 小组。负责收集数据。
  • Java 小组。负责接收、预处理海量的数据(Java)。
  • 全栈小组。负责数据再加工(node.js)和展示(web)。

image-20210416190637033

整体架构

通过 Java 和 Node 的结合,我们的监控平台既拥有高效的大数据处理能力,也拥有良好适应业务快速变化的能力,整体架构如下:

image-20210129162333983

数据存储:Java与Node的桥梁

Java 与 node.js 进行数据交换的桥梁就是,数据存储系统。 我们一共用到了 4 类数据存储系统,它们各有各的侧重。我们先简单介绍一下这 4 类数据存储系统。

MySQLRedisElasticSearchApache Druid
描述关系型数据库基于内存的键值对存储数据库基于Lucene搜索引擎构建的存储高性能的实时分析数据库
主数据模型RelationalKey-valueSearch EngineTime Series DBMS
使用场景项目表、权限表、告警表共享缓存、分布式锁全文搜索聚合分析、告警
  • MySQL 是大家最熟悉的,它是关系型数据库,类似于 Excel 的表。它的数据是一行行的存储,在我们的业务表中用的较多。
  • Redis 在我们的监控场景中,主要用于 Java node.js 的共享缓存,另外还有分布式锁。

    • Redis 用的是内存来存储数据的,比 MySQL 用的磁盘存储数据的性能更好,因此更适合比较频繁的数据操作。
  • ElasticSearch (简称 ES) ,在我们的监控场景中主要用于日志的全文搜索。

    • ES 会通过分词构建的索引直接查找,比 MySQL 要一行一行的全表扫描性能更好,因此更适合通过错误关键字、堆栈关键字来搜索具体的报错信息这类场景。
  • Druid 在我们的监控场景中主要用于日志的聚合分析。

    • 比如 count(*),Druid 是列式存储聚合,分析时可以直接读取一列数据, MySQL 要一行一行的全表扫描进行聚合分析,因此 Druid 更适合大数据的聚合分析。

当然具体的业务场景需要具体的分析。如果你们的业务数据量不大,不会遇到性能问题,那么 MySQL + Redis 也许就够了,不用着急上 ES 或 Druid 来优化性能。如果你们的业务数据量很大,PV 都破亿了,建议架构设计的时候,就需要考虑用上 ES 或 Druid。

功能架构

技术架构是整体项目的结构,其中设计细节必须结合功能才能更容易让大家理解,所以必须得先和大家介绍一下功能架构。

SDK 会将通用数据、性能数据、正常数据、异常数据进行上报。这些数据上报后,经过处理形成了监控平台的 5 个功能:

  • 明细查询:通过 ES 的搜索功能可以展示某条日志的明细。
  • 轨迹查询:某个用户的浏览轨迹,包括性能、正常、异常日志记录(敏感信息需要用户授权才上报),通过 ES 将多种类型的日志按时间顺序排序,就能显示出某次用户的浏览轨迹了。
  • 项目看板:一个项目会有多种聚合指标,如JS异常、接口异常、白屏时间等等,多种指标会一起请求回来,汇总一个看板中便于查看。
  • 项目分析:所谓的分析就是异常、性能的具体分布情况,比如你可以对某个项目的 xx is undefind JS 错误和通用数据中的机型关联起来,就能知道它是在 android 还是 ios 分布的多了。将异常、性能与通用数据关联起来,就能对项目的某个异常、某个慢加载的机型、版本、地理位置等等通用数据进行分析了。
  • 项目告警:当判断出某个指标超出阈值时,就要发出告警通知开发者。具体会在第四篇展开。

image-20210129162443424

设计细节

数据归类:Node写Redis,Java读

数据是以 projectId 为维度存储在 ES、Druid 中的。但 Java 只能拿到 url,而 url 与 projectId 的对应关系是在监控平台填写的,因此需要 node.js 把 projectId 和 url 怎么关联的数据传给 Java。

projectId 与 url 怎么关联的数据,会被高频读取,所以 node.js 会把它们存在 redis。日志数据被上报时,java 就会去 redis 读最新 projectId 与 url 关联关系。再进行数据归类,并存在 ES/Druid 中。

image-20210128145122626

具体的数据归类逻辑如下:

  1. 错误日志 { error, url:"https://58.com/fang/list"}
  2. 先直接提取项目 ID
  3. 不存在则提取 URL "58.com/fang/list"
  4. 从 Redis HASH({"58.com/fang/list": 3}) 中查找 ID
  5. 按项目 ID 维度归类

日志查询:Java写ES,Node读

Java 把日志明细数据给 node.js 是通过 ES 进行的。具体的是,在 Java 数据归类后,将数据写到 ES 中。开发者在监控平台查询日志明细时,就请求 node.js ,由 node.js 再请求 ES。

image-20210129161231796

node.js 请求 ES 的关键参数如下:

"query": {
    "bool": {
        "filter": [
            {"range": {"timestamp": {"lte": 1607322167607,"gt": 1607235767606}}},
            {"term": {"projectId": 1}},
            {"term": {"type": "exception" }},
            {"wildcard": {"exception.content.keyword": "*TypeError*"}}
        ]
    }
}

在 ES 中,timestamp、projectId、exception 的索引事先就被创建了。比如, TypeError: Parameter 'url' must be a string, not object 这段文本,在 ES 内部会被分词为一个个单词,TypeError Parameter url must be a string not object ,每个单词都是一个索引。这些索引组合的搜索结果会有 0~N 条日志明细,都会被返回给 node.js。

node.js 再把数据返回给 web,在 web 中展开其中一条日志明细,并把它展示如下:

image-20210128165245123

日志聚合:Java写Druid,Node读

Java 把日志聚合数据给 node.js 是通过 Druid 进行的。具体的就是,日志数据被 SDK 上报给 Java,在 Java 数据归类后,将数据写到 Druid 中。开发者在监控平台查询日志聚合时,就请求 node.js ,由 node.js 再请求 Druid。

image-20210128165634947

node.js 请求 Druid 一般是通过 SQL 来请求的,一条请求 PV 的 SQL 如下:

SELECT 
    TIME_BUCKET(__time,'P1D','Asia/Shanghai') timestamp,
  pv  
FROM hdp_ubu_tech_wei_beidou_data  
WHERE 
    type='performance' AND
  __time>='2021-02-14T00:00:00+08:00' AND
  __time<='2021-02-14T23:59:59+08:00' 
GROUP BY 1

Druid 是为聚合分析专门设计的时间序列数据库。它可以对时间进行智能的分区,比如,在上面我们提到的 SQL 查询中, P1D 就是告诉 Druid 要以天为单位分区,查询 pv 返回的就是一天的 pv。你可以更改配置,以小时、分钟进行分区,查询 pv 返回的就是一小时或一分钟的 pv。

日志聚合:聚合数据流转的整个流程

Druid 会对原始数据做预聚合,加快查询的速度。示例如下:

image-20210129105403021

node.js 把读取回来的数据返回给 web,由 web 展示如下(小时维度的 pv uv 数据):

image-20210129162546196


fitfish
1.6k 声望950 粉丝

前端第七年,写一个 RN 专栏。