1. 为什么我选择用 Docker 来搭一套 HDFS 完全分布式集群
如果你接触过大数据生态,大概率绕不开 Hadoop 这套东西。而 HDFS 作为 Hadoop 的分布式文件系统底座,几乎是所有离线计算、数据仓库、日志归集场景的起点。但真正让人头疼的,往往不是写 MapReduce 或者调 Spark 任务,而是怎么把一套完全分布式的 HDFS 集群顺顺当当地跑起来。
传统做法是准备三台以上的物理机或者虚拟机,装系统、配网络、改 hosts、传 JDK、传 Hadoop 包、逐个节点改配置文件、格式化、启动。这一套流程走下来,快的话半天,慢的话两天,而且一旦某台机器环境有差异,排查起来非常折磨人。更别提很多人手头只有一台笔记本,想学完全分布式却凑不出多台机器。
Docker 恰好解决了这个痛点。它把每个节点封装成一个独立容器,容器之间通过自定义网络互通,我可以在一台 16G 内存的机器上模拟出三节点甚至五节点的 HDFS 集群。镜像构建一次,之后随时销毁重建,环境完全一致,再也不用担心“这台机器能跑那台跑不起来”的问题。
这篇文章要讲的就是:如何用 Docker 从零搭建一套真正意义上的 HDFS 完全分布式集群,包含 NameNode、SecondaryNameNode、多个 DataNode,节点之间通过 SSH 免密互通,配置全部走完全分布式模式,而不是伪分布式。适合正在学大数据、需要反复搭环境做实验、或者想给团队做一套可复现演示环境的同学。哪怕你之前只玩过单机伪分布式,跟着走也能搭起来。
我下面会先讲整体设计思路,再拆解镜像构建和配置细节,然后给出完整的实操流程,最后把我踩过的坑和排查经验整理出来。整套方案我在不同配置的机器上复现过多次,稳定性可以放心。
2. 集群整体设计与方案选型拆解
2.1 为什么是“完全分布式”而不是伪分布式
很多人初学 HDFS 时用的是伪分布式,也就是所有角色(NameNode、DataNode、SecondaryNameNode)跑在同一台机器上,只是用不同进程区分。这种模式适合熟悉命令,但它掩盖了分布式最核心的东西:跨节点的网络通信、数据块的实际分布、节点故障后的副本恢复。
完全分布式模式下,NameNode 在一台容器上,DataNode 分散在另外几台容器上,数据块真正被切分并复制到不同节点。你才能观察到副本因子为 3 时,一个块到底落在哪几个 DataNode 上;才能模拟某个 DataNode 挂掉后,集群如何重新复制副本。这些才是 HDFS 的精髓。
所以我的设计目标很明确:至少一个 NameNode、一个 SecondaryNameNode、两个以上 DataNode,各自独立容器,通过 SSH 免密和自定义网络组成集群。
2.2 容器编排方式的选择:docker-compose 还是手动 docker run
搭建多节点集群,编排方式有两种常见选择。
第一种是纯手动docker run,逐个启动容器,再进容器改配置。这种方式直观,但节点一多,命令冗长,网络、端口、挂载都要手动指定,重建时容易漏参数。
第二种是用docker-compose,把每个节点的镜像、网络、端口、卷、启动命令写进一个 YAML 文件,一条docker-compose up -d全部拉起,down一下全部销毁。可复现性极强,特别适合需要反复实验的场景。
我选的是docker-compose。原因很简单:这套集群我经常需要“推倒重来”,比如测试格式化流程、测试配置改动效果,compose 的一键重建能省掉大量重复劳动。而且 YAML 文件本身就是一份清晰的集群拓扑文档,谁拿到都能一眼看懂有几个节点、各自什么角色。
2.3 基础镜像与 JDK、Hadoop 版本考量
基础镜像我倾向用ubuntu:20.04或者centos:7这类带完整包管理器的系统镜像,而不是 alpine。原因是 Hadoop 依赖 SSH、部分 native 库,alpine 的 musl libc 在跑 Hadoop 时偶尔会有兼容性小问题,排查成本高。用 ubuntu 虽然镜像大一点,但省心。
JDK 选JDK 8。Hadoop 3.x 虽然支持更高版本 JDK,但生态里大量组件对 JDK 8 的兼容性最稳,踩坑最少。Hadoop 版本选3.3.x系列,这个版本对 HDFS 的纠删码、Router-based federation 支持完善,同时配置方式和 2.x 差异不大,学习资料多。
提示:JDK 和 Hadoop 的安装包建议提前下载好放在构建上下文目录里,构建镜像时直接 COPY 进去,避免容器内联网下载受网络波动影响。
2.4 网络与主机名规划
Docker 自定义 bridge 网络自带 DNS 解析,容器之间可以直接用容器名或指定的 hostname 通信。我给集群规划的主机名和角色如下:
| 主机名 | 角色 | 说明 |
|---|---|---|
| nn | NameNode | 主节点,管理元数据 |
| snn | SecondaryNameNode | 辅助节点,合并 edits |
| dn1 | DataNode | 数据节点一 |
| dn2 | DataNode | 数据节点二 |
| dn3 | DataNode | 数据节点三 |
所有节点在同一个自定义网络hadoop-net下,网段用172.20.0.0/16,避免和宿主机常用网段冲突。每个节点在 compose 里通过hostname指定名字,这样 SSH 和 Hadoop 配置里直接用主机名即可,不用记 IP。
3. 镜像构建与核心配置细节解析
3.1 Dockerfile 的关键步骤拆解
镜像构建是整个集群的地基。我把它拆成几个关键动作:装 SSH 和基础工具、配 JDK、装 Hadoop、配 SSH 免密、写环境变量。
先看 Dockerfile 的核心结构:
FROM ubuntu:20.04 ENV DEBIAN_FRONTEND=noninteractive ENV JAVA_HOME=/usr/local/jdk ENV HADOOP_HOME=/usr/local/hadoop ENV PATH=$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH RUN apt-get update && apt-get install -y \ openssh-server openssh-client rsync vim net-tools iputils-ping \ && rm -rf /var/lib/apt/lists/* COPY jdk /usr/local/jdk COPY hadoop /usr/local/hadoop RUN mkdir -p /var/run/sshd && \ ssh-keygen -t rsa -f /root/.ssh/id_rsa -N "" && \ cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys && \ chmod 600 /root/.ssh/authorized_keys COPY ssh_config /root/.ssh/config COPY bootstrap.sh /usr/local/bin/bootstrap.sh RUN chmod +x /usr/local/bin/bootstrap.sh EXPOSE 22 9870 9000 CMD ["/usr/local/bin/bootstrap.sh"]这里有几个细节值得展开。
第一,SSH 免密为什么在镜像里就配好。Hadoop 的启动脚本start-dfs.sh依赖 SSH 到各节点去拉起进程。如果每次启动容器后再手动分发公钥,节点一多就很繁琐。我在镜像构建阶段就生成密钥对并把公钥写进authorized_keys,所有节点用同一个镜像,天然互信。生产环境当然不能这么干,但实验环境这样最省事。
第二,ssh_config的作用。默认 SSH 到新主机时会提示确认指纹,脚本化启动会被卡住。我在/root/.ssh/config里加上:
Host * StrictHostKeyChecking no UserKnownHostsFile /dev/null LogLevel ERROR这样 SSH 连接不会交互式询问,启动脚本能顺畅跑完。
第三,bootstrap.sh负责容器启动时的动态动作。比如启动 sshd、根据环境变量决定这个容器扮演什么角色。镜像本身是通用的,角色差异通过 compose 里的环境变量注入。
3.2 Hadoop 核心配置文件逐个说明
Hadoop 完全分布式的配置集中在etc/hadoop下的几个 XML 文件。我把每个文件的关键项列出来,并解释为什么这么配。
core-site.xml定义默认文件系统和临时目录:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://nn:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/usr/local/hadoop/data/tmp</value> </property> </configuration>fs.defaultFS指向nn主机的 9000 端口,这是客户端访问 HDFS 的入口。用主机名而不是 IP,是因为 Docker 网络内 DNS 会解析nn到对应容器。hadoop.tmp.dir一定要显式指定,默认在/tmp下,容器重启可能丢失,指定到持久化目录更稳。
hdfs-site.xml是 HDFS 的核心:
<configuration> <property> <name>dfs.replication</name> <value>3</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/usr/local/hadoop/data/namenode</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/usr/local/hadoop/data/datanode</value> </property> <property> <name>dfs.namenode.secondary.http-address</name> <value>snn:9868</value> </property> </configuration>dfs.replication设为 3,配合三个 DataNode,正好每个块存三份。dfs.namenode.name.dir和dfs.datanode.data.dir分别指定元数据和数据块目录,同样要避开/tmp。dfs.namenode.secondary.http-address指向 snn 节点,让 SecondaryNameNode 独立部署。
workers 文件(Hadoop 3.x 里取代了 slaves)列出所有 DataNode 主机名:
dn1 dn2 dn3start-dfs.sh会读取这个文件,逐个 SSH 过去启动 DataNode。
hadoop-env.sh里指定 JAVA_HOME:
export JAVA_HOME=/usr/local/jdk这个必须配,否则启动脚本找不到 Java。
3.3 数据持久化与卷挂载策略
容器天生是无状态的,但 HDFS 的数据和元数据不能丢。我的做法是给 NameNode 和每个 DataNode 挂载独立的命名卷:
volumes: - nn-data:/usr/local/hadoop/data这样即使容器销毁,卷里的数据还在,重建容器后数据能恢复。对于实验环境,有时候我反而希望每次都是干净状态,那就用docker-compose down -v连卷一起删掉。
注意:NameNode 的元数据目录一旦格式化过,再次格式化会生成新的 clusterID,和 DataNode 的 clusterID 不匹配,导致 DataNode 拒绝注册。所以要么保留卷,要么格式化前清空所有节点的数据目录。
4. 完整实操流程与关键环节实现
4.1 目录结构与构建上下文准备
先在宿主机上建一个工作目录,结构如下:
hadoop-cluster/ ├── docker-compose.yml ├── Dockerfile ├── bootstrap.sh ├── ssh_config ├── jdk/ # 解压好的 JDK 目录 ├── hadoop/ # 解压好的 Hadoop 目录 └── config/ # 各 XML 配置文件JDK 和 Hadoop 我提前从官方渠道下载好,解压后直接放进构建上下文。这样构建镜像时 COPY 进去,不依赖容器内网络。
4.2 编写 docker-compose.yml 定义五节点集群
compose 文件是整套集群的骨架:
version: "3.8" networks: hadoop-net: driver: bridge ipam: config: - subnet: 172.20.0.0/16 services: nn: build: . hostname: nn container_name: nn networks: - hadoop-net ports: - "9870:9870" - "9000:9000" environment: - ROLE=namenode volumes: - nn-data:/usr/local/hadoop/data snn: build: . hostname: snn container_name: snn networks: - hadoop-net environment: - ROLE=secondarynamenode volumes: - snn-data:/usr/local/hadoop/data dn1: build: . hostname: dn1 container_name: dn1 networks: - hadoop-net environment: - ROLE=datanode volumes: - dn1-data:/usr/local/hadoop/data dn2: build: . hostname: dn2 container_name: dn2 networks: - hadoop-net environment: - ROLE=datanode volumes: - dn2-data:/usr/local/hadoop/data dn3: build: . hostname: dn3 container_name: dn3 networks: - hadoop-net environment: - ROLE=datanode volumes: - dn3-data:/usr/local/hadoop/data volumes: nn-data: snn-data: dn1-data: dn2-data: dn3-data:NameNode 暴露 9870 端口用于 Web UI 访问,9000 用于 HDFS 客户端连接。其他节点不需要对外暴露端口,集群内部通信即可。
4.3 bootstrap.sh 的角色分发逻辑
容器启动时,bootstrap.sh根据ROLE环境变量决定行为:
#!/bin/bash set -e /usr/sbin/sshd case "$ROLE" in namenode) if [ ! -f /usr/local/hadoop/data/namenode/current/VERSION ]; then echo "Formatting NameNode..." hdfs namenode -format -force -nonInteractive fi hdfs namenode ;; secondarynamenode) hdfs secondarynamenode ;; datanode) hdfs datanode ;; *) echo "Unknown role: $ROLE" tail -f /dev/null ;; esac这里有个关键判断:只在 NameNode 元数据目录不存在时才格式化。如果每次启动都格式化,DataNode 的 clusterID 就对不上,集群起不来。这个判断逻辑能保证容器重启后不会误格式化。
4.4 启动集群与格式化 NameNode
第一次启动的完整流程:
# 构建镜像并启动所有容器 docker-compose up -d --build # 查看容器状态 docker-compose ps # 进入 NameNode 容器 docker exec -it nn bash # 如果 bootstrap 没有自动格式化,手动执行 hdfs namenode -format # 启动 HDFS start-dfs.shstart-dfs.sh会在 NameNode 上启动 NameNode 进程,然后 SSH 到 snn 启动 SecondaryNameNode,再 SSH 到 dn1、dn2、dn3 启动 DataNode。启动完成后用jps检查各节点进程:
- nn 上应该有 NameNode
- snn 上应该有 SecondaryNameNode
- 每个 dn 上应该有 DataNode
4.5 验证集群状态与副本分布
在 NameNode 容器里执行:
hdfs dfsadmin -report输出会列出所有 DataNode 的 IP、主机名、容量、已用空间。正常情况下能看到三个 DataNode 都是Live状态。
再验证副本分布:
# 创建一个测试文件 echo "hello hdfs" > test.txt hdfs dfs -mkdir -p /test hdfs dfs -put test.txt /test/ # 查看块的分布 hdfs fsck /test/test.txt -files -blocks -locationsfsck输出会显示这个文件被切成几个块,每个块的三个副本分别落在哪几个 DataNode 上。这是完全分布式最直观的证据——你能看到数据真的分散在不同节点。
4.6 模拟节点故障与副本恢复
完全分布式的价值在于容错。我经常做的一个实验是停掉一个 DataNode:
docker stop dn3等几十秒后再看hdfs dfsadmin -report,dn3 会变成Dead。此时 NameNode 会检测到副本数不足,自动在其他存活节点上复制副本,把副本因子恢复到 3。再执行fsck,能看到块的位置已经重新分布。
这个实验能让你真正理解 HDFS 的副本机制和故障恢复,是伪分布式给不了的体验。
5. 常见问题与排查技巧实录
5.1 DataNode 启动后不注册到 NameNode
这是最常见的问题。现象是hdfs dfsadmin -report里一个 DataNode 都没有,或者只有部分。
排查顺序我一般这样走:
第一,看 DataNode 日志$HADOOP_HOME/logs/hadoop-root-datanode-*.log,重点找clusterID相关的报错。如果提示 clusterID 不匹配,说明 NameNode 格式化过但 DataNode 还留着旧数据。解决方法是清空 DataNode 数据目录后重启。
第二,检查网络连通性。在 DataNode 容器里ping nn,看能否解析并连通。如果解析不了,说明不在同一网络,检查 compose 里的 networks 配置。
第三,检查core-site.xml里的fs.defaultFS是否指向hdfs://nn:9000,端口和主机名不能错。
5.2 SSH 免密失效导致 start-dfs.sh 卡住
start-dfs.sh依赖 SSH 到各节点。如果免密没配好,脚本会卡在密码提示上。
验证方法是在 nn 容器里执行ssh dn1,如果能直接进去说明免密正常。如果提示要密码或确认指纹,检查/root/.ssh/authorized_keys权限是否为 600,/root/.ssh目录权限是否为 700。权限不对 SSH 会拒绝使用密钥。
还有一个隐蔽的坑:如果宿主机挂载了.ssh目录覆盖了容器内的,可能导致密钥丢失。我一般不给.ssh挂卷,让它留在镜像里。
5.3 端口冲突与 Web UI 访问不了
NameNode 的 Web UI 默认在 9870 端口。如果宿主机上这个端口被占用,compose 启动会报错。改映射端口即可,比如"19870:9870",然后访问http://localhost:19870。
如果容器起来了但 UI 打不开,先确认容器内 9870 是否在监听:netstat -tlnp | grep 9870。没监听说明 NameNode 没起来,去看日志。
5.4 内存不足导致容器被 OOM Kill
Hadoop 默认的 JVM 堆内存配置偏大,一台 8G 内存的机器跑五节点容易吃紧。我一般会在hadoop-env.sh里调小堆内存:
export HDFS_NAMENODE_OPTS="-Xmx512m" export HDFS_DATANODE_OPTS="-Xmx512m" export HDFS_SECONDARYNAMENODE_OPTS="-Xmx512m"实验环境数据量小,512M 足够。生产环境当然要按实际数据量调。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| DataNode 不注册 | clusterID 不匹配 | 清空 DataNode 数据目录重启 |
| DataNode 不注册 | 网络不通 | 检查同网络、hosts 解析 |
| start-dfs.sh 卡住 | SSH 免密失效 | 检查密钥和权限 |
| Web UI 打不开 | 端口未映射或进程未起 | 检查端口映射和 NameNode 日志 |
| 容器被 Kill | 内存不足 | 调小 JVM 堆内存 |
| 格式化后集群起不来 | 重复格式化 | 保留卷或统一清空所有节点数据 |
5.6 几个我踩过的坑
坑一:用 IP 而不是主机名。早期我在配置里直接写容器 IP,结果容器重建后 IP 变了,配置全废。后来统一用主机名,Docker DNS 自动解析,重建也不受影响。
坑二:忘了改 workers 文件。Hadoop 3.x 里 slaves 文件改名为 workers,如果还用旧名字,start-dfs.sh找不到 DataNode 列表,只会启动 NameNode。
坑三:时间不同步。容器之间时间差异过大时,HDFS 的一些心跳和租约机制会出问题。虽然 Docker 容器一般共享宿主机时钟,但如果宿主机时间本身不准,也会受影响。建议宿主机开启时间同步。
坑四:日志目录没挂卷。排查问题时日志最关键,但容器销毁日志就没了。我后来把 logs 目录也挂到卷上,方便事后分析。
6. 集群扩展与日常维护经验
6.1 动态增加 DataNode
集群跑起来后想加节点,操作其实很简单。在 compose 里复制一个 dn3 的配置改成 dn4,docker-compose up -d拉起新容器,然后把 dn4 加进 workers 文件,在 NameNode 上执行:
hdfs dfsadmin -refreshNodesNameNode 会重新读取 workers 文件,新节点自动注册进来。不需要重启整个集群,这也是 HDFS 设计上的优势。
6.2 安全下线一个 DataNode
下线节点不能直接docker stop,那样 NameNode 会认为是故障下线,触发大量副本复制。正确做法是配置dfs.hosts.exclude文件,把要下线的节点加进去,然后refreshNodes,NameNode 会先把该节点上的数据迁移到其他节点,迁移完成后再停容器。
6.3 日常巡检要看什么
我平时巡检主要看几个指标:dfsadmin -report里的 Live/Dead 节点数、容量使用率、副本因子是否达标;Web UI 上的 Block 数量和损坏块数量;各节点日志里有没有频繁的异常。这些指标正常,集群基本就健康。
6.4 备份与恢复思路
NameNode 的元数据是集群的命根子。实验环境我靠卷持久化,生产环境必须配dfs.namenode.name.dir到多个目录做冗余,再定期用hdfs dfsadmin -fetchImage拉取 fsimage 备份。恢复时用备份的 fsimage 和 edits 重建即可。
这套 Docker 方案我用了挺长时间,最大的感受是环境终于不再是障碍。以前搭一次集群要半天,现在docker-compose up -d几分钟搞定,想推倒重来就down -v再up。把精力真正放在理解 HDFS 的机制上,而不是耗在环境配置里。如果你也在学大数据,强烈建议用这种方式搭一套完全分布式集群,亲手停掉一个 DataNode 看副本怎么恢复,比看十篇文章都管用。