
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.0Spark从默认的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] namelocal-os baseurlhttp://repo.local/centos7/ gpgcheck0 enabled1 [local-ambari] namelocal-ambari baseurlhttp://repo.local/ambari/ gpgcheck0 enabled1 [local-hdp] namelocal-hdp baseurlhttp://repo.local/hdp/ gpgcheck0 enabled1 [local-hdp-utils] namelocal-hdp-utils baseurlhttp://repo.local/hdp-utils/ gpgcheck0 enabled1这里我用gpgcheck0是因为离线环境下的GPG密钥导入容易出问题稍后单独说明。配置完之后先不要急着装Ambari先跑一条验证命令yum --disablerepo* --enablerepolocal-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校验和主机映射容易被忽略但影响全局的两个细节上文用了gpgcheck0这不是偷懒。离线环境下如果你在同步源时把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的“版本管理”去注册新的VDFVersion 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.jarshdfs:///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 -DincludeScoperuntime这条命令需要在一个pom项目下执行pom里声明carbondata依赖dependency groupIdorg.apache.carbondata/groupId artifactIdcarbondata-spark2_2.11/artifactId version2.0.1/version /dependency如果并没有Maven工程作为载体也可以考虑直接从对应版本的发布包中提取。Apache CarbonData发布归档里通常包含lib/目录里面有预先打好的jar包省去自己打包的麻烦。不过要注意不同CarbonData版本对Spark的兼容性有严格区分比如2.0.x对应Spark 2.31.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.extensionsorg.apache.carbondata.spark.CarbonSparkExtensions spark.sql.hive.metastore.jarsbuiltin spark.kryo.registratororg.apache.carbondata.serializer.CarbonKryoRegistrator spark.serializerorg.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.extensionsorg.apache.carbondata.spark.CarbonSparkExtensions \ --conf spark.sql.catalog.carbondataorg.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字段会对列注释做编码读取如果元数据连接串漏了characterEncodingUTF-8中文被转成GBK存储查询时自然乱码。解决方案是在Ambari的Hive配置的hive-site.xml中将元数据库JDBC URL显式加上javax.jdo.option.ConnectionURLjdbc:mysql://node01:3306/hive?createDatabaseIfNotExisttruecharacterEncodingUTF-8useSSLfalse同时在Spark的spark-defaults.conf里新增spark.sql.session.timeZoneAsia/Shanghai spark.sql.legacy.charVarcharAsStringtrue然后删除旧表重建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/下的缓存度量信息清理后重启Agentrm -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比反复刷新网页要管用得多。这套从零搭建的流程跑通之后后续维护会顺滑很多。至少对我而言现在再看到离线部署的需求心里已经有一张完整的“源准备—组件安装—版本切换—插件集成”的路线图了。