阿明是城东一家面包店的账房先生。店里的每个面包出炉、卖出,都得记一条账:几点几分出炉、什么面包、卖多少钱。一天下来好几千条,一年下来账本摞得比人还高。纸张越来越贵,仓库越来越满,东家急得直跺脚:“阿明,能不能让每页纸多装点?” 阿明翻着账本发现了秘密:记录几乎每隔固定时间才来一条,价格也总在差不多的地方浮动——今天是5.23元,昨天是5.24元。“既然变化这么小,何必每个字都写全?”他琢磨出一套“省字记账法”,让一本账本顶过去十本。 我们的电脑监控系统就像这家面包店:服务器每秒都在产生 CPU、内存、网络指标,一年存下几千亿条记录。如果每条都完整写下,磁盘再多也不够。幸运的是,时序数据有极强的规律——采样间隔稳定,数值变化微小。于是诞生了 Gorilla 这类压缩算法,用极小空间装下海量数据。今天,我们就翻开阿明的账本,看看他是怎么省字的。

岗位关联

掌握时序压缩原理,是搭建高性价比监控与 IoT 数据平台的必备技能,直接影响存储成本与查询性能。

目录
  1. 可视化演示
  2. 一、数据压缩背景——账本太厚了
  3. 二、Gorilla架构总览——省字账本的格式
  4. 三、Delta-of-Delta——时间戳的省字大法
  5. 四、XOR浮点压缩——数值的省字大法
  6. 五、字典与行程编码——专治高频词和连笔
  7. 六、压缩选型对比——不同账本配不同策略
  8. 七、工程实践落地——从账房走向全城分店
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示时序数据库压缩算法的核心机制,通过入门/进阶/实战/优化四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

时序数据库压缩算法:无损压缩 无损压缩的基本概念与信息论基础 霍夫曼编码与自适应编码 核心机制可视化 时序数据库压缩算法整体架构与关键组件

一、数据压缩背景——账本太厚了

阿明把旧账本摊在桌上,一页一页翻给东家看:“你看,每一条都要写'2024年3月10日下午3点04分32秒,法式长棍,5.23元'。一天两千条,光写'下午3点04分32秒'这几个字就够累的。”东家问:“那能怎么办?”阿明说:“我发现记的时间几乎都是整点间隔,价格也总在5.2元左右打转。只要把'不变的废话'省掉,账本就能薄一大截。”

这正是时序数据的写照。所谓时序数据,就是按时间顺序不断产生的记录——服务器的CPU使用率、机房的温度、股票的报价。它们数量极大,一台设备一年就能产生三千多万条样本;但它们又极有规律:采样间隔基本固定,数值变化幅度很小。“固定”和“微小”就是压缩算法眼中的金矿——规律越强,冗余越多,可压缩的空间就越大。

为了衡量省字效果,我们需要一个指标:压缩率。它等于原始体积除以压缩后体积,数值越大说明压得越狠。原始情况下,每条样本如果各用8字节存时间戳、8字节存数值,总共16字节;而经过压缩,平均可能不到2字节。

阿明把第一个月省下来的纸摞在旧账本旁边,东家眼睛亮了:“省下的纸钱,够再开一家分店了。”压缩率,就是阿明向老板证明自己价值的那个数字。

压缩率=S原始S压缩后\displaystyle \text{压缩率} = \frac{S_{\text{原始}}}{S_{\text{压缩后}}}
压缩率>1 表示数据变小了;压缩率越大,存储越省。
  1. 原始体积 \(S_{\text{原始}}\)未压缩时所有数据点的字节总数。假设有 \(N\) 个样本,每个样本时间戳占 8 字节、数值占 8 字节:
    S原始=N×16S_{\text{原始}} = N \times 16
  2. 压缩后体积 \(S_{\text{压缩后}}\)用特殊编码(如 Gorilla)压缩后,所有样本占用的字节总数,包含少量块头元数据:
    S压缩后=S块头+S时间戳编码+S数值编码S_{\text{压缩后}} = S_{\text{块头}} + S_{\text{时间戳编码}} + S_{\text{数值编码}}
  3. 压缩率 \(\text{CR}\)原始体积与压缩后体积之比,是衡量算法好坏的核心指标:
    CR=S原始S压缩后\text{CR} = \frac{S_{\text{原始}}}{S_{\text{压缩后}}}

二、Gorilla架构总览——省字账本的格式

阿明设计的不是零散的省字小技巧,而是一套完整的账本格式。他把账本分成一页一页的“块”,每页只管两个小时内的记录;每一页又分成左右两栏——左栏专门记时间,右栏专门记数值。“时间和价格是两码事,分开记,各有各的省字法。”

这套思路正是 Gorilla 压缩算法的核心架构。Gorilla是Facebook在2015年提出的时序压缩算法,后来被Prometheus等主流监控系统采用。它把一条样本拆成两个独立通道:时间戳走“Delta-of-Delta“通道,数值走”XOR”通道,两条通道各自压缩、各自解码,互不干扰。

分块是一个精妙的设计。一个块通常包含约两小时的数据,块头部记录起始时间、样本数量等元数据。查询时,系统先看块头判断“这块是不是我想要的时段”,不是就整块跳过。这样既能按时间范围快速定位,又避免了把整年数据压成一个大文件——万一中间坏了,至多丢一个块,不会殃及全部。

阿明的新账本,每一页都是独立的“小账房”:两栏各管各的、按页检索。东家想查某天下午的账,阿明直接翻到那一页,不用从年头翻到年尾。这就是分块的智慧。

每点平均存储=块总大小块内样本数\displaystyle \text{每点平均存储} = \frac{\text{块总大小}}{\text{块内样本数}}
这是评估一个块压得好不好的尺子:平均到每个样本上占了多少字节。
  1. 块总大小一个块内所有编码后的数据(块头+时间戳通道+数值通道)的总字节数,压缩后通常只有几KB:
    S块=S块头+S时间戳编码+S数值编码S_{\text{块}} = S_{\text{块头}} + S_{\text{时间戳编码}} + S_{\text{数值编码}}
  2. 块内样本数 \(N\)这个块里一共装了多少条记录,由时间跨度除以采样间隔决定:
    N=T块跨度采样间隔N = \frac{T_{\text{块跨度}}}{\text{采样间隔}}
  3. 每点平均存储 \(b\)块总字节数除以样本数,得到每个样本平均摊到的字节数:
    b=S块Nb = \frac{S_{\text{块}}}{N}

三、Delta-of-Delta——时间戳的省字大法

阿明发现,出炉记录的时间几乎总是“每隔60秒一条”。“反正都是整点,我何必写'下午3点04分32秒'?写上一条是3点03分32秒,这一条就写'和上一条隔60秒'。”更妙的是,下一页的新记录又和上一条隔60秒——连“隔60秒”三个字他都懒得写,只画一个勾表示“跟上一条一模一样”。

这就是Delta-of-Delta(差值再求差值,简称DOD)的思想。第一步先算一阶差分:相邻两个时间戳的间隔,比如60秒;第二步再算二阶差分:相邻两个“间隔”的间隔。如果采样一直稳定在60秒,那么每个间隔都是60,间隔之差就是0。编码时,0只需要花1个bit记录,而原始时间戳是64个bit——省下了63个bit。

当然,真实场景不会永远完美稳定。Gorilla为此设计了控制位:当差值确实是0时,用一个控制位标记;当差值很小(比如只偏了几秒)时,用控制位加几bit存下这个小偏差;只有当偏差很大时才花大代价完整写入。解码器在读数据时,先看控制位,就知道该继续读几位。

阿明的画勾妙招,放到监控系统里就是DOD编码。服务器每15秒采集一次CPU,间隔稳定得像钟表,DOD几乎全零,时间戳的存储成本趋近于零。

{δi=ti−ti−1(一阶差分)Δi=δi−δi−1(二阶差分)\displaystyle\begin{cases}\delta_i = t_i - t_{i-1} & \text{(一阶差分)}\\\Delta_i = \delta_i - \delta_{i-1} & \text{(二阶差分)}\end{cases}
二阶差分就是“间隔的间隔”:采样稳定时它恒为0,于是只需1bit标记。
  1. 时间戳序列 \(t_i\)每条记录到达的时刻,是DOD的原始输入。原始时间戳是64位整数,直接存储代价高:
    t1,t2,t3,…,tnt_1, t_2, t_3, \dots, t_n
  2. 一阶差分 \(\delta_i\)相邻两个时间戳之间的间隔,比如固定的60秒。它把“绝对时刻”转化为“相对间隔”,数值通常很小:
    δi=ti−ti−1(i≥2)\delta_i = t_i - t_{i-1} \quad (i \ge 2)
  3. 二阶差分 \(\Delta_i\)相邻两个间隔的差。采样稳定时 \(\Delta_i = 0\),只需1bit即可编码:
    Δi=δi−δi−1\Delta_i = \delta_i - \delta_{i-1}
  4. 控制位决策解码器根据控制位判断后续读法——\(\Delta=0\) 读1bit,小 \(\Delta\) 读若干bit,大 \(\Delta\) 读完整64bit:
    最终存储={0,Δi=010+少量bit,∣Δi∣ 小完整值,其他\text{最终存储} = \begin{cases} 0, & \Delta_i = 0 \\ 10 + \text{少量bit}, & |\Delta_i| \text{ 小} \\ \text{完整值}, & \text{其他} \end{cases}

四、XOR浮点压缩——数值的省字大法

时间省下来了,数值怎么办?阿明看着面包价格:今天5.23元,明天5.24元。“这两个数在纸面上只差一分钱,但在电脑里,它们是64个0和1排成的长串。我要找的是:这64位里,到底哪几位变了?”

浮点数在计算机里用64位二进制表示,稍有变化时,大多数位是相同的。Gorilla用“异或”运算(XOR,逐位比较、相同为0、不同为1)把新旧两个数值变成一串差异位。如果值完全没变,异或结果是全零,只存一个0bit!如果变了,异或结果里通常只有中间一小段是1,前后都是0。于是算法只记录“从第几位开始变”“变了多少位”和“中间那段有效位”,把前后的0统统跳过——这叫前导零和尾零跳变。

XOR压缩最擅长处理“数值微变”的场景:CPU从50.1%变成50.2%,温度从25.3变成25.4,股价从100.01变成100.02。这些看似不同的数,二进制层面高度相似,XOR一算,真正变化的bit少得可怜。

阿明管这叫“只记改了哪一笔,不重抄整本账”。价格从5.23变到5.24,账本上只需补一笔“第几位变了”,连价格都不用重新写。

Ci=B(vi)⊕B(vi−1)\displaystyle C_i = B(v_i) \oplus B(v_{i-1})
\(C_i\) 是相邻两个浮点值二进制的异或结果。\(C_i=0\) 表示值完全没变,只需1bit;否则只记录中间变化的那段。
  1. 浮点值的二进制表示 \(B(v_i)\)每个浮点数在内存中是64位(8字节)的0/1串,这是XOR运算的原材料:
    B(vi)∈{0,1}64B(v_i) \in \{0, 1\}^{64}
  2. 异或运算 \(\oplus\)逐位比较两个二进制串,相同位记0,不同位记1,找出“到底哪里变了”:
    Ci=B(vi)⊕B(vi−1)C_i = B(v_i) \oplus B(v_{i-1})
  3. 全零判断若 \(C_i = 0\),说明两个值完全相同,只需记录1个bit的标记:
    Ci=0⇒1 bitC_i = 0 \Rightarrow 1\text{ bit}
  4. 非零时的压缩若 \(C_i \neq 0\),头部大多是0、尾部大多是0,只保留中间连续变化的“有效位段”:
    Ci=00⋯0⏟前导零 有效位段 00⋯0⏟尾零C_i = \underbrace{00\cdots0}_{前导零}\, \text{有效位段} \, \underbrace{00\cdots0}_{尾零}

五、字典与行程编码——专治高频词和连笔

有些时候,变化的不只是数字,还有文字。阿明发现店里卖的“法式长棍面包”出现频率极高,每次写全称手指都酸。他灵机一动:在账本扉页建一张对照表——“法式长棍面包”写作“棍”,“全麦乡村面包”写作“麦”。后面记正文时写一个字就够,这叫字典编码。

还有另一种情况:面包价格连着十天没变,阿明干脆写“连续10日价同”,而不是每天重复记一遍5.23元。把连续相同的值合并成“值+重复次数”,这叫行程编码(Run-Length Encoding,简称RLE)。

这两种方法虽然不是Gorilla的独创,但在时序压缩中非常有用。监控系统的数据常带标签,比如主机名“web-server-01-prod“、区域”us-east-1”,这些字符串反复出现,用字典编码映射成短ID,能省下一大截。而像传感器状态(在线/离线)、开关量这类取值不多、连续不变的字段,RLE几乎是量身定做。

阿明在账本扉页建字典、在正文里画“连续十日”的横线,不到一个月,账本又薄了三分之一。东家笑得合不拢嘴:“这些省字法,单看都是小聪明,合在一起就是大智慧。”

RLE编码长度=2\displaystyle \text{RLE编码长度} = 2
无论连续重复多少个值,RLE只存“这个值”和“重复了几次”两样东西,长度恒定。
  1. 待压缩序列假设一串连续相同的值 \(v\) 重复了 \(k\) 次,原始存储要写 \(k\) 个值:
    v,v,v,…,v⏟k 次\underbrace{v, v, v, \dots, v}_{k \text{ 次}}
  2. RLE 编码只存一个值和一个重复计数,总共两个部分:
    (v,k)(v, k)
  3. 字典编码映射把高频的字符串 \(s\) 映射到短整数 \(id\),后续只写 \(id\):
    s→字典ids \xrightarrow{\text{字典}} id

六、压缩选型对比——不同账本配不同策略

东家高兴之余问了个关键问题:“这套省字法,是不是所有分店都能直接用?”阿明摆摆手:“隔壁的蛋糕店是手工烘焙,出炉时间一点都不规律,价格也天天变。硬套我的方法,反而帮倒忙。”他开始帮每家分店量身定做记账策略——这就是压缩选型。

工程师面对时序压缩时,也该问“数据三问”。第一问:采样间隔稳定吗?稳定,DOD立功;不稳定,DOD的差值经常很大,压缩率大打折扣。第二问:数值变化幅度大吗?微变,XOR如鱼得水;剧烈跳变,XOR的前导零很少,省不了多少。第三问:字符串基数高吗?基数低(值域小、重复多),字典和RLE都是利器;基数高(几乎每个值都独一无二),字典反而会越压越大。

除了压缩率,还要权衡CPU开销。Gorilla的压缩和解压需要逐位运算,压缩率高但消耗CPU;追求极低延迟的系统可能选择更简单的编码。存储和计算永远是一场交易——省了空间,就要多花时间算;想要快,就得让数据多占点地方。

阿明给每家分店画了一张“记账策略表”:规律出炉的用时间压缩,价格稳定的用数值压缩,名字老长的用字典——没有最好的账本,只有最合适的账本。

总成本=存储成本+计算成本\displaystyle \text{总成本} = \text{存储成本} + \text{计算成本}
压缩选型不是单看压缩率,而是存储与CPU的联合优化。
  1. 存储成本项压缩后的数据占用的磁盘空间,由压缩率决定,压缩率越高此项越低:
    C存储=S原始CRC_{\text{存储}} = \frac{S_{\text{原始}}}{\text{CR}}
  2. 计算成本项写入时压缩、查询时解码消耗的CPU时间,压缩算法越复杂此项越高:
    C计算=t压缩+t解码C_{\text{计算}} = t_{\text{压缩}} + t_{\text{解码}}
  3. 总成本最小化选型的本质是让两项之和最小,而非只看其中一项:
    min⁡(C存储+C计算)\min (C_{\text{存储}} + C_{\text{计算}})

七、工程实践落地——从账房走向全城分店

阿明的省字账本在总店试验了一个月,效果惊人:同样厚的一沓纸,原来记一周,现在能记三个月。东家一声令下,全城分店统一换新账本。监控系统界也发生了同样的故事——Prometheus从2.0版本开始,在TSDB中采用了Gorilla风格的压缩,成为云原生监控的事实标准。

实际落地时,写数据只是故事的一半,读数据同样重要。查询时,系统先扫描块头,快速跳过不相关的时间范围,定位到目标块;然后只解压需要的块,而不是把整年的数据全部还原。这套“先定位、再解压”的路径,让海量数据的查询依然快如闪电。

工程实践还告诉我们,压缩效果可以“源头优化”。如果采样时间能规整到固定间隔(比如统一整点或每秒),DOD的差值会大量归零;如果采集端能减少数值抖动,XOR的压缩率也会明显提升。很多团队发现,把采集器调到“规整采样+合理精度”,比换任何压缩算法都管用。

阿明的账本从总店走向全城,省下的纸张堆满了一间库房。他把口诀传给每个分店的账房:“先看规律,再选方法,最后别忘了怎么把账读回来。”时序压缩的实践,正是这套口诀的工程版。

b=S块×8N\displaystyle b = \frac{S_{\text{块}} \times 8}{N}
把块字节数乘以8换算成bit,除以样本数,得到每个样本平均占用多少bit,Gorilla实测常常小于2bit。
  1. 块总bit数块大小 \(S_{\text{块}}\) 以字节为单位,乘以8得到总bit数:
    Sbit=S块×8S_{\text{bit}} = S_{\text{块}} \times 8
  2. 每点平均bit数除以块内样本数 \(N\),得到每个样本实际消费了多少bit:
    b=SbitNb = \frac{S_{\text{bit}}}{N}
  3. 经验参考Gorilla在Facebook的真实监控数据上,每样本平均约1.37字节,远低于原始的16字节:
    b≈1.37 字节/样本b \approx 1.37 \text{ 字节/样本}

AI 应用场景

云原生监控平台(Prometheus)

阶段:数据存储与压缩

解决的问题:传统方式下,监控系统把每条指标(时间戳+数值)完整落盘,百万级目标、每秒上亿条时间序列会让磁盘在数日内耗尽。Prometheus采用Gorilla压缩后,同一份数据占用空间可降至原来的十分之一甚至更低,同时保留原始精度的无损查询能力。

具体案例:某电商平台的Kubernetes集群有2万个Pod,每15秒上报一次容器CPU、内存指标。启用Gorilla压缩后,原先每天消耗500GB磁盘的监控数据,压缩到约60GB,历史数据留存从7天延长到60天,为容量规划提供了长期依据。

IoT设备数据回传

阶段:边缘数据预处理

解决的问题:海量物联网设备通过窄带网络回传传感器数据,带宽和电量都是稀缺资源。传统方式直接上报完整JSON,流量大、费电。借鉴Gorilla思想,在边缘网关做DOD和XOR压缩后再回传,可减少80%以上传输量。

具体案例:某智慧农业项目在田间部署了5000个温湿度传感器,每30秒上报一次数据。网关先将时间戳做DOD编码(采样间隔固定,几乎全零)、温湿度做XOR编码(数值微变),再批量上报。每月节省流量费超过60%,电池续航延长近一倍。

金融行情数据归档

阶段:历史数据存储与回放

解决的问题:股票、期货的报价数据量极大且要求无损,传统方案按原始格式存储,十年行情数据需要数PB空间。由于报价变化多为微小波动(如100.01→100.02),XOR压缩能取得极高压缩率,同时解码速度满足历史回放需求。

具体案例:某量化基金需要保存全市场逐笔行情做策略回测。采用类Gorilla压缩后,单日行情从12GB压缩至1.5GB,十年数据归档占用从43TB减至5.5TB,且回测时按块解压,查询单日行情只需几十毫秒。

Python 演示

python107 行
"""Gorilla时序压缩算法简化实现:DOD时间戳编码 + XOR浮点编码""" import struct  def dod_encode(timestamps):    """把时间戳序列编码为Delta-of-Delta差值序列"""    encoded = []    prev_delta = 0    for i, ts in enumerate(timestamps):        if i == 0:            encoded.append(ts)              # 第一个时间戳全量存        elif i == 1:            delta = ts - timestamps[i - 1]  # 第二个存一阶差分            encoded.append(delta)            prev_delta = delta        else:            delta = ts - timestamps[i - 1]            dod = delta - prev_delta        # 之后存二阶差分            encoded.append(dod)            prev_delta = delta    return encoded  def dod_decode(encoded):    """从Delta-of-Delta差值序列还原时间戳"""    timestamps = [encoded[0]]    if len(encoded) == 1:        return timestamps    curr = encoded[0] + encoded[1]    timestamps.append(curr)    prev_delta = encoded[1]    for dod in encoded[2:]:        delta = dod + prev_delta        curr += delta        timestamps.append(curr)        prev_delta = delta    return timestamps  def xor_encode(values):    """把浮点序列编码为XOR差值字节串(控制位简化为0x00/0x01/0xFF)"""    out = bytearray()    prev_bits = None    for v in values:        bits = struct.unpack('>Q', struct.pack('>d', v))[0]        if prev_bits is None:            out.append(0xFF)                        # 首值全量标记            out.extend(struct.pack('>Q', bits))     # 存8字节原始值        else:            xor = bits ^ prev_bits            if xor == 0:                out.append(0x00)                    # 值没变,只需1字节标记            else:                out.append(0x01)                    # 有变化标记                out.extend(struct.pack('>Q', xor))  # 简化:直接存8字节XOR值        prev_bits = bits    return bytes(out)  def xor_decode(encoded):    """从XOR差值字节串还原浮点序列"""    values = []    i = 0    prev_bits = 0    while i < len(encoded):        tag = encoded[i]        if tag == 0xFF:            prev_bits = struct.unpack('>Q', encoded[i + 1:i + 9])[0]            i += 9        elif tag == 0x00:            i += 1                                  # 值不变,直接复用prev_bits        else:            xor = struct.unpack('>Q', encoded[i + 1:i + 9])[0]            prev_bits = prev_bits ^ xor            i += 9        values.append(struct.unpack('>d', struct.pack('>Q', prev_bits))[0])    return values  def main():    timestamps = [1000, 1060, 1120, 1180, 1240, 1300, 1360, 1420]    values = [25.3, 25.3, 25.3, 25.4, 25.4, 25.5, 25.5, 25.6]     ts_codes = dod_encode(timestamps)    val_codes = xor_encode(values)     raw_bytes = 8 * len(timestamps) + 8 * len(values)    comp_bytes = len(ts_codes) * 8 + len(val_codes)     print("原始时间戳:", timestamps)    print("DOD编码后:", ts_codes)    print("原始值:", values)    print("XOR编码字节:", val_codes.hex())    print()    print("原始占用: %d 字节" % raw_bytes)    print("压缩占用: %d 字节" % comp_bytes)    print("压缩率: %.2fx" % (raw_bytes / comp_bytes))    print()    print("解码校验: 时间戳一致=%s, 值一致=%s" % (        dod_decode(ts_codes) == timestamps,        xor_decode(val_codes) == values    ))  if __name__ == '__main__':    main()
目的:用纯Python实现Gorilla压缩的两大核心——DOD时间戳编码与XOR浮点编码,并验证编解码往返无损。
  1. delta = ts - timestamps[i - 1]计算一阶差分:相邻时间戳的间隔,如“每隔60秒一条”。
  2. dod = delta - prev_delta二阶差分:间隔的间隔,稳定采样时恒为0。
  3. bits = struct.unpack('>Q', struct.pack('>d', v))[0]把浮点数拆成64位二进制整数,才能做XOR位运算。
  4. xor = bits ^ prev_bits异或找变化位:两个数哪几位不同,结果里就是1。
  5. if xor == 0:值为0表示和上一个完全相同,只需一个标记字节。
  6. out.append(0x01)` + `struct.pack('>Q', xor)值有变化时,先写变化标记,再写XOR差值。
  7. raw_bytes / comp_bytes计算压缩率,展示这套编码省下了多少空间。
  8. dod_decode(ts_codes) == timestamps解码回放校验,确认无损压缩不丢任何信息。

常见误区

误区我听说Gorilla能把数据压到十分之一,那一定是把一些数据点丢掉了,或者把精度降低了,监控数据不就失真了吗?

正解Gorilla是无损压缩,解压后能逐位还原原始时间戳和浮点值。它丢掉的不是数据,而是冗余——就像阿明把“下午3点04分32秒”简写成“跟上一条一样”,还原时照样能算出完整时刻。真正丢数据的是“有损采样”(比如每10条只留1条),那是另一个层面的事。

为何常见:大家熟悉图片压缩里的“有损JPEG”,容易把“压缩”默认理解为“牺牲质量”。但无损压缩在数据库领域是基本功,压缩率和无损并不矛盾。

误区Delta-of-Delta?我明白了,就是把相邻时间戳相减,存下间隔。比如每60秒一条,就存一个60。

正解存间隔只是一阶差分,DOD要在间隔的基础上再减一次——存的是“间隔的间隔”。固定间隔时,这个值恒为0,于是每个时间戳只需1bit。如果只存间隔,每个时间戳仍然要几十bit,压缩效果天差地别。

为何常见:从“时间戳”到“差分”这一步已经显得很聪明,容易让人以为公式到此为止,忽略了“再差分一次才是精妙所在”。

误区XOR不是加密算法里常用的运算吗?Gorilla拿它对数据加密,那压缩后的数据还怎么读?安全性会不会有问题?

正解这里的XOR不是加密,而是“找出两个浮点值的二进制差异”。加密的目的是让别人看不懂,压缩的目的是让存储变小。Gorilla用XOR算出新旧值的变化位,再只保存变化的那一小段;解码时拿变化位和上一位做XOR,就能还原出新值,整个过程无损且不涉及任何安全性。

为何常见:程序员都知道XOR是位运算,也常在加密算法里见到它,于是把“用途”和“加密”绑定。实际上XOR既能加解密,也能做差异检测,要看上下文。

误区时序压缩就用Gorilla,它是Facebook出品、Prometheus都在用,肯定万能。我直接套上准没错。

正解Gorilla的强项是“稳定间隔+微变浮点”,比如CPU、温度、内存指标。如果数据是随机大跳变(网络抖动延迟)、间隔不规律(事件触发型日志)、或是高基数字符串(每次请求的URL都不同),Gorilla的DOD和XOR都省不了多少。此时RLE、字典编码甚至通用压缩可能更合适。选型要看数据特征,不是看谁名声大。

为何常见:技术社区对主流算法的宣传容易让人产生“默认最优”的错觉,忽略了任何算法都有其适用边界。

面试题

Q1. Gorilla压缩算法最适合以下哪类数据特征?

单选题 · easy

  • A.随机波动的日志文本
  • B.周期性时间戳与微变浮点← 正确答案
  • C.高度加密的二进制流
  • D.大型JSON嵌套结构

答案:B。Gorilla的两大核心——DOD和XOR——分别针对“时间戳稳定”和“浮点微变”设计。周期性的时间戳让DOD大量归零,微变的浮点让XOR只有少数bit变化,这正是账房先生最擅长的“规律性账本”。随机日志、加密流和JSON都不具备这种规律。

Q2. "Delta-of-Delta"编码所压缩的数据是?

单选题 · mid

  • A.相邻时间戳求差值再求差值← 正确答案
  • B.对时间戳做哈希散列处理
  • C.直接压缩时间戳的文本表示
  • D.按分钟取整然后丢弃秒数

答案:A。Delta-of-Delta字面意思就是“差分的差分”。先算相邻时间戳的间隔(一阶差分),再算间隔与间隔的差(二阶差分)。固定采样间隔时,二阶差分恒为0,只需1bit即可存储,这是时间戳压缩的核心。

Q3. 当相邻两个浮点值做XOR运算的结果为0时,Gorilla会怎么做?

单选题 · mid

  • A.存储完整八字节的异或结果
  • B.仅记录一个0位表示数值不变← 正确答案
  • C.立即丢弃该采样数据点
  • D.改用字典编码重新存储

答案:B。XOR为0说明两个浮点的64位二进制完全相同,即数值没有变化。这时不需要存任何XOR结果,只需一个bit(或简化实现中的一字节标记)告知解码器“沿用上一个值”。丢弃数据点会造成时间戳与数值错位,无损压缩绝不允许。

Q4. Gorilla压缩算法的组成部分包括以下哪些?

多选题 · mid

  • A.基于二阶差分的时间戳编码← 正确答案
  • B.基于异或运算的浮点编码← 正确答案
  • C.基于哈夫曼树的熵编码
  • D.按块独立存储的压缩结构← 正确答案

答案:ABD。Gorilla由三大件构成:DOD编码时间戳(A)、XOR编码浮点值(B)、分块存储以便按时间定位(D)。哈夫曼树是通用熵编码,Gorilla并未使用。分块结构让每个块独立压缩和解码,查询时能跳过无关块。

Q5. 大量监控标签值(如主机名"web-server-01-prod")反复出现,哪种压缩方法最合适?

单选题 · easy

  • A.二阶差分的时间戳编码
  • B.字典编码映射为短索引← 正确答案
  • C.异或运算的浮点编码
  • D.行程编码压缩连续重复

答案:B。标签是字符串,且基数有限、重复度高,字典编码先把每个字符串映射成短整数,正文只存短整数,压缩效果显著。DOD面向时间戳、XOR面向浮点、RLE面向连续相同值,都不适合处理这种“整体重复但未必连续”的字符串。

Q6. Gorilla为什么将数据划分为多个块(block)独立压缩?

单选题 · mid

  • A.便于按时间范围快速定位查询← 正确答案
  • B.为了压缩率能更高一些
  • C.为了兼容老版本的数据库
  • D.为了减少CPU占用率

答案:A。每个块有独立的头部(起始时间、样本数),查询时先读块头判断时间范围,命中才解压该块,从而快速跳过无关数据。分块不能直接提高压缩率,也不影响CPU占用,更与版本兼容无关。

Q7. 以下哪些因素会显著影响时序压缩算法的压缩率?

多选题 · hard

  • A.采样间隔是否长期稳定← 正确答案
  • B.浮点数值变化的幅度大小← 正确答案
  • C.数据库的版本号高低
  • D.相同数值连续出现的长度← 正确答案

答案:ABD。采样稳定→DOD归零多;数值微变→XOR有效位段短;连续相同值→RLE效率高。这三个维度是数据特征的核心。数据库版本号是软件属性,与数据本身的熵无关,不影响压缩率。

Q8. XOR压缩中记录“前导零数”和“尾零数”的作用是?

单选题 · mid

  • A.跳过不变的bit只存变化段← 正确答案
  • B.对浮点值进行加密混淆
  • C.计算平均值做聚合统计
  • D.验证数据的完整性校验

答案:A。相邻浮点值差异小,异或结果的高位和低位大多是0,只有中间一小段是1。记录前导零和尾零的数量,就是为了告诉解码器“哪些位不用存”,只保存中间的有效位段,从而大幅缩减存储。

Q9. 当XOR的控制位显示“前导零数量与上一个值不同”时,编码器需要额外写入哪些信息?

单选题 · extreme

  • A.前导零数与有效位段长度← 正确答案
  • B.原始浮点数的完整二进制
  • C.上一个样本的完整存储副本
  • D.当前时间戳的完整数值

答案:A。Gorilla的一个优化是:如果前导零和尾零数和上次一样,就用更短的控制位;如果不同,则需多写几个bit来记录新的前导零数、尾零数和有效位段长度。写原始浮点或上一个样本的副本就失去了压缩意义。

Q10. 关于时序数据压缩的选型,下列说法正确的有?

多选题 · hard

  • A.稳定采样间隔优先用DOD压缩← 正确答案
  • B.微变浮点数值优先用XOR压缩← 正确答案
  • C.高基数字符串适合用字典编码
  • D.连续不变的状态值适合用RLE← 正确答案

答案:ABD。DOD吃定稳定间隔,XOR吃定微变数值,RLE吃定连续重复,这三组搭配是经典组合。字典编码只在低基数(值域小、重复多)时高效,高基数字符串几乎每个都不同,建字典反而增加额外开销,所以C错误。