时序数据库(Time Series Database,TSDB)是专门为处理时间序列数据而设计的数据库。时间序列数据是以时间戳为索引、按时间顺序排列的数据点序列,在基础设施监控、IoT 传感器、金融行情、运营指标等场景中大量产生。传统关系型数据库在处理这类高写入、高压缩、按时间范围查询的负载时力不从心,时序数据库因此应运而生。本文从核心数据模型出发,深入解析 InfluxDB 的 TSM 存储引擎、Prometheus 的拉取式架构、压缩算法原理,以及高基数问题与查询优化等核心话题。
时序数据库简介
时序数据库的核心价值在于针对时间序列数据的写入、存储和查询进行了深度优化。理解时序数据库,首先要理解时序数据的特点:
- 写多读少:监控指标每隔几秒写入一次,读取频率远低于写入频率
- 按时间追加:几乎所有写入都是最新时间戳的数据,旧数据极少修改
- 时间范围查询:查询通常是"最近1小时"、"过去7天"这样的时间范围查询
- 高压缩率:同一指标的相邻值通常变化很小,具有极高的压缩潜力
- 数据生命周期明确:通常只保留一段时间的明细数据,旧数据自动过期或降精度
典型应用场景
基础设施监控:服务器的 CPU 使用率、内存占用、网络流量、磁盘 I/O 等指标每隔 15 秒采集一次,一个中型公司可能有数千台服务器,每天产生数十亿个数据点。Prometheus + Grafana 是这类场景的标准组合。
IoT 物联网:工厂的传感器、智能电表、车联网设备持续上报温度、压力、电量、位置等数据。设备数量可能达到百万级,写入吞吐量极高,对存储效率要求苛刻。InfluxDB 和 TDengine 在这类场景表现突出。
金融行情:股票、期货、外汇的逐笔成交数据(Tick Data)是典型的时序数据,证券交易所每天产生数亿条记录,金融分析需要高精度的时间序列回测能力。
应用性能监控(APM):微服务架构下每个接口的 QPS、P99 延迟、错误率等指标需要实时监控,配合告警系统在异常发生时快速通知。
时序数据的三要素
无论哪种时序数据库,其数据模型都围绕三个核心概念展开:
- 时间戳(Timestamp):数据点的时间,通常精确到毫秒或纳秒。是时序数据的第一维度,也是最重要的索引字段。
- 标签(Tag / Label):描述数据来源的维度信息,如 host=server01、region=us-east、job=mysql。标签是低基数(Low Cardinality)的离散值,会被建立索引,支持按标签过滤和分组聚合。
- 字段值(Field / Value):实际的测量值,如 cpu_usage=72.3、memory_free=1024。字段值通常是数值型,不建索引,支持数学运算和聚合。
核心数据模型
时序数据库的数据模型是理解其存储和查询行为的基础。不同数据库在术语上有差异,但核心概念高度一致。
序列与数据点
在 InfluxDB 中,一条时间序列(Series)由 Measurement(指标名,类似关系数据库的表名)+ Tag Set(所有标签的组合)唯一确定。每个序列对应一条时间线,沿时间轴不断追加新的数据点。
-- InfluxDB Line Protocol 格式
-- measurement,tag_key=tag_value field_key=field_value timestamp
cpu_usage,host=server01,region=us-east usage_idle=21.3,usage_user=72.5 1713340800000000000
cpu_usage,host=server02,region=us-west usage_idle=35.1,usage_user=60.2 1713340800000000000
memory,host=server01,region=us-east free=1073741824,used=3221225472 1713340800000000000
上面的例子中,cpu_usage,host=server01,region=us-east 是一条序列,cpu_usage,host=server02,region=us-west 是另一条序列,两者共享同一个 Measurement 名称,但 Tag Set 不同。
在 Prometheus 中,等价的概念是指标(Metric)和标签集(Label Set):
# Prometheus 指标格式
# metric_name{label_key="label_value",...} value [timestamp]
cpu_usage_idle{host="server01", region="us-east"} 21.3 1713340800
cpu_usage_idle{host="server02", region="us-west"} 35.1 1713340800
node_memory_free_bytes{host="server01"} 1073741824 1713340800
序列基数问题
序列基数(Series Cardinality)是时序数据库最重要的性能指标之一,指数据库中唯一时间序列的数量。序列基数 = 所有可能的 Tag 值组合数量。
以一个监控系统为例:
- 1000 台服务器(host tag)× 50 种指标(metric)= 50,000 个序列,这是正常规模
- 如果再加上 pod_id tag(Kubernetes 环境中 Pod 频繁创建销毁),pod 数量达到 10 万,基数可能暴增至数十亿
高基数会导致:
- 内存占用暴增:InfluxDB 将所有序列的索引(倒排索引 + tag 索引)存储在内存中,高基数直接导致 OOM
- 写入性能下降:每次写入都要查找或创建序列,高基数使序列查找开销剧增
- 查询变慢:按 tag 过滤需要先在索引中定位序列,高基数的 tag 索引查找耗时增加
主流时序数据库对比
目前市场上主流的时序数据库各有侧重,适用场景有所不同。
数据库特性对比
InfluxDB:InfluxData 公司开发,Go 语言实现,是目前最流行的时序数据库之一。提供专有的 TSM 存储引擎,原生支持 InfluxQL 和 Flux 查询语言,内置 Retention Policy、Continuous Query 等时序特性。商业版支持集群,开源版仅单机。InfluxDB 3.0 已用 Apache DataFusion + Apache Parquet 重构,性能大幅提升。
Prometheus:云原生监控的事实标准,采用 Pull 模式采集指标,自带高效的 TSDB 存储引擎,PromQL 表达能力极强,与 Kubernetes 生态深度融合。Prometheus 设计上是单机的,高可用通过 Thanos/Cortex/VictoriaMetrics 等方案实现。
OpenTSDB:基于 HBase 的分布式时序数据库,天然具备 HBase 的水平扩展能力,适合超大规模数据。但 HBase 运维复杂,查询语言较弱,且 HBase 架构本身的延迟不适合低延迟查询场景。
TimescaleDB:基于 PostgreSQL 扩展的时序数据库,兼容完整的 SQL 语法和 PostgreSQL 生态,对已有 PostgreSQL 用户友好。通过超表(Hypertable)和自动分区实现时序优化,支持连续聚合(Continuous Aggregates)。
TDengine:国产时序数据库,涛思数据开发,针对 IoT 场景深度优化,采用"一个设备一张表"的超级表(Super Table)模型,写入吞吐量极高,并内置消息队列、流计算能力。
| 数据库 | 存储引擎 | 查询语言 | 集群 | 适用场景 |
|---|---|---|---|---|
| InfluxDB | TSM(自研) | InfluxQL / Flux | 企业版 | 监控、APM、IoT |
| Prometheus | TSDB(自研) | PromQL | 借助第三方 | K8s 监控 |
| OpenTSDB | HBase | HTTP API | 原生 | 超大规模数据 |
| TimescaleDB | PostgreSQL 扩展 | SQL | 企业版 | 复杂 SQL 分析 |
| TDengine | 自研列存 | SQL 方言 | 原生 | IoT、工业数据 |
InfluxDB 深度解析
InfluxDB 是最具代表性的时序数据库,其 TSM 存储引擎的设计思路对整个时序数据库领域影响深远。
TSM 存储引擎
TSM(Time-Structured Merge Tree)是 InfluxDB 自研的存储引擎,参考了 LSM-Tree(Log-Structured Merge Tree)的设计,但针对时序数据做了深度定制。TSM 引擎由四个核心组件构成:
WAL(Write-Ahead Log):所有写入首先追加到 WAL 文件,保证数据持久性。WAL 是纯追加写,写入速度极快。WAL 文件按 Segment 分割,每个 Segment 最大 10MB。
Cache:WAL 数据同时写入内存中的 Cache 结构,Cache 是一个按序列组织的有序 Map,支持对最新数据的快速读取。当 Cache 超过阈值(默认 25MB)时,触发 Snapshot 将 Cache 刷入磁盘,生成新的 TSM 文件。
TSM 文件:磁盘上的持久化存储文件,采用列式存储,每个文件内按序列存储压缩后的时间戳和字段值块(Block)。TSM 文件一旦写入就不再修改(Immutable),类似 SSTable。
Compaction(合并压实):后台进程持续将小的 TSM 文件合并为更大的文件,减少文件数量,提升读取性能,并清除过期数据(Retention Policy 到期的数据点)。
# InfluxDB 存储引擎配置(influxdb.conf)
[data]
# WAL 目录
wal-dir = "/var/lib/influxdb/wal"
# 数据目录(TSM 文件)
dir = "/var/lib/influxdb/data"
# Cache 大小上限,超过后触发 Snapshot
cache-max-memory-size = "1g"
# Cache Snapshot 触发阈值
cache-snapshot-memory-size = "25m"
# Cache 写入 WAL 超时
cache-snapshot-write-cold-duration = "10m"
# Compaction 并发度
compact-full-write-cold-duration = "4h"
# TSM 文件大小上限(合并目标)
max-concurrent-compactions = 0
TSM 文件格式
TSM 文件由以下部分组成:
- Header:4 字节魔数 + 1 字节版本号
- Blocks:数据块,按序列存储压缩后的时间戳和字段值。每个 Block 对应一个序列的一段时间范围内的数据点
- Index:每个序列在文件中的位置索引(offset + size),以及该序列在此文件中的时间范围(min_time, max_time)
- Footer:Index 的起始偏移量
查询时,InfluxDB 首先从 Index 定位到目标序列的 Block 位置,再解压读取 Block 中在查询时间范围内的数据点,充分利用时间范围索引跳过不相关的 Block。
InfluxQL 与 Flux 查询
InfluxDB 提供两种查询语言:InfluxQL(类 SQL)和 Flux(函数式脚本语言)。
-- InfluxQL:查询 server01 最近 1 小时的 CPU 使用率,每 5 分钟降采样
SELECT MEAN(usage_user) AS cpu_mean
FROM cpu_usage
WHERE host = 'server01'
AND time >= now() - 1h
GROUP BY time(5m)
ORDER BY time DESC;
-- InfluxQL:按 region 聚合,计算各区域平均内存使用
SELECT MEAN(used) / (MEAN(used) + MEAN(free)) * 100 AS memory_pct
FROM memory
WHERE time >= '2026-04-17T00:00:00Z'
AND time < '2026-04-18T00:00:00Z'
GROUP BY region, time(1h)
FILL(previous);
-- InfluxQL:创建持续查询(Continuous Query),每小时自动降采样写入另一个 measurement
CREATE CONTINUOUS QUERY "cq_cpu_1h" ON "telegraf"
BEGIN
SELECT MEAN(usage_user) AS usage_user_mean,
MAX(usage_user) AS usage_user_max
INTO "telegraf"."rp_1year"."cpu_usage_1h"
FROM "cpu_usage"
GROUP BY time(1h), *
END;
# Flux 查询:更强大的函数式语法(InfluxDB 2.x+)
from(bucket: "telegraf")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "cpu_usage" and r.host == "server01")
|> filter(fn: (r) => r._field == "usage_user")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
|> yield(name: "cpu_mean")
Retention Policy(数据保留策略)
时序数据通常有明确的生命周期:近期数据保留高精度明细,历史数据降精度或删除。InfluxDB 的 Retention Policy(RP)定义了数据的保留时长,超期数据在 Compaction 过程中被自动清除。
-- 创建一个数据库,包含多个 Retention Policy
CREATE DATABASE telegraf;
-- 默认 RP:保留 30 天明细数据,副本数为 1
CREATE RETENTION POLICY "rp_30d" ON "telegraf"
DURATION 30d REPLICATION 1 DEFAULT;
-- 长期 RP:保留 1 年的降采样数据
CREATE RETENTION POLICY "rp_1year" ON "telegraf"
DURATION 365d REPLICATION 1;
-- 修改 RP
ALTER RETENTION POLICY "rp_30d" ON "telegraf"
DURATION 60d REPLICATION 1;
-- 查看所有 RP
SHOW RETENTION POLICIES ON "telegraf";
Prometheus 深度解析
Prometheus 是云原生监控的事实标准,由 SoundCloud 开发,2016 年加入 CNCF,现已成为 Kubernetes 生态中监控的首选方案。
Pull 拉取模型
Prometheus 与大多数监控系统最大的不同在于采用 Pull 模式:Prometheus Server 主动定期拉取(Scrape)各目标暴露的 HTTP metrics 端点,而不是由目标主动推送(Push)。
Pull 模式的优势:
- 易于调试:任何时候都可以手动 curl 目标的 /metrics 端点查看当前指标,无需等待推送
- 服务发现友好:Prometheus 可以通过 Kubernetes、Consul、DNS 等服务发现机制自动发现新目标
- 控制权在 Server 端:采集频率、目标列表均由 Prometheus 统一配置管理
- 故障检测:如果目标不可达,Prometheus 会立即感知(Scrape 失败),而 Push 模式下 Server 无法区分"目标挂了"还是"暂时没有数据"
对于短生命周期的 batch job(无法被 Pull),Prometheus 提供了 Pushgateway 作为中间层,job 完成时 Push 到 Pushgateway,再由 Prometheus 从 Pushgateway Pull。
# prometheus.yml 配置示例
global:
scrape_interval: 15s # 默认采集间隔
evaluation_interval: 15s # 规则评估间隔
# Alertmanager 配置
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
# 告警规则文件
rule_files:
- "rules/alert_rules.yml"
# 采集目标配置
scrape_configs:
# 采集 Prometheus 自身指标
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# 采集 Node Exporter(服务器指标)
- job_name: 'node_exporter'
static_configs:
- targets:
- 'server01:9100'
- 'server02:9100'
- 'server03:9100'
# Kubernetes Pod 动态服务发现
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
Prometheus TSDB 存储
Prometheus 的本地存储是一个高效的 TSDB,其数据组织方式如下:
Head Block(内存块):最近写入的数据(默认最近 2 小时)存放在内存的 Head Block 中,支持高速写入和低延迟读取。Head Block 也有 WAL 保证持久性。
Chunks:数据在 Head Block 中以 Chunk 形式存储,每个 Chunk 对应一个序列的一段时间内的样本(默认 120 个样本/Chunk)。Chunk 内采用 Gorilla 压缩算法(XOR + delta-of-delta 编码),压缩率极高。
Block(磁盘块):Head Block 每 2 小时切割一次,将内存数据持久化为磁盘上的 Block。每个 Block 是一个目录,包含 Chunks 文件、index 文件(倒排索引)和 meta.json。Block 的时间范围不重叠,互相独立。
Compaction:Prometheus 后台会将小的 Block 合并为大的 Block,减少查询时需要扫描的文件数量。较旧的 Block 合并后时间范围变大(如 2h → 6h → 24h)。
PromQL 查询语言
PromQL 是 Prometheus 的查询语言,以其简洁而强大的表达能力著称。
# 瞬时向量(Instant Vector):当前时刻的指标值
http_requests_total{job="api-server", status="200"}
# 区间向量(Range Vector):过去 5 分钟内的所有样本
http_requests_total{job="api-server"}[5m]
# 速率计算:每秒请求数(对 Counter 类型指标求增长率)
rate(http_requests_total{job="api-server"}[5m])
# 聚合:按 job 和 status 汇总所有实例的请求速率
sum by (job, status) (
rate(http_requests_total[5m])
)
# P99 延迟(使用 histogram_quantile)
histogram_quantile(0.99,
sum by (le, job) (
rate(http_request_duration_seconds_bucket[5m])
)
)
# CPU 使用率(1 - 空闲率)
1 - avg by (instance) (
rate(node_cpu_seconds_total{mode="idle"}[5m])
)
# 预测:基于线性回归预测磁盘 4 小时后是否会满
predict_linear(node_filesystem_free_bytes[6h], 4 * 3600) < 0
告警规则
# rules/alert_rules.yml
groups:
- name: node_alerts
interval: 30s
rules:
# CPU 使用率持续 5 分钟超过 90%
- alert: HighCPUUsage
expr: |
1 - avg by (instance) (
rate(node_cpu_seconds_total{mode="idle"}[5m])
) > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
description: "CPU usage is {{ $value | humanizePercentage }}"
# 内存可用率低于 10%
- alert: LowMemory
expr: |
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.1
for: 2m
labels:
severity: critical
annotations:
summary: "Low memory on {{ $labels.instance }}"
description: "Available memory: {{ $value | humanize }}"
# 磁盘预计 4 小时内写满
- alert: DiskWillFillUp
expr: |
predict_linear(node_filesystem_free_bytes{fstype!="tmpfs"}[6h], 4*3600) < 0
for: 10m
labels:
severity: critical
annotations:
summary: "Disk will fill up on {{ $labels.instance }}"
存储引擎设计
时序数据库的存储引擎设计是其性能的核心,本节从压缩算法、数据组织和写入路径三个维度深入分析。
为什么 LSM-Tree 适合时序数据
传统 B-Tree 存储引擎(如 InnoDB)在随机写入时性能较差,因为需要定位到页并原地更新,产生大量随机 I/O。时序数据的写入是按时间戳单调递增的顺序写,天然适合 LSM-Tree 的追加写模式:
- 顺序写:所有写入追加到 WAL + 内存,再批量 Flush 为有序的磁盘文件,充分利用磁盘顺序写性能
- 无原地修改:时序数据几乎不修改历史数据,不存在 B-Tree 的"写放大"(Update 需要读-改-写整个页)
- 时间范围 Compaction:LSM-Tree 的 Compaction 过程可以自然地将相邻时间段的数据合并,老数据合并压缩,节省空间
时间戳压缩
时序数据的时间戳通常是等间隔或近似等间隔的,适合使用 delta-of-delta 编码压缩:
# 原始时间戳(秒级 Unix 时间戳)
timestamps: [1713340800, 1713340815, 1713340830, 1713340845, 1713340860]
# 第一步:计算 delta(相邻时间戳之差)
deltas: [-, 15, 15, 15, 15]
# 第二步:计算 delta-of-delta(delta 的差值)
delta_of_deltas: [-, -, 0, 0, 0]
# 如果采集间隔非常规律,大量 delta-of-delta 为 0,
# 用 variable-length integer 编码后压缩率极高
# 原始 5 * 8 = 40 字节,编码后可能只需 4-5 字节
浮点值压缩:Gorilla 算法
Gorilla 是 Facebook 2015 年论文中提出的时序数据压缩算法,被 Prometheus(Chunks)和 InfluxDB(TSM Block)广泛采用,核心思想是对相邻浮点数进行 XOR 运算:
# 相邻监控值通常变化很小
values: [72.3, 72.5, 72.1, 72.4, 72.3]
# 将 float64 作为 64 位整数处理,对相邻值做 XOR
# XOR 后,高位相同的位都变为 0
# 对于变化很小的浮点数,XOR 结果的前导零(leading zeros)很多
# 只存储 XOR 结果中有意义的位(meaningful bits),实现压缩
# 典型压缩率:
# - 完全不变的值(如某些状态指标):每个样本约 1 bit
# - 正常监控指标:每个样本约 1.37 字节(原始 8 字节,压缩率约 17%)
乱序写入处理
在分布式采集场景中,数据可能以非严格递增的时间戳到达(网络延迟、时钟偏差)。时序数据库对乱序写入的处理策略:
- InfluxDB:支持有限的乱序写入,允许写入时间戳早于当前写入窗口但在 Retention Policy 范围内的数据。但乱序数据不能直接追加到 WAL,需要触发额外的 Out-of-Order Compaction。
- Prometheus:Head Block 默认不接受超过 1 小时的乱序数据。Prometheus 2.39 引入了 Out-of-Order Ingestion 特性,支持配置乱序允许窗口(
out_of_order_time_window)。 - ClickHouse(时序场景):MergeTree 引擎的 Compaction 过程会对数据重新排序,对乱序容忍度更高,但 Compaction 前的乱序数据会影响查询性能。
高基数问题
高基数(High Cardinality)是时序数据库生产环境中最常见的"地雷",很多性能问题和 OOM 都源于此。
高基数的成因
任何可能取大量不同值的 Tag 都会导致高基数问题,常见的"高基数 Tag"包括:
- 用户 ID / 请求 ID:如果将 user_id 作为 Tag,则序列数量 = 用户数量,几亿用户就是几亿条序列
- Pod ID:Kubernetes 中 Pod 频繁创建和销毁,Pod name(通常包含随机 hash)作为 Tag 会产生大量"僵尸序列"
- 时间戳作为 Tag:这是新手最常犯的错误,将时间戳放入 Tag 而不是 Field,会造成无限膨胀的基数
- URL 路径:如果 URL 包含动态参数(如 /api/users/12345),不经过规范化直接作为 Tag,会产生极高基数
高基数的危害
以 InfluxDB 为例,序列索引(Series Index)存储在内存中。每个序列约占用 300-500 字节内存(Tag Key-Value 字符串 + 倒排索引条目)。
- 100 万序列 ≈ 300MB~500MB 内存(可以接受)
- 1 亿序列 ≈ 30GB~50GB 内存(通常导致 OOM)
- 10 亿序列 ≈ 无法承载
Prometheus 的情况类似,每个活跃序列在内存中需要维护 Head Block 相关状态,高基数直接导致 Prometheus 进程 OOM 重启。
高基数解决方案
Schema 设计时避免高基数 Tag:这是根本解决方案。将高基数字段放入 Field 而不是 Tag。例如:
# 错误做法:将 request_id 作为 Tag(无限基数)
http_request,method=GET,path=/api/users,request_id=abc123 duration_ms=45
# 正确做法:request_id 放入 Field,只做记录,不用于索引
http_request,method=GET,path=/api/users duration_ms=45,request_id="abc123"
URL 规范化:将动态路径参数替换为占位符,再作为 Tag:
# 原始 URL(高基数)
path="/api/users/12345/orders/67890"
# 规范化后(低基数)
path="/api/users/{id}/orders/{order_id}"
序列修剪(Series Pruning):定期删除长期无新数据写入的"僵尸序列"(Dead Series)。InfluxDB 通过 Retention Policy 自动删除过期数据;Prometheus 通过 --storage.tsdb.retention.time 控制数据保留时间,过期的 Block 被整体删除,其中包含的序列自然消失。
预聚合(Pre-aggregation):对高基数数据在写入时进行降维聚合,只存储聚合结果而非明细。例如将数百万个用户的行为事件,在写入时按分钟聚合为各行为类型的计数,大幅减少序列数量。
使用支持高基数的系统:部分时序数据库对高基数有更好的支持,如 VictoriaMetrics 采用基于磁盘的倒排索引而非内存索引,对高基数场景更友好;ClickHouse 虽然不是专用时序数据库,但其列存 + MergeTree 引擎在高基数场景下表现优秀。
查询优化
时间范围查询优化
时间范围查询是时序数据库最常见的查询模式,各数据库针对此类查询有专门的优化:
时间分区:数据按时间范围分区存储(InfluxDB 的 Shard,Prometheus 的 Block),查询时先确定时间范围对应的分区,只扫描相关分区的数据,跳过大量不相关数据。
Block-level 索引:TSM 文件和 Prometheus Block 都在文件/块级别记录了时间范围(min_time, max_time),查询引擎可以快速跳过不相关的文件块。
降采样与预聚合
对于长时间跨度的查询(如"过去1年每天的平均值"),直接查询原始数据会扫描海量数据点,性能较差。解决方案是降采样(Downsampling):将高精度原始数据聚合为低精度的汇总数据,查询时根据时间跨度自动选择合适精度的数据。
-- InfluxDB:使用 Continuous Query 实现自动降采样
-- 每 5 分钟执行一次,将原始数据降采样为 1 小时粒度
CREATE CONTINUOUS QUERY "downsample_cpu_1h"
ON "telegraf"
RESAMPLE EVERY 5m FOR 2h
BEGIN
SELECT
MEAN(usage_user) AS usage_user_mean,
MAX(usage_user) AS usage_user_max,
MIN(usage_user) AS usage_user_min,
PERCENTILE(usage_user, 95) AS usage_user_p95
INTO "telegraf"."rp_2years"."cpu_usage_1h"
FROM "telegraf"."rp_30d"."cpu_usage"
GROUP BY time(1h), *
END;
-- 查询时指定降采样的 measurement(1 年跨度只需查 cpu_usage_1h)
SELECT usage_user_mean
FROM "telegraf"."rp_2years"."cpu_usage_1h"
WHERE host = 'server01'
AND time >= '2025-04-17T00:00:00Z'
AND time < '2026-04-17T00:00:00Z'
GROUP BY time(1d);
# Prometheus:使用 Recording Rules 实现预聚合
# 计算结果存储为新的指标,查询时直接用预聚合结果
groups:
- name: recording_rules
interval: 1m
rules:
# 预聚合:每分钟计算各 job 的请求速率
- record: job:http_requests_total:rate5m
expr: |
sum by (job) (
rate(http_requests_total[5m])
)
# 预聚合:每分钟计算 P99 延迟
- record: job:http_request_duration_p99:rate5m
expr: |
histogram_quantile(0.99,
sum by (job, le) (
rate(http_request_duration_seconds_bucket[5m])
)
)
Tag 索引与查询加速
时序数据库对 Tag 建立倒排索引,使得按 Tag 过滤的查询可以快速定位到相关序列而无需全表扫描。InfluxDB 的序列索引(TSI,Time Series Index)结构:
- Measurement 索引:记录所有 Measurement 名称
- Tag Key 索引:每个 Measurement 下的所有 Tag Key
- Tag Value 索引:每个 Tag Key 下的所有 Tag Value,以及对应的 Series Keys(倒排列表)
按 Tag 查询流程:先在 Tag Value 索引中找到所有匹配的 Series Key,再在 TSM 文件的 Index 中查找这些序列的数据位置,最后读取数据块。整个过程无需全量扫描,时间复杂度接近 O(log N)。
集群与高可用
InfluxDB 集群方案
InfluxDB 开源版(OSS)仅支持单机部署,集群功能(InfluxDB Enterprise)为商业版特性。企业版采用以下架构:
- Meta Node(元数据节点):3 个 Meta Node 组成 Raft 集群,存储数据库元数据(Shard 分布、RP 配置、用户权限等),保证强一致性
- Data Node(数据节点):多个 Data Node 存储实际数据,数据以 Shard 为单位分配,每个 Shard 默认有 2~3 个副本分布在不同 Data Node 上
- Shard 分配策略:每个 Shard 覆盖一段连续的时间范围(默认 7 天),写入时根据时间戳路由到对应的 Shard 所在的 Data Node
Prometheus 高可用方案
Prometheus 原生不支持集群,高可用方案通常是:
双写 + Dedup:部署两个完全相同的 Prometheus 实例,同时拉取相同的目标。查询层(如 Thanos Query)对两个实例的结果去重。简单但存储开销翻倍。
Thanos:CNCF 毕业项目,为 Prometheus 提供全局查询视图、长期存储(对接 S3/GCS 等对象存储)和数据压缩(Compactor)能力。Thanos Sidecar 随每个 Prometheus 实例部署,将本地数据上传到对象存储,实现数据持久化和跨实例全局查询。
VictoriaMetrics:高性能时序数据库,兼容 Prometheus 的 remote_write 协议,原生支持集群模式,可作为 Prometheus 的长期存储和扩展方案。
# Prometheus remote_write 配置:将数据写入远端长期存储
remote_write:
- url: "http://victoriametrics:8428/api/v1/write"
queue_config:
max_samples_per_send: 10000
max_shards: 200
capacity: 2500
remote_timeout: 30s
# Thanos Sidecar 配置(以 GCS 为例)
# thanos sidecar
# --tsdb.path=/prometheus/data
# --prometheus.url=http://localhost:9090
# --objstore.config-file=/etc/thanos/bucket.yml
# bucket.yml
type: GCS
config:
bucket: "prometheus-long-term"
service_account: ""
一致性与可用性权衡
时序数据库通常倾向于 AP(可用性 + 分区容错性),在一致性上做了妥协:
- 写入一致性:InfluxDB Enterprise 支持配置写入时等待几个副本确认(consistency level:any/one/quorum/all),默认 quorum(多数副本写入成功即返回)
- 读取一致性:查询时可能读到不同副本的数据,存在短暂的不一致窗口,但最终一致
- CAP 实践:对于监控场景,短暂的数据不一致通常可以接受(监控本身就有采样误差),优先保证写入可用性和查询性能
生产实践
Schema 设计最佳实践
Schema 设计是时序数据库性能的基础,错误的 Schema 几乎无法通过运维手段弥补:
- Tag vs Field 的选择原则:如果一个字段需要用于过滤(WHERE 条件)或分组(GROUP BY),且基数较低(<10万),放入 Tag;否则放入 Field
- Measurement 命名:一个 Measurement 对应一类指标,不要把所有指标混在一个 Measurement 里,也不要为每个指标创建一个 Measurement(浪费 Schema)
- 时间精度一致:明确并统一时间精度(秒/毫秒/微秒/纳秒),混用精度会造成时序混乱
- 避免稀疏数据:如果一个 Measurement 中某些 Field 只有极少数序列有值(大量 null),考虑拆分为多个 Measurement
Retention Policy 与容量规划
合理的 RP 配置可以显著降低存储成本:
# 典型的三层 RP 配置(对应不同时间跨度的查询精度)
# Layer 1: 保留 7 天的 10 秒粒度原始数据(用于故障排查)
# Layer 2: 保留 90 天的 1 分钟粒度降采样(用于近期趋势分析)
# Layer 3: 保留 2 年的 1 小时粒度降采样(用于历史趋势)
# InfluxDB 配置
CREATE RETENTION POLICY "raw_7d" ON "metrics" DURATION 7d REPLICATION 1 DEFAULT;
CREATE RETENTION POLICY "1m_90d" ON "metrics" DURATION 90d REPLICATION 1;
CREATE RETENTION POLICY "1h_2y" ON "metrics" DURATION 730d REPLICATION 1;
# 容量估算公式(InfluxDB)
# 存储空间 = 序列数 × 每序列每天数据点数 × 每点压缩后字节数 × 保留天数
# 示例:10万序列,15秒采集间隔(每天5760点),每点约1字节(压缩后),保留30天
# = 100,000 × 5,760 × 1 × 30 ≈ 17.28 GB(仅原始数据,不含索引)
监控监控系统本身
"监控监控系统"(Meta-monitoring)是生产环境中容易被忽视但至关重要的实践:
# Prometheus 关键自监控指标
# 采集延迟:某个 job 的所有 target scrape 耗时
sum by (job) (scrape_duration_seconds)
# 采集失败数:采集失败的 target 数量(应持续为 0)
up == 0
# WAL 重放时间:Prometheus 重启时重放 WAL 的耗时(越长说明 WAL 积压越多)
prometheus_tsdb_head_chunks_storage_size_bytes
# 序列数量:监控高基数的增长趋势
prometheus_tsdb_head_series
# 查询耗时:p99 查询延迟
histogram_quantile(0.99, rate(prometheus_engine_query_duration_seconds_bucket[5m]))
# InfluxDB 关键指标
# 写入速率
influxdb_write_points_ok_total
# Compaction 状态
influxdb_tsm1_compact_compactions_active
# 序列基数(重点关注)
influxdb_tsm1_engine_series_count
性能调优实践
生产环境中的常见调优策略:
- 批量写入:将多个数据点合并为一个 HTTP 请求批量写入(InfluxDB 建议每批 5000~10000 点),大幅减少网络开销和写入 WAL 的 fsync 次数
- 写入分片:对于超高吞吐场景,在客户端按序列(或时间范围)对多个 InfluxDB 实例进行分片写入,利用多机并行写入能力
- 查询缓存:在 InfluxDB 前加 Grafana + Grafana 的 Query Caching,将相同查询结果缓存一段时间,减少重复计算
- SSD 存储:时序数据库的 Compaction 过程有大量顺序读写,SSD 比 HDD 性能好 5x~10x,生产环境强烈推荐 SSD
- 调整 GOMAXPROCS:InfluxDB 和 Prometheus 都是 Go 实现,确保 GOMAXPROCS 设置为 CPU 核心数,充分利用多核并行能力
总结
时序数据库是大数据生态中不可或缺的一环,专门解决传统数据库在时间序列数据上的性能瓶颈。核心要点总结:
- 数据模型理解是基础:清楚时间戳、Tag(低基数维度,建索引)、Field(高基数度量值,不建索引)的区别,是正确使用时序数据库的前提
- 高基数是头号敌人:生产事故中很大比例是高基数 Tag 导致的内存耗尽。Schema 设计时严格审查每个 Tag 的基数,永远不要将用户 ID、请求 ID、动态路径等高基数字段放入 Tag
- TSM/LSM-Tree 存储的本质:追加写 + 批量 Flush + 后台 Compaction,以写放大换取高吞吐写入和高压缩率,这是时序数据库的核心设计哲学
- 压缩是时序数据库的核心竞争力:delta-of-delta(时间戳)+ Gorilla XOR(浮点值)可将原始数据压缩至原大小的 5%~15%,存储成本是传统数据库的几十分之一
- 降采样是长期存储的标配:原始数据高精度保留 7~30 天,通过 Continuous Query/Recording Rules 自动降采样为更低精度的历史数据,在查询灵活性和存储成本之间取得平衡
- 技术选型建议:K8s 监控首选 Prometheus + Thanos;IoT/高吞吐写入选 InfluxDB 或 TDengine;需要复杂 SQL 分析选 TimescaleDB;超大规模历史数据分析可考虑 ClickHouse(非专用 TSDB 但列存性能突出)