时序数据库
> 介绍时序数据库的定义、诞生背景以及它与传统关系型数据库的核心区别和典型应用场景。 **时序数据库** (Time-Series Database, 简称 ==TSDB==) 是一种专门为处理时间序列数据而优化的数据库。时间序列数据是由*...
时序数据库
时序数据库 (TSDB) 基础入门
本文将介绍时序数据库的定义、诞生背景,并对比其与传统关系型数据库的核心区别及典型应用场景。
核心概念
时序数据库 (Time-Series Database, 简称 TSDB) 是一种专门为处理时间序列数据而优化的数据库。时间序列数据是由带有时间戳的客观数据点组成的序列,通常按时间顺序持续产生。
为什么需要专门的时序数据库?
传统关系型数据库(如 MySQL、PostgreSQL)虽然也能存储带时间戳的数据,但在面对时序数据特有的场景时,往往会暴露出性能瓶颈:
- 极高的写入吞吐量:物联网传感器或服务器集群每秒可能产生数百万个数据点。
- 追加写入为主:时序数据记录的是已发生的历史事实,通常是 Append-only(仅追加) 的,极少涉及更新或删除。
- 时间范围查询需求:最常见的查询是“获取过去一小时的平均 CPU 使用率”,而非查找某条特定记录。
典型应用场景
- IT 基础设施监控:监控服务器集群的 CPU、内存、网络流量(如 Prometheus)。
- 物联网 (IoT):收集传感器设备采集的温度、湿度、车辆轨迹等数据。
- 金融量化分析:记录股票市场的逐笔交易数据、K 线数据等。
主流时序数据库代表
- InfluxDB:最流行的开源 TSDB,生态系统完善。
- Prometheus:云原生监控的标准,内置 TSDB 功能。
- TimescaleDB:基于 PostgreSQL 构建,完美支持 SQL。
graph TD
A[数据源] --> B(高并发追加写入)
A1[IoT 传感器] --> A
A2[应用监控] --> A
A3[金融交易] --> A
B --> C{时序数据库 TSDB}
C --> D(时间范围聚合查询)
D --> E[数据可视化/Grafana]
D --> F[告警系统]
参考链接:
时序数据模型与多维查询
深入解析时序数据的内部结构,包括时间戳、指标、标签和数据字段,帮助你理解其多维数据模型。
时序数据的基本结构
尽管不同数据库的术语有所差异,但一条典型的时序数据记录(Data Point)通常包含以下四个核心维度:
- 时间戳 (Timestamp):数据的绝对时间,是时序数据的主键维度,通常精确到毫秒甚至纳秒。
- 指标名/表名 (Metric/Measurement):描述数据类别,例如
cpu_usage或temperature。 - 标签/维度 (Tags/Labels):键值对格式,用于描述数据源的元数据。标签通常会被建立索引,以便快速过滤和分组计算,例如
host=server01, region=cn-hangzhou。 - 数据值/字段 (Fields/Values):实际测量的值。字段通常不建索引,可以是数值型(整数、浮点数)、布尔型或字符串,例如
value=85.5。
多维数据模型 (Multi-Dimensional Model)
传统数据库多采用扁平的表格结构,而现代时序数据库(如 Prometheus、InfluxDB)采用的是多维数据模型。这意味着同一个指标(Metric)加上不同的标签(Tags)组合,就构成了一个唯一的时间序列(Time Series)。
示例数据结构:
# InfluxDB 的 Line Protocol 格式
measurement,tag_set field_set timestamp
cpu_load,host=server01,region=cn value=0.64 1629811200000000000
cpu_load,host=server02,region=cn value=0.55 1629811200000000000
在这个例子中:
cpu_load是指标 (Metric)host=server01, region=cn是标签 (Tags),会被索引value=0.64是字段 (Field),即实际数值1629811200...是时间戳 (Timestamp)
为什么要区分 Tags 和 Fields?
区分两者的核心在于索引成本。数据库会为 Tags 构建倒排索引,使得 WHERE host='server01' 这样的查询极快;但如果将不断变化的随机数值(如 CPU 负载具体值)当作 Tag,会导致索引数量爆炸,这就是著名的 高基数 (High Cardinality) 问题。
graph TD A[Data Point 数据点] --> B(Timestamp 时间戳) A --> C(Metric/Measurement 指标名) A --> D(Tags/Labels 标签-建索引) A --> E(Fields/Values 字段-不建索引) D --> D1[host=web-server-1] D --> D2[env=production] E --> E1[cpu_usage=85.2] E --> E2[memory_usage=1024]
参考链接:
时序查询与聚合函数分析
探讨时序数据库特有的查询语言和操作模式,重点理解基于时间窗口的聚合分析。
时序查询的核心特征
时序数据库的查询模式与关系型数据库有本质区别,绝大部分查询属于基于时间范围的聚合分析。用户通常不关心某一个特定毫秒的数据点,而是关注“过去一段时间的整体趋势”。
时间窗口 (Time Windows)
在分析连续的时间线时,我们通常会将数据划分到不同的时间窗口中进行聚合:
- 滚动窗口 (Tumbling Window):固定大小、不重叠的窗口。例如,计算每分钟的平均 CPU 使用率(00:00-00:01, 00:01-00:02)。
- 滑动窗口 (Sliding Window):固定大小、有重叠的窗口。例如,每 10 秒钟计算一次过去一分钟的平均值。
常用聚合函数
除了常规的 SUM、AVG、MAX、MIN 外,时序分析中还经常使用特殊的数学函数:
- 百分位数 (Percentiles, 如 P99, P95):在监控系统中至关重要。P99 延迟为 100ms 表示 99% 的请求在 100ms 内完成,相比平均值,它能更好地反映长尾效应。
- 变化率 (Rate/Derivative):计算指标随时间的增长速率。例如,通过对累计总流量(Counter 类型)求导,得出当前的实时带宽。
查询语言示例
PromQL (Prometheus Query Language) 示例:
获取过去 5 分钟内,所有 web 服务器 HTTP 500 错误率的平均值:
rate(http_requests_total{job="web", status="500"}[5m])
SQL 扩展示例 (TimescaleDB): 将数据按 1 小时对齐(滚动窗口),并计算最高温度:
SELECT
time_bucket('1 hour', time) AS bucket,
MAX(temperature)
FROM sensor_data
GROUP BY bucket
ORDER BY bucket DESC;
flowchart LR A[原始数据点] -->|每秒一个点| B(时间窗口划分) B -->|Tumbling Window 1m| C[聚合计算] C --> D1[AVG/MIN/MAX] C --> D2[P99 / P95] C --> D3[Rate变化率] D1 --> E[平滑趋势图] D2 --> E D3 --> E
参考链接:
底层存储引擎与高压缩率原理
剖析时序数据库为了应对海量写入和降低存储成本,在底层存储架构(如 LSM Tree)及数据压缩算法上的核心设计。
为什么不用 B+ 树?
传统关系型数据库多采用 B+ Tree 作为存储引擎。B+ 树适合随机读写,但在时序场景下,源源不断的数据涌入会导致 B+ 树频繁进行节点分裂,产生严重的 写放大 (Write Amplification) 和随机磁盘 I/O,使写入性能急剧下降。
LSM Tree 与 TSM Tree
现代时序数据库普遍采用基于 LSM Tree (Log-Structured Merge-Tree) 的变种架构:
- WAL (Write Ahead Log):写入数据首先追加到日志中,确保宕机不丢数据。
- MemTable / 内存缓存:数据随后写入内存,当积攒到阈值时,直接批量 Dump 到磁盘,将随机写转化为极速的顺序写。
- SSTable / TSM 文件:落盘后的只读数据文件。后台会定期执行 Compaction (合并操作),将小文件合并并清理过期数据。
(注:InfluxDB 针对时序场景定制了 LSM Tree,演进出了专门的 TSM (Time-Structured Merge Tree) 架构。)
列式存储与高压缩率
时序数据非常适合列式存储。因为同一列的数据类型一致,且随时间变化幅度小,这为高效压缩提供了基础:
- Delta-of-Delta 压缩 (针对时间戳):计算相邻时间戳的差值(Delta),再计算差值的差值(Delta-of-Delta),结果往往是 0 或极小的整数,极大地减少了存储空间。
- Gorilla 算法 (针对浮点数):Facebook 开源的算法,通过异或 (XOR) 运算比较相邻的浮点数值,大幅降低存储开销。
graph TD A[客户端写入请求] --> B[WAL 预写日志] A --> C[内存: MemTable] C -->|达到阈值 Flush| D[磁盘: SSTable / TSM File 层级1] D -->|后台 Compaction| E[磁盘: TSM File 层级2] E -->|后台 Compaction| F[磁盘: TSM File 层级3] style C fill:#f9f,stroke:#333,stroke-width:2px style B fill:#ffd,stroke:#333
参考链接:
生命周期管理:降采样与数据保留
探讨如何通过降采样 (Downsampling) 和保留策略 (Retention Policy) 在长期存储成本与查询精度之间取得平衡。
无限数据增长的困境
时序数据具有持续且无限产生的特征。如果以 1 秒的精度存储所有监控数据,几个月内就会消耗掉数百 TB 空间。然而,数据的价值会随时间急剧衰减:
- 最近 1 小时:需要秒级精度,用于故障排查。
- 1 个月前:仅需小时级精度,用于查看趋势。
- 1 年前:仅需天级精度,用于长期容量规划。
降采样 (Downsampling / Continuous Queries)
降采样是一种自动化机制,定期读取高精度数据,利用聚合函数转换为低精度数据,并写入新表。
典型的降采样流程:
- 收集
1秒精度的原始数据,存入raw_data表。 - 配置后台任务:每 5 分钟计算一次平均值,将
1分钟精度的数据写入data_1m表。 - 每 1 小时,基于
data_1m计算聚合值,写入data_1h表。
数据保留策略 (Retention Policy, RP)
结合降采样,TSDB 提供了自动删除过期数据的机制。由于时序数据按时间分片(Time Sharding)存储,过期删除不需要执行低效的 DELETE 语句,而是直接删除底层过期的时间片文件,开销极低。
生命周期组合拳示例:
- 策略 A:
raw_data(秒级) 保留 7 天。 - 策略 B:
data_1m(分钟级) 保留 30 天。 - 策略 C:
data_1h(小时级) 保留 1 年。 通过此机制,数据库可节省 90% 以上的存储空间,且不影响宏观趋势分析。
参考链接:
分布式架构与高基数问题挑战
深入探讨分布式时序数据库的集群架构、分片策略,以及如何应对“高基数 (High Cardinality)”这一终极挑战。
分布式分片策略 (Sharding Strategy)
当单机容量无法支撑海量数据时,时序数据库需扩展为集群。其分片通常采用双维度策略:
- 按时间分片 (Time Sharding):每个分片存储特定时间跨度(如一天)的数据,便于生命周期管理和缩小扫描范围。
- 按哈希/标签分片 (Hash/Tag Sharding):将同一时间段的数据根据 Tags(如设备 ID)哈希到不同节点,实现负载均衡。
核心挑战:高基数问题 (High Cardinality)
基数 (Cardinality) 指集合中不同元素的数量。在时序数据中,总时间序列数量 = 所有索引标签组合的总数。
高基数的灾难场景: 如果在 Tags 中引入了 IP 地址、随机 UUID 或容器 ID 等无界值,时间序列总数会呈指数级爆炸(数亿甚至数十亿级别)。
高基数的危害:
- 倒排索引膨胀:内存无法装下庞大的索引字典,导致严重的内存溢出 (OOM)。
- 查询极慢:底层需要扫描成千上万的零碎序列来合并数据。
优化方案
- 严格的数据建模:禁止将无界的随机值放入 Tags(应放入不建索引的 Fields)。
- 倒排索引优化:如 Prometheus 借鉴了全文检索的倒排结构,InfluxDB 引入了 TSI (Time Series Index) 架构,将庞大的内存索引落盘。
- 分离计算与存储:新一代 TSDB(如 VictoriaMetrics, InfluxDB IOx)将存储卸载到 S3 等对象存储,计算节点无状态化,通过 Parquet 列存格式应对高基数分析。
graph TD A[路由/代理层] --> B(Hash: device_id=A) A --> C(Hash: device_id=B) B --> D[节点1: 昨天数据切片] B --> E[节点1: 今天数据切片] C --> F[节点2: 昨天数据切片] C --> G[节点2: 今天数据切片] style D fill:#f9f,stroke:#333 style F fill:#f9f,stroke:#333
参考链接: