Docker 搭建 Hadoop 集群这件事,我在不同规模的机器上反复折腾过很多轮,从最早用一台 8G 内存的旧笔记本硬扛三节点,到后来在正经服务器上跑五节点做数据验证,中间踩过的坑基本能写一本小册子。很多人第一次听到这个组合的第一反应是:Hadoop 本身是个分布式系统,Docker 是容器化工具,两者凑一起是不是多此一举?实际恰恰相反,用容器把 Hadoop 集群跑起来,能让你在半小时内从零得到一个可复现、可销毁、可版本化的实验环境,这对学习分布式原理、验证配置参数、做小规模数据处理来说,性价比高得离谱。
这篇内容面向的是有一定 Linux 基础、想在自己机器上快速搭一套 Hadoop 集群来练手的开发者,也适合需要频繁重建测试环境的数据方向从业者。我会把整个搭建过程拆成设计思路、核心细节、实操落地、问题排查四大块,把每一步为什么这么做讲清楚,把参数怎么算讲明白,把踩过的坑提前给你标出来。你照着做,大概率能一次跑通;跑不通的时候,第四部分的排查表能帮你快速定位。
1. 整体设计与方案选型拆解
1.1 为什么用容器跑 Hadoop 而不是直接装物理机
先说清楚这个方案的核心价值。传统方式装 Hadoop 集群,你得准备至少三台机器或者三个虚拟机,每台装 JDK、配 SSH 免密、改 hosts、分发安装包、格式化 HDFS,一套流程走下来半天没了,而且一旦某个节点的配置文件改错,排查起来要在三台机器之间来回跳。更麻烦的是,实验做完想清理干净,残留的进程、数据目录、环境变量能让你下次重装时怀疑人生。
容器化之后,这些痛点基本被抹平。镜像里预置好 JDK 和 Hadoop 二进制,容器启动即用;节点之间的网络通过自定义 bridge 网络打通,hosts 解析由容器名自动完成;数据目录挂载到宿主机卷上,容器删了数据还在,想彻底重来就把卷一起删掉。整个集群的"定义"浓缩在几个配置文件和一条启动命令里,换台机器照样复现。
我自己的习惯是:学习阶段用三节点(一主两从),验证配置和小数据量任务足够;如果要模拟更真实的分布场景,加到五节点(一主四从)也不过多占几个 G 内存。下面所有内容以三节点为主线,扩到五节点只是多起两个容器的事。
1.2 镜像选型与版本搭配的考量
镜像这块有两个主流路线:一是用官方或社区维护的 Hadoop 镜像,二是自己基于基础镜像装。我倾向后者,原因很实在——社区镜像的版本往往偏旧,而且内部预置的配置和你的需求未必对得上,改起来还不如从干净的基础镜像自己装来得可控。
基础镜像选带 JDK 的 Linux 发行版即可,重点是 JDK 版本要和 Hadoop 版本匹配。Hadoop 3.x 系列要求 JDK 8 或 JDK 11,我实测下来 JDK 8 兼容性最稳,JDK 11 也能跑但个别组件会有告警。Hadoop 版本建议选 3.3.x 的稳定版,2.x 系列虽然经典但生态在往 3.x 迁移,新项目没必要从旧版本起步。
这里有个容易被忽略的点:镜像里要装openssh-server和openssh-client。Hadoop 的启动脚本依赖 SSH 在节点间拉起守护进程,容器里默认没有 sshd,不装的话启动脚本会卡在连接阶段。另外pdsh或sshpass不是必须的,但装了之后批量操作节点会方便很多。
1.3 网络拓扑与容器编排方式
三节点的拓扑很直接:一个master容器跑 NameNode 和 ResourceManager,两个slave容器跑 DataNode 和 NodeManager。所有容器接入同一个自定义 bridge 网络,容器名即主机名,Hadoop 配置里直接用容器名做地址,省去手动维护 hosts 的麻烦。
编排方式上,用docker-compose比手写一堆docker run命令清晰得多。compose 文件里把三个服务的镜像、网络、卷挂载、端口映射一次性声明清楚,docker-compose up -d一条命令拉起整个集群,down一条命令收工。如果你习惯用原生命令,也不是不行,但节点一多命令就长得没法看,维护成本陡增。
端口映射方面,master 需要暴露 NameNode 的 Web UI(默认 9870)和 ResourceManager 的 Web UI(默认 8088),方便宿主机浏览器直接访问看集群状态。slave 节点一般不需要对外暴露端口,除非你要单独访问某个 DataNode 的 UI。这里提醒一句:容器内 Hadoop 各组件监听的端口是固定的,映射到宿主机时注意别和本机已占用端口冲突,9870 和 8088 这两个端口在不少开发机上会被别的服务占掉,起之前先ss -tlnp看一眼。
2. 核心细节解析与实操要点
2.1 镜像构建:把 JDK 和 Hadoop 装进去
自己写 Dockerfile 构建镜像,核心就三步:装 JDK、装 SSH 服务、解压 Hadoop。JDK 用包管理器装最省事,比如基于 Debian 系的镜像apt-get install -y openjdk-8-jdk,装完java -version验证一下。SSH 服务装完要生成主机密钥并允许 root 登录(容器内实验环境,安全性要求不高,但生产环境绝不能这么干)。
Hadoop 二进制包建议在构建时用ADD或RUN wget拉取后解压到固定路径,比如/opt/hadoop。解压后设置HADOOP_HOME和PATH环境变量,写进镜像的ENV指令里,这样容器一启动这些变量就生效,不用每次手动 export。
FROM ubuntu:20.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ openjdk-8-jdk openssh-server openssh-client vim net-tools curl \ && rm -rf /var/lib/apt/lists/* RUN mkdir /var/run/sshd \ && ssh-keygen -A \ && sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config \ && echo 'root:root' | chpasswd ADD hadoop-3.3.6.tar.gz /opt/ RUN mv /opt/hadoop-3.3.6 /opt/hadoop ENV HADOOP_HOME=/opt/hadoop ENV PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 EXPOSE 22 9870 8088 CMD ["/usr/sbin/sshd", "-D"]这段 Dockerfile 里几个细节值得说。DEBIAN_FRONTEND=noninteractive是为了避免 apt 安装时弹出交互式配置卡住构建;ssh-keygen -A生成所有类型的主机密钥,缺了这步 sshd 起不来;PermitRootLogin yes和设置 root 密码是为了容器间免密登录做准备,实验环境图省事,真实环境请用普通用户加密钥认证。CMD让容器前台跑 sshd,保证容器不退出。
构建命令很简单:docker build -t hadoop-base:3.3.6 .,注意 Hadoop 压缩包要放在 Dockerfile 同目录下,ADD才能找到。
2.2 网络与主机名规划
自定义 bridge 网络是容器间通信的基础。创建命令:docker network create --subnet=172.20.0.0/16 hadoop-net。指定子网是为了避免和宿主机已有网段冲突,172.20 这个段一般不会撞车。创建后可以用docker network inspect hadoop-net确认。
主机名规划上,compose 里给每个服务指定container_name,比如hadoop-master、hadoop-slave1、hadoop-slave2。容器加入同一网络后,彼此可以用容器名互相解析,这是 Docker 内置 DNS 的能力。Hadoop 配置里fs.defaultFS写成hdfs://hadoop-master:9000,yarn.resourcemanager.hostname写成hadoop-master,slave 的workers文件里写hadoop-slave1和hadoop-slave2,全部用容器名,不用碰 IP。
注意:容器名里不要用下划线,Docker 的 DNS 解析对下划线支持不稳定,用短横线最保险。我早期用
hadoop_master这种命名,结果 slave 解析不到 master,排查了半天才发现是命名问题。
2.3 Hadoop 核心配置文件逐项拆解
Hadoop 的配置集中在$HADOOP_HOME/etc/hadoop/目录下,需要改的文件有core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml和workers。逐个说关键项。
core-site.xml里最重要的是fs.defaultFS,指定 HDFS 的访问入口,格式hdfs://主机名:端口,端口默认 9000,我习惯保持默认。另一个常配的是hadoop.tmp.dir,指定 Hadoop 运行时临时目录,建议指到挂载卷上,避免容器重启丢数据。
hdfs-site.xml里dfs.replication控制副本数,三节点集群设 2 或 3 都行,设 3 的话每个 DataNode 存一份,设 2 能省点空间。dfs.namenode.name.dir和dfs.datanode.data.dir分别指定 NameNode 元数据和 DataNode 数据的存放路径,同样建议挂到卷上。dfs.namenode.secondary.http-address指定 SecondaryNameNode 地址,三节点里一般放在 slave 上或者干脆不单独起。
yarn-site.xml里yarn.resourcemanager.hostname指向 master,yarn.nodemanager.aux-services设为mapreduce_shuffle,这是跑 MapReduce 任务必须的。内存相关的yarn.nodemanager.resource.memory-mb要根据容器实际内存设,给多了容器会被 OOM kill,给少了任务跑不动。
mapred-site.xml里mapreduce.framework.name设为yarn,让 MapReduce 跑在 YARN 上。workers文件(老版本叫slaves)里每行写一个 slave 容器名,启动脚本会按这个列表去拉起 DataNode 和 NodeManager。
2.4 内存与资源参数的计算逻辑
这是很多人翻车的地方。Hadoop 各组件都是 JVM 进程,每个进程默认堆内存不小,容器内存给不够就会频繁 OOM。我的经验算法是:先确定宿主机能给集群的总内存,比如 16G,三节点平均分,每容器约 5G。master 上跑 NameNode 和 ResourceManager 两个 JVM,各给 1G 堆;slave 上跑 DataNode 和 NodeManager,各给 1G 堆,加上系统开销,每容器 4G 内存足够,留 1G 余量。
YARN 的yarn.nodemanager.resource.memory-mb要设成容器可用内存减去系统和其他进程占用后的值。比如 slave 容器 4G 内存,DataNode 占 1G 左右,NodeManager 自身占 1G,那这个值设 2048(单位 MB)比较稳妥。设太大 YARN 会以为资源充足,调度多个容器任务时把内存吃爆。
提示:
yarn.nodemanager.resource.memory-mb和yarn.scheduler.maximum-allocation-mb要配合着看,后者是单个任务能申请的最大内存,不能超过前者。我见过有人前者设 2048、后者设 4096,结果任务一提交就被拒。
3. 实操过程与核心环节实现
3.1 目录结构与 compose 文件编写
先在宿主机建一个工作目录,结构大概这样:docker-hadoop/下放Dockerfile、hadoop-3.3.6.tar.gz、docker-compose.yml,再建config/放改好的 Hadoop 配置文件,data/下按节点建子目录挂载数据卷。配置文件单独抽出来挂载进容器,好处是改配置不用重新构建镜像,改完重启容器就生效。
compose 文件的核心结构:
version: "3" services: master: image: hadoop-base:3.3.6 container_name: hadoop-master hostname: hadoop-master networks: - hadoop-net ports: - "9870:9870" - "8088:8088" volumes: - ./config:/opt/hadoop/etc/hadoop - ./data/master:/opt/hadoop/data tty: true slave1: image: hadoop-base:3.3.6 container_name: hadoop-slave1 hostname: hadoop-slave1 networks: - hadoop-net volumes: - ./config:/opt/hadoop/etc/hadoop - ./data/slave1:/opt/hadoop/data tty: true slave2: image: hadoop-base:3.3.6 container_name: hadoop-slave2 hostname: hadoop-slave2 networks: - hadoop-net volumes: - ./config:/opt/hadoop/etc/hadoop - ./data/slave2:/opt/hadoop/data tty: true networks: hadoop-net: external: truetty: true让容器保持运行不退出,因为镜像的 CMD 是跑 sshd,不加这个有些环境下容器会立刻退出。external: true表示网络是提前用docker network create建好的,compose 不负责创建。
3.2 免密登录配置的实操步骤
Hadoop 启动脚本靠 SSH 在节点间执行命令,所以 master 要能免密 SSH 到所有 slave,slave 之间最好也互通。步骤是:进 master 容器docker exec -it hadoop-master bash,执行ssh-keygen -t rsa一路回车生成密钥对,然后ssh-copy-id hadoop-slave1和ssh-copy-id hadoop-slave2把公钥分发过去。首次连接会提示确认指纹,输 yes 即可。
这里有个实操技巧:ssh-copy-id需要输入目标机密码,前面 Dockerfile 里设了 root 密码为 root,直接输就行。如果嫌一个个分发麻烦,可以在构建镜像时就把公钥预置进去,但那样镜像就不通用了,我一般还是手动分发,三节点也就两条命令的事。
分发完验证一下:ssh hadoop-slave1 hostname,能返回hadoop-slave1就说明免密通了。这一步不通,后面格式化 HDFS 和启动集群全都会失败,务必先验证。
3.3 格式化与集群启动的完整流程
配置和免密都就绪后,进 master 容器执行格式化:hdfs namenode -format。这条命令只在首次搭建时执行,重复执行会把 NameNode 元数据清掉,集群里原有数据就找不回来了。格式化输出里看到Storage directory ... has been successfully formatted才算成功。
格式化完成后启动集群:start-all.sh。这个脚本会依次拉起 NameNode、SecondaryNameNode、ResourceManager、DataNode、NodeManager。启动过程会有大量日志刷屏,重点看有没有ERROR或Exception。启动完用jps命令检查进程,master 上应该看到NameNode、SecondaryNameNode、ResourceManager,slave 上应该看到DataNode、NodeManager。少哪个进程就说明对应组件没起来,去$HADOOP_HOME/logs/下翻对应日志。
进程都齐了之后,浏览器访问http://宿主机IP:9870看 HDFS 状态,访问http://宿主机IP:8088看 YARN 状态。9870 页面里 Live Nodes 显示 2 个 DataNode 就说明集群健康。8088 页面里能看到两个 NodeManager 的资源和状态。
3.4 跑一个验证任务确认集群可用
光看进程起来不算数,得跑个实际任务验证。最经典的是跑 Hadoop 自带的 WordCount 示例。先在 HDFS 上建输入目录:hdfs dfs -mkdir -p /input,然后造个测试文件传上去:echo "hello hadoop hello docker" > test.txt && hdfs dfs -put test.txt /input。
接着提交任务:hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar wordcount /input /output。任务跑完看输出:hdfs dfs -cat /output/part-r-00000,能看到每个单词的计数就说明 MapReduce 在 YARN 上正常跑通了。这一步是整个搭建的验收标准,跑通了才算集群真正可用。
注意:输出目录
/output不能预先存在,否则任务会报Output directory already exists错误。重跑任务前先hdfs dfs -rm -r /output清掉。
4. 常见问题与排查技巧实录
4.1 启动失败类问题速查
集群起不来是最常见的情况,我把遇到过的典型问题和排查路径整理成表,方便对照。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
start-all.sh卡住无响应 | SSH 免密未配通 | 手动ssh hadoop-slave1测试,不通则重新分发公钥 |
| slave 上 DataNode 进程缺失 | 多次格式化导致 clusterID 不一致 | 清空 slave 数据目录后重新格式化,或统一各节点 VERSION 文件里的 clusterID |
| NameNode 启动即退出 | hadoop.tmp.dir目录不存在或无权限 | 检查目录是否存在,chmod给足权限 |
| 容器启动后立即退出 | CMD 进程前台运行问题 | 确认tty: true,或 CMD 用sshd -D保持前台 |
| 9870 页面打不开 | 端口未映射或宿主机端口冲突 | docker port确认映射,ss -tlnp查端口占用 |
其中 clusterID 不一致这个坑我踩过不止一次。现象是 slave 的 DataNode 起不来,日志里报Incompatible clusterIDs。根因是 NameNode 格式化时生成了新的 clusterID,而 slave 数据目录里还留着旧的。解决办法要么把 slave 数据目录清空重来,要么手动把 master 上dfs.namenode.name.dir/current/VERSION里的 clusterID 抄到 slave 对应文件里。我一般选前者,干净利落。
4.2 资源与性能类问题
容器内存不足导致的 OOM 是第二大类问题。表现是某个 JVM 进程莫名其妙消失,日志里出现Killed或OutOfMemoryError。排查思路是先docker stats看各容器内存占用,再看dmesg有没有 OOM killer 的记录。解决就是调大容器内存限制,同时把 Hadoop 各组件的堆参数(HADOOP_HEAPSIZE、YARN_HEAPSIZE)调小,两头配合。
另一个常见的是 YARN 任务一直处于 ACCEPTED 状态不进入 RUNNING。这通常是资源不够,NodeManager 上报的可用内存小于任务申请的内存。去 8088 页面看集群资源总量,对比任务申请量,要么调大yarn.nodemanager.resource.memory-mb,要么调小任务的mapreduce.map.memory.mb。
4.3 网络与解析类问题
容器间解析失败的表现是配置里写了容器名但连不上。先docker exec进容器ping hadoop-master测试,不通的话检查容器是否都在同一网络下(docker network inspect hadoop-net看 Containers 列表),再检查容器名有没有用下划线。还有一种情况是宿主机防火墙拦了容器网络,这个在部分环境下会出现,临时关掉防火墙验证一下就能定位。
Web UI 访问不了但容器内curl localhost:9870正常,说明是端口映射问题。检查 compose 里的ports配置,格式是宿主机端口:容器端口,别写反了。另外 Hadoop 3.x 的 NameNode Web UI 默认端口是 9870,2.x 是 50070,版本不同端口不同,别照着老教程配。
4.4 我踩过的几个独家坑
第一个坑是时间同步。容器默认用宿主机时间,但如果宿主机时间不准,Hadoop 各节点间心跳会出问题,表现为节点频繁被标记为 dead。实验环境里一般没事,但如果你的机器时间漂移严重,进容器date看一眼,必要时装个时间同步工具。
第二个坑是配置文件挂载的权限。宿主机上编辑的配置文件挂进容器后,如果文件属主和容器内运行用户不一致,Hadoop 读配置可能报权限错误。我的做法是挂载后在容器内chown一下,或者干脆在宿主机上把文件权限放开到 644。
第三个坑是重复start-all.sh。集群已经起来的情况下再执行一次启动脚本,会报端口被占用或者进程重复启动的错。正确做法是先stop-all.sh再启动,或者用hdfs --daemon start这类单组件命令精确控制。养成"先停后起"的习惯能省很多莫名其妙的报错。
第四个坑是数据卷的清理。实验做多了,data/目录下会堆积大量 HDFS 块文件,占满磁盘。定期清理时注意,如果只想重置集群,把data/下各节点的current目录删掉再重新格式化即可,不用整个删。但删之前确认没有需要保留的数据,HDFS 的块文件删了就是真没了。
这套环境我前后重建过十几次,每次从零到跑通 WordCount 大概二十分钟,其中大部分时间花在等镜像构建和容器启动上。真正的手工操作就是改几个配置文件、分发一次公钥、执行两条命令。把 compose 文件和配置目录用版本管理工具管起来之后,换机器就是 clone 下来up一下的事,这种可复现性才是容器化跑 Hadoop 最大的价值。后续如果想扩到五节点,在 compose 里复制两个 slave 服务、workers 文件加两行、重新分发公钥,其余配置一个字不用改。