大数据中的数据压缩原理
问题
遇到一个情况,IOT设备每秒上报10万条数据,每条数据2.5k大小,算下来一天的有2T的数据,然而存储到HDFS后数据大小仅为400G,压缩率为1:5是如何产生的,想从原理的角度来了解下大数据的压缩这方面的知识。
原理
主要有两个主要原理:列式存储、压缩方式,两者互为补充。
列式存储
想象一下原始数据(假设是 JSON 或 CSV 格式):
{"timestamp": 1723456789123, "device_id": "SN-12345", "temp": 25.6, "humidity": 65.2, "vibration": 0.012, ... (其他字段)}
{"timestamp": 1723456789124, "device_id": "SN-67890", "temp": 26.1, "humidity": 63.8, "vibration": 0.008, ...}
{"timestamp": 1723456789125, "device_id": "SN-12345", "temp": 25.8, "humidity": 65.5, "vibration": 0.015, ...}
...(每秒10万条)
-
行式存储 (如原始文本、传统数据库行存储):
-
数据按行组织:每一行数据的所有字段值紧挨着存储在一起。
-
写入快:追加新行很方便。
-
读取整行快:一次 I/O 就能拿到一行的所有数据。
-
压缩劣势: 同一行内的不同字段数据类型差异巨大(时间戳是长整数,设备 ID 是字符串,温度/湿度是浮点数,振动可能是浮点或整数)。通用压缩算法(如 gzip, Snappy, LZ4)在处理这种“混合沙拉”式的数据时,很难找到长的、重复的模式,压缩效率相对较低。压缩比通常在 2:1 到 4:1 之间,达到 5:1 比较困难。
-
-
列式存储 (如 HDFS 上常用的 Parquet, ORC):
-
数据按列组织: 文件被分成多个列块。一个文件块内,所有行的 timestamp 值存储在一起,接着是所有行的 device_id 值存储在一起,然后是所有 temp 值,依此类推。
-
写入稍慢:需要按列重组数据。
-
读取整行慢:需要从多个地方读取不同列的数据再拼成行。
-
压缩的巨大优势: 这是关键!同一列内的数据具有极高的相似性、规律性和重复性! 这使得针对特定列数据类型的、高度优化的压缩技术得以大显身手,显著提升压缩比:
-
相同数据类型: 一列里的所有值都是同一种基本类型(全是整数、全是浮点数、全是字符串等)。压缩算法可以针对性地优化。
-
高重复性:
-
device_id : 同一个设备会在短时间内连续上报多次。列存储后, SN-12345 , SN-12345 , SN-12345 … 会连续出现很多次。
-
timestamp : 通常是单调递增的毫秒或微秒时间戳。相邻时间戳之间的差值 ( delta ) 通常很小且非常规律(接近上报间隔)。
-
传感器值 ( temp , humidity , vibration ): 物理世界的测量值通常变化缓慢。相邻数据点之间的值 ( delta ) 通常很小,或者在一个有限的范围内波动。
-
-
低基数 (Low Cardinality): 某些列(如状态码、错误码、设备类型)可能只有少数几种取值,重复性极高。
-
-
压缩方式
针对列特性的“组合拳”
列式存储格式(如 Parquet, ORC)内部采用了多种高效的编码和压缩技术,充分利用了上述列数据的特性:
-
编码 (Encoding):在压缩前对数据进行转换,使其更易压缩或占用空间更小。
-
字典编码 (Dictionary Encoding): 针对高重复性、低基数列(如 device_id )。为列中所有不重复的值建立一个字典( SN-12345 -> 0 , SN-67890 -> 1 ),然后将实际数据替换成字典中的短整数编码。字符串变短整数,压缩率极高!这是压缩 device_id 这类列的主力。
-
游程编码 (Run-Length Encoding - RLE): 针对连续重复值。例如,如果 status 列连续 1000 次都是 OK ,RLE 会存储为 (OK, 1000) 而不是重复写 1000 次 OK 。对重复性极高的列非常有效。
-
增量编码 (Delta Encoding): 针对单调递增或变化平缓的数值列(如 timestamp , 缓慢变化的传感器值)。存储第一个值,然后存储后续值与前一值的差值( delta )。这些 delta 通常是非常小的整数(时间差)或非常小的浮点数(传感器变化量),本身占位就小,更容易被后续压缩。 timestamp 列压缩的核心。
-
位图编码 (Bit-Packing): 针对数值较小的整数。如果一个整数可以用 4 位表示(0-15),就不会浪费整个字节(8位)去存储它。将多个小整数打包进一个字节/字中。常与 delta 或 frame of reference 结合使用。
-
帧偏移编码 (Frame of Reference - FOR): 针对数值范围有限的列。存储该列(或块)的最小值,然后存储每个值与这个最小值的差值。这些差值通常比原始值小得多,更容易位打包和压缩。
-
-
通用压缩 (Compression): 在应用了高效的编码之后,再对整个列块(或页)应用标准的、快速的通用无损压缩算法,进一步消除冗余:
-
Snappy: Google 开发,速度极快,压缩比适中。非常流行,适合追求速度的场景。
-
LZ4 / LZO: 类似 Snappy,高速,压缩比略好于或接近 Snappy。
-
Gzip (Deflate): 压缩比较好,速度比 Snappy/LZ4 慢。经典选择。
-
Zstandard (Zstd): Facebook 开发,在速度和压缩比之间取得了非常好的平衡,通常优于 Gzip 和 Snappy/LZ4。越来越流行。
-
Brotli: Google 开发,压缩比非常高,但速度较慢。适合对存储空间极度敏感,对写入/读取速度要求不高的场景。
-
LZ77 / Huffman (Deflate 基础): Gzip 等算法的基础,通过查找重复字符串和用更短的编码表示高频字符来压缩。
-
IoT 场景分析压缩过程
-
原始数据 (2TB):
-
包含所有字段(时间戳、设备ID、多个传感器值、可能的状态/元数据)。
-
每条数据包含大量结构信息(字段名、分隔符、引号、逗号等)。
-
数据以行为单位混合存储,不同类型数据交织,压缩效率不高。
-
-
写入 HDFS (列式存储格式如 Parquet):
-
数据重组: 系统将接收到的数据按列拆分开。
-
列块编码: 对每一列应用最适合的编码:
-
timestamp : Delta Encoding (存储时间差) + Bit-Packing (压缩小整数差值)。
-
device_id : Dictionary Encoding (用短整数代替长字符串) + 可能结合 RLE (如果同一设备连续上报)。
-
temp , humidity 等数值: Delta Encoding (存储变化量) 或 FOR (存储与最小值的差) + Bit-Packing。如果精度允许,可能先转换为整数(如 temp * 10 存储为整数)再应用上述编码。
-
低基数列(如 status ): Dictionary Encoding + RLE。
-
压缩: 对经过编码的每个列数据块(或页)应用选定的通用压缩算法(如 Snappy, Zstd)。
-
元数据存储: 文件头存储一次表结构(Schema)、字典信息、列块统计信息(最小值、最大值、空值数等),避免了每条记录重复存储字段名等结构信息。
-
文件结构优化: Parquet/ORC 文件内部有精心设计的结构(Row Group, Column Chunk, Page),便于分块处理、谓词下推和高效压缩。
-
-
结果 (400GB):
-
消除结构冗余: 字段名、分隔符等只在元数据中存一次。
-
利用列内相似性: 字典编码消灭了字符串重复;增量/游程编码大幅减少了数值和ID的存储量;位打包节省了整数存储空间。
-
高效压缩算法: 通用压缩算法在已经高度冗余(经编码后)的数据上进一步压缩。
-
数据局部性: 列式存储本身让相似数据聚集,提高了压缩算法的效率。
-
总结关键点
-
列式存储是基础: 它通过将同类型、高相似性的数据物理上聚集在一起,为后续高效的、针对特定数据类型的编码和压缩创造了必要条件。没有列式存储,后续的优化压缩效果会大打折扣。
-
编码是关键: 字典编码、增量编码、游程编码、位打包等技术,直接利用了IoT数据中普遍存在的重复性、时间局部性、低基数、小范围变化等特性,在压缩前就将数据体积大幅缩减。它们是实现高压缩比(如5:1)的核心功臣。
-
通用压缩是最后一步: 在编码大幅减少冗余后,通用的无损压缩算法(Snappy, Zstd等)再进一步消除剩余的位级冗余,锦上添花。
-
元数据优化: 列式格式避免了行式格式中每条记录的结构信息冗余。
-
IoT数据特性是内因: 设备ID重复、时间戳规律递增、传感器值缓慢变化、存在低基数列,这些天然特性是能够被高效压缩的根本原因。
因此,从2TB 原始数据压缩到 400GB 的 HDFS 存储,正是列式存储(Parquet/ORC等)巧妙地利用了 IoT 数据的强规律性和重复性,通过一系列针对性的编码和高效的通用压缩算法,层层“瘦身”后的必然结果。这不仅节省了巨大的存储成本(5倍!),也极大地提高了后续大数据分析(如Spark, Hive, Presto)的查询效率(只需读取相关列)。