广告系统上线后,除了把曝光/点击/转化这些业务事件灌进 Kafka 再进 OLAP 出报表,还有另一条被忽视但同样致命的数据流:系统自己的运行指标——每个 bidder 的 QPS、P99 延迟、竞价成功率、超时率、每秒烧掉多少钱。这些指标有一个共同特征:按时间戳持续写入、几乎不改、查询总带时间范围。用 MySQL 存会被写垮,用 ClickHouse 又偏重——这正是**时序数据库(TSDB)**的主场,而 InfluxDB 是这条赛道上最有代表性的一块积木。
这篇把 InfluxDB 从数据模型讲到存储引擎再到版本演进:为什么它敢在单机上扛住百万级 metric/s、series cardinality 为什么是新手第一个踩的坑、TSM 引擎怎么用 LSM 思路对付「写多」、以及 1.x→2.x→3.x 这一路从自研列存到 Apache Arrow 生态的大转身。
一句话定位:InfluxDB 把「按时间追加、按时间范围聚合」这条访问模式吃到极致——Line Protocol 极简写入、TSM 引擎顺序落盘、保留策略自动过期、降采样把原始高频数据压成看板能秒开的粗粒度序列。它擅长的不是「存事件明细做 join」,而是「存指标序列做趋势」。
TL;DR
- 时序数据的三个铁律:写远多于读、几乎只 append 不 update、查询总带时间窗。围绕这三条设计的库才叫 TSDB,MySQL/Redis 都不是。
- 数据模型:
measurement(表)+tags(被索引的维度,字符串)+fields(实际测量值,不索引)+timestamp。tag 组合唯一确定一条 series,series 是 InfluxDB 一切存储与索引的最小单元。 - Line Protocol:
weather,location=cn temp=23.5,humidity=61 1699999999——measurement,tagset fieldset timestamp,极简文本协议,写入不需要先建表。 - series cardinality(序列基数)是头号杀手:把
user_id、request_id、时间戳这种高基数字段塞进 tag,series 会爆炸到百万千万级,内存索引直接打爆。高基数只能进 field,不能进 tag。 - TSM 存储引擎:Time-Structured Merge Tree,LSM 变体。写入先落 WAL(持久化)+ 进 内存 Cache;Cache 满了 snapshot 成不可变 TSM 文件(列式 + 时间戳 delta/zigzag + 多种压缩);后台 Compaction 分级合并小文件。TSI 是把 series 索引落盘、突破内存限制的倒排索引。
- 保留策略(RP)+ 降采样:原始 10s 精度只留 7 天,聚合成 5min 精度留 1 年、1h 精度留 5 年——用 Continuous Query(1.x)/ Task(2.x)自动跑。这是 TSDB 相对通用库最大的产品化红利。
- 版本大转身:1.x(TSM+TSI、InfluxQL、CQ/RP)→ 2.x(Flux 语言、bucket、Task、内置 UI/token)→ 3.x(InfluxDB IOx:Rust 重写、列存 Parquet + Apache Arrow + DataFusion + 对象存储,彻底解决高基数)。
- AdTech 落地:bidder/网关的 QPS/延迟/成败率写 InfluxDB,Grafana 出看板;与 Prometheus 的区别是 push vs pull、长期存储更强。别拿它存「按 imp_id 查明细」——那是 OLAP/KV 的活。
Table of contents
Open Table of contents
1. 先分清:什么数据才配叫「时序」
不是「带了个时间字段」就是时序数据。真正的时序负载有三个共同特征,正是这三条决定了该不该上 TSDB:
- 写远多于读,且写是持续洪流:一个中等规模广告集群,几百台机器 × 每台几十个指标 × 每 10 秒一个点,轻松几十万 point/s。写入必须是主路径优化的对象。
- 几乎只 append,不 update:
2026-08-05 14:30:00 的 bidder-07 QPS = 8123这个事实写下就永不修改。没有「改历史某个点」的需求——这让存储可以放弃就地更新,全用不可变文件 + 合并。 - 查询总带时间范围 + 聚合:没人问「所有时间里 QPS 等于 8123 的点」,只会问「过去 1 小时各 bidder 的 P99 延迟趋势」。时间是天然的第一分区维度。
对照一下就知道通用库为什么不合适:
| 负载特征 | OLTP(MySQL/InnoDB) | 缓存(Redis) | TSDB(InfluxDB) |
|---|---|---|---|
| 访问模式 | 按主键点查 + 事务改行 | KV 点查 | 按时间范围扫 + 聚合 |
| 写入形态 | 增删改混合 | 覆盖写 | 纯 append |
| 数据生命周期 | 长期保留、手动清理 | TTL 淘汰 | 自动过期 + 降采样 |
| 索引重点 | B+ 树主键/二级索引 | 哈希 | series + 时间 |
心智切换:把「一行对象」换成「一条随时间前进的序列(series)」。你不是在存记录,你是在给每一条 series 的时间轴上不断钉点。理解了 series,就理解了 InfluxDB 的一切。
用 InnoDB 存指标的经典翻车:时间戳做主键 → 海量顺序插入把 B+ 树右端写成热点,二级索引膨胀,几周后磁盘和查询双双崩溃。这正是 InnoDB B+ 树结构 面对「纯追加超高写入」的不适配。
2. 数据模型:measurement / tag / field / timestamp
InfluxDB 的一条数据点由四部分组成,理解它们的索引差异是用好(不用炸)InfluxDB 的关键。
- measurement:类比关系表名,如
cpu、bidder_metrics。 - tag set:一组
key=value,值永远是字符串,会被索引。用来做维度切分与过滤,如host=bidder-07,region=sg,dc=ap-southeast-1。 - field set:一组
key=value,值可以是 float/int/bool/string,不被索引。这是真正的测量值,如qps=8123,p99=18.5,timeout=0.003。 - timestamp:纳秒精度(可配),不给就用服务器时间。
2.1 Line Protocol:极简到不需要建表
写入用的是纯文本的 Line Protocol,一行一个点:
bidder_metrics,host=bidder-07,region=sg qps=8123i,p99=18.5,win_rate=0.42 1722866400000000000
│─────┬─────│ │────────┬────────│ │──────────┬──────────│ │────────┬────────│
measurement tag set field set timestamp(ns)
规则要点:
measurement,tag1=v1,tag2=v2(逗号连接,无空格)→ 空格 →field1=v1,field2=v2→ 空格 → 时间戳。- 整数要带
i后缀(8123i),否则默认 float;字符串 field 要加引号。 - 写入即建模:不需要预先
CREATE TABLE,第一次写入某 measurement 就隐式创建了。schema-on-write 的灵活也意味着打错 tag key 会悄悄生成一条新 series。
2.2 series:tag 组合唯一确定的那条时间轴
这是全篇最重要的概念。一条 series 由 measurement + 完整 tag set + field key 唯一确定,它的「身份证」叫 series key:
bidder_metrics,host=bidder-07,region=sg#qps
bidder_metrics,host=bidder-07,region=sg#p99
bidder_metrics,host=bidder-08,region=sg#qps
上面第一行和第三行只差一个 host,就是两条独立 series。InfluxDB 的存储、索引、内存占用全部以 series 为单位。 series 数量(cardinality)就是它的规模标尺。
measurement 是表、tag 是被索引的维度、field 是不索引的测量值;tag 组合的笛卡尔积决定了 series 数量。
2.3 cardinality:新手第一个(也是最痛的)坑
series 数量 ≈ 各 tag 取值数量的笛卡尔积。看一个反面教材:
# 灾难写法:把 user_id / request_id 放进 tag
requests,host=web-01,user_id=8f3a...,request_id=abc123 latency=12.3
user_id 上千万、request_id 每次都不同——series 瞬间爆到千万甚至无上限。1.x/2.x 的 series 索引严重依赖内存(TSI 缓解但不根治),cardinality 一高:内存打爆、写入变慢、查询超时、OOM。
黄金法则:
- tag 放低基数、用于过滤/分组的维度:
host、region、dc、status、campaign_id(若可控)。 - 高基数值(user_id、request_id、trace_id、连续变化的数值)只能进 field,接受它「不能被高效过滤」的代价。
- 估算 cardinality:
∏(每个 tag 的基数),上线前先算一遍。
一句话记牢:「你会拿它
GROUP BY/WHERE的,才配当 tag;其余全是 field。」 这条纪律违反了,再好的存储引擎也救不了你。
3. TSM 存储引擎:用 LSM 思路对付「写多」
InfluxDB 1.x/2.x 的核心是自研的 TSM(Time-Structured Merge Tree) 引擎,名字直接致敬 LSM-Tree(和 RocksDB/Cassandra 一个家族 的思路)。它的目标很纯粹:把随机写变成顺序写,把小文件合并成大文件。
写入路径分四个组件:
- WAL(Write-Ahead Log):写入先追加到 WAL 落盘,保证 crash 后不丢——和 MySQL redo log 同一套 WAL 哲学。
- Cache(内存):同一份数据同时进内存 Cache,供即时查询。
- TSM 文件(不可变):Cache 达到阈值就 snapshot 成一个只读的 TSM 文件,顺序写盘、按 series 组织成列式 block。
- Compaction(后台合并):小 TSM 文件分级合并成更大文件,去重、重压缩、丢弃过期数据。
写先进 WAL + Cache,Cache 满了固化成不可变 TSM 文件,后台 Compaction 分级合并——典型 LSM 套路,把随机写摊平成顺序写。
3.1 TSM 文件内部:列存 + 时间戳专属压缩
一个 TSM 文件里,数据按 series 分块,每块内部时间戳和值分开、各自列式存储——这点和 OLAP 列存 同源。压缩因此可以针对性极强:
- 时间戳:多数指标等间隔采集,用 delta-of-delta(二阶差分)+ zigzag + RLE,一串规整时间戳能压到几乎不占空间。
- float 值:Facebook Gorilla 论文的 XOR 压缩——相邻浮点值往往很接近,异或后高位全零,只存有效位。
- int:delta + zigzag + 简单打包;bool 位图;string Snappy。
这套「按数据类型选专属编码」的思路,和 ClickHouse 的 Delta/DoubleDelta/Gorilla codec 是同一份工程直觉:同列同类型相邻 → 压缩比拉满。指标数据压到原始体积的几十分之一很常见。
3.2 shard 与保留:时间就是天然分区
InfluxDB 按时间把数据切成 shard(默认按 shard group duration,如 7 天一个)。好处:
- 查询裁剪:查「过去 1 小时」只碰最近一个 shard,其余全跳过——和 OLAP 的分区裁剪一模一样。
- 过期即删:保留策略到期,直接整个 shard 文件删掉,不需要逐行
DELETE。这是 TSDB 能低成本管理海量历史的关键——删除是 O(1) 的文件操作,不是昂贵的行扫描。
3.3 TSI:把 series 索引搬下盘
早期 1.x 的 series 索引全在内存(in-memory index),cardinality 一高就 OOM。TSI(Time Series Index) 把倒排索引(tag → series、series → 文件位置)落盘 + mmap,靠操作系统页缓存按需加载,把可支撑的 cardinality 抬高了一个量级。但要强调:TSI 是缓解,不是免死金牌——把 request_id 塞 tag 该炸还是炸。根治要等 3.x。
4. 查询、保留策略与降采样
4.1 InfluxQL vs Flux:两代查询语言
- InfluxQL(1.x):类 SQL,上手快。
SELECT mean("p99") FROM "bidder_metrics"
WHERE "region" = 'sg' AND time > now() - 1h
GROUP BY time(1m), "host"
- Flux(2.x 力推的函数式管道语言):表达力强得多,能做跨 measurement join、数学变换、外部数据拉取,但学习曲线陡:
from(bucket: "metrics")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "bidder_metrics" and r.region == "sg")
|> aggregateWindow(every: 1m, fn: mean)
|> group(columns: ["host"])
Flux 是 2.x 的旗舰卖点,但社区反响两极——很多人只想写 SQL。这也埋下了 3.x 回归 SQL 的伏笔。
4.2 保留策略(RP)+ 降采样:TSDB 的产品化红利
这是 TSDB 相对通用库最香的能力。你不会想把 10 秒精度的原始点存 5 年——存储成本爆炸、看板也画不动。标准玩法是分级降采样 + 分级保留:
| 精度 | 保留时长 | 用途 |
|---|---|---|
| 原始 10s | 7 天 | 排障、看瞬时毛刺 |
| 降采样 5min | 90 天 | 周/月趋势 |
| 降采样 1h | 2 年 | 容量规划、年度对比 |
实现方式随版本:
- 1.x:Retention Policy(RP) 定义每个精度的留存时长 + Continuous Query(CQ) 定时把高频数据聚合写进低频 measurement。
- 2.x:概念统一成 bucket(自带 retention)+ Task(用 Flux 周期跑降采样)。
原始高频只留短期供排障,聚合成粗粒度长期保存供趋势分析;到期按 shard 整块删除,成本可控。
落地经验:降采样要在写入侧就规划好三级精度,别等原始数据存满了才想起来。 一旦 cardinality 和数据量起来,补建降采样任务的回填(backfill)会非常痛苦。
5. 版本大转身:1.x → 2.x → 3.x(IOx)
InfluxDB 的版本演进不是小修小补,而是两次架构级重写,理解这条线才能做对选型。
5.1 1.x:经典 TSM 栈
TSM + TSI 引擎、InfluxQL、RP + CQ。稳定、文档多、Grafana 支持最成熟。至今仍是大量存量系统的主力。TICK 技术栈(Telegraf 采集 / InfluxDB 存储 / Chronograf 可视化 / Kapacitor 告警)就是这一代。
5.2 2.x:Flux + 一体化平台
- 引入 Flux 函数式查询语言;
- 存储概念统一为 bucket(org/bucket/token 的多租户与鉴权模型);
- Task 取代 CQ 做定时处理;内置 UI、Dashboard、告警,单进程一体化。
- 底层存储引擎仍是 TSM/TSI——所以 cardinality 的天花板还在。
5.3 3.x(InfluxDB 3 / IOx):Rust 重写的列存新引擎
这是最关键的一跳。InfluxDB 3 用 Rust 重写了引擎(代号 IOx),彻底换血:
- 真·列式存储:数据落成 Apache Parquet 文件,存到对象存储(S3 等)——存算分离。
- Apache Arrow 做内存列式格式,DataFusion 做查询引擎,原生说 SQL(并保留 InfluxQL 兼容)。
- 无限 cardinality:因为不再靠内存倒排索引扛 series,高基数不再是死穴——这直接干掉了前四节反复警告的头号坑。
- 生态对齐:Parquet/Arrow 让数据能被 Spark、Flink、DuckDB 等直接消费,融进现代数据湖。
| 维度 | 1.x | 2.x | 3.x(IOx) |
|---|---|---|---|
| 引擎 | TSM + TSI | TSM + TSI | IOx(Rust) |
| 存储 | 本地 TSM 文件 | 本地 TSM 文件 | Parquet + 对象存储 |
| 查询语言 | InfluxQL | Flux(主推) | SQL + InfluxQL |
| 高基数 | 易爆 | 缓解 | 基本解决 |
| 降采样 | RP + CQ | bucket + Task | 表 + SQL/任务 |
选型提醒:新项目直接评估 InfluxDB 3;存量 1.x 稳定跑着的别盲目迁移(Flux/查询语言、RP→bucket 的迁移成本不低)。3.x 的架构和它想解决的高基数问题,本质上是把 InfluxDB 从「自研 TSDB」拉进了「Arrow 生态的列存分析库」,和 OLAP 列存 的边界越来越模糊。
6. AdTech 落地:广告系统指标监控
回到开头的场景。广告链路里 InfluxDB 的典型位置是系统可观测性的指标存储:
- Telegraf / 自研 agent 在每台 bidder、网关、Flink 作业上采集 QPS、P99 延迟、竞价成功率、超时率、错误码分布、每秒花费;
- 通过 Line Protocol push 进 InfluxDB;
- Grafana 接 InfluxDB 出实时看板 + 告警,和我们在 Grafana JVM 监控 里搭的那套链路思路一致。
tag / field 的划分直接照搬第 2 节纪律:
bidder_metrics,host=bidder-07,region=sg,dc=ap-southeast-1 \
qps=8123i,p99=18.5,win_rate=0.42,timeout_rate=0.003,spend=1240.5 1722866400000000000
host/region/dc 做 tag(低基数、要按它切看板),qps/p99/win_rate/spend 做 field。绝不把 request_id、imp_id、user_id 放 tag。
6.1 InfluxDB vs Prometheus:一对常被拿来比的组合
广告监控里另一主流是 Prometheus。核心差异:
| 维度 | InfluxDB | Prometheus |
|---|---|---|
| 采集模型 | Push(客户端主动写) | Pull(server 定时抓 /metrics) |
| 长期存储 | 强(RP + 降采样,可存年) | 弱(默认本地短期,靠 Thanos/Cortex 远程存) |
| 数据模型 | measurement/tag/field | metric name + labels(≈ tag) |
| 查询 | InfluxQL/Flux/SQL | PromQL |
| 定位 | 通用时序 + 长期存储 | 云原生监控 + 告警一等公民 |
常见组合:Prometheus 抓实时监控告警,InfluxDB(或远程存储)扛长期趋势;或统一走 InfluxDB push 模型。两者都不是拿来做「按 imp_id 查单条明细」的——那是 OLAP 或 KV 的活。
6.2 什么时候别用 InfluxDB
- 要按高基数主键点查明细(查某个 request 的全链路)→ 用 OLAP / [ES] / KV,别硬塞 tag。
- 强事务、要改历史数据 → 不是时序负载,回 OLTP。
- 就几台机器、指标量很小 → Prometheus 单机或直接 Grafana + 简单存储可能就够,别为运维一套 InfluxDB 集群付成本。
7. 生产踩坑与经验清单
- 上线前先算 cardinality:
∏(tag 基数)。超过百万级就要警惕,超千万(1.x/2.x)基本等着 OOM。这是所有 InfluxDB 事故的第一大来源。 - tag/field 划分是不可逆的建模决策:写错了要洗数据。「会 GROUP BY/WHERE 的进 tag,其余进 field」 抄在墙上。
- 降采样三级精度在写入前就设计好,别等数据存满了补 backfill。
- shard duration 要匹配保留时长:留 7 天却用 7 天一个 shard,会导致过期删除粒度太粗;一般 shard duration 取保留期的 1/10 ~ 1/4 量级。
- 批量写入:Line Protocol 一次多行批量写(几千点/批),别一个点一个 HTTP 请求——和 Kafka producer 攒批、请求合并 同一个道理。
- 监控 InfluxDB 自己:Compaction 积压、WAL 大小、Cache 命中、series cardinality 走势都要有二级监控——监控系统自己挂了最尴尬。
- 版本选型:新项目认真评估 InfluxDB 3(高基数解放 + SQL + 对象存储);老 1.x 稳定就别为了新而迁。
参考
- InfluxData Docs. Key concepts:measurement/tag/field/series 官方定义。
- InfluxData. In-memory indexing and the Time-Structured Merge Tree (TSM):TSM/WAL/Compaction 存储引擎细节。
- InfluxData. Line protocol:写入协议语法。
- InfluxData. InfluxDB 3 (IOx) architecture:Rust/Arrow/Parquet/DataFusion 新引擎。
- T. Pelkonen et al. Gorilla: A Fast, Scalable, In-Memory Time Series Database:float XOR 压缩的经典论文,TSM 值压缩的思想来源。