深入 Hadoop 17 - 序列化与压缩
上一篇讲了 RPC。本篇讲 RPC 和数据存储都依赖的两个底层机制——序列化和压缩。
序列化与压缩常被合并介绍。这两个机制其实是不同维度——序列化解决"对象怎么变字节流"(结构化),压缩解决"字节流怎么变更小的字节流"(空间优化)。两者经常组合使用——序列化后的字节流再压缩存储或传输。准确的说法是:Hadoop 提供多种序列化格式(Writable / Protobuf / Avro),与多种压缩格式(Gzip / BZip2 / Snappy / LZ4 / Zstd)正交组合,按场景选择最优组合。
本篇只抓一个问题:Writable 与 Java Serializable 的差别、Protobuf 与 Avro 各自的取舍、Snappy vs Zstd vs Gzip 的压缩比/速度权衡、压缩在 Hadoop 各个环节的具体应用位置。
序列化的本质
序列化(serialization)解决"对象 → 字节流"和"字节流 → 对象"的双向转换。分布式系统的所有跨进程数据交换都依赖序列化——RPC 参数、HDFS 文件内容、Shuffle 中间数据、作业配置传递。
序列化机制的设计目标可以拆成五个维度:
1 | |
没有一种序列化在所有维度都最优——这是典型的工程权衡。Hadoop 支持多种序列化格式让用户按场景选择。
Writable:Hadoop 原生序列化
Writable 是 Hadoop 自己的序列化机制,所有 Hadoop 内部数据结构(Text、IntWritable、LongWritable、ArrayWritable 等)都实现 Writable 接口:
1 | |
实现 Writable 的类自己定义怎么序列化到 DataOutput、怎么从 DataInput 反序列化。这种"自己实现"的方式让序列化速度极快(直接调用方法,无反射开销),同时让字节流紧凑(没有 Java Serializable 的类名等冗余信息)。
Writable 与 Java Serializable 的对比:
1 | |
Writable 的设计动机是 Java Serializable 不适合 Hadoop 的场景——MapReduce 处理海量记录,每条记录的序列化开销累加显著,Writable 把这个开销降到最低。
Writable 的简单示例(自定义类型):
1 | |
注意 write 和 readFields 的字段顺序必须一致——这是手写序列化的责任,框架不检查。如果顺序写错(write 是 first/second,readFields 是 second/first),反序列化会得到错误数据但不报错,调试困难。
Protobuf:跨版本兼容的选择
Protobuf 是 Google 设计的跨语言序列化格式。Hadoop 2.x 起 RPC 协议改用 Protobuf(上一篇讲过),Hadoop 3.x 起 RPC 强制 Protobuf。
Protobuf 的核心抽象是 schema(.proto 文件)。Schema 定义消息结构,编译器生成各语言的桩代码:
1 | |
编译器生成 Java、Python、C++、Go 等语言的类,每个类知道怎么序列化/反序列化自己的字段。Protobuf 的 wire format 紧凑(varint 编码 + field tag),通常比 Java Serializable 小 3-5 倍。
Protobuf 的版本兼容机制是 optional 字段——schema 添加新字段不会破坏旧版本反序列化(旧版本忽略未知字段)。这让 Hadoop 可以平滑滚动升级。
Protobuf 的代价:
CPU 开销比 Writable 高约 10-20%(解析 field tag、varint 解码)。
Schema 必须 precompile——.proto 文件编译生成桩代码,不支持运行时动态 schema。
不适合"任意位置拆分"——Protobuf 消息长度在前缀,必须从消息边界开始解析(不是任意位置)。这让 Protobuf 不适合 HDFS 文件内容的直接存储,但适合 RPC(每条 RPC 是完整消息)。
Avro:Schema 嵌入的序列化
Avro 是 Hadoop 生态系统的另一种序列化格式,由 Doug Cutting(Hadoop 创始人)2009 年创建。Avro 与 Protobuf 类似(schema-based、跨语言),但有几个关键差异:
1 | |
Avro 的核心创新是"schema + data 同放"。Avro 文件包含 schema 头 + 多个 data block,每个 block 是一批按 schema 序列化的记录。这种结构让 Avro 文件天然支持 MapReduce 拆分——每个 Map Task 处理一个 block 边界开始。
Avro 的 schema evolution 比 Protobuf 更宽松——只要字段类型兼容,可以增删改字段(删除字段需要默认值,添加字段需要默认值)。
Avro 在 Hive / Pig 生态里广泛使用,作为 Sequence File 的替代品。HBase 内部也用 Avro 做某些元数据存储。
三种序列化的选择
Hadoop 生态里同时存在三种序列化,按场景选择:
1 | |
实际项目中混合使用很常见——MapReduce 内部用 Writable(速度优先),最终数据存到 HDFS 用 Avro / Parquet(schema 友好),RPC 用 Protobuf(兼容性)。
压缩:节省空间与传输
压缩(compression)解决"字节流怎么变更小"。Hadoop 集群里的数据从 GB 到 PB 级,每字节的存储成本和网络带宽成本都被压缩放大。
Hadoop 支持的压缩格式:
1 | |
注意"可拆分"这一列。MapReduce 处理大文件时按 InputSplit 切分,每个 Split 由一个 Map Task 处理。如果文件被 Gzip 压缩,从压缩流中间开始读不可能(Gzip 必须从头解压到目标位置)。所以 Gzip 文件无法拆分——整个文件作为一个 InputSplit,Map Task 数 = 1,无法并行。
BZip2 是个例外——它在压缩流里嵌入了 stream marker(同步点),从 marker 位置可以开始独立解压。这让 BZip2 文件可以按 marker 拆分,每个 Split 从 marker 开始。
Snappy / LZ4 / Zstd 都不可拆分——但这通常不构成问题,因为这些格式主要用于 MapReduce 中间数据(中间数据按 record 边界拆分,不需要 stream marker)或者用于 Sequence File / Parquet(这些格式内部自带 block 结构)。
Hadoop 3.x 起推荐 Zstd——它在压缩比和速度上都是当前最优折中(压缩比接近 Gzip,速度接近 Snappy)。Zstd 需要 Hadoop 3.0+ 和 native library 支持。
压缩在 Hadoop 各个环节的应用
压缩不只是"压缩文件"——Hadoop 在多个环节都应用压缩,每个环节的策略不同:
1 | |
生产集群的典型配置:
1 | |
这种组合让 Map 输出在 Shuffle 时用 Snappy 快压缩(CPU 敏感),最终输出用 Gzip 高压缩比(存储敏感)。BLOCK 类型让 Sequence File 按 block 压缩(不是按 record),更高效。
实验:观察压缩效果
提交一个 word count 作业,配置不同的 Map 输出压缩,观察 Shuffle 网络流量变化:
1 | |
在 ApplicationMaster Web UI 的 Counter 标签页观察三个作业的 Reduce shuffle bytes:
1 | |
Shuffle 字节减少的代价是 CPU 占用上升。对比作业总执行时间——通常 Snappy 的总时间最短(CPU 开销小,节省的 Shuffle 时间 > 压缩时间),Gzip 的总时间可能更长(CPU 开销大,超过节省的 Shuffle 时间)。
文件级压缩效果用 hadoop fs -du 观察:
1 | |
Snappy 让输出大小减少 84%。对 PB 级集群,这个节省可以折算成数百万美元的存储成本。
模式提炼
Hadoop 的序列化与压缩体现的设计模式:
1 | |
这个模式不只是 Hadoop。数据库系统同样支持多种存储格式(行存 / 列存)和多种压缩(字典 / RLE / Delta),让用户按查询模式选择。Kafka 的 message 格式(v1 / v2)也涉及类似权衡——压缩放在 record batch 级别,让批量压缩效率高。
数据湖格式(Parquet / ORC / Iceberg / Hudi)是这种模式的现代化版本——内置列式存储 + 多种压缩 + schema evolution,把序列化和压缩组合做成了文件格式的一部分。
工程迁移表
| Hadoop 概念 | Parquet | ORC | Kafka | Database |
|---|---|---|---|---|
| Writable(行存) | Record | Row | message v2 | row |
| Avro(schema 嵌入) | File footer | file footer | header schema | catalog |
| Sequence File(Hadoop 自带) | Parquet File | ORC File | log segment | tablespace |
| Snappy codec | Snappy | Snappy | Snappy | LZ4 / Snappy |
| 可拆分性 | row group | stripe | partition | partition |
| 压缩组合 | column-wise + codec | column-wise + codec | batch + codec | page-level |
注意 Parquet / ORC 这一列。这两个列式存储格式是 Hadoop 生态系统的现代化产物——它们把"列式存储 + 压缩 + schema 演化"做成了一等公民。Parquet 在 Spark 生态流行,ORC 在 Hive 生态流行,两者都是 Hadoop 文件格式(Sequence File、Avro)的进化版本。
常见误解
误解一:“Writable 就是 Java Serializable 的 Hadoop 版本”。Writable 与 Java Serializable 是完全不同的机制——Java Serializable 用反射自动序列化,Writable 让用户自己实现。Writable 更快更紧凑,但要求用户手写序列化代码。
误解二:“压缩总是好的”。压缩节省存储和网络,但消耗 CPU。如果作业是 CPU 密集型(大量计算),压缩反而拖慢——节省的 I/O 时间 < 压缩时间。生产经验:I/O 密集型作业(ETL)开压缩,CPU 密集型作业(机器学习推理)关闭压缩。
误解三:“Gzip 是最佳压缩格式”。Gzip 历史悠久但性能差——压缩比不如 Zstd,速度不如 Snappy。Hadoop 3.x 起推荐 Zstd 替代 Gzip。Gzip 仍然在使用是因为兼容性(老数据是 Gzip 压缩的),新部署应该用 Zstd。
误解四:“压缩文件无法 MapReduce 拆分”。只有 Gzip / Snappy / LZ4 / Zstd 等流式压缩不可拆分。BZip2 有 stream marker 可以拆分。Sequence File / Avro / Parquet / ORC 即使压缩也可以拆分(block 结构)。所以"压缩 + 可拆分"不矛盾,关键是选对格式。
误解五:“Avro 比 Protobuf 新所以更好”。Avro(2009)和 Protobuf(Google 2001 内部,2008 开源)都历史悠久。两者设计目标不同——Avro 为数据存储设计(schema 嵌入文件),Protobuf 为 RPC 设计(schema 编译进代码)。不能简单比较"哪个更好",要看场景。
练习
-
提交一个 MapReduce 作业,对比启用 Snappy 压缩和不压缩两种情况下 Shuffle 字节数和作业总时间。验证 Snappy 在你的集群上是节省时间还是增加时间。
-
写一个自定义 Writable 类(例如包含两个字段的 Pair),故意让 write 和 readFields 的字段顺序不一致,运行 MapReduce 观察反序列化错误的奇怪表现。然后修复顺序重新运行。
-
在
apache/hadoop源码里找到Writable.java、CompressionCodec.java、CodecPool.java、ZStandardCodec.java(hadoop-common-project 模块),观察序列化和压缩接口的核心定义。 -
思考题:如果让 Hadoop 把所有内部序列化从 Writable 切换到 Protobuf(包括 MapReduce 中间数据),会带来什么好处和坏处?为什么 Hadoop 团队没有做这种切换?
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 00-06 | HDFS 存储层 | 第一阶段(已完成) |
| 07-12 | YARN 资源管理层 | 第二阶段(已完成) |
| 13-15 | MapReduce 计算模型 | 第三阶段(已完成) |
| 16 | Hadoop RPC 协议栈:Protobuf、SASL 与 Connection Idle | 上一篇 |
| 17 | 序列化与压缩:Writable、Avro 与 Codec | 本篇 |
| 18 | Hadoop 安全:Kerberos、Delegation Token、Proxy User | 下一篇 |
| 19 | 监控与运维:Metrics V2、JMX、日志聚合 | |
| 20-22 | 演进、生态与对比 | 第五阶段,待开始 |
参考资料
- Doug Cutting. Avro: A New Data Serialization System.(Apache Avro 设计文档)
- Google Protocol Buffers 文档:https://protobuf.dev/overview/
- Apache Hadoop 官方文档:Compression and Codecs. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/CompressionCodec.html
- Apache Hadoop 官方文档:Hadoop Native Libraries. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/NativeLibraries.html
- Apache Hadoop 源码:
Writable.java、CompressionCodec.java、ZStandardCodec.java. https://github.com/apache/hadoop - Tom White. Hadoop: The Definitive Guide. O’Reilly, 4th Edition 2015. Chapter 5 “Hadoop I/O: Compression” 详细对比了各种压缩格式。
- Yann Collet. Zstandard Compression. Facebook Engineering Blog.(Zstd 设计与性能基准)
