拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

大数据压缩提速:算法选型与Hive/Spark配置实践

大数据压缩提速:算法选型与Hive/Spark配置实践

1. 为什么压缩能“让处理速度飞起来”

先讲一个很多刚接触大数据的人都会有的误区:压缩是拿时间换空间,让数据变小省硬盘,但代价是解压耗时,处理速度只会更慢。这个说法在小规模单机场景里基本成立,但放到 Hadoop、Spark、Hive 这类分布式框架里,结论完全反过来了。

我最早在业务里感受到压缩对速度的“正向加速”,是在跑一张网约车订单明细表——每天几亿条记录,按天分区落成 Parquet 格式,表体量轻松上 PB。最开始表里存的是未压缩的 Parquet,集群跑一个按日期聚合的 Hive 任务,Map 阶段光是从 HDFS 读数据就要十几分钟。后来把存储格式改成 ORC,压缩算法从默认的 ZLIB 换成 Snappy,同样的任务,Map 阶段读数据的时间降到了三分钟以内。不是 CPU 不够,而是整个链路被磁盘 I/O 和网络 I/O 卡死了,压缩好比把“搬砖”变成了“搬压缩砖”,砖变轻了,搬得就快。

在大数据计算引擎里,数据处理的瓶颈通常不在 CPU 计算,而在数据搬运:从 HDFS 读文件、在节点之间走网络传输、Shuffle 阶段往磁盘写中间结果、最后再落盘输出。任何一个环节的数据量变小,整个任务就跑得更快。这就是为什么在大数据领域,压缩不是“省空间”的附属品,而是“提性能”的一等公民。

这篇内容我会围绕几个实操问题来讲:压缩算法到底怎么选、列式存储和压缩怎么配合、Hive 和 Spark 里怎么配置压缩、以及压缩之后常见的那些坑怎么排查。适合正在做离线数仓、实时链路压测、或者刚搭好大数据集群但任务跑得不够快的人参考。我会把选型逻辑和踩过的坑一并写出来,尽量少讲空理论,多给能直接抄作业的参数和方案。

2. 整体思路:把“压缩”放在整个数据处理链路里看

2.1 大数据场景下压缩的本质:用 CPU 换 I/O

要理解压缩为什么在大数据里是“提速度”的,得先摸清楚分布式计算的钱花在哪里。以一个典型 Hive SQL 为例,数据从 HDFS 读出来,经过 Map 端处理,然后 Shuffle 到 Reduce 端,最后写回 HDFS。整个链路中,数据至少经历两次写盘、两次读盘、一次网络传输。

一次磁盘读写的成本比 CPU 做一次压缩/解压要高出好几个数量级。磁盘机械臂寻道、SSD 闪存擦写、网络 TCP 传输,都是用“物理时间”换数据流动。如果你把 2GB 的数据压缩成 800MB,那么从 HDFS 读取的字节数少了一半多,网络传输的字节数少了一半多,甚至中间落盘的临时文件也小了一半多。压缩引入的那点 CPU 开销,在动辄几十上百 GB 的作业里几乎可以忽略。

我把这个思路总结成一句话:在大数据场景里,压缩是用小部分 CPU 换取大幅的 I/O 缩减,而 I/O 才是分布式计算真正的瓶颈。这个等式什么时候会失效?当你集群的 CPU 本身已经跑满,或者数据是纯 CPU 密集型计算(比如复杂的正则匹配、大规模 JOIN)时,压缩引入的开销才会体现出来。但在绝大多数数仓 ETL 和统计聚合场景下,压缩带来的收益是压倒性的。

2.2 降低存储成本只是副产品,真正目标是“减少网络与磁盘搬运量”

很多团队在决定“要不要上压缩”时,第一个念头是“存储都快满了,得省点空间”。这个诉求没错,但容易让人选错方案。如果单纯为了省空间,你可能直接选 GZIP,压缩率确实最高,但 GZIP 解压速度慢,反而会拖慢作业。但如果从“减少搬运量”的角度去思考,就会更关注压缩率和解压速度的平衡,而不是一味追求压缩比。

我参与过一个集群压缩方案评估,当时存储使用率已经到了 85%,团队希望把所有 Hive 表重建成 GZIP 压缩格式。我看完任务清单后拦了下来,因为这批表大部分跑的都是 T+1 全量聚合任务,读多写少,GZIP 的高压缩率确实能省空间,但每个查询任务都要付出昂贵的解压时间,跑一遍全量数据等于拖慢整个集群的节奏。后来我们改用了 ORC + Snappy,存储使用率降到 60%,任务耗时反而比压缩前还短。空间省了,速度还快了,这就是把目标从“省存储”变成“减 I/O”之后带来的思路转变。

另一个关键点:压缩要放在整个数据链路里看,而不是只看单张表。你会发现一个作业慢,往往不是某一段计算慢,而是数据从 A 节点搬到 B 节点时把带宽打满了。如果中间结果也做压缩,比如 Spark Shuffle 开启压缩、Hive 的中间 Map 输出开启压缩,整体的加速效果会比只压缩表文件更明显。链路里每一处流量都瘦身,任务才能整体“飞起来”。

2.3 压缩策略的分层:表文件压缩、中间结果压缩、网络传输压缩

在实操中,我会习惯把大数据压缩分成三个层面:表文件压缩、计算中间结果压缩、网络传输压缩。三个层面解决问题不同,配置方式也不同。

表文件压缩是最常见的一层,通过建表语句指定存储格式和压缩算法,落盘到 HDFS 的文件就是压缩后的。这一层直接影响存储成本和全表扫描的读速度。中间结果压缩发生在 Map 输出和 Shuffle 阶段,Map 输出的数据会先写到本地磁盘再被 Reduce 拉取,这个环节的数据量往往比最终结果大得多,压缩收益明显。网络传输压缩在部分引擎里有独立开关,比如 Spark 的spark.io.compression.codec和spark.shuffle.compress,这两个参数管的是节点间数据传输的压缩行为。

合理的策略是三层都开,但算法可能不同。表文件用 ORC + Snappy,既有不错的压缩率又有较快的解压速度;Shuffle 中间结果用 LZ4 这类极致速度型算法,因为中间数据生命周期短,不需要追求高压缩率;网络传输环节用 Zstandard 平衡带宽和 CPU。下面的章节我会展开讲每个算法到底怎么选。

3. 压缩算法选型:LZO、Snappy、GZIP、Zstandard 到底怎么选

3.1 五大数据压缩算法横向对比

这一节先把主流压缩算法的优缺点列清楚,再讲选型逻辑。我会用表格展示核心参数,然后逐条解释适用场景。

算法压缩速度解压速度压缩率是否可切分典型应用场景
GZIP慢慢很高是冷数据归档、存储成本敏感场景
BZIP2极慢慢最高是极少使用,压缩太慢基本被淘汰
LZO快快中等是(需建索引)Hadoop 旧生态中使用广泛
Snappy快很快中等是大数据最常用,适合数仓分析
LZ4极快极快偏低是极速场景、实时链路
Zstandard较快较快较高是兼顾速度与压缩率的新选择

表格里的“可切分”是指单个文件能否被多个 Map 任务并行读取。这是大数据场景里很容易被忽略的一个性质,实际影响非常大。如果一个压缩文件不可切分,那么无论文件多大,也只能由一个 Map 任务处理,分布式计算就退化成单机处理,速度直接从“飞起来”变成“爬着走”。

在 HDFS 存储层面,Snappy 压缩的块文件天然支持切分,因为它是按块压缩的,每个块可以独立解压。而 GZIP 虽然整体不可切分,但当它作为 ORC 或 Parquet 这类列式存储的内部编码方式时,文件内部已经按 stripe 或 row group 划分了块,Map 任务可以按块读取并独立解压,所以也是可切分的。这个知识点容易混淆,后面我会单独用一个章节讲清楚。

3.2 算法选择逻辑:先看场景再看压缩率

我在实践中总结了一套选型规则,把场景分成三类。

第一类是离线数仓、分析查询类场景,比如 Hive 表、Spark SQL 读取的明细数据。这类场景读多写少,存储占用大,查询时要频繁全表扫描,推荐首选ORC + Snappy或者Parquet + Snappy。Snappy 压缩率虽然不如 GZIP,但解压速度快,查询 SQL 的响应时间直接受益。这是目前业界最主流的选择,网上能搜到的 Hive 优化文章基本都推荐这个组合。

第二类是实时计算、写入链路类场景,比如 Flink 写 Kafka、Kafka 到 HDFS 的落盘、或者频繁的临时结果集写入。这类场景对写入吞吐要求极高,数据生命周期短,推荐LZ4。LZ4 的压缩速度飞快,CPU 开销几乎可以忽略,虽然压缩率偏低,但对于短暂存在的中间数据来说完全够用。

第三类是冷数据归档、备份、极少访问的报表场景。这类数据几年都不一定查一次,但对存储成本敏感,推荐GZIP或者Zstandard(高压缩级别)。Zstandard 在相近压缩率下压缩和解压速度都比 GZIP 快很多,是 GZIP 的现代替代品。如果你的集群版本支持,我建议直接用 Zstandard 替代 GZIP,效果更好。

还有一个比较偏门但值得说的情况:MapReduce 任务里,Map 输出的中间数据如果使用 LZO 压缩,需要额外建立索引才能支持切分,否则会退化成串行处理。搭建老版本 Hadoop 集群的人可能踩过这个坑,新版本用 Snappy 或 LZ4 就没有这个问题,这也算是一个“能用新不用旧”的理由。

3.3 列式存储 + 压缩 = 大数据分析的黄金组合

既然说到了表和文件的存储格式,那就必须把“列式存储”和“压缩”绑在一起讲。ORC 和 Parquet 是两个主流的列式存储格式,它们和压缩算法的配合关系,决定了查询性能和压缩率的上限。

列式存储的核心逻辑是:同一列的数据连续存储在一起。这样做有两个直接好处。第一,查询时只需要读取涉及的列,比如一个宽表有 100 个字段,但聚合 SQL 只用其中的 5 个,列式存储只需扫描这 5 列的数据,I/O 量直接减少到原来的 1/20。第二,同一列的数据类型一致、值域相近,比如“城市”字段里大量重复值,“金额”字段里数值分布有一定规律,这种局部相似性让压缩算法能发挥出比行式存储高得多的压缩比。可以这么理解:一个班学生的身高列表(列式)和一个记录每个人各项信息的花名册(行式),前者在压缩时更容易找出重复规律。

ORC 文件对压缩策略做了进一步的优化,它在写入时支持对不同的列使用不同的编码方式,比如字典编码、增量编码、位图编码。以订单状态字段为例,只有“成功”“失败”“处理中”三种取值,字典编码能把这些字符串映射成短编号,再配合压缩算法,这一列的存储开销可以忽略不计。Parquet 采用类似的列块(Column Chunk)组织方式,也支持丰富的编码和压缩。

所以最佳实践不是简单地把“压缩算法”加到一个普通文本文件上,而是把“列式存储格式”和“压缩算法”组合起来。用 ORC/Parquet 替代纯文本或 CSV,本身就是最大的“压缩”,再加上 Snappy 或 Zstandard,才能把存储和性能同时做好。

4. 实操:把 Hive 和 Spark 的压缩配置一步步调起来

4.1 Hive 建表:ORC + Snappy 的正确写法

Hive 里配置压缩,涉及两个层面:表文件的存储格式和压缩算法、MR 计算过程中的中间压缩开关。先讲最常见的场景,怎么建一张希望全表扫描飞起的大表。

表的建表语句写法如下:

CREATE TABLE ods_order_daily ( order_id STRING, user_id STRING, city_id STRING, order_amount DECIMAL(10, 2), order_status INT, create_time STRING ) PARTITIONED BY (dt STRING) STORED AS ORC TBLPROPERTIES ( 'orc.compress' = 'SNAPPY' );

注意几个细节。用STORED AS ORC指定存储格式后,默认的压缩算法其实是 ZLIB。很多教程不会强调这一点,导致表确实用了 ORC,但压缩率奇低或者作业速度不符合预期,原因就是压缩算法不对。在TBLPROPERTIES里明确设置'orc.compress' = 'SNAPPY'才能生效。

如果使用的是 Parquet 格式,配置项是'parquet.compression' = 'SNAPPY'。还有一种方法是建表时不指定,靠 Hive 全局配置兜底,对应参数是hive.exec.orc.default.compress,我建议在建表语句层面写清楚,避免后续维护时搞不清某张表到底用了什么压缩,无法预估任务性能。

建表之外,还要关注 Hive 中间过程的压缩。在 Hive CLI 或 Beeline 中执行:

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

hive.exec.compress.intermediate控制的是 Map 输出到 Reduce 拉取的数据是否压缩,这个参数关了,Map 输出的数据会以非常庞大的体量落盘再传输,Shuffle 阶段就变成了性能杀手。hive.exec.compress.output控制的是最终输出文件是否压缩,如果跑完INSERT OVERWRITE后希望结果文件也采用压缩格式,这个开关必须打开。

4.2 Spark 作业压缩参数调整速查

如果你用的是 Spark SQL 或 Spark 批处理作业,压缩配置参数不太一样。以 Spark 3.x 为例,核心参数如下:

spark.sql.parquet.compression.codec snappy spark.sql.orc.compression.codec snappy spark.io.compression.codec lz4 spark.shuffle.compress true spark.shuffle.spill.compress true spark.broadcast.compress true

这里的逻辑和 Hive 很相似但又略有差异。spark.sql.orc.compression.codec和spark.sql.parquet.compression.codec管的是最终落盘数据文件的压缩格式,生产环境我一般设为 snappy。spark.io.compression.codec管的是 Spark 内部 RDD 数据、Shuffle 数据和广播变量的压缩编码,默认是 LZ4,一般不用改,LZ4 速度最快。spark.shuffle.compress和spark.shuffle.spill.compress管的是 Shuffle 写磁盘时是否压缩以及溢写文件是否压缩,这两个开关务必保持 true。

实际调优现场,我曾经处理过一个 Spark 作业跑 2 小时的问题。任务本身逻辑很简单,无非是读取订单表、按城市聚合、输出结果。用 Spark UI 观察发现,Shuffle 读数据量高达 1.2TB,而输入数据才 300GB,说明 Shuffle 环节存在严重的数据膨胀。逐个检查参数后,发现spark.shuffle.compress被设置成了 false,导致 Shuffle 中间结果全部明文落盘。改回 true 并确认 LZ4 生效后,同一作业的耗时降到 43 分钟。这就是中间结果压缩开关的威力,它不直接影响某一张表的存储,但对作业速度的提升是肉眼可见的。

4.3 压缩实操之后:如何确认压缩真的生效了

很多人配置完参数后不管结果,直接跑任务,最后发现读到的数据量确实变小了,但说不清是哪一层压缩起的作用。我建议用以下几个方法验证压缩是否生效。

第一个方法:查看表的存储信息。在 Hive 里执行DESCRIBE FORMATTED table_name,输出信息里有Storage Desc Params一栏,会显示orc.compress=SNAPPY,这就确认表文件层面压缩生效。

第二个方法:查看 HDFS 文件的实际大小和块数量。比如压缩前一张表一天的分区有 400GB、4000 个文件,压缩后变成 150GB、1500 个文件(其实压缩不影响文件数,但影响块大小),如果文件大小明显小于逻辑预计值,说明存储层面压缩正常。

第三个方法:在 Spark UI 或 YARN 的 Counter 里看 Shuffle 字节数。如果一个作业的 Shuffle Write 和 Shuffle Read 数据量明显小于输入量,说明中间压缩生效。

第四个方法最简单粗暴:拿一张表做实验,分别记录压缩前后的文件总大小和一次全表 COUNT 的耗时。注意 COUNT 不涉及计算逻辑,能比较纯粹地反映读取速度差异。这不是严格的性能基准,但在日常调整中足以验证方向是否走对了。

4.4 关于“是否所有 Hive 表都适合压缩”的答案

有人可能会想,既然压缩这么有效,那我把所有表都改成 ORC + Snappy 不就行了?这个想法不全面。

第一,频繁 INSERT 小批量数据的表,比如业务方通过 JDBC 往 Hive 里插数、或者 Flink 实时写入且每秒只产生少量数据,这类表如果用 ORC,会产生大量小文件,后续查询会被文件数拖垮。小文件问题有时候比压缩问题更严重,处理不好,压缩带来的性能收益全被文件寻址开销吃回去。面对这种场景,要么采用分区合并策略,要么用 Hudi 这类支持小文件自动合并的表格式。

第二,超宽表里的 JSON 嵌套字段、array/map 复杂类型,在 ORC 中有一些优化限制,压缩率可能不如预期,遇到这种表建议先做小规模测试再决定是否全量改造。

第三,表是给其他团队通过 Presto/ClickHouse 等外部引擎读取的,格式和压缩的选择要和这些引擎的特性对齐。比如某些版本的外接引擎对 ORC 支持不如 Parquet 好,这时候就得优先考虑查询方的兼容性。

所以我给的建议是:核心大表、慢查询表优先改造为 ORC/Parquet + Snappy,小文件严重、外部对接复杂、写入频繁的表单独调研后再动。压缩不是万能药,但在对的方向上用,效果立竿见影。

5. 大数据集群层面的压缩配置与部署策略

5.1 HDFS 块大小和压缩文件的切分关系

既然前面多次提到“可切分”,这里把 HDFS 块大小与压缩的配合关系讲透,这也是好多人配置集群时没注意的问题。

HDFS 默认块大小是 128MB,如果是大表文件,一个文件通常有几十上百块,Map 任务会按块去读取并处理,天然分布式并行。当一个文件是 GZIP 压缩且不是列式存储时,它作为一个整体才可解压,Map 任务只能整个拉取,无法按块切分。这就是“GZIP 不可切分”的由来。

但 ORC 和 Parquet 不一样。ORC 文件内部自带索引和多个 stripe,每个 stripe 是独立的压缩单元,即使整体是一个大文件,Map 任务也可以从文件的某个偏移量开始读取并解压对应的 stripe。我把这个概念比喻成“一本书的每一章都可以单独撕下来读”。所以 ORC/Parquet + GZIP 也是可切分的,只是 GZIP 的解压速度较慢,影响查询效率。

实操中的建议是:如果你还在用 Hive 的 TEXTFILE 格式且想上压缩,优先选择 Snappy 或 LZO,别直接上 GZIP,否则大文件会退化成单 Map 任务。如果你已经用 ORC/Parquet,那么压缩算法可以更自由一些,但考虑到查询性能,仍然建议 Snappy 优先。

5.2 集群部署策略中与压缩相关的关键调参点

集群部署阶段,有几个参数和压缩关联密切,容易被部署文档一笔带过,但实际影响了数据压缩和任务调度效率。

dfs.blocksize和dfs.namenode.handler.count影响文件块数量和 NameNode 的并发处理能力。块越小,文件数量越多,NameNode 的内存压力和 RPC 请求量就越大。块越大,单个 Map 任务读取的数据块越少,并行度可能下降。通常大文件场景推荐 256MB 或 512MB 块,压缩后的数据块更大,能减少任务调度开销。但注意,如果数据量本身不大,块设大了反而浪费资源。

YARN 侧的yarn.nodemanager.resource.cpu-vcores和yarn.scheduler.maximum-allocation-vcores决定了集群能提供多少 CPU 给计算任务。压缩解压需要消耗 CPU,如果你在任务里大量使用高压缩率算法(比如 GZIP),而集群 CPU 核心数配置不足,解压会阻塞 Map 任务的处理速度。反过来,Snappy 和 LZ4 这类轻量算法的 CPU 开销很小,即使在 vcore 不太宽裕的集群上也能跑得不错。

mapreduce.map.output.compress、mapreduce.map.output.compress.codec在 MR 任务中控制 Map 输出压缩,对应 Hive 的 SET 语句。很多部署脚本只配置了 HDFS 层面的压缩,忽略了任务中间结果的压缩,导致 Shuffle 流量始终降不下来。我见过不少集群,表文件全部是压缩格式,但作业运行速度依然很慢,最后排查发现 Map 输出是明文,Shuffle 数据量又大又卡。

5.3 常用压缩组件的部署版本选择建议

大数据组件版本决定你能用哪些压缩算法,这个点值得单独说。

Hadoop 2.x 时代,LZO 需要额外安装hadoop-lzo库并重新编译 native 库,好多团队因为嫌麻烦直接放弃了 LZO。Hadoop 3.x 开始,Snappy 和 LZ4 已经内置支持,Zstandard 也在较新版本中得到支持。如果你的集群还在 Hadoop 2.7 这类老版本,建议至少升级到 Hadoop 3.2 以上,这样 Zstandard 库不需要手动部署,Hive/Spark 里直接配ZstdCodec就能用。

还有一个易踩的坑:Spark 3.0 之前spark.io.compression.codec的默认值是 LZ4,但某些发行版的 Spark 二进制包可能没有包含 LZ4 的 native 库,导致运行时报错“Cannot load liblz4.so”。排查思路很简单:检查$SPARK_HOME/lib下 native 库是否完整,或者换用snappy作为spark.io.compression.codec的值规避。这个坑在腾讯云、阿里云 EMR 的早期版本里都出现过,不是个别现象。

6. 常见问题与排查技巧实录

6.1 压缩之后文件变小了,但作业反而变慢

这个问题我遇到不止一次。先说结论:大概率是“压缩算法选型不当”和“小文件过多”两者之一导致的。

选型不当的场景,是把所有 Hive 表默认建成了 GZIP 压缩。GZIP 压缩率确实高,但解压速度慢,在 CPU 资源本就不充足的集群上,读取文件时的解压开销反而盖过了 I/O 节省。排查方法也很直观:在 Spark UI 的 Stage 详情里看每个 Task 的 CPU 时间。如果 CPU 时间明显高于任务实际执行时间,说明 CPU 大量花在解压上,这时候换 Snappy 或 Zstandard 就能缓解。

小文件过多的场景是:数据本身写入时被切成了几千个小 ORC 文件,每个文件只有几 MB,压缩后确实小,但读一个分区需要打开上千个文件,调度和元数据开销远超数据读取本身。这类问题没法靠调整压缩算法解决,需要做文件合并,或者在写入端设置分区大小和文件大小阈值,让落盘文件保持在合理的 200MB~500MB 区间。

6.2 压缩率达不到预期:从 10:1 变成 2:1 的原因排查

不少人在验证压缩效果时发现,表格里的数值列压缩效果很好,但字符串列压缩率很低,甚至某些列完全没压下去。

原因要从 ORC 的列编码机制说起。对于取值非常离散的列,比如订单 ID、用户 ID,随机性强、重复度低,字典编码失效,压缩算法在它们身上能捞到的“水分”极少。反观用户城市、订单状态这种重复度高的列,字典编码能把重复值转成短编号,压缩率惊人。

如果你的业务需求必须存储大量高基数、低重复列,压缩率不理想是正常现象,并不代表配置出了问题。真正需要排查的异常是:某些列明明重复度很高,压缩率却很差。这种情况往往是因为该列的数据类型设置成了 STRING 而不是 INT 或枚举类,导致字典编码无法按数值类型做最优处理。能改成数值类型的字段尽量用数值类型,既能提升压缩率,也能提升查询性能。

6.3 Shuffle 阶段数据膨胀的定位与处理方法

Shuffle 数据量超过输入数据量,是一件让人头疼的事。除了压缩开关没打开,还有一个常见原因是JOIN 和 GROUP BY 的 Key 设计不合理,导致数据被分发到大量 Reduce 任务后产生了严重的数据复制或热点倾斜。

举个例子,一个订单表 JOIN 城市维度表,城市字段里“北京”的数据量是其他城市的几十倍。Shuffle 时北京的数据全部涌向同一个 Reduce 任务,该任务的 Shuffle 数据量爆发,整个作业被单个任务拖死。压缩能减少这个环节的字节数,但不能解决数据倾斜问题。处理方式通常是加随机前缀打散 Key,或者用 Salting 技术把大 Key 拆成多个子 Key 分开聚合,最后再合并结果。

排查 Shuffle 问题时,看 Spark UI 的“Shuffle Read Size / Records”是第一步,把所有 Task 的 Shuffle 数据量按大小排序,如果最大的几个 Task 数据量是平均值的几十倍,那基本可以确认是数据倾斜。压缩解决了“字节多”的问题,数据倾斜解决的是“字节挤在一起”的问题,两个层面都要处理。

6.4 常见问题速查表

现象可能原因解决办法
压缩后作业反而变慢GZIP 解压开销过大换 Snappy / Zstandard
表文件数量剧增压缩后文件块变小但文件数不变合并小文件、调整分区粒度
单个 Map 任务处理很慢使用了不可切分压缩格式改用 ORC/Parquet + Snappy
Shuffle 数据远超输入数据中间压缩未开启设置hive.exec.compress.intermediate为 true
CPU 使用率异常偏高压缩算法解压开销大降低压缩级别或换低 CPU 开销算法
大表 COUNT(*) 变慢扫描了大量不必要列或文件过碎使用列式存储并合理切分文件
压缩率远低于预期高基数低重复字段多调整字段类型,使用字典编码优化
Spark 作业报 LZ4 native 库错误发行版缺少 native 库换 snappy 或补齐 native 库

6.5 几个独家避坑技巧

最后分享几个常规文档里不会写的小技巧。

第一个:优先压“重复度高的列”,而不是盲目压所有列。在 ORC 中,可以通过TBLPROPERTIES的orc.compress.size调整压缩块大小,让每一列的压缩更充分。但这个参数的调整幅度不宜太大,块越大,查询时解压的数据单元也越大,反而拖慢随机查询。经验值 256KB 是比较均衡的选择。

第二个:压缩算法和存储格式在写入和读取两侧必须对称。有一次我把一张表从 ORC + Snappy 改成了 Parquet + Snappy,但忘了检查下游读取方(一个 Presto 查询服务)对 Parquet 的支持配置,结果 Presto 解析新表时直接失败。改任何存储或压缩方案之前,先确认整条数据链路的消费方都能兼容,这是最容易被忽略的坑。

第三个:大数据里的“最优压缩方案”不存在,只有“对当前集群最合适的方案”。如果团队资源紧张,CPU 核数较少且业务以离线 T+1 为主,用 ORC + ZLIB 也不是不行,因为夜间批量跑任务对速度不敏感,省存储成本更划算。如果业务强调实时性和查询交互,Snappy/LZ4 就是必然选择。方案没有绝对好坏,全看业务场景。

第四个关于验证的小技巧:压测时不要直接拿生产任务跑,先造一张 2~3GB 的测试表,分别用 GZIP、Snappy、Zstandard 建三个版本,跑同一个聚合 SQL,对比执行时间和资源消耗。数据量小,实验成本几乎为零,但结果能直接指导线上选型。这个方法我每次到新团队都会用一次,屡试不爽。

7. 写在最后:压缩提速的底层逻辑与我的经验

做大数据这行,很多时候我们花了大量精力去调 SQL、调 JOIN 逻辑、调参数,却忽略了最基础的数据组织方式。数据压缩不是最高深的优化技巧,但它是在整个数据链路里影响面最广、改造成本最低、收益最直接的优化手段之一。

从我个人的实际经验来看,一个集群的数据压缩策略,从设计到落地,其实只需要考虑三件事:业务读写的特性是读多写少还是写多读少、集群 CPU 和 I/O 哪个更紧张、上下游组件对存储格式的兼容性如何。想清楚这三件事,压缩算法的选择自然就浮出水面。

分享一个小技巧作为收尾:每次写完压缩配置,都别急着跑正式任务,先跑一个 EXPLAIN 或者小数据量的抽样任务,确认配置真正生效、文件格式正确、下游消费方读取正常,再放量执行。我见过太多次因为压缩配置写错但任务没报错,导致数据全部以未压缩方式落盘的情况,等发现时存储已经翻了一倍。先验证再放量,这六个字能帮你省下无数返工时间。

如果后续你有机会接触 Hudi、Iceberg 或者 ClickHouse 这类新生态,你会发现“压缩 + 列式存储”的思路依然贯穿始终,只是换了一套实现方式和参数名。理解了底层逻辑,不管组件怎么换,你都能快速找到那条让数据“飞起来”的路。

返回列表