Hadoop实战:从零搭建大数据环境到运行第一个MapReduce程序(Linux版)
Hadoop实战:从零搭建大数据环境到运行第一个MapReduce程序(Linux版)
很多朋友对大数据技术心向往之,但往往被“集群”、“分布式”这些概念吓退,觉得门槛太高。其实,搭建一个能跑起来的Hadoop环境,并没有想象中那么复杂。今天,我们就抛开厚重的理论课本,直接动手,在Linux系统上从零开始,一步步搭建一个伪分布式Hadoop集群,并亲手编写、运行一个经典的词频统计MapReduce程序。这个过程,你会遇到配置文件的修改、环境变量的设置、服务的启动与验证,甚至是一些常见的“坑”。别担心,我会把每一步的操作、背后的原理以及踩坑后的解决方案都讲清楚。我们的目标很简单:让你在终端敲下最后一条命令,看到自己程序的输出结果时,能真切地感受到,大数据处理的核心引擎,已经在你手中成功启动了。
1. 环境准备与基础配置
在开始安装Hadoop之前,我们需要一个干净、稳定的Linux环境。我强烈建议使用一台独立的虚拟机进行操作,这能避免污染你的主力开发环境,也方便随时回滚。Ubuntu Server 20.04 LTS 或 CentOS 7/8 都是不错的选择,它们拥有广泛的社区支持和稳定的软件源。这里我以Ubuntu 20.04为例进行演示。
首先,确保你的系统是最新的。打开终端,执行以下命令更新软件包列表并升级现有软件:
sudo apt update && sudo apt upgrade -y
Hadoop是使用Java编写的,因此Java Development Kit (JDK) 是必须的。Hadoop 3.x 版本通常需要 JDK 8 或 JDK 11。我们将安装 OpenJDK 11,它是开源且广泛兼容的。
sudo apt install openjdk-11-jdk-headless -y
安装完成后,验证Java是否安装成功,并确认JAVA_HOME环境变量的位置至关重要,因为Hadoop的启动脚本依赖于此。
java -version
# 输出应类似:openjdk version "11.0.xx" ...
接下来,找到JDK的安装路径。一个可靠的方法是使用 update-alternatives 命令:
update-alternatives --config java
在输出中,你会看到类似 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 的路径。JAVA_HOME 需要的是其上级目录,即去掉末尾的 /bin/java。所以在这个例子中,JAVA_HOME 应该是 /usr/lib/jvm/java-11-openjdk-amd64。
现在,我们需要将Java环境变量配置写入shell的配置文件中,以便每次登录都自动生效。编辑 ~/.bashrc 文件:
nano ~/.bashrc
在文件末尾添加以下几行,请将 JAVA_HOME 的路径替换为你实际查到的路径:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export PATH=$PATH:$JAVA_HOME/bin
保存并退出编辑器(在nano中按 Ctrl+X,然后按 Y,最后按 Enter)。让配置立即生效:
source ~/.bashrc
最后,创建一个专门的用户来运行Hadoop服务虽然不是必须的,但这是一个好习惯,可以避免使用root权限带来的潜在风险。
sudo adduser hadoopuser
# 按照提示设置密码和其他信息(可直接回车跳过)
# 将当前用户(或你自己)添加到sudo组,并切换到新用户环境
sudo usermod -aG sudo hadoopuser
su - hadoopuser
至此,一个纯净的、配备了Java环境的基础Linux系统就准备好了。接下来,我们将迎来主角——Hadoop。
2. Hadoop安装与伪分布式模式配置
Hadoop的安装包可以从Apache官网的镜像站点直接下载。我们选择当前稳定的3.3.x版本。首先,切换到我们为Hadoop创建的用户(如果上一步已切换,可忽略),并进入用户主目录进行操作。
cd ~
wget https://downloads.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
下载完成后,解压压缩包到合适的目录。通常我们会将其放在 /usr/local 或用户主目录下。这里我们选择解压到 /usr/local。
sudo tar -xzf hadoop-3.3.6.tar.gz -C /usr/local
sudo mv /usr/local/hadoop-3.3.6 /usr/local/hadoop
sudo chown -R hadoopuser:hadoopuser /usr/local/hadoop
现在,需要配置Hadoop的环境变量。编辑 ~/.bashrc 文件,在之前Java配置的后面,追加Hadoop的配置:
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export YARN_HOME=$HADOOP_HOME
同样,执行 source ~/.bashrc 使配置生效。然后,我们可以通过一个简单命令验证Hadoop是否已加入路径:
hadoop version
如果看到Hadoop版本信息输出,说明基础安装成功。
注意:伪分布式模式是Hadoop的一种运行方式,它在一台机器上模拟一个小型集群,所有守护进程(NameNode, DataNode, ResourceManager, NodeManager)都运行在同一台机器上。这对于学习和测试来说非常完美,因为它保留了分布式系统的所有特性,而无需多台物理机器。
接下来进入核心环节:配置Hadoop。所有配置文件都位于 $HADOOP_HOME/etc/hadoop/ 目录下。我们需要修改以下几个关键文件:
1. hadoop-env.sh: 设置Java环境。
nano $HADOOP_HOME/etc/hadoop/hadoop-env.sh
找到 export JAVA_HOME= 这一行,取消注释,并将其值设置为之前确定的JDK路径,例如:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
2. core-site.xml: 配置Hadoop核心参数,主要是定义默认的文件系统FS和临时目录。
nano $HADOOP_HOME/etc/hadoop/core-site.xml
在 <configuration> 标签内添加:
<property>
<name>fs.defaultFS</name>
<value>hdfs://localhost:9000</value>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>/home/hadoopuser/hadoopdata/tmp</value>
</property>
这里 fs.defaultFS 指定了HDFS的访问地址和端口。hadoop.tmp.dir 是Hadoop许多守护进程存储临时数据的目录,务必确保该路径存在且有写入权限(可以用 mkdir -p 创建)。
3. hdfs-site.xml: 配置HDFS相关参数,主要是副本因子。
nano $HADOOP_HOME/etc/hadoop/hdfs-site.xml
在 <configuration> 标签内添加:
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>/home/hadoopuser/hadoopdata/namenode</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>/home/hadoopuser/hadoopdata/datanode</value>
</property>
在伪分布式模式下,由于只有一台机器,我们将副本因子 dfs.replication 设置为1。同时,我们显式指定了NameNode和DataNode存储元数据和实际数据块的目录。
4. mapred-site.xml: 配置MapReduce框架,指定其运行在YARN上。
nano $HADOOP_HOME/etc/hadoop/mapred-site.xml
添加配置:
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
5. yarn-site.xml: 配置YARN资源管理器。
nano $HADOOP_HOME/etc/hadoop/yarn-site.xml
添加配置:
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<property>
<name>yarn.nodemanager.env-whitelist</name>
<value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_HOME,PATH,LANG,TZ</value>
</property>
配置文件修改完成后,在启动服务前,有一个至关重要的步骤:配置SSH免密登录到localhost。因为Hadoop的脚本需要通过SSH来启动不同节点上的守护进程,即使在同一台机器上。
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
现在,尝试 ssh localhost,应该可以直接登录而无需密码。
3. HDFS初始化与集群服务启动
配置妥当后,我们正式进入启动阶段。首先,需要格式化HDFS的NameNode。请注意,格式化操作会清空NameNode上所有的元数据,仅在第一次安装时执行,或者需要彻底重置集群时执行。
hdfs namenode -format
看到提示 “Storage directory /home/hadoopuser/hadoopdata/namenode has been successfully formatted” 类似的成功信息即可。
现在,可以启动HDFS相关的守护进程了。Hadoop提供了便捷的脚本:
start-dfs.sh
这个脚本会依次启动NameNode、DataNode和SecondaryNameNode。你可以用 jps 命令(Java进程查看工具)来检查它们是否成功运行:
jps
你应该能看到类似以下的进程:
XXXXX NameNode
XXXXX DataNode
XXXXX SecondaryNameNode
XXXXX Jps
接下来,启动YARN资源管理框架:
start-yarn.sh
再次使用 jps 检查,应该会多出 ResourceManager 和 NodeManager 进程。
至此,你的伪分布式Hadoop集群已经全部启动完毕。我们可以通过Web UI来直观地查看集群状态,这比命令行更友好。
- HDFS NameNode Web UI: 在浏览器中访问
http://你的服务器IP:9870。这里你可以看到HDFS的整体概况,包括存储容量、活跃节点数、文件系统浏览等。 - YARN ResourceManager Web UI: 访问
http://你的服务器IP:8088。这里管理着所有提交的MapReduce作业,可以查看作业状态、日志和集群资源使用情况。
在终端里,我们也可以使用HDFS shell命令来与文件系统交互。让我们创建一个用户目录并上传一个测试文件:
hdfs dfs -mkdir -p /user/hadoopuser
hdfs dfs -mkdir /user/hadoopuser/input
# 创建一个本地文本文件
echo -e "Hello World\nHello Hadoop\nThis is a test file for Hadoop MapReduce" > ~/test.txt
# 上传到HDFS
hdfs dfs -put ~/test.txt /user/hadoopuser/input/
# 查看HDFS上的文件
hdfs dfs -ls /user/hadoopuser/input
如果以上命令都能成功执行,并且Web UI也能正常访问,那么恭喜你,一个功能完整的Hadoop伪分布式环境已经搭建成功,并且HDFS文件系统工作正常。接下来,我们就可以在这个“小集群”上运行自己的大数据处理程序了。
4. 编写与运行第一个MapReduce程序:词频统计
MapReduce是Hadoop的核心编程模型,其思想是“分而治之”。一个经典的入门例子就是词频统计(WordCount)。它统计输入文本中每个单词出现的次数。我们将分别编写Mapper、Reducer和驱动主程序。
首先,确保你位于Hadoop用户的主目录,并创建一个项目目录:
cd ~
mkdir -p wordcount/src
cd wordcount/src
1. 编写Mapper类 (WordCountMapper.java):
Mapper的任务是读取输入的文本行,将其拆分成单词,并为每个单词输出一个中间键值对 <word, 1>。
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> {
// 定义常量‘1’,避免在map函数中反复创建对象,提升性能
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
// 将一行文本拆分成单词
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
// 输出中间键值对:<单词, 1>
context.write(word, one);
}
}
}
2. 编写Reducer类 (WordCountReducer.java):
Reducer接收Mapper输出的所有相同单词的键值对列表(例如 <"Hello", [1, 1]>),然后将这些值相加,得到该单词的总出现次数,输出最终结果 <word, totalCount>。
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
@Override
public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
// 遍历同一个单词对应的所有‘1’,进行求和
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
// 输出最终结果:<单词, 总次数>
context.write(key, result);
}
}
3. 编写驱动主类 (WordCountDriver.java):
这个类负责配置和提交MapReduce作业到YARN集群。它定义了使用哪个Mapper和Reducer,输入输出路径等。
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCountDriver {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: WordCountDriver <input path> <output path>");
System.exit(-1);
}
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "Word Count");
// 设置包含主类的Jar包
job.setJarByClass(WordCountDriver.class);
// 设置Mapper和Reducer类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 设置Mapper和Reducer的输出键值类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 如果Mapper和Reducer的输出类型一致,可以只设置最终输出类型
// 否则需要单独设置Map输出类型:job.setMapOutputKeyClass/ValueClass
// 设置输入和输出路径,来自命令行参数
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 提交作业并等待完成
boolean success = job.waitForCompletion(true);
System.exit(success ? 0 : 1);
}
}
4. 编译与打包
我们需要将Java源代码编译成.class文件,然后打包成JAR文件,以便提交到Hadoop集群运行。首先,设置CLASSPATH,让javac能找到Hadoop的库文件。
export HADOOP_CLASSPATH=$(hadoop classpath)
cd ~/wordcount
javac -cp "$HADOOP_CLASSPATH" -d . src/*.java
编译成功后,当前目录会生成对应的 .class 文件。现在将它们打包成JAR:
jar -cvf wordcount.jar *.class
5. 运行MapReduce作业
现在,万事俱备。我们使用 hadoop jar 命令来提交作业。输入路径是我们在HDFS上创建的 /user/hadoopuser/input,输出路径指定一个新的目录,例如 /user/hadoopuser/output。Hadoop规定,输出目录在运行前不能存在。
hadoop jar wordcount.jar WordCountDriver /user/hadoopuser/input /user/hadoopuser/output
提交后,你会在终端看到作业执行的日志滚动输出。也可以打开YARN的Web UI (http://IP:8088) 实时监控作业进度。作业完成后,查看结果:
hdfs dfs -cat /user/hadoopuser/output/part-r-00000
你应该能看到类似以下的输出:
Hadoop 2
Hello 2
This 1
World 1
a 1
file 1
for 1
is 1
MapReduce 1
test 1
恭喜!你已经成功在自行搭建的Hadoop集群上运行了第一个MapReduce程序。这个过程虽然步骤不少,但每一步都有其明确的目的。从环境准备、服务配置到程序编写、作业提交,你完整地体验了一个大数据处理任务的生命周期。
5. 常见问题排查与基础性能调优
第一次搭建和运行,难免会遇到各种问题。这里我汇总了几个最常见的“坑”及其解决方案,希望能帮你快速排雷。
问题一:启动HDFS时,jps 看不到 NameNode 或 DataNode 进程。
- 可能原因1:SSH免密登录未配置成功。
- 检查:运行
ssh localhost是否仍需密码。 - 解决:确保
~/.ssh/authorized_keys文件权限为600,并包含正确的公钥。可尝试重新生成密钥对并追加。
- 检查:运行
- 可能原因2:配置文件有语法错误。
- 检查:查看日志文件,通常位于
$HADOOP_HOME/logs/目录下,查看对应进程(如hadoop-hadoopuser-namenode-*.log)的日志。 - 解决:仔细核对
core-site.xml,hdfs-site.xml等文件的XML格式,确保所有标签闭合,属性值正确。
- 检查:查看日志文件,通常位于
- 可能原因3:端口被占用。
- 检查:NameNode默认使用9000和9870端口。使用
netstat -tulnp | grep :9000检查。 - 解决:杀死占用进程或修改Hadoop配置中的端口号。
- 检查:NameNode默认使用9000和9870端口。使用
问题二:运行 hadoop jar 命令时报错 ClassNotFoundException 或 NoClassDefFoundError。
- 可能原因:编译时使用的Hadoop库版本与运行环境的版本不一致,或者打包时未包含用户类。
- 解决:
- 确保编译时的
HADOOP_CLASSPATH设置正确。 - 使用
jar tf wordcount.jar检查打包的JAR文件中是否包含了你的.class文件(位于正确的包路径下)。 - 在驱动类中,明确设置
job.setJarByClass(WordCountDriver.class);,这有助于Hadoop在集群上分发你的JAR包。
- 确保编译时的
问题三:作业提交后,在YARN Web UI上一直处于 ACCEPTED 状态,不运行。
- 可能原因:集群资源不足。伪分布式模式下,NodeManager可用的内存和CPU资源可能默认配置过低。
- 解决:修改YARN资源配置。编辑
$HADOOP_HOME/etc/hadoop/yarn-site.xml,增加或修改以下属性(根据你的机器配置调整,以下是一个示例):
修改后需要重启YARN服务:<property> <name>yarn.nodemanager.resource.memory-mb</name> <value>2048</value> </property> <property> <name>yarn.scheduler.minimum-allocation-mb</name> <value>512</value> </property> <property> <name>yarn.nodemanager.resource.cpu-vcores</name> <value>2</value> </property>stop-yarn.sh然后start-yarn.sh。
问题四:HDFS操作(如 -put, -ls)非常慢。
- 可能原因:DNS解析或网络配置问题。Hadoop会尝试反向解析主机名。
- 解决:
- 编辑
/etc/hosts文件,确保127.0.0.1对应localhost和你的主机名。 - 在
core-site.xml中,可以尝试关闭DNS反向解析(生产环境慎用):<property> <name>hadoop.security.token.service.use_ip</name> <value>true</value> </property>
- 编辑
除了解决问题,了解一些基础的调优思路也能提升你的学习和使用体验。对于伪分布式环境,调优空间有限,但以下原则在真实集群中至关重要:
- HDFS块大小: 在
hdfs-site.xml中通过dfs.blocksize设置。对于海量大文件,增大块大小(如256MB或512MB)可以减少NameNode元数据压力和管理开销。对于大量小文件,应考虑使用HAR(Hadoop Archives)或SequenceFile进行合并。 - MapReduce任务数量: Map任务数量通常由输入数据的总大小和块大小决定。Reduce任务数量可以通过
job.setNumReduceTasks(int n)在驱动程序中显式设置。合理的Reduce任务数能避免数据倾斜和单个Reducer负载过重。 - JVM重用: 对于大量小任务的作业,开启JVM重用(在
mapred-site.xml中设置mapreduce.job.jvm.numtasks)可以避免为每个任务都启动和销毁一个JVM,从而减少开销。 - Combiner的使用: 如果Reduce操作满足结合律(如求和、求最大值),可以在Map端使用Combiner进行本地聚合,这能显著减少Mapper到Reducer的网络传输数据量。在我们的WordCount例子中,因为Reducer就是求和,所以完全可以设置
job.setCombinerClass(WordCountReducer.class);。
纸上得来终觉浅,绝知此事要躬行。我建议你在成功运行基础程序后,可以尝试修改输入文件的内容和大小,观察作业运行时间的变化;或者尝试修改Reducer的数量,看看输出文件(part-r-xxxxx)的数量如何变化。这些小小的实验能让你对Hadoop分布式计算的分片、并行机制有更直观的理解。
更多推荐
所有评论(0)