资讯动态

大数据压缩全解析:从算法原理到生产选型实践

发布时间:2026/10/14 2:28:04 来源:尧图企业网站定制
数据压缩这四个字看起来是个老话题可只要你在大数据链路里待过一阵子就会发现自己绕不开它。HDFS 里一个日志表半年不清理存储账单翻倍跑数仓任务时 map 端读文件把磁盘 IO 打满Spark Shuffle 数据量一大整个集群的网络就冒烟——这些问题的第一反应几乎都是同一个把数据压一压。这篇文章想把大数据领域压缩技术的脉络一次性讲透底层算法靠什么在做演进Hadoop 到现在各代引擎怎么选压缩Parquet 这类列式存储为何要把编码和压缩拆开以及在真实集群里怎么选型、怎么避开坑。适合正在做数据平台、数仓建设或实时链路优化的人也适合刚入门、想建立整体认知的同学。1. 先把动机讲透压缩在大数据链路里到底解决什么问题1.1 存储、带宽与 IO三个容易被低估的瓶颈很多人理解压缩第一反应就是“省磁盘”。这个方向没错但只看到这一层就容易把压缩当成一个后端维护的杂活。做大数据的人都知道真正的瓶颈往往不是 CPU而是磁盘 IO、网络带宽和内存。一个典型的分析任务数据从 HDFS 读出来要经过网络传输Shuffle 阶段又要落盘、再传输中间结果还要写临时文件。每一步都在搬字节字节越少整个管道越快。我见过一个实际案例某日志分析系统每天产生大约 8 亿条访问日志原始文本形式落在 HDFS 上单日体积在 4TB 左右。集群磁盘倒还够用但每天凌晨的批量任务要把这 4TB 全部读一遍再加上排序和聚合整个批处理窗口被拉得很长。后来只是把存储格式改成列式并且开启合理的压缩单日体积降到 900GB 左右扫描耗时直接掉了 60% 以上。这里压缩省下来的不只是空间而是把读数据的 IO 开销、网络传输开销、洗牌开销一起降下来了。还有一个容易忽略的点压缩可以让 CPU 和 IO 之间重新平衡。现代服务器 CPU 核数很多但单个磁盘的吞吐是有限的。当 IO 已经饱和而 CPU 还有大量空闲时花一点 CPU 去压缩数据换来更少的数据读取量整体任务反而更快。这不是反直觉而是大数据场景里压缩最基本的“性价比逻辑”写一次数据可能要读几十次压缩时花的时间可以在后续每次读取中赚回来。1.2 分布式场景下的压缩从工具思维到系统思维单机环境下压缩是一件很私人的事一个文件一个压缩器一个目标。到了分布式环境压缩变成了系统级问题文件能不能切分、解压时要不要完整流、不同节点上的数据如何对齐、压缩格式能不能被计算框架感知。这些问题单机工具根本不会考虑。举个很经典的例子一个 10GB 的 gzip 压缩文件放在 HDFS 上如果直接用 MapReduce 或 Hive 去读它可能只会启动一个 map 任务来处理整个文件。原因在于 gzip 是一种流式格式解压必须从头开始框架没办法从文件中间某个偏移量直接开始解压。10GB 的数据一个并行度去啃其他几百个核在旁边看热闹效果可想而知。这在大数据里是一个不断重复的“翻车现场”后面我会专门展开。分布式场景还改变了压缩的目标排序。单机上你关心的是“我个人等不等得起”分布式里你关心的是“集群整体吞吐如何”。所以你会发现Hadoop 时代流行的压缩器不一定是压缩率最高的而是综合了速度、可切分性、编解码库成熟度的方案。理解这一点再看后面这些压缩器和格式的选择就有了主线。2. 压缩算法背后的硬原理读懂 LZ 系与熵编码的演进2.1 字典匹配与熵编码两类算法的分工与合作无损压缩技术基本上可以分成两条主线一类是“找重复”另一类是“改编码”。找重复的代表是 LZ77。它的做法大致是用一个滑动窗口保存已经出现过的数据当后面遇到重复的字符串时不再重复记录原始内容而是输出一个“距离长度”的匹配对告诉解压器“去前面哪个位置复制多长一段”。你可以把它理解成在书上写批注“这段内容见前文第几行”。Gzip 使用的 DEFLATE 算法就是 LZ77 加 Huffman 编码的组合而大数据生态里常见的 Snappy、LZ4、LZO、Zstd底层也都没有离开滑动窗口匹配这个思路差别主要体现在查找匹配的策略、窗口大小、最小匹配长度等工程细节上。改编码这一路的代表是熵编码典型的有 Huffman 和近些年很火的 ANS。Huffman 的思想是把高频符号换成短编码、低频符号换成长编码让平均码长逼近信息熵。ANS 更进一步可以按比特粒度逼近熵率不需要把每个符号固定在整数个比特上解码吞吐还很高。Zstd 里那个 FSE 就属于 ANS 这一支。值得多说一句的是传统印象里“压缩率高”和“速度快”是矛盾的但 Zstd 这类新算法打破了部分刻板印象。它把 LZ 的快速匹配和现代熵编码的组合做得相当好再配合各个压缩级别下的大量工程优化所以在很多场景里能做到“体积比 Gzip 小或者接近速度却快一个量级”。这背后不是魔法而是组合算法和工程打磨的结果。2.2 压缩率与速度的权衡为什么没有免费的午餐任何压缩算法本质上都在做一个交换CPU 换空间。压缩级别越高查找重复和建模就越认真CPU 开销越大文件越小反过来快速压缩器用更粗暴的方式找重复省了 CPU但压缩率就不好看。关键问题是这张交换曲线在不同算法上差异很大。Gzip 的 level 1 到 level 9压缩率提升有限但越往后耗时越长。Zstd 的级别范围更宽从负级别到 level 22但它有个特点level 3 的压缩率已经相当不错继续加大级别压缩率的提升变得越来越微弱CPU 开销却可能几十倍地增长。我在实际项目中见过有人把一个热表配成 zstd level 19结果写入任务 CPU 打满压缩率只比 level 3 好了 10% 左右完全得不偿失。还有一个更高维度的权衡解压速度往往比压缩速度更重要。数据通常只写一次但要被读很多很多次尤其数仓场景里一张表每天被几十个查询扫描。所以选压缩器时比起“写进去多快”更要关注“读出来多快”。这也是为什么 Snappy 这种压缩率并不算高的算法能在大数据生态里活这么多年它压缩快解压更快CPU 开销又低在绝大多数查询链路里都是稳的选择。3. 从 Hadoop 到 Spark/Flink压缩技术在大数据生态中的脉络演化3.1 可切分性分布式压缩的第一道坎回到 Hadoop 时代HDFS 上的数据往往是用一个压缩器直接压完整个文件然后交给 MapReduce 处理。这时候一个绕不开的问题浮出水面文件能不能切分。MapReduce 的 map 并行度由输入分片决定一个分片通常对应 HDFS 上的一个 block。如果压缩格式支持从数据流中间某个位置开始解压那同一个大文件的不同 block 就能分给不同 map 并行处理如果不支持整个文件就只能由一个 map 串行处理。Gzip 压缩率好但它是一个连续的流式格式中间没有同步点一个大 gzip 文件在 MapReduce 里通常只能起一个 map。“一个超大压缩文件打满所有时间其他并行度全部闲置”这个现象早年做 Hive 的人应该都有印象。LZO 在 Hive 生态里流行一个重要原因就是它可以配合索引文件实现切分bzip2 自带 block 结构也能切分但速度实在太慢。Snappy 压缩速度快但传统用法下同样没有可切分性所以它更多是被当作“块压缩器”使用也就是大数据系统把数据切成一段一段每段独立压缩、独立解压。到了 Parquet、ORC 这种列式文件格式问题被绕开了文件内部按 row group 和 column chunk 分别压缩每个 chunk 都有自己的起始位置框架可以按块独立读取和解压不需要整个文件从头解压到尾。这也是为什么你在 Parquet 上用 Snappy、Zstd 都不太会遇到“不可切分”问题。3.2 计算引擎迭代压缩从省钱工具变成性能工具Spark 时代默认值悄悄发生了变化。Spark 的 Shuffle 中间数据压缩默认 codec 是 LZ4而不是 Snappy也不是 Gzip。为什么因为 Shuffle 数据是临时数据生命周期短追求的是压缩速度和解压速度的均衡LZ4 在这两项上表现很突出。RDD 持久化到磁盘时也有独立的压缩配置广播变量同样有压缩选项。可以说到了 Spark/Flink 时代压缩不再只是“归档时省点空间”而是贯穿计算过程本身的性能开关。Flink 和 Kafka 的流式链路里Snappy 和 Zstd 也很常见。Kafka 的 producer 端开启压缩能明显减少跨节点、跨机房的网络传输量Flink 的检查点数据落盘时同样需要压缩否则状态大的作业很容易把磁盘写满。Zstd 在这种场景越来越受欢迎原因很简单它提供了一个更好的“压缩率-速度”平衡点在低带宽、跨机房传输的场景下收益尤其明显。再看存储侧对象存储和云上数据湖越来越普及。云上存储的读写费用是计费项数据体积往往直接对应账单金额。这种情况下Zstd 这类“压缩率高但解压也不慢”的算法很快成了新宠。整个脉络总结下来就是从“能压就行”到“压得快、读得快、还能切分、最好库兼容性也好”的综合博弈。4. 列式存储重塑压缩逻辑Parquet 与 ORC 里的编码与压缩4.1 列式存储为什么天生利于压缩如果只用一句话回答“列式存储为什么压缩率高”那就是同一列的数据类型一致取值范围高度聚集重复值被连续放在一起。一张订单表状态字段就那么几个值待支付、已支付、已取消、已退款。日期字段在某天分区里几乎全是一个值。城市、渠道、设备类型这些低基数列连续存储之后的重复度高得惊人。行式存储里一条记录的各列类型混杂字符串、数字、布尔交错在一起重复值被其他列打散压缩器需要在紧邻位置同时面对完全不同类型的数据字典匹配效率自然差不少。而列式存储让同一列的连续字节拥有了极高的局部相似性压缩器可以轻而易举地找到重复片段。再加上下面要讲的编码层很多列压根不需要走到通用压缩那一步就已经被缩小了一个数量级。理解这一点非常重要因为很多人以为 Parquet 压缩率高是因为上了 Snappy其实 Snappy 只是最后一步真正的功臣是前面的编码。4.2 编码encoding和压缩compression是两码事Parquet 和 ORC 的精髓是把数据处理拆成了两层先编码再压缩。很多人在配完 compression 参数之后就以为大功告成完全忽略了编码层的作用。先看编码。低基数列可以走字典编码把每个唯一值映射成一个整数 ID后面所有重复值都只存这个 ID连续重复很多的列用游程编码 RLE把一连串相同值记成“值重复次数”时间戳、自增 ID 这类递增或相近的列用 delta 编码存差值而不是存原值布尔值用 bit packing一个字节塞 8 个布尔位。这些编码手段对低基数列极其有效。比如一个状态字段经过字典编码之后可能已经只有原来几十分之一的大小这时候再套一层 Snappy 或者 Zstd结构上已经没什么可压的了。再看压缩。通用压缩器处理的是编码层输出后的字节流。当编码已经解决了大量重复压缩器主要负责干掉残余的统计规律。两层配合下来一个高重复率的列最终体积可能让人不敢相信。举一个我实际见过的数仓表某个线上交易项目的订单状态列Parquet 文件里这一列的压缩后体积占原始 CSV 版本的不到 1%。当然这是低基数场景的极端例子高基数列就没有这么幸福了。高基数列是什么情况比如 request_id、device_id 这类几乎不重复的字段。字典编码会因为字典太大而自动退化RLE 也用不上delta 编码也帮不上忙数据最终以近似原始字节的形式交给通用压缩器去压随机内容那压缩率自然惨不忍睹。这时候再换 Gzip 也救不回来因为压缩器面对的本质上是高熵数据。所以排查压缩率问题的时候第一反应应该是看列的特征而不是急着换算法。5. 实操选型不同环节该用哪种压缩器参数怎么配5.1 按场景选压缩器一张决策表把主流压缩器放在一张表里看会比背一堆抽象概念直观得多。以下是我个人实践里的判断标注了典型使用位置供参考。压缩器压缩率压缩速度解压速度CPU 占用大数据生态典型位置Gzip高慢中中高冷数据归档、老 Hive 表Bzip2很高很慢慢很高极少极端归档场景LZO低快快低早期 Hive 可切分表维护成本高Snappy较低很快快低Parquet/ORC 默认兼容组合、中间数据LZ4低-中极快极快低Spark Shuffle 默认、RDD 缓存Zstd中-高快级别可调快中网络传输、冷热表平衡、Kafka/Flink这张表的信息量集中在“位置”那一列。你会发现没有哪个压缩器是绝对王者一切都取决于场景。热数据、被频繁扫描的明细表我更倾向 Snappy 或 Zstd level 3因为它们解压够快不会让查询 CPU 变成新的瓶颈冷数据、归档数据、基本没人查但必须留存的表可以用 Zstd level 9 甚至更高省下来的存储成本是实打实的。5.2 三个指标一套流程先测再上不要直接抄别人的压测结论。压缩效果和数据特征强相关同样的 Zstd在一张全是重复状态码的表上可能压到 10%在一张全是随机 JSON 字段的表上可能只省 30%。别人告诉你“Zstd 比 Snappy 好”放在你的数据上可能完全是反的。我自己的线上选型流程非常简单固定拿一个典型分区做 AB同一批数据分别用 Snappy、LZ4、Zstd level 3、Zstd level 9 写一份然后记录三个数字输出大小、写入耗时、全表扫描或典型聚合查询的耗时。写入耗时除以数据量能够得到吞吐输出大小计算压缩率查询耗时则反映端到端效果。如果查询是全扫描型且磁盘 IO 率先到瓶颈那么 Zstd 带来的体积缩小可以直接转化为查询加速如果查询是点查型每次只取少量行压缩率的收益就没那么明显反而要更在意解压时的 CPU 开销。这个测量流程不用做得很精细但一定要在真实数据上做。我见过太多人拿普通文本 benchmark 数据直接拍板选型上线以后发现完全不是那么回事。真实数据的基数分布、字段类型、重复模式才是决定压缩效果的根本因素。5.3 常见引擎与框架的配置落地配置要在具体框架里落地这里列几个我常用的写法都是生产环境验证过的。Spark 写 Parquet最稳妥的方式是在 DataFrameWriter 里显式指定压缩类型df.write .format(parquet) .option(compression, zstd) .option(zstd.level, 3) .save(hdfs://...)Spark 的 Shuffle 压缩和 RDD 持久化压缩在配置文件里调整spark.io.compression.codeclz4 spark.io.compression.zstd.level3 spark.shuffle.compresstrue spark.rdd.compresstrueKafka producer 开启消息压缩compression.typezstd compression.zstd.level3Flink 写 Parquet 文件时在表配置里指定压缩比如CREATE TABLE sink_table ( ... ) WITH ( connector filesystem, format parquet, parquet.compression zstd );这里有一个容易被版本坑到的点不同版本的 Spark 对 Parquet 压缩参数的名称不统一。老版本常用spark.sql.parquet.compression.codec新版本又兼容parquet.compression。如果你在配置层面发现不生效建议直接回到 DataFrameWriter 的.option(compression, ...)这个最直接不受会话级配置影响。6. 实践中的坑与排查那些文档不会写的翻车现场6.1 压缩率差得离谱先查数据再查配置压缩率不佳最常见的根因根本不在压缩器而在数据本身。有一次我排查一个任务原始 JSON 日志压缩完只省了 30% 左右怎么调参数都没用。抽了几行数据一看每条消息里都带了一长串设备 ID 和加密 token还有一段 base64 编码的图片内容。这类高熵字段占了体积大头通用压缩器面对随机字节基本无能为力。这个问题的解法不是换 Gzip而是把这个字段从主存储里拆出去挪到对象存储或者干脆丢掉。还有一个高频问题对已经压缩过的数据再做压缩。比如上游给的是 gzip 后的文本文件你再套一层 Snappy压缩率自然很差因为第一层已经把重复干掉了。遇到这种数据要么直接透传要么换列式格式重新组织让列存和第二层压缩发挥真正的价值。还有高基数列过多、小文件过多都是压缩率“看着不对劲”的常见原因。6.2 CPU 飙升、GC 恶化压缩级别不是越高越好我知道有人听过“Zstd 很强”之后会把所有表的压缩都配成 level 19结果就是写入任务 CPU 打满GC 时间飙升整个批处理链路也随之变慢。压缩率确实提升了一点点但代价完全不成比例。生产环境里level 3 到 level 9 之间已经能覆盖绝大多数需求归档数据可以到 15 左右真的没必要一上来就上最高级别。另一个容易被忽视的坑是 native 库版本不一致。Hadoop、Spark 生态里的压缩能力依赖 JNI 加载本地库不同节点上 zstd-jni、snappy-java 版本不一致会在运行期随机报解压异常。我遇到过新扩容节点解压老文件直接报错的情况排查半天才发现是某台机器的 native 库版本被装旧了。解决办法也很笨但有效统一集群依赖清单把压缩库版本写死在发布流程里新节点上线前过一遍校验。6.3 排查清单与我的默认做法把项目里踩过的坑收敛一下我给自己定了一个压缩问题排查清单遇到可疑现象按顺序查查文件头的 codec 与 encoding。使用 parquet-tools 或其他格式检查工具确认文件真的用了你以为的压缩算法。查数据特征。抽样看字段基数、有没有已压缩或加密内容、有没有超长高熵字段。查并行度。如果一个超大压缩文件只起了一个 task优先怀疑可切分性问题。查 CPU 和 GC。Spark UI 里看 scan task 的 GC 时间、输入数据量、解压耗时。查文件数量。大量小文件会稀释压缩收益并且压缩后不可切分的单文件反而会拖慢并行度。我现在的默认做法是热数据用 Parquet 加 Snappy 或 Zstd level 3冷数据用 Zstd level 9Shuffle 用 LZ4 或 Zstd level 3Kafka 传输用 Zstd。凡是上线新表先跑一遍 AB 测量确认三个数字都符合预期再定最终配置。最后再说一个伴随我很久的体会压缩这件事从来不是“越压越小越好”。它是在存储成本、查询延迟、集群 CPU 和运维复杂度之间找平衡。那个最初的“压缩率越高越厉害”的阶段过去之后你会发现真正值钱的是对数据和场景的判断力。希望这篇脉络梳理能帮你在下次遇到压缩问题时少走几步弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑