最近有个大数据环境相关的测试需求要快速验证一套三节点的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_FRONTENDnoninteractive ENV JAVA_HOME/usr/lib/jvm/java-11-openjdk-amd64 ENV HADOOP_VERSION3.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_USERroot - HDFS_DATANODE_USERroot - HDFS_SECONDARYNAMENODE_USERroot - YARN_RESOURCEMANAGER_USERroot - YARN_NODEMANAGER_USERroot command: [/usr/sbin/sshd, -D] worker1: image: my-hadoop:3.3.6 container_name: hadoop-worker1 hostname: worker1 networks: - hadoop-net environment: - HDFS_NAMENODE_USERroot - HDFS_DATANODE_USERroot - HDFS_SECONDARYNAMENODE_USERroot - YARN_RESOURCEMANAGER_USERroot - YARN_NODEMANAGER_USERroot command: [/usr/sbin/sshd, -D] worker2: image: my-hadoop:3.3.6 container_name: hadoop-worker2 hostname: worker2 networks: - hadoop-net environment: - HDFS_NAMENODE_USERroot - HDFS_DATANODE_USERroot - HDFS_SECONDARYNAMENODE_USERroot - YARN_RESOURCEMANAGER_USERroot - YARN_NODEMANAGER_USERroot 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.xmlconfiguration property namefs.defaultFS/name valuehdfs://master:9000/value /property property namehadoop.tmp.dir/name value/opt/hadoop/tmp/value /property /configurationhdfs-site.xmlconfiguration property namedfs.namenode.name.dir/name valuefile:///opt/hadoop/data/name/value /property property namedfs.datanode.data.dir/name valuefile:///opt/hadoop/data/data/value /property property namedfs.replication/name value2/value /property property namedfs.permissions.enabled/name valuefalse/value /property /configurationyarn-site.xmlconfiguration property nameyarn.resourcemanager.hostname/name valuemaster/value /property property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /property property nameyarn.nodemanager.aux-services.mapreduce_shuffle.class/name valueorg.apache.hadoop.mapred.ShuffleHandler/value /property property nameyarn.nodemanager.resource.memory-mb/name value2048/value /property property nameyarn.nodemanager.resource.cpu-vcores/name value2/value /property /configurationmapred-site.xmlconfiguration property namemapreduce.framework.name/name valueyarn/value /property property namemapreduce.application.classpath/name value$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*:$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/lib/*/value /property /configuration还有关键文件workersHadoop 3.x用这个名字老版本叫slavesworker1 worker2dfs.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.shstart-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确认集群健康分别在三个容器执行jpsmaster节点应该看到NameNode SecondaryNameNode ResourceManager Jpsworker1和worker2节点应该看到DataNode NodeManager Jps如果某个MessageNode没起来优先看对应日志不要急着重启整个集群。比如DataNode起不来访问/opt/hadoop/logs/hadoop-root-datanode-master.log看报错比盲目重复尝试有效得多。浏览器访问宿主机上的两个Web界面HDFS界面http://localhost:9870YARN界面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一次正好作为校验结果的标准答案。把文件上传到HDFShdfs 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 jarhadoop 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目录后重新formatWordCount输出目录报FileAlreadyExists输出目录已存在先删除输出目录或换新路径YARN界面Active Nodes为0NodeManager内存配置未显式指定设置yarn.nodemanager.resource.memory-mbSSH连接卡在主机指纹确认StrictHostKeyChecking未关闭在/root/.ssh/config中配置noWeb界面无法打开容器端口未映射或映射冲突检查docker-compose ports配置容器重启后HDFS状态异常DataNode IP变化且NameNode地址写死使用hostname沟通不要写容器IP7.2 排查顺序与我的实操心得遇到集群异常我有一套固定的排查顺序。先看docker ps确认容器都活着再进master执行jps看进程是否存在然后看对应日志的尾部最后才考虑改配置重启。这个顺序可以避免很多瞎猜。整个过程踩过最深的坑就是格式化数据目录不彻底导致DataNode反复横跳。后来我总结出一个习惯只要涉及NameNode重新格式化必定先删掉所有容器里的name和data目录再执行format。宁可多花几秒钟清理也不要赌它没问题。还有一个容器环境特有的注意点容器内存和宿主机的内存感知不一致。如果你的机器内存不多建议把yarn.nodemanager.resource.memory-mb调小比如1024或512同时mapreduce的作业内存参数也跟着调小否则作业可能因为内存申请失败被反复重试。测试集群里资源配得松一点不是坏事毕竟目的是验证功能和流程。最后分享一个小技巧把所有配置文件、docker-compose.yml、启动脚本放进同一个项目目录再用一个脚本完成“清理容器、构建镜像、启动集群、格式化、启动服务”的一键流程。这样每次环境搞坏了跑一遍脚本就能恢复到干净状态WordCount测试也能反复跑不用每次手动清理。Docker方案的真正价值也就在这里环境是代码的一部分随时可以重建而不是一个运行了很久、状态不可复现的黑盒。