☰
Docker Compose搭建Hadoop集群并跑通WordCount实战
2026/10/17 5:43:14 网站建设 项目流程

最近有个大数据环境相关的测试需求:要快速验证一套三节点的Hadoop集群,还要跑通经典的WordCount用例。按老思路开几台虚拟机的话,内存和磁盘都是问题;买云主机又没必要,毕竟只是临时测试。最后我选择用Docker在单机上通过Compose编排一主两从的结构,整个过程花了一个小时左右就跑起来了,WordCount也顺利出结果。

这篇文章就把整个流程从头到尾拆开讲,包括基础镜像怎么打造、容器网络怎么铺、Hadoop的四个核心配置文件怎么改、NameNode格式化有什么坑、WordCount怎么提交怎么验结果,最后是一份常见问题速查表。适合两类人看:一类是刚开始接触Hadoop、想有个真实集群环境练手的入门开发者;另一类是已经在用Docker、但没试过把Hadoop集群容器化的后端工程师。

1. 整体方案设计:为什么选择Docker Compose而不是虚拟机

1.1 我的目标场景

我要的是一个最少三节点的Hadoop集群:一个节点承担NameNode和ResourceManager,另外两个节点承担DataNode和NodeManager。这样虽然说是“分布式”,但所有容器都落在同一台宿主机上,既保留了多节点之间的网络通信和任务调度逻辑,又不会像真机集群那样需要维护多套系统环境。

数据规模也不大,主要为了跑MapReduce作业,验证HDFS的上传下载、YARN的资源调度、容器间通信这些核心机制。既然目标是“快速搭建、用完即丢”,那么可重复性是硬指标。Docker正好把环境打包成镜像,任何机器上拉下来就能跑,完全不用重新配一遍JDK和Hadoop。

1.2 方案对比

用虚拟机建集群是很多教程里的经典方案,但虚拟机吃资源太厉害。三台CentOS虚拟机加在一起,内存轻松占掉6GB以上,启动还要等操作系统完完整整跑起来,整体体验非常沉重。云主机方案同样面临成本和等待问题,虽然性能隔离更好,但对本地测试来说有点杀鸡用牛刀。

Docker方案在资源占用和启动速度上有天然优势。每个容器只携带JDK、Hadoop运行环境,总共也就一两个GB的空间占用,启动速度以秒计。再加上Compose可以把网络、端口、环境变量、挂载目录全部声明式写清楚,任何新机器上执行完命令就能得到一模一样的集群,这正好命中测试环境最需要的“可重置、可复现”能力。当然代价也有:容器不完全等同于物理机器,比如内核参数、磁盘IO隔离会弱一些,但跑WordCount这种轻量级作业完全够用。

2. 基础镜像制作:JDK11 + Hadoop3.3.6 + SSH免密

2.1 版本选型

Hadoop 3.3版本对JDK的要求是Java 8或Java 11。我选了JDK 11,因为新一些的组件生态对JDK 11支持更友好,而且Ubuntu 20.04的软件源里直接有openjdk-11-jdk-headless,安装省事。Hadoop版本选了3.3.6,这是目前比较稳定的小版本,Common、HDFS、MapReduce都包含在同一个tar包里,用起来最顺手。

另外很重要的一点:Hadoop脚本通过SSH远程连接到各节点启动守护进程,所以镜像里必须装好OpenSSH Server,同时做好root用户的免密登录。很多从零搭建教程会把这步藏在细节里,但恰恰是集群启动失败的高发区。

2.2 Dockerfile完整解析

我直接在Ubuntu基础镜像上定义当前环境的构建文件,先把JDK和SSH装上,再下载Hadoop安装包解压到/opt,最后配置SSH免密。

FROM ubuntu:20.04 ENV DEBIAN_FRONTEND=noninteractive ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 ENV HADOOP_VERSION=3.3.6 ENV HADOOP_HOME=/opt/hadoop ENV PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin RUN apt-get update && apt-get install -y --no-install-recommends \ openjdk-11-jdk-headless \ openssh-server \ openssh-client \ curl \ vim \ && rm -rf /var/lib/apt/lists/* RUN curl -fSL \ "https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/hadoop-${HADOOP_VERSION}.tar.gz" \ -o /tmp/hadoop.tar.gz \ && tar -xzf /tmp/hadoop.tar.gz -C /opt \ && ln -s /opt/hadoop-${HADOOP_VERSION} ${HADOOP_HOME} \ && rm /tmp/hadoop.tar.gz RUN ssh-keygen -q -t rsa -N "" -f /root/.ssh/id_rsa \ && cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys \ && chmod 600 /root/.ssh/authorized_keys \ && printf "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile=/dev/null\n LogLevel ERROR\n" > /root/.ssh/config RUN mkdir -p /opt/hadoop/data/name /opt/hadoop/data/data /opt/hadoop/logs RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \ && sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config EXPOSE 22 8020 9000 9870 8088 19888 CMD ["/usr/sbin/sshd", "-D"]

这里有几个细节需要注意。首先,ssh-keygen生成密钥后必须把公钥写入authorized_keys,而且权限不能错,否则从master节点SSH到worker节点时会卡在密码确认上。其次,StrictHostKeyChecking一定要禁用,否则第一次连接会出现一个交互式确认,而Hadoop的启动脚本是无人值守的,只要出现这个确认,进程就会一直挂着。最后,/opt/hadoop/data下的name和data目录在format后会自动生成内容,但目录本身需要提前创建好。

2.3 构建镜像与基础验证

镜像构建命令很简单:

docker build -t my-hadoop:3.3.6 .

构建完成后可以顺手进去验证一下基础环境:

docker run --rm -it my-hadoop:3.3.6 bash

进入容器后执行:

java -version hadoop version ssh-keygen -l -f /root/.ssh/id_rsa.pub

三条命令都能正常输出,说明JDK、Hadoop、SSH密钥三件套已经就绪。这里强调一下:镜像里的authorized_keys是所有容器共用的,也就是说每个容器内都有其他容器的公钥,互信关系天然成立。这比容器启动之后再互相拷贝公钥要省事得多。

3. 用Compose编排一主两从:网络与容器配置

3.1 自定义桥接网络与主机名解析

Docker容器各自有独立IP,但IP是动态分配的,重启后可能变化。为了避免在配置文件里写死IP,我选择让Docker Compose创建自定义网络,并显式设置每个容器的hostname。在自定义网络中,容器之间可以直接通过hostname互相访问,这就解决了Hadoop集群里最基础的主机名解析问题。

我的规划是三个容器,分别叫master、worker1、worker2。master对外暴露HDFS的Web界面9870端口、YARN的Web界面8088端口,方便在宿主机上直接用浏览器查看集群状态。worker节点不映射端口到宿主机,只在Docker网络内部提供数据存储和计算资源。

3.2 docker-compose.yml完整配置

编排文件如下:

version: "3" services: master: image: my-hadoop:3.3.6 container_name: hadoop-master hostname: master networks: - hadoop-net ports: - "9870:9870" - "8088:8088" - "19888:19888" volumes: - ./hadoop-conf:/opt/hadoop/etc/hadoop environment: - HDFS_NAMENODE_USER=root - HDFS_DATANODE_USER=root - HDFS_SECONDARYNAMENODE_USER=root - YARN_RESOURCEMANAGER_USER=root - YARN_NODEMANAGER_USER=root command: ["/usr/sbin/sshd", "-D"] worker1: image: my-hadoop:3.3.6 container_name: hadoop-worker1 hostname: worker1 networks: - hadoop-net environment: - HDFS_NAMENODE_USER=root - HDFS_DATANODE_USER=root - HDFS_SECONDARYNAMENODE_USER=root - YARN_RESOURCEMANAGER_USER=root - YARN_NODEMANAGER_USER=root command: ["/usr/sbin/sshd", "-D"] worker2: image: my-hadoop:3.3.6 container_name: hadoop-worker2 hostname: worker2 networks: - hadoop-net environment: - HDFS_NAMENODE_USER=root - HDFS_DATANODE_USER=root - HDFS_SECONDARYNAMENODE_USER=root - YARN_RESOURCEMANAGER_USER=root - YARN_NODEMANAGER_USER=root command: ["/usr/sbin/sshd", "-D"] networks: hadoop-net: driver: bridge

这里最需要关注的是environment里的那批HDFS_*_USER和YARN_*_USER变量。因为容器默认使用root用户,而Hadoop 3.x版本的脚本出于安全考虑,默认不允许root直接启动守护进程。如果不把这些环境变量声明为root,执行start-dfs.sh时会直接报“Attempting to operate on hdfs as root”这种错误。这是很多新手第一次启动失败的头号原因。

另外,我把Hadoop配置目录挂载到了宿主机的./hadoop-conf上。这样一来,改配置不需要进容器编辑,直接在宿主机上用vim改完,再在容器里重启服务即可。当然前提是镜像内的/opt/hadoop/etc/hadoop目录存在,如果挂载本地目录为空,需要先手动把镜像里已有的配置文件复制出来。

3.3 启动容器后的基础自检

执行启动命令:

docker compose up -d

刚启动时先别急着配置和格式化,进master容器确认三台机器的网络连通性:

docker exec -it hadoop-master bash ping -c 2 worker1 ping -c 2 worker2

能ping通worker1、worker2,说明Docker网络内的hostname解析生效了。接着检查SSH双向免密:

ssh master echo "connect master ok" ssh worker1 echo "connect worker1 ok" ssh worker2 echo "connect worker2 ok"

如果三条命令都直接打出对应的输出,没有要求输入密码,说明SSH互信没有问题,后面启动Hadoop脚本时就不会卡在连接环节。

4. Hadoop配置与NameNode格式化

4.1 四个配置文件的核心逻辑

Hadoop集群的配置可以拆成三个层次:文件系统层、调度层、作业执行层。core-site.xml负责定义集群入口,也就是默认的HDFS地址;hdfs-site.xml负责NameNode和DataNode的存储路径以及副本数;yarn-site.xml负责ResourceManager的位置和NodeManager的资源参数;mapred-site.xml决定MapReduce跑在什么框架上。

理解了这层关系之后,配置过程就不会只是机械地抄XML。四个文件各管一段,组合起来才是一个完整的集群。

4.2 配置文件完整内容

我把下面的四个文件放到宿主机的./hadoop-conf目录下,因为已经挂载到所有容器中,所以不用分别进容器修改。

core-site.xml:

<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://master:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/opt/hadoop/tmp</value> </property> </configuration>

hdfs-site.xml:

<configuration> <property> <name>dfs.namenode.name.dir</name> <value>file:///opt/hadoop/data/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>file:///opt/hadoop/data/data</value> </property> <property> <name>dfs.replication</name> <value>2</value> </property> <property> <name>dfs.permissions.enabled</name> <value>false</value> </property> </configuration>

yarn-site.xml:

<configuration> <property> <name>yarn.resourcemanager.hostname</name> <value>master</value> </property> <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> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>2048</value> </property> <property> <name>yarn.nodemanager.resource.cpu-vcores</name> <value>2</value> </property> </configuration>

mapred-site.xml:

<configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> <property> <name>mapreduce.application.classpath</name> <value>$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*:$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/lib/*</value> </property> </configuration>

还有关键文件workers(Hadoop 3.x用这个名字,老版本叫slaves):

worker1 worker2

dfs.replication为什么设成2而不是默认的3?因为我们只有两个DataNode。设成3的话,任何一个文件都会因为找不到第三个副本而触发健康告警,虽然WordCount多半还能跑完,但集群状态看起来始终不健康,没必要给自己添堵。

yarn.nodemanager.resource.memory-mb是NodeManager最多可用的物理内存。如果不设置,默认会按照宿主机物理内存检测,但容器里的NodeManager看到的可能是宿主机的大量内存,而容器本身又有cgroup内存限制,两者的信息不一致很容易导致容器被OOM杀掉。显式限制为2048MB,更稳妥。

4.3 格式化NameNode与“集群ID不一致”的坑

配置完成后,进入master容器执行:

hdfs namenode -format

这一步会生成NameNode的数据目录和集群ID,并且会打印一行类似“Storage directory /opt/hadoop/data/name has been successfully formatted”的输出。看到这行才能确认格式化成功。

格式化最经典的坑是“集群ID不一致”。如果集群启动过一次,NameNode和DataNode的数据目录里都保存了clusterID。当你在不清理DataNode数据的情况下重新格式化NameNode,新的NameNode会生成新的clusterID,而DataNode还在用旧ID,结果就是DataNode连不上NameNode,日志里一直报Incompatible clusterIDs。

解决思路说穿了很简单:清掉所有节点的/opt/hadoop/data/name和/opt/hadoop/data/data目录,再重新格式化。但要注意,必须三个容器都清理干净,不是只清master。实操上我通常直接删掉容器再重建,再执行format,整个环境回归纯净状态。

5. 启动集群与验证进程

5.1 启动HDFS与YARN

在master容器内按顺序启动:

start-dfs.sh start-yarn.sh

start-dfs.sh会通过SSH连接所有worker节点,启动相应的HDFS进程。由于我们在镜像里已经配置好免密登录,这里不会出现交互确认。第一次启动时日志里会有较多INFO信息,不用紧张,只要最后没有ERROR或异常堆栈,基本就是正常启动。

启动YARN同理。ResourceManager在master节点,NodeManager会在两个worker节点启动。如果之前设置的环境变量没有生效,这里会报“Attempting to operate on yarn as root”,所以上面的YARN_*_USER环境变量必须确保已经注入。

5.2 用jps和Web UI确认集群健康

分别在三个容器执行jps:

master节点应该看到:

NameNode SecondaryNameNode ResourceManager Jps

worker1和worker2节点应该看到:

DataNode NodeManager Jps

如果某个MessageNode没起来,优先看对应日志,不要急着重启整个集群。比如DataNode起不来,访问/opt/hadoop/logs/hadoop-root-datanode-master.log看报错,比盲目重复尝试有效得多。

浏览器访问宿主机上的两个Web界面:

  • HDFS界面:http://localhost:9870
  • YARN界面:http://localhost:8088

在HDFS界面上看“Datanodes”标签,两个节点应该都是Live状态。在YARN界面上看“Active Nodes”,两个NodeManager都应该被列出来。Web UI都正常,说明集群的基础服务已经就绪。

6. WordCount完整测试流程

6.1 准备测试数据并上传HDFS

我习惯先准备一个很小的文本文件,内容设计得能清楚看到单词频次是否准确:

mkdir -p /tmp/wc-input echo "hello world hadoop hello docker docker hadoop" > /tmp/wc-input/input.txt

这个文件里出现了hello两次、hadoop两次、docker两次、world一次,正好作为校验结果的标准答案。

把文件上传到HDFS:

hdfs dfs -mkdir -p /wc/input hdfs dfs -put /tmp/wc-input/input.txt /wc/input/

查看上传是否成功:

hdfs dfs -ls /wc/input

确认input.txt已经在HDFS上之后,才能提交MapReduce任务。

6.2 提交MapReduce任务

WordCount是Hadoop自带的示例程序,不用自己写代码。直接调用examples jar:

hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar \ wordcount /wc/input /wc/output

作业提交后,终端会滚动输出Map和Reduce的进度信息。每条map/reduce的百分比都会显示,如果有任务失败,会在日志中看到具体错误。整个作业完成时间通常在一分钟内,因为这个输入文件非常小。

执行完之后检查输出目录:

hdfs dfs -cat /wc/output/part-r-00000

最终结果应该和标准答案完全一致:

docker 2 hadoop 2 hello 2 world 1

这里有一个很容易犯的错:输出目录/wc/output必须事先不存在。MapReduce框架要求输出目录是全新的,如果重复运行同一个输出路径,作业会直接报错。所以每次做验证时,要么先删掉旧输出目录:

hdfs dfs -rm -r /wc/output

要么换个新路径。

6.3 结果解读与验证

看到正确的统计结果,说明整条链路已经通了:HDFS能正常读写、YARN能正常调度Container、MapTask和ReduceTask能正确执行、Shuffle过程把相同key聚合到了一起。

我还习惯额外验证一个点:故意把输入文件复制成多个,制造出多次分片的效果,比如再创建两个同样内容的文件重新上传,然后重新跑一次WordCount。最终结果应该还是单词频次相同,但数据量变成三倍。这样可以顺便观察MapTask的数量是否增加了。这种小实验对加深MapReduce执行流程的理解很有帮助。

7. 常见问题排查与避坑经验

7.1 常见错误速查表

现象原因解决办法
启动脚本报“Attempting to operate on hdfs as root”未设置HDFS_NAMENODE_USER等环境变量在Compose的environment中声明root用户
DataNode日志提示Incompatible clusterIDsNameNode重新格式化但DataNode数据未清理清空所有节点/opt/hadoop/data目录后重新format
WordCount输出目录报FileAlreadyExists输出目录已存在先删除输出目录或换新路径
YARN界面Active Nodes为0NodeManager内存配置未显式指定设置yarn.nodemanager.resource.memory-mb
SSH连接卡在主机指纹确认StrictHostKeyChecking未关闭在/root/.ssh/config中配置no
Web界面无法打开容器端口未映射或映射冲突检查docker-compose ports配置
容器重启后HDFS状态异常DataNode IP变化且NameNode地址写死使用hostname沟通,不要写容器IP

7.2 排查顺序与我的实操心得

遇到集群异常,我有一套固定的排查顺序。先看docker ps确认容器都活着,再进master执行jps看进程是否存在,然后看对应日志的尾部,最后才考虑改配置重启。这个顺序可以避免很多瞎猜。

整个过程踩过最深的坑就是格式化数据目录不彻底,导致DataNode反复横跳。后来我总结出一个习惯:只要涉及NameNode重新格式化,必定先删掉所有容器里的name和data目录,再执行format。宁可多花几秒钟清理,也不要赌它没问题。

还有一个容器环境特有的注意点:容器内存和宿主机的内存感知不一致。如果你的机器内存不多,建议把yarn.nodemanager.resource.memory-mb调小,比如1024或512,同时mapreduce的作业内存参数也跟着调小,否则作业可能因为内存申请失败被反复重试。测试集群里资源配得松一点不是坏事,毕竟目的是验证功能和流程。

最后分享一个小技巧:把所有配置文件、docker-compose.yml、启动脚本放进同一个项目目录,再用一个脚本完成“清理容器、构建镜像、启动集群、格式化、启动服务”的一键流程。这样每次环境搞坏了,跑一遍脚本就能恢复到干净状态,WordCount测试也能反复跑,不用每次手动清理。Docker方案的真正价值也就在这里:环境是代码的一部分,随时可以重建,而不是一个运行了很久、状态不可复现的黑盒。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询