你执行mvn clean package -DskipTests编译 Hadoop 源码,前面几百个模块都顺利过了,结果在hadoop-yarn-applications-catalog-webapp这里弹出一条触目惊心的错误:
[ERROR] Failed to run task: 'yarn install' failed. org.apache.commons.exec.ExecuteException: Process exited with an error: 1是不是有点懵?一个 Java 项目编译,为什么会去执行yarn install?这个报错看起来像是前端构建工具抛出来的,怎么和 Hadoop 扯上关系了?网上一搜,一堆人遇到同样的问题,但答案七零八落,有的说换 Node 版本,有的说改 registry,有的说直接跳过,到底该信谁?
我前前后后折腾过几轮,踩了不少坑,今天就把这个报错的完整链路、排查思路和最终解决方案一次性说清楚。这篇文章适合所有在编译 Hadoop 源码时遇到类似前端模块构建失败的人,包括用 Maven 构建其他同时包含前端和后端模块的 Java 项目的人。
1. 编译到一半卡住:这个报错究竟发生在前端还是后端
1.1 catalog webapp 模块是什么,为什么出现在 Hadoop 源码里
hadoop-yarn-applications-catalog-webapp是较新版本 Hadoop 源码里的一个子模块,全路径一般在hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp。它的作用是给 YARN 的 Catalog Service(资源目录服务)提供一个 Web 管理界面,用来登记、检索和管理数据集、表、函数这类元数据信息。
说白了,这是一个典型的“Java 后端 + 前端管理界面”的模块。后端负责 API 和元数据存取,前端负责用户在浏览器里的操作页面。Apache 项目里这种结构不少见,但问题就出在这里:前端代码不是 Java,不能直接交给 Maven 编译,Maven 需要借助一个插件来“客串”前端构建工具。
1.2 Maven 生命周期里为什么会跑 yarn install
这里的关键角色是frontend-maven-plugin(com.github.eirslett)。它的作用是在 Maven 构建过程中自动下载 Node.js、Yarn/npm,然后执行前端依赖安装和打包,最终把生成的静态资源丢进 JAR 包。Hadoop 的 webapp 模块就是通过它把前端构建流程嵌入到了 Maven 生命周期里。
看一段典型的配置就能明白:
<plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>1.12.0</version> <configuration> <nodeVersion>v16.20.0</nodeVersion> <yarnVersion>v1.22.19</yarnVersion> </configuration> <executions> <execution> <id>yarn install</id> <goals> <goal>yarn</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>install</arguments> </configuration> </execution> </executions> </plugin>这段配置绑定在generate-resources阶段,而generate-resources是每个 Maven 模块编译早期就会执行的阶段。所以你会看到整个构建已经跑了好一会儿了,到 webapp 模块时突然开始下载 Node、执行 yarn install,任何一个环节失败,整个 reactor 构建就中断了。
注意一个关键点:frontend-maven-plugin 会下载一个它自己指定的 Node.js 到项目目录里,而不是使用你系统里node -v能看到的那个 Node。这就引出了很多人的第一个疑惑——“我机器上明明装了 Node 18,怎么还报 Node 相关错误?”因为插件用的是 pom 里指定的 v16.20.0,这跟你系统里装的是什么完全没有关系。
2. 先别急着改配置,把报错信息拆开看
2.1 “Failed to run task”只是外层包装
大多数人看到Failed to run task: 'yarn install' failed这行就慌了,然后开始到处搜方案。但这行信息其实只是一个“包装”,它只说明 frontend-maven-plugin 在底层执行 yarn 命令时,子进程以非零状态退出了。org.apache.commons.exec.ExecuteException同样是 apache commons-exec 在执行外部进程时的标准异常包装。
打个比方:这就像你在终端里执行一个脚本,脚本内部出错了,但终端只告诉你“退出码是 1”,至于脚本里哪一步挂的,得看脚本自己打印的日志。yarn install 失败后,真正有用的信息在它自己打印的日志里,也就是上面的npm ERR!、yarn error这些行。
2.2 从日志里找出真正的错误码
要定位根因,必须往前翻日志,看 yarn 自己输出了什么。常见的输出形式有这几种:
npm ERR! code ETIMEDOUTnpm ERR! code EINTEGRITYyarn error There are no scenarios; must have at least onenpm ERR! unable to resolve dependency treeERR_PNPM_OUTDATED_LOCKFILE(如果你不小心用了 pnpm 去解析 yarn.lock)
每种错误码对应完全不同的处理方式,所以第一步要做的是定位真正的错误。
如果日志刷得太快,或者你执行的是整个 Hadoop 项目的全量编译,建议单独编译 webapp 这个模块,缩小排查范围:
mvn -pl hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp -am package -DskipTests -Dmaven.javadoc.skip=true-pl指定要构建的模块路径,-am表示同时构建它依赖的模块。这样就不用在几千行日志里捞错误信息了。
2.3 同一个报错,三种不同根因的识别方法
我见过很多人在同一个报错上反复尝试不同的方案:先清空 node_modules,再换 npm 镜像,又装个新版本 Node,结果问题依旧,最后才发现根因是网络根本访问不了默认源。识别根因其实可以用一个简单的对照表:
| 日志特征 | 根因方向 | 处理策略 |
|---|---|---|
npm ERR! code ETIMEDOUT、ECONNREFUSED、请求 registry 超时 | 网络问题,访问 npm 默认源失败 | 换 registry、配镜像、设置超时 |
npm ERR! Unable to resolve dependency tree、peer 依赖冲突 | 依赖版本冲突 | 检查 package.json 与锁文件一致性 |
yarn error There are no scenarios; must have at least one | yarn 版本与 yarn.lock 格式不匹配 | 用项目内 yarn 版本重新 install |
Error: certificate has expired | Node 版本太老,内置 CA 证书过期 | 升级 Node / 更新插件中的 nodeVersion |
磁盘明明有空间,但报No space left on device | 分区 inode 耗尽 | 清 inode,或把构建目录换到空间充足的分区 |
核心是先判断这是网络失败、版本不匹配、还是磁盘问题,再动手。直接乱试方案,经常会把问题越搞越复杂,甚至把原本好的依赖目录弄坏。
3. 逐项排查:网络、Node 工具链、yarn.lock 谁在捣乱
3.1 网络问题与 registry 超时
这是国内用户最常见的坑。Hadoop 的 webapp 模块默认会去https://registry.npmjs.org拉取前端依赖,这个源在不同网络环境下访问速度差异巨大,一旦超时,yarn install 就会整体失败,然后被 Maven 包装成这行报错。
判断其实很直观:如果你在命令行里单独执行 yarn install,同样卡住或者报网络错误,那基本就是网络问题。验证方法很简单,切到 webapp 项目目录,看node_modules是否已经被创建、里面是否只有少量包;或者直接看日志里有没有ETIMEDOUT。
解决方案也直接,给 yarn 换 registry。可以手动执行:
yarn config set registry https://registry.npmmirror.com也可以不落地到全局配置,只针对这次安装生效:
yarn install --registry=https://registry.npmmirror.com但如果问题出在 Maven 执行阶段,光在终端手动设置还不够。因为 frontend-maven-plugin 执行 yarn install 时,会按照 pom 里配置的 arguments 来跑,你在命令行里的手工配置不一定能生效。这时可以通过环境变量把这个 registry 传下去:
export NPM_CONFIG_REGISTRY=https://registry.npmmirror.com mvn clean package -DskipTestsNode 生态的工具链都会读取NPM_CONFIG_*开头的环境变量,这个方式比手动改.npmrc更隐蔽,也更适合在 CI 里统一配置。
3.2 frontend-maven-plugin 自带的 Node 版本和系统 Node 不一致
有人电脑上装了 Node 20,代码里也用得好好的,但 Hadoop 模块在构建时下载了 v16.20.0 的 Node,这时候如果前端依赖里有某个包需要更高版本 Node 才能安装或运行,就会出现你本地手工执行 yarn install 明明能过、Maven 一编译就挂的诡异现象。
遇到这种情况,先看 frontend-maven-plugin 下载的 Node 到底是多少版本。项目构建后,Node 会被解压在模块目录下的node文件夹里,可以直接查看:
cd hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp ./node/node -v ./node/yarn/dist/bin/yarn -v这就是“项目私有 Node”的位置。先确认它和你系统里用的是否一致,再做版本决策。
如果确实需要换 Node 版本,可以修改 pom 里 frontend-maven-plugin 配置的nodeVersion参数。但要注意:不要为了适配最新特性就把 Node 版本拉得太高。我试过一次把 nodeVersion 改成 v18,结果 webpack 构建报 OpenSSL 相关的错误,反而更麻烦。版本选择以项目原本配套的为准,除非你明确知道某个依赖需要更高版本。
3.3 yarn.lock 版本带来的兼容性陷阱
这个坑比较隐蔽。Hadoop 源码里是带了yarn.lock的,而这个锁文件是 Yarn 1.x 格式。如果你在本地用系统全局的 Yarn 3.x 或 4.x(Berry)手动跑过一次 install,锁文件可能被重写成新版格式,等你再用 Maven 时,frontend-maven-plugin 用的是 pom 里指定的 Yarn 1.22.x,读不懂新格式的锁文件,就会报很奇怪的错。
yarn.lock第一行会写__metadata:这种字段,这是 Berry 的标志;Yarn 1.x 的锁文件没有这个字段。看到__metadata,基本可以确定锁文件被动过了。
碰到这种情况,先看 webapp 项目目录下是否有被改过的 yarn.lock,如果有,恢复成源码仓库里的原始版本,或者直接删除后让 yarn 重新生成(注意用对的 yarn 版本)。我的建议是:在涉及 Hadoop 这种项目建设时,优先用项目内node/yarn/dist/bin/yarn,而不是系统全局的 yarn,从源头上避免锁文件格式错乱。
3.4 排查顺序建议
我踩了几次坑后,总结出一套排查顺序,推荐你照这个顺序来:
- 先看报错行里有没有
ETIMEDOUT、ECONNREFUSED,有则直接定位到网络问题。 - 看是不是磁盘或 inode 满了,执行
df -h和df -i确认。 - 在 webapp 目录下,用项目内自带的 yarn 手工执行 install,看能不能复现问题。
- 检查 yarn.lock 是否被其他版本工具改写过。
- 确认 pom 里指定的 nodeVersion、yarnVersion 与项目历史一致,不要轻易改动。
这套顺序能避免绝大多数“瞎试”的时间浪费。
4. 落地的三套方案:跳过、复用、换源
4.1 方案A:跳过前端构建,只编后端模块
如果你的目标只是编译 Hadoop 核心源码、做二次开发,或者压根用不到 catalog 模块的 Web UI,那么最省事的方案就是跳过前端构建。frontend-maven-plugin 支持通过系统属性跳过 Node 下载和 yarn 执行,取决于 Hadoop 这个模块是否开放了对应参数。
我测试下来,比较有效的是在 Maven 命令行加这两个参数:
mvn clean package -DskipTests -Dmaven.javadoc.skip=true -Dskip.installnodenpm=true -Dskip.yarn=true-Dskip.installnodenpm=true会跳过 frontend-maven-plugin 安装 Node 和 Yarn 的过程,-Dskip.yarn=true会跳过 yarn 相关的 goal 执行。但要注意,不是所有版本的插件都支持这两个参数,Hadoop 具体版本的 pom 里未必定义了这两个属性的透传。如果加了参数发现还是照常执行 yarn install,说明 pom 没有把系统属性绑定到插件配置上,这时候就得直接修改 pom:
找到 webapp 模块的 pom.xml 中 frontend-maven-plugin 的配置,把 yarn 相关的 execution 注释掉,例如:
<executions> <!-- <execution> <id>yarn install</id> <goals> <goal>yarn</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>install</arguments> </configuration> </execution> --> </executions>跳过后,后端 Java 代码能正常编译,但最终 JAR 包里可能缺少前端静态资源。如果你只是编译、做后端逻辑开发,问题不大;但如果你要交付完整发行包、启动后要用到 Web UI,就得老老实实把前端依赖装好。
4.2 方案B:手动构建前端依赖,让 Maven 直接复用
有的场景不能跳过前端构建,但又不想在 Maven 里一次次地卡住,这时候可以先手动把依赖装好,让 Maven 的 yarn install 阶段变成一个“空操作”。
操作方式:
cd hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp ./node/yarn/dist/bin/yarn install --registry=https://registry.npmmirror.com --network-timeout 600000注意两点:
- 一定用项目内
node/yarn/dist/bin/yarn,不要用系统全局的 yarn,原因前面讲过。 - 加上
--network-timeout 600000,把网络超时拉到十分钟,避免大依赖包下载到一半断开。
手动执行完成后,node_modules已经存在且完整。再次执行 Maven 构建时,yarn install 会检测到依赖已就绪,直接跳过实际安装过程,构建就能顺利通过。
如果 Maven 执行时还想进一步减少网络交互,可以在 pom 的 yarn 参数里加--prefer-offline,让 yarn 优先使用本地缓存。这是我实际使用中比较稳定的一个组合:
<configuration> <arguments>install --prefer-offline --network-timeout 600000</arguments> </configuration>4.3 方案C:给 frontend-maven-plugin 换下载源和 registry
方案 A 是绕路,方案 B 是手动搞定,但如果想根治问题,就得让插件本身的下载行为也走顺畅的源。frontend-maven-plugin 支持通过-Dfrontend.nodeDownloadRoot和-Dfrontend.yarnDownloadRoot覆盖 Node 和 Yarn 的下载地址,同时配合 npm registry 环境变量,可以在 Maven 层面一次性解决:
export NPM_CONFIG_REGISTRY=https://registry.npmmirror.com mvn clean package -DskipTests -Dmaven.javadoc.skip=true \ -Dfrontend.nodeDownloadRoot=https://npmmirror.com/mirrors/node/ \ -Dfrontend.yarnDownloadRoot=https://npmmirror.com/mirrors/yarn/注意npmmirror.com这里对应的 Node 镜像路径和 Yarn 镜像路径,前者是 Node.js 二进制包的镜像,后者是 Yarn 安装包的镜像。这样配置后,插件下载 Node 和 Yarn 时走的是国内镜像,速度会有明显提升,随后 yarn install 时也会因为NPM_CONFIG_REGISTRY被设置而走镜像仓库。
这套方案适合需要反复编译、经常清空构建目录、每次都要重新下载工具链的情况。而且这些参数都可以继续传给 CI 环境,不用改源码。
4.4 我最终用的组合
我的场景是:需要完整跑通 Hadoop 编译,并且需要 webapp 模块的静态资源能正常打进包里,所以不能Skip。最终采用的组合是:先手动执行一遍项目内 yarn install(配 registry 和超时),让node_modules完整落地,然后再用 Maven 全量构建,并给 Maven 传了NPM_CONFIG_REGISTRY环境变量作为兜底,同时给 frontend 插件配了 Node/Yarn 的国内下载源。
这样一套下来,构建稳定通过,后续再编译就不会在那个模块上反复翻车了。如果你的场景是快速验证核心模块,老老实实用方案 A 跳过就行,没必要在 web UI 资源上死磕。
5. 顺着这次构建摸到的其他坑
5.1 磁盘空间和 inode 不足是个隐形炸弹
很多人盯着 Node、yarn 排查了半天,最后发现No space left on device。但更迷惑的是,df -h明明显示还有空间,yarn 却报磁盘满了。这时候要看 inode:
df -h df -iyarn install 会解压大量小文件到node_modules,每个文件都要消耗一个 inode。如果某个分区的 inode 已经满了,即使可用空间还有几十 GB,写入照样失败,进程返回非零退出码,被 Maven 包装成Failed to run task。我遇到过/tmp目录 inode 满导致 Maven 构建临时文件写不进去的情况,清理完/tmp下旧的构建临时文件后,问题立刻消失。
如果系统分区比较紧张,建议把 Maven 本地仓库和构建临时目录放到空间充足的分区:
mvn clean package -Dmaven.repo.local=/data/m2 -Djava.io.tmpdir=/data/tmp5.2 JDK 版本、Maven 版本与 protoc 的要求
webapp 模块编译报错只是整个 Hadoop 构建中的一站。Hadoop 是个庞然大物,不同分支对构建工具链要求非常严格。很多人一上来就遇到编译问题,其实根本不是 webapp 相关,而是环境版本不对。
根目录下的BUILDING.txt把这一切都写清楚了。常见的要求包括:
- JDK 版本:不同分支要求不同,有的要求 JDK 8,有的要求 JDK 11 或 17,JDK 版本和 Maven 插件不匹配时会出现各种奇怪的编译失败。
- Maven 版本:过旧的 Maven 可能无法解析某些插件参数,建议直接用项目推荐的稳定版本。
- protoc(Protocol Buffers 编译器)版本:Hadoop 源码编译依赖特定版本的 protoc,版本不匹配会在编译 protobuf 相关的模块时报错。
我建议在编译前先看BUILDING.txt,并且按照官方推荐安装好对应版本的工具链。我见过不少人卡在 webapp 报错上,但真实问题是本机 JDK 版本过高,前面的模块侥幸编译通过,到 webapp 这种涉及更多插件协同的模块时,底层问题才暴露出来。环境本身一致了,很多前端构建的报错也会跟着消失。
5.3 依赖仓库慢的全局加速方式
前端依赖可以通过 npmmirror 解决,Maven 依赖同样有加速办法。在~/.m2/settings.xml里配置一个镜像源,可以明显提升整个构建速度。注意只是加速 Maven 中央仓库下载,不涉及任何安全问题:
<mirrors> <mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <name>aliyun maven mirror</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors>这条配置能让所有通过 Maven 中央仓库下载的依赖都走国内镜像。Hadoop 全量编译需要拉取上百个依赖,这个优化能省下不少时间。
6. 沉淀下来的排错习惯
这次排错给我最大的收获,不是某个具体的 registry 地址,而是一套应对构建工具报错的方法论。
遇到Failed to run task这类外层报错,第一反应永远是找“子进程自己打印的错误信息”。frontend-maven-plugin 只是执行了 yarn install,它不可能知道 yarn 内部因为网络、版本还是磁盘而失败。所以我会先翻日志,锁定是npm ERR! code里的哪一类,再决定下一步动作。这个思路不仅适用于 Hadoop,也适用于任何 Maven 集成前端构建的项目,甚至适用于所有通过 CI 执行外部命令的构建链。
另外,遇到这类问题别急着改版本、重装环境。先想清楚一个问题:这个插件为什么要在这里执行这个命令?它的下载源是什么?它使用的是哪个版本的 Node?如果这三个问题能答上来,90% 的根因已经浮出水面了。我每次排这类问题,真正花时间的地方不在于搜方案,而在于把构建链路的每一环拆清楚。
最后一个小建议:如果你要长时间折腾 Hadoop 编译,建议在BUILDING.txt之外,额外记录一份自己环境的版本组合,JDK 版本、Maven 版本、protoc 版本、Node 版本各是多少,下次换机器或换分支,直接照着自己的那份记录搭环境,能省掉很多重复踩坑的时间。