一、现象:写入突然变慢,排查从监控面板开始
那天下午,运维同事突然在群里喊:“InfluxDB 写入掉到每秒才几百条了,以前都是几万的!”我赶紧打开 Grafana,看到写入吞吐那条线直接断崖式下滑,同时 CPU 利用率飙到了 80% 以上,磁盘 IO 也在嗡嗡响。这种场面在时序数据库里其实挺常见,但一般不会发生在生产环境刚稳定运行的时候。咱们先别慌,一步一步来看看到底是哪里出了问题。
1.1 先确认数据写入速度是不是真的慢了
直接用 InfluxDB 自带的工具跑一条查询,看看当前写入速率。这里用 InfluxQL 做演示,因为它是 InfluxDB 最常用的查询语言。
-- 查看最近1分钟每分钟写入的数据点数,用导数算瞬时速率
SELECT derivative("pointReq", 1s) FROM "_internal"."monitor"."httpd" WHERE time > now() - 1m;
如果这个值从几千掉到几百,基本就坐实了。然后我们再看一下 Tag 的基数,因为天梯上很多人说 Tag 基数太大是 InfluxDB 性能杀手。
1.2 查一下 Tag 基数到底有多大
用 SHOW TAG KEYS 和 SHOW TAG VALUES 可以快速扫一遍。比如我们有一个测量值叫 “device_metrics”,里面有个 Tag 叫 “device_id”。
-- 看看这个测量值里有哪些 Tag 键
SHOW TAG KEYS ON "mydb" FROM "device_metrics";
-- 然后查某个 Tag 有多少个不同的值(基数)
SHOW TAG VALUES ON "mydb" FROM "device_metrics" WITH KEY = "device_id";
如果返回的结果里,device_id 有几十万甚至上百万个不同值,那基本就是元凶了。Tag 基数过大意味着 InfluxDB 在写入每条数据时,都要去维护一个巨大的倒排索引,内存和 CPU 都扛不住。
二、为啥 Tag 基数猛增?——从业务代码里挖原因
我们查了一下业务日志,发现最近上了一个新的采集任务,每个设备都带着一个唯一的 device_id,而且这些 device_id 数量还在每天增加。原本以为没问题,后来才知道 InfluxDB 对 Tag 基数非常敏感。咱们来看一个典型的写入数据点示例:
-- 假设每条数据长这样
INSERT device_metrics,device_id=uuid-12345,city=beijing temperature=25.6,humidity=68
如果 device_id 每新增一个设备就产生一个新值,那么随着设备数量积累到几万甚至几十万,Tag 基数就失控了。更可怕的是,我们当时没有对 Tag 做任何限制或聚合,所有设备都直接打进去。
2.1 什么场景下容易触发这个坑?
在物联网、监控等场景,每个设备、每个传感器都会有一个唯一标识。如果直接把标识当 Tag,不做预聚合或降维,就很容易踩雷。另外,一些 SaaS 服务会把租户 ID 也当成 Tag,租户数量一多同样挂。
它的优点:Tag 可以直接用于过滤和分组,查询很快。缺点:一旦基数过高,写入性能雪崩,内存占用飙升,磁盘上的索引文件也会越来越大。
三、从 TSM 索引机制看本质问题
InfluxDB 底层存储引擎叫 TSM(Time-Structured Merge Tree),它有一套自己的索引策略。简单来说,写入的数据会先写进内存里的 WAL 和缓存,然后定期合并成 TSM 文件。每个 TSM 文件里会附带一个叫 “Index” 的部分,用于记录 Tag 和时间的映射关系。
3.1 索引是怎么膨胀的?
每次写入一个包含新 Tag 值的数据点,InfluxDB 都会在内存里更新一个倒排索引表。如果 Tag 基数太大,这个索引表会变得非常巨大,导致内存被吃光,进而触发频繁的 GC 和磁盘写入。更关键的是,当内存中的数据刷到磁盘时,每个 TSM 文件的索引也要写入,文件数量一多,合并时的开销也成倍增加。
我们可以用一个脚本来模拟看看索引增长的速度。这里用 Python 写个简单的压测,但注意要指定技术栈。因为文章要求单一技术栈,所以这里只展示 InfluxDB 的命令行或查询,不混用。但为了帮助理解,可以用一个 Shell 脚本来批量写入测试数据,这属于命令工具。
#!/bin/bash
# 向 InfluxDB 批量写入带有大量不同 Tag 的数据,模拟基数爆炸
# 注意:生产环境千万别这么干,这是演示用的
for i in $(seq 1 100000); do
curl -i -XPOST 'http://localhost:8086/write?db=testdb' --data-binary "test_metrics,device_id=dev_$i,city=shanghai temp=25.6"
done
跑完后,用系统监控工具看内存占用,会发现 InfluxDB 进程的内存蹭蹭往上走。这就是索引膨胀的直接后果。
3.2 分片策略在其中的角色
InfluxDB 默认按照时间区间分片,每一个分片(shard)是一个独立的存储单元,拥有自己的 TSM 文件和索引。如果 Tag 基数都集中在某一个分片里(比如最近一小时数据),那么这个分片的索引会非常大,导致这个分片的写入和查询都变慢。相反,如果分片时间窗口设置得当,可以让新数据分散到多个 shard 中。
四、实战优化:从 Tag 治理到分片调优
既然找到了病根,那就对症下药。我们采取了一套组合拳,把写入吞吐从几百又拉回到几万。
4.1 立即停掉高基数 Tag 的使用
先把业务代码里的 device_id 从 Tag 改成 Field。虽然 Field 不能直接过滤,但写入不受影响。当然,如果还需要按设备查询,可以在 Field 上建一个时间范围 + 设备 ID 的组合条件。改动如下(以 InfluxQL 写入示例):
-- 之前(Tag方式,高基数)
INSERT device_metrics,device_id=uuid-12345 temperature=25.6
-- 之后(Field方式,低基数)
INSERT device_metrics temperature=25.6,device_id="uuid-12345"
注意:这样修改后,原来的查询语句也要调整,不能再按 device_id 做 WHERE 过滤,而是要用正则或子查询。但性能损失远小于 Tag 基数爆炸带来的写入崩溃。
4.2 对必要的 Tag 做降维
有些 Tag 必须保留,比如城市、机房等枚举值有限的。我们可以在业务层做预聚合,比如只保留城市这个 Tag,把设备 ID 放到 Field 里,或者把多个设备的采样点合并成一条记录,用数组存 ID。这部分不在 InfluxDB 层面,而是业务改造。
4.3 调整分片策略
InfluxDB 的分片时间窗口可以在创建数据库时指定,也可以事后修改(但影响后续数据)。比如我们把默认的 7 天分片改成了 1 天,这样每个 shard 里包含的数据量变小,索引也小很多。同时调整保留策略,只保留 90 天数据。
-- 创建数据库时指定分片时长(单位是小时,24代表1天)
CREATE DATABASE "mydb" WITH DURATION 90d REPLICATION 1 SHARD DURATION 24h NAME "rp_90d"
这个配置的好处是,每天的数据都落在一个独立 shard 里,即使某一天的 Tag 基数很大,也只影响当天的 shard,不会拖慢其他时段。缺点是需要更多磁盘空间(因为 shard 文件数量增多),但现在磁盘便宜,可以接受。
4.4 使用 Tag 预聚合
如果业务上确实需要按设备 ID 做时间线,可以换个思路:先按城市、类型等低基数 Tag 建一个测量值,然后在里面存储一个高基数的设备 ID 列表作为 Field,查询时再做展开。或者使用 InfluxDB 的 continuous query 定期把高基数数据聚合到新的测量值里,压缩粒度。
我们写了一个 CQ 示例:
-- 每分钟聚合一次设备温度的平均值,按城市分组
CREATE CONTINUOUS QUERY "cq_device_avg" ON "mydb" BEGIN
SELECT mean("temp") AS "avg_temp" INTO "device_agg_1m" FROM "device_metrics" GROUP BY time(1m), "city"
END
这样高基数的原始数据只保留短时间,聚合后的数据按城市分组,Tag 基数很小。查询历史趋势查聚合表就行。
五、注意事项与总结
5.1 注意事项
- 不要等出问题才查 Tag 基数:建议在数据库初始化时,就预估好 Tag 值的种类数,超过 10 万就要警惕。
- Tag 值长度也影响性能:如果 Tag 值是一个很长的 UUID(比如 36 字符),索引占用的内存比短字符串多很多,可以考虑用哈希后再存储。
- 分片时长不是越短越好:如果你查询跨度很大,比如要查一个月的数据,分片是 1 天,那查询时会合并 30 个 shard,效率反而低。要根据实际查询场景折中。
- 修改 Tag 为 Field 后,原来按 Tag 查询的接口必须改:否则会报错或返回空。建议先用别名过渡。
- 监控一定要做:定期跑 SHOW TAG VALUES 检查基数,超过阈值就告警。
5.2 技术优缺点总结
- Tag 高基数优点:支持极快的时间线过滤,适合枚举值有限的场景(如 CPU 型号、操作系统版本)。
- Tag 高基数缺点:写入性能随基数线性下降,内存占用按基数倍数增长,索引合并开销大。
- TSM 索引机制:对高基数敏感,但提供了倒排查询的优势。
- 分片策略:通过合理设置窗口可以隔离高基数冲击,但增加了查询合并成本。
5.3 应用场景
这套优化方案特别适合物联网、APM、业务监控等场景,因为这些场景天然带有大量设备或用户 ID。如果你在做 IoT 平台,一定要在一开始就把 Tag 基数控制住,否则后期迁移数据非常痛苦。
六、整个排查手记的总结
回到开头那个异常,我们通过监控确认写入慢,然后查到 Tag 基数从几万飙升到百万级别,接着分析了 TSM 索引的膨胀原理,最终通过将 device_id 从 Tag 改为 Field、调整分片窗口、建立聚合查询三步操作,把写入吞吐从每秒几百条恢复到每秒几万条。整个过程持续了大概半天,没有走任何弯路。如果你的 InfluxDB 也遇到了类似问题,强烈建议照这个思路检查一下——先看 Tag 基数,再看分片配置,最后看业务模型。记住,时序数据库不像关系型,Tag 用不好真的会要命。
Comments