大数据的存储和计算,本质上是CPU、内存、磁盘和网络这四个资源的生意。压缩算法选得好,相当于用一部分CPU预算,去换硬盘容量和网络带宽的巨额折扣,这笔账几乎在每一套大数据架构里都划算。但前提是,你得知道什么场景该用哪把尺子去量。
我这些年帮团队做技术基建,最常被问到的问题不是“要不要压缩”,而是“到底用哪个压缩算法”。网上搜出来一堆对比表,不是太理论就是太片面,真拿到自己的集群上一跑,经常发现数据完全对不上。今天我就把这几年在真实环境里摸爬滚打积累的选型经验整理一下,从原理到实测,从Hadoop到ClickHouse,把不同场景下的权衡逻辑说清楚。
1. 压缩算法选型在大数据架构里为什么这么重要
先说个容易被忽视的事实:在大数据架构里,压缩不是存储层的专属功能,而是一个贯穿数据全生命周期的通用策略。数据在磁盘上要压缩,在网络上传输要压缩,在内存中做Shuffle时也要压缩,甚至列式存储的每一列都在用不同的压缩思路。你选的每一个压缩算法,都在默默影响着任务跑得快不快、成本高不高。
1.1 压缩付出的代价到底是什么
很多人觉得压缩就是“空间换时间”,其实不准确。压缩的本质是用CPU计算时间换取磁盘空间和网络带宽。这个交换值不值,完全取决于你的瓶颈在哪个资源上。
举个例子,我们有一套日增日志量在TB级别的数据管道。在没有压测评估之前,我直接沿用老一套的Gzip方案,结果集群CPU经常冲到90%以上,而磁盘和网络的利用率还不到一半。后来换成LZ4,CPU占用掉到30%左右,虽然存储空间多了20%,但任务整体耗时缩短了近一半。这背后的逻辑很直白:在CPU是稀缺资源的架构里,前面省下来的磁盘空间,远不如早点把任务跑完有价值。
1.2 大数据场景的数据量级决定了压缩算法的下限
这也是为什么同样一个压缩算法,在小数据场景和在大数据场景里给人的体感完全不同。你本地的MySQL导出一个100MB的SQL文件,Gzip压缩到20MB,你会觉得Gzip天下第一。但在HDFS上,数据规模是PB级别,同样比例的压缩率差异,意味着几百TB存储成本和几个机架网络流量的差距。
算一笔粗账:假设集群里存了2PB的原始数据,用LZ4大概能压缩到1PB左右,用Zstandard压到0.6PB左右,用Gzip压到0.7PB。假设存储单价是每TB每月20元,那么LZ4和Gzip的方案相比,一年就要多花近100万的存储成本。但如果你的集群CPU常年跑不满,而查询任务需要不停地读数据,那多出来的几百TB空间也许比不上硬生生的读取速度优势重要。在大数据架构里,没有绝对的好坏,只有合不合适的成本模型。
1.3 压缩算法选型会影响下游任务链
这点是最容易被忽略的坑。压缩格式不只是写在存储层面,它还会影响下游的计算引擎能不能高效地并行处理。最典型的就是文件能否切片(Splittable),有些压缩格式在HDFS上是没法做块级切分的,这会导致一个超大压缩文件只能被一个Map任务读取,你的Spark或MapReduce并行度直接被拉回到串行时代。
我见过不止一次,有人图省事把几百GB的大表统一压成Bzip2,平时查询还没感觉,一到数据倾斜的Task就特别慢。后来一排查,才发现是Bzip2的单一文件无法切片,整份数据只能由一个Task读,性能和容量完全不成正比。
提示:在大数据架构里选择压缩算法,必须把“压缩率”“压缩/解压速度”“CPU开销”“是否支持切片”“与存储格式的兼容性”这五个维度一起看,缺一个都可能埋雷。
2. 主流压缩算法能力图谱与核心差异
先把常见的压缩算法摆出来,Snappy、LZ4、Gzip、Zstandard、Bzip2、LZMA,这是我在实际架构里见得最多的六位。给它们做个横向对比,大概能看出各自的性格。
| 压缩算法 | 压缩比(典型) | 压缩速度 | 解压速度 | CPU开销 | 是否支持切片 | 常见出场场景 |
|---|---|---|---|---|---|---|
| Snappy | 2.0~2.9 | 极快 | 极快 | 低 | 取决于容器格式 | HBase、Parquet默认 |
| LZ4 | 2.1~3.0 | 极快 | 极快 | 很低 | 取决于容器格式 | Kafka、Redis、实时管道 |
| Gzip | 3.0~4.5 | 较慢 | 快 | 中高 | 不支持单文件切片 | 传统Hive表、日志归档 |
| Zstandard(zstd) | 3.0~5.0 | 快(可调) | 快 | 中 | 取决于容器格式 | 冷热数据兼备的数据湖 |
| Bzip2 | 4.0~6.0 | 很慢 | 较慢 | 很高 | 支持(块级) | 极少使用,趋向淘汰 |
| LZMA | 5.0~7.0+ | 很慢 | 较快 | 极高 | 压缩层不支持 | 离线归档、压缩比极度敏感 |
这些数字不是绝对精确,实际值会随数据特征波动,但数量级关系是稳定的——压缩比越高,压缩往往越慢,解压不一定慢,但CPU吃得更狠。
2.1 Snappy 和 LZ4:极速派代表
Snappy是Google开源出来的,目标是“高速压缩”而不是极限压缩。它的算法思路非常直白,基于LZ77的变体,对重复字节串做替换,不搞特别复杂的熵编码,所以压缩速度能跑到几百MB每秒甚至上GB每秒。
LZ4和Snappy定位几乎一模一样,但它的压缩和解压速度通常比Snappy还要快一点,尤其是解压速度,基本可以跟着内存带宽跑。代价是压缩率稍微低一点点,但在很多场景里这点差异可以忽略。
为什么这两个是“快但省”的最佳选择。因为它们在大数据链路里最常见的用途不是省存储,而是减少网络IO和磁盘IO。数据在节点间传输时,用LZ4把数据“收窄”一半,网络耗时几乎立减一半,而CPU多花的那点时间,相对于网络延迟来说简直是免费。
2.2 Gzip 和 Zstandard:平衡与进阶派
Gzip是经典的Deflate算法实现,压缩率比Snappy好了不少,但压缩速度只能算是中等偏下。在大数据里它的问题是文件不支持块级切片,所以如果你把一张大表存成一个Gzip压缩文件,MapReduce或Spark读取时只能一个Task顺序读,这在分布式架构里几乎是灾难。
但Gzip有一个很大的优势——兼容性极好。Linux系统自带、几乎所有工具都认识、Java生态最成熟,作为外部数据交换格式,Gzip仍然是一个下限极高的选择。
Zstandard是Facebook开源的新一代算法,本质上做了两件事:一是用更聪明的有限状态熵编码器替代了Huffman编码,二是支持从1到22的压缩级别可调。在同样的压缩级别下,它的压缩速度和压缩率都能比Gzip好出一截,压缩率接近甚至超过Bzip2,速度却和Snappy一个量级。因为支持预设字典和长距离匹配,它对重复度较高的数据效果非常突出。要说当前大数据架构里最值得关注的压缩算法,我首推Zstandard。
2.3 Bzip2 和 LZMA:压缩比极限派
Bzip2走的是BWT变换路线,把文本数据进行块排序后再做熵编码,压缩率确实不错,而且天然是块级结构、支持切片。但它的致命问题是压缩和解压速度都太慢,在现在的数据规模下有点扛不住。除非你的场景极端到“存储空间比计算资源金贵得多”,否则我不推荐主动选它。
LZMA用的是LZ77加马尔可夫链的区间编码,压缩比在大数据全家桶里基本是最高的,但压缩时CPU和内存吃得也最狠,正常场景下用它压一个几十GB的文件,光是压缩时间就能把人等崩溃。它更适合一次性归档、极少读取的冷数据,而且是在离线任务里预处理,不在实时链路上玩。
实操心得:我自己在选型时有一条简单经验——如果数据要被反复读取分析,优先保解压速度和CPU预算;如果数据只是躺着睡大觉,才考虑极限压缩率。数据有没有人看,比它能压多小重要得多。
3. 各组件默认压缩方案速查与调整方法
大数据架构不是一个孤立的组件,而是一套流水线。压缩算法选型必须跟着每个组件的角色走,因为不同组件面对的IO瓶颈差异极大。
3.1 Hadoop/HDFS 侧的压缩配置
HDFS本身不感知文件的压缩格式,它只负责把文件块存下来。但MapReduce/Spark框架在读取时,需要知道某个文件用什么解压器。所以在Hadoop生态里,压缩配置有两层意思:一是文件用什么格式存,二是框架层怎么注册对应的Codec类。
常见的Codec类有这几个:
org.apache.hadoop.io.compress.DefaultCodec(对应Gzip)org.apache.hadoop.io.compress.BZip2Codecorg.apache.hadoop.io.compress.SnappyCodecorg.apache.hadoop.io.compress.Lz4Codecorg.apache.hadoop.io.compress.ZStandardCodec
在Hive表里,你可以用TBLPROPERTIES ("orc.compress" = "ZLIB")或建表时的STORED AS PARQUET TBLPROPERTIES ("parquet.compression" = "SNAPPY")来指定存储层的压缩格式。如果什么都不配,默认值往往偏保守,例如Parquet默认Snappy,而ORC默认ZLIB。不同默认值意味着,同样一张表,用不同格式存储,性能差异非常显著。
3.2 Hive/Spark 侧的压缩配置
在Hive里,中间结果和最终输出的压缩建议分开配置:
-- 中间结果压缩,建议Snappy/LZ4,追求速度 SET hive.exec.compress.intermediate=true; SET hive.intermediate.compression.codec=org.apache.hadoop.io.compress.SnappyCodec; -- 最终输出压缩,可以根据下游使用方式决定 SET hive.exec.compress.output=true; SET hive.output.compression.codec=org.apache.hadoop.io.compress.ZStandardCodec;Spark侧类似,但参数名不太一样:
spark.conf.set("spark.sql.shuffle.partitions", "200") spark.conf.set("spark.shuffle.compress", "true") spark.conf.set("spark.shuffle.spill.compress", "true") spark.conf.set("spark.io.compression.codec", "lz4") spark.conf.set("spark.io.compression.zstd.level", "3")注意Spark的Shuffle压缩和RDD压缩是独立的开关,shuffle压缩往往比存储压缩对任务性能影响更大,因为Shuffle数据要先落盘再被下游拉取,瓶颈在磁盘和网络。这边用LZ4或者Snappy,对跑批任务的提速效果立竿见影。
3.3 Kafka 消息队列侧的压缩配置
Kafka的压缩发生在Producer端,Broker只是透明存储和转发。它支持Gzip、Snappy、LZ4、Zstandard四种压缩类型。Producer配置的compression.type选择直接影响消息体大小和吞吐量。
在Kafka里,消息往往比较小、重复度不高,国内大部分核心链路我都推荐配置lz4,它在低CPU开销下能拿到不错的压缩率。如果追求比LZ4更高一点的压缩,又想保持快速,zstd可以选,但要留意Kafka版本对zstd的支持情况。Gzip和Snappy在Kafka里较少作为首选,因为要么CPU贵,要么压缩率稍逊。
3.4 ClickHouse 列式存储侧的压缩选型
ClickHouse的默认压缩策略很有意思,老版本默认用的是LZ4,后来又允许你设为ZSTD。它是在列级别做的压缩,每一列独立选择压缩算法,天然支持压缩级别的调优。
建表时可以直接指定CODEC:
CREATE TABLE events ( event_time DateTime CODEC(ZSTD(1)), user_id UInt64 CODEC(ZSTD(3)), payload String CODEC(ZSTD(12)) ) ENGINE = MergeTree ORDER BY event_time;ZSTD(1)表示压缩级别1,速度快压缩率低;ZSTD(12)压缩率高但CPU开销猛增。相当于同一张表的每一列,你能按数据特征独立搭配,这在其他引擎里还真不常见。
注意:ClickHouse里的CODEC配置一定要结合列的数据基数来选择。比如枚举值很少的低基数列,用Delta+LZ4的组合往往比纯ZSTD更快;而文本类高基数大字段,ZSTD(3)才是比较好的平衡点。
4. 不同业务场景的选型逻辑与实战建议
压缩算法没有万金油,每个场景的流量特征、读写比例、成本敏感度不同,选型就不同。下面直接按业务场景拆解。
4.1 冷数据归档场景:压缩率优先于一切
如果你的数据是“日志归档”“历史订单快照”这类常年没人查询的文件,那么存储成本就是最大的成本项,这时候压缩率就是王。Zstandard的高级别或LZMA都是合理的选型方式,压缩一次,反复受益。
实操中我建议把归档任务做成离线预处理,用Spark或MR批量读取原始数据,在写入归档目录时指定ZSTD的高级别压缩:
// 伪代码,表示在归档时按需指定压缩级别 df.write .option("compression", "zstd") .option("spark.hadoop.zstd.level", "12") .parquet("/archive/events/2024/")如果数据是文本日志,也可以不打Parquet直接压成Zstandard的单文件,反正半年没人看一次。这里的关键是和下游查询方约定好格式,避免归档一时爽、解压两行泪。
4.2 热数据在线分析场景:解压速度和CPU占用决定查询体验
热数据就是业务经常跑查询、报表、即席分析的数据,这类场景的核心诉求是查询快。查询慢不只是读到的字节数多,更在于解压Barrier把CPU打满。所以我推荐Parquet/ORC加Snappy或LZ4的组合,或者试试ZSTD(1),再或者ZSTD(3)配合高并发CPU充足的环境。
在实际测试里,TPC-DS这类分析型负载下,同样的Parquet表,Snappy和ZSTD(3)的查询耗时差别通常在10%以内,但ZSTD(3)能省下近30%的存储空间。如果你的集群CPU余量充足,ZSTD的低级别是热数据的隐藏优等生。
4.3 日志与流式管道场景:吞吐优先,兼顾实时性
日志/流式管道的特征是无休止地写、偶尔读,每秒钟可能要处理几十万条消息。这类场景既要压缩降低网络和存储压力,又不能因为CPU压缩太慢拖垮整体吞吐。LZ4在这里几乎是天然最优解,压缩和解压都快,不会让Producer变成瓶颈。
如果日志量巨大而且需要长时间保留,可以考虑虚实结合:实时链路用LZ4快速写入,再通过离线压缩任务把7天前的日志重写成ZSTD格式归档。这样实时性不损失,长期成本也控制住了。
4.4 边缘计算与终端设备场景:资源受限下的压缩取舍
最近“边缘计算盒子”这个概念很热,在数据采集盒子、边缘网关这类设备上,CPU和内存都非常脆弱,而且通常会伴随带宽贵、延迟敏感等问题。压缩算法在这里的取舍,更多要关注两点:解压时的计算开销和内存占用。
在边缘设备上,我一般不建议上Gzip或者LZMA,因为这些算法的CPU占用率对嵌入式芯片很不友好。更稳的做法是LZ4,它几乎不需要额外内存,压缩和解压极快;如果云端存储很贵,可以考虑ZSTD的低级别,比如level 1或3,它在大多数嵌入式芯片上仍能跑得动,同时压缩率比LZ4好。
实操心得:边缘盒子上做选型,不要只看压缩率,还要看这个压缩算法对内存的占用。很多嵌入式设备的可用内存就几百MB,如果你选了一个需要几十MB预设字典的算法,其他程序就没法活了。实测下来,LZ4在树莓派和各类ARM盒子上的表现都非常稳,这也就是它在物联网链路里那么常见的原因。
5. 选型实操:评估流程与切换落地
前面把原理和场景讲清楚了,最终还是得落到“我怎么在自己的环境里验证、怎么平滑切换”这一步。
5.1 如何在真实数据上做基准测试
不要直接相信网上的Benchmark,因为压缩率极度依赖数据特征。必须用自己的数据测,而且要在真实的目标架构上跑一遍。
我建议做一个最简测试脚本,从正式数据集里抽样10GB左右,用不同的压缩算法分别压一遍,记录大小和耗时。比如在Linux环境下可以直接用工具对比:
# 抽样文件 head -c 10G input_data.csv > sample.csv # 分别压缩 time gzip -k -1 sample.csv -c > sample.csv.gz time zstd -k -3 sample.csv -c > sample.csv.zst time lz4 -k -1 sample.csv -c > sample.csv.lz4 # 查看压缩后大小 ls -lh sample.csv.*这样能很快看出每个算法在你数据上的实际压缩率和大致耗时。压缩率要按真实数据算,不能拍脑袋。有的数据重复度极高,Snappy都能压出60%的降幅,而有些随机性强的数据,哪怕ZSTD最高级别也压不动,这时候就别在压缩上做文章了,还得靠分区裁剪和列裁剪来解决。
5.2 用“综合成本”替代“单一指标”评估
我建议做一张简单的评分表,给不同候选算法打分,权重基于你的实际情况来定。比如:
- 存储成本权重 0.3
- 查询/写入性能权重 0.4
- CPU占用权重 0.2
- 兼容性权重 0.1
然后把每种算法对应的指标归一化后加权求和。这样做的好处是,逼着团队的所有成员把各自的诉求摆到桌面上,而不是谁嗓门大听谁的。
当年我们把核心数据仓库从Gzip切到ZSTD的时候,就是用这个方式说服了存储团队。他们一开始担心ZSTD的解压速度不达标,但实测在同样的集群上,ZSTD(3)的解压速度比Gzip还要快,压缩率和CPU开销反而优于老方案,最终全票通过评审。
5.3 线上切换的几个稳妥步骤
压缩格式切换不是“改一行配置”就能完事的,在大数据架构里,它可能会影响下游所有读数据的任务。我建议按这样几步走:
- 分区并行:在Hive或者数据湖表上,用时间分区做灰度。新分区用新压缩格式,老分区暂时不动。下游查询如果没感知,说明Schema兼容;即使有问题,也只影响新增数据。
- 下游任务兼容测试:先跑一遍读取新格式数据的离线任务,确认Spark/Hive/Trino都能正常识别。注意有些老版本引擎对ZSTD的Codec注册不全,要提前打补丁或加参数。
- 双跑与校验:新老格式数据同时跑几天,对比行数、字段值抽样和查询结果,确保压缩层只是改编码,不影响数据内容。
- 数据迁移与归档:确认稳定后,用后台任务批量把老分区重写为新格式,再之后才真正回收老文件的存储空间。
5.4 写入优化:别让压缩成为写入链路的拥堵点
如果你用的是Kafka、Hudi或者Iceberg这类准实时管道,压缩发生在写入路径上,压缩算法的选型就更敏感了。例如Hudi的写入端默认启用了压缩,如果配置的压缩算法CPU开销过高,写入延迟和CPU争抢会明显放大。
在这些实时链路上,我的建议是:写路径用LZ4,压缩率和速度都比较稳;如果非要提高压缩率,用ZSTD(1)而不是ZSTD(3),因为写入链路里CPU的优先级永远要高于那少占的几GB磁盘。
6. 常见问题与排查技巧实录
最后把这几年遇到的典型问题和排查方法分享出来,也算帮大家少走一点弯路。
6.1 明明选了高压缩率,文件却没怎么变小
这通常不是算法选错,而是数据本身的可压缩性差。如果数据已经被加密、已随机化,或者内容本身的熵值很高,那什么算法都压不下来。遇到这种情况,先别急着换算法,应该抽样用zstd --ultra -22这种极限级别试一下,如果压缩率提升仍然很小,就说明数据本身的问题不是压缩算法的问题。
排查技巧:可以用
ent工具或 Python 的math.log2粗略估算数据熵值,如果信息熵接近8,意味着每个字节几乎都是随机数,压缩率基本无解。
6.2 单文件太大了,任务并行度上不去
压缩格式不支持切片是大数据场景最隐蔽的敌人。比如Gzip压缩的单个大文件,在Hadoop里没法拆分成多个Split,只能由一个Map任务读取,数据量一大,其他节点全在闲着。
解决方案有三个方向:换用支持块级切片的格式如Parquet/ORC;或者把文件提前切割成多个小文件分别压缩;或者用支持可切分的压缩容器,比如LZO、Bzip2和部分ZSTD的帧模式。
在实际业务里,最推荐直接把存储格式升级为Parquet/ORC,压缩层用ZSTD或Snappy,既解决了压缩问题,又顺带拿到了列式存储的谓词下推优化。
6.3 压缩后查询反而变慢了
有时候是你选的压缩算法解压速度太慢,有时候是存储格式的问题。比如用Bzip2或LZMA压缩的列式表,扫描时解压的CPU时间比磁盘IO省下来的时间还多,整体查询自然变慢。这是典型的**“省了空间却输掉时间”**。
遇到这个情况,建议把查询热点列的压缩降到ZSTD(1)或LZ4,非热点列保留高压缩级别。一张表里不同列用不同压缩算法,在ORC和ClickHouse里都是支持的,没必要全表统一。
6.4 Zstd Codec 报错或识别不了
老版本Hive/Spark对ZSTD的支持不完整,经常出现java.lang.ClassNotFoundException或者读数据乱码。这类问题大部分是Codec 类没有注册到Hadoop生态里。
排查步骤很简单:先确认Hadoop版本、Spark版本,如果组件较老,要么升级依赖,要么把hadoop-lzo或zstd-jni相关jar包放到集群的classpath中。另外注意,ZSTD压缩级别过高时,会产生较大的压缩帧,有可能触发读取端的超时或内存问题,这时候适当调低级别比优化代码更直接。
6.5 CPU 被压缩拖垮了
压缩率和CPU总是对立的。如果某一天你发现集群CPU长期高位,第一件事就是看看压缩配置是不是用了太激进的算法。把压缩等级从ZSTD(9)降到ZSTD(3),压缩率可能只下降5%,CPU开销却少了一半多。这种性价比极高的优化,值得每次做架构巡检的时候看一眼。
一些零碎但有用的心得
压缩算法选型这件事,特别像装修时选门窗:样式、材料、价格都不是孤立的,你得考虑这间房是给谁住、住多久、旁边有没有噪音源。最终选下来,每个人的答案都可能不同,但思考框架是一致的——先明确资源瓶颈,再做针对性测试,最后灰度上线。
我自己经历这么多次压缩优化之后,最大的一个体会是:你永远不该追求一个“最好”的压缩算法,而是要给每一层数据管道找到当时当下最匹配的那一个。数据热了,就把压缩级别调低,让它跑得更快;数据凉了,就调高,让它睡得更省地方。这个动态调整的思路,比一次性选个终极方案靠谱得多。
最后再分享一个小技巧:在你的数据任务跑批的低峰期,可以加一个定时任务,扫描所有表的存储格式和压缩Codec,把不符合新规范的表自动列出来。这个“压舱石报表”看起来不起眼,但它会在半年后帮你发现,原来有那么多临时表还在用Gzip压着几TB的近期数据,白白浪费着CPU和查询时间。