接手过 HDP 集群运维的朋友应该都有同感:Ambari 装集群确实方便,但真要升级组件,就像给一栋住着人的老房子换承重墙,不动则已,一动就要撬开一大片。前阵子我们遇到一个很典型的场景——业务方要求把集群里的 Spark 从自带的 2.3.x 升到 2.4.8,但集群里跑着十几个基于 Hive、Tez、Flink 的常态化作业,Ambari 本身一点都不能乱碰。于是就有了这篇实操记录:在不升级 Ambari 的前提下,把 HDP 自带的 Spark 整体替换成 Spark 2.4.8。
整个过程听起来不复杂,但中间牵扯到的版本兼容、Yarn Shuffle、Hive Metastore 联调、History Server 权限这些环节,每一个单独拎出来都够喝一壶。这篇博文把决策思路、准备清单、完整操作步骤和踩坑实录一次性列清楚,适合正在维护 HDP 集群、又不想被 Ambari 的版本捆绑机制牵着走的运维和平台工程师参考。
1. 为什么非要绕开 Ambari:决策背后的技术逻辑
大多数人第一反应是:直接在 Ambari 里升级 Spark 不就完了吗?实际上 HDP 集群里通过 Ambari 升级任意组件,都很难做到“只动一个”。理解了 Ambari 的版本管理机制,你就能明白为什么不升级 Ambari 才是对生产环境最友好的方案。
1.1 先看懂 Ambari 的版本锁是怎么工作的
Ambari 管理 HDP 集群时,会把组件版本号写死在 Stack 的 Metadata 和 repo 定义里。它眼里没有一个独立的“Spark 版本”,只有一整套 HDP Stack 版本。当你想在界面上把 Spark 从 2.3 升到 2.4.8 时,Ambari 实际要做的是执行一次 Stack Upgrade,也就是把 Hive、Tez、HDFS 客户端、Yarn 客户端这些组件统统检查一遍。
这意味着两件事:第一,升级时间窗口会拉得很长,因为组件依赖校验会触发一堆关联服务的滚动重启;第二,一旦升级过程中某个组件失败,整个滚动升级就会进入半吊子状态,Ambari 会反复提示你需要回滚或者继续,处理起来非常棘手。
对生产环境来说,这种“全家桶式”的变更往往风险大于收益。我们只需要一个组件的版本变化,其他组件继续维持现状,这本身就是一种合理的架构决策——不要让工具链的耦合度绑架你的运维节奏。
1.2 目标版本为什么是 2.4.8,而不是 3.x
Spark 2.4.8 是 2.4 系列的最后一个版本,等于整个 Spark 2.x 时代所有 bug 修复和稳定性补丁的集大成者。相比 HDP 3.x 自带的 2.3.x,2.4.8 在以下几个方面的改善尤其明显:
- 内置支持 Java 11(但运行仍然以 Java 8 为主,兼容性没有压力)
- 增强了 Structured Streaming 的状态管理和 Kafka 数据源容错
- 修复了若干 Yarn 模式下 executor 丢失后无法正确恢复的问题
- Barrier Execution Mode 和新的 Stage 级调度也在这个版本趋于成熟
那为什么不直接上 Spark 3.x?现实很骨感:Spark 3.0 开始默认 Scala 2.12,很多基于 Scala 2.11 编译的业务 jar 包要全部重新编译;并且 Spark 3.x 与 Hive 1.2/2.3 的 Metastore 交互有许多行为变更,在 HDP 老版本生态里适配成本很高。如果业务没有强依赖 Spark 3 的动态分区裁剪、自适应执行等特性,选择 2.4.8 是风险和收益之间最舒服的平衡点。
1.3 三条技术路线的取舍对比
在确定“不升级 Ambari”的前提下,我实际评估过三条路线,也建议每个团队都按照这个维度去权衡:
| 方案 | 改动范围 | 回滚难度 | 风险点 |
|---|---|---|---|
| Ambari 整栈升级 | 最广,多个组件连带变更 | 困难,半途失败很难处理 | 时间窗口长,影响面不可控 |
| 直接替换 Ambari 管理目录下的 spark2 文件 | 中等,集中在一个目录 | 中等,需要保留原目录备份 | Ambari 健康检查会报警,权限和属主容易乱 |
| 独立目录部署新版 Spark,环境变量切换 | 最小,新老并行互不影响 | 容易,切换环境变量即可 | 需要手工处理 Yarn Shuffle 和 Hive 依赖 |
最后选的是第三条:在/opt下独立部署一套 Spark 2.4.8,不碰/usr/hdp/current/spark2-client原来的目录。这样 Ambari 依然认为集群里的 Spark 是旧版本,它的心跳检查和健康检查不会异常,而业务提交作业时,显式地走新版的SPARK_HOME。新老版本共存,回滚就是把环境变量指回去,一秒钟的事。
这套思路本质上是把“组件升级”从“平台级变更”里剥离出来,用应用部署的逻辑处理大数据组件。集群可以继续被 Ambari 管理,但关键的计算引擎掌握在我们自己手里。
2. 升级前的底细排查与回滚设计
任何生产环境的变更,前期功课做得越足,后面踩坑越少。这一章把动手前必须完成的检查项和回滚策略整理成清单,照着做基本能规避八成以上的低级事故。
2.1 环境自查清单:先摸清集群老家底
别一上来就下载安装包,先把集群当前的状态摸清楚。我建议至少执行以下几组命令并记录输出:
# 查看 HDP 栈版本和三方组件的版本 hdp-select status # 查看当前 Spark 路径和版本 /usr/hdp/current/spark2-client/bin/spark-submit --version # 查看 Hadoop 和 Yarn 版本 hadoop version # 查看 Hive 版本 hive --version # 查看 Java 版本 java -version重点关注三个关键点:
第一,Spark 2.4.8 要求 Java 8,如果你的集群还跑在 Java 7 上,得先处理 JDK 的问题。第二,Yarn 的 ResourceManager 和 NodeManager 是否启用了 Auxiliary Shuffle Service,如果启用了,新旧 Spark 的 shuffle 插件版本怎么兼容需要在后文专门处理。第三,Hive Metastore 的版本,HDP 3.x 里普遍是 Hive 3.1.x,而 Spark 2.4.x 内置的 Hive 版本是 2.3.4,这个差异不解决,Spark SQL 连 Hive 表百分之百会出问题。
另外建议查一下/etc/spark2/conf下有没有历史遗留的自定义参数。很多人已经在spark-defaults.conf里配了spark.dynamicAllocation.enabled、spark.shuffle.service.enabled这类运行参数,搬迁时要把这些参数原封不动地带到新版本里,否则升级后作业行为会发生微妙变化。
2.2 备份与回滚方案:不能玩单程票
生产升级最怕的就是“改完回不去”。这套方案里回滚设计非常简单,核心原则是老目录一个字节都不动,新东西全部放独立路径。
具体需要备份的是:
# 备份旧版 Spark 配置目录,防止后续排查需要比照 cp -rp /etc/spark2/conf /etc/spark2/conf.bak.$(date +%Y%m%d) # 记录当前环境变量内容 echo $SPARK_HOME > /tmp/spark_home_before.txt echo $SPARK_CONF_DIR > /tmp/spark_conf_dir_before.txt # 导出旧的 spark-env.sh 作为配置参考 cp /etc/spark2/conf/spark-env.sh /tmp/spark-env.sh.bak还有 HDFS 侧也需要检查:/user/spark 或自定义的 spark 历史目录、event log 目录当前是否有大量存量数据,以及对应目录的属主和权限。新版 History Server 起来后要读写这些目录,权限不对会直接导致 Spark UI 没有历史任务记录,排查起来还挺隐蔽。
回滚动作在方案设计阶段就要想好。后面我们会把所有环境变量写到一个独立的 profile 文件里,回滚时只要把这个文件的指向改回旧的/usr/hdp/current/spark2-client,再重新登录即可。整个回滚时间不超过五分钟,这才是生产级方案该有的底线。
2.3 获取安装包:官方包与源码编译的抉择
Spark 2.4.8 官方发布包里默认提供的是spark-2.4.8-bin-hadoop2.7.tgz,它针对 Hadoop 2.7 编译,并没有提供开箱即用的 Hadoop 3.x 版本包。HDP 3.x 底层是 Hadoop 3.1.x,所以这里有两个选择。
第一个选择:直接用官方 hadoop2.7 的预编译包。大多数时候 Spark 作业并不会直接调用 Hadoop 内部 API,而是通过 Yarn 客户端和 HDFS 客户端接口交互,所以兼容性通常情况下是能跑通的。但一旦遇到 Hadoop 3 引入的某些新特性,或者用到需要 native 库支持的功能,就会出现各类奇怪的NoSuchMethodError。
第二个选择:源码编译,指定 Hadoop 3.1 的 profile。这是我们最终采用的方式,因为它能保证 Spark 的hadoop-client-api和集群实际的 Hadoop 版本严格匹配,减少运行时冲突:
# 在构建机上执行,提前装好 JDK8 和 Maven 3.5+ git clone --branch v2.4.8 https://github.com/apache/spark.git cd spark # 编译并打包,指定 Hadoop 3.1、Hive、Thrift Server、Yarn 支持 ./dev/make-distribution.sh \ --name 2.4.8-hadoop3.1 \ --tgz \ -Phadoop-3.1 \ -Phive \ -Phive-thriftserver \ -Pyarn编译大概需要半小时到一小时,取决于构建机性能和网络。生成物是spark-2.4.8-bin-2.4.8-hadoop3.1.tgz这种命名的压缩包。如果公司对二进制供应链有要求,编译一次也方便后续做安全扫描和内部校验。
这里再提醒一句:如果集群里其他组件依赖 Spark 提供的某些 jar(比如 Livy、Zeppelin),建议先盘点清楚它们到底用的是哪个SPARK_HOME,不要等到切换完才发现笔记本服务连不上集群。
3. 核心实操:不碰 Ambari 完成 Spark 整体替换
这一章是整个方案的核心动作。按照“先部署新包、再迁配置、最后切流量”的顺序来,每一步都尽量给出可直接复制的命令和需要避开的坑。
3.1 目录规划与环境变量:先把新家安好
我习惯把第三方部署的东西统一放在/opt下,和 HDP 的/usr/hdp/current区分开,互不污染。目录结构这样设计:
mkdir -p /opt/spark tar -zxvf spark-2.4.8-bin-2.4.8-hadoop3.1.tgz -C /opt/spark/ ln -s /opt/spark/spark-2.4.8-bin-2.4.8-hadoop3.1 /opt/spark/spark-current软链接是这套方案的精髓。它能保证后续升级小版本时,只需要重新解压一个目录、改一下软链接指向,作业侧完全感知不到变化。
接下来配置环境变量。不直接改全局的/etc/profile,而是创建独立的 profile 文件,方便回滚:
cat > /etc/profile.d/spark-248.sh <<'EOF' export SPARK_HOME=/opt/spark/spark-current export SPARK_CONF_DIR=/etc/spark2/conf.new export PATH=$SPARK_HOME/bin:$PATH EOF这里有个关键细节:SPARK_CONF_DIR指向的是新创建的/etc/spark2/conf.new,而不是老配置目录。新版安装包里自带的conf目录只是模板,我们要把 HDP 环境里针对 Yarn、HDFS、Hive 的适配配置单独放到这个新目录里。这样两个版本之间的配置完全隔离,切换时不会互相干扰。
需要立刻检查一下新目录是否生效:
source /etc/profile.d/spark-248.sh which spark-submit spark-submit --version如果输出显示的是/opt/spark/spark-current/bin/spark-submit,说明新版本已经处于待命状态。注意此时先不要提交作业,配置还没迁完。
3.2 配置迁移:从 spark-defaults 到 hive-site
新版 Spark 的conf目录下需要准备这些文件:
spark-env.sh里最要紧的是让 Spark 能正确找到 Hadoop 的配置和 native 库。HDP 环境里推荐这样做:
cat > /etc/spark2/conf.new/spark-env.sh <<'EOF' #!/usr/bin/env bash export SPARK_HOME=/opt/spark/spark-current export HADOOP_HOME=/usr/hdp/current/hadoop-client export HADOOP_CONF_DIR=/etc/hadoop/conf export SPARK_CONF_DIR=/etc/spark2/conf.new # 关键:让 Spark 直接使用 HDP 提供的 Hadoop classpath export SPARK_DIST_CLASSPATH=$(hadoop classpath) EOF chmod +x /etc/spark2/conf.new/spark-env.shSPARK_DIST_CLASSPATH这个变量特别重要。如果不设置,Spark 会默认使用自己 lib 目录下的 Hadoop 相关 jar,这些 jar 与 HDP 的 Hadoop 版本存在细微差异,运行到某些方法调用时会突然炸出NoSuchMethodError。设置之后,Spark 的 executor 启动时会把 HDP 提供的 Hadoop classpath 一并带上,从源头消除版本冲突。
spark-defaults.conf里沿用旧版本的关键配置,同时补充新版本需要的 Yarn 归档配置:
spark.master=yarn spark.eventLog.enabled=true spark.eventLog.dir=hdfs:///user/spark/applicationHistory spark.serializer=org.apache.spark.serializer.KryoSerializer spark.dynamicAllocation.enabled=true spark.dynamicAllocation.minExecutors=1 spark.dynamicAllocation.maxExecutors=50 spark.shuffle.service.enabled=true spark.yarn.archive=hdfs:///user/spark/spark-archive.zipspark.yarn.archive这个参数是 2.4 版本跑 Yarn 模式的推荐做法。它把 Spark 的 jar 包打成压缩包上传到 HDFS,executor 启动时直接从这个目录拉取依赖,而不是每个任务都把几百个 jar 从本地复制一遍。没有这行配置,任务一多会出现大量Container launch failed的报错。
准备好目录和 HDFS 归档:
# 打包上传 Spark jars 到 HDFS cd /opt/spark/spark-current/jars jar cf /tmp/spark-archive.zip -C . . hdfs dfs -mkdir -p /user/spark hdfs dfs -put -f /tmp/spark-archive.zip /user/spark/ # 创建 history 目录,并确认 spark 用户的写权限 hdfs dfs -mkdir -p /user/spark/applicationHistory hdfs dfs -chown -R spark:hadoop /user/spark最后一个大项是 Hive 相关的配置。把 HDP 集群的hive-site.xml复制到新配置目录,并确认spark-defaults.conf里设置了 Metastore 版本和 jar 路径:
spark.sql.hive.metastore.version=2.3.4 spark.sql.hive.metastore.jars=/usr/hdp/current/hive_metastore/lib/* spark.sql.hive.metastore.sharedPrefixes=com.mysql.jdbc,org.postgresqlHDP 3.x 的 Hive Metastore 是 3.1,和 Spark 内置的 2.3.4 存在版本差。这里让 Spark 直接加载 HDP 自带的 metastore lib,可以有效规避 Thrift 协议版本不一致的问题。如果你的集群还开启了 HiveServer2 的 ACID 特性,需要额外验证 Spark 读写事务表是否正常。
3.3 Yarn 集成:Shuffle 服务是最大的坑
这一章值得单独拎出来说,因为几乎所有升级后作业异常都出在这里。
HDP 的 Yarn 默认启用了 External Shuffle Service,也就是 NodeManager 上跑着一个spark_shuffle的辅助服务。当 executor 需要拉取 shuffle 数据时,是通过这个辅助服务来完成的,而不是直接从其他 executor 的磁盘上读取。
问题在于:旧版集群的spark_shuffle辅助服务加载的是 Spark 2.3 的spark-network-yarn-shufflejar,而新版 executor 的 shuffle 数据格式和协议与 2.3 存在差异。两者混跑时,会间歇性出现 reducer 拉取不到数据、作业卡在 shuffle 阶段、甚至 executor 直接丢失的现象。
解决思路有两种。第一种是同时升级 NodeManager 上的 shuffle jar,把新版 Spark 的spark-network-yarn-shuffle_2.11-2.4.8.jar替换到 NM 的 classpath 里,然后滚动重启 NodeManager。这个方案对运行中的作业有影响,必须选在维护窗口操作。
第二种方式更省事:在新版 Spark 配置里禁用 External Shuffle Service,让 executor 自己管理 shuffle 数据:
spark.shuffle.service.enabled=false spark.dynamicAllocation.enabled=true代价是如果开启了动态资源分配,executor 闲置被回收后,它持有的 shuffle 文件也会被清理,正在执行的 reducer 可能会重算或者拉取失败。所以如果你依赖动态资源分配,建议还是老老实实升级 NM 的 shuffle 插件,不要为了省事牺牲稳定性。
我在生产上最终选择了替换 NM shuffle jar 的方案,具体操作:
# 确认新版 shuffle jar 的完整名称 ls /opt/spark/spark-current/jars/ | grep spark-network-yarn # 复制到 HDP 的 spark aux 目录(具体版本号以 hdp-select 为准) cp /opt/spark/spark-current/jars/spark-network-yarn_2.11-2.4.8.jar \ /usr/hdp/3.1.5.0-152/spark2/aux/替换完成后,通过 Ambari 或直接操作 Yarn 进行滚动重启 NodeManager。重启过程中存量作业会短暂重试,整体时间窗口控制在二十分钟以内基本没有感知。
3.4 History Server 与 HDFS 权限准备
History Server 是升级后团队日常排查问题的眼睛。新版 Spark 的 History Server 需要从 HDFS 的 event log 目录读取历史任务数据,如果目录权限不对,会出现“任务跑完但 UI 上看不到记录”的诡异现象。
启动方式不用走 Ambari,直接以独立进程形式拉起:
mkdir -p /var/log/spark nohup /opt/spark/spark-current/sbin/start-history-server.sh \ --properties-file /etc/spark2/conf.new/spark-defaults.conf \ >> /var/log/spark/history-server.log 2>&1 &然后马上验证事件日志目录可写:
sudo -u spark hdfs dfs -ls /user/spark/applicationHistory如果启动失败,大概率是/user/spark/applicationHistory的属主不对,或者配置里的spark.eventLog.dir路径和实际目录不一致。排查时始终先看日志,不要瞎猜。
4. 升级验证与存量作业适配
部署完成后不能直接宣布成功,得有一整套验证流程把新版本从“能启动”推进到“能正常扛业务”。
4.1 冒烟测试:从版本号到 Pi 任务
打开三个终端,第一组验证命令行工具的版本输出,第二组跑一个简单的 Pi 任务,第三组盯 Yarn 资源管理器页面。
# 验证版本 spark-submit --version # 运行官方自带 Pi 示例 $SPARK_HOME/examples/jars/spark-examples_2.11-2.4.8.jar 才需要指定 # 如果上面的 examples jar 不存在,从 jars 目录找 spark-submit --class org.apache.spark.examples.SparkPi \ --master yarn \ --deploy-mode cluster \ /opt/spark/spark-current/examples/jars/spark-examples_2.11-2.4.8.jar \ 10Pi 任务能通过不算什么,它只验证了基本提交链路。接下来要验证的是与 Hive 的交互,这部分在升级中最容易出幺蛾子。
4.2 Spark SQL 与 Hive Metastore 联调
准备一张测试用的 Hive 表,执行几个典型操作:
spark-sql \ --master yarn \ --conf spark.sql.hive.metastore.version=2.3.4 \ --conf spark.sql.hive.metastore.jars=/usr/hdp/current/hive_metastore/lib/* \ -e "show databases;"如果show databases能正常返回,再测试建表、插入和查询:
CREATE TABLE test_spark_248 (id INT, name STRING); INSERT INTO TABLE test_spark_248 VALUES (1, 'hello'); SELECT * FROM test_spark_248;这道流程走通,说明新 Spark 和 Hive Metastore 的通信没问题。如果这里卡住,优先排查hive-site.xml是否放进了SPARK_CONF_DIR,以及 metastore 的 lib 路径是否真正存在。版本不匹配时常见的报错是MetaException: Got exception: java.io.IOException: Cannot connect to metastore,定位方式在后面章节统一讲。
最后把 Spark Thrift Server 也顺手验证一下。如果你的团队里有同事习惯用数据库客户端连 Spark,那这个服务的正常启动和查询就意味着整条链路真正打通了。
4.3 存量作业与 PySpark 兼容性检查
存量作业最怕三类问题:依赖冲突、jar 包缺失、Scala 版本变化。Spark 2.4.8 和旧版本一样默认 Scala 2.11,所以基于 Scala 2.11 编译的 jar 基本可以直接复用。但建议挑出业务价值最高的三到五个作业,在新版本上做一遍回归测试。
PySpark 用户需要确认 Python 版本。Spark 2.4.8 官方支持 Python 2.7 和 3.4+,但如果集群环境的默认 Python 是 2.6 或者太老的版本,会出现启动报错。一般生产环境建议用 Python 3.6 以上,同时确保每台机器上pyspark能正确找到SPARK_HOME。
实际操作中发现,PySpark 的报错经常是 “Python in worker has different version”,这是因为 driver 节点和 executor 节点上 Python 默认版本不一致。处理办法是在 spark-env.sh 里显式指定: export PYSPARK_PYTHON=/usr/bin/python3 export PYSPARK_DRIVER_PYTHON=/usr/bin/python3回归结束后,把新 Spark 相关的使用说明同步给业务同学,尤其是告知他们不要再依赖旧的spark2-submit绝对路径,一律改用环境变量里的spark-submit。
5. 实战问题排查与避坑心得
最后这部分是真正值钱的内容。整个升级过程里我们踩了不少坑,有些问题表面上毫无头绪,实际都是非常固定的套路,整理成速查表方便后人。
5.1 高频报错速查表
| 报错现象 | 根因 | 解决动作 |
|---|---|---|
NoSuchMethodError: org.apache.hadoop.security.authentication.client.Authenticator | Spark 自带的 Hadoop 客户端与 HDP 的 Hadoop 版本不一致 | 在 spark-env.sh 中设置SPARK_DIST_CLASSPATH=$(hadoop classpath),或者删掉 Spark lib 下冲突的 hadoop-common jar |
Cannot connect to metastore | Hive Metastore 版本不匹配或 hive-site.xml 未加载 | 确认SPARK_CONF_DIR下存在 hive-site.xml,确认spark.sql.hive.metastore.jars路径指向 HDP 实际 lib 目录 |
Container launch failed: org.apache.spark.network.shuffle.ShuffleService | NM 上的 shuffle jar 仍是旧版,与新协议不兼容 | 替换 NM 的 shuffle jar,滚动重启 NodeManager |
Failed to create local dir或 HDFS 目录写入失败 | History 目录或 spark-archive 目录权限错误 | 检查/user/spark目录的属主,确认 spark 用户有读写权限 |
SparkContext: Error initializing SparkContext后跟一堆类找不到 | SPARK_HOME 环境变量没有指向新版本,或 PATH 被旧目录覆盖 | 重新source /etc/profile.d/spark-248.sh,确认which spark-submit指向新路径 |
Java HotSpot 报内存不足 | 升级后默认内存参数与旧集群配置不一致 | 对照旧/etc/spark2/conf备份,把spark.executor.memory、spark.executor.cores等参数迁过来 |
表格只是入口,真正排查时核心原则是“不要用肉眼看配置,去看日志”。Spark 的日志分三处:driver 的 stdout/stderr、Yarn 的 container 日志、History Server 的日志。遇到问题先定位是哪个阶段出错,再对照表格排查。
5.2 细节避坑:软链、健康检查、日志排查
最后交代几个容易忽略的细节,都是真实操作里用了代价才换来的经验。
第一,Ambari 的健康检查。因为新 Spark 进程是手工启动的,Ambari 对自己管理的旧 Spark 服务状态一无所知。如果你之前开着 Ambari 里 Spark2 服务的 History Server,升级前一定要在 Ambari 界面先停掉旧服务,释放端口,否则新 History Server 会一直报端口占用,而且很难看出来。
第二,软链接不要乱指。有些教程会让你直接把/usr/hdp/current/spark2-client这个软链接指到新目录,千万不要这么做。HDP 的很多工具脚本和 Ambari 的监控脚本会硬编码读取这个路径下的版本信息,一旦发现版本与栈定义不一致,轻则告警刷屏,重则影响后续其他组件的升级操作。独立部署才是正解。
第三,日志目录要提前想好。新版 Spark 默认的日志位置不一定有权限写,建议在spark-env.sh里显式配置SPARK_LOG_DIR=/var/log/spark,并提前mkdir -p和chown。很多“看不出原因”的启动失败,其实只是日志目录写不进去。
第四,如果集群里有 Livy 或 Zeppelin 这类上层服务,升级后必须检查它们的SPARK_HOME指向。这个排查步骤很容易被遗忘,但它恰恰是升级后“数据开发平台不可用”的头号原因。宁可提前在测试环境先改好配置,也不要等业务同学来报障。
整个替换过程做下来,我最深的体会是:HDP 自带的 Spark 版本和 Ambari 的版本管理只是平台默认值,不是业务必须遵守的锁。只要把新组件独立部署、做好环境隔离、提前设计好回滚路径,就能在不惊动 Ambari 的前提下完成核心引擎的平稳迭代。这个思路不仅仅适用 Spark,集群里其他组件的定向升级也完全可以复用同一套方法论。后续如果再遇到类似需求,记得把配置文件迁移和 Shuffle 服务适配这两块作为重点,这两处不出问题,升级就已经成功了八成。