CSP + JS 完整性监控做进 InfluxDB 后,线上直接炸了
千万月活 SaaS 的安全监控系统,选 InfluxDB 存储上报数据。测试环境 3 天上线秒过,上线后 Node 内存暴涨、查询卡死。根本原因:时序数据库不是这么用的。
起因
前段时间我们想做一套前端安全监控:
- CSP(Content Security Policy)违规监控
- 自有 JS 完整性校验
- 第三方 JS hash 校验
- 风险脚本告警
业务是 SaaS,多国家站点,所有站点加起来月活接近千万。
需求听上去不复杂:用户每次打开页面,检查 CSP 合规、校验 JS hash,异常就直接上报。然后做趋势分析、风险统计、实时监控、异常检索。
我们选了 InfluxDB。原因非常朴素:
"时序数据库快。"
老实说,当时我对时序数据库并不熟。但现在 AI 开发速度太快了——架构还没想清楚,系统已经跑起来了。
整个系统几乎都是 Codex 写的。数据结构、InfluxDB 写入、dashboard、聚合查询、前端 hash 校验、查询接口,全部 AI 出品。
第一版,3 天上线。测试环境,一切正常。
然后上线。
直接炸了。
第一版:内存聚合的灾难
最开始我以为是查询写得太烂。
因为第一版的逻辑其实非常简单粗暴:把 raw data 全部拉出来,Node.js 内存聚合。
测试环境完全没问题。数据量小,用户少,cardinality 很低。
但线上根本不是一个世界。
上线大概 20 分钟,告警就开始响了:
- Node 内存暴涨
- CPU 飙升
- dashboard 卡死
- 查询越来越慢
后来才发现,真正慢的不是 InfluxDB,而是我自己把 raw data 全拉出来在内存里算。
于是马上改第二版。
第二版:还是挂了
这次做了几件事:
- 尽量使用 InfluxDB 原生聚合函数
- 减少 Node 内存计算
- 增加原始查询优化
- 重新设计查询逻辑
结果还是挂了。
而且这次更诡异——有些查询几分钟都查不出来。
这时候我开始怀疑:InfluxDB 是不是不行?
后来我才意识到,不是 InfluxDB 不行,而是:
我在拿时序数据库做高维检索。
时序数据库到底解决什么问题
很多人第一次接触 InfluxDB、Prometheus、OpenTSDB,都会有一个误区:觉得既然是数据库,应该也能像 MySQL 一样查数据。
但时序数据库真正解决的问题是:
某个指标,随着时间变化是什么样子。
它擅长的是:
- CPU 使用率变化趋势
- 错误率趋势
- QPS 波动
- 时间桶聚合
- 监控指标告警
也就是 Time Series Analysis。
而不是:
- 任意条件组合检索
- 多维度筛选
- 分页查询
- 明细记录查某一条
但我重新审视自己的需求后发现,我真正要做的是:
- 查某个 userId
- 查某个 scriptUrl
- 查某个 hash
- 查某个 pageUrl
- 多条件组合筛选
- 风险排查
- 分页
- 明细检索
这已经不是"时序分析"了,更像 风控系统、日志检索、OLTP 查询系统。
真正的深渊:Series Cardinality
真正的问题,是在我开始理解 InfluxDB 里 tag 和 field 到底意味着什么的时候出现的。
最开始我以为:
- tag = 有索引
- field = 普通字段
后来发现完全不是这么回事。
InfluxDB 的 tag 不只是"有索引的字段"——它实际上是 series identity 的一部分。数据写入的时候,InfluxDB 就已经决定了 series structure。
这和 MySQL 完全不是一个世界。在 MySQL 里,今天没索引,明天 ALTER TABLE ADD INDEX 就行。但 InfluxDB 不是——tag 一旦设计错,后面通常意味着 backfill、rewrite、rebuild index,基本等于重建整个时序结构。
75 亿 Series
然后我开始真正研究什么叫 series cardinality。
InfluxDB 会把 measurement + tag set 组合成一个唯一 series。
比如:
measurement=csp_report
siteCode=us
env=prod
userId=123
scriptUrl=a.js
pageUrl=/checkout
hash=xxxx
这一整套就是一个 series。
换一个 userId,新的 series。换一个 hash,又是新的 series。
如果你熟悉 InfluxDB,看到 userId 和 hash 放进 tag,可能已经开始笑了。
然后我算了一下:
| 字段 | 数量 |
|---|---|
| siteCode | 30 |
| userId | 100,000 |
| hash | 500 |
| scriptUrl | 50 |
| pageUrl | 100 |
理论 series:
30 × 100,000 × 500 × 50 × 100 = 7,500,000,000
75 亿。
我第一次算出来的时候,甚至怀疑自己算错了。重新按了几遍计算器,发现没错。就是 75 亿 series。
那一刻我突然意识到,InfluxDB 最恐怖的地方不是某个字段特别大,而是 多个"看起来不大"的维度,组合后直接指数爆炸。
InfluxDB 最贵的资源不是数据量
更离谱的是,我们一天的数据其实只有 170 万 rows。
说实话,这个量级并不算夸张。MySQL 分表、ClickHouse、Elasticsearch,其实都还能打。
但问题在于,InfluxDB 最贵的资源从来不是数据量,而是 series cardinality。
很多人第一次接触 InfluxDB 时会误以为 series ≈ 数据量。其实完全不是。
series 更像 索引空间。InfluxDB 会为每一个 series:
- 分配 series id
- 建立 inverted index
- 建立 metadata
- 建立 tag index
所以真正吃资源的不是数据本身,而是 series metadata。
后来我才明白,为什么 InfluxDB 官方一直强调:不要把 userId、hash、uuid、requestId、random string 这种高离散字段放进 tag。因为这些字段会直接把 series cardinality 打爆。
为什么查询越来越慢
高 cardinality 下,InfluxDB 查询不是简单的"扫数据",而是 扫海量 series metadata。
尤其:
- tag filter
- group by
- series expansion
这些操作会导致:
- index scan 成本急剧增加
- shard traversal 跨分片扫描
- mmap page fault 频繁触发
这不是"优化查询"能解决的问题。这是底层架构选错了。
最终方案:InfluxDB + MySQL 双存储
最后整个系统重新拆了。
InfluxDB 继续保留,但只负责它擅长的事:
- 趋势分析
- count / sum 聚合
- TOP N 排行
- 时间桶聚合
- 告警趋势
MySQL 负责检索:
- 明细查询
- userId 检索
- hash 检索
- 多条件筛选
- 风险排查
- 分页
也就是:
InfluxDB 做趋势,MySQL 做检索。
最终的 Tag / Field 设计
改造后的 InfluxDB measurement 只保留低基数的 tag:
# Tags(低基数,参与 series 定义)
siteCode → 约 30 个值
env → prod / staging / dev
type → csp_violation / js_integrity / third_party_hash / risk_script
# Fields(高基数,不参与 series 定义)
reportCount → integer(聚合用)
riskLevel → string(高/中/低)
所有高基数字段(userId、hash、scriptUrl、pageUrl)全部只写进 MySQL。InfluxDB 只关心"某种类型的异常,今天发生了多少次,趋势如何"。
写入层面做了双写:一条数据同时写 InfluxDB(聚合用)和 MySQL(明细用)。写入一致性靠应用层重试 + MySQL 事务保证,InfluxDB 少写几条对趋势影响不大。
后来我才真正理解,判断一个系统是不是适合时序数据库,不要看"数据里有没有时间字段",而要看:
你到底是在看趋势,还是在查记录。
这是两个完全不同的问题。
回头看
整个项目其实特别像 AI 时代的软件开发缩影。
Codex 帮我在 3 天内完成了整个系统。测试环境秒过。开发速度快得离谱。
但它不会提醒我:
30 × 100,000 × 500 × 50 × 100 = 7,500,000,000
AI 可以极大加速开发。但系统最终还是要遵守底层架构规律。
速度再快,方向错了也是白搭。