从零搭建个人大数据集群:Spark安装与Standalone实战
从零开始搭建个人大数据集群6——SPARK安装个人大数据集群搭建系列走到第6篇终于轮到Spark了。前面我们搞定了存储和资源调度HDFS里能存数据YARN上能跑MapReduce任务但说实话MapReduce那套开发和调试体验是真的磨人。写个WordCount都要继承一堆类提交一次任务好几分钟迭代一版改个逻辑又得重新打包上传。所以这篇我们给集群加装Spark把内存计算的威力引进来日常的数据清洗、分析、临时查询都能快好几个量级。这篇适合已经完成前面基础组件安装、准备让集群具备真正的计算能力的朋友阅读。我会从版本选型讲起到目录规划、配置文件、启动验证再到我踩过的几个典型坑全程基于个人集群的实际环境三台物理机一个Master两个Worker操作系统是常见的Linux分发版本Java和Hadoop环境已就绪。装完Spark你就能直接用它跑SQL、写PySpark脚本也能作为独立计算框架跑算法任务。个人集群的Spark安装其实没那么多玄学关键是把版本对应关系和内存规划想清楚其他都是机械操作。1. 版本选型装之前先把三件事想清楚1.1 为什么我选Spark 3.3.2Spark的版本迭代这两年非常快3.0出来以后就一路狂奔3.1、3.2、3.3、3.4、3.5到2024年4.0都进入预览阶段了。个人集群装哪个版本很多人第一反应是装最新版但真不是越新越好。我选3.3.2是考虑了三件事稳定性、生态兼容性、参考资料完整度。3.3.x这个版本发布于2022年中下旬到现在经历了大量生产环境的检验网上踩坑案例、博客、社区回复都非常充足。一个人折腾集群最大的成本不是安装而是出问题之后能不能快速找到答案。你装个最新刚发布的版本遇到报错去搜可能连Stack Overflow上都只有两三条讨论心态会很崩。而3.3.2可以刚好避开这个问题它有新版本的核心能力比如Adaptive Query Execution已经成熟又不至于老到跟新版Hadoop生态产生兼容问题。再一个是Python版本兼容。Spark 3.3系列对Python 3.6到3.9支持良好还能兼容3.10。如果你像我一样日常用PySpark比较多这个兼容区间很舒服。过于新的版本有时候会对Python版本有严格限制反而带来不必要的麻烦。1.2 Hadoop版本和Spark预编译包的对应关系这就是个人集群安装Spark最容易翻车的地方。Spark有源码包也有预编译包预编译包还针对不同Hadoop版本做了不同分支。下载页面上一堆选项很容易眼花。关键的区分点是标识hadoop2的包对应旧版Hadoop 2.x标识hadoop3的包对应Hadoop 3.xwithout-hadoop的包是把Hadoop相关依赖完全抽离靠外部环境提供个人集群如果是新搭建的大概率装的是Hadoop 3.x那就直接选spark-3.3.2-bin-hadoop3这个系列。我见过不止一个朋友套件版本是Hadoop 3.3.x结果SPARK包下成了hadoop2版本启动没问题一提交任务就各种NoSuchMethodError查来查去最后才发现是包选错了。还有一种特殊情况是without-hadoop包它适合那些想严格复用现有Hadoop环境的场景。使用这种包需要在Spark的配置里显式指定HADOOP_HOME和相关依赖路径稍麻烦一些。对于个人集群我不建议用这个变体。预编译的hadoop3版本已经内嵌了所有Spark运行时需要的Hadoop客户端依赖跟外部的HDFS、YARN通信完全没障碍省心。1.3 部署模式选standalone还是yarn个人集群怎么选Spark支持多种部署模式本地模式、Standalone、YARN、Kubernetes、Mesos等。个人集群最常用的两个选择是YARN模式和Standalone模式。两者没有绝对的优劣取决于你想怎么用。YARN模式的意思是Spark任务跑在已有的YARN资源调度体系上跟MapReduce共享同一个资源池。好处是资源统一管理不会出现一个框架把集群资源全占了的情况。代价是配置较复杂涉及HADOOP_CONF_DIR设置、Spark和YARN的交互调优排查问题时链路更长。Standalone模式是Spark自带的简易集群管理器由一个Master进程和若干Worker进程组成安装配置极简资源管理逻辑清晰一眼能看懂非常适合个人集群。个人集群通常不会有几百个并发任务抢资源的情况Standalone完全够用。而且它自带Web UI端口8080集群状态、任务执行情况一目了然对学习和调试都很友好。我的建议如果是自己搭着学技术、跑日常分析先用Standalone。等真正需要跟历史MapReduce任务共享集群的时候再配置YARN模式也不迟。一套Spark安装包两种模式都能启用并不冲突。2. 下载与目录规划干净的第一步省掉后面很多事2.1 下载哪个具体文件确定版本之后去官网下载页面。找到spark-3.3.2-bin-hadoop3.tgz这个文件体积大概290MB左右。注意别下带src标识的源码包源码包还需要额外执行Maven构建费时费力完全没必要。下载命令可以直接在Master节点执行cd /opt wget https://archive.apache.org/dist/spark/spark-3.3.2/spark-3.3.2-bin-hadoop3.tgz2.2 目录结构与解压后的软链接我把所有大数据组件统一放在/opt目录下并且坚持用软链接。什么意思就是解压后实际目录名带版本号比如spark-3.3.2-bin-hadoop3然后创建一个不带版本号的软链接指向它比如这样tar -zxvf spark-3.3.2-bin-hadoop3.tgz -C /opt ln -s /opt/spark-3.3.2-bin-hadoop3 /opt/spark为什么要多此一举因为以后的升级、换版本都只需要改软链接指向配置文件里的路径完全不用动。这种小细节在学习阶段觉得无所谓等集群组件多了以后体会到的好处会翻倍。2.3 环境变量与JAVA_HOME定位在所有节点Master和Worker上都配置环境变量编辑/etc/profile.d/spark.shexport SPARK_HOME/opt/spark export PATH$SPARK_HOME/bin:$SPARK_HOME/sbin:$PATH配置完执行source /etc/profile.d/spark.sh让环境变量生效。这里有个容易忽略的点Spark进程定位Java不是靠系统的java命令而是强制读取JAVA_HOME。很多报错找不到Java或者Unsupported class file major version根子就是JAVA_HOME没设好或者指向了错误的JDK版本。我的集群里用的是JDK 1.8也就是Java 8这在Spark 3.x中是受支持的。有人用JDK 11也没问题。但如果你之前装的Hadoop就需要某个特定JDK版本Spark的JAVA_HOME解析最好跟它保持一致避免两个框架混用不同JDK版本导致的奇怪问题。2.4 同步与分发三台机器的目录结构必须一致。最省事的办法是把解压好的Spark目录直接用scp分发到各Worker节点scp -r /opt/spark-3.3.2-bin-hadoop3 rootworker1:/opt/ scp -r /opt/spark-3.3.2-bin-hadoop3 rootworker2:/opt/分发后分别在Worker节点建立软链接。注意环境变量文件也要一并分发确保每一台机器的SPARK_HOME都能正确解析。这一步做完先别急着启动集群我们还有几个配置文件要处理。3. 配置文件与启动Standalone集群的关键字段Spark的配置核心在$SPARK_HOME/conf目录下。模板文件都在我们要基于模板生成实际配置。3.1 三个最重要的配置文件第一个是spark-env.sh由模板spark-env.sh.template复制而来。这个文件主要设置JVM、环境变量和集群级别的资源参数。我的配置如下# 指定Java环境 export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 # 指定Master节点主机名 export SPARK_MASTER_HOSTmaster # 每个Worker上分配的CPU核数 export SPARK_WORKER_CORES4 # 每个Worker上分配的可用内存 export SPARK_WORKER_MEMORY8g # 每个Worker上可运行的最大Executor数量 export SPARK_WORKER_INSTANCES2第二个是workers文件由workers.template复制而来。注意文件名在新版本里通常是workers老版本是slaves别搞混了。内容很简单一行一个Worker节点主机名master worker1 worker2这里我写了一个我自己的选择把Master也作为Worker加入集群。个人集群只有三台机器如果Master只做调度不干活计算资源直接少三分之一非常不划算。但如果你后续要跑的生产负载比较重建议Master与其他节点职责分离否则Master既要跑Driver又要跑Executor压力会很大。第三个是spark-defaults.conf由模板复制而来。这个文件配置Spark任务的默认行为spark.master spark://master:7077 spark.driver.memory 2g spark.driver.cores 2 spark.executor.memory 2g spark.executor.cores 2 spark.serializer org.apache.spark.serializer.KryoSerializer spark.sql.shuffle.partitions 43.2 资源参数背后的计算逻辑这部分很多新人容易一台机器给多少就配多少完全不考虑和Worker层级的关系后面就容易出问题。SPARK_WORKER_MEMORY设置的是Worker进程能用来分配给所有Executor的总量上限而spark.executor.memory设置的是单个Executor的内存大小。如果把SPARK_WORKER_MEMORY降到不合理的低值比如2g那无论你的Executor参数填多大Worker也分配不出来。打个比方Worker是一个房东它手里的房源总量是固定的SPARK_WORKER_MEMORY。Executor是租客每个租客签的合同spark.executor.memory决定了它能租多大面积。房东手里总共只有8g就不可能同时签十个2g的租客。我机器是16G内存。系统本身要占用一部分NameNode、DataNode、HDFS相关进程还要各占一部分所以给Spark Worker的8g已经是个相对宽裕的数值。同时设了SPARK_WORKER_INSTANCES2意思是每台机器上起两个Worker进程每个进程4g。这样是有好处的即使一个Worker进程异常崩溃另一个还能继续接受任务减小单点故障对集群的冲击。你如果内存紧张完全可以只设一个Worker实例不设这个参数即可。3.3 启动顺序与校验先启动Master再启动workers。使用sbin/start-all.sh脚本可以一步到位$SPARK_HOME/sbin/start-all.sh这个脚本会读取workers文件通过SSH免密登录到各节点逐一启动Worker进程。假如之前Hadoop配置时已经打通了Master到各Worker的SSH免密这里就不用再做额外设置。如果没打通就得先配好否则启动会卡在密码输入处。启动后检查进程jpsMaster上应该能看到Master进程Worker节点上能看到Worker进程。然后打开浏览器访问http://master:8080能看到Web UI页面显示集群的总内存、总CPU核数、活跃Worker列表。到这里Spark Standalone集群就算起来了。之前我每次装完都会犯一个懒既然Web UI正常就直接开始跑任务。但真正严谨的做法是核对页面上的Total Resources和你的配置参数是否一致。如果页面上的总内存跟期望值差很多基本都是某个Worker的配置没生效或者Worker进程没有正常注册。4. 验证集群跑单词统计还不够很多教程验证Spark安装会直接跑spark-shell写一段WordCount代码。这只能证明Spark进程能启动证明不了集群真的在工作。我习惯用两种方式来验证一是跑一次正式的spark-submit任务二是用Python脚本模拟一个贴近实际的小任务。4.1 用自带样例做提交验证Spark自带了一些example JAR包是最稳的验证材料。先看它在不在ls $SPARK_HOME/examples/jars/spark-examples*.jar如果打包时没有包含样例JAR可以在解压包中找。有它的话我们直接用spark-submit跑一个官方范例例如计算圆周率$SPARK_HOME/bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master spark://master:7077 \ --executor-memory 2g \ --total-executor-cores 4 \ $SPARK_HOME/examples/jars/spark-examples*.jar 100这个任务会用蒙特卡洛方法估算圆周率参数100表示模拟的次数规模。任务跑起来后日志里会出现很多INFO信息最终在结尾附近能看到类似Pi is roughly 3.142233的输出。到这里至少说明Spark集群能被客户端正确连接Master成功调度任务Worker拉起了ExecutorExecutor之间的Shuffle通信没有问题。把这段跑通之后可以再跑一个每个节点都参与计算的、带数据读取的验证任务$SPARK_HOME/bin/spark-submit \ --class org.apache.spark.examples.SparkWordCount \ --master spark://master:7077 \ $SPARK_HOME/examples/jars/spark-examples*.jar \ hdfs://master:8020/user/test_dir/data.txt这个任务会读HDFS上的文件做词频统计。如果输出成功就说明Spark不仅集群内部通信正常连和HDFS的对接也没有问题。我现在跑集群这一条基本是必做的相当于给整个链条做了个全链路体检。4.2 用PySpark模拟一个贴近实际的小任务既然个人集群常用于跑数据分析我建议做一次PySpark层面的验证贴近之后的真实用法。这里写了一个小脚本test_spark.py作用是读一个CSV文件按某个字段分组做一个简单的count聚合from pyspark.sql import SparkSession spark SparkSession.builder \ .appName(pyspark_smoke_test) \ .master(spark://master:7077) \ .getOrCreate() # 造一点测试数据 data [(zhang, 1), (zhang, 2), (li, 3), (li, 4), (wang, 5)] df spark.createDataFrame(data, [name, value]) df.groupBy(name).count().show() spark.stop()执行方式$SPARK_HOME/bin/spark-submit test_spark.py如果能看到按name分组的结果输出说明Python开发的链路通了。这个脚本虽然简单但已经覆盖了SparkSession初始化、数据分片分发到各Worker、结果聚合返回、Driver端收集结果这几个核心环节。个人开发的话还可以在spark-defaults.conf里把spark.master默认设好这样跑PySpark脚本时连--master都不用带代码里也不用手写master地址更接近生产环境的使用习惯。4.3 从Web UI推断配置生效情况Spark的Web UI不止一个页面。除了主页面展示集群资源以外任务运行时点进某个Active Application能看到每个Executor的日志、Stages的进度。我经常用这个页面检查每个任务的Executor个数是否和预期一致每个Executor的内存和核数是否对得上数据Shuffle的量是否异常巨大如果数据量很小但Shuffle很大说明分区设置不合理这些判断不用太早依赖命令行页面上的信息就足够直观。5. 个人集群上的Spark排错笔记五个经典事故Spark安装本身不难难的是装好以后跑任务时零零碎碎暴露的问题。我把自己实际踩过的五个坑记录下来每一个都对应一个具体的现象你可以对照排查。5.1 Executor无法启动worker内存配置与Executor内存的关系我先遇到的一个典型故障是任务提交后一直卡在WAITING状态点开Web UI看Executor个数始终是0没有任何Executor被创建。查看日志发现反复出现“Not enough space to launch executor”或“Failed to allocate memory for executor”的英文提示。逐层排查后定位到是自己把spark.executor.memory设成了4gSPARK_WORKER_MEMORY也设成了4gWorker上还有Master进程、NameNode、DataNode占用根本腾不出4g给Executor。Executor申请内存时Spark还会额外预留一部分开销默认是Executor内存的10%左右叫spark.executor.memoryOverhead所以实际需要的内存比参数值更大。调整思路很简单把spark.executor.memory调低到2g同时适当减小Worker上其他组件的内存占用。这个坑最大的教训是不要只看一个参数要把整台机器所有的常驻内存进程加起来算一笔总账。5.2 所有任务都卡在“resolving”阶段hostname解析问题有一次所有任务提交后都卡在等待状态Web UI上显示一个Executor都没有而且Master日志中一直出现“Failed to include worker”或者类似字样。这就不是内存的问题而是Worker向Master注册时失败了大概率是hostname反解出了问题。个人集群为了省事很多人会在/etc/hosts里写死IP和主机名的映射这本身没有错。但如果某个节点的/etc/hostname和/etc/hosts中的配置不一致网络通信时就可能出现一台机器用A名字注册、另一台机器用B名字回话导致对不上。我的经验是所有节点的主机名都用纯短名称不要带域名后缀/etc/hosts里统一写清每个节点的IP和短名对应关系同时在spark-env.sh里用SPARK_MASTER_HOST明确指定Master的hostname把潜在歧义消除掉。改完这些以后start-all.sh重启整个集群问题就消失了。5.3 java.lang.NoClassDefFoundError目录结构与环境变量不一致某次在worker2上启动Worker进程时日志里报出一堆NoClassDefFoundError指向某个Spark核心类。第一反应是JAR包损坏重新解压一遍还是不行。后来发现不是包的问题而是worker2上的SPARK_HOME指向了一个不存在的路径。原来我之前只在worker2上解压了带版本号的目录但忘记做软链接环境变量依然指向/opt/spark。由于Spark的启动脚本依靠SPARK_HOME定位所有类和配置这个路径一旦失效整个进程就完全起不来。这个问题的排查方法就是在出问题的节点上执行echo $SPARK_HOME ls -l $SPARK_HOME/conf jps一对比路径问题马上就暴露了。很基础但实际出问题时手忙脚乱就容易忽略。5.4 任务结果正确但写入HDFS报错写权限与临时目录还有一次跑WordCount最后结果collect到Driver端正常但把结果写到HDFS时报Permission denied。原因很朴实当前用户对目标目录没有写权限HDFS的权限控制默认开启。解决办法有两个hdfs dfs -chmod -R 777 /user/当前用户或者在spark-defaults.conf里给每个任务指定HDFS目录归属。个人集群一般直接建好用户目录然后把owner改成自己就行。这类问题跟Spark本身关系不大更多是HDFS权限带来的连锁反应排查时要学会分辨报错是属于Spark层还是HDFS层的。另外还遇到过临时目录写满的情况。Spark执行shuffle时会写大量临时文件到spark.local.dir指定的目录默认是系统临时目录/tmp。如果系统盘空间不大跑大任务很容易把它占满。安装时就要提前规划好在spark-env.sh里加一行export SPARK_LOCAL_DIRS/data/spark-local,/tmp让自己准备一个独立的磁盘空间来做shuffle落盘既避免占满系统盘又能提升I/O性能。5.5 版本不匹配产生的诡异报错最后聊一个比较隐蔽的问题如果前面阶段不是全新建的集群之前有过旧版本的Spark或不同版本依赖你在同一个Spark目录里跑了多个不同版本项目的任务可能会遇到版本不兼容的怪异问题。比如Spark 2.4的代码直接扔到Spark 3.3的环境里很多API已经废弃编译期看不出问题运行期却会报错。我自己遇到过最典型的例子之前用Spark 2.4跑过一个结构化流任务后来升级到3.3后没有重新编译就直接提交结果在Runtime出现了关于“MicroBatchExecution”的类找不到或方法签名不匹配的问题。排查了半天最终的根源就是旧代码依赖打包时指向了旧版本Spark API杀头的不是版本而是代码打包时用的Spark版本和运行时Spark版本不一致。解决这个问题的方法是统一依赖用3.3.2对应的依赖重新构建应用。如果你在写项目前不确定当前集群的Spark版本可以在应用代码里打印print(spark.version)或者在客户端执行$SPARK_HOME/bin/spark-submit --version确认提交的代码版本与集群版本兼容避免神秘报错浪费大量时间。6. 按需扩展启动模式和其他组件的小建议Spark装完不算结束很多朋友装完就急着跑大量任务。个人集群的意义在于你随时可以上手实验不必为资源调度担忧。接下来还有几个值得深挖的方向。6.1 开启动态资源分配Standalone模式下默认是固定资源占用任务提交时申明多少资源整个运行期就占着不放。对于个人集群这种任务密度不高的场景其实可以开启动态资源分配让任务结束后迅速释放资源给其他任务spark.dynamicAllocation.enabledtrue spark.dynamicAllocation.minExecutors0 spark.dynamicAllocation.maxExecutors6 spark.shuffle.service.enabledtrue注意开启动态分配时需要额外部署Shuffle External Shuffle Service否则Executor销毁后shuffle文件就丢了。如果是纯学习用途不大规模并行跑任务也可以不开保持默认也足够。6.2 历史服务与SQL的引入Spark自带的HistoryServer可以把任务的历史日志持久化到HDFS或本地目录重启集群后依然能看到之前的任务记录。个人集群一旦开了这个功能排查问题的体验会提升一个档次$SPARK_HOME/sbin/start-history-server.sh另外我建议后续装一个Spark SQL ThriftServer这样你可以直接拿类似数据库客户端的工具连接Spark跑标准SQL查询体验不比查询一个数据库差。做数据分析和临时探索非常方便。当然这属于进阶玩法先把核心安装和任务提交跑顺了再逐步扩展不迟。6.3 与现有HDFS、YARN的配合方式Spark只用Standalone模式时和YARN之间的关系是并行的——这是两套独立的资源调度系统。如果你想在Spark里访问HDFS的数据直接写hdfs://master:8020/...路径就能读因为Spark内置了Hadoop客户端。但如果你在YARN模式下跑Spark就需要确保每台机器都有HADOOP_CONF_DIR环境变量指向Hadoop的配置文件目录这样Spark应用提交后Spark Executor才能正确访问Hadoop集群的服务。个人集群我的建议是先保持Standalone专注体验Spark本身的计算抽象和优化机制。等对资源管理和任务提交的流程消化透了再切到YARN上跑几天你会更深刻地理解两个调度框架各自擅长的领域。两套模式都跑通集群的使用弹性会非常大。Spark装好以后这台个人集群的主要价值才真正释放出来。从前期的数据存储到现在的内存计算算是一个里程碑。接下来你可以考虑继续搭建数据仓库工具或者接入调度框架让集群能真正干活。如果安装过程中遇到上面没覆盖的问题也可以从Web UI的日志入手一层层剥开看大部分问题都能在里面找到线索。