最近在Linux上把Hadoop 3.1.3和Spark 3.4.4整套环境从零配了一遍,配合Python 3跑PySpark,前前后后折腾了几天。不少朋友也问过我这个组合怎么搭,尤其是PySpark3和Hadoop集群的对接问题,今天就把整个过程整理出来,包括版本选择、配置细节、启动验证和生产中容易踩的坑,给需要的同学一份可以直接照着做的检查单。
这套环境说难不难,说简单也不简单。难在版本匹配和细节配置上,比如Java版本、Python解释器路径、Hadoop和Spark的CLASSPATH互相依赖,都是容易卡住的地方。简单在思路只要理清,按照步骤走基本能一路通关。下面我按实际操作的顺序,从上到下讲一遍。
1. 整体思路与版本选型
1.1 为什么选择 Hadoop 3.1.3 + Spark 3.4.4
Hadoop 3.1.3是Apache Hadoop 3.x里一个非常稳定的基础版本,HDFS和YARN的功能都已经很成熟,很多生产环境用的就是这个版本。Spark 3.4.4则属于Spark 3.4系列的最新维护版本,对PySpark的Python API支持相当完整,包括pandas UDF、Structured Streaming等功能在3.4版本里都有明显改进。
有人会担心Spark官方下载页写的是"Pre-built for Apache Hadoop 3.3",会不会和Hadoop 3.1.3不兼容?实际上这里的"3.3"指的是Spark自带的Hadoop客户端库版本,而不是说你必须运行一个Hadoop 3.3的集群。Spark 4.x之前的设计都是通过Hadoop Client连接外部集群,只要你的HDFS和YARN对外提供的协议兼容,3.1.3完全能配合Spark 3.4.4工作。我自己测试过HDFS读写、YARN资源申请,都没有问题。
选这个组合还有一个实际原因:不少公司现有集群就是Hadoop 3.x早期版本,而团队想用更新的Spark来做数据处理。你不可能把整个Hadoop集群升级,所以在Spark侧做适配是更现实的做法。这个组合既能吃到Spark新版本的红利,又不用动Hadoop那摊庞大的底层设施,性价比很高。
1.2 核心组件与架构关系
先理清各组件之间的关系,这样才能在配置时知道每条路径、每个变量的意义。Hadoop负责两件事:存储和管理文件,也就是HDFS;资源调度和任务分配,也就是YARN。Spark则是一个计算引擎,本身不存数据,它跑在HDFS之上,从HDFS读数据,算完之后再写回HDFS。YARN在这里可以看作是Spark的"房东",Spark应用以Container的形式租用YARN上的CPU和内存资源。
PySpark是Spark为Python用户提供的一套API。你在Python里写from pyspark import SparkContext,PySpark会通过Py4J在背后启动一个JVM,把你的Python代码翻译成JVM指令去执行。所以这套环境里Python解释器、Java JDK、Spark本来要用到的Scala库,三者是缺一不可的。这就是为什么很多人在Spark环境里遇到"Python not found"或者"JAVA_HOME没有设置"这类错误,正是因为这三者的关联没配置正确。
2. 环境准备与前置条件
2.1 Linux系统基础配置
我这次用的系统是Ubuntu 22.04,CentOS 7序列的原理也完全一样,只是包管理命令不同。硬件上建议内存至少8GB,最好16GB,因为NameNode、DataNode、ResourceManager再加上Spark进程,至少要占用4~6GB内存。
首先更新系统源并安装基础工具:
sudo apt update sudo apt install -y openssh-server rsync vim curl wget net-tools然后建议单独创建一个非root用户,比如叫hadoop,因为Hadoop和Spark的守护进程都不建议用root跑,防止误操作和权限问题。
sudo useradd -m -s /bin/bash hadoop sudo passwd hadoop sudo usermod -aG sudo hadoop su - hadoop配置SSH免密登录,这一步很关键,后面start-dfs.sh启动HDFS时会通过SSH分发命令到各个节点。即使是单机环境,也要把本机localhost做免密,否则会卡在输入密码的交互上。
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys做完后测试ssh localhost,应该不需要密码就能登录。这一步基本上每个新手都会被坑一次,我第一次配的时候忘了做免密,启动脚本一直停在那里,还以为是网络问题。
2.2 Java JDK 安装与配置
Hadoop 3.1.3官方要求Java 8或Java 11,Spark 3.4.4官方支持Java 8/11/17。为了最大兼容性,我用的是OpenJDK 1.8,这个版本经过大量生产环境验证,踩雷概率最低。装JDK的时候注意不要用系统自带的很老的openjdk,最好指定版本。
sudo apt install -y openjdk-8-jdk java -version然后配置JAVA_HOME,写入~/.bashrc:
echo 'export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64' >> ~/.bashrc echo 'export PATH=$PATH:$JAVA_HOME/bin' >> ~/.bashrc source ~/.bashrc如果你的Java安装在别的路径,用readlink -f $(which java)找到真实路径再填进去。这里有个容易忽略的点:Spark和Hadoop的脚本都会直接读JAVA_HOME环境变量,如果你在/etc/profile里设置了,但当前Shell没有重新加载,就还是找不到。所以我每次都习惯把这两个export写到~/.bashrc里,因为普通登录Shell默认会加载它。
2.3 Python 3 环境准备
PySpark要求Python 3.6及以上,实测3.8、3.10、3.11都能跑。Ubuntu 22.04自带Python 3.10,可以直接用。但为了环境隔离,我建议装一个虚拟环境,特别是在机器上同时有其他项目的时候。
这里给出基于venv的做法:
sudo apt install -y python3-venv python3-pip python3 -m venv ~/spark_env source ~/spark_env/bin/activate稍后配置PySpark时要让Spark知道用哪个Python解释器。因为PySpark启动时会执行python、python3等命令,如果你有多个Python,必须显式指定。这个我放到后面Spark配置一节单独说,因为它是PySpark3能否正常工作的关键。
3. Hadoop 3.1.3 部署与配置
3.1 下载安装与目录规划
到Apache Hadoop官网下载hadoop-3.1.3.tar.gz,或者使用国内镜像加快速度。下载后解压到/opt下,然后做一个软链接,方便后续切换版本。
wget https://archive.apache.org/dist/hadoop/common/hadoop-3.1.3/hadoop-3.1.3.tar.gz sudo tar -zxvf hadoop-3.1.3.tar.gz -C /opt sudo ln -s /opt/hadoop-3.1.3 /opt/hadoop sudo chown -R hadoop:hadoop /opt/hadoop接着设置Hadoop环境变量,同样写入~/.bashrc:
export HADOOP_HOME=/opt/hadoop export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin这里面的HADOOP_CONF_DIR很重要,Spark在连接HDFS时会从这里读取core-site.xml等配置。所以后面配置Spark时,要么显式把HADOOP_CONF_DIR列入CLASSPATH,要么让它自己去找。
还要在$HADOOP_HOME/etc/hadoop/hadoop-env.sh里明确JAVA_HOME,因为Hadoop的启动脚本在通过SSH远端执行时,不一定能继承你Shell里的JAVA_HOME变量。打开这个文件,找到JAVA_HOME=那行,取消注释并改成:
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64这一步经常被忽略,结果就是本地跑命令正常,但start-dfs.sh启动在各个节点上一会儿JVN就报错。
3.2 核心配置文件详解
Hadoop的配置都在$HADOOP_HOME/etc/hadoop目录下,单机模式只需要改三个文件:core-site.xml、hdfs-site.xml、yarn-site.xml。
core-site.xml主要设置NameNode的地址和临时目录:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/opt/hadoop/data/tmp</value> </property> </configuration>hdfs-site.xml设置NameNode和DataNode的数据存储目录,以及副本数。单机环境副本数设为1:
<configuration> <property> <name>dfs.namenode.name.dir</name> <value>/opt/hadoop/data/namenode</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/opt/hadoop/data/datanode</value> </property> <property> <name>dfs.replication</name> <value>1</value> </property> </configuration>yarn-site.xml设置ResourceManager和NodeManager的基本参数:
<configuration> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.nodemanager.aux-services.mapreduce_shuffle.class</name> <value>org.apache.hadoop.mapred.ShuffleHandler</value> </property> </configuration>这三组配置里的路径都建议放到/opt/hadoop/data下,而不是默认的/tmp,因为/tmp系统重启会清空,数据目录就没了,又要重新格式化,很坑。另外目录权限必须是当前运行用户有读写权限,我用的是hadoop用户。
3.3 格式化NameNode并启动HSDFS/YARN
在第一次启动之前,必须格式化NameNode,这会初始化文件系统的元数据:
hdfs namenode -format看到successfully formatted字样就算成功。然后启动HDFS:
start-dfs.sh再启动YARN:
start-yarn.sh用jps命令检查进程,正常情况下应该看到:
NameNode DataNode SecondaryNameNode ResourceManager NodeManager有时候jps本身没有安装,用sudo apt install -y openjdk-8-jdk-headless会顺便装上,或者ps aux | grep java来看。如果某个进程缺失,去对应日志文件里查原因,日志路径在$HADOOP_HOME/logs/。
3.4 验证HDFS与YARN功能
先看Web UI,NameNode默认在9870端口,ResourceManager默认在8088端口。用浏览器打开http://localhost:9870,能看到NameNode状态和DataNode列表,说明HDFS启动成功。打开http://localhost:8088,能看到YARN集群资源和运行中的应用。
命令行验证更直接:
hdfs dfs -mkdir -p /user/hadoop hdfs dfs -put /etc/hostname /user/hadoop/ hdfs dfs -cat /user/hadoop/hostname如果mkdir报错,多半是端口9000没监听,或者NameNode没有启动。检查netstat -tlnp | grep 9000。另外YARN验证可以运行一个示例:
yarn jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar pi 2 10能输出PI的近似值,说明YARN调度、NodeManager、MapReduce都正常。这一步做完,Hadoop侧就完全OK了。
4. Spark 3.4.4 安装与PySpark环境集成
4.1 下载Spark与选择适合的包
Spark下载页面提供了多个预编译选项,比如Pre-built for Apache Hadoop 3.3和Pre-built with user-provided Apache Hadoop。我推荐选后者,也就是spark-3.4.4-bin-without-hadoop.tgz,因为这样可以彻底避免Hadoop客户端库版本冲突。Spark会根据HADOOP_CONF_DIR和CLASSPATH去连接你实际安装的Hadoop集群。
当然,直接用spark-3.4.4-bin-hadoop3.3.tgz也可以,它能自动识别外部HDFS。但为了避免某些Jar包版本不一致导致的NoClassDefFoundError,我还是用without-hadoop包,把CLASSPATH交给Hadoop自己管理,这样最干净。
wget https://archive.apache.org/dist/spark/spark-3.4.4/spark-3.4.4-bin-without-hadoop.tgz sudo tar -zxvf spark-3.4.4-bin-without-hadoop.tgz -C /opt sudo ln -s /opt/spark-3.4.4-bin-without-hadoop /opt/spark sudo chown -R hadoop:hadoop /opt/spark然后设置Spark环境变量,写入~/.bashrc:
export SPARK_HOME=/opt/spark export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin4.2 配置spark-env.sh
Spark的核心配置文件是$SPARK_HOME/conf/spark-env.sh,它默认不存在,先把模板复制一份:
cp $SPARK_HOME/conf/spark-env.sh.template $SPARK_HOME/conf/spark-env.sh编辑这个文件,至少要配置以下几项:
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 export HADOOP_HOME=/opt/hadoop export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export SPARK_DIST_CLASSPATH=$(/opt/hadoop/bin/hadoop classpath) export PYSPARK_PYTHON=/home/hadoop/spark_env/bin/python export PYSPARK_DRIVER_PYTHON=$PYSPARK_PYTHON这里解释一下每项的作用。HADOOP_CONF_DIR告诉Spark去哪里找HDFS的core-site.xml,这样spark命令里的hdfs://localhost:9000路径才能被解析。SPARK_DIST_CLASSPATH是Spark连接HDFS、YARN所需的Hadoop类路径,用hadoop classpath命令生成,比自己手动拼Jar包靠谱得多。PYSPARK_PYTHON是执行器的Python解释器路径,PYSPARK_DRIVER_PYTHON是驱动端的路径,两者通常一致。
需要注意,如果你把Python放在虚拟环境里,那么PYSPARK_PYTHON必须指向该虚拟环境的python绝对路径。因为Spark的worker在远端相同的路径上寻找Python解释器,如果路径写错或者该路径在别人的机器上不存在,就会报python: not found。实际生产中,最好让所有节点Python安装路径一致。
4.3 PySpark Python绑定与JVM内存
配置好spark-env.sh后,建议先用一个简单的命令行测试PySpark能否启动,同时观察JVM和Python进程的情况:
$SPARK_HOME/bin/pyspark --master local[2]正常的话会进入一个Python交互式Session,屏幕上有Welcome to Spark横幅。这个过程中,你会注意到后台其实有一个Java进程在运行,这就是PySpark通过Py4J启动的JVM。如果这里一直报错,多半是JAVA_HOME或SPARK_DIST_CLASSPATH没有设置对。
对于内存配置,可以在spark-env.sh里额外设置:
export SPARK_DRIVER_MEMORY=2g export SPARK_EXECUTOR_MEMORY=2g如果机器内存有限,建议先调小,防止启动时OOM。这块我建议用默认值测试,真正跑大任务时再通过--executor-memory参数或者spark-defaults.conf调整。
PySpark还涉及Python worker的内存,Spark会默认从执行器内存中划一部分给Python worker,也可以在spark-env.sh里设置spark.executor.pyspark.memory来控制。这个参数在处理超大DataFrame时比较关键,不然容易出现Python侧内存溢出。
4.4 连接HDFS测试PySpark读写
进入PySpark交互环境后,先试试HDFS连通性。创建一个测试文件并写入HDFS,然后通过PySpark读取:
rdd = sc.textFile("hdfs://localhost:9000/user/hadoop/test.txt") rdd.count()如果返回文件行数,说明Spark已经能访问HDFS。再测试写入:
rdd.saveAsTextFile("hdfs://localhost:9000/user/hadoop/output")然后去HDFS上确认内容是否存在。这里的重点是hdfs路径前要加hdfs://localhost:9000,不然Spark会认为路径是本地文件。如果你在core-site.xml里已经把fs.defaultFS配好了,也可以简写为/user/hadoop/test.txt,Spark会自动补全scheme。
这一步能通,就说明PySpark和Hadoop集群已经完整打通。之后再想用Spark on YARN模式,只要保证YARN配置正确,用--master yarn --deploy-mode client就能把Spark任务提交到YARN上运行。
5. 实操验证与常见问题排查
5.1 运行第一个PySpark任务:WordCount实战
作为对这个环境的最终验收,我跑了一个最经典的WordCount。在PySpark里开一个Python脚本文件wordcount.py:
from pyspark.sql import SparkSession spark = SparkSession.builder.appName("PySparkWordCount").getOrCreate() sc = spark.sparkContext lines = sc.textFile("hdfs://localhost:9000/user/hadoop/input.txt") words = lines.flatMap(lambda line: line.split(" ")) pairs = words.map(lambda word: (word, 1)) counts = pairs.reduceByKey(lambda a, b: a + b) output = counts.collect() for (word, count) in output: print(f"{word}: {count}") spark.stop()先用hdfs dfs -put上传一个input.txt到HDFS,然后用spark-submit提交:
$SPARK_HOME/bin/spark-submit --master local[2] wordcount.py运行结束后,终端会打印每个单词的出现次数。这里有一个很小的细节:Spark对空字符串的处理,如果你用默认的split(" "),空行会生成空字符串,导致计数里多出空值。生产环境我会用split("\\s+")并过滤掉空项:
words = lines.flatMap(lambda line: line.split("\\s+")).filter(lambda w: w != "")这也是我第一次写WordCount时踩到的小坑,明明文本很简单,结果多了一堆空串。
5.2 常见错误与解决方案
我把自己实际遇到过的问题整理成一张速查表,方便各位对照排查:
| 错误现象 | 常见原因 | 解决办法 |
|---|---|---|
| jps没有NameNode | 未格式化或NameNode崩溃 | 运行hdfs namenode -format后重启 |
| start-dfs.sh卡住 | SSH免密配置缺失 | 按2.1节设置localhost免密 |
| 连接localhost:9000拒绝 | core-site.xml的fs.defaultFS错误 | 检查地址和端口,确认NameNode监听 |
| PYSPARK报python: not found | PYSPARK_PYTHON路径不对 | 用which python确认路径并更新spark-env.sh |
| Spark连接HDFS报NoClassDefFoundError | 缺少Hadoop类路径 | 设置SPARK_DIST_CLASSPATH=$(hadoop classpath) |
| YARN Web UI访问不了 | 防火墙或ResourceManager未启动 | 检查8088端口,看ResourceManager日志 |
| Worker OOM | 资源分配过小 | 调大spark.executor.memory,或减小worker数量 |
| HDFS DataNode目录权限错误 | hadoop.tmp.dir/data目录权限不足 | 保证属主是当前用户,并给予读写权限 |
| Spark运行Map任务特别慢 | 同一主机HDFS走loopedback | 配置/etchosts,将主机名映射到内网IP,而不是127.0.0.1 |
最后一条特别容易忽略。默认情况下localhost映射到127.0.0.1,但这会导致DataNode之间走回环网络,数据传输性能骤降。正确做法是在/etc/hosts里把主机名指向实际的内网IP,比如192.168.1.100 master,这样HDFS的节点间通信才能真正走网卡。
5.3 我的踩坑经验总结
配置这套环境的过程中,我最大的体会就是"版本匹配"并不是最难的,难在把各种配置路径和环境变量让每个进程都能正确继承。Java环境、Python环境、Hadoop配置、Spark配置,任何一个环节脱节,后面就会出现诡异的错误。
一个很实际的经验是,所有配置尽量写在~/.bashrc和spark-env.sh里,不要只写在当前终端。因为系统Service脚本和spark-submit在提交任务时的Shell上下文可能完全不同,特别是通过SSH和crontab启动的时候,环境变量会丢失。另外,每次修改hadoop-env.sh或spark-env.sh后,要确保重启所有相关进程,不要图省事只重开一个pyspark,有时候CLASSPATH缓存还在,改了等于白改。
还有一点,我在生产环境配置时会刻意避开系统自带的Python,因为系统升级往往会动它,造成PySpark的路径突然失效。创建一个独立的venv,或者在/usr/local/bin下放一个稳定的Python解释器,是更稳妥的做法。
最后想说,PySpark的调试思路和普通Python程序很不一样。普通Python报错直接看堆栈,PySpark有些错误是JVM抛出来的,要在Spark日志里找关键信息。遇到问题不要慌,先看$SPARK_HOME/logs和$HADOOP_HOME/logs,里面几乎都能找到根因。这套环境搭好之后,后续跑数据处理、机器学习特征工程都会顺手很多,尤其是Spark 3.4对Python 3的兼容性已经非常成熟,作为大数据入门和日常开发环境完全够用。