
1. 写在前面数仓环境里的第一道坎数仓项目的学习笔记写到第19篇终于到了把Spark装进虚拟机这一步。前面Hive、HDFS、Zookeeper一个个装下来每个组件都有脾气到了Spark这里你会发现它的安装难度其实排在倒数——但坑一点都不少。网上一搜“尚硅谷数仓搭建”跟着视频敲命令很多人卡在Spark这关不是版本对不上就是启动后Job跑不起来要么就是历史服务死活看不到日志。这篇笔记我整理了自己完整趟过一遍的安装配置过程从版本选型、安装步骤、配置文件逐行说明到提交Job时的三种模式选择、常见报错排查全部按实际动过的命令和改过的文件来写适合正在跟数仓课程、或者自己搭大数据环境练手的人直接对照操作。先说下我做这套环境时的基础CentOS 7.9虚拟机三台node1、node2、node3内存分别4G、3G、3GJDK 1.8.0_271Hadoop 3.1.3Hive 3.1.2Spark选的是3.0.0版本。这套组合我实测下来比较稳后面所有内容都基于这个版本说。需要说明的是Spark不是数仓里唯一的数据处理引擎但它是目前离线数仓用得最多的一套框架MapReduce虽然也能跑数仓任务效率实在太低。学数仓搭建把Spark环境搞定后面跑数仓分层任务、ADS层指标计算都会舒服很多。2. 安装前的准备版本选型与必备组件2.1 Spark与Hadoop版本的匹配问题版本匹配是Spark安装里第一个容易翻车的点。很多人下载最新版Spark就想直接配Hadoop结果提交任务时报出各种奇怪的序列化错误、协议不兼容错误。Spark官方针对不同Hadoop版本提供了对应的预编译包在下载页面可以看到类似spark-3.0.0-bin-hadoop2.7.tgz和spark-3.0.0-bin-hadoop3.2.tgz这样的文件名。数仓里用的Hadoop版本如果是3.1.3建议选择带有hadoop3.2标识的包。有人可能疑惑Hadoop是3.1.3但包是3.2会不会有问题实际上这个标识表示的是Spark编译时所依赖的Hadoop主版本系列3.x系列之间兼容性比较好我实测3.1.3搭配hadoop3.2的Spark包跑示例和后续数仓脚本都没出现问题。如果用的是Hadoop 2.7系列就选hadoop2.7的包这是最稳妥的匹配思路。另外JDK版本也要提前确认。Spark 3.x版本建议JDK 8不要用JDK 11或更高版本我在JDK 11下测试过部分组件运行时会有告警虽然不一定致命但没必要给自己添堵。2.2 先确认好三台机器的互通与免密登录安装之前先把三台虚拟机的/etc/hosts配置好确保机器名和IP的映射正确。我的三台节点配置如下192.168.10.101 node1 192.168.10.102 node2 192.168.10.103 node3同时确认SSH免密登录已经配好从node1向node2、node3执行ssh node2、ssh node3能直接登进去。这一步很关键因为后续Spark启动脚本要通过SSH去各节点拉起Worker进程如果没有免密脚本会卡在密码输入上而且没法自动化。免密配置如果还没做可以按这个流程操作ssh-keygen -t rsa -P -f ~/.ssh/id_rsa ssh-copy-id node1 ssh-copy-id node2 ssh-copy-id node3最后用ssh node3验证一下能直接进去就说明配好了。3. Spark安装从上传到目录规划3.1 下载与解压我习惯把软件统一放在/opt/software目录安装目录放在/opt/module这个目录结构在很多大数据课程里也是标准做法好处是软件包和安装环境分离后期清理升级都很方便。cd /opt/software # 上传spark-3.0.0-bin-hadoop3.2.tgz到该目录 tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/module cd /opt/module mv spark-3.0.0-bin-hadoop3.2 spark把目录名改短成spark纯粹是为了后面写路径时少打几个字符所有配置里引用的路径也更干净。3.2 如果非要自己编译需要知道的事源码编译这套流程我建议学习阶段直接跳过。下载源码包再通过Maven去编译需要联网拉取大量依赖时间成本和出错的概率都远高于直接用预编译包。只有在底层源码被修改、或需要适配特定Hadoop版本时才需要自己编译。数仓学习场景下预编译包完全够用。4. 核心配置逐一拆解Spark配置文件都在/opt/module/spark/conf目录下。刚解压完时目录里只有一堆.template后缀的模板文件需要复制一份出来再改。4.1 spark-env.sh环境变量与参数设置cd /opt/module/spark/conf cp spark-env.sh.template spark-env.sh vi spark-env.sh在文件末尾追加以下内容export JAVA_HOME/opt/module/jdk1.8.0_271 export SPARK_MASTER_HOSTnode1 export SPARK_MASTER_PORT7077 export SPARK_WORKER_CORES2 export SPARK_WORKER_MEMORY2g export SPARK_MASTER_WEBUI_PORT8080逐行说下为什么这么写JAVA_HOME这一行最容易忽略。很多教程默认Spark会去读系统的JAVA_HOME环境变量但如果你把JAVA_HOME只写进了/etc/profile而没有让它全局生效Spark的启动脚本在SSH到其他节点时找不到JDK就会报JAVA_HOME is not set错误。所以保险起见直接在spark-env.sh里写死路径让每个节点都用同一个JDK。SPARK_MASTER_HOST指定了Master运行在哪台机器我这边是node1。Spark在集群模式下会有一个Master节点和多个Worker节点Master负责接收任务、分配资源Worker负责真正干活的Executor进程。SPARK_MASTER_PORT是Master的通信端口默认7077和下面要讲的提交命令里的spark://node1:7077要对应上。SPARK_WORKER_CORES和SPARK_WORKER_MEMORY分别限制每个Worker能提供给Spark的CPU核数和内存大小。这里我建议不要给满。虚拟机内存总共就那么大HDFS的DataNode、NodeManager、Hive的Metastore都在跑Spark如果把自己能用的资源设置得过大反而会把其他进程挤垮。2c2g是一个比较保守且够用的配置。SPARK_MASTER_WEBUI_PORT是Master的Web UI端口可以改成别的但默认8080就行只要没有被其他服务占用。注意一下Hadoop的NameNode Web UI默认端口是9870不要搞混了。4.2 spark-defaults.conf提交任务的默认参数同样的方式复制配置文件cp spark-defaults.conf.template spark-defaults.conf vi spark-defaults.conf在文件末尾追加spark.master spark://node1:7077 spark.executor.memory 1g spark.driver.memory 1g spark.serializer org.apache.spark.serializer.KryoSerializer这里重点说下spark.serializer。Spark默认使用Java自带的序列化机制但性能一般。Kryo序列化器序列化后的数据体积更小、速度更快是生产环境常用的配置。不过Kryo有个特点使用前最好注册需要用到的类否则会有性能回退。对于学习阶段的WordCount和数仓SQL任务不注册也能正常运行先加上这行没坏处后面做调优时再深入研究注册规则。spark.executor.memory的设置要和spark-env.sh里的SPARK_WORKER_MEMORY配合考虑。每个Worker能分配2g内存如果你给每个Executor设置1g则一个Worker上最多能启动2个Executor。如果设置的executor memory大于worker memory任务提交时会直接报错提示内存资源不足。4.3 workers文件指定Worker节点cp workers.template workers vi workers模板文件里原本有localhost这行需要删掉然后写入三台机器的名称node1 node2 node3Master节点本身也可以当Worker用特别是资源紧张的学习环境所以node1也写进去了。这里有个比较隐蔽的问题如果仅写了node2和node3而node1不参与计算那Master所在节点的计算资源会被浪费掉任务执行也会更慢。我的建议是三台都写进去物尽其用。4.4 分发到其他节点配置完成后把整个Spark目录同步到node2和node3cd /opt/module xsync spark如果没有配置xsync脚本直接用scp命令也行scp -r spark node2:/opt/module/ scp -r spark node3:/opt/module/分发完成后在node2和node3上确认一下/opt/module/spark/conf/spark-env.sh里配置的JAVA_HOME路径是否存在。有些时候JDK安装位置不同可能导致node1能启动但node2、node3启动Worker时报JDK找不到的错误。5. 启动集群与验证5.1 启动Master和Worker在node1上执行/opt/module/spark/sbin/start-master.sh /opt/module/spark/sbin/start-workers.sh等几秒钟后分别在node1、node2、node3上执行jps应该能看到如下进程node1Master、Workernode2Workernode3Worker注意start-workers.sh这个脚本会读取workers文件里的节点列表通过SSH逐个启动Worker。所以如果某个节点没启动成功第一件事检查免密登录第二件事检查JAVA_HOME是否写入了spark-env.sh。还有一个操作细节start-all.sh在Spark目录下也可以执行但这个脚本在新版本里会把HDFS的脚本也一起调用如果环境变量里有HADOOP_HOME行为可能变得不可预期。我习惯分开执行start-master.sh和start-workers.sh控制更明确。5.2 通过Web UI确认集群状态启动成功后浏览器访问http://node1:8080。正常情况下能看到Spark Master的Web页面显示Workers列表、每个Worker的CPU和内存资源、以及当前运行的任务。这里有一个容易误判的地方如果页面打不开先别急着怀疑Spark配置用curl http://node1:8080先在本机验证一下再检查虚拟机防火墙状态。我在本地学习环境里一般先执行systemctl stop firewalld把防火墙关掉避免不必要的网络限制。生产环境当然不能这么干但学习环境怎么方便怎么来。5.3 跑通第一个Spark示例启动完成后先跑一个官方自带的示例程序验证环境是否正常/opt/module/spark/bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master spark://node1:7077 \ /opt/module/spark/examples/jars/spark-examples_2.12-3.0.0.jar 10这条命令的意思是向Spark集群提交一个计算圆周率的任务参数10表示生成10个分区来并行计算。如果环境正常终端最后会输出类似Pi is roughly 3.1425914235914235的结果同时Web UI上能看到该任务运行完成的状态。如果提交后任务一直卡在RUNNING状态用jps看看Workers是否都正常存在再用free -h查一下内存很可能是某个Worker的Executor内存没分配出来任务在等待资源。5.4 spark-submit三种模式区别和应用场景spark-submit提交任务时有几种--master参数的选择很多人在这地方懵过--master spark://node1:7077Standalone模式基于Spark自带的资源调度。学习环境用得最多不需要额外依赖YARN。--master yarn把Spark任务提交到Hadoop YARN上进行资源分配。如果在真实集群中同时运行Hive和Spark生产上更常用这种模式因为可以复用Hadoop已有的计算资源。--master local[N]本地模式N表示用几个线程跑。不启动集群就能跑适合开发调试代码逻辑时用。数仓学习阶段建议先是Standalone模式跑通任务理解Executor、Task、Stage这些概念后再去尝试YARN模式。我实际把两种模式都跑过一遍后发现理解了Standalone的资源调度流程后YARN模式的理解成本会低很多——两者本质都是“申请资源—启动Executor—运行Task—回收资源”这套流程。6. 配置Spark历史服务日志保存与查看6.1 为什么需要历史服务Spark Web UI只显示当前正在运行的任务任务一旦结束页面上的信息就消失了。数仓中跑SQL、跑指标计算任务执行完想看之前的运行情况、看某个Stage的耗时、看GC耗时都离不开历史服务。6.2 配置步骤历史服务的核心思路是任务运行时的日志写入指定目录可以是HDFS也可以是本地文件系统历史服务启动后读取这些日志并展示在Web页面上。配置步骤如下第一步在HDFS上创建日志目录hdfs dfs -mkdir -p /spark-history第二步修改spark-defaults.conf追加spark.eventLog.enabled true spark.eventLog.dir hdfs://node1:8020/spark-history spark.history.fs.logDirectory hdfs://node1:8020/spark-history注意这里的hdfs://node1:8020要和Hadoop的core-site.xml里配置的NameNode RPC地址一致。很多新手在这里配错把8020写成9000导致历史服务启动后读取不到任何日志。第三步复制并修改spark-history-server配置cp spark-defaults.conf.template spark-defaults.conf不对这里是目录下面还需要复制一个history相关的配置模板cp /opt/module/spark/conf/spark-defaults.conf.template /opt/module/spark/conf/spark-defaults.conf # 此时上面已经配置过确认包含上述属性即可之后启动历史服务/opt/module/spark/sbin/start-history-server.sh浏览器访问http://node1:18080就能看到历史服务页面。这里注意端口是18080和Master Web UI的8080不一样别搞混了。6.3 历史服务常见的日志读不到问题之前使用时遇到过历史服务启动成功、但页面始终看不到任何任务记录的情况。排查步骤是先跑一个Spark任务然后执行hdfs dfs -ls /spark-history如果目录下有eventLog_xxx文件说明任务日志写成功了问题出在历史和Spark之间的读取配置不匹配。这时逐一核对spark.eventLog.dir和spark.history.fs.logDirectory是否一致、能否访问。如果目录下没有文件说明日志压根没写进去优先检查spark.eventLog.enabled是否为true再检查HDFS路径是否写对。我在本地配置时卡过一次原因是HDFS路径写成了hdfs://node1:9000但集群的NameNode端口是8020日志写入失败历史服务自然什么都读不到。7. 提交一个真正的数仓任务WordCount与SQL场景7.1 用WordCount验证完整链路环境搭建完毕、历史服务配置好之后建议跑一个WordCount来验证完整链路。这里不只是跑通程序还要验证日志能写入历史服务、Web UI能看到完整的Job信息。我之前准备了一个打包好的jar包里面包含WordCount的类和依赖放在/opt/software/sparktest.jar。提交命令如下/opt/module/spark/bin/spark-submit \ --class com.example.WordCount \ --master spark://node1:7077 \ /opt/software/sparktest.jar \ hdfs://node1:8020/input/word.txt \ hdfs://node1:8020/output/wc_result在提交之前先在HDFS上准备好输入文件hdfs dfs -mkdir -p /input hdfs dfs -put /opt/software/word.txt /input/跑完后到http://node1:8080和http://node1:18080上分别确认任务记录。完整链条需要表现为job运行成功、输出目录正常生成、历史服务页面有记录可查。7.2 数仓场景下的Spark SQL任务提交方式在数仓的后续操作里大部分时间是通过Spark SQL来跑SQL任务不是写Java代码。启动Spark SQL命令行的方式是/opt/module/spark/bin/spark-sql \ --master spark://node1:7077 \ --executor-memory 1g \ --driver-memory 1g进入spark-sql之后可以执行标准的SQL语句CREATE DATABASE IF NOT EXISTS dwd; USE dwd; SELECT * FROM ods_user_info LIMIT 10;这里给个提醒spark-sql和Hive的元数据是两套体系除非在spark-site.xml里配置了Hive的Metastore地址否则Spark SQL里建的表和Hive里建的表互不可见。数仓项目中如果需要Spark和Hive共用元数据需要在spark-defaults.conf里追加spark.sql.catalogImplementationhive javax.jdo.option.ConnectionURLjdbc:mysql://node1:3306/hive?createDatabaseIfNotExisttrue javax.jdo.option.ConnectionDriverNamecom.mysql.jdbc.Driver javax.jdo.option.ConnectionUserNameroot javax.jdo.option.ConnectionPassword123456这个是数仓搭建里很容易踩坑的地方。我在第一次做数仓练习时用spark-sql查Hive的表一直报Table not found后来才发现是元数据没打通。如果只是单纯练Spark可以不接Hive元数据但既然在做数仓打通是必须的。8. Spark安装配置中常见问题与排查实录8.1 版本不匹配导致的序列化或连接异常现象提交任务时Executor启动失败日志中提示类似java.lang.NoSuchMethodError或SerializationException。原因Spark预编译包与Hadoop版本不一致或者JDK版本过高。处理确认Hadoop版本后重新选择对应的预编译包同时用java -version确认JDK是8。我自己的经验是遇到这类报错不要花时间去改代码或者加参数先排查版本矩阵配合错误的概率最高。8.2 JAVA_HOME is not set现象启动Worker时终端输出该错误进程没有起来。原因spark-env.sh里没有显式指定JAVA_HOME或该路径在部分节点上不存在。处理统一在spark-env.sh里写死JDK路径然后同步到所有节点。执行ssh node2 ls /opt/module/jdk1.8.0_271确认目录存在。这个问题我在Workers节点上复现过好几次概率最高的原因就是只改了node1的配置忘记同步node2和node3。8.3 ExecutorLostFailure或Container killed现象任务运行几秒后失败日志中看到ExecutorLostFailure或Container killed by YARN for exceeding memory limits。原因最常见的是Worker内存配得太小或者一个Worker上启动了多个Executor导致内存溢出。另外HDFS和Spark同时抢内存也会造成容器被物理内存限制杀死。处理调整spark-env.sh里的SPARK_WORKER_MEMORY和spark-defaults.conf里的spark.executor.memory让总内存占用控制在实际物理内存的三分之二以内。我一开始给Worker配了3g结果node2、node3物理内存只有3g运行数仓SQL时直接把系统内存吃满机器直接卡死后来改成2g才稳定下来。8.4 端口冲突现象Master或Web UI无法启动日志提示地址已被占用。原因8080端口被其他服务占用比如有些工具也喜欢用8080。处理可以用netstat -tlnp | grep 8080查看占用情况或者干脆把SPARK_MASTER_WEBUI_PORT改成8088。注意8088通常被YARN的ResourceManager使用如果Hadoop在运行改的时候要避开这些已知端口。8.5 Spark Shell能跑Spark Submit失败现象spark-shell中执行sc.textFile没问题但spark-submit提交jar包时报找不到主类或其他类加载错误。原因大概率是jar包问题。如果用Maven打的包没有包含依赖或者主类路径写错都会出现这个问题。另外还要确认jar包确实上传到了指定路径很多人在本地打包后忘了传到虚拟机。处理用jar tf 包名.jar | grep 主类名可以快速确认主类是否存在如果是依赖缺失用mvn package时加上shade插件重新打包或者提交时用--jars带上依赖。8.6 报错信息太长从哪里开始看Spark的报错日志非常长尤其是提交阶段。我个人的习惯是先看最后20行通常能看到异常的核心原因再看Caused by后面的部分那里往往写着真正的根因。如果日志里出现了Exception和Error一起来优先看最后一个Caused by。这个习惯帮我省过不少时间比在几百行日志里翻来翻去有效得多。9. 一点心得体会数仓搭建的过程Spark安装配置只是众多环境步骤中的一个但它的重要性在于这次装好了后面整条数仓链路的数据处理、指标计算都跑在它上面。装的过程中遇到报错不要慌绝大多数问题都集中在版本匹配、内存配置、JAVA_HOME路径、端口这几个点上。我建议新手装完Spark之后一定要跑通一个WordCount、再看一眼历史服务页面确认整个链路通畅了再往下走。不少人在装完环境后就直接开始跑数仓任务结果任务失败时到底是环境问题还是代码问题都分不清排查起来会非常痛苦。另外环境搭建类的内容非常依赖实际操作光看文章理解不了里面的细节。我写的每个配置项都建议自己手动改一遍、启动一遍、提交一个任务验证一遍踩坑之后再来对照这篇文章你会理解得更深。祝各位数仓搭建顺利Spark跑起来不再翻车。