时序数据库深度解析

时序数据库(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 但列存性能突出)