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 检查,应该会多出 ResourceManagerNodeManager 进程。

至此,你的伪分布式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/outputHadoop规定,输出目录在运行前不能存在。

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配置中的端口号。

问题二:运行 hadoop jar 命令时报错 ClassNotFoundExceptionNoClassDefFoundError

  • 可能原因:编译时使用的Hadoop库版本与运行环境的版本不一致,或者打包时未包含用户类。
  • 解决
    1. 确保编译时的 HADOOP_CLASSPATH 设置正确。
    2. 使用 jar tf wordcount.jar 检查打包的JAR文件中是否包含了你的 .class 文件(位于正确的包路径下)。
    3. 在驱动类中,明确设置 job.setJarByClass(WordCountDriver.class);,这有助于Hadoop在集群上分发你的JAR包。

问题三:作业提交后,在YARN Web UI上一直处于 ACCEPTED 状态,不运行。

  • 可能原因:集群资源不足。伪分布式模式下,NodeManager可用的内存和CPU资源可能默认配置过低。
  • 解决:修改YARN资源配置。编辑 $HADOOP_HOME/etc/hadoop/yarn-site.xml,增加或修改以下属性(根据你的机器配置调整,以下是一个示例):
    <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>
    
    修改后需要重启YARN服务:stop-yarn.sh 然后 start-yarn.sh

问题四:HDFS操作(如 -put, -ls)非常慢。

  • 可能原因:DNS解析或网络配置问题。Hadoop会尝试反向解析主机名。
  • 解决
    1. 编辑 /etc/hosts 文件,确保 127.0.0.1 对应 localhost 和你的主机名。
    2. 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分布式计算的分片、并行机制有更直观的理解。

更多推荐