1. 为什么要在离线环境折腾Ambari这套组合
事情起因很简单,我接手了一套处于内网隔离环境的测试集群,硬件资源都到位了,但机房除了管理网段之外,基本没有出公网的通道。也就是说,常规的yum install、pip install、从GitHub直接拉源码,在这些机器上全都走不通。当时集群规划里明确要求使用Ambari来做Hadoop生态组件管理,计算引擎需要Spark,底层存储格式又指定了CarbonData来做列式存储和索引过滤。这仨凑在一起,就引出了本文要讲的核心问题:在断网环境下,如何把Ambari装起来、把自带的Spark版本换成目标版本、再让CarbonData顺利跑在Spark之上。
先说结论,这套流程一点都不复杂,但细节相当多。很多资料把离线安装Ambari和换Spark版本分开讲,结果真到集成的环节,坑全在衔接处。我踩了一遍之后,最深的感受是:离线环境的难点从来不是“装不上”,而是“缺东西没法临时下载”,所以前期的资源盘点、路径规划、版本匹配,往往决定了后面是半小时收工还是熬夜排错。这篇文章更适合有过Ambari实操经验、但没在离线环境里完整跑过一遍的读者,或者是刚搭完HDP集群、正准备引入CarbonData做加速查询的朋友。
文章里所有操作都以我实际验证过的组合为例:Ambari 2.7.4 + HDP 3.1.0,Spark从默认的2.3.2更换到2.4.0版本,CarbonData使用2.0.1。这套组合不算新,但胜在社区资料多、版本兼容性好,当你换成其他版本时,原理完全一致,只要注意具体路径和版本参数即可。
2. 离线安装Ambari:不是把rpm拷过去就完事
2.1 离线源的三层结构:OS源、Ambari源、HDP源
离线安装Ambari,首先要搞清楚一件事:Ambari装完之后,它要给集群里的每一台机器下发组件、安装依赖包。这个“依赖包”不只来自Ambari仓库,还来自操作系统的基础源。比如装HDFS NameNode时,会依赖libtirpc、nc、snappy这些老牌系统包;装Spark时,会依赖snappy-java、lz4之类的压缩库。所以离线环境里,你至少需要准备三套源,缺一套都可能在Agent安装或组件部署阶段突然失败。
- OS基础源:对应你使用的操作系统ISO仓库,例如CentOS 7的Base、Extras、Updates。制作方式是把ISO挂载后,通过
createrepo生成repodata,再放到HTTP目录下。这一步多数资料都会提到,但很多人会漏掉EPEL源。Ambari和HDP的某些依赖(比如python-psutil)在EPEL里,没有它会在部署Grafana或Metrics Collector时报错。 - Ambari源:包含Ambari Server、Ambari Agent两个核心安装包。下载对应版本的tar包后,解压到HTTP目录,同样的
createrepo处理。 - HDP源:这是最大头。HDP仓库包含HDFS、YARN、Hive、Spark、Tez等所有组件的rpm包和bundle,体积通常在20GB以上。离线环境下这部分必须完整下载,同时建议把HDP-UTILS也一并放进去,它是很多组件的公共依赖库,漏了会在配置阶段出现找不到
libhadoop.so这一问题。
2.2 本地Yum源的配置顺序和验证方法
三套源准备好之后,需要在每台服务器上统一配置.repo文件。我这里有一个比较稳妥的写法:
[local-os] name=local-os baseurl=http://repo.local/centos7/ gpgcheck=0 enabled=1 [local-ambari] name=local-ambari baseurl=http://repo.local/ambari/ gpgcheck=0 enabled=1 [local-hdp] name=local-hdp baseurl=http://repo.local/hdp/ gpgcheck=0 enabled=1 [local-hdp-utils] name=local-hdp-utils baseurl=http://repo.local/hdp-utils/ gpgcheck=0 enabled=1这里我用gpgcheck=0,是因为离线环境下的GPG密钥导入容易出问题,稍后单独说明。配置完之后,先不要急着装Ambari,先跑一条验证命令:
yum --disablerepo=* --enablerepo=local-os,local-ambari,local-hdp,local-hdp-utils repolist正常情况下列表里能看到每一个源以及对应的包数量。这里尤其要检查local-hdp的包数量是否和源发布时一致,比如HDP 3.1.0常见的是两千个左右的rpm包,如果只有几百个,说明同步不完整,后续装到一半就会卡死。
验证通过后,安装Ambari Server就很简单了:
yum install ambari-server -y但我的建议是,安装前先看一眼系统环境:关闭SELinux、确认防火墙端口、配置好主机的hostname映射。Ambari Server第一次安装时其实不太挑环境,但环境不干净会直接导致你后面加节点时怀疑人生。
2.3 GPG校验和主机映射:容易被忽略但影响全局的两个细节
上文用了gpgcheck=0,这不是偷懒。离线环境下,如果你在同步源时把GPGKey文件也正确放置在Web根目录,同时配置了gpgkey指向,那么开启校验是可以的。但如果你不想维护密钥文件,或者曾经出现过密钥文件损坏导致的安装失败,关闭校验既安全又省事。
真正影响全局的是/etc/hosts的映射。Ambari集群对主机名解析要求很高,每台服务器的hostname必须和IP一一对应,并且Agent在注册时会执行反向解析。如果/etc/hosts写得不全,做Ambari集群向导的第一步“Confirm Hosts”就会卡在注册阶段。我在一次部署中遇到的情况是:Server装好了,Agent也装了,但Agent总是处于“Registering”状态,排查下来发现是/etc/hosts里只写了当前这台机器,其他节点的主机名解析走的是局域网DNS,而局域网DNS对部分短主机名的解析并不稳定,导致注册请求被挂起。
所以建议在每台节点执行以下命令,而不是只改Server端:
echo "192.168.10.10 ambari-server" >> /etc/hosts echo "192.168.10.11 node01" >> /etc/hosts echo "192.168.10.12 node02" >> /etc/hosts即使有内网DNS,也建议在/etc/hosts里写全集群节点。Ambari的组件分配、服务检查和滚动重启都依赖主机名正确解析,这一步做的越扎实,后面出问题的概率越低。
3. 更换Spark版本:必须先搞懂Ambari的版本管理机制
3.1 Ambari里的Spark版本不是Jar包那么简单
Ambari安装的Spark版本,实际操作下来是一整套目录结构和软链接体系。以HDP 3.1.0自带的Spark2为例,它的安装路径通常是/usr/hdp/current/spark2,而真正的安装主目录指向/usr/hdp/3.1.0.0-78/spark2,其中3.1.0.0-78是HDP的堆栈版本号。Ambari在部署服务时,是基于这个“版本堆栈”来管理的,它会在/usr/hdp/下为不同组件建立目录,然后通过current软链接指向当前活跃版本。
明白了这个结构,就会知道想更换Spark版本,最担心的就是两件事:
- 新版本的目录和默认堆栈版本不一致,导致Ambari在健康检查时找不到对应的脚本或jar。
- Spark的配置目录(
conf)、执行目录(YARN的shuffle辅助脚本、Spark History Server)等位置全变了,必须重新指引Ambari去读取新的路径。
最直接的做法是:把新版本的Spark完整目录放在/usr/hdp/3.1.0.0-78/下,这样Ambari原有的路径解析逻辑就能大概率命中新版本。例如:
# 备份原版本目录(如果还想回滚) mv /usr/hdp/3.1.0.0-78/spark2 /usr/hdp/3.1.0.0-78/spark2.bak # 解压新版本到标准位置 tar -zxf spark-2.4.0-bin-hadoop2.7.tgz mv spark-2.4.0-bin-hadoop2.7 /usr/hdp/3.1.0.0-78/spark2 # 重建current软链接 rm -f /usr/hdp/current/spark2 ln -s /usr/hdp/3.1.0.0-78/spark2 /usr/hdp/current/spark2之后要检查Spark目录下的python文件夹是否存在,因为Ambari默认会把pyspark的路径用于Livy交互,缺失会导致Spark Job在通过Livy提交时频繁报错。
3.2 离线环境下多版本Spark共存的落地步骤
业务上有时不止需要一个Spark版本,尤其是老作业和测试作业并存的场景。Ambari本身不支持一个组件配置两个版本,所以在线环境可以靠Ambari的“版本管理”去注册新的VDF(Version Definition File)来灵活切换,而离线环境下多版本共存用的最多的方法是“复制目录+修改启动脚本”。
我当时在离线环境做的方案是:
/usr/hdp/3.1.0.0-78/spark2:作为Ambari管理的默认版本,通过current/spark2软链接暴露。/data/spark-2.4.0-custom/:额外的Spark独立部署目录,仅供特定作业使用。作业提交时通过--master yarn --conf spark.yarn.jars=hdfs:///spark-jars/这样的方式,显式指向自定义的Spark jar包。
这里是关键点,如果是接入YARN的Spark作业,YARN的NodeManager节点上需要新版本Spark的spark-yarn-shuffle.jar。Ambari管理的Spark目录中自带Shuffle服务,更换Spark版本后,需要在YARN配置里重新指向对应的Shuffle jar路径,否则Spark应用启动时NodeManager会报YarnShuffleService相关的ClassNotFoundException。
离线环境下多版本共存的另一个坑是spark-env.sh。Ambari生成的spark-env.sh里有大量export SPARK_HOME、export HADOOP_CONF_DIR这样的变量,它们的路径都写死到/usr/hdp/current/spark2。如果新版本目录的层级不同,启动脚本可能因为找不到conf/hive-site.xml而无法访问Hive元数据。所以我每次切换后都会先执行:
sudo -u spark bash -c "source ${SPARK_HOME}/conf/spark-env.sh && echo ${SPARK_HOME}"如果输出路径不是预期的新版本目录,就说明Ambari注入的环境变量覆盖了你手动修改的内容,此时需要回到Ambari的Spark配置页,检查Custom spark-env里是否残留了旧路径。
3.3 更换版本后的重启顺序与验证指标
更换Spark版本后,千万不要直接在Ambari UI上一键重启所有服务。合理顺序是:
- 只重启Spark组件中的History Server,确认进程起来、日志无ClassNotFound异常。
- 在Ambari界面中逐步重启NodeManager相关的YARN节点,因为NodeManager混用了Spark Shuffle服务。重启时留意NodeManager日志中是否出现
Could not instantiate org.apache.spark.network.yarn.YarnShuffleService。 - 用一段低负载的Spark SQL作业验证基本连通性,确认
spark-submit能成功提交,Hive仓库路径能正确读取。
验证Spark替换是否成功,我通常不看Ambari的“版本号显示”,它经常是缓存了旧数据,而是跑一个最直接的Spark作业并观察YARN的资源管理器页面上的Application显示。如果应用正常运行,且日志中的Spark版本号是新的,就说明替换成功。如果只改了jar没重启Shuffle服务,作业提交能成功,但运行时间一长就会莫名失败,日志定位到NodeManager,这类现象是最有迷惑性的。
4. CarbonData与Spark的集成:核心原理与配置顺序
4.1 CarbonData为什么能跑在Spark上
CarbonData并不是一个独立的计算引擎,它更像是Spark生态里的“列式存储插件”。和Parquet这类纯列式存储相比,CarbonData最核心的卖点是在文件格式内部建立了多维索引,适合做过滤条件较多、大表聚合类的查询加速。它的实现方式是借助Spark的DataSource接口,把表和文件格式的读写逻辑交给CarbonData自己管理。Spark在接收到SQL后,会通过DataSource解析器将对应的数据路径解析为CarbonData表,然后由CarbonData的底层代码完成索引读取和文件扫描。
在Ambari管理的数据环境下,集成CarbonData的核心问题不是“格式能不能识别”,而是jar包和配置项有没有进入Spark的运行时Classpath。只要Spark能加载到对应版本的carbondata_2.4-2.0.1.jar、carbondata-spark2-2.0.1.jar这类核心包,并且spark.sql.extensions和spark.sql.catalog.*配置正确,CarbonData就能作为Spark SQL的表引擎工作。
4.2 离线获取CarbonData依赖包的方案
离线环境下没有公网Maven仓库可用,所以获取CarbonData jar包时必须提前准备好。常用方式是:在能联网的机器上,通过Maven的dependency:copy-dependencies插件将所需的jar和传递依赖导出,然后打包拷入离线环境。我常用的命令是:
mvn dependency:copy-dependencies -DoutputDirectory=/tmp/carbondata-libs -DincludeScope=runtime这条命令需要在一个pom项目下执行,pom里声明carbondata依赖:
<dependency> <groupId>org.apache.carbondata</groupId> <artifactId>carbondata-spark2_2.11</artifactId> <version>2.0.1</version> </dependency>如果并没有Maven工程作为载体,也可以考虑直接从对应版本的发布包中提取。Apache CarbonData发布归档里通常包含lib/目录,里面有预先打好的jar包,省去自己打包的麻烦。不过要注意:不同CarbonData版本对Spark的兼容性有严格区分,比如2.0.x对应Spark 2.3+,1.6.x对应Spark 2.1/2.2/2.3。下载jar包时,看清楚名字里的spark2/spark3后缀,以及构建时使用的Scala版本是2.11还是2.12,这决定了它能否和你的Spark运行环境匹配。
离线下还有一个隐藏环节:CarbonData启动时会加载commons相关的第三方包(例如commons-dbcp2、commons-pool2),这些包在Spark的jars/目录中不一定齐全,所以即使核心jar拷过去了,启动时还是可能报ClassNotFoundException。遵循旧方法,把所有第三方依赖jar都丢进Spark的jars/目录是一种做法,但不推荐,因为容易和其他组件产生版本冲突。更稳妥的方式是把它们统一放到Spark的conf/同级目录下,修改spark-env.sh里的SPARK_CLASSPATH变量。
4.3 配置文件修改与spark-shell冒烟验证
CarbonData集成,最关键的配置项是这几个,你需要在Ambari的Spark自定义配置里新增或调整:
spark.sql.extensions=org.apache.carbondata.spark.CarbonSparkExtensions spark.sql.hive.metastore.jars=builtin spark.kryo.registrator=org.apache.carbondata.serializer.CarbonKryoRegistrator spark.serializer=org.apache.spark.serializer.KryoSerializer为什么必须加这几个配置?第一,spark.sql.extensions是Spark SQL解析扩展包的入口,CarbonData通过这个配置注册自己的DDL语法和索引规则,没有它,建表语句执行后Spark只会把它当作普通的物理表路径处理。第二,CarbonData底层用了Kryo进行序列化,如果不告知Spark加载对应的Registrator,在写入大量数据时会触发序列化失败,报错信息还会指向某些晦涩的ClassNotFound而不是直接提到CarbonData,排查起来相当费劲。
配置文件准备好之后,不要急着在Ambari里重启,先在命令行做一次冒烟验证:
cd /usr/hdp/current/spark2 bin/spark-shell --master local[2] \ --jars $(ls /opt/carbondata-libs/*.jar | tr '\n' ',') \ --conf spark.sql.extensions=org.apache.carbondata.spark.CarbonSparkExtensions \ --conf spark.sql.catalog.carbondata=org.apache.carbondata.spark.CarbonSessionCatalogExtension在spark-shell中执行一段最简单的CarbonData建表语句:
CREATE DATABASE IF NOT EXISTS carbon_test; USE carbon_test; CREATE TABLE IF NOT EXISTS user_click ( user_id int, click_time timestamp, page_url string ) USING carbondata; INSERT INTO user_click VALUES (1, current_timestamp(), 'home'); SELECT COUNT(*) FROM user_click;如果这一步能跑通,说明核心jar和扩展配置都没问题。如果建表时报UNSUPPORTED DATASOURCE,那是spark.sql.catalog.carbondata或扩展类没有被识别,优先检查jar包是否真的在Classpath中。如果查询时报文件路径错误,则要检查HDFS上CarbonData表的默认路径权限,确保运行Spark的用户有写权限。
5. 实测中踩过的坑:从根因到解决的完整链路
5.1 本地源里缺了Uber jar导致Spark无法启动
有一次在Ambari界面中执行Spark组件重启,过程显示成功,但Spark History Server始终处于“Starting”状态。登录服务器看日志,发现报错信息反反复复指向找不到spark-examples或spark-sql相关的运行类。当时第一反应是Spark目录损坏,于是重新解压新版本,问题依旧。
排查到最后才意识到,问题不在Spark目录,而在HDP源缺包。Ambari在启动Spark组件时,会根据堆栈定义执行spark2-client相关的安装包,这个spark2-client是一个聚合rpm包,里面包含了一系列依赖,包括spark2-core、spark2-sql以及最新的Uber jar(把所有依赖打成一个包的那个)。因为离线源里同步的时候漏掉了spark2-uber这个rpm,导致Ambari认为client组件没有完整安装,启动时自然缺类。
解决方式分两步:先确认这个rpm是否真的缺失:
yum list installed | grep spark然后在离线源中补上对应的Uber jar rpm,重新执行:
yum install spark2-uber -y补装后用Ambari重启Spark组件,两分钟之内恢复正常。这个坑的核心教训是:离线源的同步不是“能ping通就行”,一定要把repodata的包数量、名称和官方发布列表做一次全量比对,尤其在Ambari中“install”过的服务,它会约束实际安装到的rpm包。
5.2 CarbonData表写入后查询中文乱码
集群中接入的第一批业务数据就有中文,比如用户昵称、商品名称等。导入CarbonData表后,用Spark SQL查询返回的结果全是???这样的乱码。我最初怀疑Hive元数据库的编码配置有问题,检查了hive-site.xml,也检查了MySQL的库表编码,均为utf8,但问题依旧。
后来发现,问题出在Spark侧的spark.sql.warehouse.dir和Hive仓库路径。CarbonData在写入时默认使用SparkSQL的编码风格,但读取时如果Hive仓库的路径下存在旧表的元数据指向了不一致的SerDe或InputFormat,就会触发编码转换异常。另外,CarbonData的DDL语句里有COMMENT字段会对列注释做编码读取,如果元数据连接串漏了characterEncoding=UTF-8,中文被转成GBK存储,查询时自然乱码。
解决方案是在Ambari的Hive配置的hive-site.xml中,将元数据库JDBC URL显式加上:
javax.jdo.option.ConnectionURL=jdbc:mysql://node01:3306/hive?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useSSL=false同时,在Spark的spark-defaults.conf里新增:
spark.sql.session.timeZone=Asia/Shanghai spark.sql.legacy.charVarcharAsString=true然后删除旧表,重建CarbonData表,重新导入数据,乱码消除。实际上后一个配置项主要是为了兼容一些老Hive表对varchar类型的处理,配合它能减少非常多的隐式转换问题。
5.3 替换Spark后Ambari健康检查误报
替换Spark版本后,Ambari页面上一直有Spark组件的黄点警告,点进去第一条是Spark History Server alb readiness或类似的路径检查失败。我检查了进程、端口、日志,发现Spark History Server是正常的,页面也能打开,但Ambari依旧告警。这个问题其实是Ambari的metric脚本里写死的URL或预期响应内容和新版本不匹配导致的。比如旧版本返回的history列表HTML结构里有一个特定标记,新版本改了模板,所以Ambari脚本认为服务不健康。
临时解决方法是进入Ambari的“服务健康检查”配置界面,把Spark的历史服务器度量指标采集项禁用或修改为新的URL访问方式。如果要彻底解决,需要查看/var/lib/ambari-agent/data/下的缓存度量信息,清理后重启Agent:
rm -rf /var/lib/ambari-agent/data/* systemctl restart ambari-agent但更实际的做法是:如果Ambari UI的告警只影响监控界面、不影响作业的运行,就先在告警定义里降低这个检查项的阈值,避免它频繁触发短信和邮件。AWS云厂商踩过坑的人会知道,在Air-gapped环境里不要花太多时间纠结UI上的健康检查是否全部点亮,而是应该关注组件本身的功能是否完整。把Spark History Server的访问URL手动在浏览器里打开,确认页面渲染和日志读取正常,比在Ambari界面上消黄点更有价值。
6. 实测后的配置固化与备查清单
6.1 把关键配置固化成模板,避免二次部署重复踩坑
离线环境的部署最怕的就是“这次能跑,下次重装完又忘了之前怎么配的”。我在完成整套安装之后,会把下列内容整理成一份cluster-bundle目录,存放在集群外的一台管理机上,同时也拷贝一份到每台节点的/root/offline-deploy-backup/目录下:
- 三套Yum源的
.repo文件和对应的本地HTTP根目录结构树。 - Ambari Server安装完成后导出的主配置快照,路径在
/etc/ambari-server/conf/,压缩后几百KB,换机器恢复很有用。 - Spark替换版本时的目录软链接变更记录,以及
spark-env.sh中我手动追加的变量、spark-defaults.conf中的CarbonData配置段。 - CarbonData jar包和第三方依赖的来源说明,包含版本号、构建时使用的Scala版本、以及从哪个发布包提取的路径。不要只存jar,不存“来源信息”,否则半年后想升级时连去哪找对应包都不知道。
- Ambari Agent在每台机器上的健康检查清理命令、注册日志位置。
这套文档并不花哨,但正是这些细节决定了你下次部署时是从容地恢复备份,还是又花两天踩同一个坑。
6.2 几个值得长期保存的运维脚本思路
离线环境下,Ambari和Spark的运维讲究的是“批量操作前先确认幂等性,操作后能快速验证”。
以Spark版本替换后的Shuffle jar分发为例,我会在管理机上放一个脚本,用scp或ansible批量把jar推到所有NodeManager节点,并自动重启节点上对应的服务。脚本里最关键的不是传输命令,而是推送前的校验逻辑:检查目标节点的进程是否在运行、推送后校验文件的md5是否一致、以及是否需要在重启前先记录NodeManager当前运行状态。在校验产物阶段,可以准备几个简单的Spark SQL测试场景,分别覆盖读写CarbonData表、查询Hive外部表、提交Spark Streaming任务这三类典型负载,跑完一轮即认为版本替换成功。
还有一个小脚本是关于Ambari Agent注册状态监控的。离线环境下Agent偶尔会因为网络闪断进入“UNKNOWN”状态,单纯重启Agent不一定有效,因为Ambari Server端的注册记录还残留在旧状态。脚本的解决办法是:检测到Agent是UNKNOWN后,先重启Agent,再用Ambari API接口调用hosts注册接口强制刷新状态。这一步在维护大集群时非常实用。
6.3 扩展思路:升级CarbonData版本时应该同步调整什么
如果后续想升级CarbonData版本,比如从2.0.1升级到2.1.x或更高版本,建议按这个顺序来做:
- 先把新版本的jar包放到所有Spark节点,同时保留旧版本jar在备份目录,不建议直接覆盖删除。
- 修改
spark-defaults.conf里的扩展类名和Catalog类名,注意新版本可能改了类名路径,先查清再改。 - 在低峰期重建或者ALTER各CarbonData表,因为部分老表格的索引版本在新版本中不会被自动升级,查询时可能走不上文件索引。
- 在一台节点上用
spark-shell做完整的建表、插入、查询验证,再决定是否全量滚动重启。
这套思路同样适用于Spark版本升级。Ambari管理环境下的“升级”看似是个UI按钮,离线环境下却总伴随路径依赖和Classpath问题。把一次成功的升级步骤固化成笔记,比依赖记忆更可靠。
7. 写在最后的几点实在话
折腾了几天离线环境,最想分享的并不是某条命令,而是一套对待这类问题的思路。
先把“版本兼容矩阵”视为第一优先级。Ambari、HDP、Spark、CarbonData、Scala、Hadoop,每一个都互相咬合,断网环境里出了版本问题,补一个jar往往要绕非常远的路,甚至要重打整个源。所以我现在的习惯是:动手前先对照版本兼容表,画出大版本依赖关系图,确认没有问题再开始准备离线包。
其次是优先级排序。离线安装Ambari、换Spark版本、集成CarbonData,这三个步骤其实是有严格依赖顺序的。先装好稳定的Ambari,再做Spark版本的目录替换和Shuffle校验,最后才轮到CarbonData的配置和建表验证。如果图省事把顺序颠倒,出了问题你会很难判断是Spark加载配置失败了,还是CarbonData扩展类在Classpath里没生效。
最后是多利用日志和进程健康度来确认“真的是装好了”。Ambari UI上的绿色勾和真实服务状态有时候并不同步。离线环境下没有那么多现成工具帮你判断,多敲几条yum list installed、jps、netstat -lnp | grep <port>,比反复刷新网页要管用得多。
这套从零搭建的流程跑通之后,后续维护会顺滑很多。至少对我而言,现在再看到离线部署的需求,心里已经有一张完整的“源准备—组件安装—版本切换—插件集成”的路线图了。