在x86架构的MacBook上拿IntelliJ IDEA去编译Hadoop 2.6.0-cdh5.14.0,这个组合听起来就像是给十年前的项目做考古修复。但只要你的公司里还跑着CDH 5.14的集群,或者你接手了一个基于CDH源码做二次开发的模块,这事儿就绕不开。我为了在本地调试一个HDFS的定制功能,花了整整一个周末把这条编译链路彻底走通,过程中踩掉的坑几乎覆盖了所有能在macOS上遇到的经典问题。
先说结论:CDH 5.14的Hadoop虽然内核是Apache Hadoop 2.6.0,但它的源码结构、依赖坐标、构建脚本和原生库编译方式都做了不少改动,直接拿Apache版本的教程来套,十有八九会在中途报错。这篇内容就是记录我在x86 macOS(Intel芯片,10.14/10.15,带Xcode Command Line Tools)上用IDEA编译这套源码的全过程,顺便把那些报错信息一条条对出来讲清楚。
1. CDH 5.14的Hadoop 2.6源码:为什么值得在macOS上费力
1.1 它和Apache Hadoop 2.6.0的差异
CDH是Cloudera发行版的代号,5.14.x对应的是2017年前后的一批稳定版本,当中的Hadoop主版本就是2.6.0-cdh5.14.0。很多人以为Cloudera只是换个壳,实际不是。CDH在Apache Hadoop之上打了大量patch,包括HDFS的Sentry集成、HA故障切换的细化、配额和审计日志的改动,以及一堆bugfix。
这些patch不是注释级别的修改,而是直接改变了源码树的结构。你在IDEA里打开CDH仓库,会发现它比Apache版本多出不少子模块,比如hadoop-sentry、hadoop-fairscheduler-ext,甚至在hadoop-hdfs模块里也多了一些Cloudera自己加的类。如果你直接拿Apache Hadoop 2.6的源码包来编译,跑出来的东西和公司CDH集群上跑的二进制不一致,二开出来的代码很可能在线上行为完全不同。
所以,做CDH体系的二次开发,编译CDH自己的源码是第一步,不是可选步骤。
1.2 哪些场景下需要本地编译
我遇到的需求很典型:公司那套CDH 5.14集群上有个HDFS的NameNode内存监控逻辑是定制过的,代码在运维团队手里,但文档基本等于没有。我需要把整套源码拉下来,在本地跑一个伪分布式的NameNode和DataNode,打断点看路径,才能搞清楚它到底改了哪些行为。
这种场景下本地编译有几个硬性要求:
- 编译结果要和线上CDH版本一致,不能用Apache原版代替
- 要能在IDE里直接启动HDFS进程,而不是打包完丢到服务器上黑盒运行
- 需要把native library编出来,否则部分JNI调用会报warning,甚至某些加密代码路径跑不起来
如果你也是冲着这些目标来的,那这篇笔记应该能帮你省掉不少时间。
2. 环境准备:先对付JDK、Maven和protobuf三条拦路虎
2.1 JDK只能选8,原因在这里
CDH 5.14的Hadoop 2.6时代,官方推荐的是JDK 7和JDK 8,但我建议直接装JDK 8。原因有两个:
第一,JDK 7在Intel Mac上已经很难找到合适的macOS版本,且Oracle早就停止支持,没必要给自己找麻烦。
第二,JDK 8是这套源码的实测安全区间,编译时不会碰到模块化问题。如果你用JDK 9或更高版本,会立刻在编译期撞上javax.annotation和javax.xml.bind找不到的问题,因为JDK 9模块化之后,这些包不再默认包含在classpath里了。Hadoop 2.6的代码里不少类依赖它们,没有添加--add-modules java.xml.bind的旧版构建逻辑,基本过不去。
我的建议是安装一个干净的JDK 8,比如jdk1.8.0_291.jdk,然后把JAVA_HOME明确指过去:
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home export PATH=$JAVA_HOME/bin:$PATH注意x86 macOS的Intel Mac不需要额外处理Rosetta,但如果你用的是Apple Silicon Mac,还得给这整套工具链加一层x86转译,那就更折腾了。所以标题里强调x86架构是有实际意义的,Intel Mac至少不用处理arch匹配问题。
2.2 Maven版本不要贪心,3.3.9最稳
Hadoop 2.6的pom结构是在Maven 3.2/3.3时代写的。我一开始用的是Maven 3.8.6,结果一堆老插件直接翻车,比如maven-remote-resources-plugin报执行失败,maven-antrun-plugin的脚本在解析时也出现了古怪的格式错乱。
后来我换成了Maven 3.3.9,整个编译过程就顺畅多了。如果是新装的机器,直接用Homebrew安装指定版本:
brew install maven@3.3装完之后确认版本:
mvn -version只要输出里能看到Maven 3.3.9和正确的Java version: 1.8,就可以进入下一步。别小看这个版本对齐,我在这个环节浪费了一个下午,最后发现就是Maven太新惹的祸。
2.3 protobuf必须卡在2.5.0
这是整个编译过程中最不能妥协的一个依赖。Hadoop 2.6的RPC协议定义用的protobuf是2.5.0,HDFS的NameNodeRpcServer和DataNode之间的通信协议由一系列.proto文件生成Java代码。如果protoc编译器版本不是2.5.0,生成的代码会在运行时出现协议不匹配的异常。
CDH源码里的hadoop-common目录下,一堆.proto文件的语法是proto2写法,protobuf 2.5.0能正常解析。你要是装个3.x的protoc,虽然大部分proto2语法它也兼容,但某些生成类的签名和Hadoop源码里写死的接口不一致,编译期就会报错。
我安装protobuf 2.5.0的方式是下载官方源码包,在本地编译安装:
tar -zxvf protobuf-2.5.0.tar.gz cd protobuf-2.5.0 ./configure --prefix=/usr/local/protobuf-2.5.0 make -j4 make install编译完后,把/usr/local/protobuf-2.5.0/bin放到PATH的最前面,再用protoc --version确认输出为libprotoc 2.5.0。
在比较新的macOS系统上,protobuf 2.5.0的C++代码可能会因为编译器太新而出一些兼容性报错,我当时的处理方式是直接用CC=/usr/bin/clang、CXX=/usr/bin/clang++来configure,注意不要让它默认找Homebrew里的更新版GCC,老版本源码对新编译器反而更敏感。
2.4 其他系统级依赖
除了JDK、Maven、protobuf,native部分编译还需要cmake和一堆构建工具。我在x86 macOS上预先用Homebrew装好了这些:
brew install cmake autoconf automake libtool snappy brew install openssl zlib bzip2openssl尤其重要。如果你编译时开启了native库的OpenSSL支持,却找不到头文件,会直接报openssl/evp.h不存在。Homebrew安装的openssl是/usr/local/opt/openssl,Hadoop的configure脚本不一定会自动找到这个路径,后面如果有需要,我会讲怎么利用环境变量把这个路径传进去。
到这里,环境准备算是告一段落。我建议你先把每个依赖的版本都确认一遍,然后再启动Maven构建。磨刀不误砍柴工,在这个项目上尤其适用。
3. Maven构建:我的关键参数选择与执行顺序
3.1 官方打包命令逐一拆解
CDH源码根目录下的README会告诉你用Maven构建,但给的命令很笼统。实战下来,我用的命令是这样的:
mvn clean install -DskipTests \ -Dmaven.javadoc.skip=true \ -Dfindbugs.skip=true \ -Dtar=false \ -Pdist,native逐个参数看它做了什么:
-DskipTests:跳过测试执行,但保留测试代码的编译。这一步很重要,因为Hadoop的测试用例量很大,很多测试需要启动本地进程,在macOS上经常因为端口占用或系统权限失败。-Dmaven.javadoc.skip=true:跳过javadoc生成。这个纯粹是为了省时间,一个模块的javadoc就要跑好几分钟。-Dfindbugs.skip=true:跳过FindBugs静态检查。CDH的老pom里findbugs插件版本比较旧,在JDK 8的新字节码格式下经常误报,而且跑起来极慢。-Dtar=false:不生成tar.gz发行包。这个参数能让构建跳过不少打包后处理,缩短时间。-Pdist,native:激活两个profile。dist会生成一个完整可运行的Hadoop发行目录;native会触发JNI和native代码库的编译。
如果你的目的只是把Java源码编译好然后导入IDEA,其实可以暂时不加-Pnative,后面单独编native库。我第一次就直接上了全量native,结果一堆依赖问题全涌上来,反而不利于定位。
所以我的建议是分两步走:
第一步,先纯Java编译:
mvn install -DskipTests -Dmaven.javadoc.skip=true -Dfindbugs.skip=true -Dtar=false这一步能通过,说明Java层面的所有源码、资源依赖都没问题。第二步再考虑native。
3.2 子模块依赖顺序问题
CDH 5.14的源码是一个超多模块的Maven聚合工程,模块依赖呈现明显的分层。你从根目录执行mvn install,Maven会计算依赖拓朴并依次构建,但有些时候如果根pom的仓库配置不全,某个中间子模块会拉不到依赖,然后一直失败。
我在构建过程中就遇到过hadoop-project-dist模块报错,问题不是代码,而是它依赖的hadoop-client和hadoop-minicluster中的某些CDH版jar在Maven中央仓库根本没有,只有Cloudera的仓库里有。如果你的Maven日志里出现Could not find artifact org.apache.hadoop:hadoop-hdfs:jar:2.6.0-cdh5.14.0之类的错误,多半就是仓库列表不够。
解决方法是把Cloudera仓库加进settings.xml的profile里:
<profile> <id>cloudera</id> <repositories> <repository> <id>cloudera-repos</id> <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> </profile>然后在activeProfiles里激活它。这样Maven在解析依赖时,中央仓库找不到的CDH专属构件就会自动去Cloudera仓库拉取。
3.3 首次构建的耗时和内存控制
Hadoop这种规模的聚合工程,首次构建会下载几百个jar,加上Java源码编译,整体耗时在30到60分钟之间,具体看机器性能。x86 Mac上我实测大概是40分钟出头。如果中途某个子模块编译失败,修改后重新执行mvn install时,由于前面的模块已经安装了,第二次的速度会快不少。
另外要特别注意Maven JVM内存。Hadoop编译时会启动多个插件进程,内存不足会出现java.lang.OutOfMemoryError: PermGen space。虽然JDK 8已经没有PermGen,但部分老插件还是会申请较大的堆内存。我习惯了在编译前设置:
export MAVEN_OPTS="-Xmx4096m -XX:MaxPermSize=512m"这个设置对后面导入IDEA也有帮助,IDEA里的Maven importer同样会用到这类内存参数。
构建成功以后,你会看到一堆BUILD SUCCESS,尤其是最后几个核心子模块的构建结果,这就说明命令行层面的编译已经打通了。接下来才轮到IDEA出场。
4. 从命令行到IntelliJ IDEA:导入与运行配置
4.1 导入前的仓库预热
很多人喜欢把源码直接拖进IDEA,让IDEA自己去解析Maven,然后卡在indexing和依赖下载上半天。我试过,CDH这个工程模块实在是多,IDEA的首次导入会扫描所有pom并建立索引,不预热的话体验很差。
更顺滑的做法是:先用命令行执行一次完整的mvn install(跳过测试即可),把本地Maven仓库填满。这样IDEA导入时,所有依赖都能从本地仓库直接读取,解析速度会快很多,也基本不会出现Cannot resolve symbol的红字。
如果你遇到IDEA里的Maven窗口显示某些dependency还是红的,点一下Reload All Maven Projects,然后检查IDEA使用的settings.xml路径是否和命令行一致。这一步很关键,IDEA默认有自己的一套User settings路径,如果它和命令行用的不是同一个,就会觉得依赖是乱的。
4.2 配置Project SDK和Maven Runner
在IDEA的File -> Project Structure -> Project里,把Project SDK设为1.8,Language Level也设为8。然后进入Settings -> Build, Execution, Deployment -> Build Tools -> Maven:
- Maven home path指向Maven 3.3.9的安装目录
- User settings file指向你配置了Cloudera仓库的
settings.xml - Local repository指向命令行使用的本地仓库
这些配置对齐以后,Maven窗口里的子模块列表会清晰很多。注意导入的时候IDEA可能会问你是否信任这个Maven project,选择信任,否则某些插件执行会被拦截。
这里还有个容易忽略的地方:IDEA内置的编译器和Maven编译器是两个独立系统。如果你直接在IDEA的工具栏点Build,它用的可能是IDEA自己的编译器,报错信息经常和Maven构建不一样。所以我个人的习惯是,先用Maven窗口执行clean和install,确认源码本身没问题,然后再用IDEA的Build来增量编译做代码跳转和调试。
4.3 调整IDEA的内存和索引设置
CDH 5.14工程包含的子模块数量很多,再加上自带的三方依赖,IDEA首次打开时索引任务会很重。x86 Mac上如果内存只有8G,建议在Help -> Change Memory Settings里把IDEA的堆内存调到至少2G,否则卡到键盘冒烟。
导入完成后,你可以试着搜索一个核心类,比如NameNode,如果能正常跳转到hadoop-hdfs模块的源码,说明整个工程的索引已经建立成功。如果跳转失败,可能是这个模块没有被IDEA正确识别为源码目录,右键对应根目录,在Mark Directory as里选择Sources Root。
这些准备工作做完以后,IDEA里的代码浏览和搜索就已经可用了。但真正要跑起HDFS进程,还需要额外的运行时配置,我放到第6章再讲。
5. 遍历macOS特有坑:从glibtoolize到protoc版本
5.1 glibtoolize和libtoolize的软链问题
这套源码的native部分在macOS上编译时,第一个经典报错来自autotools。CDH的configure.ac脚本会优先找libtoolize命令,但Linux发行版上的GNU libtool提供的是这个名字,macOS上Homebrew安装的libtool提供的是glibtoolize和glibtool,因为系统里还有一个Apple版本的libtool(用来做Mach-O库管理的),两者冲突了。
于是configure阶段很容易出现这样的错误:
checking for libtoolize... no checking for glibtoolize... glibtoolize然后后续的Makefile生成过程会因为libtool宏问题直接失败。解决方式很直接,做个软链:
brew install libtool ln -s /usr/local/bin/glibtoolize /usr/local/bin/libtoolize ln -s /usr/local/bin/glibtool /usr/local/bin/libtool做完软链后,重新执行Maven构建,这一步就能越过去。这个坑在Linux上的教程里基本见不到,是macOS专属。
5.2 protoc版本被覆盖
Hadoop的native编译过程中,会调用protoc来生成一些协议代码。如果你系统里有多个protobuf版本,比如为了其他项目装过protoc 3.x,而且它的路径在PATH中排在2.5.0之前,那configure脚本会检测到3.x版本,并报版本不兼容的错误。
报错一般长这样:
checking protoc version... 3.1.0 configure: error: cannot find compatible protoc遇到这个问题时,不要急着改代码,先把PATH理顺。我直接把protoc 2.5.0的bin目录放在PATH最前面,并在构建命令前再次验证:
which protoc protoc --version确保输出的是/usr/local/protobuf-2.5.0/bin/protoc和libprotoc 2.5.0,然后再跑Maven构建。
这个问题的深层原因是CDH源码的许多.proto文件生成的Java代码是强绑定protoc 2.5.0的,版本一旦漂移,生成出来的源码接口会变化,后续javac编译时会大量报错。你如果看到一堆cannot find symbol错误,先别急,回头检查protoc版本比逐行改代码靠谱得多。
5.3 OpenSSL和snappy头文件路径
当启用-Pnative构建时,configure脚本会探测OpenSSL、snappy、zlib、bzip2等依赖库。macOS系统自带的/usr/include/openssl在早期版本还存在,但较新的Xcode Command Line Tools已经把它移除了。如果你用的是12或13代的macOS,建议在编译前把Homebrew的openssl路径导出到环境变量:
export OPENSSL_ROOT_DIR=/usr/local/opt/openssl export OPENSSL_INCLUDE_DIR=/usr/local/opt/openssl/include export OPENSSL_LIBRARY_DIR=/usr/local/opt/openssl/lib export CPPFLAGS="-I/usr/local/opt/openssl/include -I/usr/local/include" export LDFLAGS="-L/usr/local/opt/openssl/lib -L/usr/local/lib"snappy的库路径同理。Hadoop的configure脚本在macOS上经常找不到snappy.h和libsnappy.dylib,导出CPPFLAGS和LDFLAGS是最省事的做法。如果你只是做Java层二开,可以暂时不编进native依赖,把-Pnative去掉,然后在运行时使用-Djava.library.path指向不存在的目录,Hadoop会退回到纯Java模式,虽然会打warning,但大部分HDFS测试功能可以跑。
5.4 老插件和Maven 3.8+的新仇旧账
我必须强调一下这个坑。CDH 5.14默认pom里的maven-antrun-plugin版本很老,旧插件在用模板引擎和资源过滤时,对新版Maven的API兼容性非常差。如果你坚持用Maven 3.8+,可能会遇到这样的错误:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-antrun-plugin:1.7:run此时你有两条路:
一条是像我一样,把Maven降级到3.3.9,用CDH时代的工具链去编译CDH时代的代码。这条路的成本最低,效果最快。
另一条是在父pom里覆盖插件版本,比如把maven-antrun-plugin升到1.8或更高,但风险是升级后的插件行为和旧脚本的预期不完全一致,可能引入新的问题。我建议除非你对Maven插件机制十分熟悉,否则还是降级Maven版本更稳。
这个问题的本质是:Hadoop 2.6年代还没适配Maven 3.6之后引入的一些插件执行细节。你非要让老车跑新路,也不是完全不行,但前提是你愿意处理一长串连锁反应。
6. 编译产物检查与本地伪分布式调试
6.1 编译完成后应该出现哪些目录
当mvn install全量通过后,最好先检查一下产物。在hadoop-dist/target下,你会看到一个类似hadoop-2.6.0-cdh5.14.0的目录,这是-Pdist生成的可运行发行目录。
里面包含:
bin/:hdfs、yarn、mapred等启动脚本etc/hadoop/:默认配置模板lib/:Java依赖jarlib/native/:本地库目录,如果native编译成功,这里会有libhadoop.dylib,而不是Linux下的libhadoop.so
如果你发现自己电脑上生成的是libhadoop.dylib,别觉得奇怪,macOS的动态库后缀就是dylib。IDEA启动NameNode时,-Djava.library.path指向这个目录就行。
6.2 在IDEA里启动NameNode和DataNode
本地调试HDFS最常用的办法是启动两个Java进程:NameNode和DataNode。在IDEA里,我用Application类型的Run Configuration来跑,具体配置如下。
NameNode:
- Main class:
org.apache.hadoop.hdfs.server.namenode.NameNode - VM options:
-Djava.library.path=/你的路径/hadoop-dist/target/hadoop-2.6.0-cdh5.14.0/lib/native - Program arguments: 第一次运行时先用
-format,之后用默认参数启动即可
DataNode:
- Main class:
org.apache.hadoop.hdfs.server.datanode.DataNode - VM options: 同上
启动前还需要设置环境变量HADOOP_CONF_DIR或准备好一份core-site.xml。最简单的做法是在IDEA的Run Configuration里加一个环境变量:
HADOOP_CONF_DIR=/你的路径/hadoop-dist/target/hadoop-2.6.0-cdh5.14.0/etc/hadoop用这个发行目录自带的etc/hadoop配置,虽然默认配置比较粗糙,但足以让NameNode和DataNode在本地跑起来。你要调试的二开代码如果涉及某个特定配置项,再单独往配置文件里加property。
6.3 native library加载问题
本地调试时最不显眼但又最常出问题的是native库加载。Hadoop启动日志如果出现:
WARN util.NativeCodeLoader: Unable to load native-hadoop library处理方式是用显式的-Djava.library.path指定到lib/native目录,然后再次启动。如果还不行,就检查这个目录下是否真的生成了libhadoop.dylib,有时候-Pnative没启用或者native构建失败,这个目录是空的。
从IDEA启动时,VM options里的路径不要有中文或空格,否则JNI的加载逻辑会非常脆弱。我当时把整个工程放在/Users/me/work/cdh-hadoop下,路径干干净净,就是不想在这种地方踩低级坑。
6.4 调试时的常见断点位置
如果你和我一样,目的是分析NameNode的内存或元数据管理逻辑,推荐关注这几个类的断点:
org.apache.hadoop.hdfs.server.namenode.FSNamesystem,几乎所有元数据操作的主战场org.apache.hadoop.hdfs.server.blockmanagement.BlockManager,Block的状态机核心org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer,RPC请求入口
打断点时要注意,NameNode进程是一个持续运行的daemon,断点打在启动路径上会在格式化阶段就停住。建议先以-format参数跑一次,格式化完成后再正常启动,否则断点命中时机不对,会让你以为代码有问题。
把断点打在NameNodeRpcServer的某个RPC方法上,然后用HDFS的shell命令或一个简单的Java客户端去触发mkdir、写文件之类的操作,就能观察完整的调用链。这一套流程在IDEA里跑通以后,本地二开的效率比写代码+丢服务器验证高出一个数量级。
最后说一点实在的:整个过程最大的感受是,不要把CDH的源码当成Apache Hadoop来编译。版本对齐、仓库配置、native工具的软链处理,哪一步都不能用“差不多”的心态去糊弄。我在x86 macOS上用IDEA跑通这套CDH 5.14编译,前后踩掉的坑如果算成时间,够我写完十个业务模块了。但一旦这条链路稳定下来,后续每天改代码、跑进程、断点调试都变得非常顺手。如果你也是被CDH钉在老版本上的开发,希望这篇记录能帮你少走一圈弯路。