1. 为什么要在 Mac 上折腾 Hadoop 与 Snappy 的集成
大数据生态里,压缩算法的选择从来不是一件小事。存储成本、网络传输开销、CPU 占用这三者之间的平衡,直接决定了集群的吞吐表现。Snappy 作为 Google 开源的压缩库,主打的就是一个"快"字——压缩速度极快,解压速度更快,虽然压缩比不如 Gzip 和 Bzip2,但在 Hadoop 这种需要频繁读写中间数据的场景下,Snappy 几乎是默认的优选方案。尤其是 HDFS 上的 SequenceFile、MapReduce 的中间输出、Hive 的 ORC 文件,只要开启压缩,Snappy 基本都是第一选择。
但问题来了:Hadoop 原生并不自带 Snappy 的本地库支持。它依赖的是hadoop-snappy这个桥接库,底层通过 JNI 调用系统的libsnappy.so。在 Linux 环境下,装个libsnappy-dev再编译一下就能搞定,但在 Mac 上,这件事的复杂度直接翻倍。原因有三:第一,Mac 的动态库后缀是.dylib而不是.so,Hadoop 的加载逻辑默认找.so;第二,Mac 的 Homebrew 安装路径和 Linux 的/usr/lib完全不同,涉及JAVA_LIBRARY_PATH和java.library.path的配置;第三,Hadoop 官方提供的hadoop-snappy预编译包几乎都是针对 Linux 的,Mac 上必须自己从源码编译。
我这次的目标很明确:在一台 M 系列芯片的 Mac 上,搭建一个单机伪分布式 Hadoop 环境,并且让 Snappy 压缩真正跑通——不是那种"配置写了但实际没生效"的假成功,而是通过CompressionCodec工厂能正确加载SnappyCodec,并且实际写入和读取压缩文件都正常。整个过程踩了不少坑,有些是 Mac 特有的,有些是 Hadoop 版本兼容性带来的,还有一些是环境变量顺序导致的玄学问题。下面我把整个流程拆开,从依赖组件的准备到最终验证,一步步说清楚。
注意:本文所有操作基于开源社区版本,不涉及任何特定商业发行版。不同 Hadoop 版本(2.x 与 3.x)在 Snappy 集成上略有差异,文中会分别说明。
2. 依赖组件的完整清单与获取方式
2.1 核心组件一览
在开始动手之前,先把需要的东西列清楚。很多人失败的原因就是缺了某个组件,或者版本对不上。下面这张表是我实测下来必须准备的组件清单:
| 组件名称 | 作用 | 获取方式 | 版本建议 |
|---|---|---|---|
| Snappy 源码 | 编译本地库 | 官方源码包 | 1.1.9 或以上 |
| hadoop-snappy 源码 | JNI 桥接库 | 开源仓库 | 与 Hadoop 版本匹配 |
| protobuf | hadoop-snappy 编译依赖 | 包管理器安装 | 2.5.0(关键) |
| Hadoop 二进制包 | 运行环境 | 官方归档 | 3.3.x 或 2.10.x |
| JDK | Java 运行环境 | 包管理器安装 | 8 或 11 |
| Maven | 编译 hadoop-snappy | 包管理器安装 | 3.6+ |
| CMake / Autotools | 编译 Snappy | 包管理器安装 | 最新稳定版 |
这里要特别强调protobuf 的版本。hadoop-snappy 的编译依赖 protobuf 2.5.0,如果你用 Homebrew 直接装protobuf,大概率会装上 3.x 甚至 4.x 版本,编译时直接报错。这是第一个大坑,后面会详细说怎么处理。
2.2 Snappy 本地库的编译
Snappy 的编译本身不复杂,但 Mac 上要注意架构问题。如果你用的是 M 系列芯片,默认编译出来的是 arm64 架构的 dylib,而你的 Hadoop 如果跑在 Rosetta 转译的 x86_64 JDK 下,就会遇到架构不匹配的问题。所以第一步要确认你的 JDK 架构:
java -XshowSettings:properties -version 2>&1 | grep os.arch如果输出aarch64,说明是原生 arm64 JDK;如果输出x86_64,说明是转译模式。两者对应的 Snappy 编译参数不同。
编译 Snappy 的步骤:
# 下载源码并解压 tar -xzf snappy-1.1.9.tar.gz cd snappy-1.1.9 # 创建构建目录 mkdir build && cd build # 如果是 arm64 JDK,直接编译 cmake -DCMAKE_BUILD_TYPE=Release .. make -j8 # 如果是 x86_64 JDK,需要指定架构 cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 .. make -j8编译完成后,你会得到libsnappy.1.dylib和libsnappy.dylib。把它们复制到一个固定目录,比如/usr/local/lib,然后确认动态库能被找到:
sudo cp libsnappy*.dylib /usr/local/lib/ sudo ldconfig # Mac 上其实没有 ldconfig,用下面这个替代 export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH提示:Mac 上没有
ldconfig,动态库搜索路径靠DYLD_LIBRARY_PATH环境变量控制。这个变量在后续 Hadoop 启动时也必须存在,否则 JNI 加载会失败。
2.3 hadoop-snappy 桥接库的编译
hadoop-snappy 是连接 Java 和本地 Snappy 库的桥梁。它的源码在开源仓库里可以找到,但编译过程比 Snappy 本身更容易出问题。
首先确认 protobuf 版本:
protoc --version如果输出不是libprotoc 2.5.0,就需要手动安装 2.5.0 版本。Homebrew 默认不提供旧版本,所以需要从源码编译:
tar -xzf protobuf-2.5.0.tar.gz cd protobuf-2.5.0 ./configure --prefix=/usr/local/protobuf-2.5.0 make -j8 sudo make install然后设置环境变量指向这个版本:
export PROTOC=/usr/local/protobuf-2.5.0/bin/protoc export PATH=/usr/local/protobuf-2.5.0/bin:$PATH接下来编译 hadoop-snappy:
cd hadoop-snappy mvn package -DskipTests -Dprotobuf.version=2.5.0编译成功后,在target目录下会生成hadoop-snappy-0.0.1-SNAPSHOT.jar和libhadoop-snappy.dylib。这两个文件是后续集成的核心。
注意:如果编译过程中报错
cannot find -lsnappy,说明编译器找不到 Snappy 库。需要把/usr/local/lib加到链接器搜索路径,或者用-Djava.library.path显式指定。
3. Hadoop 端的配置与文件部署
3.1 文件放置位置
编译出来的产物需要放到 Hadoop 能识别的目录下。具体来说:
hadoop-snappy-0.0.1-SNAPSHOT.jar放到$HADOOP_HOME/share/hadoop/common/lib/目录libhadoop-snappy.dylib和libsnappy.1.dylib放到$HADOOP_HOME/lib/native/目录
为什么放这两个位置?因为 Hadoop 启动时会自动扫描share/hadoop/common/lib/下的所有 jar 包加入 classpath,而lib/native/是java.library.path的默认搜索路径之一。这样配置的好处是不需要额外修改HADOOP_CLASSPATH和JAVA_LIBRARY_PATH,减少环境变量冲突的风险。
但 Mac 上有个特殊情况:Hadoop 的lib/native/目录默认可能不存在,需要手动创建:
mkdir -p $HADOOP_HOME/lib/native cp libhadoop-snappy.dylib $HADOOP_HOME/lib/native/ cp libsnappy.1.dylib $HADOOP_HOME/lib/native/3.2 core-site.xml 的关键配置
在core-site.xml中需要配置压缩编解码器:
<property> <name>io.compression.codecs</name> <value>org.apache.hadoop.io.compress.GzipCodec, org.apache.hadoop.io.compress.DefaultCodec, org.apache.hadoop.io.compress.SnappyCodec</value> </property>这个配置的作用是告诉 Hadoop 有哪些编解码器可用。如果不加SnappyCodec,即使 jar 包和 dylib 都到位了,Hadoop 也不会去加载它。
另外,如果你希望 MapReduce 中间输出也使用 Snappy 压缩,需要在mapred-site.xml中配置:
<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>3.3 环境变量的正确设置顺序
这是 Mac 上最容易出问题的地方。Hadoop 启动脚本hadoop-env.sh中需要设置几个关键变量:
export JAVA_HOME=$(/usr/libexec/java_home -v 11) export HADOOP_HOME=/path/to/hadoop export DYLD_LIBRARY_PATH=$HADOOP_HOME/lib/native:/usr/local/lib:$DYLD_LIBRARY_PATH export JAVA_LIBRARY_PATH=$HADOOP_HOME/lib/native顺序很重要:DYLD_LIBRARY_PATH必须包含lib/native和/usr/local/lib,前者是 hadoop-snappy 的 dylib,后者是 Snappy 本身的 dylib。如果顺序反了,可能会出现"找到了 hadoop-snappy 但找不到 snappy"的情况。
提示:在 Mac 的较新系统版本中,
DYLD_LIBRARY_PATH可能会被系统安全机制忽略。如果遇到这种情况,可以改用-Djava.library.path参数在启动命令中显式指定。
4. 验证 Snappy 是否真正生效
4.1 用 CompressionCodecFactory 做快速检测
配置完成后,不要急着跑 MapReduce 任务,先用一个简单的 Java 程序验证SnappyCodec能否被正确加载:
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.io.compress.CompressionCodec; import org.apache.hadoop.io.compress.CompressionCodecFactory; public class SnappyCheck { public static void main(String[] args) { Configuration conf = new Configuration(); CompressionCodecFactory factory = new CompressionCodecFactory(conf); CompressionCodec codec = factory.getCodecByName("SnappyCodec"); if (codec != null) { System.out.println("SnappyCodec loaded: " + codec.getClass().getName()); } else { System.out.println("SnappyCodec NOT found!"); } } }编译并运行:
javac -cp $(hadoop classpath) SnappyCheck.java java -cp $(hadoop classpath):. SnappyCheck如果输出SnappyCodec loaded,说明基础加载没问题。但这还不够,因为getCodecByName只是检查类是否存在,并不代表本地库能真正调用。
4.2 实际写入和读取压缩文件
更可靠的验证方式是实际创建一个 Snappy 压缩文件并读取:
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.io.compress.CompressionCodec; import org.apache.hadoop.io.compress.CompressionCodecFactory; import org.apache.hadoop.io.compress.CompressionOutputStream; public class SnappyWriteRead { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(conf); Path path = new Path("/tmp/test.snappy"); CompressionCodecFactory factory = new CompressionCodecFactory(conf); CompressionCodec codec = factory.getCodecByName("SnappyCodec"); CompressionOutputStream out = codec.createOutputStream(fs.create(path)); out.write("hello snappy".getBytes()); out.close(); System.out.println("Write OK, file size: " + fs.getFileStatus(path).getLen()); // 读取验证 CompressionInputStream in = codec.createInputStream(fs.open(path)); byte[] buf = new byte[1024]; int len = in.read(buf); System.out.println("Read: " + new String(buf, 0, len)); in.close(); } }如果这段代码能正常输出写入和读取的内容,说明 Snappy 压缩链路完全打通了。
4.3 常见失败信号与含义
| 错误信息 | 原因 | 解决方向 |
|---|---|---|
ClassNotFoundException: SnappyCodec | jar 包未放入 classpath | 检查share/hadoop/common/lib/ |
UnsatisfiedLinkError: no hadoop-snappy in java.library.path | dylib 路径未配置 | 检查JAVA_LIBRARY_PATH |
UnsatisfiedLinkError: no snappy in java.library.path | Snappy 本地库缺失 | 检查/usr/local/lib和DYLD_LIBRARY_PATH |
UnsatisfiedLinkError: wrong architecture | 架构不匹配 | 确认 JDK 和 dylib 架构一致 |
NullPointerExceptionin codec | 编解码器未注册 | 检查io.compression.codecs配置 |
5. Mac 特有的坑与排查过程
5.1 SIP 对 DYLD_LIBRARY_PATH 的干扰
Mac 的系统完整性保护(SIP)会限制DYLD_LIBRARY_PATH在某些进程中的传递。具体表现是:你在终端里echo $DYLD_LIBRARY_PATH能看到正确的值,但 Hadoop 启动后 JVM 却找不到 dylib。这是因为 SIP 会清理掉受保护进程的环境变量。
排查方法:在 Java 代码中打印System.getenv("DYLD_LIBRARY_PATH")和System.getProperty("java.library.path"),对比终端中的值。如果发现不一致,就是 SIP 在作怪。
解决方案有两个:一是把 dylib 放到/usr/local/lib这种系统默认搜索路径下,不依赖环境变量;二是在 Hadoop 启动脚本中通过-Djava.library.path显式指定,这个参数不受 SIP 影响。
5.2 架构不匹配的隐蔽表现
M 系列芯片的 Mac 上,如果你用 Homebrew 安装的 JDK(通常是 arm64),但 Hadoop 官方二进制包里的某些本地库是 x86_64 的,就会出现混合架构的情况。表现是:Snappy 本身能加载,但一调用压缩就崩溃,报SIGSEGV或者EXC_BAD_ACCESS。
排查方法:用file命令检查所有相关二进制文件的架构:
file $(which java) file $HADOOP_HOME/lib/native/libhadoop-snappy.dylib file /usr/local/lib/libsnappy.1.dylib如果输出中有的显示arm64,有的显示x86_64,就需要统一。最彻底的方式是全部用 arm64 重新编译,或者全部用 x86_64 并在 Rosetta 下运行。
5.3 protobuf 版本冲突的连锁反应
前面提到 hadoop-snappy 编译需要 protobuf 2.5.0,但如果你系统里同时存在多个版本的 protobuf,编译时可能会链接到错误的版本。表现是编译能过,但运行时protoc生成的代码和库不匹配,报Protocol message tag had invalid wire type之类的错误。
解决方式是编译前显式指定PROTOC环境变量,并且在 Maven 命令中传入-Dprotobuf.version=2.5.0。编译完成后,用otool -L检查生成的 dylib 链接的是哪个版本的 protobuf:
otool -L libhadoop-snappy.dylib | grep protobuf如果链接的不是 2.5.0,说明编译环境有问题,需要清理后重新编译。
5.4 Hadoop 版本差异带来的配置变化
Hadoop 2.x 和 3.x 在 Snappy 集成上有一些细微差别。2.x 时代,hadoop-snappy是作为独立项目维护的,需要手动编译;3.x 之后,部分 Snappy 支持已经内置到hadoop-common中,但仍然需要本地库。具体表现是:3.x 中SnappyCodec类可能已经存在,但调用时仍然报UnsatisfiedLinkError,因为本地库没配好。
另外,3.x 的io.compression.codecs默认值已经包含了一些编解码器,添加SnappyCodec时要注意不要覆盖默认值,而是追加。否则可能导致其他压缩格式失效。
6. 实操心得与长期维护建议
6.1 把编译产物做成可复用的包
每次换机器都重新编译一遍太浪费时间。我的做法是把编译好的hadoop-snappyjar 和 dylib 打包成一个压缩包,附上一个安装脚本。脚本自动检测 JDK 架构,把文件复制到正确位置,并输出需要添加的环境变量。这样在新机器上部署只需要跑一个脚本,几分钟就能搞定。
脚本的核心逻辑:
#!/bin/bash HADOOP_HOME=${HADOOP_HOME:-/usr/local/hadoop} ARCH=$(java -XshowSettings:properties -version 2>&1 | grep os.arch | awk '{print $3}') if [ "$ARCH" = "aarch64" ]; then cp libhadoop-snappy-arm64.dylib $HADOOP_HOME/lib/native/libhadoop-snappy.dylib else cp libhadoop-snappy-x86_64.dylib $HADOOP_HOME/lib/native/libhadoop-snappy.dylib fi cp hadoop-snappy.jar $HADOOP_HOME/share/hadoop/common/lib/ echo "export DYLD_LIBRARY_PATH=$HADOOP_HOME/lib/native:/usr/local/lib:\$DYLD_LIBRARY_PATH" >> ~/.bash_profile6.2 版本升级时的注意事项
Hadoop 升级时,hadoop-snappy的 jar 包通常需要重新编译,因为 JNI 接口可能发生变化。我的经验是:每次升级 Hadoop 大版本(比如从 3.2 到 3.3),都重新编译一次hadoop-snappy,不要直接复用旧包。虽然有时候旧包也能用,但一旦出问题,排查成本远高于重新编译的成本。
另外,Snappy 本身的版本升级相对安全,1.1.x 系列之间 ABI 兼容,直接替换 dylib 即可。但如果升级到 2.x,需要重新编译hadoop-snappy。
6.3 生产环境与本地环境的差异
本地 Mac 上跑通不代表生产环境没问题。生产环境通常是 Linux 集群,Snappy 的集成方式不同:Linux 上用libsnappy.so,路径通常是/usr/lib或/usr/local/lib,而且不需要处理 SIP 和架构问题。但有一个共同点:hadoop-snappy的 jar 包版本必须和 Hadoop 版本匹配。
我的建议是:本地开发环境用 Mac 跑通流程,理解原理;生产环境部署时,直接用 Linux 的包管理工具安装 Snappy 开发库,然后编译hadoop-snappy,不要试图把 Mac 上的编译产物直接拷过去。
6.4 性能调优的几点经验
Snappy 跑通之后,还可以做一些调优。比如调整io.compression.codec.snappy.buffersize参数,默认是 256KB,对于大块数据可以适当增大到 512KB 或 1MB,减少压缩调用次数。但也不要太大,否则会增加内存占用。
另外,Snappy 的压缩级别是固定的,不支持像 Gzip 那样调整级别。所以如果发现压缩比不理想,只能换算法,不能调参数。这也是 Snappy 的一个特点:用压缩比换速度,适合对延迟敏感的场景。
最后分享一个排查技巧:如果怀疑 Snappy 没有真正生效,可以在 MapReduce 任务运行时观察 Counter 中的HDFS_BYTES_WRITTEN和HDFS_BYTES_READ,对比压缩前后的数据量。如果压缩前后数据量几乎一样,说明压缩没有生效,需要回头检查配置。