这题我太熟了。不管是搞数仓、做实时计算还是维护Hadoop集群,压缩格式选型几乎是每天都要碰的事。早些年大家无脑选Snappy,因为这玩意儿到处都支持,性能也稳;但这两年Zstandard(简称zstd)势头很猛,压缩率能跟gzip掰手腕,速度却快得离谱。我最早是在ClickHouse里用zstd,后来一步步把离线链路、实时链路的压缩策略都改成了按场景混合使用。
这篇就从一个数据平台架构师的实际视角,把Snappy和Zstandard从原理、性能、生态兼容到生产落地的那些门道一次说清楚。
1. 大数据架构里压缩技术到底卡在哪些环节
在展开算法对比之前,得先搞清楚压缩在大数据架构里到底影响什么。我之前带过一个数据平台项目,日增量大概几十TB,当时集群的存储增长曲线让人非常头疼。后来做了一次全链路压缩策略盘点,才发现问题远不止HDFS上那点数据文件的事。
1.1 大数据架构的四个层次,每一层都离不开压缩
通常讲大数据架构,大家都习惯分成四个层次:数据采集层、数据存储层、数据计算层、数据应用层。压缩在这四个层次里的角色完全不同,选错格式带来的后果也完全不一样。
数据采集层,典型组件是Kafka和Flume,这一层压缩主要解决网络带宽瓶颈。Kafka的broker之间同步副本时,如果消息不压缩,千兆网卡很快被打满,而CPU可能还有大量空闲。所以Kafka生产端通常会配compression.type=lz4或者zstd,这就涉及压缩算法在吞吐和CPU开销之间的取舍。
数据存储层,典型组件是HDFS、Hive表、Iceberg表、ClickHouse等。这一层压缩直接决定了存储成本,也决定了下游读取时的解压开销。HDFS上文件存的是Snappy还是Zstandard,扫表时的吞吐可能差出两三倍。
数据计算层,主要是Spark、Flink、Presto这些引擎读取压缩文件做计算。这一层压缩影响的不是存储,而是CPU和I/O的平衡。有些压缩算法解压太慢,会把计算节点的CPU直接打满,反而拖慢整个作业。
数据应用层,比如OLAP引擎的响应时间、在线服务的查询延迟,这一层对解压延迟极度敏感。ClickHouse里用zstd还是LZ4,小查询延迟能差出一个数量级。
一句话总结:压缩选型不是"选个算法"那么简单,而是要在存储成本、CPU开销、网络带宽、解压延迟之间找平衡点。很多团队只有一套压缩策略通吃所有场景,这其实是很浪费的。
1.2 压缩算法的核心矛盾:压缩率与速度的博弈
压缩这件事的本质,是用CPU时间换存储空间和I/O时间。算法设计上存在一条天然的光谱:
- 光谱左端是LZ4、Snappy这类追求极速的算法,压缩率一般,但CPU开销很小;
- 光谱右端是gzip、bzip2、xz这类追求极限压缩率的算法,压缩慢、解压也慢;
- 中间地带就是Zstandard,它在压缩率和速度之间重新画了一条曲线,让"既快又小"不再是梦。
理解了这个光谱,就能明白为什么没有一个算法能通吃所有场景。如果你的瓶颈是磁盘空间,选压缩率高的;如果你的瓶颈是CPU,选解压快的;如果两者都紧张,那就需要精打细算了。
我之前做过一个简单的基准测试,同一份Parquet数据(大约5GB),分别用Snappy和zstd(默认级别)压缩,结果存储从5GB降到1.8GB(Snappy)和1.2GB(zstd),而Spark作业的整体运行时间,zstd反而比Snappy快了一点,因为从磁盘读的数据量少了,磁盘I/O这个瓶颈被缓解了。这个结果当时让我挺意外的。
2. Snappy为什么能统治大数据生态这么多年
讲Snappy之前,得先说个容易混淆的点:Snappy是Google开源的一个压缩算法库,它不提供文件格式的封装。我们平时说"Snappy压缩的Parquet文件",实际含义是用Snappy算法对Parquet页内数据进行压缩,文件容器格式还是Parquet。这个区别很关键,因为很多人在排查问题时会把这个搞混。
2.1 Snappy的设计哲学:一切为了速度
Snappy的官方定位非常明确——它不是追求最高压缩率,而是追求极高的压缩速度和合理的压缩率。它的目标场景是"把压缩开销压到足够低,低到在引入压缩后整体系统仍然是净收益"。
Snappy的压缩率通常在2到3倍之间,纯文本日志数据可能更高一点,但对已经压缩过的数据(比如图片、加密数据)基本无效。它的强项在于压缩和解压都非常快,尤其是解压速度,几乎能达到内存带宽的极限水平。
在大数据生态里,Snappy能火这么多年,核心原因其实三个字:生态好。HDFS、Hive、Parquet、ORC、Kafka、HBase、Cassandra,几乎所有你能叫得上名字的大数据组件默认都支持Snappy,而且支持得非常稳定。对平台团队来说,选Snappy意味着几乎不会遇到"这个组件不支持那个格式"的尴尬。
2.2 Snappy的技术原理简述:LZ77变体加上哈希表
Snappy的核心思路是LZ77变体:扫描输入数据,用哈希表查找之前出现过的字符串,找到匹配就输出"距离+长度"的标记,找不到就原样输出字面量。这个过程不涉及哈夫曼编码,所以整体非常轻量。
用大白话解释:压缩算法就像在写"引用笔记"。比如你在一篇文章里反复写"数据湖"这个词,Snappy就把第一次出现的完整"数据湖"记录下来,后面再出现时只写"见第5页第3段的那个词",这样篇幅就小了。Snappy用的是固定大小的哈希表,匹配窗口相对有限,所以压缩率比gzip低,但速度就快在处理逻辑简单、哈希计算开销小。
需要说明的是,Snappy的实现本身是C++写的,JNI的org.xerial.snappy库在JVM生态里被广泛使用,所以Spark、Hive跑在JVM上也能无缝使用Snappy。
2.3 Snappy在Kafka和HDFS中的实战定位
我可以直接给出生产经验:Kafka的线上topic,如果对延迟极度敏感、CPU资源又比较紧张,Snappy仍然是很好的选择。因为Kafka的解压发生在consumer端,consumer集群的CPU被解压占满的情况我见过不止一次,这时候用Snappy能把CPU占用降到最低。
而HDFS上如果存的是Parquet格式的列存数据,Snappy依然是很多团队的基线配置,原因就是读数据时解压快,扫描性能稳定。缺点是存储占用比zstd高,磁盘成本会多出30%左右。如果你的磁盘还没到瓶颈,Snappy完全够用;如果磁盘成本压力大,下面出场的主角就更有吸引力了。
3. Zstandard(zstd)凭什么成为新宠
Zstandard是Facebook在2016年开源的压缩算法,作者Yann Collet之前写过LZ4。这个背景很重要——LZ4追求极致速度,而Zstandard的目标是"在很宽的压缩率和速度区间内,提供比gzip更高的压缩率和接近LZ4的速度"。
3.1 Zstandard核心原理:有限状态熵编码与窗口参数
Zstandard的基本框架还是LZ77,它先把数据解析成字面量和匹配对,然后用一个叫FSE(有限状态熵)的编码器对结果做熵编码。FSE是ANS(非对称数字系统)的一种具体实现,比哈夫曼编码更接近香农极限,也就是能用更少的bit表达同样的信息。
打个比方,哈夫曼编码像给常用字分配短编号,生僻字分配长编号;FSE则是进一步利用符号出现概率的具体分布来分配bit,连"概率比例"都编码进去了,所以压缩率更高。Zstd还在后面加了由--long参数支持的超大窗口匹配,专门对付几十GB级别的重复模式数据。
最核心的亮点是:zstd支持从1到22共22个压缩级别,而且每个级别背后是一组精心调过的参数(搜索深度、哈希链长度、窗口大小等)。级别越高,压缩率越高,但压缩速度越慢;解压速度在所有级别下都基本一致。这个设计和gzip完全不同——gzip调-9,解压也变慢;zstd调高压缩级别,解压性能几乎不受影响,这是它替代gzip的最大底气。
3.2 与gzip的对比:压缩率、速度、CPU的多维数据
我直接给一组我实测的数据,环境是20核CPU的Linux服务器,样本是Web日志(纯文本,约1GB):
| 算法 | 压缩后大小 | 压缩速度 | 解压速度 | 压缩CPU占用 |
|---|---|---|---|---|
| gzip -6 | 约210MB | 约30MB/s | 约180MB/s | 高 |
| gzip -9 | 约197MB | 约12MB/s | 约170MB/s | 很高 |
| Snappy | 约360MB | 约450MB/s | 约800MB/s | 低 |
| zstd -3 | 约270MB | 约300MB/s | 约600MB/s | 中低 |
| zstd -9 | 约190MB | 约45MB/s | 约600MB/s | 高 |
| zstd -19 | 约178MB | 约8MB/s | 约600MB/s | 很高 |
从这个表能看到几个关键结论:
- zstd在默认级别附近(
-3),压缩率已经超越gzip,速度却比gzip快一个数量级; - zstd把压缩级别拉到
-9,压缩率就追平甚至超越gzip -9,速度还是比gzip -9快接近4倍; - zstd的解压速度在所有级别下都在600MB/s上下,而gzip解压只有不到200MB/s。这意味着如果你频繁读数据,zstd的解压开销远低于gzip。
3.3 生产环境用zstd的三种典型姿势
姿势一:Kafka消息压缩。Kafka从2.1版本开始原生支持zstd,生产端配置compression.type=zstd即可。zstd在Kafka场景的优势是兼顾压缩率和CPU开销,特别是跨机房复制时,带宽成本降幅明显,我实测压缩率比lz4提升约20%到30%。
姿势二:Parquet/ORC文件格式。Parquet在较新版本里已经原生支持zstd通道,Hive、Spark、Presto这些引擎基本都能读了。ORC对zstd的支持更早,Hive在较新版本里可以直接指定"orc.compress"="zstd"。
姿势三:ClickHouse / RocksDB等OLAP与KV存储。ClickHouse最推荐的是zstd,压缩率比LZ4高很多,但查询性能依然很好,因为ClickHouse的压缩是列级块压缩,zstd能拿到很高的压缩收益。
4. 一次真实压测:同一份数据在Hive + Spark下的全链路对比
前面聊了不少原理,这部分把我做过的一次完整选型测试复盘一下。那时候团队里正好有个数仓重构项目,ODS层的原始数据大概20TB,在HDFS上以Parquet存储,用的是Snappy。因为存储成本上涨明显,我就专门组织了一次压缩算法全链条压测。
4.1 测试环境与数据样本设计
测试集群是8台物理机,每台CPU 32核、内存128GB,HDFS副本数为3,Spark版本3.3,Hive版本3.1.2,样本是我从线上抽取的ODS层日志数据,通过Hive CTAS语法生成不同压缩格式的表格:
CREATE TABLE ods_sample_snappy STORED AS PARQUET TBLPROPERTIES ('parquet.compression'='SNAPPY') AS SELECT * FROM ods_source_sample; CREATE TABLE ods_sample_zstd STORED AS PARQUET TBLPROPERTIES ('parquet.compression'='ZSTD') AS SELECT * FROM ods_source_sample;这里有个要注意的问题:Parquet的压缩配置在不同版本里可能写法不同,有的版本用parquet.compression,有的用parquet.compression.codec,建议先SHOW TBLPROPERTIES确认。
4.2 压缩率对比:存储成本直接降了33%
生成完成后,我立刻执行了目录级统计:
hdfs dfs -du -h /warehouse/ods_sample_snappy hdfs dfs -du -h /warehouse/ods_sample_zstd结果非常直观:Snappy版约5.8TB,zstd默认级别约3.9TB,存储少了约33%。对一个20TB的ODS层来说,这意味节省出了两个多TB的存储,半年下来能省不少机器或云存储费用。
注意我这里用的是zstd默认级别,也就是-3。如果我把压缩级别调到-9,存储还能再降一点,但写入时的CPU开销会明显上升。对于ODS层这种"写一次、读多次"的数据,用略高的压缩级别划算;对于实时写入的场景,级别必须调低。
4.3 查询性能对比:I/O减少带来的意外收益
接下来是关键:用相同的Spark SQL统计任务去扫全表,比如计算某个字段的去重用户数,看运行时间。
- Snappy版表的扫描时间约12分钟;
- zstd版表的扫描时间约10分钟。
这个结果出乎团队很多人的意料:压缩率更高、数据量更小的zstd表,跑起来反而比Snappy更快。原因是Spark扫描时最大的瓶颈是HDFS网络I/O和磁盘I/O,zstd文件体积小了三分之一,I/O压力大幅缓解,虽然解压消耗了更多CPU,但集群CPU整体不紧张,所以净效果是变快了。
如果集群CPU非常紧张,解压开销可能会吃掉I/O节省的收益,结果就会反过来。所以做测试时一定要看自己的瓶颈到底在I/O还是CPU,不要照搬别人的结论。
5. 压缩格式选型决策:给出可以直接用的判断标准
看完前面的测试,肯定会有朋友问:那我到底该用Snappy还是zstd?这里我整理了一个决策框架,按这个思路去选,基本不会出大错。
5.1 五个判断维度:数据温度、查询模式、CPU水位、存储成本、生态限制
第一个维度是数据温度:热数据(频繁读取)优先考虑解压速度和查询性能,Snappy或低级别zstd更优;冷数据(很少读取但占用存储)优先考虑压缩率,zstd -9甚至-19更划算。
第二个维度是查询模式:如果查询是罕见的全表扫描,比如数仓里的定期ETL,压缩率高的zstd优势明显;如果查询是高频点查,比如在线业务,解压延迟敏感的Snappy或LZ4更稳妥。
第三个维度是CPU水位:集群CPU使用率长期在70%以上,解压开销不能太大,Snappy优先;集群CPU有大量空闲、磁盘I/O经常打满,zstd的I/O节省能显著改善作业时间。
第四个维度是存储成本:如果存储成本占总成本比重很大,比如上云后按量付费,zstd几乎是必选项,因为33%的存储节省是实打实的钱。
第五个维度是生态限制:如果下游是一个比较老的Hive版本、或者某个商业BI工具不认zstd编码的Parquet,那就别勉强,兼容性问题比性能损失更麻烦,Snappy这种"全环境通吃"的格式反而省心。
5.2 常见场景的推荐配置
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| HDFS离线数仓(ODS层) | Parquet + zstd -9 | 写少读多,压缩率优先,I/O节省明显 |
| HDFS离线数仓(DWD层) | Parquet + zstd -3 | 需要平衡查询响应和存储成本 |
| Kafka实时消息 | zstd或lz4 | 带宽瓶颈先用zstd,CPU紧张换lz4 |
| 高频OLAP点查 | ClickHouse + zstd(LZ4) | 延迟敏感,按列块选择 |
| 跨机房传输 | zstd -19 | 压缩率优先,但CPU开销要测试确认 |
| 临时测试表 | Snappy | 追求最快的写入速度,不求压缩率 |
5.3 从Snappy迁移到zstd的三种平滑方案
如果决定从Snappy切到zstd,不建议直接停掉旧格式,我实际用下来最稳的是这三种方案。
方案一是分层迁移,先只对新增分区使用zstd,保留历史分区不动。比如日期分区表,新一天的数据写zstd,旧数据用后台任务慢慢重写。好处是风险隔离,缺点是存储收益分阶段释放。
方案二是双跑验证,在一个单独的表空间里把关键表格用zstd重建一份,跑一周生产任务做对比,核对数据一致性、任务耗时、资源占用。没问题再切换调度依赖。
方案三是TBLPROPERTIES热切换,Hive和Spark对Parquet格式都支持在表属性里直接指定压缩算法,可以先只改表属性让新写入的数据用zstd,但要注意旧数据如果还是Snappy,读的时候引擎会自动识别,问题不大。
6. 生产环境里的那些坑:zstd落地的隐藏成本
zstd虽然好,但不是银弹。我在推进zstd落地的过程中踩过、也看同事踩过不少坑,这里挑几个有代表性的讲。
6.1 压缩级别选太高,写入慢到怀疑人生
这是最常见的坑。有人一看到zstd压缩率高于gzip,就直接上-19,结果离线作业本来20分钟跑完,变成两小时还没结束。原因很简单:zstd -19的压缩速度可能只有几分钟才压完1GB,这个开销在大规模数据写入场景下是完全不可接受的。
我的建议是:ODS层批量写用zstd -9最多,实时写入用zstd -3,离线一次性压缩归档才用zstd -19。而且不要凭感觉选,生产前用真实数据跑一下分段测试,看看每个级别下的耗时和压缩率。
6.2 下游组件不支持zstd,查询报兼容性错误
千万别以为Parquet支持zstd,所有引擎就都能读。Parquet格式里zstd编码是后来才加入的,如果你的Presto版本比较老、或者Impala版本久远,读zstd编码的文件直接报Unsupported codec。
处理办法是摸清整条数据链路里所有组件的版本,尤其注意Hive、Spark、Presto、Impala、Flink这几类。有一个原则:宁可在上游用Snappy保兼容,也不要在下游排查两小时发现是版本问题。
6.3 CPU紧急场景:解压开销会在高峰期凸显
实时链路高峰时,consumer端解压zstd数据的CPU开销比Snappy高,我有一次碰到凌晨峰值流量时consumer延迟上涨,一查是zstd解压把CPU打到了80%。后来把该topic切回lz4,延迟立刻回落。
这类问题比较隐蔽,因为平时压测时CPU并不紧张,但线上峰值流量一来,任何额外的CPU开销都会被放大。实时链路的核心是延迟,不是存储成本,这里的压缩选型要谨慎。
6.4 压缩策略监控:三个必须盯紧的指标
无论最终选了哪种算法,建议把这三个指标接到监控平台里:
- 压缩率(compression ratio):通过表大小/原始数据估算,比如HDFS目录大小变化;
- 压缩和解压耗时(通过引擎日志的Metrics观察);
- CPU使用率和I/O等待时间(结合集群监控)。
我有一次发现某个重要topic的存储涨得很快,一查是某次升级把压缩配置覆盖了,消息变成不压缩裸传,存储和带宽成本立刻飙升。如果监控里有压缩率指标,这个问题当天就能发现。压缩配置被"无意间改掉"这种事,在大数据平台里发生的概率真的不低。
7. 几个主流组件的zstd配置备忘
写到这里,把主流组件开启zstd配置直接列出来,方便当手册用。
7.1 Hive/Spark:表级别和会话级别
Hive建表时指定:
CREATE TABLE ods_user ( uid STRING, name STRING ) STORED AS PARQUET TBLPROPERTIES ('parquet.compression'='ZSTD');Spark写入时在会话级指定:
spark.conf.set("spark.sql.parquet.compression.codec", "zstd") spark.conf.set("spark.sql.orc.compression.codec", "zstd")如果你的Spark版本高于3.2,还可以通过spark.sql.parquet.compression.codec.zstd.level设置zstd具体级别,默认是1(注意,不同版本Level默认值不一样,最好显式设置)。
7.2 Kafka:broker端和生产端
Broker端全局配置,改server.properties:
compression.type=zstd生产端按topic覆盖:
props.put("compression.type", "zstd"); props.put("compression.zstd.level", "3");如果消费者版本太老不支持zstd,会出现"Unknown compression type"的报错,升级客户端版本前先确认线的链路版本。
7.3 Parquet/ORC独立配置
写Parquet时(比如用PyArrow):
import pyarrow as pa import pyarrow.parquet as pq table = pa.table({"col": range(1000000)}) pq.write_table(table, "data.parquet", compression="ZSTD", compression_level=3)写ORC时(Hive SQL):
CREATE TABLE orc_zstd ... STORED AS ORC TBLPROPERTIES ('orc.compress'='ZSTD');如果出现"zstd library not loaded"之类的异常,通常是因为底层原生库没有正确链接,检查libzstd.so是否存在、环境变量LD_LIBRARY_PATH是否正确即可。
7.4 文件级别手动测试
如果你只是想在命令行快速验证一下某个文件用zstd压缩能省多少空间:
# 压缩 zstd -3 -o output.zst input.log # 查看压缩率 zstd -l output.zst # 解压且保留源文件 zstd -d output.zst -o restored.log级别-3是日常性价比最高的档位,-19适合离线归档。注意zstd命令行压缩出来的是独立的.zst文件格式,和Parquet内部的zstd编码并不是一回事,不要把二者混为一谈。
8. 最后说点个人体会
数据压缩这事,看起来是选个算法的小事,实际上是牵一发动全身的平台决策。我见过太多团队,压缩格式随便定了一个,之后三年都没人再过问,直到存储账单爆炸才想起来优化,那时候要改全量数据的压缩格式,工作量就非常大了。
从Snappy到Zstandard,本质上是大数据生态对"平衡"的理解在升级:以前我们默认"快就是一切",现在越来越多人意识到,"又快又省"才是真正的爽点。zstd不是要取代Snappy在所有场景的地位,而是给了架构师一个更宽的调节旋钮,让你能根据数据温度、CPU余量、存储成本去微调,这种"可调节性"才是它最大的价值。
我的建议很简单:先建一条监控基线,摸清自己的数据和负载特征,再用小范围分区做真实压测,最后渐进式切换。不要盲目跟风,也不要固步自封。压缩算法选得好,省下来的不只是字节数,更是集群的寿命和团队的排障时间。