用户浏览页面生产的数据,会被 SDK 整理成 3+1 类数据,再上报给后端。后端将这些数据进行处理、存储、再加工后,被展示出来被最终的开发者消费。
现在,虽然我们可以使用 node.js 来开发后端部分。但是,要 hold 住集团内部所有的前端监控请求,还是挺难的,要同时解决下面 3 个难点:
- 流量大: 预计单日请求峰值可达10亿次
- 数据量大:预计每日产生数据峰值可达5TB
- 实时性强:需要提供分钟级别的告警、分析功能
前端开发同学是最了解前端监控系统的需求的,但是对于海量数据处理我们的前端团队并没有经验,那么应该怎么应对大数据的挑战呢?
我们给出的答案是,合作。 JavaScript 和 Java 团队合作,发挥各自的优势,用 JavaScript 处理业务,用 Java 处理大数据。 团队整体上分为 3 个小组:
- SDK 小组。负责收集数据。
- Java 小组。负责接收、预处理海量的数据(Java)。
- 全栈小组。负责数据再加工(node.js)和展示(web)。
整体架构
通过 Java 和 Node 的结合,我们的监控平台既拥有高效的大数据处理能力,也拥有良好适应业务快速变化的能力,整体架构如下:
数据存储:Java与Node的桥梁
Java 与 node.js 进行数据交换的桥梁就是,数据存储系统。 我们一共用到了 4 类数据存储系统,它们各有各的侧重。我们先简单介绍一下这 4 类数据存储系统。
MySQL | Redis | ElasticSearch | Apache Druid | |
---|---|---|---|---|
描述 | 关系型数据库 | 基于内存的键值对存储数据库 | 基于Lucene搜索引擎构建的存储 | 高性能的实时分析数据库 |
主数据模型 | Relational | Key-value | Search Engine | Time 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
分布的多了。将异常、性能与通用数据关联起来,就能对项目的某个异常、某个慢加载的机型、版本、地理位置等等通用数据进行分析了。 - 项目告警:当判断出某个指标超出阈值时,就要发出告警通知开发者。具体会在第四篇展开。
设计细节
数据归类: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 中。
具体的数据归类逻辑如下:
- 错误日志 { error, url:"https://58.com/fang/list"}
- 先直接提取项目 ID
- 不存在则提取 URL "58.com/fang/list"
- 从 Redis HASH({"58.com/fang/list": 3}) 中查找 ID
- 按项目 ID 维度归类
日志查询:Java写ES,Node读
Java 把日志明细数据给 node.js 是通过 ES 进行的。具体的是,在 Java 数据归类后,将数据写到 ES 中。开发者在监控平台查询日志明细时,就请求 node.js ,由 node.js 再请求 ES。
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 中展开其中一条日志明细,并把它展示如下:
日志聚合:Java写Druid,Node读
Java 把日志聚合数据给 node.js 是通过 Druid 进行的。具体的就是,日志数据被 SDK 上报给 Java,在 Java 数据归类后,将数据写到 Druid 中。开发者在监控平台查询日志聚合时,就请求 node.js ,由 node.js 再请求 Druid。
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 会对原始数据做预聚合,加快查询的速度。示例如下:
node.js 把读取回来的数据返回给 web,由 web 展示如下(小时维度的 pv uv 数据):
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。