bigywater.tech
微服务 · 82

指标的存储与压缩:时序数据怎么编码(与编码系列呼应)

指标数据在时序数据库里怎么存?delta-of-delta 和 XOR 压缩到底省了多少?不同形状的数据差别多大?Prometheus 真实的 chunk 每个样本占几个字节?这篇用自己写的 Gorilla 编码器和 Prometheus 的快照逐项量出来。

0.26 vs 7.83
我的编码器下,恒定值与随机游走浮点 gauge 每个样本占的字节数
4.43 字节
Prometheus 3.15.0 真实 chunk 里平均每个样本占的字节数(未压缩是 16 字节)
6.51 位
真实抓取的 1 秒间隔时间戳,在 delta-of-delta 编码下平均每个样本占的位数(理想的整齐间隔是 1 位)

编码按形状分

实测 · 本机执行输出
  数据形状                         | 解码后与原样本完全一致 | 时间戳 位/样本 | 数值 位/样本 | Gorilla 编码后字节数 | 字节/样本 | 16 字节原始数据交给 zstd -3 后的字节数
  -----------------------------+-------------+----------+---------+----------------+-------+--------------------------
  恒定值                          | 是           | 1.00     | 1.00    | 918            | 0.26  | 6776                     
  计数器,每秒固定 +10,时间间隔恰好 1 s      | 是           | 1.00     | 14.75   | 7102           | 1.97  | 16082                    
  计数器,每秒随机 +0~39               | 是           | 1.00     | 14.34   | 6920           | 1.92  | 22472                    
  整数 gauge,在 19~21 之间跳         | 是           | 1.00     | 3.01    | 1823           | 0.51  | 10528                    
  浮点 gauge,随机游走(σ = 0.02)      | 是           | 1.00     | 61.59   | 28174          | 7.83  | 37640                    
  浮点 gauge,每次完全随机              | 是           | 1.00     | 57.99   | 26558          | 7.38  | 38195                    
  计数器 +10,时间戳有 ±5 ms 抖动        | 是           | 8.61     | 14.75   | 10523          | 2.92  | 17108                    
  计数器 +10,时间间隔在 0.5~1.5 s 之间随机 | 是           | 13.85    | 14.74   | 12881          | 3.58  | 17106                    
  所有形状都无损还原:True。时间间隔恰好固定时,时间戳只要 1 位;计数器每秒固定 +10 时数值的 XOR 变化很小;随机浮点数几乎没得压缩(每个样本接近 64 位的数值部分);时间戳有抖动或间隔不规则时,delta-of-delta 不再为 0,时间戳部分增大到几位到十几位

时间间隔整齐时时间戳只要 1 位;计数器的 XOR 变化小,随机浮点数几乎压不动。

Prometheus 真实的 chunk

实测 · 本机执行输出
  项目                                      | 结果                   
  ----------------------------------------+----------------------
  块里的序列数 / chunk 数 / 样本数                  | 155 / 465 / 45,880   
  chunks 目录的字节数                           | 203,410              
  每个样本的 chunk 字节数(Prometheus)             | 4.43                 
  index 文件的字节数                            | 16,907               
  每条序列的 index 字节数                         | 109                  
  我的编码器编码同一批样本(含各序列的 16 字节起始值,不含 chunk 头) | 166,224 字节,3.74 字节/样本
  未压缩(每个样本 16 字节)                         | 734,080 字节           
  分序列类型的结果(我的编码器;时间戳位/样本、数值位/样本、字节/样本):const:6.51、1.00、0.99;counter:6.51、12.45、2.42;gauge:6.51、55.55、7.81;全部序列平均:时间戳 6.51 位/样本、数值 23.00 位/样本。这批样本都解码回来与原样本逐个相同(断言通过)
  Prometheus 里每个样本平均占 4.43 字节(chunks 目录,含 chunk 头),我的编码器是 3.74 字节;三种类型差别很大:计数器和恒定值很省,随机游走的浮点 gauge 几乎压不动,是主要的开销。恒定值序列也不是几乎免费——真实的抓取时间戳有毫秒级抖动(见上一组的“时间戳 位/样本”),时间戳部分比理想的 1 位大。我的编码器是论文版本,和 Prometheus 的 chunk 格式在时间戳位宽、chunk 头(每个 chunk 有样本数、校验和)等细节上不同,两个字节数在同一个量级,不要求相等

平均每个样本 4.43 字节;真实抓取的时间戳有毫秒级抖动,时间戳部分比理想值大。

压缩之外

实测 · 本机执行输出
  快照里的文件     | 字节数    
  -----------+--------
  chunks/*   | 203,410
  index      | 16,907 
  meta.json  | 302    
  tombstones | 9      
  按每样本 4.43 字节外推:155 条序列 × 86400 个样本 = 13,392,000 个样本,约 59.4 MB/天(算出来的);序列数多、标签多时 index 的占比会变大(上面每条序列的 index 约 109 字节,和样本数无关)
ERRORS {}

index 每条序列约 109 字节,序列多时占比上升;可以按样本字节数外推存储量。

分步演示

时序数据的编码
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。Prometheus 3.15.0(admin API 对 head 做快照,统计块里 chunks 与 index 文件);我的编码器是按 Gorilla 论文写的(delta-of-delta 时间戳,XOR 浮点),delta-of-delta 的取值范围用的是补码区间,Prometheus 自己的 XOR chunk 在时间戳位宽、chunk 头等细节上与它不同,所以两者只能对照数量级;150 条序列是我构造的三种形状(计数器、浮点 gauge、恒定值),真实指标的形状更多样;外推到一天是线性的,没有考虑 head 压缩与块压缩之后的变化
    • 没有验证:Prometheus 的块压缩(compaction)之后 chunk 的大小;native histogram 的存储;VictoriaMetrics 等的专用压缩;remote write 的 snappy 压缩;整数 counter 的专用编码(如 Delta + zigzag + varint);与编码系列里 zstd / 字典压缩的组合效果
    京ICP备2026067502号-1京公网安备11010502063487号