时序数据库原理
问题
时序数据库的核心原理是什么?它如何实现高写入和高压缩?
答案
一、时序数据模型
时序数据由以下三个要素组成:
Metric(度量名)+ Tags(标签/维度) + Fields(值) + Timestamp(时间戳)
示例:
cpu_usage, host=server1, region=us-east | value=85.3 | 2024-01-15T10:30:00Z
cpu_usage, host=server2, region=us-west | value=72.1 | 2024-01-15T10:30:00Z
| 要素 | 说明 | 示例 |
|---|---|---|
| Metric | 测量指标名称 | cpu_usage、http_requests_total |
| Tags | 维度标签(用于过滤和分组) | host=server1、region=us-east |
| Fields | 实际测量值 | value=85.3、count=1024 |
| Timestamp | 数据采集时间 | 2024-01-15T10:30:00Z |
Metric + Tags 的唯一组合构成一个 Series(时间线)。例如 cpu_usage{host=server1, region=us-east} 就是一条时间线。
时序数据库的核心挑战之一是管理海量的时间线(Series Cardinality)。
二、存储引擎
时序数据库通常使用基于 LSM-Tree 的变体作为存储引擎,针对时序特征做了大量优化。
TSM(Time-Structured Merge Tree)
TSM 是 InfluxDB 使用的存储引擎,本质是 LSM-Tree 的时序优化版:
写入流程
- WAL:写入先追加到 WAL 文件(保证持久性)
- Cache:数据缓存在内存中(按 Series 分组排序)
- Flush:内存达到阈值后,刷写到磁盘生成 TSM 文件
- Compaction:后台合并小文件,去重,压缩
为什么写入快?
- 追加写:时序数据天然按时间递增,所有写入都是 Append-Only
- 批量写:通常 SDK 会攒批(batch)后一次写入
- 无更新:时序数据极少修改,避免了 Read-Modify-Write
三、列式存储
传统行式存储按行存储数据,时序数据库按列存储:
行式存储(MySQL):
Row1: [timestamp, host, cpu, memory]
Row2: [timestamp, host, cpu, memory]
列式存储(TSDB):
timestamp列: [t1, t2, t3, t4, ...]
cpu列: [85.3, 72.1, 90.5, ...]
memory列: [4096, 8192, 4096, ...]
列式存储的优势:
- 高压缩比:同一列的数据类型相同,压缩率极高
- 快速聚合:
SUM(cpu)只需读取 cpu 列,不读其他列 - 按需读取:查询只读取需要的列
四、数据压缩
时序数据有很强的时间局部性(相邻时间点的值变化不大),可以实现极高的压缩比。
时间戳压缩:Delta-of-Delta
原始时间戳: 1000, 1010, 1020, 1030, 1040
Delta: -, 10, 10, 10, 10
Delta-of-Delta: -, -, 0, 0, 0
等间隔采集的时间戳,Delta-of-Delta 几乎全是 0,只需要 1 bit 编码。
浮点数压缩:XOR 编码(Gorilla 压缩)
相邻值:85.3, 85.5, 85.2, 85.4
XOR: 很多前导零/尾部零
→ 只存储有效位,通常每个值只需 1~2 字节
Facebook 的 Gorilla 论文提出了 XOR 编码,利用相邻浮点数的二进制表示高度相似的特性,压缩比可达 10:1 ~ 20:1。
压缩效果对比
| 数据类型 | 原始大小 | 压缩后 | 压缩比 |
|---|---|---|---|
| 时间戳(10s 间隔) | 8B | ~0.1B | 80:1 |
| 浮点数(缓慢变化) | 8B | ~1B | 8:1 |
| 整数(计数器) | 8B | ~2B | 4:1 |
| 字符串(标签) | 变长 | 字典编码 | 5~10:1 |
五、降采样与数据保留
时序数据量随时间快速增长。降采样(Downsampling)将高精度数据聚合为低精度数据,减少存储开销。
数据保留策略示例:
- 原始数据(1s 粒度):保留 7 天
- 1 分钟聚合:保留 30 天
- 1 小时聚合:保留 1 年
- 1 天聚合:保留 3 年
这种分层保留策略能将存储成本降低 90% 以上。
六、时序查询特点
-- 典型时序查询:过去 1 小时的平均 CPU 使用率,按主机分组
SELECT
time_bucket('5 minutes', time) AS bucket,
host,
AVG(cpu_usage) AS avg_cpu,
MAX(cpu_usage) AS max_cpu
FROM metrics
WHERE time > NOW() - INTERVAL '1 hour'
AND region = 'us-east'
GROUP BY bucket, host
ORDER BY bucket DESC;
时序查询的共同特征:
- 时间范围过滤:几乎所有查询都带时间范围
- 聚合函数:AVG、MAX、MIN、SUM、COUNT、PERCENTILE
- 分组:按时间窗口和标签维度分组
- 最新数据优先:大部分查询关注最近的数据
常见面试问题
Q1: 时序数据库为什么比 MySQL 快?
答案:
针对时序数据的四个核心优化:
- 追加写入:数据按时间递增,全部是 Append-Only,无随机写
- 列式存储:聚合查询只读需要的列
- 极致压缩:Delta-of-Delta + XOR 编码,压缩比 10~20 倍
- 时间分区:数据按时间分区,查询只扫描相关时间段
MySQL 是通用型 OLTP 数据库,为随机读写优化;TSDB 为顺序写入 + 范围聚合查询优化。
Q2: 什么是 Series Cardinality(时间线基数)问题?
答案:
每个唯一的 Metric + Tags 组合对应一条时间线。如果标签值的组合过多,时间线数量会爆炸。
例如:http_requests{host, path, status_code, user_id}
如果 user_id 有 100 万个不同值 → 100 万条时间线
时间线过多会导致:
- 内存索引膨胀(每条时间线需要索引)
- 查询性能急剧下降
- 这就是「高基数」问题
解决:避免将高基数值(如 user_id、trace_id)作为 Tag,改为 Field 或使用其他存储。
Q3: 时序数据库的数据保留策略如何设计?
答案:
三层策略:
| 层级 | 粒度 | 保留时间 | 用途 |
|---|---|---|---|
| 原始数据 | 秒级 | 7~14 天 | 实时告警、精细排查 |
| 分钟级聚合 | 1~5 分钟 | 30~90 天 | 趋势分析、日报 |
| 小时级聚合 | 1 小时 | 1~3 年 | 长期趋势、容量规划 |
降采样时保留 AVG、MAX、MIN、COUNT 等聚合值,满足不同分析需求。
Q4: Prometheus 和 InfluxDB 的区别?
答案:
| 对比 | Prometheus | InfluxDB |
|---|---|---|
| 数据采集 | Pull 模式(主动拉取) | Push 模式(被动接收) |
| 查询语言 | PromQL | InfluxQL / Flux |
| 适合场景 | 基础设施监控 | 通用时序数据 |
| 集群 | Thanos/Cortex(第三方) | 企业版原生支持 |
| 长期存储 | 需要外部方案 | 原生支持 |
| 生态 | Grafana + AlertManager | Telegraf + Grafana |
Prometheus 更专注于监控告警,InfluxDB 更通用。