ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大数据压缩选型:Snappy与Zstandard的对比与实战优化

大数据压缩选型:Snappy与Zstandard的对比与实战优化 压缩格式这件事在大数据集群里看起来只是一个小配置项实际上它直接影响作业跑多久、磁盘占多少、网络传输快不快。我之前帮一个团队优化Spark批处理作业时仅仅把shuffle压缩从Snappy换成Zstandard整个任务耗时缩了将近四成。这让我意识到很多人对Snappy和Zstandard的认知还停留在“一个快一个省”的模糊层面远没到能支撑选型决策的程度。这篇就把两种算法放在同一个工作台上彻底拆开设计原理、关键参数、组件配置、测试方法、踩坑经验都按可以直接复现的方式写出来希望对正在做大数据架构选型或集群调优的朋友有帮助。1. 为什么大数据架构绕不开压缩选型1.1 压缩解决的核心矛盾IO瓶颈与CPU预算先掰扯清楚压缩在大数据架构里到底解决什么问题。数据平台一旦上了规模存储量从TB走到PB最先暴露的瓶颈通常不是计算能力而是IO带宽和网络传输。一个几百GB的shuffle文件写入磁盘如果裸写磁盘吞吐会先被打满如果走网络传输千兆网卡甚至万兆网卡的数据量也会让人头疼。压缩在这里的作用就是把数据体积变小减少磁盘读写量和网络传输量用更少的物理资源承载更多的数据流动。代价自然也有就是CPU。压缩要消耗CPU做匹配和编码解压要消耗CPU做还原。一台机器上的CPU资源是有限的如果用了压缩率很高但速度很慢的算法原本IO瓶颈倒是解决了可是CPU先飙到100%作业反而更慢。所以压缩算法选型本质是平衡IO收益和CPU开销的一个决策。这个决策做得好集群整体吞吐能明显上一个台阶做得差轻则浪费资源重则把作业拖垮。以常见的MapReduce或Spark作业为例中间结果shuffle数据和最终落盘结果默认都可以压缩。如果写放大严重比如反复读写临时文件压缩对整体耗时的影响非常显著。这里的核心思路是不能只看压缩率一个指标速度、CPU、序列化框架兼容性都要一起看场景不同结论也不同。1.2 Snappy与Zstandard在生态中的位置在大数据生态里压缩算法选择其实不算多。老牌的gzip压缩率高但慢bzip2更极致但更慢LZO曾经在Hadoop里流行过但维护一般LZ4速度飞快但压缩率偏低Snappy则长期充当“默认值”角色很多团队不假思索地在Hive、Spark、Kafka里选用Snappy理由是它快且资源占用可控。但Snappy的压缩率确实很一般导致很多磁盘和网络开销其实是可以继续压缩的。Zstandard通常叫zstd在2016年前后由Facebook开源Yann Collet主导开发定位刚好卡在“压缩率接近gzip、速度接近LZ4”的中间地带。它最核心的设计是提供了从1到22的压缩等级用户可以根据业务对CPU和压缩率的偏好去调整。这个特性让它在很多场景中可以平滑替代Snappy、gzip甚至LZ4近几年在大数据生态里的接受度越来越高。于是实际问题就变成了既然zstd覆盖这么广是不是所有场景都换zstdSnappy是否该退出答案没有那么绝对。要根据数据特征、组件版本、CPU预算、读写模式综合判断。这也是我写这篇文章的初衷先讲清楚原理再给实测和数据最后给出决策方法论而不是简单一句“用zstd就完了”。2. 算法原理拆解两者到底差在哪2.1 Snappy用简单结构换极致速度Snappy是Google开源的压缩库项目目标非常明确在合理的压缩率下提供极致的压缩速度和极快的解压速度。它的内部设计刻意保持了简洁。压缩数据被划分为固定大小的块通常64KB每块独立查找匹配和编码不做过深的历史匹配也不做高强度的熵编码。这种“轻量级”路线让它在实现上可以高度优化运行时内存占用很低CPU占用也很可控非常适合在线场景中频繁压缩和解压的数据链路。但代价也很直接压缩率典型表现一般。对文本类数据Snappy通常能压到原始体积的50%—70%左右远不如gzip和zstd。如果你的数据本身就是JSON、日志这种重复度不低的文本Snappy的压缩率会比较浪费存储空间和网络IO。更关键的是Snappy基本没有调节档位你没法通过参数让它在“更快”和“更省”之间做取舍一旦数据特征有变化只能接受它的固定路线。我们团队最初的Kafka消息压缩用的就是Snappy图的是解压快、延迟低、CPU开销小。这个选择本身没错但当消息体越来越大、Topic累积越来越多之后磁盘占用和下游消费的带宽压力开始变得刺眼这时候你就不得不考虑更高压缩比的算法。2.2 Zstandard压缩等级背后的现代设计Zstandard采用的技术栈比Snappy现代很多。它基于LZ77的匹配思路做数据扫描同时引入Finite State EntropyFSE做熵编码熵编码阶段能更好地利用统计冗余把序列中出现的字符分布频率压得更极致。FSE技术源自Yann Collet此前开发的Huffman和ANS算法沉淀这也是zstd在相似速度下压缩率显著优于Snappy的核心原因。zstd最突出的设计是压缩等级机制。从1到22每一级背后对应不同的匹配搜索深度、哈希策略和熵编码精细度。默认是3级通常在“压缩速度和压缩率”之间取得较好平衡往上到6、9、12压缩率逐步提升但速度下降19级之后基本接近极限压缩适合离线归档这类对时间不敏感的场景。此外zstd还支持词典训练zstd --train可以通过业务数据样本训练一份自定义词典让小数据块的压缩率有质的提升这是Snappy完全没有的能力。这给了架构师一个很大的自由同一个算法在线链路用3级保持低延迟批处理用9级压得更狠归档用19级追求极致空间节省不同阶段各取所需。这也就解开了很多人“zstd到底快不快”的疑惑你要先说你用的是哪个等级等级不同结论完全不同。2.3 关键指标与资源开销对照把两个算法拉到一起对比比较容易理解各自擅长什么。我基于常见测试数据日志文本、JSON、CSV和此前项目里的压测结果整理了一张参考表因为机器配置和数据分布不同数字不会完全一致但趋势是稳定的对比维度SnappyZstandard默认级别3Zstandard级别9Zstandard级别19定位极速压缩均衡型高压缩率极限压缩压缩速度很高中高中低低解压速度很高很高高高压缩率一般明显优于Snappy优于gzip接近gzip最高档级别调节无1-221-221-22典型CPU开销低中中高高生态支持Hadoop/Kafka/Parquet原生主流组件近年版均支持同左同左值得单独强调的是解压速度。zstd的解压速度从低级别到高级别都不会太差这是因为解压路径的复杂度基本固定不随压缩等级大幅变化。对读写链路来说解压通常发生在热点读取路径上zstd整体表现比gzip快得多这是它在批处理和查询场景中能替代gzip的核心原因。而Snappy的解压速度虽然也很快但压缩率差距在容量敏感场景下难以忽视。3. 实操落地在主流组件中配置与验证3.1 组件级配置参考理论看完就得上手。现在主流大数据组件的原生支持情况已经比较成熟Snappy和Zstandard基本都是开箱即用的。下面是几个常用组件里的配置方式以“我想把压缩改成zstd”为例HadoopMapReduce场景在mapred-site.xml里设置property namemapreduce.output.fileoutputformat.compress/name valuetrue/value /property property namemapreduce.output.fileoutputformat.compress.codec/name valueorg.apache.hadoop.io.compress.ZStandardCodec/value /property property namemapreduce.map.output.compress/name valuetrue/value /property property namemapreduce.map.output.compress.codec/name valueorg.apache.hadoop.io.compress.ZStandardCodec/value /property property nameio.compression.codec.zstd.level/name value6/value /propertyio.compression.codec.zstd.level这个参数对应zstd的压缩等级。Hadoop的Codec类会读取它作为默认level实测中设为6到9对MR作业比较友好CPU开销可控压缩率提升明显。Spark SQL读写Parquet时不需要改配置文件在提交参数或设置会话参数时直接指定spark-sql --conf spark.sql.parquet.compression.codeczstd也可以在spark-defaults.conf里固化spark.hadoop.mapreduce.output.fileoutputformat.compress.codecorg.apache.hadoop.io.compress.ZStandardCodec spark.sql.parquet.compression.codeczstd spark.sql.orc.compression.codeczstd注意Spark SQL和Hive的默认压缩方式经常是分开设置的改完Parquet压缩后最好再确认一下ORC的配置两套未必同步。Kafka侧更简单Broker的compression.type可以设为zstdProducer端也可以在自己的配置里指定比如在Java客户端里设置props.put(compression.type, zstd)。Kafka从2.1版本开始原生支持zstd所以较新的集群都能直接用旧的版本则需要确认依赖。Parquet文件单独生成时在ParquetWriter的builder里调用withCompressionCodec(CompressionCodecName.ZSTD)ORC同理Writer的options里可以指定CompressionKind.ZSTD。实际操作里到底有没有生效可以用几个验证办法。最直观的是看文件大小写同样的数据zstd落盘文件应该明显比snappy小。也可以用parquet-tools或hdfs dfs -ls查看SNAPPY/ZSTD标记。如果跑MR任务留意作业日志里压缩codec的加载信息或者直接对输出目录的文件做一次hdfs dfs -text读取确认不会报decode异常就行。3.2 自己跑一轮可靠基准测试网上能找到各种压缩算法benchmark但那些数据未必适合你的业务。压测这件事最靠谱的还是用你自己的数据跑一遍。测试方法不难难在控制变量。我建议按下面的步骤做第一步准备测试数据集。从业务系统日志、JSON事件、CSV导出、ORC/Parquet压缩前数据里各抽一部分每个文件控制在50MB到500MB之间。为什么要多种类型因为日志有大量重复字符串JSON有很多引号和字段名数字列几乎压不动数据特征不同压缩率差异很大。只用一种数据得出的结论容易误导选型。第二步用命令行工具做一轮快速验证。Linux环境下直接安装zstd命令然后对同一样本分别跑Snappy和不同level的zstd# 压缩且只输出基准信息 zstd -b -3 /path/to/sample.log zstd -b -9 /path/to/sample.log zstd -b -19 /path/to/sample.log # 用snappy命令行或者直接改用代码测试 # snappy没有官方独立CLI这里更推荐用Java代码统一测zstd -b是内置benchmark模式会输出压缩率、压缩速度和解压速度非常方便。Snappy没有等价官方CLI所以需要靠代码统一量化。第三步用Java代码在同一个进程里做压缩和解压测试这样才能更贴近Hadoop/Spark的实际行为。下面的代码基于Hadoop的CompressionCodec接口对SnappyCodec和ZStandardCodec都适用import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.io.compress.CompressionCodec; import org.apache.hadoop.io.compress.CompressionInputStream; import org.apache.hadoop.io.compress.CompressionOutputStream; import org.apache.hadoop.io.compress.SnappyCodec; import org.apache.hadoop.io.compress.ZStandardCodec; import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; public class CompressionBenchmark { public static void main(String[] args) throws Exception { if (args.length 3) { System.out.println(Usage: CompressionBenchmark snappy|zstd input output [zstdLevel]); return; } String codecName args[0]; String inputPath args[1]; String outputPath args[2]; Configuration conf new Configuration(); CompressionCodec codec; if (snappy.equalsIgnoreCase(codecName)) { codec new SnappyCodec(); } else { codec new ZStandardCodec(); if (args.length 3) { conf.setInt(io.compression.codec.zstd.level, Integer.parseInt(args[3])); codec.setConf(conf); } } File srcFile new File(inputPath); File dstFile new File(outputPath); long rawSize srcFile.length(); long start System.nanoTime(); try (InputStream in new FileInputStream(srcFile); OutputStream out new FileOutputStream(dstFile); CompressionOutputStream cout codec.createOutputStream(out)) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) 0) { cout.write(buffer, 0, len); } } long compressMs (System.nanoTime() - start) / 1_000_000; long compressedSize dstFile.length(); start System.nanoTime(); try (InputStream in new FileInputStream(dstFile); CompressionInputStream cin codec.createInputStream(in); ByteArrayOutputStream out new ByteArrayOutputStream()) { byte[] buffer new byte[8192]; int len; while ((len cin.read(buffer)) 0) { out.write(buffer, 0, len); } } long decompressMs (System.nanoTime() - start) / 1_000_000; double ratio (double) compressedSize / rawSize; System.out.printf(codec%s raw%d compressed%d ratio%.3f compressMs%d decompressMs%d%n, codecName, rawSize, compressedSize, ratio, compressMs, decompressMs); } }在pom.xml里引入hadoop-common依赖版本尽量和你集群一致编译以后直接对同一批文件多跑几轮。实际测试时要注意JVM预热先跑一两轮让类加载和JIT编译稳定正式记录时每轮跑多次取中位数不要用平均值掩盖抖动。3.3 不同数据特征下的实测参考结果这里给一组基于之前项目数据的测试结果供参考。测试环境是8核CPU、JDK8、Hadoop 3.x数据分别取了一段业务日志文本重复度高、一段JSON事件字段多、随机Value多、一段数字型CSV数字和逗号占比高。每组数据统一用Java代码跑三遍取中位数据样例算法压缩比压缩速率(MB/s)解压速率(MB/s)日志文本 256MBSnappy0.52220480日志文本 256MBzstd-30.41190520日志文本 256MBzstd-90.3590510JSON事件 300MBSnappy0.68240500JSON事件 300MBzstd-30.52170490JSON事件 300MBzstd-90.4680480数字CSV 120MBSnappy0.88210460数字CSV 120MBzstd-30.82160450数字CSV 120MBzstd-90.7875440从结果能看出几个规律文本数据压缩率收益最大zstd-3就能比Snappy多压缩十几个百分点而压缩速率下降不算多数字密集数据本身就难压缩两种算法的差距也缩小解压速度这块zstd几乎不比Snappy差高级别也一样。所以如果你的数据里文本占比高换成zstd的收益会非常直观。4. 选型参考不同业务场景怎么决策4.1 三步法看懂测试结果拿到一组测试数据之后不要急着做决定。我先用三步法来梳理第一步看压缩率差距是否显著。如果Snappy和zstd-3的压缩率差距在5个百分点以内说明你的数据里高熵内容多那么zstd的优势就不明显继续保持Snappy也没有问题。差距超过10个百分点容量和带宽节省就很可观值得认真考虑切换。第二步看CPU余量。压缩率差距大但CPU非常紧张不能盲目追求高级别。比较稳妥的做法是先上zstd-1或zstd-3让压缩率有提升但CPU压力不失控观察集群整体负载再微调。CPU有冗余的话再逐步升到6或9。第三步看读写链路对延迟的敏感度。在线查询、实时流式处理这类场景解压延迟极为重要那就优先保证解压速度压缩率排第二离线批处理、数据落仓、归档备份这类场景压缩率优先级更高因为CPU时间换取的是长期存储和传输成本的下降。4.2 分场景推荐矩阵结合我们实际做过的一些优化整理了一个简明的推荐矩阵可以作为选型起点场景推荐方案理由实时消息链路Kafka/PulsarSnappy或zstd-1延迟敏感CPU开销要压到最低压缩率够用就行在线查询类数仓OLAPSnappyParquet或zstd-3查询要快速扫描和解压不能让CPU成为瓶颈离线批处理Hive/Sparkzstd-6到zstd-9IO和shuffle量大多花一点CPU换明显更快的作业时间列式存储数据落地Parquet/ORCzstd-6到zstd-9列式文件读取时会过滤解压吞吐优势明显压缩率更关键冷数据归档zstd-19或zstd-22不常读存储成本优先CPU时间可以多花小对象/字典型数据zstd 训练字典Snappy完全没这个能力字典训练可大幅优化小数据压缩率这套矩阵不是一成不变的。如果你所在团队的CPU配额非常紧张即使是离线批处理也应该往低一级靠。反过来如果存储成本极高、网络费用高昂那可以往高一级试探。总之先小范围试跑再看监控数据调整。4.3 切换压缩格式的存量数据处理策略很多团队最大的顾虑不是新任务用哪种压缩而是历史数据已经用Snappy落盘了换zstd之后旧数据怎么处理。这个确实不能草率。压缩格式不是简单改个配置就完事读取方如果不能解压作业必然失败。实际项目中我一般建议分三步。第一步新数据切新格式代码和配置全部切换到zstd让新的落盘文件以zstd编码写入。第二步对存量数据制定分级迁移计划。如果平台有类似Hive的分区表可以按时间分区逐个重刷如果数据是纯文件堆在HDFS上需要写一个迁移任务读取旧Snappy文件再重新压缩写出zstd格式。第三步确认旧数据还在保留期内读取客户端兼容性先验证好再逐步清理。这里还有个工程细节不要试图“原地转格式”。你无法直接把一个snappy文件改成zstd后缀就算完成压缩和解压必须成对出现必须真正执行解压再压缩的过程。跑迁移任务时最好在业务低峰期进行并且把迁移作业的并行度控制好避免IO和CPU占用过高反过来影响在线业务。5. 实战避坑常见问题与排查思路5.1 编解码器缺失与类名拼写最常见的坑出现在切换配置后作业一起动就报ClassNotFoundException或UnsupportedCodecException。大多数情况是某个客户端或执行节点缺少对应的codec jar。Hadoop发行版通常会自带SnappyCodec和ZStandardCodec但Spark、Flink、Kafka这些组件各自打包方式不同执行端未必完整包含Hadoop的codec类。排查方式很直接找一个报错节点在启动类路径里搜一下对应类是否存在find /opt -name hadoop-common-*.jar -exec sh -c unzip -l {} | grep -i ZStandardCodec \;如果找不到引入对应依赖。Snappy还要注意某个发行版里默认的snappy是native库光有jar不够需要确认libsnappy.so是否安装java.library.path是否正确。zstd一般依赖hadoop-common自带的native实现但还是建议在正式环境验证一遍。类名拼写也属于低级但多发的问题比如有人把Hadoop的org.apache.hadoop.io.compress.ZStandardCodec写成com.github.luben.zstd.ZstdInputStream。后者是纯Java的zstd-jni库接口完全不同直接填在Hadoop配置里必然报错。总是优先使用你正在运行的框架的标准codec类不要看到网上某个示例就直接复制。5.2 同名陷阱Snappy压缩库与“Snappy Driver Installer”这里必须单独提一个容易混淆的点。你在网上搜Snappy压缩时很可能会混出来一个叫“Snappy Driver Installer”的结果简称SDI。这个名字非常迷惑它其实是Windows系统上一个驱动安装工具专门帮用户批量安装或更新显卡、网卡等硬件驱动跟数据压缩、大数据架构没有半点关系。很多新手在搜资料时看到“Snappy”就认为是同一个项目找半天找不到压缩库入口甚至有人把下载来的驱动包当成压缩库依赖引入白白消耗大量排查时间。从工程角度用包依赖管理工具能避免这类混乱。Java项目里Snappy的常用包是org.xerial.snappy:snappy-java这是最主流的Java绑定Go、Python等语言也各有对应的官方绑定。而“Snappy Driver Installer”完全是另一个软件生态只在Windows驱动场景中出现。如果你在Linux集群上搜索Snappy搜出来的结果出现了“Driver”“Windows”“显卡”这类字眼基本可以确认跑偏了。记住压缩库的Snappy对标的永远是google/snappy这个GitHub仓库不是那个驱动安装器。5.3 zstd高等级导致CPU飙升切换到zstd之后最典型的问题是CPU突然飙升。原因通常是把压缩等级调太高比如直接设成19、22。zstd高级别在极端数据上消耗的时间可能比3级高出几十倍等到作业队列全线变慢才想起去查就晚了。我的建议是不要把压缩等级写死在业务代码里而是通过配置中心或环境变量动态下发。上线前先用1到3个样本文件测试不同等级的压缩时间和压缩率画个曲线再决定。实际操作中在线服务一般用1到3离线批处理用6到9再往上除非是归档任务否则性价比很低。如果你用了zstd字典功能也要注意字典训练阶段本身需要额外CPU和内存训练频率不要设置太高建议每周级甚至每月级重建一次即可。5.4 随机读取场景的“负收益”问题还有一个容易被忽略的问题压缩不是所有情况下都能带来净收益。对于每天全量扫描的数仓大查询压缩减少磁盘IO的好处非常明显但如果任务会反复随机读取文件中的小块数据比如线上服务频繁点查Parquet文件中的某一行每次都要从压缩块开始解压这时候解压开销反而可能超过IO节省。这种场景下建议在文件级或块级做压缩策略分离小文件、频繁访问的热数据可以走Snappy或LZ4这类轻量方案甚至不压缩大文件、低频访问的冷数据再使用zstd高等级。分区级不同压缩策略在Parquet和ORC里是完全可行的一个表的不同分区甚至不同文件可以用不同codec下游组件按文件头信息自动识别。这也说明选型不是一次性的全局决策而是可以细化到数据生命周期各个环节的持续优化。6. 写在最后我们团队的迁移经历那次把Spark批处理从Snappy换到zstd的优化我们并没有做全量切换而是按数据温度做了分流。实时链路上依然保留了Snappy因为在线服务对P99延迟非常敏感压缩率低一点但CPU开销小整体更稳。离线数仓和归档路径切换成了zstd离线批处理用9级冷数据归档用19级压缩率提升带来的磁盘节省确实肉眼可见整体任务耗时也显著下降。后来我又在几个项目里验证过同样的思路发现最值得留意的不是技术指标本身而是团队对压缩这个事儿的重视程度。很多人调优CPU、调优内存却把压缩当成“能用就行”的默认配置错过了非常便宜的IO和存储收益。跑一轮针对自己数据的基准压测花不了半天时间但换来的是后续长期的吞吐提升和成本节省。最后一个小建议如果你们的集群已经运行了较长时间不妨每个季度抽一批真实数据重新跑一轮压缩基准。业务数据特征会变压缩算法的实现和组件版本也在迭代半年前的结论现在未必仍然正确。把这个测试固化到日常运维流程里远比临时抱佛脚翻网上文章靠谱得多。
返回列表