数据库基础体系 · 第 54/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

InfluxDB 时序数据:时间模型、Schema、写入、查询和保留策略

InfluxDB 是面向时序数据的数据库。它的核心对象不是“某一行当前的状态”,而是“某个实体在某个时间点或时间区间内的观测值”。

典型数据包括:

  • 主机 CPU、内存、磁盘和网络指标;
  • 服务请求延迟、错误率和吞吐量;
  • IoT 设备的温度、压力和电量;
  • 业务事件及其发生时间;
  • 应用程序日志中的数值化指标。

使用 InfluxDB 时,最容易出现的问题不是写不进去,而是数据虽然成功写入,却无法高效查询、聚合、保留或解释。原因通常来自四个概念混淆:

  1. measurement、tag、field、timestamp 当成普通关系表的列;
  2. 没有理解 InfluxDB 的 series 与基数;
  3. 只关注写入接口,没有考虑时间戳、重复点和字段类型;
  4. 把存储保留策略误认为查询过滤、删除数据或备份策略。

本文围绕时间模型、Schema、写入、查询和保留策略,说明这些机制之间的因果关系,并区分 InfluxDB 1.x、2.x 与 3.x 的接口和存储边界。


一、先确定版本和部署边界

InfluxDB 的产品形态经历过明显变化。相同的“InfluxDB”名称,在不同版本中的查询语言、存储引擎和管理方式并不完全相同。

版本或产品形态 主要数据模型 常见查询方式 典型保留配置
InfluxDB 1.x measurement、tag、field、timestamp InfluxQL retention policy
InfluxDB OSS 2.x bucket、measurement、tag、field、timestamp Flux;部分版本和场景支持 InfluxQL 兼容接口 bucket retention period
InfluxDB 3 Core 仍接收 Line Protocol,但底层采用列式 Parquet 等组件 SQL、InfluxQL 数据库级 retention period

本文的示例会明确边界:

  • Line Protocol 写入示例:适用于 InfluxDB 的通用写入模型;HTTP 路径和认证参数根据版本不同。
  • Flux 查询示例:以 InfluxDB OSS 2.x 为边界。
  • SQL 查询示例:以 InfluxDB 3 Core 的 SQL 接口为边界。
  • InfluxQL:说明其语义,但不把某个版本的 HTTP 路径误称为所有版本通用。
  • InfluxDB 的写入通常不是关系数据库意义上的跨多表事务。本文不会把批量写入包装成 ACID 事务。

如果团队同时运行多个版本,应把“数据模型语义”和“API 入口”分开管理:前者可以相近,后者不能想当然复用。


二、InfluxDB 的时间模型

2.1 一个数据点是什么

InfluxDB 中,一个数据点通常由以下部分组成:

measurement + tag set + field set + timestamp

例如:

cpu,host=app-01,region=cn-shanghai usage_user=63.2,usage_system=12.7 1710000000000000000

可以拆成:

  • measurement:cpu
  • tags:
    • host=app-01
    • region=cn-shanghai
  • fields:
    • usage_user=63.2
    • usage_system=12.7
  • timestamp:1710000000000000000

在概念上,InfluxDB 把 measurement、tag 集合和时间戳视为定位一组字段值的重要组成部分。

可以把一个点形式化表示为:

P=(m,T,t,F)P = (m, T, t, F)

其中:

  • mm 是 measurement;
  • TT 是 tag 集合;
  • tt 是时间戳;
  • FF 是 field 集合。

例如:

P=(cpu,{host=app-01,region=cn-shanghai},1710000000000000000,{usage_user=63.2,usage_system=12.7})P = ( \text{cpu}, \{ \text{host}=\text{app-01}, \text{region}=\text{cn-shanghai} \}, 1710000000000000000, \{ \text{usage\_user}=63.2, \text{usage\_system}=12.7 \} )

这里的时间戳不是“写入时间”,而是该观测发生的时间。客户端可以显式提供它;如果不提供,服务端通常使用接收时刻,但这取决于具体写入接口和版本语义。


2.2 时间戳精度

Line Protocol 中的时间戳是一个整数,但整数本身没有单位,单位由请求参数或接口上下文决定。常见精度包括:

  • s:秒;
  • ms:毫秒;
  • us:微秒;
  • ns:纳秒。

例如,同一个 Unix 时间点:

1710000000

如果解释为秒,与解释为纳秒,结果相差 10910^9 倍。

以 UTC 时间 2024-03-09T16:00:00Z 为例,其 Unix 秒时间戳约为:

1710000000

若使用纳秒,则是:

1710000000000000000

写入时必须保证三件事一致:

  1. 应用生成的整数确实使用了预期单位;
  2. 请求参数声明了相同精度;
  3. 查询窗口使用的时间表达式与存储时间类型匹配。

时间精度错误通常不会导致 HTTP 请求失败,而会产生更危险的结果:数据被写入一个极远的过去或未来,随后在正常查询窗口内“消失”。


2.3 时间范围和边界

查询经常使用半开区间:

[start,stop)[start, stop)

也就是:

startt<stopstart \leq t < stop

例如查询 10:00:00 到 11:00:00 的数据,通常应写成:

start = 10:00:00
stop  = 11:00:00

而不是把 11:00:00 的点也包含进来。半开区间的好处是相邻窗口不会重复:

[10:00, 11:00)
[11:00, 12:00)

如果两个窗口都使用闭区间,就可能重复统计恰好位于 11:00:00 的点。

Flux 查询示例:

from(bucket: "metrics")
  |> range(start: -1h)
  |> filter(fn: (r) =>
    r._measurement == "cpu" and
    r._field == "usage_user" and
    r.host == "app-01"
  )

这里 range 提供时间范围,filter 再限制 measurement、field 和 tag。对于 OSS 2.x,range 是 Flux 中读取时间范围的关键步骤;没有时间范围,查询可能无法获得预期的存储裁剪效果,某些部署还会拒绝缺少时间范围的查询。

SQL 查询示例,适用于 InfluxDB 3 Core 的 SQL 接口:

SELECT
    time,
    host,
    usage_user
FROM cpu
WHERE time >= now() - INTERVAL '1' HOUR
  AND time < now()
  AND host = 'app-01'
  AND usage_user IS NOT NULL
ORDER BY time;

这里的 time、measurement 名称和字段列的暴露方式属于 InfluxDB 3 的 SQL 语义。不要把这条 SQL 原样发送给只支持 Flux 的 InfluxDB 2.x。


2.4 同一时间点的重复写入

InfluxDB 中常被称为“同一个点”的判定,核心是:

(m,T,t)(m, T, t)

也就是:

  • measurement 相同;
  • tag 集合相同;
  • timestamp 相同。

如果相同的 (measurement, tag set, timestamp) 再写入 field:

  • 已存在的同名 field 通常会被新值覆盖;
  • 新写入的 field 会与原有 field 合并;
  • 不同 field 名称可以共存;
  • 同一个 field 如果前后类型不兼容,可能写入失败。

例如,先写入:

temperature,device=d-01 value=21.5 1710000000000000000

再写入:

temperature,device=d-01 value=22.0,humidity=40.0 1710000000000000000

逻辑结果可能等价于:

temperature,device=d-01 value=22.0,humidity=40.0 1710000000000000000

这里 value 的新值覆盖了旧值,humidity 则被补充。

但以下两条不是同一个点:

temperature,device=d-01 value=21.5 1710000000000000000
temperature,device=d-02 value=22.0 1710000000000000000

因为 tag 集合不同。

这带来一个重要后果:如果生产者重试写入,必须理解它是“幂等覆盖”还是“产生新观测”。使用相同时间戳和相同 tags 的重试,通常不会生成第二个独立样本;如果每次重试都重新生成当前时间戳,则会产生多个点。


2.5 乱序数据与迟到数据

实时采集系统常出现:

  • 设备离线后补报;
  • 消息队列重新投递;
  • 网络延迟导致旧数据后到;
  • 批量导入历史数据。

因此,写入顺序不一定等于事件发生顺序。

“迟到数据能否写入”与“迟到数据能否立即影响查询结果”是两个问题:

  1. 存储层是否接受该时间戳;
  2. 查询时是否读取到了对应文件、缓存或分片;
  3. 下游连续聚合是否已经处理过这个时间范围;
  4. 数据是否已经被保留策略删除。

不要把“写入成功”理解成所有派生统计立即重算。对于含有预聚合、任务或外部 ETL 的系统,迟到数据可能需要重新执行聚合任务。


三、Schema:measurement、tag、field 和 series

InfluxDB 是“无固定关系表结构”的数据库,但这不等于没有 Schema。

更准确地说,InfluxDB 的 Schema 通常由以下部分组成:

  • measurement 命名;
  • tag 键和值的设计;
  • field 键和值的设计;
  • 时间戳精度;
  • field 类型约束;
  • bucket 或数据库的保留周期;
  • 查询和聚合约定。

Schema 可以是显式管理的,也可以在写入过程中隐式形成。隐式形成并不意味着不需要设计。


3.1 Measurement

measurement 可以理解为一类观测的逻辑名称,类似关系数据库中的表名,但两者不完全等价。

例如:

cpu
http_request
temperature

measurement 表示“测量什么”,而不是“每个设备建一张表”。

不推荐这样设计:

cpu_app_01
cpu_app_02
cpu_app_03

因为设备数量增加时,measurement 数量也不断增加,查询、权限和维护都会变复杂。

通常更合理的设计是:

cpu,host=app-01 ...
cpu,host=app-02 ...
cpu,host=app-03 ...

measurement 保持稳定,实体差异通过 tags 表示。

但 measurement 也不应成为万能容器。把 CPU、HTTP 请求、订单状态和温度全部写入一个 measurement,会使字段语义、类型和查询条件混杂,降低可维护性。


3.2 Tag

tag 是字符串键值对,用来描述数据的维度,适合用于过滤、分组和定位。

例如:

host=app-01
region=cn-shanghai
service=checkout
env=prod

一个 tag 集合可以写成:

T={(k1,v1),(k2,v2),,(kn,vn)}T = \{(k_1,v_1),(k_2,v_2),\dots,(k_n,v_n)\}

tag 的典型特点:

  • 通常是字符串;
  • 适合 WHERE 过滤;
  • 适合 GROUP BY 分组;
  • 会参与 series 标识;
  • tag 值变化可能产生新的 series。

适合作为 tag 的值通常是:

  • 主机名;
  • 服务名;
  • 集群名;
  • 可控数量的地域;
  • 环境名;
  • 设备型号。

不适合作为 tag 的值通常是:

  • 请求 ID;
  • 用户 ID;
  • 邮箱地址;
  • URL 全路径;
  • 错误堆栈;
  • 纳秒级唯一事件 ID。

原因不是“字符串不能做 tag”,而是 tag 的值域可能无限增长。


3.3 Field

field 是实际观测值或事件内容,例如:

usage_user=63.2
latency_ms=18.7
success=true
message="timeout"

field 适合保存:

  • 数值;
  • 布尔值;
  • 字符串内容;
  • 需要被聚合的指标;
  • 不用于高基数分组的载荷。

例如:

http_request,service=checkout,method=POST \
latency_ms=18.7,status_code=200,success=true

这里:

  • servicemethod 是 tags;
  • latency_msstatus_codesuccess 是 fields。

如果查询是:

WHERE service = 'checkout'
GROUP BY method

那么 servicemethod 应是 tag。

如果查询是:

percentile(latency_ms)

那么 latency_ms 必须是 field,因为它是被计算的观测值。


3.4 Tag 和 field 的选择不是性能口诀,而是查询代数

常见说法是“低基数放 tag,高基数放 field”。这句话有用,但不完整。

应先观察查询模式:

SELECT mean(latency_ms)
WHERE service = 'checkout'
GROUP BY region

可设计为:

http_request,service=checkout,region=cn-shanghai latency_ms=18.7

因为:

  • service 用于过滤;
  • region 用于分组;
  • latency_ms 用于聚合。

如果把 region 放为 field,查询仍可能能够读取它,但分组和索引过滤通常不如 tag 直接,并且不同版本的查询执行器表现不同。

反过来,如果把 latency_ms 放为 tag:

http_request,service=checkout,latency_ms=18.7 region=cn-shanghai

会产生大量 tag 值,无法有效表达数值范围和百分位计算,还会放大 series 数量。这是数据模型错误,而不只是“性能较差”。


3.5 Series 与基数

一个 series 可以近似理解为:

S=(m,T)S = (m, T)

即 measurement 加上完整 tag 集合。时间戳和 field 值变化不会产生新的 series;tag 值变化会产生新的 series。

假设:

  • measurement 数量为 M=2M=2
  • hostH=100H=100 个值;
  • regionR=3R=3 个值;
  • serviceV=10V=10 个值。

在所有组合都存在的情况下,series 上界为:

Smax=M×H×R×VS_{\max} = M \times H \times R \times V

代入:

Smax=2×100×3×10=6000S_{\max}=2\times100\times3\times10=6000

这是上界,不代表实际一定有 6000 个 series,因为部分组合可能没有数据。

再加入 request_id,假设每天有 1 亿个不同值,则理论组合会急剧膨胀。即使某些存储引擎能够写入,索引、内存、查询规划、压缩和 compaction 的成本也会受到影响。

“高基数”不是 tag 值多就必然错误,而是 tag 组合数量接近或超过系统能够稳定管理的范围。判断时要考虑:

  • 活跃 series 数量;
  • 历史 series 数量;
  • 每个 measurement 的 field 数量;
  • 查询是否常按这些 tags 过滤;
  • 数据写入速率;
  • 单机或集群资源;
  • InfluxDB 具体版本和存储引擎。

四、Line Protocol:写入格式和解析规则

4.1 基本语法

Line Protocol 的基本形式是:

measurement[,tag_key=tag_value...] field_key=field_value[,...] [timestamp]

例如:

cpu,host=app-01,region=cn-shanghai usage_user=63.2,usage_system=12.7 1710000000000000000

三个区域由空格分隔:

  1. measurement 和 tags;
  2. fields;
  3. 可选 timestamp。

如果 tag 值或 measurement 名称包含空格、逗号或等号,需要按 Line Protocol 规则转义。field 字符串值需要使用双引号。

例如:

weather,location=shanghai description="light rain",temperature=18.5 1710000000000000000

其中:

  • description 是字符串 field;
  • temperature 是浮点 field。

整数 field 通常使用 i 后缀:

http_request,service=checkout status_code=200i,success=true 1710000000000000000

不加 i 时,200 通常按浮点数解释,而不是整数。


4.2 字段类型必须稳定

同一个 measurement 中,同一个 field key 在相同时间范围或存储分片中不能任意改变类型。

例如先写:

temperature,device=d-01 value=21.5

随后写:

temperature,device=d-01 value="unknown"

这会带来字段类型冲突,具体错误范围与版本和分片边界有关。不要依赖“换一个时间点就一定能改变类型”来设计 Schema。

更安全的做法是:

temperature,device=d-01 value=21.5,status="unknown"

也就是:

  • value 永远是数值;
  • status 永远是字符串。

如果历史数据已经混用了类型,应通过新 measurement、字段重命名或离线迁移修复,而不是在查询层不断猜测类型。


4.3 写入时的逗号、空格和转义

下面的写法容易失败:

weather,location=shanghai temperature=18.5, description="rain"

fields 区域中出现不必要的空格,可能被解析为 timestamp 或非法内容。

正确形式应是:

weather,location=shanghai temperature=18.5,description="rain"

如果 measurement 是:

service latency

则空格必须转义:

service\ latency,env=prod latency_ms=18.5

如果 tag 值包含逗号:

location=cn,shanghai

必须写成:

location=cn\,shanghai

实际生产系统不应手工拼接未经转义的字符串。应使用官方客户端库或可靠的 Line Protocol 编码器,尤其是当 tag 和 field 来自用户输入时。


五、端到端写入示例

5.1 InfluxDB OSS 2.x 的 HTTP 写入

下面以 InfluxDB OSS 2.x 的 /api/v2/write 为例。前置条件:

  • 已创建组织,例如 acme
  • 已创建 bucket,例如 metrics
  • 已获得具有写入权限的 token;
  • InfluxDB 服务监听在 http://localhost:8086
  • 时间戳使用纳秒。
curl --request POST \
  "http://localhost:8086/api/v2/write?org=acme&bucket=metrics&precision=ns" \
  --header "Authorization: Token ${INFLUX_TOKEN}" \
  --header "Content-Type: text/plain; charset=utf-8" \
  --data-binary @- <<'EOF'
cpu,host=app-01,region=cn-shanghai usage_user=63.2,usage_system=12.7 1710000000000000000
cpu,host=app-02,region=cn-shanghai usage_user=41.8,usage_system=9.4 1710000000000000000
EOF

正常接受时,HTTP 响应通常是无正文的成功响应。失败时应检查:

  • HTTP 状态码;
  • 响应正文;
  • 服务端日志;
  • bucket、org 和 token 是否匹配;
  • Line Protocol 是否有解析或类型错误。

写入成功只表示服务端接受了该请求或其中可接受的内容,不应等同于“业务事件已经在所有下游派生结果中可见”。


5.2 写入批次和错误处理

写入端一般会批量发送多行数据。批量的价值是减少网络请求和协议开销,但批量越大,失败重试的代价越高。

客户端应区分:

  • 网络超时:服务端可能已经收到数据,盲目重试可能产生重复写入;
  • HTTP 4xx:通常是认证、参数、格式或数据问题,不能简单重试;
  • HTTP 5xx:可能是服务端暂时故障,可以退避重试,但仍需考虑请求是否已被处理;
  • 部分批次错误:必须根据具体 API 和客户端库的语义处理,不能假设整批必然原子成功。

InfluxDB 的普通写入接口不是关系数据库事务。下面两行数据之间没有“全部提交或全部回滚”的通用事务保证:

cpu,host=app-01 usage_user=63.2
memory,host=app-01 used_percent=72.1

如果业务要求“CPU 和内存指标必须同时存在”,应在上游使用消息事务、幂等事件 ID、批次状态表或外部协调机制实现,而不是依赖 InfluxDB 写入接口具备跨 measurement 事务。


六、查询:从原始点到窗口聚合

6.1 查询的基本步骤

一个时序查询通常包含四个逻辑步骤:

  1. 限定时间范围;
  2. 选择 measurement;
  3. 选择 field;
  4. 过滤或分组 tags;
  5. 对时间窗口进行聚合。

假设数据如下:

cpu,host=app-01,region=cn-shanghai usage_user=60.0 1710000000000000000
cpu,host=app-01,region=cn-shanghai usage_user=70.0 1710000060000000000
cpu,host=app-01,region=cn-shanghai usage_user=80.0 1710000120000000000

若按 1 分钟窗口求平均:

xˉw=1nwi=1nwxi\bar{x}_w = \frac{1}{n_w}\sum_{i=1}^{n_w}x_i

其中:

  • ww 是时间窗口;
  • nwn_w 是窗口内的样本数;
  • xix_i 是第 ii 个样本。

如果三个点属于同一个窗口,则平均值为:

xˉ=60+70+803=70\bar{x}=\frac{60+70+80}{3}=70

但这不代表整个一分钟内 CPU 始终是 70%。它只是样本平均值。采样不均匀时,普通算术平均可能不能代表时间加权平均。


6.2 Flux 查询示例:OSS 2.x

from(bucket: "metrics")
  |> range(start: -30m)
  |> filter(fn: (r) =>
    r._measurement == "cpu" and
    r._field == "usage_user" and
    r.host == "app-01"
  )
  |> aggregateWindow(
    every: 1m,
    fn: mean,
    createEmpty: false
  )

执行过程可以理解为:

  1. from 选择 bucket;
  2. range 限定最近 30 分钟;
  3. filter 选出 CPU 的 usage_user 字段和指定主机;
  4. aggregateWindow 将时间轴切成 1 分钟窗口;
  5. mean 对每个窗口求平均;
  6. createEmpty: false 表示没有数据的窗口不生成空记录。

如果监控图表要求“无数据窗口显示为零”,不能直接把缺失数据当作零。缺失可能表示:

  • 主机下线;
  • 采集器故障;
  • 过滤条件写错;
  • 数据已过期删除。

将缺失填零必须由业务语义明确授权,否则会把“没有观测”误判为“指标等于零”。


6.3 SQL 查询示例:InfluxDB 3 Core

SELECT
    date_bin(INTERVAL '1' MINUTE, time) AS window_start,
    host,
    AVG(usage_user) AS avg_usage_user
FROM cpu
WHERE time >= now() - INTERVAL '30' MINUTE
  AND time < now()
  AND host = 'app-01'
GROUP BY window_start, host
ORDER BY window_start;

这条 SQL 的意图是:

  • time 限定查询范围;
  • date_bin 将时间映射到 1 分钟窗口;
  • 按窗口和主机分组;
  • 计算 usage_user 的平均值。

InfluxDB 3 Core 使用 SQL 时,measurement 通常以表的形式暴露,tags 和 fields 以列的形式参与查询。具体函数支持和列名映射应以部署版本的 SQL 文档为准,不能把 2.x Flux 管道语法与 3.x SQL 函数混用。


6.4 GROUP BY 的含义

时序查询中,分组通常包含两种维度:

  1. 按 tag 分组;
  2. 按时间窗口分组。

例如按 region 和 5 分钟窗口统计:

GROUP BY region, 5m

其逻辑结果不是一个总平均值,而是:

G={(region,window)}G = \{(region, window)\}

每个 (region, window) 组合独立计算聚合结果。

如果 tag 基数很高,分组结果数量也会增多。例如:

  • 10000 个用户;
  • 60 个 1 分钟窗口;

则结果上界可达到:

10000×60=60000010000 \times 60 = 600000

这不仅影响返回数据量,也会影响查询执行时的内存和中间状态。因此,高基数 tag 会同时影响写入索引、查询过滤、分组和结果序列化。


七、存储引擎与数据流

7.1 InfluxDB 1.x/2.x 的 TSM 思路

InfluxDB 1.x 和 OSS 2.x 的经典存储路径可以抽象为:

写入请求
  ↓
解析 Line Protocol
  ↓
内存中的写入缓存
  ↓
WAL / 持久化日志
  ↓
TSM 文件
  ↓
compaction
  ↓
查询读取缓存和持久化文件

TSM,即 Time-Structured Merge Tree,是面向时序数据的存储结构。它通常按 measurement、series、field 和时间组织数据,并通过压缩和合并减少存储开销。

写入时:

  1. 服务端解析 measurement、tags、fields 和 timestamp;
  2. 数据进入内存缓存;
  3. 通过 WAL 等机制增强崩溃恢复能力;
  4. 后台将内存数据写成持久化文件;
  5. compaction 合并多个文件,清理被覆盖或过期的数据。

查询时:

  1. 读取时间范围和过滤条件;
  2. 找到相关 series 和 field;
  3. 同时检查内存数据与持久化文件;
  4. 合并不同层级的数据;
  5. 执行聚合和排序。

这解释了两个常见现象:

  • 数据写入成功后通常很快可查询,但不能把客户端收到成功响应等价为所有后台整理工作已完成;
  • 查询变慢可能不是写入失败,而是 series 数量过多、文件未充分合并、时间范围过大或过滤条件选择性不足。

7.2 InfluxDB 3 Core 的列式路径

InfluxDB 3 Core 的底层路径不同,核心组件包括:

  • Line Protocol 写入层;
  • catalog 元数据;
  • 对象存储或本地文件系统中的 Parquet 数据;
  • WAL 等用于恢复的机制;
  • 查询引擎;
  • compaction 或文件整理过程。

可以抽象为:

Line Protocol
  ↓
写入服务解析
  ↓
WAL / 写入缓冲
  ↓
Parquet 数据文件
  ↓
查询引擎读取列和时间范围

Parquet 是列式存储格式。查询只需要 usage_user 时,理论上可以避免读取不相关的 field 列;时间范围和文件统计信息也可能帮助跳过不相关的数据文件。

但“列式存储”不代表任何查询都快:

  • 不带时间范围的查询仍可能扫描大量历史数据;
  • 按高基数列分组仍可能产生巨大中间结果;
  • 选择了很多列仍会增加读取量;
  • 数据文件过多或 compaction 滞后会增加元数据和 I/O 开销。

因此,InfluxDB 3 的列式特性与 ClickHouse 的列式建模有相似之处,但不能把两者的排序键、分区键或 MergeTree 参数直接套用。InfluxDB 3 的存储组织和管理接口属于另一套产品语义。


八、保留策略:它解决什么问题

8.1 保留策略的定义

保留策略,即 retention policy,规定数据在数据库中最多保留多长时间,或者在 InfluxDB 3 Core 中由数据库级 retention period 表达类似约束。

设当前时间为 tnowt_{\text{now}},保留周期为 RR,则一个时间戳为 tt 的点满足:

ttnowRt \geq t_{\text{now}} - R

时才有资格继续保留。若:

t<tnowRt < t_{\text{now}} - R

则该点会进入过期范围,并在系统的后台清理或文件整理过程中被删除。

注意三个边界:

  1. retention 是按时间判断,不是按总行数判断;
  2. 过期点不一定在到期瞬间物理消失;
  3. retention 不是备份,删除后的数据不能依赖 retention 恢复。

8.2 OSS 2.x 的 bucket retention

在 InfluxDB OSS 2.x 中,bucket 是数据、访问策略和保留周期的重要边界。创建 bucket 时可以设置保留时间,例如通过 CLI:

influx bucket create \
  --name metrics-raw-30d \
  --org acme \
  --retention 30d \
  --host http://localhost:8086 \
  --token "${INFLUX_TOKEN}"

命令成立的前提是:

  • influx CLI 已安装;
  • token 有创建 bucket 的权限;
  • 目标 InfluxDB 是支持该 CLI 接口的 OSS 2.x 部署;
  • 组织 acme 已存在。

验证:

influx bucket list \
  --org acme \
  --host http://localhost:8086 \
  --token "${INFLUX_TOKEN}"

应检查目标 bucket 的 retention 值是否为预期的 30d。不要只检查创建命令的退出码,因为脚本可能连接到了错误的 host 或组织。


8.3 InfluxDB 1.x 的 retention policy

InfluxDB 1.x 通常在 database 内定义 retention policy:

CREATE RETENTION POLICY "thirty_days"
ON "metrics"
DURATION 30d
REPLICATION 1
DEFAULT;

这里:

  • DURATION 30d 表示数据保留周期;
  • REPLICATION 1 是副本因子,具体意义受部署形态影响;
  • DEFAULT 表示未显式指定 RP 时使用它。

InfluxDB 1.x 的 measurement 并不是唯一完整定位。查询常需要指定 database 和 retention policy,或者使用默认 RP。

不要把 RP 当成普通 SQL 表。修改或删除 RP 可能触发数据过期或删除,必须先验证目标 database、RP 名称和当前默认配置。


8.4 InfluxDB 3 Core 的 retention period

InfluxDB 3 Core 采用数据库级保留周期。创建数据库时可以指定 retention period,例如:

influxdb3 create database metrics \
  --retention-period 30d

这是 InfluxDB 3 Core 的命令行管理边界,不能直接用于 InfluxDB OSS 2.x。

修改 retention 前要确认:

  • 当前版本是否允许在线修改;
  • 修改是缩短、延长还是重新设置;
  • 已过期数据是否仍位于待清理文件中;
  • 对象存储中的旧文件是否由数据库自动管理;
  • 是否需要先备份或导出。

保留策略缩短通常是不可逆的数据删除操作;保留策略延长也不能恢复已经被删除的数据。


九、原始数据、降采样和多级保留

高频原始数据通常查询成本高、存储时间长。常见方案是:

原始数据:短期保留
分钟级聚合:中期保留
小时级聚合:长期保留

设原始采样间隔为 10 秒,则每天每个 series 的原始点数约为:

Nraw=24×60×6010=8640N_{\text{raw}}=\frac{24\times60\times60}{10}=8640

如果保存 30 天:

N30d=8640×30=259200N_{\text{30d}}=8640\times30=259200

若将其聚合为 1 分钟数据:

Nminute=24×601=1440N_{\text{minute}}=\frac{24\times60}{1}=1440

每天的点数约减少:

114408640=5683.3%1-\frac{1440}{8640}=\frac{5}{6}\approx83.3\%

这只是点数估算,不是实际磁盘压缩比。实际存储还受 field 数量、tag 数量、时间戳编码、压缩效果和文件布局影响。

降采样的关键不是“求平均”三个字,而是选择与指标语义匹配的聚合:

  • CPU 使用率:平均值、最大值可能都需要;
  • 请求延迟:平均值不足以表示尾延迟,应考虑分位数或直方图;
  • 计数器:应使用差分、速率或增量;
  • 温度:平均值和最大值可能都重要;
  • 状态值:不能随意求平均。

例如请求延迟数据:

latency_ms=10
latency_ms=20
latency_ms=1000

平均值为:

10+20+10003=343.3\frac{10+20+1000}{3}=343.3

它掩盖了尾部的 1000 ms。若监控目标是用户体验,p95 或 p99 通常比平均值更有解释力,但前提是数据采集方式支持正确计算分位数。


十、删除、过期和查询过滤不是一回事

以下三种操作经常被混淆。

10.1 查询过滤

|> range(start: -7d)

它只改变本次查询读取的时间范围,不删除任何数据。

10.2 按条件删除

删除接口可以根据 measurement、tag、时间范围等条件删除数据。删除前必须确认:

  • endpoint 对应的版本;
  • bucket 或 database;
  • 时间范围;
  • predicate 是否正确;
  • 是否有备份;
  • 是否可能影响其他业务。

删除操作通常不可逆。一个过宽的 predicate 可能删除多个主机、服务或 measurement 的数据。

10.3 Retention 过期

retention 根据时间自动淘汰数据。它通常由后台任务、分片清理或文件整理完成,因此存在逻辑到期和物理回收之间的时间差。

因此:

  • 查询过滤是“这次不读”;
  • 删除是“主动移除匹配数据”;
  • retention 是“按生命周期自动淘汰”。

十一、常见失败表现和诊断路径

11.1 写入成功但查询不到

按以下顺序排查:

  1. 时间单位是否错误

    查询最新数据:

    SELECT *
    FROM cpu
    WHERE time >= now() - INTERVAL '1' HOUR
    LIMIT 10;
    

    如果使用 Flux,则检查:

    from(bucket: "metrics")
      |> range(start: -1h)
      |> limit(n: 10)
    
  2. measurement、tag 和 field 是否写反

    写入:

    cpu,host=app-01 usage_user=63.2
    

    查询时应把 host 作为 tag 条件,把 usage_user 作为 field。

  3. 目标 bucket、database 或组织是否正确

  4. 默认 retention policy 是否与写入目标一致

  5. 时间戳是否落入已过期范围

  6. 客户端是否忽略了非 2xx 响应或响应正文


11.2 写入返回 field type conflict

检查同一个 measurement 和 field key 的历史数据类型。例如:

cpu,host=app-01 usage_user=63.2
cpu,host=app-02 usage_user="63.2"

第二行把 usage_user 当成字符串写入,容易造成类型冲突。

修复方式通常包括:

  • 统一生产者序列化逻辑;
  • 使用新的 field key;
  • 使用新的 measurement;
  • 导出并清理错误数据后重新导入。

不要用同一个 field 既表示数值,又表示错误文本。


11.3 查询很慢

先检查查询是否具备以下条件:

  • 有明确的时间范围;
  • 尽早过滤 measurement;
  • 只选择需要的 field;
  • 避免对高基数 tag 做大范围分组;
  • 避免一次读取多年原始数据;
  • 检查数据文件数量、compaction 和资源使用情况。

例如:

SELECT *
FROM cpu;

即使数据库允许执行,也可能扫描大量历史数据和列。更明确的查询是:

SELECT time, host, usage_user
FROM cpu
WHERE time >= now() - INTERVAL '15' MINUTE
  AND host = 'app-01';

诊断时应区分:

  • 数据读取慢;
  • 聚合中间状态大;
  • 结果行数过多;
  • 客户端下载和反序列化慢;
  • 服务端 CPU、内存或对象存储延迟高。

11.4 series 数量异常增长

如果发现内存增长、写入延迟上升或查询分组变慢,应检查近期是否把以下值写入 tag:

  • request ID;
  • trace ID;
  • 用户 ID;
  • 完整 URL;
  • 动态错误消息;
  • 时间戳字符串;
  • 随机 UUID。

可以将它们改为 field,或者先在采集端做归类。例如把:

error_message="database connection timeout after 3127 ms"

保留为 field,把可控的错误类别设计为 tag:

error_class=database_timeout error_message="database connection timeout after 3127 ms"

这样查询可以按 error_class 聚合,而不会为每条不同文本创建 tag 组合。


十二、数据模型反例

反例一:把所有属性都放成 tag

http_request,service=checkout,user_id=u-83921,url=/pay/order/1234 \
latency_ms=18.7

问题:

  • user_id 基数高;
  • URL 可能无限增长;
  • 路径中的订单号进一步制造高基数;
  • series 数量随请求实体数量增长。

更合理的形式可能是:

http_request,service=checkout,route=/pay/order,status_class=2xx \
user_id="u-83921",url="/pay/order/1234",latency_ms=18.7

其中 route 应是经过模板化的低基数路由,而不是原始 URL。


反例二:把需要过滤的稳定维度放成 field

cpu usage_user=63.2,host="app-01",region="cn-shanghai"

如果所有查询都必须按 host 和 region 过滤,把它们设计为 field 会削弱模型表达:

cpu,host=app-01,region=cn-shanghai usage_user=63.2

但如果某个字段是自由文本、唯一标识或不参与分组,就不应为了“能过滤”而强行变成 tag。


反例三:为每个设备建立 measurement

device_d_001 temperature=21.5
device_d_002 temperature=22.1

这样会把实体维度编码到 measurement 名称中,导致:

  • 查询所有设备需要动态拼接 measurement;
  • 新设备不断产生新 measurement;
  • 权限、保留和聚合难以统一。

更稳定的模型是:

temperature,device_id=d-001 value=21.5
temperature,device_id=d-002 value=22.1

不过 device_id 是否适合作为 tag,还要看设备数量和查询模式。如果设备数量极大且很少按单设备查询,可能需要重新设计数据分层或采样方式。


十三、生产取舍:如何把约束落实到 Schema

设计一个 measurement 时,可以按以下顺序推导,而不是先决定“哪些放 tag”。

第一步:写出实际查询

例如目标查询是:

查询最近 1 小时,按 service、region 分组,计算 p95 latency

第二步:标出操作角色

  • 时间范围:timestamp;
  • 按 service 过滤和分组:tag;
  • 按 region 分组:tag;
  • 计算 p95 的 latency:field;
  • 请求 ID:通常不参与聚合,field 或不写入。

第三步:估算 series 上界

若:

  • service 有 50 个值;
  • region 有 10 个值;
  • env 有 3 个值;

则单 measurement 的组合上界为:

50×10×3=150050\times10\times3=1500

如果再加入 100 万个 request ID:

1500×1,000,000=1.5×1091500\times1{,}000{,}000=1.5\times10^9

即使实际组合远低于上界,也说明 request ID 不适合成为 series 维度。

第四步:定义字段类型

例如:

latency_ms: float
status_code: integer
success: boolean
route: tag
service: tag
region: tag

第五步:定义保留层级

例如:

  • 原始请求指标:7 天;
  • 1 分钟聚合:30 天;
  • 1 小时聚合:1 年。

第六步:用真实数据验证

至少验证:

  • 写入吞吐;
  • 活跃 series 数;
  • 查询 P50、P95 和 P99;
  • compaction 后的文件数量;
  • 迟到数据;
  • retention 到期行为;
  • 删除和恢复流程。

十四、与 ClickHouse 列式建模的边界

InfluxDB 与 ClickHouse 都能处理大规模时间数据,也都重视列式读取、时间过滤和数据压缩,但两者的建模入口不同。

在 ClickHouse MergeTree 中,工程师通常显式设计:

  • ORDER BY 排序键;
  • PARTITION BY 分区键;
  • 数据跳过索引;
  • 主键稀疏索引;
  • TTL 和物化视图。

在 InfluxDB 中,核心建模首先是:

  • measurement;
  • tag;
  • field;
  • timestamp;
  • series 基数;
  • bucket 或数据库保留周期。

因此不能简单地把:

tag = ClickHouse ORDER BY
field = 普通列
bucket = ClickHouse partition

当成严格对应关系。

两者都要求查询与数据布局匹配,但实现机制不同:

  • InfluxDB 的 tags 参与 series 组织和时序查询模型;
  • ClickHouse 的排序键决定数据排序与主索引跳过;
  • InfluxDB 的 retention 是产品级生命周期语义;
  • ClickHouse TTL、分区和物化视图具有不同的执行和删除机制。

如果业务需要复杂 JOIN、宽表分析、维度字典关联或多表事务,InfluxDB 不应被当作关系数据库替代品。它更适合以时间为中心的观测数据读写和聚合。


十五、安全边界

InfluxDB 的数据模型不自动解决安全问题。

最小权限

写入器通常只需要对指定 bucket 或数据库写权限;仪表盘查询器只需要读权限;管理员才应具备组织、bucket、数据库和 token 管理能力。

不要让采集器使用管理员 token。token 泄露后,攻击者可能读取、写入或删除大量数据。

加密

  • 客户端到服务端应使用 HTTPS;
  • 对象存储访问应使用加密传输;
  • token、云厂商密钥和配置文件应放入密钥管理系统;
  • 备份文件和导出数据也应加密。

审计

应记录:

  • 谁创建或修改了 bucket、数据库和 retention;
  • 谁删除了数据;
  • 哪些 token 被创建、撤销或轮换;
  • 关键查询和管理 API 的调用来源。

注入防护

InfluxQL、Flux、SQL 和 Line Protocol 都不能直接拼接不可信输入。

错误示例:

WHERE host = '${user_input}'

应使用对应客户端的参数化能力、严格白名单或转义函数。对于 measurement、bucket、tag key 这类结构性标识符,通常不能只依赖参数化值,而应使用白名单映射:

用户选择 "cpu" → 服务端映射为固定 measurement
用户提交任意字符串 → 不直接拼入查询

Line Protocol 也需要转义。用户输入中的空格、逗号、等号、双引号可能改变数据结构,造成写入失败或字段污染。


结语

InfluxDB 的核心不是“把带时间的行存进去”,而是围绕以下关系组织数据:

measurement+tag set+field set+timestamp\text{measurement} + \text{tag set} + \text{field set} + \text{timestamp}

时间戳决定观测何时发生;measurement 表示观测类别;tags 决定可检索和可分组的维度,并影响 series 基数;fields 保存实际值,并承担类型稳定和聚合语义;bucket、retention policy 或数据库级 retention period 则决定数据生命周期。

一个可靠的 InfluxDB 方案应同时回答:

  • 时间戳由谁生成,单位是什么;
  • 重试写入是否幂等;
  • 哪些维度用于过滤和分组;
  • 哪些值需要聚合;
  • field 类型如何长期保持稳定;
  • series 数量如何估算和监控;
  • 原始数据与降采样数据分别保留多久;
  • 查询使用 Flux、InfluxQL 还是 SQL;
  • 版本、事务、删除和恢复边界在哪里。

只要这些问题在写入前明确,InfluxDB 的 Schema、查询性能和数据生命周期通常能够保持一致;如果把它们留给写入接口自动决定,问题往往会在数据量增长或第一次故障恢复时集中暴露。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。