前两篇都是概念,这篇是动手部分:用三台虚拟机从零搭一个 Hadoop 集群。

搭之前我以为难点在配置文件,搭完发现——真正花时间的是环境准备。配置文件只是照着改参数,而环境里任何一个版本不对,后面全跑不起来。这篇按实际操作的顺序记录下来。

集群规划

三台机器,规划如下:

机器IPNameNodeSecondaryNameNodeDataNodeResourceManagerNodeManager
node01192.168.174.100
node02192.168.174.110
node03192.168.174.120

这是最典型的"伪分布式之上、完全分布式之下"的教学配置:主节点 node01 兼任了 NameNode、SecondaryNameNode 和 ResourceManager 三个角色,两台从节点只做 DataNode 和 NodeManager。

把三个主角色都压在一台机器上当然有单点故障,但实验环境无所谓——能看清数据怎么分布到三台机器上,目的就达到了

第一步:环境准备(这里最容易翻车)

基础环境

虚拟机配置要求:内存 4G 以上,硬盘 40G 以上,操作系统用 64 位的 CentOS 6.9。

64 位这一条不是可选项。Hadoop 的本地库(native library)只有 64 位版本,32 位系统上跑会报各种奇怪的库加载错误。

另外要关掉防火墙和 SELinux——它们会拦截集群各节点之间大量的内部通信:

# 关闭防火墙
service iptables stop
chkconfig iptables off

# 关闭 SELinux
vim /etc/selinux/config
# 把 SELINUX=enforcing 改成 SELINUX=disabled

SELinux 那条改完要重启才生效,别改完就急着往下做。

为什么需要"编译" Hadoop

这一步我一开始没理解——官网上不是有现成的安装包吗,为什么还要自己编译?

讲义里给了解释:Apache 官方发布的二进制包没有提供带 C 程序访问的接口。这意味着压缩功能(snappy、bzip2 这些)用不了,因为压缩库是 C 写的,需要通过本地库调用。

所以要用到这些功能,就得拿源码包重新编译一遍。要准备的依赖链不短:

组件版本作用
JDK必须是 1.7编译 hadoop-2.7.5
Maven3.0.5构建工具
findbugs1.3.9代码静态检查(构建流程需要)
protobuf2.5.0序列化,需源码编译安装
snappy1.1.1压缩库,需源码编译安装

最大的坑:JDK 版本

讲义里用加粗专门强调了一条:

注意 hadoop-2.7.5 这个版本的编译,只能使用 jdk1.7,如果使用 jdk1.8 那么就会报错

这是个硬约束。而且 CentOS 6.9 自带一堆 openjdk,得先全部卸干净,否则编译时可能链接到系统自带的版本:

# 查看系统自带的 openjdk
rpm -qa | grep java

# 全部卸载
rpm -e java-1.6.0-openjdk-1.6.0.41-1.13.13.1.el6_8.x86_64 \
       tzdata-java-2016j-1.el6.noarch \
       java-1.7.0-openjdk-1.7.0.131-2.6.9.0.el6_8.x86_64

然后是安装自己的 JDK 并配环境变量:

mkdir -p /export/servers
mkdir -p /export/softwares
cd /export/softwares
tar -zxvf jdk-7u71-linux-x64.tar.gz -C ../servers/

vim /etc/profile
# 加入:
export JAVA_HOME=/export/servers/jdk1.7.0_71
export PATH=:$JAVA_HOME/bin:$PATH

source /etc/profile

/export 这个目录不是系统标准目录,是大数据课程约定俗成的安装位置——把软件、数据、临时文件都放在一起,方便统一管理。

Maven 与阿里云镜像

tar -zxvf apache-maven-3.0.5-bin.tar.gz -C ../servers/

vim /etc/profile
export MAVEN_HOME=/export/servers/apache-maven-3.0.5
export MAVEN_OPTS="-Xms4096m -Xmx4096m"
export PATH=:$MAVEN_HOME/bin:$PATH

编译 Hadoop 要下载大量依赖,Maven 默认从国外的中央仓库拉,速度很慢。所以要在 settings.xml 里加一个阿里云镜像:

<mirror>
  <id>alimaven</id>
  <name>aliyun maven</name>
  <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
  <mirrorOf>central</mirrorOf>
</mirror>

编译

cd /export/servers/hadoop-2.7.5
mvn package -DskipTests -Pdist,native -Dtar -Drequire.snappy -e -X

参数含义:-DskipTests 跳过测试(编译快很多),-Pdist,native 启用本地库编译,-Drequire.snappy 要求支持 snappy 压缩。编译产物在 hadoop-dist/target 下。

第二步:配置文件

Hadoop 的配置文件都在 $HADOOP_HOME/etc/hadoop/ 下,要改 5 个。逐个说明。

core-site.xml —— 全局核心配置

<configuration>
  <property>
    <name>fs.default.name</name>
    <value>hdfs://node01:8020</value>
  </property>

  <property>
    <name>hadoop.tmp.dir</name>
    <value>/export/servers/hadoop-2.7.5/hadoopDatas/tempDatas</value>
  </property>

  <property>
    <name>io.file.buffer.size</name>
    <value>4096</value>
  </property>

  <property>
    <name>fs.trash.interval</name>
    <value>10080</value>
  </property>
</configuration>

四项分别:

fs.default.name 指定 HDFS 的默认访问地址。8020 是 NameNode 的 RPC 端口,客户端读写文件时连的就是这里。

hadoop.tmp.dir 是 Hadoop 的临时目录。这个路径非常重要——很多组件的默认数据目录都基于它。如果没配或者配到会被清理的目录(比如 /tmp),集群重启后数据可能就找不回来了。

fs.trash.interval = 10080 开启了回收站,单位是分钟。10080 分钟 = 7 天。开了之后 hdfs dfs -rm 删除的文件会先进回收站,还能捞回来;没开的话是直接删。

hdfs-site.xml —— HDFS 专属配置

这个文件最长,但可以分三组看。

第一组:Web 界面地址

<property>
  <name>dfs.namenode.http-address</name>
  <value>node01:50070</value>
</property>
<property>
  <name>dfs.namenode.secondary.http-address</name>
  <value>node01:50090</value>
</property>

50070 是 NameNode 的 Web UI 端口,搭完集群要打开这个页面验证。

第二组:数据目录

<property>
  <name>dfs.namenode.name.dir</name>
  <value>file:///export/servers/hadoop-2.7.5/hadoopDatas/namenodeDatas,
         file:///export/servers/hadoop-2.7.5/hadoopDatas/namenodeDatas2</value>
</property>
<property>
  <name>dfs.datanode.data.dir</name>
  <value>file:///export/servers/hadoop-2.7.5/hadoopDatas/datanodeDatas,
         file:///export/servers/hadoop-2.7.5/hadoopDatas/datanodeDatas2</value>
</property>

注意每个都配了两个路径,用逗号分隔。这是 HDFS 的一个特性:同一个目录可以配多份,写的时候同时写进去。讲义里的注释解释了实际用途——

定义 dataNode 数据存储的节点位置,实际工作中,一般先确定磁盘的挂载目录,然后多个目录用逗号分割

也就是说:如果一台机器挂了多块盘,就把每块盘的挂载点都列上,HDFS 会自动把数据分散到不同物理磁盘。既扩大了容量,也避免了单盘故障导致整个 DataNode 不可用。

第三组:块与副本

<property>
  <name>dfs.replication</name>
  <value>3</value>
</property>
<property>
  <name>dfs.blocksize</name>
  <value>134217728</value>
</property>
<property>
  <name>dfs.permissions</name>
  <value>false</value>
</property>

前两个上一篇详细讲过(3 副本、128MB 块)。第三个 dfs.permissions = false 是关掉了 HDFS 的权限检查——这是实验环境图省事的做法,生产环境绝对不能这么配,否则任何用户都能读写别人的数据。

yarn-site.xml

<configuration>
  <property>
    <name>yarn.resourcemanager.hostname</name>
    <value>node01</value>
  </property>
  <property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
  </property>
  <property>
    <name>yarn.log-aggregation-enable</name>
    <value>true</value>
  </property>
  <property>
    <name>yarn.log-aggregation.retain-seconds</name>
    <value>604800</value>
  </property>
  <property>
    <name>yarn.nodemanager.resource.memory-mb</name>
    <value>20480</value>
  </property>
  <property>
    <name>yarn.scheduler.minimum-allocation-mb</name>
    <value>2048</value>
  </property>
  <property>
    <name>yarn.nodemanager.vmem-pmem-ratio</name>
    <value>2.1</value>
  </property>
</configuration>

几项关键的:

yarn.nodemanager.aux-services = mapreduce_shuffle必填项。Shuffle 是 MapReduce 里把 map 输出传给 reduce 的过程,YARN 需要这个辅助服务来支持它。漏配的话 MapReduce 任务跑不起来。

yarn.log-aggregation-enable = true 开启日志聚合。默认情况下每个容器(Container)的日志散落在各个 NodeManager 本地磁盘上,任务跑完想查日志得挨个机器找。开启聚合后会统一收集到 HDFS 上,通过 Web 界面就能看。

yarn.nodemanager.resource.memory-mb = 20480 表示这个节点能给 YARN 用的内存总量是 20480MB(20GB)。注意——这是讲义里给生产环境写的值,实验用的虚拟机内存只有 4G,实际得按自己机器的配置改小,否则 NodeManager 会因为"可用内存不足"拒绝启动容器。

hadoop-env.sh 与 mapred-env.sh

两个文件都只要改一行,指定 JAVA_HOME:

export JAVA_HOME=/export/servers/jdk1.8.0_141

注意这里出现了版本问题:编译用的是 JDK 1.7,运行用的是 1.8。这个不一致是讲义里就有的——编译源码时 2.7.5 版本只认 1.7,但集群运行起来用 1.8 没问题。

如果只做实验不编译源码,那全程用 1.8 也可以。关键是这两个 env 文件里的路径要和你实际装的 JDK 对上——路径写错是启动失败最常见的原因,而且报错信息通常很含糊,只提示找不到 Java。

slaves

node01
node02
node03

这个文件列出所有 DataNode 和 NodeManager 节点。启动脚本会读它,自动登录到这些机器上把进程拉起来。

第三步:分发与启动

建目录

配置里写了那么多数据目录,得先手动创建——Hadoop 不会自动建

mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/tempDatas
mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/namenodeDatas
mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/namenodeDatas2
mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/datanodeDatas
mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/datanodeDatas2
mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/nn/edits
mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/snn/name
mkdir -p /export/servers/hadoop-2.7.5/hadoopDatas/dfs/snn/edits

漏建目录的后果是启动时报路径不存在。这类错误很直白,但目录多,容易漏。

分发安装包

cd /export/servers/
scp -r hadoop-2.7.5 node02:$PWD
scp -r hadoop-2.7.5 node03:$PWD

因为 node01 上已经配好了所有文件,直接整个目录复制过去,从节点就不用重复配置了。这也是为什么要在配完文件之后才分发——顺序反了就得配三遍。

分发之后,三台机器都要配 Hadoop 的环境变量:

vim /etc/profile
export HADOOP_HOME=/export/servers/hadoop-2.7.5
export PATH=:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH

source /etc/profile

格式化与启动

首次启动 HDFS 必须先格式化——这时候 HDFS 在物理上还不存在,格式化是做一些初始化的准备工作:

cd /export/servers/hadoop-2.7.5/
bin/hdfs namenode -format
sbin/start-dfs.sh
sbin/start-yarn.sh
sbin/mr-jobhistory-daemon.sh start historyserver

关键的一点:格式化只能在首次启动时做一次。如果集群已经跑过并存了数据,再执行 format 会把 NameNode 的元数据全部清空——虽然 DataNode 上的数据块还在,但 NameNode 不认识它们了,等同于数据丢失。这是新手最容易造成"事故"的操作。

第四步:验证

启动后有三个 Web 界面可以检查集群状态:

地址看什么
http://node01:50070/explorer.html#/HDFS 文件系统,能浏览目录和文件
http://node01:8088/clusterYARN 集群状态,看有哪些节点、跑了哪些任务
http://node01:19888/jobhistory历史任务记录,跑完的 MapReduce 作业在这里查

50070 页面上有个 Live Nodes 计数,正常应该是 3。如果显示 2 或者 0,说明有 DataNode 没起来——去对应的机器上看日志。

回头复盘

整个过程走下来,我觉得最值得记的不是那几段 XML,而是三个教训。

环境准备才是真正花时间的地方。卸载 openjdk、装 JDK 1.7、编译 protobuf 和 snappy、配 Maven 镜像——这些听起来都是"前置工作",但它们加起来占了大部分时间。

更麻烦的是报错指不到真正的原因。JDK 版本不对,可能报一个编译到一半的语法错误;目录没建,只报路径不存在却不说该建哪个。这些信息都不指向根因,只能靠一步步回退定位。所以环境越复杂,越要在每一步之后先确认它能用,再往下走。

版本约束看着像玄学,其实都有原因。"hadoop-2.7.5 只能用 jdk1.7 编译"这一条,讲义里特意加粗强调过。我一开始的想法是"1.8 不是更新吗,用新的应该没问题"——但这是版本间的 API 兼容问题,不是新旧问题。遇到这类硬约束,老老实实按文档来。

格式化只能做一次。这是纪律问题。集群里已经有数据时执行 namenode -format,等于把图书馆的索引全烧了而书还在书架上——DataNode 上的数据块还在,但 NameNode 不认识它们了。养成习惯:只在全新集群上格式化。