☰
Hadoop数据压缩全解析:格式对比、选型逻辑与生产配置
2026/10/15 21:50:09 网站建设 项目流程

聊到 Hadoop 生态,大家关注点通常集中在分布式存储、计算引擎、资源调度这些“大件”上,但有一样东西经常被忽略,却直接影响集群成本和任务效率——数据压缩。它贯穿 HDFS 的落盘、MapReduce/Spark 的 Shuffle、Hive 数仓的存储格式,甚至决定你的任务是几十个 Map 并行跑完,还是一个 Map 孤军奋战跑到天黑。这篇文章从实际运维和调优的角度,把 Hadoop 数据压缩的关键机制、主流格式差异、选型逻辑和生产配置完整梳理一遍,刚接触大数据的朋友能拿来避坑,正在压存储成本、优化离线任务的人也可以当一份选型清单来用。

1. 先搞懂:Hadoop 里压缩到底在解决什么问题

1.1 一笔真实的存储成本账

HDFS 默认三副本,意味着 1TB 原始数据落进去,实际占用集群空间是 3TB。这一点很多新人都知道,但很少换算成成本去感受。假设一个日志系统每天产生 1TB 原始数据,保留 30 天,那就是 30TB 原始数据、90TB 实际占用。如果对这批数据做一次 gzip 压缩,压缩率按 30% 算,压缩后只有 9TB 原始体积,三副本是 27TB。也就是说,同样保留一个月,集群空间从 90TB 降到 27TB,省下 63TB。这还只是存储侧的收益,不含后续读取时节省的磁盘 IO、网络传输和机架带宽。

压缩对性能的影响更直接。很多大数据任务的瓶颈不在 CPU,而在 IO。一个需要 Shuffle 的 Join 或聚合任务,中间结果从 Map 端写到本地磁盘、再通过网络拉到 Reduce 端,如果中间数据能压缩到原来的三分之一甚至四分之一,传输量大幅下降,整个作业的完成时间肉眼可见地缩短。这也是为什么我在评估集群上某个任务“是不是该优化”时,第一件事就去看它的 Shuffle 数据量,而不是急着调并行度。

1.2 天下没有免费的压缩:CPU 和 IO 的博弈

压缩不是白拿的。压缩要耗 CPU,解压也要耗 CPU,如果场景选错,压缩非但不会提速,反而会把任务拖慢。IO 密集型任务比如全表扫描、大表 Join、Shuffle 重操作,压缩收益非常明显,因为磁盘和网络通常是瓶颈;但 CPU 密集型任务比如复杂正则解析、大量特征计算,压缩带来的 IO 节省可能不足以抵消 CPU 的额外开销。

举个生活化的类比。你给朋友传一张 2GB 的工程文件,如果网速很慢,先把文件压成 500MB 再传,省下的传输时间远比压缩消耗的时间多,这是划算的;如果你们在同一个内网、网速极快,那直接传反而更快,因为压缩和解压消耗的时间超过了传输节省的时间。大数据场景也是一样的道理:判断要不要压缩、选哪种压缩,本质是在“CPU 成本”和“IO 成本”之间寻找平衡点。这个平衡点又跟集群的 CPU 资源是否充裕、磁盘是机械盘还是 SSD、网络是万兆还是千兆都有关系,所以我从来不相信有“一套配置走天下”的答案。

2. 压缩格式核心机制:为什么“能不能切分”是生死线

2.1 可切片性的真实含义

HDFS 文件默认按 128MB 切成 Block 存储,MapReduce 和 Spark 在读取文件时,会尽量按照 Block 的边界来生成输入分片,每个分片交给一个 Map 或一个 Partition 去处理。这样并行度才能上去。这里就冒出一个关键问题:如果文件是整块压缩的,比如一个 1GB 的 gzip 文件,HDFS 底层虽然还是把它切成多个 Block,但计算引擎从 Block 中间位置开始解压时,根本没有办法独立解出数据,因为 gzip 是一个连续的压缩流,后面的数据依赖前面的解压上下文。

结果就是:1GB 的 gzip 文件,即使底层占了 8 个 Block,计算引擎也只能把它当作一个整体,交给一个 Map 去处理。其他节点只能围观。这就是典型的“压缩导致数据倾斜”,不是数据本身倾斜,而是切分能力被压缩格式废掉了。相反,lzo、bzip2、zstd 这类支持切片或自带独立块结构的格式,可以按 Block 边界切分,并行度不受影响。

我习惯把可切片性理解成“切西瓜”:gzip 是一个没切过的整瓜,不管它多大,你只能抱起来整个啃;lzo 是切好的西瓜盒,拿出一块就能吃,不需要先还原成完整的瓜。对分布式计算来说,能不能切开直接决定了并行度的上限。

2.2 从算法层面看为什么 gzip 不能切片

这里值得稍微往下挖一层。gzip 用的 DEFLATE 算法,是一连串有状态依赖的压缩块:压缩器维护一个滑动窗口字典,后面的数据压缩时会引用前面出现过的字符串。解压器必须从文件开头依次处理前面的块,才能正确还原后面的内容。所以你在文件中间随便挑一个位置开始解压,根本拿不到有效的输出。

相比之下,bzip2 的底层是多个独立的 BWT 变换块,每个块可以单独解压,天然支持按块切分。lzo 和 LZ4 同样采用独立块或带可分割索引的流式结构,所以它们能更好地配合分布式计算引擎。很多人在选压缩格式时只盯着“压缩率”和“速度”两个指标,忽略了可切片性,结果把 gzip 用在了每天被大量扫描的明细表上,查询性能惨不忍睹。这个教训在 Hadoop 生态里反复出现,值得每个新人记住。

2.3 压缩格式和容器格式:Snappy 的“可切”是有条件的

还有一个经常被绕进去的细节:Snappy 本身并不保证文件级别的可切分。很多人看到“Snappy 速度快”就直接把纯文本文件用 snappy 压了存进 HDFS,然后发现扫描该表的 Map 数只有一个,任务跑了两个小时还没结束。问题不在于 Snappy 算法,而在于你把它用在了裸文本文件上,裸 Snappy 流没有块索引,引擎无法从中间切入。

Snappy 的优势要在“容器格式”里才能完全发挥。比如 Parquet 或 ORC 文件内部有行组(Row Group)或 Stripe 结构,每个行组内部的数据可以独立被 Snappy 压缩,计算引擎知道行组的边界,就能在任意行组处切片读取。这也是为什么数据仓库领域形成了“Parquet/ORC + Snappy”这种黄金组合:列式存储负责裁剪列和切分,Snappy 负责压缩和解压速度,两者搭配后既有高压缩率又有高并行度。如果只是想把文本日志压缩存放,又不想失去并行度,优先考虑 lzo 加索引文件,或者直接换 Parquet 这类带自描述结构的容器格式。

3. 主流压缩 Codec 横向对比与选型指南

3.1 一张表看全所有参数

Hadoop 生态常见的压缩格式就那么几种,我把它们的关键参数放在一起做个对比,方便大家对照选择。

Codec扩展名平均压缩比压缩速度解压速度是否支持切片典型适用场景
gzip / deflate.gz较高一般一般否冷数据归档、不常读的备份
bzip2.bz2最高很慢慢是极冷归档、追求极限压缩率
lzo.lzo中等快快是(需索引)高频读取且需要切分的裸文本
snappy.snappy较低极快极快容器格式内可切Parquet/ORC 内嵌压缩、中间数据
lz4.lz4较低极快极快容器格式内可切需要极致速度的临时数据
zstd.zst高快快是(流式/容器)既想省空间又想速度快的新项目

看这张表时注意一点:压缩比数字不是固定值,具体取决于数据特征。日志文本里有大量的重复字段和固定模式,压缩效果就很好;如果是随机数、加密数据或已经压缩过的图片视频,再压也压不出多少空间。所以上面说“平均压缩比”,是在文本型数据的典型情况。真正做选型时,一定要拿自己的数据样本实测,不要照搬别人的结论。

3.2 按场景选型:给出一套决策规则

我每次给团队建议压缩格式时,都会按场景来分,而不是直接指定一个“最好”的格式。

数据仓库的 Hive 表,走 Parquet + Snappy 基本上是默认答案。Parquet 的列式裁剪和谓词下推能省掉大量 IO,Snappy 的解压速度足够快,查询引擎不会在解压上卡脖子。如果集群 CPU 资源比较宽裕、存储压力又大,可以试试 Parquet + Zstd,压缩率明显优于 Snappy,解压速度也没落后太多。

原始日志文件的归档,如果保留后会偶尔被读取、但不需要频繁做全表扫描,lzo 加索引是不错的选择,兼顾了压缩率和并行读。真正的冷数据,比如超过一年的历史明细,几乎不会被读,直接用 gzip 或 bzip2 压到极限,空间优先,读取慢一点也无所谓,因为一年也不会有几次人去看它。

MapReduce 或 Spark 任务的中间结果,也就是 Shuffle 阶段的数据,选 Snappy 或 LZ4。这个场景的特点是数据只在一个作业内部流动,生命周期极短,压缩率没那么重要,速度才是第一位。用高压缩率格式压缩 Shuffle 数据,往往得不偿失——压缩时间变长了,网络省下的那点时间还不够还债。

3.3 值得重点关注的 Zstd

最近几年 Zstd 在大数据生态里的存在感越来越强。它在高压缩级别下能达到接近 bzip2 的压缩率,同时速度和 LZ4 处于同一量级,比 gzip 快得多。更关键的是它支持预设字典,对于一批结构高度相似的数据,训练一个小字典后压缩率还能进一步提升。Hadoop 3.3 之后的发行版对 Zstd 的原生支持已经比较完善,Hive 和 Spark 侧也都能透传配置。

我在一个模拟项目里对比过 Zstd level 3 和 Snappy:同样的 100GB 文本日志,Zstd 压缩后体积比 Snappy 少了约 35%,解压速度大概是 Snappy 的八成左右。也就是说,在 CPU 没有明显瓶颈的集群上,Zstd 是一个“多花一点 CPU、省下三分之一存储和大量 IO”的划算买卖。如果集群还在用 Hadoop 2.x,接入 Zstd 需要额外编译 native 库,稍微麻烦一点;但如果你已经开始用 Hadoop 3.x 系列,我建议把 Zstd 放进选型测试列表,它在很多场景里会给你惊喜。

4. 生产环境实操:从配置到验证

4.1 全局配置:core-site.xml 与 mapred-site.xml

Hadoop 使用压缩能力的第一步,是把用到的 Codec 注册到运行时环境。在 core-site.xml 的 io.compression.codecs 属性里,把需要的 Codec 类全部列上,逗号分隔。少了这一步,任务执行时可能抛 “Codec not found” 或直接退化为不压缩输出。基础配置如下:

<property> <name>io.compression.codecs</name> <value>org.apache.hadoop.io.compress.GzipCodec, org.apache.hadoop.io.compress.BZip2Codec, org.apache.hadoop.io.compress.SnappyCodec, org.apache.hadoop.io.compress.Lz4Codec, org.apache.hadoop.io.compress.ZStandardCodec </value> </property>

MapReduce 的输出压缩在 mapred-site.xml 里控制。开启最终输出压缩和中间结果压缩两组开关,并指定各自使用的 Codec:

<property> <name>mapreduce.output.fileoutputformat.compress</name> <value>true</value> </property> <property> <name>mapreduce.output.fileoutputformat.compress.codec</name> <value>org.apache.hadoop.io.compress.SnappyCodec</value> </property> <property> <name>mapreduce.map.output.compress</name> <value>true</value> </property> <property> <name>mapreduce.map.output.compress.codec</name> <value>org.apache.hadoop.io.compress.SnappyCodec</value> </property>

注意这里我特意把最终输出和中间结果都设置了。分不清这两个概念是常见的配置误区:mapreduce.map.output.compress 控制的是 Map 端写到本地磁盘、再通过网络传给 Reduce 的那部分数据,也就是 Shuffle 中间数据;mapreduce.output.fileoutputformat.compress 控制的是作业最终落盘在 HDFS 上的输出文件。两者独立生效,很多任务慢在 Shuffle,但你只改了最终输出压缩,自然看不到效果。

4.2 Hive 与 Spark 侧的压缩设置

Hive 在跑 MapReduce 引擎时,同样需要打开输出压缩和中间压缩。直接在 Hive 会话里执行即可:

SET hive.exec.compress.output=true; SET hive.exec.compress.intermediate=true; SET hive.intermediate.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;

这里有个容易被忽略的问题:如果 Hive 表的存储格式是 Parquet 或 ORC,压缩其实是由表本身的存储格式决定的。你在会话里打开输出压缩,指的是 MapReduce 结果文件的压缩,而 Parquet 表内部的列压缩由表属性控制。建表时直接指定:

CREATE TABLE ods_xxx ( id bigint, name string ) STORED AS PARQUET TBLPROPERTIES ('parquet.compression'='SNAPPY');

Spark 侧的配置类似,在 SparkSession 里设置:

spark.conf.set("spark.sql.parquet.compression.codec", "snappy") spark.conf.set("spark.io.compression.codec", "lz4")

spark.sql.parquet.compression.codec 控制 Parquet 文件的压缩;spark.io.compression.codec 控制内部数据如 RDD 序列化数据的压缩。注意 Spark 3.x 里 Parquet 压缩默认值通常是 snappy,但如果你用的是高版本,默认可能变成了 uncompressed 或 zstd,最好在项目初始化时显式设置,避免不同环境行为不一致。

4.3 命令与 API:快速验证压缩是否生效

配置改完,最怕的是“看着改了,实际没生效”。我习惯用几个命令快速验证。

先确认 native 库是否加载正常:

hadoop checknative

正常输出里会列出 gzip、bzip2、snappy、zstd 等对应的是 true 还是 false。如果 Snappy 是 false,说明用的是 Java 实现,性能大打折扣。

再看 HDFS 上文件的压缩前后体积:

hadoop fs -du -h /data/raw hadoop fs -du -h /data/compressed

如果一个 10GB 的文件压缩后变成 3GB,说明压缩确实生效了。验证文件内部是否真的可切片,可以查看文件的物理 Block 分布和计算引擎切出的分片数量。在 MapReduce 的 Counter 里能看到 Input Split 数量,如果一个大 gzip 文件只有一个 Input Split,那就是不可切分的典型表现。

Java API 层面,MapReduce 作业里可以用 FileOutputFormat 直接设置压缩输出:

job.setOutputFormatClass(TextOutputFormat.class); FileOutputFormat.setCompressOutput(job, true); FileOutputFormat.setOutputCompressorClass(job, SnappyCodec.class);

4.4 一次日志目录压缩的完整试验

有一次我接手一个日志平台的存储优化任务,现状是 HDFS 上按天分目录存原始 JSON 日志,三副本后占用了大量空间。我的处理步骤是:先挑一个非核心的日期目录做小规模验证,不直接动全量数据。

先统计目录原始体积:

hadoop fs -du -h /logs/2024/06/01

然后写一个临时 MapReduce 任务或直接用 Hive CTAS,把该日期数据转成 Parquet + Snappy 存储到新目录。完成后对比新旧目录的体积,并跑一个典型查询对比耗时。确认存储节省比例和查询耗时都可接受后,再批量处理其他日期目录。最后一步才是切换上层应用的表路径,并观察一段时间的稳定性。

整个过程遵循“先测试、再小范围试点、最后全量推广”的节奏。压缩格式的改动涉及所有下游任务,一旦全量切换后才发现查询性能下降,回滚成本远高于事前多花一两天做验证。

5. 最容易踩的坑:Codec 缺失、索引与格式嵌套

5.1 LZO 的 native 库困境

Apache Hadoop 官方发行版默认不包含 LZO 的 native 实现,因为它涉及的底层库授权方式比较特殊。你配置了 LzopCodec 后直接跑任务,大概率会碰到异常,提示无法找到对应的 native 库。解决方案是自行编译安装 lzo 相关依赖,并把编译好的 so 文件部署到所有节点,同时修改 core-site.xml 注册 LzopCodec。

这个坑在第一次接触 LZO 时几乎人人都会踩。很多人以为配置项写对就行,结果忽略了 native 库的部署。一个可用的检查手段是 hadoop checknative,输出里如果没有 lzo 对应的 true,说明它压根没加载成功。若团队里没有人愿意折腾编译部署,又需要可切分的裸文本压缩,其实可以考虑 Zstd,它同样能切片且在新版本 Hadoop 里支持度更好,部署成本低不少。

5.2 索引文件丢失导致任务打回“单 Map”

LZO 支持切片的前提是存在索引文件,通常是 .lzo.index。如果只有 .lzo 数据文件而索引文件缺失,计算引擎不知道每个块从哪里开始独立解压,只能把整个文件当成一个分片处理。之前某数据团队在做跨集群数据迁移时,用 distcp 拷贝了一批 lzo 文件,结果索引没复制过去,下游跑批任务突然从几百个 Map 降到一个 Map,作业时间从半小时拉长到十几个小时。

这个坑的教训很明确:lzo 索引文件是数据的一部分,迁移、备份时必须一并保留。在迁移脚本里明确写出对所有 .lzo 及 .lzo.index 文件一并处理,事后检查文件数量是否成对出现。如果索引真的丢了,可以用工具重新扫描原文件生成索引,但这会消耗大量 IO 资源,最好选在业务低峰期操作。

5.3 重复压缩:白白损耗 CPU

还有一种情况是“压了个寂寞”。有次我排查一个数据链路,发现上层已经把 Parquet + Snappy 文件写好了,下层任务读取后又做了一次 gzip 压缩输出到归档目录。Parquet 内部的 Snappy 已经把数据压过一遍,再套一层 gzip,几乎压不动,反而白白增加了整条链路的 CPU 消耗。碰到这种问题,检查文件后缀和配置就能发现:一个文件同时带 .parquet 和 .gz 后缀,或者 Hive 表属性是 PARQUET 但会话又打开了 text 输出的 gzip 压缩,都是重复压缩的迹象。

重复压缩的另一个常见字体是“对人畜无害,对系统有害”:单看每个任务耗时不多,但全集群任务叠加起来,浪费的 CPU 相当可观。建议在开发规范里明确:列式存储表用表属性控制压缩,不要额外打开文件输出压缩;归档链路只保留一层压缩,不用对已有压缩格式二次包装。

5.4 小文件加 gzip:调度灾难的隐藏源头

如果把大量小文件分别用 gzip 压缩后存放,而每个文件又是不可切分的,那么每个文件都对应一个 Map 任务。文件数量一多,任务调度、启动、清理的开销会淹没真正的计算时间,集群会出现大量短命 Map 任务,NameNode 的 RPC 压力也随之暴涨。这种问题通常不会立刻报警,但你会发现集群整体吞吐越来越差、队列积压越来越多。

正确的处理方式是把小文件合并成大文件后再压缩。比如日志类小文件可以先合并成 SequenceFile 或直接写入 Parquet 表,再启用压缩。Parquet 本身有行组结构,一个文件可以做得很大且保持可切分性,配合 Snappy 或 Zstd,既解决了小文件问题,又保住了并行度和压缩率。

6. 压箱底经验:先测再选、参数与监控

6.1 动手前先做一个最小可对比的压缩测试

我强烈建议所有团队在引入新压缩格式前,先用真实数据样本做一次压缩测试,不要直接信网上的经验值。方法很简单:取真实业务数据的一个小目录,比如 20GB 左右的文本日志,用不同 Codec 分别压缩一遍,记录压缩时间、压缩后体积和一次典型查询的耗时。

测试后做成一张自己的对比表。某次我用 Hadoop 自带工具压缩一份 25GB 的日志样本,结果是:gzip 压到约 7.5GB,用时约 6 分钟;lzo 压到约 10GB,用时约 2 分钟;snappy 压到约 11GB,用时不到 1 分钟;zstd 压到约 8.5GB,用时约 3 分钟。当然这个结果只对当时那份数据有效,不同数据分布下数字会变化,但这类测试能帮团队建立“自己的数据到底适合什么”的判断。测试时别用 core-site 的全局配置,直接在命令行或任务配置里指定要测的 Codec 即可。

6.2 三个容易被忽略的调优点

第一个是压缩级别。gzip 和 zstd 都支持调节压缩级别,gzip 从 1 到 9,数字越大压缩率越高但耗时越长;zstd 也有类似级别选项。很多人默认用级别 6 或默认值,但日志归档场景下其实可以试试更高级别看能省多少空间,因为这是低频读取的数据,压缩时间稍长问题不大。

第二个是 IO buffer 大小。io.file.buffer.size 默认通常是 4KB 或 128KB,取决于版本。调大这个值可以提高大文件顺序读写的效率,对压缩数据的读写也有正向影响。我在一次调优中把 buffer 从 4KB 调到 128KB 后,压缩文件读取任务的整体耗时下降了大约 10%。

第三个是 HDFS 块大小与数据量的匹配。当一个压缩文件解压后的大小远大于一个 Block 时,可切分格式的优势才体现得出来;如果文件本身就小于 128MB,可不可切其实无所谓,因为本来也只能一个 Map 处理。所以不要盲目追求“所有文件都可切”,小文件该合并先合并,再来谈压缩格式。

6.3 用集群指标确认压缩收益

配置上线后,要用监控数据确认收益,而不是只看“任务跑完了”。我一般关注三个指标:HDFS 实际存储量变化、典型作业的 Shuffle 数据量变化、任务完成时间变化。HDFS 侧用 hadoop fs -du 按目录对比即可;Shuffle 数据量可以在 YARN 的 Counter 或 Spark UI 里看到;作业时间对比最好是相同数据、相同资源配置、相同时间段的多次采样,避免偶然波动。

还有一个容易忽略的收益指标是磁盘 IO 压力。如果集群之前频繁出现 DataNode 磁盘繁忙,压缩生效后这一指标通常会明显缓解。这不是压缩格式本身的功劳,而是因为读写的数据量变少了。反过来,如果你发现压缩后 CPU 使用率明显上升、任务反而变慢,就要考虑降低压缩级别或改选速度更快的格式。

7. 收尾的几句实在话

压缩格式没有绝对的“最好”,只有“当前场景下最合适”。我的习惯是每半年或每换一批硬件后,重新做一轮压缩测试,因为集群的 CPU 算力、网络带宽、磁盘类型都会影响最优选择。曾经觉得不可用的方案,可能在资源充裕后变得非常划算。最后一个小技巧:在做选型对比时,把“解压速度”和“压缩率”放在同等重要的位置,很多团队的失败不是压缩率不够,而是解压太慢导致查询越来越卡。数据压缩是省钱的杠杆,也是性能的隐形开关,花一两天认真测一次,后面一整年都会感谢当时的自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询