
做大数据平台的这几年我几乎把主流的压缩算法都轮了一遍。从最早默认不用压缩到后来被磁盘和带宽逼着上Snappy再到现在Zstandardzstd逐渐成为默认选项这个选型过程里其实踩了不少坑。很多人问“Snappy和Zstandard到底选哪个”这个问题问得越多说明大家越意识到在分布式架构里压缩根本就不是一个简单的“省空间”问题它直接影响HDFS存储成本、Shuffle网络开销、任务执行时间和CPU使用率甚至能决定一个Spark任务能不能在资源限制内跑完。我最初接触Snappy是因为它在Hadoop生态里几乎无处不在——HDFS、Parquet、Kafka、HBase全默认支持而且快得像个跑车。后来Zstd出来Facebook开源带着可调压缩级别和逆天的解压速度一下子把“压缩率”和“速度”这对冤家撮合到了一起。项目从试用zstd-3到全线替换Snappy中间做了非常多的压测和调参这篇文章就是把我的实测数据、配置细节和踩坑记录完整拆给你希望能给正在选型或者准备调优的朋友一个落地的参考。1. 大数据场景下的压缩选择为什么总绕不开这两兄弟1.1 分布式架构里压缩到底在解决什么问题先聊一个很朴素的问题为什么大数据架构必须关注压缩数据量一旦上了PB级别存储成本和网络带宽才是真正的瓶颈。一个TPC-DS测试生成的1GB原始数据存成Parquet用Snappy压缩后可能只有300MB用Zstd默认级别能压到250MB甚至更低。别小看这50MB的差距在几PB的数据规模下就是几百TB的硬盘开支也是好几个机架的网络吞吐量差距。更关键的是压缩不只是影响存储它还影响计算。在MapReduce或Spark的Shuffle阶段Map端输出的中间结果如果不压缩网络传输和磁盘IO会被拉爆如果压缩CPU又要多干活。这就是典型的时间和空间交换而不同压缩算法的“交换效率”天差地别。Snappy当初设计的初衷就是极致速度宁可压缩率差点Zstd则想两者兼得——用现代熵编码技术让高压缩率和高速度并存。选错了轻则集群跑慢重则任务直接OOM。1.2 为什么是这两个算法而不是LZO或Gzip早期Hadoop生态里其实还有LZO和Gzip在竞争。LZO虽然压缩速度比Snappy还快但有一个致命伤——GPL协议让它在商业发行版里很难用而且Hadoop的LzoCodec通常需要额外装hadoop-lzo库光编译就能坑死一批人。Gzip压缩率确实高能达到70%甚至更高的压缩比但压缩和解压速度都不够看在分布式场景下CPU资源太贵根本耗不起。所以你会发现生态里最常被嵌入的其实是Snappy和Zstd。原因很直白两者的许可证都友好Snappy是BSDZstd是BSDGPL dualHadoop内建支持Spark、Flink、Hive、Parquet、Kafka这些组件几乎都能通过配置直接启用。再一个它们都提供native C实现并且有稳定的JNI绑定大数据框架能够无缝调用底层的裸性能。Snappy赢在生态成熟和零配置上手Zstd赢在压缩率和速度的平衡上而且由于Zstd支持21个压缩级别1-3侧重快速10-19侧重极致压缩给了工程师巨大的调优空间。2. 从实现原理层面拆解Snappy和Zstandard到底差在哪2.1 Snappy的设计哲学极致速度省CPUSnappy是Google在2011年开源的它最初的目标就很明确不是追求最小体积而是追求“Thousand gigabytes per second”级别的吞吐。它的核心是LZ77变种只会做简单的匹配和替换把重复字节串用引用替代对剩余数据不做熵编码而是自己定义一个轻量的格式来记录匹配项和原文。这种设计带来的优势是极其明显的压缩算法本身只用很少的CPU周期解压速度更是无敌几乎可以做到内存带宽级别。我实测下来单机解压Snappy能达到2-3GB/s的吞吐。对于CPU密集型比如在Map端做实时压缩或对额外CPU开销敏感的场景这是杀手级优势。缺点是格式固定没有可调节压缩级别的说法——出厂即巅峰压缩率上限很低。相比Zstd默认级别同样数据Snappy体积通常要再大20%-35%这在存储成本高的场景下很吃亏。2.2 Zstd的技术底气可调级别加上现代熵编码ZstdZstandard是Facebook在2016年开源的它的核心思路是结合现代LZ77与有限状态熵编码FSEFSE可以把比特的浪费压缩到极致因此能在保持接近LZ77速度的同时实现远高于LZ77的压缩率。这是它能超越Snappy的关键所在。此外Zstd还支持字典训练、长距离匹配、多帧并行压缩这些都是Snappy完全没有的能力。Zstd最大特色在于压缩级别level从1到19Level 1-3是Fast模式4-9是Trade-off模式10-19是High compression模式。很多人的误区是zstd level 3一定比level 1慢很多虽然确实会慢一些但和Snappy比zstd level 3的压缩率能达到Snappy的两倍压缩速度却往往比Snappy还要快至少不相上下。这就要归功于FSE编码器的高效性以及它在实现层面对SIMD指令的深度优化。我额外要提一下ZSTD的解压速度几乎是恒定的不受压缩级别影响。这是它另一个战略优势——做数据湖或数仓时数据被反复scan高频解压才占大头。所以哪怕压缩慢一点解压快就能保证消费端流畅。2.3 核心对比表几个绕不开的关键差异维度SnappyZstandard (Zstd)压缩级别无固定策略1-19可调另有超长窗口--long典型压缩率TPC-DS/Parquet压缩后约为原始体积的30%-40%默认级别约为25%-35%高级别可达20%甚至更小压缩速度非常高设计目标就是内存带宽级level 1-3下可与Snappy持平或更快高级别明显更慢解压速度极高2-3GB/秒量级极高甚至比Snappy更快更稳定CPU占用压缩侧低低~中取决于级别内存占用低低~中高级别窗口更大字典支持无支持针对小记录可显著提升压缩率Hadoop集成度极其成熟成熟Hadoop 2.6支持需native库适用定位对延迟和CPU开销极度敏感希望兼顾压缩率、吞吐和可调性单看这个表其实还不够因为真实的选型还得结合你在架构里具体的使用位置来看。下面我用几个大数据组件里的实际场景来说。3. 在大数据组件里真实落地HDFS、Parquet、Kafka的选型差别3.1 HDFS文件存储用Zstd是肉眼可见地省钱在HDFS层面我们通常把压缩策略写在文件格式里最常见的就是Parquet和ORC。ORC原生和Hive绑定Parquet则是Spark生态的默认格式。在这两种列式格式里Snappy和Zstd都是最受欢迎的压缩选择。我建议在OLAP数仓和湖上直接用Zstd。我在一个300GB真实订单数据的Parquet表上做过对比Snappy压缩后约112GBZstd level 3压缩后约88GB存储省了20%以上。而压缩和解压耗时的差距其实很小Parquet写入时Snappy约耗时14分钟Zstd level 3约19分钟但读取扫描时Zstd反而比Snappy快一点。因为解压后数据量更小磁盘读得更快。对HDFS来说磁盘带宽通常比CPU珍贵Zstd把体积变小了后续的读取、拷贝、回收都会更快。3.2 Kafka消息队列压缩级别的选择就是网络带宽的输赢Kafka本身有compression.type参数支持snappy、zstd、gzip、lz4。在消息队列场景下压缩的收益主要通过网络流量和Broker磁盘占用体现。Kafka Producer在发送前压缩Consumer在拉取后解压这中间的CPU开销其实很小。我专门在Kafka 3.x版本里做过压测用一个高频埋点Topic每条消息约1KB吞吐量在10万条/秒左右。用Snappy时单Broker网卡流量约320Mbps改用Zstd压缩级别5之后流量降到220Mbps左右而Producer和Consumer CPU只增加了不到15%。对拥有跨机房复制的架构来说这30%带宽节省意味着可能少买一对专线。很多人一听说Zstd就想直接用最高级别这在Kafka里千万别犯傻。级别太高会把Producer的吞吐拉下来导致生产端积压我推荐先用zstd level 2或3优先保住生产端的快速滚动。3.3 Spark与Hive中间Shuffle对“速度敏感”场景Snappy依然有席位Shuffle阶段和存储代建是完全不同的场景。Map端输出压缩写磁盘和网络传输要快Task数少压缩开销串行在每个task里。如果压缩级别太高CPU会成为瓶颈整个作业反而被拖慢。我在Spark 3.x跑TPC-DS时明显感觉到如果shuffle压缩用zstd level 6以上大任务的Map端耗时增加显著甚至让整个DAG从12分钟涨到17分钟压缩率虽然高了但总执行时间上涨了40%。这种情况下Snappy仍然是最稳的选择。它不会给任务增加太多负担Map端写完立刻就能开始Shuffle传输。如果要想兼顾压缩率折中方案是使用zstd但把级别调到1或2spark.shuffle.compression.codeczstd配合spark.shuffle.io.compression.level1。我用这个组合跑了同样的TPC-DS时间几乎回到Snappy水平但Shuffle传输量少了大约18%。这算是用zstd替代snappy时性价比比较高的姿势。3.4 冷热分层离线归档选高级别Zstd数据湖或数仓通常有冷热分层比如热分区ParquetZstd-3冷分区用Zstd-15甚至Zstd-19存Orc/Hive表。为什么冷数据敢用高级别因为冷数据几乎不再频繁写入压缩耗时完全无所谓但查询扫描时解压速度恒定意味着你获得了一份比Snappy小近40%的归档文件而查询性能几乎不丢。我用过一个老订单冷分区表原始数据大小1.2TBSnappy压缩后约430GB改用Zstd-19后压到约290GB。一次离线补数任务跑了2个小时但之后仓库省下了140GB磁盘。对存储计费敏感的企业这就是实打实的成本压缩。另外提醒一句Zstd还有--long模式可以指定窗口大小如--long27来支持更大的重复数据匹配但会占用更多内存Spark executor如果内存不宽裕不要轻易启用。4. 用一轮真实压测手把手教你选压缩方案4.1 压测前的准备清单很多人在选型时喜欢拿官方Benchmark说话但官方数据往往是单机纯压缩测试跑集群和真实数据的时候完全另一回事。我自己做压测时从来只相信三个东西集群真实数据、完整执行链路、可重复的对比环境。建议准备一段真实业务数据样本至少10GB以上别用随机数随机数压缩率灌水严重。同一集群同样资源队列下跑任务避免人为干扰。为每个算法设置独立任务记录Total Execution Time、Shuffle Write Size、Shuffle Read Size、CPU Time、Peak Memory。对着HDFS看实际文件大小。如果只是快速验证压缩率和速度也可以先在命令行用snappy或Java和zstd工具做单文件测试但最终一定要回归到Spark或Hive任务里因为调度、序列化和堆内存都会影响真实体验。4.2 比较实用的几组基准数据下面是我在一个10节点集群每节点64GB内存16核上跑一个200GB原始日志ETL任务的实测记录。日志格式为JSONSchema较重复。统一用Parquet存储格式。参数SnappyZstd-1Zstd-3Zstd-6Parquet文件体积61GB52GB48GB44GB写入耗时分钟1112.515.522读取扫描耗时分钟8.27.86.56.3CPU Time总量核-小时145162203291Peak Executor Memory正常正常正常略高一眼就能看出Zstd-3是性价比最高的相比Snappy文件体积缩小21%读取扫描速度快了20%写入慢了4.5分钟CPU开销高了约40%。如果你的CPU预算充足Zstd-3和Zstd-6都很香如果集群CPU本身就很紧张从Snappy迁移到Zstd-1或Zstd-3更稳妥。4.3 从指标反推业务选型有了数据怎么判断用哪个给大家一个思路如果你的数据是热数据经常被BI、报表、即席查询扫描那么关注“读取扫描耗时”和“文件体积”Zstd-3基本可以无脑选。如果你的数据是高频写入的日志或埋点业务上对写入完成时间敏感优先保证“写入耗时”和“CPU Time”那Snappy或Zstd-1是更保险的选择。如果你的架构里Shuffle特别多比如频繁Join、重分区那么调低压缩级别甚至用Snappy然后通过观察Shuffle耗时来判断是否更换。如果你的瓶颈在跨机房带宽或存储容量而CPU长期空闲那Zstd-9甚至更高才是最优解。这里我特别想说一句别迷信某一种算法能覆盖所有场景。我用过不少项目最成功的做法是把压缩策略按数据层分拆实时摄入层用Snappy或zstd-1数据湖和仓库层用zstd-3归档层用zstd-15以上。这样一个成熟的压缩体系才让集群的每块资源都花在刀刃上。5. 配置与排障压缩相关代码怎么配、坑在哪5.1 在Hadoop与Spark中的相关配置项先列一份我常用的核心配置清单方便你直接抄作业Hadoop级别core-site.xmlproperty nameio.compression.codecs/name valueorg.apache.hadoop.io.compress.SnappyCodec,org.apache.hadoop.io.compress.ZStandardCodec/value /property property nameio.compression.codec.zstd.level/name value3/value /propertySpark SQL Parquetspark.sql.parquet.compression.codeczstd spark.sql.parquet.compression.codec.zstd.level3如果没有显式设置Spark新版本的Parquet默认就是Snappy一定要记得手动切到zstd。Kafka Producercompression.typezstd compression.zstd.level3Flink的Parquet Sink或Hadoop输出也可以直接指定扩展名或压缩器# 若使用StreamingFileSink PartTempFileWriter 对 parquet 写入可以用 CompressionCodecFactory 指定 ZStandardCodec5.2 配置最常见的坑找不到native库很多人在Hadoop上启用Zstd时第一步就栽了程序跑起来的时候要么报java.lang.UnsatisfiedLinkError: no zstd-jni in java.library.path要么日志提示Could not locate executable null/bin/winutils.exe之类但本质都是本机native库不完整。Hadoop自带的libhadoop.so必须链接到libzstd.so同时需要hadoop-zstd这个native库配合。最简单的解决办法是把Zstd的JNI jar如zstd-jni放到Hadoop的classpath然后确认系统的LD_LIBRARY_PATH包含Hadoop native目录。在HDP或CDH发行版里一般已经带好了但用开源Apache Hadoop裸装的会很容易踩这个坑。我自己的排查四步法如下跑hadoop checknative命令看zstd前面的true/false。如果显示false先安装系统zstd库yum install zstd或apt install libzstd-dev。确认hadoop-3.2.*以上的lib/native包含libhadoop.so且可以用ldd libhadoop.so | grep zstd看到已链接。重启所有节点再checknative看是否变true。同理Snappy如果在集群里报Unable to load libsnappy基本都是发行版没把snappy的native和java都放全。解决办法类似先装snappy系统库再把snappy-java放进classpath。5.3 高级别Zstd引发的Executor OOM问题还有一次在Spark上用Zstd-9压缩一个超大Parquet表直接导致几个Executor GC Overhead Limit。仔细查了一下发现Zstd在高压缩级别下会用到大量内存做搜索窗口和匹配表尤其是在未开启--long的情况下单任务内存占用比Snappy高出数倍。后来我把级别降回3并且把spark.sql.parquet.compression.codec.zstd.level显式设置为3问题迎刃而解。这里给你一个判断标准如果executor堆设了4GB用zstd-6以上做超大列存压缩风险会明显上升。要么降低级别要么处理数据前先执行repartition控制单个分片大小要么在Parquet写入时使用更小的行组parquet.block.size。如果业务上非要用高级别尽量选择离线、内存充足的冷备job别和生产热任务混跑。最后再分享一个我最近养成的小习惯每次给集群调完压缩参数都会顺手跑一个包含所有核心SQL的回归任务并且重点看两个指标——Shuffle Write Size和Executor CPU Time。压缩策略调优不是一锤子买卖随着集群规模和业务Schema的变化当初“最优”的配置可能半年后就变了。Zstd和Snappy的对比本身没有绝对输赢只有在你自己的数据、硬件和业务诉求里跑出来的结果才说了算。我现在的新项目里仓库和归档层已经从Snappy全面迁到Zstd实时链路上依然保留Snappy这个组合在我这儿是最舒服的。