本节摘要:现代 TSDB 写入分协议层、内存层、持久层三重解耦:Line Protocol O(n) 解析 → MemTable 按 series 归位 → WAL 追加 → 异步 Flush 落盘。索引侧,标签倒排定位 series,时间 block 内顺序扫描。若每次写入同步 fsync,P99 写入延迟可轻易突破 100ms——异步刷盘是默认工程选择。
阅读完本节,你应当能够:
关系型数据库每次 INSERT 可能触发 B+ 树页分裂——在 60M points/s 级 IoT 场景下不可接受。TSDB 把写请求转化为顺序 append:WAL 与 SSD 顺序带宽对齐,MemTable 按时间归位,Compaction 后台合并小 block。2023 年 CNCF《Time-Series Databases Landscape》称:超 50 万点/秒且 P99 查询 <200ms 的生产场景,原生架构 CPU/GB/Query 效率平均高出混合方案 3.7 倍——差距来自是否尊重「写入递增、查询近端」规律。
为什么「顺序写」这么重要?机械盘时代磁盘随机写与顺序写带宽可差百倍;SSD 时代差距缩小,但块擦写与写放大仍在。更重要的是,顺序写天然适合批量合并:MemTable 攒一批同 series 的样本,一次落盘就能生成一个紧凑的列存块,后续查询与压缩都受益。这就是 TSDB 把「INSERT 语义」重构成「append 语义」的根本动机。
| 层级 | 职责 | Prometheus | InfluxDB | QuestDB |
|---|---|---|---|---|
| 协议 | 解析与批处理 | Remote Write protobuf | Line Protocol | ILP + PG wire |
| 内存 | series 归位 | head block 哈希表 | shard MemTable | 列式 mem table |
| 持久 | 顺序落盘 | WAL + snapshot | WAL + TSM | WAL + mmap 分区 |
协议层:Influx 一行 cpu,host=web01 usage=0.73 1682753400000000000 仅字符串切分;客户端应 1000 点/批 发送——RTT 成本约为单点的 1/1000。
解析一行 line protocol 的大致过程:
# 以 Influx line protocol 为例的解析示意 line = "cpu,host=web-01,region=cn usage=0.73,idle=0.27 1717023456789000000" measurement, rest = line.split(",", 1) # measurement = "cpu" tag_part, field_part, ts = rest.split(" ", 2) # 拆分三个部分 tags = dict(kv.split("=") for kv in tag_part.split(",")) fields = dict(kv.split("=") for kv in field_part.split(","))
注意这里没有正则、没有数据库查找,全是线性字符串处理,因此单行解析是 O(长度) 的。真正的开销在协议层之后:为每个新 series 在内存里登记 identity、把样本插入到该 series 的样本数组。
内存层:Prometheus 在内存维护 (metric, labels) → []sample;Influx shard 内按 series 排序;QuestDB 列式缓冲后批量 commit。
持久层:用户收到 204/200 仅表示进 WAL 缓冲;Flush Daemon 按内存水位(80%)、WAL 大小(128MB)或时间窗(10s)触发 fsync——durability 与 latency 解绑。
一条数据在 WAL 中的生命周期大致四个阶段:追加进 WAL 缓冲(返回 204)→ Flush Daemon 触发 fsync(落盘保证)→ 内存块刷成 TSM/block 文件(数据进主存储)→ 旧 WAL 段回收。其中只有第一步是客户端可感知的,其余都在后台异步完成。
查询 cpu{host="a"}[1h] 的路径:
host=a → series ID 集合倒排索引的定位过程可以简化为两步:
倒排表(简化示意) host=a → [series_1, series_3, series_5] region=cn → [series_1, series_2, series_5] service=api → [series_3, series_5] 查询 host=a AND service=api → 交集 [series_3, series_5]
高基数:标签组合数 = series 数。倒排 posting list 与内存索引随 series 线性涨——InfluxData 2023 报告:基数超 50 万 时 P99 查询延迟平均升 3.7 倍。

| 产品 | 写入强项 | 索引特点 |
|---|---|---|
| Prometheus | 本地 head 极低延迟 | 内存倒排 + posting |
| InfluxDB | shard 水平扩展 | TSI 倒排 + TSM 时间文件 |
| TimescaleDB | SQL COPY 批量 | B-tree/BRIN on chunk |
| QuestDB | 百万行/s ingest | symbol 列 + 时间分区 |
批量写入:Telegraf metric_batch_size=1000;Prometheus Remote Write 默认批处理。
Cardinality 治理:metric_relabel_configs drop 高基数 label;勿把 pod_uid、trace_id 作 label。
fsync 策略:金融闭环控制若要求 RPO=0,需显式配置同步刷盘——接受吞吐下降。
当写入开始变慢,按下面顺序排查,比直接调参数更有效:先看 WAL 队列积压(write_queue_length),积压说明下游消费慢;再看 fsync 延迟(wal_fsync_duration_seconds),偏高说明磁盘 IOPS 到顶;然后看是否新 series 剧增(head_series),突增说明基数问题而非容量问题;最后才考虑扩 shard。这个顺序把「容量」与「基数」两类根因区分开,避免为基数问题错误地采购硬件。
补充一个常见的性能误判:很多人把「写入变慢」直接归因于磁盘,实际上在高基数场景下,写入路径里最贵的一步往往是「为新 series 登记 identity 并更新倒排索引」——它在内存里做,不碰磁盘。所以当 series 数翻倍而磁盘 IO 正常时,问题几乎一定在内存索引侧。判断技巧是看两条曲线:head_series 与写入延迟是否同步上涨。同步涨,先治基数;不同步,再查磁盘与 WAL。
⚠️ 常见坑:每次写入
syncWAL——NVMe 上 fsync 仍可能数 ms,P99 写入轻松超 100ms。
💡 关键直觉:索引跳转次数主导 P99——IoT 场景 92.7% 查询延迟在索引定位阶段(TSDB Benchmark Consortium 2024)。
下一节讲 retention 与冷热分层如何控制长期成本。