Hadoop 架构与安装部署

本文合并自:02-大数据处理架构Hadoop.md、01-Hadoop安装部署.md、02-CentOS基础环境准备.md(Hadoop 集群准备部分)

本文系统讲解 Hadoop 的起源与生态、核心架构(HDFS / YARN / MapReduce)、集群规划与 Linux 前置环境准备、单机 / 伪分布式 / 完全分布式三种模式的完整安装配置、六个核心配置文件逐参数详解、集群格式化与启动验证、常用端口对照、集群扩缩容与排错清单,最后给出练习与答案。所用示例软件为 JDK 8u191 与 Hadoop 2.7.1 / 2.9.2,示例主机名为 server.example.com,示例网段为 192.168.224.0/24。


一、Hadoop 概述与生态

1.1 大数据的特征与处理挑战

大数据通常用 4V 特征概括:

特征 含义 对处理架构的挑战
Volume(大量) 数据规模从 TB 级到 PB、EB 级 单机磁盘容量与单机文件系统无法容纳,必须分布式存储
Velocity(高速) 数据产生与要求处理的速度快 批处理时延需可控,部分场景需要流式计算
Variety(多样) 结构化、半结构化(日志、JSON、XML)、非结构化(图片、音视频) 存储层不能强制 Schema,需"先存储后建模"(Schema-on-Read)
Value(价值密度低) 有效信息稀疏,需从海量原始数据中提炼 必须支持全量数据的低成本长期保存与反复计算

由此带来的三大工程挑战,也正是 Hadoop 要解决的问题:

  1. 存储挑战:单机磁盘容量、IO 带宽、可靠性都有上限。解决思路是把文件切成块(Block)分散到大量廉价机器,并以多副本保证可靠性 —— 即 HDFS。
  2. 计算挑战:把 PB 级数据搬到计算节点,网络会成为瓶颈。解决思路是"移动计算比移动数据更划算",把计算任务调度到数据所在节点执行 —— 即 MapReduce 的数据本地性。
  3. 调度挑战:成百上千节点上同时跑多个作业,需要统一的资源分配与隔离 —— 即 YARN。

详见 16-01-大数据概述.md

1.2 Hadoop 简介

Hadoop 是 Apache 软件基金会旗下的一个开源分布式计算平台,为用户提供了系统底层细节透明的分布式基础架构。Hadoop 基于 Java 语言开发,具有很好的跨平台特性,并且可以部署在廉价的计算机集群中。

Hadoop 的核心是分布式文件系统 HDFS(Hadoop Distributed File System)和 MapReduce。Hadoop 被公认为行业大数据标准开源软件,在分布式环境下提供了海量数据的处理能力。几乎所有主流厂商都围绕 Hadoop 提供开发工具、开源软件、商业化工具和技术服务,如谷歌、雅虎、微软、思科、淘宝等,都支持 Hadoop。

Hadoop 2.x 使用新的 MapReduce 框架(MapReduce V2,即 YARN,Yet Another Resource Negotiator),将资源管理与任务调度从 MapReduce 中分离出来,这是 1.x 与 2.x 最本质的架构差别。

1.3 Hadoop 的起源与发展

Hadoop 最初是由 Apache Lucene 项目的创始人 Doug Cutting 开发的文本搜索库,源自始于 2002 年的 Apache Nutch 项目 —— 一个开源的网络搜索引擎,也是 Lucene 项目的一部分。Nutch 在抓取全网数据时遇到了存储与计算的瓶颈,恰好此时 Google 发表了三篇奠基性论文,Nutch 团队按论文思路做了开源实现。

Google 三篇论文与 Hadoop 组件的对应关系:

Google 论文(年份) 论文主题 Hadoop 生态中的开源实现
GFS(2003) Google File System,分布式文件系统,大文件分块 + 多副本 + 主从元数据管理 HDFS(前身为 Nutch 的 NDFS)
MapReduce(2004) 分布式并行计算编程模型,Map 分而治之 + Reduce 汇总 Hadoop MapReduce
BigTable(2006) 构建在 GFS 之上的分布式结构化数据存储(稀疏、多维、排序 Map) HBase

发展时间线:

  • 2002 年,Apache Nutch 项目启动,作为 Lucene 的子项目开发开源搜索引擎;
  • 2003 年,Google 发表 GFS 论文;
  • 2004 年,Nutch 项目模仿谷歌 GFS 开发了自己的分布式文件系统 NDFS(Nutch Distributed File System),即 HDFS 的前身;
  • 2004 年,谷歌发表论文阐述了 MapReduce 分布式编程思想;
  • 2005 年,Nutch 开源实现了谷歌的 MapReduce;
  • 2006 年 2 月,Nutch 中的 NDFS 和 MapReduce 独立出来,成为 Lucene 项目的一个子项目,称为 Hadoop(名字来自 Doug Cutting 儿子的黄色玩具大象),同时 Doug Cutting 加盟雅虎;
  • 2006 年,Google 发表 BigTable 论文,后来催生了 HBase;
  • 2008 年 1 月,Hadoop 正式成为 Apache 顶级项目,开始被雅虎之外的其他公司使用;
  • 2008 年 4 月,Hadoop 打破世界纪录,成为最快排序 1TB 数据的系统,采用 910 个节点的集群,排序时间仅用 209 秒;
  • 2009 年 5 月,Hadoop 把 1TB 数据排序时间缩短到 62 秒,从此名声大震,迅速发展成为大数据时代最具影响力的开源分布式开发平台,成为事实上的大数据处理标准。

(原图:Hadoop 的标志 —— 黄色小象)

1.4 Apache Hadoop 版本演化

Apache Hadoop 早期版本分为两代,第一代称为 Hadoop 1.0,第二代称为 Hadoop 2.0。

  • 第一代 Hadoop 包含三个大版本:0.20.x、0.21.x 和 0.22.x。其中 0.20.x 最后演化成 1.0.x 稳定版;0.21.x 和 0.22.x 增加了 NameNode HA 等新的重大特性。
  • 第二代 Hadoop 包含两个版本:0.23.x 和 2.x。它们是一套全新的架构,均包含 HDFS Federation 和 YARN 两个系统;相比 0.23.x,2.x 增加了 NameNode HA 和 Wire-compatibility(线兼容性)两个重大特性。

1.x / 2.x / 3.x 关键差异对照表:

维度 Hadoop 1.x Hadoop 2.x Hadoop 3.x
资源调度 JobTracker + TaskTracker,JobTracker 既管资源又管作业,是瓶颈与单点 引入 YARN:ResourceManager + NodeManager + 每作业 ApplicationMaster 沿用 YARN,增强调度器(支持 GPU/FPGA 等资源类型、Opportunistic Container)
计算框架 只能跑 MapReduce YARN 之上可跑 MapReduce、Spark、Tez、Flink 等 同 2.x,生态更广
NameNode 高可用 无 HA,NameNode 单点故障 支持 2 个 NameNode(Active/Standby)+ JournalNode + ZKFC 支持 2 个以上 NameNode(多 Standby)
命名空间扩展 单一 Namespace HDFS Federation(多 NameNode 各管一部分目录树) Federation + Router-Based Federation(RBF)
容错与存储成本 3 副本(200% 额外开销) 3 副本 新增 Erasure Coding(纠删码),冗余开销可降到约 50%
最低 JDK JDK 6 JDK 7(2.7 起要求 JDK 7+) JDK 8+(3.3 起支持 JDK 11 运行时)
默认端口 NameNode Web 50070 NameNode Web 50070 NameNode Web 9870,DataNode 9864/9866/9867,SecondaryNN 9868
从节点清单文件 slaves slaves 改名为 workers
MR 任务本地优化 — — 支持 Task 级本地 Map 输出收集器等优化,Shuffle 性能提升
其他 — 支持 NFS 网关、快照 支持多个 SecondaryNameNode、Intra-DataNode 磁盘均衡器、YARN Timeline Service v2

选择 Hadoop 版本时通常需要考虑:是否开源(即是否免费)、是否有稳定版、是否经实践检验、是否有强大的社区支持。教学与实验环境建议使用 2.7.x / 2.9.x(资料最多、与多数教程一致)或 3.3.x(贴近当前生产)。

1.5 Hadoop 的应用现状

Hadoop 凭借其突出优势,已经在各个领域得到广泛应用,互联网领域是其应用的主阵地:

  • 2007 年,雅虎在 Sunnyvale 总部建立了 M45 —— 一个包含 4000 个处理器和 1.5PB 容量的 Hadoop 集群系统;
  • Facebook 将 Hadoop 平台主要用于日志处理、推荐系统和数据仓库等方面;
  • 国内采用 Hadoop 的公司主要有百度、淘宝、网易、华为、中国移动等,其中淘宝的 Hadoop 集群规模较大。

(原图:Hadoop 在企业中的应用架构)

1.6 Hadoop 的核心组件

组件 全称 作用 主要守护进程
HDFS Hadoop Distributed File System 分布式文件存储,高吞吐、高容错 NameNode、SecondaryNameNode、DataNode
YARN Yet Another Resource Negotiator 集群资源管理与任务调度 ResourceManager、NodeManager
MapReduce — 分布式并行计算编程模型,运行于 YARN 之上 由 YARN 容器调度执行(MRAppMaster、YarnChild)
Common Hadoop Common 底层公共工具库:RPC、序列化、配置、文件系统抽象 无独立进程

三者的职责分工可以概括为一句话:HDFS 负责"数据放在哪",YARN 负责"资源给谁用",MapReduce 负责"计算怎么做"。

  • HDFS 把文件切成固定大小的块分散存储在多台机器上,并对上层屏蔽"文件其实分布在很多机器"这一事实;
  • YARN 把整个集群的 CPU 与内存抽象成资源池,接收各种计算框架的资源申请并分配 Container;
  • MapReduce 是一种编程模型(Map 分解 + Reduce 汇总),它向 YARN 申请资源来运行自己的任务,同时利用 HDFS 提供的块位置信息实现数据本地计算。

1.7 Hadoop 生态圈全景

Hadoop 的项目结构不断丰富发展,已经形成一个丰富的 Hadoop 生态系统,各主要组件及其功能如下表所示。

表:Hadoop 生态系统主要组件及其功能

组件 定位(一句话说明)
HDFS 分布式文件系统,生态的存储基座
MapReduce 分布式并行编程模型,批处理计算引擎
YARN 资源管理和调度器,集群的"操作系统"
Tez 运行在 YARN 之上的下一代 Hadoop 查询处理框架,把 MR 的多轮作业优化成 DAG 一次执行
Hive Hadoop 上的数据仓库,把 SQL(HQL)翻译成 MapReduce / Tez / Spark 作业
HBase Hadoop 上的非关系型分布式数据库,BigTable 的开源实现,支持海量数据的随机实时读写
Pig 一个基于 Hadoop 的大规模数据分析平台,提供类似 SQL 的查询语言 Pig Latin,适合数据流式 ETL 脚本
Sqoop 用于在 Hadoop 与传统关系型数据库之间进行批量数据传输(SQL to Hadoop)
Oozie Hadoop 上的工作流管理系统,用 XML 定义作业依赖并定时调度
ZooKeeper 提供分布式协调一致性服务,是 HA 选主、分布式锁、配置同步的基础设施
Storm 流计算框架,逐条处理消息,低延迟
Flume 一个高可用、高可靠、分布式的海量日志采集、聚合和传输的系统,典型用于把日志汇聚进 HDFS/Kafka
Ambari Hadoop 快速部署工具,支持 Apache Hadoop 集群的供应、管理和监控(图形化)
Kafka 一种高吞吐量的分布式发布订阅消息系统,可以处理消费者规模网站中的所有动作流数据,常作采集与计算之间的缓冲
Spark 类似于 Hadoop MapReduce 的通用并行框架,基于内存计算,含 SQL / Streaming / MLlib / GraphX
Mahout 构建在 Hadoop 之上的可扩展机器学习与数据挖掘算法库(聚类、分类、推荐)
Avro 数据序列化系统,提供跨语言的紧凑二进制格式与 Schema 演进能力
Chukwa 大规模集群的数据收集与监控系统

各组件的详细内容参见对应专篇:

详见 16-05-HBase分布式数据库.md

详见 16-06-ZooKeeper集群.md

详见 16-08-Spark与流计算.md

详见 16-07-Ambari部署Hadoop.md

1.8 Hadoop 的特性

Hadoop 是一个能够对大量数据进行分布式处理的软件框架,以一种可靠、高效、可伸缩的方式进行处理,具有以下特性:

  • 高可靠性:采用冗余数据存储方式(默认 3 副本),即使一个副本发生故障,其他副本也能保证对外正常提供服务;元数据也可配置多目录冗余。
  • 高效性:作为并行分布式计算平台,Hadoop 采用分布式存储和分布式处理两大核心技术,能够高效地处理 PB 级数据;调度时优先把任务放到数据所在节点,减少网络传输。
  • 高可扩展性:Hadoop 的设计目标是可以高效稳定地运行在廉价的计算机集群上,可以扩展到数以千计的计算机节点;扩容只需加机器、改 workers/slaves 文件。
  • 高容错性:自动保存数据的多个副本,并且能够自动将失败的任务重新分配(Task 失败重试、节点失联后副本自动补齐)。
  • 成本低:采用廉价计算机集群,成本比较低,普通用户也很容易用自己的 PC 搭建 Hadoop 运行环境;开源免费无 License 成本。
  • 运行在 Linux 平台上:Hadoop 是基于 Java 语言开发的,可以较好地运行在 Linux 平台上(Windows 需要额外的 winutils 支持,生产环境一律用 Linux)。
  • 支持多种编程语言:Hadoop 上的应用程序也可以使用其他语言编写,如 C++、Python(通过 Hadoop Streaming)。

1.9 适用与不适用场景

适用场景:

  • 海量数据的离线批处理:日志分析、用户行为统计、报表生成、ETL 清洗;
  • 数据仓库的底层存储与计算(配合 Hive);
  • 一次写入、多次读取的全量扫描型分析;
  • 需要低成本长期归档并可随时重新计算的历史数据;
  • 对吞吐量敏感、对单次请求延迟不敏感的任务。

不适用场景:

不适用场景 原因 替代方案
低延迟的随机读写 / 点查 HDFS 面向流式顺序读,寻址开销大;MR 作业启动就要数秒到数十秒 HBase、Redis、关系型数据库
大量小文件存储 每个文件/块的元数据都占用 NameNode 内存(约 150 字节/对象),小文件会撑爆 NameNode 合并成 SequenceFile / HAR 归档 / 用对象存储
文件的随机修改 HDFS 只支持追加(append)与截断,不支持任意位置覆盖写 HBase、数据库
事务与强一致的 OLTP 无事务、无行级锁 MySQL/PostgreSQL 等 OLTP 数据库
实时流式计算(秒级/毫秒级) MapReduce 是批处理模型 Storm、Spark Streaming、Flink
迭代式计算(机器学习) 每轮迭代都要落盘 HDFS,IO 开销大 Spark(内存迭代)

1.10 发行版对比

除 Apache Hadoop 外,常见的 Hadoop 发行版还有 Hortonworks(HDP)、Cloudera(CDH,Cloudera Distribution Hadoop)、MapR 等。

发行版 特点 部署与运维 适用
Apache 原生 完全免费开源,版本最新;组件之间的版本兼容与补丁需自行验证 手工部署,或用 Ambari;升级、打补丁全靠自己 学习、教学、有较强自研能力的团队
CDH(Cloudera) 组件经过整合测试,兼容性好;自带 Cloudera Manager 图形化部署监控;曾有免费 Express 版,CDH 6 之后转为订阅(CDP) Cloudera Manager 一键部署、滚动升级、告警 企业生产,愿付费换稳定与支持
HDP(Hortonworks) 完全开源不闭源,配套 Ambari 管理;2019 年与 Cloudera 合并,后续合并为 CDP Ambari 图形化部署(本文档 16-07 讲解) 企业生产,偏好纯开源栈
MapR 用自研的 MapR-FS 替换 HDFS,支持 POSIX 与 NFS 挂载,无 NameNode 单点 商业闭源,性能强 高性能场景(公司已被 HPE 收购)
华为 FusionInsight 国产化发行版,兼容信创环境(鲲鹏/麒麟),有中文支持与本地化服务 图形化管理,支持国产 CPU/OS 政企、金融的信创项目
阿里云 EMR / 腾讯云 EMR 云上托管:按需开集群、存算分离(数据放对象存储 OSS/COS)、弹性伸缩、按量计费 控制台/API 分钟级开集群,免运维 弹性需求、峰谷差异大、不想自建机房

选型建议:教学与验证用 Apache 原生手工部署(理解每个参数的含义),生产用商业发行版或云上 EMR(降低运维成本)。


二、Hadoop 架构详解

2.1 HDFS 架构

HDFS 采用主从(Master/Slave)架构:一个 NameNode 作为主节点管理元数据,多个 DataNode 作为从节点存储数据块。文件被切分成固定大小的 Block(1.x 默认 64MB,2.x 及以后默认 128MB),每个 Block 保存多个副本(默认 3 份)分布在不同 DataNode 上。

各角色职责:

  • NameNode:管理文件系统命名空间与元数据,记录文件目录树、文件与数据块的映射关系,以及每个数据块所在的 DataNode 列表。它提供整个 HDFS 文件系统的 Namespace(命名空间)管理、块管理等所有服务。所有客户端的读写请求都先访问 NameNode 获取元数据,再直接与 DataNode 传输数据 —— 因此 NameNode 只走控制流,不走数据流。
  • SecondaryNameNode:定期合并 NameNode 的 fsimage 与 edits 日志(CheckPoint),把合并后的新 fsimage 推回 NameNode,从而避免 edits 无限增长导致 NameNode 重启极慢;它保存的元数据副本可用于辅助恢复,但不是热备,不能在 NameNode 宕机时自动接管。真正的热备是 HA 模式下的 Standby NameNode。
  • DataNode:实际存储数据块,负责数据块的读写;定期(默认每 3 秒)向 NameNode 发送心跳,并周期性(默认每 6 小时)发送块报告(Block Report)上报本机所有块信息。NameNode 若在 10 分 30 秒内收不到心跳,就判定该 DataNode 失效并在其他节点补齐副本。

元数据管理机制(重点):

元素 位置 作用
内存中的元数据 NameNode 内存 完整的目录树 + 块映射,保证元数据操作的高速响应
fsimage dfs.namenode.name.dir 目录 某一时刻元数据的完整镜像(快照)
edits(editlog) dfs.namenode.name.dir 目录 自上次镜像以来的所有写操作日志,追加写,恢复时重放
VERSION dfs.namenode.name.dir / dfs.datanode.data.dir 目录 记录 clusterID、namespaceID、storageType 等,NameNode 与 DataNode 的 clusterID 必须一致
块位置信息(BlockMap) 只在 NameNode 内存中,不持久化 由 DataNode 启动时上报的块报告重建 —— 这就是为什么集群重启后需要等 DataNode 上报完才退出安全模式

工作流程:NameNode 启动时先加载 fsimage 到内存,再重放 edits 得到最新元数据;随后进入安全模式,等待 DataNode 上报块信息达到阈值后自动退出。SecondaryNameNode 定期(默认 3600 秒或 edits 达到 100 万条事务)拉取 fsimage + edits 做合并,减轻 NameNode 重启负担。

读写流程简述:

  • 写:Client → NameNode 申请创建文件并获取可写的 DataNode 列表 → Client 与第一个 DataNode 建立管道(Pipeline),数据以 packet 为单位流水线式转发到第二、第三副本 → 全部写完后逐级 ACK → Client 通知 NameNode 完成。
  • 读:Client → NameNode 获取文件各块所在 DataNode(按网络距离排序)→ Client 直接就近读取 DataNode,读完校验 checksum,出错则换副本重试。

2.2 YARN 架构

YARN 把"资源管理"与"作业控制"彻底解耦,各角色职责如下:

  • ResourceManager(RM):集群级资源调度器,全局唯一(HA 下一主一备)。负责接收客户端提交的应用、为每个应用分配第一个 Container 来启动 ApplicationMaster、根据调度策略(FIFO / Capacity / Fair)把资源分配给各个应用。RM 内部包含 Scheduler(只管分资源,不管作业容错)和 ApplicationsManager(管理应用生命周期)。
  • NodeManager(NM):单节点资源管理器,运行在每个从节点上。向 RM 注册并汇报本机可用资源(内存、vcore)与健康状态;按 RM/AM 的指令启动、监控、销毁 Container;对超出资源限制的容器执行 kill;同时为 MapReduce 提供 Shuffle 辅助服务(aux-service)。
  • ApplicationMaster(AM):每个应用一个,运行在某个 Container 中。负责向 RM 申请运行本应用所需的资源、把任务分配到申请到的 Container 中执行、监控任务进度、失败重试。MapReduce 的 AM 叫 MRAppMaster,Spark 的 AM 是 Driver。AM 的存在使 RM 不必关心具体作业细节,这是 YARN 可以承载多种计算框架的关键。
  • Container:资源抽象与隔离单位,封装了一定量的内存、CPU(vcore)等资源。任务只能在 Container 中运行,NM 通过 cgroups 或进程监控保证不超用。

协作流程(提交一个作业的完整链路):

  1. Client 向 RM 提交应用(jar、配置、资源清单);
  2. RM 的 ApplicationsManager 为该应用在某个 NM 上分配第一个 Container,并要求该 NM 启动 ApplicationMaster;
  3. AM 启动后向 RM 注册,此后 Client 可直接向 AM 查询进度;
  4. AM 通过心跳轮询方式向 RM 申请资源(申明需要多少内存/vcore、期望在哪些节点上 —— 数据本地性偏好);
  5. RM 的 Scheduler 根据队列容量与本地性把 Container 分配给 AM;
  6. AM 拿到 Container 后与对应 NM 通信,启动任务(Map/Reduce Task);
  7. 各任务通过心跳向 AM 汇报进度与状态,AM 负责失败任务的重新申请与重试;
  8. 应用运行完毕,AM 向 RM 注销并关闭自己,RM 回收所有资源。

2.3 MapReduce 执行流程概览

MapReduce 采用"分而治之"思想,一个作业分为 Map 与 Reduce 两个阶段:

  1. InputSplit(分片):输入文件按逻辑分片,默认一个 HDFS Block 对应一个 split,一个 split 启动一个 MapTask —— 这决定了 Map 并行度。
  2. Map:逐条读取 <key, value>(默认 TextInputFormat 时 key 是行偏移量,value 是行内容),执行用户的 map() 输出中间 <k2, v2>。
  3. Shuffle & Sort(洗牌与排序):MapReduce 的核心与性能关键。Map 输出先写入环形内存缓冲区(默认 100MB,达 80% 阈值时 spill 溢写到本地磁盘),溢写前按 Reduce 分区(Partitioner,默认 HashPartitioner)排序,可选执行 Combiner 做本地预聚合;多个溢写文件最后归并(Merge)成一个有序文件。Reduce 端通过 NodeManager 的 mapreduce_shuffle 辅助服务从各 Map 节点拉取(Fetch)属于自己分区的数据,再归并排序。
  4. Reduce:对相同 key 的 value 集合执行用户的 reduce(),输出 <k3, v3>。
  5. OutputFormat:结果写回 HDFS,每个 Reduce 生成一个 part-r-000xx 文件,作业成功后生成 _SUCCESS 标记文件。

MapReduce 作业主要包括两类工作:(1)从磁盘或网络读取数据,即 IO 密集工作;(2)计算数据,即 CPU 密集工作。集群整体性能取决于 CPU、内存、网络以及存储之间的性能平衡。

详见 16-04-MapReduce编程.md

2.4 三种运行模式对比

模式 守护进程分布 使用的文件系统 需要的配置 适用场景
单机 / 本地模式(Standalone) 无任何守护进程,所有操作在一个本地 JVM 中运行 本地文件系统(file:///) 解压即用,无需任何 XML 配置,只需 JAVA_HOME 开发调试、MapReduce 业务逻辑验证,方便打断点
伪分布式(Pseudo-Distributed) NameNode / DataNode / SecondaryNameNode / ResourceManager / NodeManager 全部在一台机器上,以分离的 Java 进程运行 HDFS(单副本) core-site.xml、hdfs-site.xml(+ mapred/yarn 可选)、SSH 免密登录本机 学习、功能验证,单机上体验完整分布式行为
完全分布式(Fully-Distributed) 守护进程分布在多台物理机/虚拟机上,主从分离 HDFS(多副本) 六个核心配置文件 + workers/slaves + 全节点 SSH 免密 + hosts 一致 生产环境

Hadoop 的运行方式完全由配置文件决定:只要 core-site.xml 中没有配置 fs.defaultFS,Hadoop 就退回本地模式。因此从伪分布式切回单机模式时,需要删除或注释 core-site.xml 中的相关配置项。


三、集群规划与前置环境准备

3.1 集群角色规划原则

(1)主控角色的部署位置建议

角色 部署建议 原因
NameNode 单独一台内存最大的机器 元数据全部驻留内存,内存决定集群能管多少文件;与其他重进程混布会互相抢内存
ResourceManager 小集群可与 NameNode 同机;中大型集群单独一台 随着集群和 HDFS 文件数量增加,NameNode 需要更多主存,这时二者需要运行在不同节点上
SecondaryNameNode 建议与 NameNode 分开,硬件配置与 NameNode 相同 SecondaryNameNode 做 CheckPoint 时需要把 fsimage 全量载入内存,与 NameNode 几乎具有相同的主存需求;放一起会争抢内存,且 NameNode 磁盘损坏时元数据副本一起丢失。小型集群中可以和 NameNode 共用一台机器
DataNode + NodeManager 必须同机部署 见下文数据本地性
JobHistoryServer 与 RM 同机或单独部署 只负责查询历史,资源占用小

对于小集群,NameNode 和 JobTracker(2.x 中为 ResourceManager)运行在单个节点上通常可以接受。

(2)DataNode 与 NodeManager 同机部署 —— 数据本地性原理

Hadoop 的核心设计思想是"移动计算比移动数据更划算"。DataNode 持有数据块,NodeManager 负责在本机启动计算 Container。只有二者部署在同一台机器上,YARN 才能把 MapTask 调度到"输入分片正好在本地磁盘"的节点上,从而直接读本地磁盘而不走网络。

调度器的本地性优先级依次为:

  1. NODE_LOCAL(数据就在本节点磁盘)—— 最优,无网络传输;
  2. RACK_LOCAL(数据在同机架的其他节点)—— 走机架内交换机;
  3. OFF_SWITCH / ANY(跨机架)—— 最差,占用核心交换机带宽。

如果把 DataNode 和 NodeManager 分开部署,所有任务都会退化为 OFF_SWITCH,集群吞吐会急剧下降。这也是 Hadoop 的机架感知(Rack Awareness)要配合正确的网络拓扑脚本的原因。

(3)集群硬件配置建议

集群中大部分机器作为 DataNode 和 NodeManager(1.x 中为 TaskTracker)工作,其硬件规格可采用以下方案:

  • 4 个磁盘驱动器(单盘 1–2T),支持 JBOD(Just a Bunch Of Disks,磁盘簇)—— 注意不要做 RAID,HDFS 自身有副本机制,JBOD 的吞吐与容量利用率都更好;
  • 2 个 4 核 CPU,至少 2–2.5GHz;
  • 16–24GB 内存;
  • 千兆以太网。

NameNode 需要更多 RAM(与集群中数据块数量相对应),并需优化 RAM 的内存通道带宽,采用双通道或三通道以上内存。硬件规格可采用:

  • 8–12 个磁盘驱动器(单盘 1–2T);
  • 2 个 4 核 / 8 核 CPU;
  • 16–72GB 内存;
  • 千兆 / 万兆以太网。

SecondaryNameNode 在小型集群中可以和 NameNode 共用一台机器,较大的集群可以采用与 NameNode 相同的硬件。

(4)集群规模估算

Hadoop 集群规模可大可小,初始可从较小规模开始(如 10 个节点),然后随存储器和计算需求扩大而扩大。

估算示例:如果数据每周增大 1TB,并且有三个 HDFS 副本,则每周需要额外 3TB 作为原始数据存储;考虑中间文件和日志(假定 30%)的空间,可算出每周大约需要增加一台新机器,存储两年数据的集群大约需要 100 台机器。

(5)集群网络拓扑

普通的 Hadoop 集群结构由一个两阶网络构成:每个机架(Rack)有 30–40 个服务器,配置一个 1GB 的交换机,并向上传输到一个核心交换机或路由器(1GB 或以上)。在相同机架中的节点间带宽总和,要大于不同机架间节点间带宽总和,因此副本放置策略默认为:第一副本放本机(或随机一台),第二副本放不同机架的节点,第三副本放第二副本的同机架不同节点 —— 兼顾容错与写带宽。

(6)实验环境与生产环境的差异

维度 实验 / 教学环境 生产环境
节点数 1~3 台虚拟机 数十至数千台物理机
副本数 dfs.replication 1(伪分布式)或 2 3
运行用户 常直接用 root(简化) 必须用专用低权限用户 hadoop,root 启动是重大安全隐患
防火墙 / SELinux 直接关闭 保持开启,逐端口放行;启用 Kerberos 认证
元数据目录 单目录 dfs.namenode.name.dir 配多个不同物理磁盘的目录做冗余,并另配 NFS 挂载点
HA 不配 NameNode HA + ResourceManager HA 必配
SecondaryNameNode 与 NameNode 同机 HA 模式下由 Standby NameNode 承担 CheckPoint,不再需要 SNN
监控 jps + Web UI Prometheus/Zabbix + Ambari/CM,磁盘、GC、块丢失告警
数据目录 与安装目录混在 /usr/local/hadoop/tmp 独立数据盘挂载点,如 /data01/dfs/data、/data02/dfs/data

3.2 本文实验环境规划

软件清单:

1. Java: jdk-8u191-linux-x64.tar.gz
2. hadoop-2.7.1.tar.gz(或 hadoop-2.9.2.tar.gz)

安装目录约定:

  • JDK 解压并改名为 /usr/local/jdk
  • Hadoop 解压到 /usr/local/hadoop

双节点最小集群规划(与源文档一致):

主机名 IP 角色 运行进程
server.example.com(Master) 192.168.224.10 主节点 NameNode、SecondaryNameNode、ResourceManager
client1.example.com(Slave) 192.168.224.11 从节点 DataNode、NodeManager

三节点推荐规划(教学演练更贴近真实):

主机名 IP 角色 运行进程
hadoop01 192.168.224.10 Master NameNode、ResourceManager、JobHistoryServer
hadoop02 192.168.224.11 Slave SecondaryNameNode、DataNode、NodeManager
hadoop03 192.168.224.12 Slave DataNode、NodeManager

伪分布式环境规划:单机 server.example.com(示例 IP 192.168.224.10 或 192.168.224.132),同时承担全部 6 个角色进程。

3.3 前置准备逐项步骤

以下步骤每个节点都要做,除特别说明外。建议先在一台机器上做完所有准备工作再克隆虚拟机(克隆后记得改 IP、主机名、清理 MAC 地址与 UUID)。

3.3.1 配置静态 IP

Hadoop 集群的所有配置都写死了主机名/IP,一旦 DHCP 续租后 IP 变化,整个集群就崩了,因此必须配置静态 IP。

CentOS 6 / CentOS 7 通用(修改网卡配置文件,CentOS 6 一般为 ifcfg-eth0,CentOS 7 一般为 ifcfg-ens33):

vim /etc/sysconfig/network-scripts/ifcfg-ens33
TYPE=Ethernet
BOOTPROTO=static          # 关键:由 dhcp 改为 static
NAME=ens33
DEVICE=ens33
ONBOOT=yes                # 关键:开机自动启用
IPADDR=192.168.224.10
NETMASK=255.255.255.0
GATEWAY=192.168.224.2
DNS1=192.168.224.2
DNS2=8.8.8.8

重启网络服务:

# CentOS 6
service network restart
# CentOS 7
systemctl restart network
# 验证
ip addr show
ping -c 3 www.baidu.com

详见 07-02-网络配置与远程管理.md

3.3.2 设置主机名

CentOS 6:

# 永久生效:修改配置文件
vim /etc/sysconfig/network
NETWORKING=yes
HOSTNAME=server.example.com
# 临时生效(当前会话立即生效,重启失效)
hostname server.example.com
# 查看
hostname

CentOS 6 修改 /etc/sysconfig/network 后需重启系统(或配合 hostname 命令)才完全生效。

CentOS 7:

# 一条命令永久生效(写入 /etc/hostname),无需重启
hostnamectl set-hostname server.example.com
# 查看详细信息
hostnamectl status
hostname
# 也可直接编辑(等价)
vim /etc/hostname

CentOS 7 中 /etc/sysconfig/network 里的 HOSTNAME 已不再生效,必须用 hostnamectl 或 /etc/hostname。

主机名不要使用下划线 _,Hadoop/Java 的 URI 解析对下划线支持不佳;建议全小写。

3.3.3 配置 /etc/hosts(最常见的故障源)

所有节点的 /etc/hosts 内容必须完全一致,且必须包含集群内每一台机器的映射。

vim /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.224.10  server.example.com  hadoop01  master
192.168.224.11  client1.example.com hadoop02  slave1
192.168.224.12  client2.example.com hadoop03  slave2

这是 Hadoop 集群最常见的故障源,务必注意以下几点:

  1. 必须逐台分发保持一致。任何一台机器缺少某个映射,就会出现"NameNode 认得 DataNode,DataNode 却连不上 NameNode"的诡异现象。分发命令:

    for h in hadoop02 hadoop03; do scp /etc/hosts root@$h:/etc/hosts; done
  2. 绝对不要把本机主机名映射到 127.0.0.1。例如写成 127.0.0.1 hadoop01 会导致 NameNode 把自己的 RPC 地址注册为 127.0.0.1,其他节点连过来时被拒绝,表现为 DataNode 日志中反复出现 Retrying connect to server: hadoop01/127.0.0.1:9000。

  3. CentOS 7 安装时自动写入的 127.0.0.1 <hostname> 一定要删掉。

  4. 主机名与 IP 之间用空格或 Tab 分隔,一行可以有多个别名。

  5. 验证方法:

    ping -c 2 hadoop02
    getent hosts hadoop02        # 确认解析结果
    hostname -i                  # 应返回真实 IP 而不是 127.0.0.1
    hostname -f                  # 返回 FQDN

3.3.4 关闭防火墙与 SELinux

Hadoop 各角色之间要使用十几个端口互相通信,实验环境为省事直接关闭。

关闭防火墙 —— CentOS 6(iptables):

service iptables stop            # 停止当前
chkconfig iptables off           # 禁止开机启动
service iptables status          # 验证
chkconfig --list iptables

关闭防火墙 —— CentOS 7(firewalld):

systemctl stop firewalld         # 停止当前
systemctl disable firewalld      # 禁止开机启动
systemctl status firewalld       # 验证,应显示 inactive (dead)
firewall-cmd --state             # 应显示 not running

关闭 SELinux(CentOS 6 / 7 通用):

# 临时关闭(立即生效,重启失效)
setenforce 0
getenforce                       # 应显示 Permissive

# 永久关闭(需重启生效)
vim /etc/selinux/config
SELINUX=disabled                 # 由 enforcing 改为 disabled
SELINUXTYPE=targeted
# 一条 sed 命令搞定
sed -i 's/^SELINUX=enforcing$/SELINUX=disabled/' /etc/selinux/config
# 重启后验证
sestatus

生产环境的正确做法:不要关闭,而是按端口放行。 关闭防火墙与 SELinux 只适用于隔离的实验网络,生产环境应保留防护并精确放行 Hadoop 所需端口:

# CentOS 7:为 Hadoop 创建专用 zone 或直接放行端口
firewall-cmd --permanent --add-port=9000/tcp     # NameNode RPC
firewall-cmd --permanent --add-port=50070/tcp    # NameNode Web (2.x)
firewall-cmd --permanent --add-port=9870/tcp     # NameNode Web (3.x)
firewall-cmd --permanent --add-port=50090/tcp    # SecondaryNameNode Web (2.x)
firewall-cmd --permanent --add-port=50010/tcp    # DataNode 数据传输
firewall-cmd --permanent --add-port=50020/tcp    # DataNode IPC
firewall-cmd --permanent --add-port=50075/tcp    # DataNode Web
firewall-cmd --permanent --add-port=8088/tcp     # ResourceManager Web
firewall-cmd --permanent --add-port=8030-8033/tcp # RM 内部各接口
firewall-cmd --permanent --add-port=8040-8042/tcp # NodeManager
firewall-cmd --permanent --add-port=13562/tcp    # Shuffle
firewall-cmd --permanent --add-port=10020/tcp    # JobHistory RPC
firewall-cmd --permanent --add-port=19888/tcp    # JobHistory Web
firewall-cmd --reload
firewall-cmd --list-all

# 更简洁:信任整个集群内网网段(推荐)
firewall-cmd --permanent --zone=trusted --add-source=192.168.224.0/24
firewall-cmd --reload
# CentOS 6:iptables 放行示例
iptables -I INPUT -s 192.168.224.0/24 -j ACCEPT
service iptables save
service iptables restart

SELinux 在生产中若必须开启,需要为 Hadoop 的数据目录打上正确的上下文标签,并用 audit2allow 从 /var/log/audit/audit.log 中生成策略模块,实践中多数发行版指南仍建议设为 permissive。

详见 07-03-防火墙与SELinux.md

3.3.5 配置时间同步

集群节点时间不一致会导致:DataNode 心跳被 NameNode 判为异常、HBase RegionServer 直接拒绝启动(ClockOutOfSyncException)、Kerberos 票据校验失败、日志时间错乱难以排查、YARN 任务时间统计错误。要求全集群时间误差在秒级以内。

CentOS 6(ntpd):

yum install -y ntp
vim /etc/ntp.conf
# 注释掉默认的公网 server,改为内网时间源或阿里云 NTP
server ntp1.aliyun.com iburst
server 192.168.224.10 iburst      # 或指定 Master 为内网时间服务器
# 允许本网段客户端同步(Master 作为服务端时配置)
restrict 192.168.224.0 mask 255.255.255.0 nomodify notrap
# 外网不可用时使用本地时钟
server 127.127.1.0
fudge  127.127.1.0 stratum 10
service ntpd start
chkconfig ntpd on
ntpq -p                     # 查看同步状态,* 号表示已选中的时间源
date
# 手动强制同步一次(需先停 ntpd)
service ntpd stop
ntpdate ntp1.aliyun.com
service ntpd start

CentOS 7(chronyd,推荐):

yum install -y chrony
vim /etc/chrony.conf
server ntp1.aliyun.com iburst
server ntp2.aliyun.com iburst
# Master 作为内网时间服务器时增加:
allow 192.168.224.0/24
local stratum 10
systemctl start chronyd
systemctl enable chronyd
systemctl status chronyd
chronyc sources -v          # 查看时间源,^* 表示当前同步源
chronyc tracking            # 查看偏差
timedatectl                 # 查看时区与 NTP 状态
timedatectl set-timezone Asia/Shanghai   # 统一时区,全集群必须一致

批量校验各节点时间:

for h in hadoop01 hadoop02 hadoop03; do echo -n "$h: "; ssh $h date; done

详见 08-03-时间同步与Xinetd.md

3.3.6 创建专用用户 hadoop 与 sudo 授权

生产环境不要用 root 运行 Hadoop。Hadoop 3.x 更是默认拒绝 root 启动(需额外设置环境变量,见 5.4 节)。

如果安装系统时没有使用 hadoop 用户,需要增加一个名为 hadoop 的用户。

CentOS(RHEL 系):

useradd -m hadoop -s /bin/bash          # 创建可登录的 hadoop 用户,使用 /bin/bash 作为 shell
passwd hadoop                           # 设置密码(实验环境可简单设为 hadoop)
# 加入 wheel 组以获得 sudo 权限
usermod -aG wheel hadoop
id hadoop

Ubuntu/Debian:

sudo useradd -m hadoop -s /bin/bash
sudo passwd hadoop
sudo adduser hadoop sudo                # 为 hadoop 用户增加管理员权限,方便部署

单独授权(免密 sudo,便于自动化脚本):

visudo                                  # 或 vim /etc/sudoers.d/hadoop
hadoop  ALL=(ALL)  NOPASSWD: ALL
chmod 440 /etc/sudoers.d/hadoop         # sudoers 片段文件权限必须是 440

注意事项:

  • 所有节点的 hadoop 用户 UID/GID 建议保持一致,否则 HDFS 文件属主在不同节点上显示不同,且可能出现权限异常。可显式指定:useradd -u 1500 -m hadoop。
  • 后续的 JDK/Hadoop 安装目录属主要改为 hadoop:chown -R hadoop:hadoop /usr/local/hadoop。
  • SSH 免密要为实际运行 Hadoop 的那个用户配置。如果用 root 启动就给 root 配,用 hadoop 启动就给 hadoop 配 —— 这是新手最容易搞错的地方。

关于 sudo 与 Linux 基础的补充说明:

  • Shell:提供使用者使用界面的软件(命令解析器),类似于 DOS 下的 command 和后来的 cmd.exe,接收用户命令并调用相应应用程序。
  • sudo 命令:权限管理机制,管理员可授权普通用户执行需要 root 权限的操作,使用时需输入当前用户密码。
  • 输入密码:Linux 终端输入密码时不会显示任何字符,也不会提示已输入字符数,并非键盘无响应。
  • 终端复制粘贴:Ubuntu 终端中复制粘贴快捷键需加 Shift,即粘贴为 Ctrl+Shift+V。

3.3.7 配置 SSH 免密登录(重点)

为什么必须配置: Hadoop 名称节点(NameNode)需要启动集群中所有机器的 Hadoop 守护进程,这个过程是通过 start-dfs.sh / start-yarn.sh 脚本逐台 SSH 登录从节点执行 hadoop-daemon.sh start ... 实现的。Hadoop 没有提供 SSH 输入密码登录的形式,因此需要将所有机器配置为主节点可以无密码登录。

SSH(Secure Shell)是建立在应用层和传输层基础上的安全协议,专为远程登录会话和其他网络服务提供安全性,可有效防止远程管理过程中的信息泄露。SSH 由客户端和服务端软件组成:服务端是一个守护进程(daemon),在后台运行并响应客户端连接请求;客户端包含 ssh 程序以及 scp(远程拷贝)、slogin(远程登录)、sftp(安全文件传输)等应用程序。

(1)安装并启动 sshd(一般已自带)

rpm -qa | grep openssh
yum install -y openssh-clients openssh-server
# CentOS 6
service sshd start; chkconfig sshd on
# CentOS 7
systemctl start sshd; systemctl enable sshd

(2)生成密钥对

在每个需要发起 SSH 的节点上(至少主节点,建议全部节点都做,便于互相操作)用运行 Hadoop 的那个用户执行:

su - hadoop                     # 若用 root 运行 Hadoop 则执行 su -
ssh-keygen                      # 一路按回车,不要设置 passphrase
# 或指定类型与免交互生成
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa

生成的文件:

文件 说明
~/.ssh/id_rsa 私钥,权限必须 600,绝不外传
~/.ssh/id_rsa.pub 公钥,要追加到目标机的 authorized_keys
~/.ssh/authorized_keys 授权公钥列表,权限必须 600
~/.ssh/known_hosts 已确认过的主机指纹
~/.ssh 目录 权限必须 700

ssh-keygen 若设置了 passphrase,启动脚本仍会卡住要求输入口令,所以必须留空。

(3)把公钥分发到所有节点(含自己)

方法一:ssh-copy-id(推荐,会自动创建目录并设置正确权限):

ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop@hadoop01      # 自己到自己也必须做
ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop@hadoop02
ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop@hadoop03
# 源文档写法(root 用户、按 IP)
ssh-copy-id -i /root/.ssh/id_rsa.pub root@192.168.224.11
ssh-copy-id -i /root/.ssh/id_rsa.pub root@192.168.224.1x

方法二:本机免密(把自己的公钥追加到自己的授权文件):

su
# 或(从普通用户切换到 root 的登录 shell,会加载 root 的环境变量)
sudo su -
ssh-keygen                                        # 一路按回车
cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys
# 等价写法
cd /root/.ssh
cat id_rsa.pub >> authorized_keys

Hadoop 的启动脚本通过 SSH 管理各节点守护进程,伪分布式下即使只有一台机器,也必须先完成上面这步 root(或 hadoop 用户)到本机的免密配置,随后 ssh localhost 应能直接登录、不再提示输入密码。

方法三:手工 scp + 追加(无 ssh-copy-id 时):

scp ~/.ssh/id_rsa.pub hadoop@hadoop02:/tmp/hadoop01.pub
ssh hadoop@hadoop02 'mkdir -p ~/.ssh && cat /tmp/hadoop01.pub >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys'

(4)权限要求(最容易踩的坑)

sshd 出于安全考虑会严格检查权限,权限过宽时直接忽略 authorized_keys 而回退到密码登录(不报错,只是"免密失效"):

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub
chmod 700 ~                     # 家目录不能是 777!组或其他人有写权限也会导致失败
chown -R hadoop:hadoop ~/.ssh

(5)验证方法

ssh localhost                   # 伪分布式必须验证这一条
ssh 0.0.0.0                     # start-dfs.sh 启动 SecondaryNameNode 时会用到 0.0.0.0
ssh hadoop01                    # 自己到自己(用主机名)
ssh hadoop02
exit
# 批量验证,全部不提示输入密码即为成功
for h in hadoop01 hadoop02 hadoop03; do ssh $h hostname; done
# 排错时看详细过程
ssh -vvv hadoop02

第一次连接会提示 Are you sure you want to continue connecting (yes/no)?,输入 yes 后写入 known_hosts,后续不再提示。可提前批量写入避免脚本卡住:

ssh-keyscan -H hadoop01 hadoop02 hadoop03 >> ~/.ssh/known_hosts
# 或在 ~/.ssh/config 中关闭确认(实验环境)
cat >> ~/.ssh/config <<'EOF'
Host *
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
EOF
chmod 600 ~/.ssh/config

(6)不配免密会出现什么现象

执行 start-dfs.sh 时脚本会停住等待输入密码,输出类似:

Starting namenodes on [hadoop01]
hadoop01: root@hadoop01's password:
hadoop02: root@hadoop02's password:
Starting secondary namenodes [0.0.0.0]
0.0.0.0: root@0.0.0.0's password:

此时即使手工输入密码也能启动,但每台机器都要输一次,重启集群极其痛苦;若在自动化脚本或 crontab 中执行则会直接卡死或超时失败。特别注意:自己到自己(localhost、本机主机名、0.0.0.0)也必须免密,否则 NameNode 和 SecondaryNameNode 都启动不了 —— 这是最常被忽略的一点。

3.3.8 安装 JDK 与配置 JAVA_HOME

Hadoop 基于 Java 开发,所有节点都必须安装相同版本的 JDK。Hadoop 2.7+ 要求 JDK 7 以上,推荐 JDK 8;Hadoop 3.x 要求 JDK 8 以上。

(1)判断并卸载系统自带的 OpenJDK

CentOS 最小化安装可能自带 OpenJDK 的 JRE(只有运行环境没有编译环境),会与手工安装的 Oracle JDK 冲突,java -version 指向错误版本。

# 判断是否自带
java -version
rpm -qa | grep -i java
rpm -qa | grep -i jdk
rpm -qa | grep openjdk
which java
readlink -f $(which java)          # 看真实指向,若指向 /usr/lib/jvm/... 即为系统自带

若输出中包含 java-1.7.0-openjdk、java-1.8.0-openjdk-headless、tzdata-java 等,且需要用 Oracle JDK,则卸载:

rpm -qa | grep -i java | xargs -n1 rpm -e --nodeps
# 或
yum remove -y java-1.7.0-openjdk* java-1.8.0-openjdk*
# 确认卸载
java -version                      # 应提示 command not found

判断依据:只要 javac -version 不可用(只有 JRE 没有 JDK),或版本低于 1.8,就必须替换。若自带的已是 java-1.8.0-openjdk-devel(含 javac),完全可以直接使用,不必卸载,只需正确设置 JAVA_HOME 指向 /usr/lib/jvm/java-1.8.0-openjdk。

Ubuntu 上直接安装 OpenJDK 的方式:

sudo apt-get install openjdk-7-jre openjdk-7-jdk
# 或 JDK 8
sudo apt-get install openjdk-8-jdk

(2)解压 JDK 与 Hadoop

# Java 解压后改名为 /usr/local/jdk
# Hadoop 解压到 /usr/local/hadoop
tar -zxvf jdk-8u191-linux-x64.tar.gz -C /usr/local/
tar -zxvf hadoop-2.7.1.tar.gz -C /usr/local/
cd /usr/local/
mv jdk1.8.0_191/ jdk
mv hadoop-2.7.1/ hadoop

(若使用 2.9.2 版本,则对应命令为 tar -zxvf hadoop-2.9.2.tar.gz -C /usr/local/ 与 mv hadoop-2.9.2/ hadoop。)

(3)JAVA_HOME 的多种配置位置

配置位置 生效范围 说明
/etc/profile 所有用户的登录 shell 最常用;修改后需 source /etc/profile
/etc/profile.d/hadoop.sh 所有用户的登录 shell 推荐,不污染主文件,便于管理与回滚
~/.bash_profile 单个用户的登录 shell 只给 hadoop 用户用时适合
~/.bashrc 单个用户的每次 bash(含非登录交互式) SSH 执行远程命令时会加载它,比 profile 更"可靠"
/etc/environment 全系统(不支持变量展开与脚本语法) 仅能写 KEY=value
hadoop-env.sh 仅 Hadoop 进程 必须配置,见下

全局环境变量(源文档写法):

vim /etc/profile
export JAVA_HOME=/usr/local/jdk
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:
export CLASS_PATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

使配置生效并确认:

source /etc/profile
echo $PATH                        # 确认环境变量生效
java -version
javac -version
echo $JAVA_HOME

推荐的 /etc/profile.d/hadoop.sh 写法(更完整):

vim /etc/profile.d/hadoop.sh
export JAVA_HOME=/usr/local/jdk
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export HADOOP_HOME=/usr/local/hadoop
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_YARN_HOME=$HADOOP_HOME
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
export HADOOP_OPTS="-Djava.library.path=$HADOOP_HOME/lib/native"
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
chmod +x /etc/profile.d/hadoop.sh
source /etc/profile.d/hadoop.sh

注意源文档中 CLASS_PATH 实为 CLASSPATH 的笔误(Java 识别的变量名是 CLASSPATH),两种写法都不影响 Hadoop 运行(Hadoop 通过 hadoop classpath 自行组装类路径),但建议写成 CLASSPATH。

3.3.9 调整 ulimit 资源限制

Hadoop 进程会打开大量文件句柄(每个 Block 一个文件、大量 socket 连接)并创建大量线程,默认 1024 的限制远远不够,典型报错是 java.io.IOException: Too many open files、Cannot create native thread、DataNode 频繁掉线。

查看当前限制:

ulimit -a
ulimit -n                          # 最大打开文件数 nofile
ulimit -u                          # 最大进程/线程数 nproc
# 查看某个已运行进程的实际限制
cat /proc/$(pgrep -f NameNode | head -1)/limits

临时修改(当前会话有效):

ulimit -n 65535
ulimit -u 65535

永久修改 —— /etc/security/limits.conf(CentOS 6 / 7 通用):

vim /etc/security/limits.conf
# <domain>  <type>  <item>   <value>
*           soft    nofile   65535
*           hard    nofile   65535
*           soft    nproc    65535
*           hard    nproc    65535
*           soft    memlock  unlimited
*           hard    memlock  unlimited

# 也可只针对 hadoop 用户(更规范)
hadoop      soft    nofile   65535
hadoop      hard    nofile   131072
hadoop      soft    nproc    65535
hadoop      hard    nproc    65535

CentOS 6 必做:/etc/security/limits.d/90-nproc.conf 中默认有 * soft nproc 1024,会覆盖 limits.conf,必须同步修改:

vim /etc/security/limits.d/90-nproc.conf
*          soft    nproc     65535
root       soft    nproc     unlimited

CentOS 7 必做:/etc/security/limits.d/20-nproc.conf 同理:

vim /etc/security/limits.d/20-nproc.conf
*          soft    nproc     65535
root       soft    nproc     unlimited

修改后必须重新登录(limits.conf 由 PAM 在登录时生效,对已有会话无效),验证:

exit
ssh hadoop@hadoop01
ulimit -n                          # 应显示 65535

CentOS 7 特有 —— systemd 管理的服务不读 limits.conf。 如果把 Hadoop 做成 systemd service,或者通过 systemd 拉起的进程,必须在 unit 文件中显式声明:

vim /etc/systemd/system/hadoop-namenode.service
[Service]
LimitNOFILE=65535
LimitNPROC=65535
LimitMEMLOCK=infinity
systemctl daemon-reload
systemctl restart hadoop-namenode
# 或全局设置默认值
vim /etc/systemd/system.conf
DefaultLimitNOFILE=65535
DefaultLimitNPROC=65535
systemctl daemon-reexec

3.3.10 关闭 THP 与调整 swappiness

(1)关闭透明大页 THP(Transparent Huge Pages)

THP 的内存整理(khugepaged)会造成不可预测的 CPU 毛刺与 GC 停顿,Java 大堆场景下 Cloudera/Hortonworks 官方均要求关闭。启动时若未关闭,Ambari/CM 会给出警告。

# 检查当前状态
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
# CentOS 6 部分内核路径为:
cat /sys/kernel/mm/redhat_transparent_hugepage/enabled

输出中 [always] 表示开启,[never] 表示已关闭。

# 临时关闭(立即生效,重启失效)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

永久关闭方法一:写入 /etc/rc.local(CentOS 7 需给 rc.local 加执行权限):

vim /etc/rc.local
if test -f /sys/kernel/mm/transparent_hugepage/enabled; then
   echo never > /sys/kernel/mm/transparent_hugepage/enabled
fi
if test -f /sys/kernel/mm/transparent_hugepage/defrag; then
   echo never > /sys/kernel/mm/transparent_hugepage/defrag
fi
chmod +x /etc/rc.d/rc.local        # CentOS 7 必须

永久关闭方法二:改内核启动参数(更彻底):

# CentOS 7
vim /etc/default/grub
# 在 GRUB_CMDLINE_LINUX 中追加 transparent_hugepage=never
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot
# CentOS 6:编辑 /boot/grub/grub.conf,在 kernel 行末尾追加 transparent_hugepage=never

(2)调整 swappiness

Java 进程的堆内存被换出到 swap 会造成 GC 时间剧增甚至 DataNode 心跳超时被判死。应尽量避免使用 swap。

# 查看当前值(CentOS 6 默认 60,CentOS 7 默认 30)
cat /proc/sys/vm/swappiness
sysctl vm.swappiness

# 临时生效
sysctl -w vm.swappiness=10
echo 10 > /proc/sys/vm/swappiness

# 永久生效
vim /etc/sysctl.conf
vm.swappiness = 10
vm.overcommit_memory = 1
vm.min_free_kbytes = 1048576
net.core.somaxconn = 32768
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 1000000
sysctl -p                          # 重新加载
sysctl -a | grep swappiness        # 验证
free -m                            # 观察 swap 使用量

建议设为 1~10 而不是 0。设为 0 在较新内核上会让 OOM Killer 更激进,可能直接杀掉 DataNode。

3.3.11 创建数据目录与规划磁盘挂载点

(1)实验环境(源文档写法,数据目录在安装目录下)

mkdir /usr/local/hadoop/dfs /usr/local/hadoop/dfs/name /usr/local/hadoop/dfs/data
# 或一次性创建
mkdir -p /usr/local/hadoop/dfs/{name,data}
mkdir -p /usr/local/hadoop/tmp
mkdir -p /usr/local/hadoop/logs
chown -R hadoop:hadoop /usr/local/hadoop

如果遇到启动问题,需要清空上述目录后重新格式化。

(2)生产环境的磁盘挂载规划

生产环境应把数据目录放在独立的数据盘上,并且 dfs.datanode.data.dir 配置多个目录、每个目录对应一块独立物理盘,Hadoop 会以轮转(round-robin)方式写入,从而并行利用所有磁盘的 IO 带宽。

# 假设有 4 块数据盘 sdb sdc sdd sde
for d in b c d e; do
  mkfs.ext4 /dev/sd${d}1
  mkdir -p /data0$( echo $d | tr 'bcde' '1234' )
done

# 挂载并写入 fstab(noatime 减少元数据写入,提升性能)
vim /etc/fstab
UUID=xxxx-xxxx  /data01  ext4  defaults,noatime,nodiratime  0 0
UUID=yyyy-yyyy  /data02  ext4  defaults,noatime,nodiratime  0 0
UUID=zzzz-zzzz  /data03  ext4  defaults,noatime,nodiratime  0 0
UUID=wwww-wwww  /data04  ext4  defaults,noatime,nodiratime  0 0
mount -a
df -h

# 创建数据目录
mkdir -p /data0{1,2,3,4}/dfs/data          # DataNode 数据(每盘一个)
mkdir -p /data01/dfs/name /data02/dfs/name # NameNode 元数据(跨盘冗余)
mkdir -p /data0{1,2,3,4}/yarn/local        # YARN 中间数据(shuffle 临时文件)
mkdir -p /data0{1,2,3,4}/yarn/logs         # YARN 容器日志
mkdir -p /data01/dfs/namesecondary         # SecondaryNameNode 检查点目录
chown -R hadoop:hadoop /data0{1,2,3,4}
chmod 700 /data0{1,2,3,4}/dfs/data         # DataNode 目录权限必须是 700,否则启动报错

磁盘规划要点:

  • 数据盘不做 RAID,用 JBOD;系统盘可做 RAID1 保护 OS 与 NameNode 元数据;
  • dfs.datanode.data.dir 一盘一目录,用逗号分隔;某块盘坏了不影响其他盘(配合 dfs.datanode.failed.volumes.tolerated);
  • dfs.namenode.name.dir 配 2 个以上不同物理盘的目录,Hadoop 会同步写入所有目录做冗余;生产上再加一个 NFS 挂载点;
  • 为 OS 与日志预留至少 10~20% 空间,dfs.datanode.du.reserved 设置为每盘预留字节数(如 10GB),防止磁盘写满导致 DataNode 变 unhealthy。

3.4 安装前的预备知识(操作系统选择与安装方式)

详见 阶段01(Linux 系统安装与初始化)

以下内容摘自源文档,属于系统安装阶段的旁支知识,此处保留以便对照。

(一)Linux 的选择

  • 发行版选择:CentOS 和 Ubuntu 在服务端和桌面端使用占比最高、网络资料最齐全,建议使用 CentOS 或 Ubuntu;学习 Hadoop 推荐使用 Ubuntu(本文档正文以 CentOS 为准)。
  • 位数选择:电脑较老或内存小于 2G 建议选择 32 位版本;内存大于 4G 建议选择 64 位版本。Hadoop 生产环境一律 64 位。

(二)系统安装方式:虚拟机还是双系统

  • 电脑较新或内存 4G 以上可选择虚拟机安装;
  • 电脑较旧或内存小于等于 4G 强烈建议选择双系统安装,否则在低配计算机上运行 Linux 虚拟机会非常慢;
  • 鉴于教学中硬件配置一般不高,实践教学中建议采用双系统安装以确保运行速度。

(三)安装 Linux 虚拟机

一、材料和工具:

  1. 下载 VirtualBox 虚拟机软件(或 VMware Workstation);
  2. 下载 Ubuntu LTS 14.04 ISO 映像文件(或 CentOS 7 ISO)。

二、步骤:

(1)确认系统版本:若选择 64 位系统,安装虚拟机前需进入 BIOS 开启 CPU 虚拟化(Intel VT-x / AMD-V)。

(2)安装前的准备:

  1. 打开 VirtualBox,点击"创建"按钮创建虚拟机;
  2. 给虚拟机命名,选择操作系统和版本;
  3. 选择内存大小(示例设置为 1024M,跑 Hadoop 建议至少 2048M);
  4. 创建虚拟硬盘;
  5. 选择虚拟硬盘文件类型 VDI;
  6. 虚拟硬盘选择动态分配;
  7. 选择文件存储位置和容量大小;
  8. 点击创建。

(3)安装操作系统。

(四)安装双操作系统

  • 第一步:制作安装 U 盘;
  • 第二步:双系统安装。安装后 Windows 和 Linux 都可使用,默认 Windows 优先启动;可在电脑启动时选择进入 Linux 系统。

(五)下载注意事项

虚拟机用户应使用虚拟机中 Linux 自带的浏览器(如 Firefox)访问教程并下载 Hadoop 安装包,不要使用 Windows 系统浏览器下载,否则文件会下载到 Windows 系统,虚拟机中的 Linux 无法访问。双系统用户则进入 Linux 系统后用浏览器下载。

(六)集群的建立与安装方式

采购好硬件设备后,即可把硬件装入机架,安装并运行 Hadoop。安装 Hadoop 有多种方法:

  1. 手动安装:本文档主线方式,逐台配置,适合学习与小规模集群。
  2. 自动化安装:为缓解安装和维护每个节点上相同软件的负担,可使用自动化方法实现完全自动化安装,如 Red Hat Linux 的 Kickstart、Debian 的 preseed,或者 Docker。自动化安装部署工具通过记录安装过程中对各个选项的回答来完成自动化安装。生产环境还可使用 Ambari / Cloudera Manager / Ansible。

四、Hadoop 安装与配置文件详解

4.1 下载、解压与目录规划

(1)版本选择

  • 从 Apache 官网 https://hadoop.apache.org/releases.html 或镜像站下载;
  • 必须下载 hadoop-2.x.y.tar.gz 格式的二进制包(已编译好,解压即用);
  • 文件名中包含 src 的是 Hadoop 源代码包,需要用 Maven 编译才可使用,除需修改源码或本地库外不要下载;
  • 教学建议 2.7.1 / 2.9.2(与本文示例一致);生产建议 3.3.x;
  • 下载后校验完整性:
sha512sum hadoop-2.9.2.tar.gz          # 与官网 .sha512 文件比对
md5sum hadoop-2.7.1.tar.gz

(2)解压位置:/usr/local 还是 /opt

位置 说明
/usr/local/hadoop 遵循 FHS 中"本地编译安装软件"的惯例,本文与源文档均采用
/opt/hadoop 或 /opt/module/hadoop 第三方独立软件包的惯例,多组件共存时更整洁(/opt/module、/opt/software 分别放安装目录与安装包)

关键是所有节点保持完全相同的路径,否则脚本 SSH 到从节点后找不到 $HADOOP_HOME。

tar -zxvf hadoop-2.7.1.tar.gz -C /usr/local/
cd /usr/local/
mv hadoop-2.7.1/ hadoop            # 去掉版本号,便于以后升级只改软链接

更规范的做法是保留版本号并建软链接,升级时只需切换链接:

tar -zxvf hadoop-2.9.2.tar.gz -C /opt/module/
ln -s /opt/module/hadoop-2.9.2 /opt/module/hadoop

Ubuntu 下源文档写法(含权限修改):

sudo tar -zxf ~/下载/hadoop-2.6.0.tar.gz -C /usr/local    # 解压到 /usr/local 中
cd /usr/local/
sudo mv ./hadoop-2.6.0/ ./hadoop            # 将文件夹名改为 hadoop
sudo chown -R hadoop:hadoop ./hadoop        # 修改文件权限

(3)目录结构逐项说明

ls /usr/local/hadoop
目录 内容与作用
bin/ 客户端命令:hadoop、hdfs、yarn、mapred、container-executor。日常操作 HDFS、提交作业都用这里的命令
sbin/ 集群管理脚本:start-dfs.sh、stop-dfs.sh、start-yarn.sh、stop-yarn.sh、start-all.sh、hadoop-daemon.sh、yarn-daemon.sh、mr-jobhistory-daemon.sh、slaves.sh、distribute-exclude.sh
etc/hadoop/ 所有配置文件:hadoop-env.sh、core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml、slaves/workers、log4j.properties、capacity-scheduler.xml、hadoop-policy.xml、各种 *.template 模板
share/ JAR 包与文档:share/hadoop/common、hdfs、mapreduce、yarn、tools 各自的 jar 与 lib;官方示例 jar 在 share/hadoop/mapreduce/hadoop-mapreduce-examples-x.y.z.jar;share/doc 是离线文档(占几百 MB,可删以节省空间)
lib/ 本地库(native library):lib/native/libhadoop.so 等,提供压缩编解码(Snappy/LZ4)与 CRC 校验的本地加速。缺失时会出现 WARN util.NativeCodeLoader: Unable to load native-hadoop library 警告,功能可用但性能略低
libexec/ 脚本内部调用的公共函数库:hadoop-config.sh、hdfs-config.sh 等,一般不需修改
logs/ 运行日志(首次启动后自动生成):hadoop-<用户>-<角色>-<主机名>.log(滚动日志)与 .out(标准输出);YARN 容器日志在 logs/userlogs/
include/ C/C++ 头文件(libhdfs 开发用)
tmp/ 由 hadoop.tmp.dir 指定,非自带;存放 NameNode/DataNode 数据与临时文件
LICENSE.txt / NOTICE.txt / README.txt 许可与说明文件

(4)属主设置

chown -R hadoop:hadoop /usr/local/hadoop
chown -R hadoop:hadoop /usr/local/jdk
chmod -R 755 /usr/local/hadoop
# 验证
ls -ld /usr/local/hadoop

若用 root 运行则属主保持 root 即可,但属主与运行用户必须一致,否则会出现 Permission denied(无法写 logs 目录、无法写 pid 文件)。

(5)验证解压结果

Hadoop 解压后即可使用,输入以下命令检查是否可用,成功则显示 Hadoop 版本信息:

cd /usr/local/hadoop
./bin/hadoop version

配好环境变量后可直接执行:

hadoop version
Hadoop 2.9.2
Subversion https://git-wip-us.apache.org/repos/asf/hadoop.git -r 826afbeae31ca687bc2f8471dc841b66ed2c6704
Compiled by ajisaka on 2018-11-13T12:42Z
Compiled with protoc 2.5.0
From source with checksum 3a9939967262218aa556c684d107985
This command was run using /usr/local/hadoop/share/hadoop/common/hadoop-common-2.9.2.jar

此时 Hadoop 默认模式为非分布式模式(本地模式),无需其他配置即可运行。

4.2 环境变量与 hadoop-env.sh

(1)HADOOP_HOME / PATH / HADOOP_CONF_DIR

export JAVA_HOME=/usr/local/jdk
export HADOOP_HOME=/usr/local/hadoop
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
变量 作用
HADOOP_HOME Hadoop 安装根目录,几乎所有脚本都基于它定位 jar 与配置
PATH 中加 $HADOOP_HOME/bin 使 hadoop、hdfs、yarn、mapred 可直接调用
PATH 中加 $HADOOP_HOME/sbin 使 start-dfs.sh 等管理脚本可直接调用
HADOOP_CONF_DIR 指定配置目录,默认 $HADOOP_HOME/etc/hadoop。用于同一套二进制切换多套配置(如测试/生产、客户端配置)

(2)为什么 hadoop-env.sh 中必须再设置一次 JAVA_HOME

Hadoop 的启动脚本通过 hadoop-env.sh 读取 JDK 路径,必须显式配置:

vim /usr/local/hadoop/etc/hadoop/hadoop-env.sh
export JAVA_HOME=/usr/local/jdk

原因:start-dfs.sh 通过 SSH 非交互、非登录方式(ssh host "command")在远端执行启动命令。这种方式下 bash 不会加载 /etc/profile、~/.bash_profile(它们只在登录 shell 中加载),因此远端环境里根本没有 JAVA_HOME,报错:

localhost: Error: JAVA_HOME is not set and could not be found.

hadoop-env.sh 由 Hadoop 脚本自身强制 source,与 shell 类型无关,所以在这里写死绝对路径是唯一可靠的做法。(另一种绕法是把 export 写进 ~/.bashrc,因为非交互 SSH 会读取 bashrc 中位于交互判断之前的部分,但不同发行版行为不一致,不推荐依赖。)

(3)hadoop-env.sh 中其他重要变量

vim /usr/local/hadoop/etc/hadoop/hadoop-env.sh
# 必须:JDK 绝对路径
export JAVA_HOME=/usr/local/jdk

# 配置目录(一般无需改)
export HADOOP_CONF_DIR=${HADOOP_CONF_DIR:-"/usr/local/hadoop/etc/hadoop"}

# 所有 Hadoop 守护进程的默认堆大小(MB),2.x 默认 1000
export HADOOP_HEAPSIZE=2048

# NameNode 初始堆(可单独指定,优先于 HADOOP_HEAPSIZE)
export HADOOP_NAMENODE_INIT_HEAPSIZE="2048"

# 各角色专用 JVM 参数(可分别调 GC、堆、JMX)
export HADOOP_NAMENODE_OPTS="-Xmx4096m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -Dhadoop.security.logger=INFO,RFAS $HADOOP_NAMENODE_OPTS"
export HADOOP_SECONDARYNAMENODE_OPTS="-Xmx4096m -Dhadoop.security.logger=INFO,RFAS $HADOOP_SECONDARYNAMENODE_OPTS"
export HADOOP_DATANODE_OPTS="-Xmx1024m -Dhadoop.security.logger=ERROR,RFAS $HADOOP_DATANODE_OPTS"
export HADOOP_CLIENT_OPTS="-Xmx512m $HADOOP_CLIENT_OPTS"

# 日志与 PID 目录(生产建议移出安装目录,避免 /tmp 被清理导致 stop 脚本找不到 pid)
export HADOOP_LOG_DIR=/var/log/hadoop
export HADOOP_PID_DIR=/var/run/hadoop
export HADOOP_SECURE_DN_PID_DIR=${HADOOP_PID_DIR}

# SSH 选项(超时、端口)
export HADOOP_SSH_OPTS="-o ConnectTimeout=5 -o SendEnv=HADOOP_CONF_DIR"

# 本地库路径
export HADOOP_OPTS="-Djava.library.path=$HADOOP_HOME/lib/native"

yarn-env.sh 中对应的变量:

export JAVA_HOME=/usr/local/jdk
export YARN_HEAPSIZE=2048
export YARN_RESOURCEMANAGER_HEAPSIZE=2048
export YARN_NODEMANAGER_HEAPSIZE=1024
export YARN_RESOURCEMANAGER_OPTS="-Xmx2048m"
export YARN_LOG_DIR=/var/log/hadoop-yarn

mapred-env.sh:

export JAVA_HOME=/usr/local/jdk
export HADOOP_JOB_HISTORYSERVER_HEAPSIZE=1000
export HADOOP_MAPRED_LOG_DIR=/var/log/hadoop-mapred

Hadoop 3.x 中 HADOOP_HEAPSIZE 被拆为 HADOOP_HEAPSIZE_MAX 与 HADOOP_HEAPSIZE_MIN,角色变量改名为 HDFS_NAMENODE_OPTS、HDFS_DATANODE_OPTS、YARN_RESOURCEMANAGER_OPTS 等。

(4)验证

source /etc/profile
hadoop version
hadoop classpath                   # 打印完整类路径,确认 jar 都能找到
echo $HADOOP_HOME
which hadoop hdfs yarn

4.3 六个核心配置文件详解

配置文件全部位于 $HADOOP_HOME/etc/hadoop/。XML 配置文件的格式为:每个配置项用一个 <property> 声明,包含 <name>、<value>,可选 <description> 与 <final>(设为 true 表示不允许作业级覆盖)。

文件 类型 管辖范围
hadoop-env.sh Shell JVM 与进程级环境变量
core-site.xml XML 全局公共参数:默认文件系统、临时目录、代理用户、IO
hdfs-site.xml XML HDFS 专属:副本、元数据/数据目录、各 Web 地址、块大小、权限
mapred-site.xml XML MapReduce 专属:运行框架、JobHistory、Map/Reduce 资源
yarn-site.xml XML YARN 专属:RM 地址、NM 资源量、辅助服务、调度器
slaves(2.x)/ workers(3.x) 纯文本 从节点主机名清单

4.3.1 core-site.xml

完整可用配置(完全分布式):

<configuration>
    <!-- HDFS 默认访问地址(NameNode 的 RPC 地址)。所有相对路径都基于它 -->
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://hadoop01:9000</value>
    </property>

    <!-- Hadoop 临时目录基址,是众多其他目录参数的默认父目录 -->
    <property>
        <name>hadoop.tmp.dir</name>
        <value>file:/usr/local/hadoop/tmp</value>
        <description>Abase for other temporary directories.</description>
    </property>

    <!-- 顺序读写的缓冲区大小,单位字节,建议 128KB,必须是 4096 的倍数 -->
    <property>
        <name>io.file.buffer.size</name>
        <value>131072</value>
    </property>

    <!-- HDFS 文件回收站保留时间(分钟),0 表示禁用。生产强烈建议开启 -->
    <property>
        <name>fs.trash.interval</name>
        <value>1440</value>
    </property>
    <property>
        <name>fs.trash.checkpoint.interval</name>
        <value>60</value>
    </property>

    <!-- 支持的压缩编解码器 -->
    <property>
        <name>io.compression.codecs</name>
        <value>org.apache.hadoop.io.compress.GzipCodec,org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.BZip2Codec,org.apache.hadoop.io.compress.SnappyCodec</value>
    </property>

    <!-- 代理用户(impersonation):允许 hadoop 用户代表其他用户提交作业,
         Hive/Oozie/HttpFS/Hue 等服务必须配置,否则报 is not allowed to impersonate -->
    <property>
        <name>hadoop.proxyuser.hadoop.hosts</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.hadoop.groups</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.root.hosts</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.root.groups</name>
        <value>*</value>
    </property>
</configuration>

逐参数说明:

配置项 默认值 说明
fs.defaultFS file:/// HDFS 默认访问地址,指定 NameNode 的主机与端口(2.x 惯用 9000,3.x 官方示例用 8020,两者都可)。其值也表示 HDFS 路径的逻辑名。若不配置,Hadoop 就以本地模式运行。HA 模式下这里填 nameservice 名(如 hdfs://mycluster)。旧名为 fs.default.name(1.x)
hadoop.tmp.dir /tmp/hadoop-${user.name} Hadoop 临时目录基址。若不配置,默认 /tmp/hadoo-hadoop,系统重启时 /tmp 可能被清理,导致元数据丢失必须重新格式化。它同时是 dfs.namenode.name.dir(默认 ${hadoop.tmp.dir}/dfs/name)与 dfs.datanode.data.dir(默认 ${hadoop.tmp.dir}/dfs/data)的父目录。路径可任意指定,只要该文件夹实际存在即可
io.file.buffer.size 4096 读写序列文件时的缓冲区大小(字节)。默认 4KB 偏小,调到 65536 或 131072 可显著提升大文件顺序 IO 性能
fs.trash.interval 0 回收站中文件保留分钟数。开启后 hdfs dfs -rm 只是移动到 /user/<用户>/.Trash,可挽救误删。设 1440 即保留一天
hadoop.proxyuser.<user>.hosts / .groups 无 代理用户配置。<user> 是发起代理的服务账号(超级用户),hosts 限定可从哪些主机发起代理,groups 限定可代理哪些用户组。* 表示不限制。Hive/HiveServer2、Oozie、Hue、HttpFS、Spark on YARN 报 User: root is not allowed to impersonate xxx 时就是缺这两项
hadoop.security.authentication simple 认证方式,simple(仅按 OS 用户名,无验证)或 kerberos
hadoop.http.staticuser.user dr.who Web UI 上以哪个用户身份浏览 HDFS。改成 hadoop 后才能在页面上删除/上传文件

伪分布式的最简写法(源文档,localhost):

<configuration>
    <property>
        <name>hadoop.tmp.dir</name>
        <value>file:/usr/local/hadoop/tmp</value>
        <description>Abase for other temporary directories.</description>
    </property>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://localhost:9000</value>
    </property>
</configuration>

指定 IP 或主机名的写法(源文档另两种,均等价可用):

<configuration>
    <property>
        <name>hadoop.tmp.dir</name>
        <value>file:/usr/local/hadoop/tmp</value>
        <description>Abase for other temporary directories.</description>
    </property>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://192.168.224.10:9000</value>
    </property>
</configuration>
<configuration>
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/root/hadoop_tmp</value>
        <description>temp path</description>
    </property>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://server.example.com:9000</value>
    </property>
</configuration>
配置项 值 说明
hadoop.tmp.dir /root/hadoop_tmp Hadoop 临时目录
fs.defaultFS hdfs://server.example.com:9000 NameNode RPC 地址

建议统一使用主机名而非 IP:换 IP 时只改 hosts 即可,且 HA、Kerberos 都要求用主机名。

4.3.2 hdfs-site.xml

完整可用配置(完全分布式):

<configuration>
    <!-- 数据块副本数。必须 <= DataNode 数量,生产为 3 -->
    <property>
        <name>dfs.replication</name>
        <value>3</value>
    </property>

    <!-- NameNode 元数据(fsimage + edits)存储目录,多个目录用逗号分隔实现冗余 -->
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:///usr/local/hadoop/tmp/dfs/name</value>
    </property>

    <!-- DataNode 数据块存储目录,一块盘一个目录,逗号分隔 -->
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:///usr/local/hadoop/tmp/dfs/data</value>
    </property>

    <!-- NameNode Web UI 地址(2.x 为 50070,3.x 为 9870) -->
    <property>
        <name>dfs.namenode.http-address</name>
        <value>hadoop01:50070</value>
    </property>

    <!-- SecondaryNameNode 的 HTTP 地址,决定 SNN 在哪台机器上启动 -->
    <property>
        <name>dfs.namenode.secondary.http-address</name>
        <value>hadoop02:50090</value>
    </property>

    <!-- SecondaryNameNode 检查点目录 -->
    <property>
        <name>dfs.namenode.checkpoint.dir</name>
        <value>file:///usr/local/hadoop/tmp/dfs/namesecondary</value>
    </property>

    <!-- 数据块大小,单位字节。2.x 及以后默认 128MB(134217728),1.x 为 64MB -->
    <property>
        <name>dfs.blocksize</name>
        <value>134217728</value>
    </property>

    <!-- 是否开启 HDFS 文件权限检查。实验环境可设 false 省事,生产必须 true -->
    <property>
        <name>dfs.permissions.enabled</name>
        <value>true</value>
    </property>

    <!-- NameNode 处理 DataNode/客户端 RPC 的线程数,建议 20*ln(节点数) -->
    <property>
        <name>dfs.namenode.handler.count</name>
        <value>30</value>
    </property>

    <!-- DataNode 允许失效的磁盘卷数,超过则 DataNode 下线 -->
    <property>
        <name>dfs.datanode.failed.volumes.tolerated</name>
        <value>0</value>
    </property>

    <!-- 每块盘为非 HDFS 用途预留的空间(字节),此处 10GB,防止写满 -->
    <property>
        <name>dfs.datanode.du.reserved</name>
        <value>10737418240</value>
    </property>

    <!-- DataNode 最大传输线程数(并发读写文件句柄数),大集群建议 8192 -->
    <property>
        <name>dfs.datanode.max.transfer.threads</name>
        <value>8192</value>
    </property>

    <!-- 允许通过 WebHDFS 访问,Hue/HttpFS 需要 -->
    <property>
        <name>dfs.webhdfs.enabled</name>
        <value>true</value>
    </property>

    <!-- 允许 DataNode 动态上下线时使用的白名单/黑名单文件 -->
    <property>
        <name>dfs.hosts</name>
        <value>/usr/local/hadoop/etc/hadoop/dfs.hosts</value>
    </property>
    <property>
        <name>dfs.hosts.exclude</name>
        <value>/usr/local/hadoop/etc/hadoop/dfs.hosts.exclude</value>
    </property>

    <!-- 安全模式自动退出阈值:可用块比例达到 99.9% 即退出 -->
    <property>
        <name>dfs.namenode.safemode.threshold-pct</name>
        <value>0.999f</value>
    </property>
</configuration>

逐参数说明:

配置项 默认值 说明
dfs.replication 3 数据块副本数量。伪分布式必须设为 1(只有一个 DataNode,设 3 会导致块永久处于 under-replicated 状态、Web UI 报警告)。副本数应小于等于 DataNode 数量。这是客户端参数,改动只影响之后写入的新文件,已有文件需 hdfs dfs -setrep 调整
dfs.namenode.name.dir file://${hadoop.tmp.dir}/dfs/name 本地磁盘目录,是存储 fsimage 文件的地方(同时存 edits)。可配多个逗号分隔的目录,NameNode 会同时写入所有目录做冗余,生产至少 2 个不同物理盘 + 1 个 NFS。旧名 dfs.name.dir
dfs.datanode.data.dir file://${hadoop.tmp.dir}/dfs/data 本地磁盘目录,是 HDFS 数据存放 block 的地方。多个目录用逗号分隔,DataNode 会轮转写入以并行利用多盘 IO。目录权限必须是 700,否则 DataNode 启动失败。旧名 dfs.data.dir
dfs.namenode.http-address 0.0.0.0:50070 NameNode Web UI 地址。2.x 为 50070,3.x 改为 9870。若只写端口不写主机(或写 0.0.0.0),则监听所有网卡
dfs.namenode.https-address 0.0.0.0:50470 NameNode HTTPS Web UI(3.x 为 9871)
dfs.namenode.rpc-address 取自 fs.defaultFS NameNode RPC 地址,一般不单独配置
dfs.namenode.secondary.http-address 0.0.0.0:50090 指定 SecondaryNameNode 的 Web 地址;同时决定了 start-dfs.sh 去哪台机器上启动 SNN(3.x 端口为 9868)。若不配置,SNN 会启动在 0.0.0.0(即执行脚本的本机),所以脚本输出里会看到 Starting secondary namenodes [0.0.0.0]
dfs.namenode.checkpoint.dir file://${hadoop.tmp.dir}/dfs/namesecondary SNN 存放检查点副本的目录,NameNode 元数据损坏时可从此恢复
dfs.namenode.checkpoint.period 3600 SNN 做检查点的时间间隔(秒)
dfs.namenode.checkpoint.txns 1000000 edits 事务数达到该值即触发检查点,与 period 谁先到算谁
dfs.blocksize 134217728(128MB) 块大小。大文件顺序扫描场景可调到 256MB 减少 Map 数与元数据量;小文件多时不宜再调大。必须是 512 的倍数。旧名 dfs.block.size。是客户端参数,可按文件指定
dfs.permissions.enabled true 是否启用 HDFS 的 POSIX 权限检查。旧名 dfs.permissions(源文档使用的即旧名,在 2.x 中仍被兼容识别)。设为 false 时不再校验用户/组权限,仅适合实验环境
dfs.namenode.handler.count 10 NameNode RPC 服务端线程数,节点多时必须调大,否则 DataNode 心跳排队
dfs.datanode.address 0.0.0.0:50010 DataNode 数据传输端口(3.x 为 9866)
dfs.datanode.ipc.address 0.0.0.0:50020 DataNode IPC 端口(3.x 为 9867)
dfs.datanode.http.address 0.0.0.0:50075 DataNode Web UI(3.x 为 9864)
dfs.datanode.du.reserved 0 每个数据盘为非 HDFS 保留的字节数
dfs.datanode.failed.volumes.tolerated 0 容忍多少块坏盘。多盘节点建议设为 1
dfs.hosts 空 白名单文件路径,只有列在其中的主机才允许作为 DataNode 加入集群(空表示不限制)
dfs.hosts.exclude 空 黑名单/退役名单文件路径,用于优雅下线 DataNode
dfs.namenode.safemode.threshold-pct 0.999f 满足最小副本数的块占比达到该阈值后,NameNode 才准备退出安全模式
dfs.namenode.safemode.extension 30000 达到阈值后再等待的毫秒数才真正退出安全模式
dfs.heartbeat.interval 3 DataNode 心跳间隔(秒)
dfs.blockreport.intervalMsec 21600000 块报告间隔(6 小时)
dfs.webhdfs.enabled true(2.x) 是否开启 WebHDFS REST 接口
dfs.client.block.write.replace-datanode-on-failure.policy DEFAULT 写入管道中某 DataNode 失败时的替换策略。小集群(<3 节点)追加写报错时需设为 NEVER

伪分布式的三种源文档写法(全部保留,均可用):

写法一(file: 单斜杠):

<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:/usr/local/hadoop/tmp/dfs/name</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:/usr/local/hadoop/tmp/dfs/data</value>
    </property>
</configuration>

写法二(file:/// 三斜杠,标准 URI 形式):

<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:///usr/local/hadoop/tmp/dfs/name</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:///usr/local/hadoop/tmp/dfs/data</value>
    </property>
</configuration>

写法三(含 Web 地址与关闭权限校验,主机名形式):

<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:///usr/local/hadoop/dfs/name</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:///usr/local/hadoop/dfs/data</value>
    </property>
    <property>
        <name>dfs.namenode.http-address</name>
        <value>server.example.com:50070</value>
    </property>
    <property>
        <name>dfs.namenode.secondary.http-address</name>
        <value>server.example.com:50090</value>
    </property>
    <property>
        <name>dfs.permissions</name>
        <value>false</value>
    </property>
</configuration>
配置项 值 说明
dfs.replication 1 副本数(伪分布式设为 1)
dfs.namenode.name.dir file:///usr/local/hadoop/dfs/name NameNode 元数据目录
dfs.datanode.data.dir file:///usr/local/hadoop/dfs/data DataNode 数据目录
dfs.namenode.http-address server.example.com:50070 NameNode Web UI 地址
dfs.namenode.secondary.http-address server.example.com:50090 SecondaryNameNode Web 地址
dfs.permissions false 关闭 HDFS 权限校验

完全分布式与伪分布式的关键差异:

配置项 伪分布式值 完全分布式值 说明
dfs.replication 1 2(两节点示例)/ 3(生产) 副本数应小于等于 DataNode 数量
dfs.namenode.secondary.http-address 未配置 192.168.224.10:50090 指定 SecondaryNameNode Web 地址

完全分布式的源文档写法(双节点、按 IP):

<configuration>
    <property>
        <name>dfs.namenode.secondary.http-address</name>
        <value>192.168.224.10:50090</value>
    </property>
    <property>
        <name>dfs.replication</name>
        <value>2</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:///usr/local/hadoop/tmp/dfs/name</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:///usr/local/hadoop/tmp/dfs/data</value>
    </property>
</configuration>

说明:Hadoop 的运行方式由配置文件决定。若需从伪分布式切回单机模式,需要删除 core-site.xml 中的配置项。虽然官方教程仅配置 fs.defaultFS 和 dfs.replication 即可运行,但显式配置 hadoop.tmp.dir、dfs.namenode.name.dir、dfs.datanode.data.dir 可避免临时目录被系统清理导致的启动异常。

4.3.3 mapred-site.xml

Hadoop 2.x 默认只提供模板文件,需要先复制:

cp /usr/local/hadoop/etc/hadoop/mapred-site.xml.template \
   /usr/local/hadoop/etc/hadoop/mapred-site.xml
vim /usr/local/hadoop/etc/hadoop/mapred-site.xml

完整可用配置:

<configuration>
    <!-- MapReduce 运行框架:yarn / local / classic。设为 yarn 才提交到集群 -->
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>

    <!-- JobHistoryServer RPC 地址 -->
    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>hadoop01:10020</value>
    </property>

    <!-- JobHistoryServer Web UI 地址 -->
    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>hadoop01:19888</value>
    </property>

    <!-- 作业运行中的中间信息存放目录(HDFS 上) -->
    <property>
        <name>mapreduce.jobhistory.intermediate-done-dir</name>
        <value>/history/done_intermediate</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.done-dir</name>
        <value>/history/done</value>
    </property>

    <!-- MapTask 申请的容器内存(MB) -->
    <property>
        <name>mapreduce.map.memory.mb</name>
        <value>1024</value>
    </property>
    <!-- MapTask JVM 堆参数,一般为容器内存的 75%~80% -->
    <property>
        <name>mapreduce.map.java.opts</name>
        <value>-Xmx819m</value>
    </property>
    <!-- MapTask 申请的 CPU 核数 -->
    <property>
        <name>mapreduce.map.cpu.vcores</name>
        <value>1</value>
    </property>

    <!-- ReduceTask 申请的容器内存(MB),通常是 Map 的 2 倍 -->
    <property>
        <name>mapreduce.reduce.memory.mb</name>
        <value>2048</value>
    </property>
    <property>
        <name>mapreduce.reduce.java.opts</name>
        <value>-Xmx1638m</value>
    </property>
    <property>
        <name>mapreduce.reduce.cpu.vcores</name>
        <value>1</value>
    </property>

    <!-- MRAppMaster 容器资源 -->
    <property>
        <name>yarn.app.mapreduce.am.resource.mb</name>
        <value>1536</value>
    </property>
    <property>
        <name>yarn.app.mapreduce.am.command-opts</name>
        <value>-Xmx1229m</value>
    </property>

    <!-- Shuffle 相关调优 -->
    <property>
        <name>mapreduce.task.io.sort.mb</name>
        <value>200</value>
        <description>Map 端环形缓冲区大小 MB,默认 100</description>
    </property>
    <property>
        <name>mapreduce.map.sort.spill.percent</name>
        <value>0.8</value>
        <description>缓冲区溢写阈值,默认 0.8</description>
    </property>
    <property>
        <name>mapreduce.task.io.sort.factor</name>
        <value>50</value>
        <description>归并排序时一次合并的文件流数量,默认 10</description>
    </property>
    <property>
        <name>mapreduce.reduce.shuffle.parallelcopies</name>
        <value>10</value>
        <description>Reduce 拉取 Map 输出的并行线程数,默认 5</description>
    </property>

    <!-- 开启 Map 输出压缩,减少 Shuffle 网络与磁盘开销 -->
    <property>
        <name>mapreduce.map.output.compress</name>
        <value>true</value>
    </property>
    <property>
        <name>mapreduce.map.output.compress.codec</name>
        <value>org.apache.hadoop.io.compress.SnappyCodec</value>
    </property>

    <!-- 推测执行:对拖后腿的任务启动备份任务 -->
    <property>
        <name>mapreduce.map.speculative</name>
        <value>true</value>
    </property>
    <property>
        <name>mapreduce.reduce.speculative</name>
        <value>true</value>
    </property>

    <!-- 3.x 中若报 ClassNotFound,需要补充 classpath -->
    <property>
        <name>mapreduce.application.classpath</name>
        <value>$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*:$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/lib/*</value>
    </property>
</configuration>

逐参数说明:

配置项 默认值 说明
mapreduce.framework.name local 取值 local(本地 LocalJobRunner,单 JVM 跑,方便调试)、yarn(提交到 YARN 集群)、classic(1.x 的 JobTracker,已废弃)。设为 yarn 后必须能连上 ResourceManager,否则报 Retrying connect to server: 0.0.0.0/0.0.0.0:8032
mapreduce.jobhistory.address 0.0.0.0:10020 JobHistoryServer 的 RPC 地址,客户端查询历史作业用
mapreduce.jobhistory.webapp.address 0.0.0.0:19888 JobHistoryServer Web UI。不启动它,YARN 8088 页面上点 History 链接会打不开、看不到作业日志
mapreduce.jobhistory.done-dir /tmp/hadoop-yarn/staging/history/done 已完成作业信息的最终归档目录(HDFS)
mapreduce.jobhistory.intermediate-done-dir .../done_intermediate 运行中作业的中间信息目录(HDFS)
mapreduce.map.memory.mb 1024 每个 MapTask 向 YARN 申请的容器内存。必须 ≥ yarn.scheduler.minimum-allocation-mb,≤ maximum-allocation-mb
mapreduce.reduce.memory.mb 1024 每个 ReduceTask 的容器内存,通常设为 Map 的 2 倍(Reduce 要在内存中缓存拉取的数据)
mapreduce.map.java.opts / mapreduce.reduce.java.opts -Xmx200m 任务 JVM 的堆参数。必须小于对应的 memory.mb(留出 JVM 元空间、栈、native 内存),一般取 0.8 倍;否则 JVM 实际用量超容器上限被 NM kill,报 Container is running beyond physical memory limits
mapreduce.map.cpu.vcores / mapreduce.reduce.cpu.vcores 1 任务申请的虚拟核数。只有使用 DominantResourceCalculator 时才真正生效
yarn.app.mapreduce.am.resource.mb 1536 MRAppMaster 自身容器的内存
yarn.app.mapreduce.am.command-opts -Xmx1024m MRAppMaster 的 JVM 堆
mapreduce.task.io.sort.mb 100 Map 端环形缓冲区大小(MB),调大可减少溢写次数
mapreduce.map.sort.spill.percent 0.80 缓冲区使用率达到该比例开始后台溢写
mapreduce.task.io.sort.factor 10 归并时一次最多合并多少个溢写文件
mapreduce.reduce.shuffle.parallelcopies 5 Reduce 端并行拉取 Map 输出的线程数
mapreduce.job.reduces 1 默认 Reduce 数量,可在作业中指定
mapreduce.map.output.compress false 是否压缩 Map 中间输出,开启可大幅减少 Shuffle 数据量
mapreduce.map.speculative / reduce.speculative true 推测执行开关。慢节点场景有用,但对写外部系统的作业应关闭以避免重复写
mapreduce.job.ubertask.enable false 小作业(Map 少、数据少)在 AM 内部串行执行,省去容器启动开销
mapreduce.application.classpath — 3.x 中运行示例报 ClassNotFoundException: ...MRAppMaster 时需显式设置

内存参数的换算关系(必须满足,否则任务失败):

yarn.scheduler.minimum-allocation-mb  <=  mapreduce.map.memory.mb  <=  yarn.scheduler.maximum-allocation-mb
mapreduce.map.java.opts(-Xmx)  <  mapreduce.map.memory.mb          (约 0.8 倍)
所有并发容器内存之和  <=  yarn.nodemanager.resource.memory-mb

伪分布式最简写法(源文档):

<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
</configuration>

完全分布式源文档写法(含 JobHistory):

<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>192.168.224.10:10020</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>192.168.224.10:19888</value>
    </property>
</configuration>

4.3.4 yarn-site.xml

完整可用配置:

<configuration>
    <!-- ResourceManager 所在主机,配了它,RM 的各子服务端口自动按此主机推导 -->
    <property>
        <name>yarn.resourcemanager.hostname</name>
        <value>hadoop01</value>
    </property>

    <!-- NodeManager 上运行的附属服务,MapReduce 的 Shuffle 必须配置为 mapreduce_shuffle -->
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <!-- Shuffle 服务的实现类(2.2 之后可省略,显式写更保险) -->
    <property>
        <name>yarn.nodemanager.aux-services.mapreduce_shuffle.class</name>
        <value>org.apache.hadoop.mapred.ShuffleHandler</value>
    </property>

    <!-- 每个 NodeManager 可分配给容器的物理内存总量(MB),要给 OS 和 DataNode 留出余量 -->
    <property>
        <name>yarn.nodemanager.resource.memory-mb</name>
        <value>8192</value>
    </property>
    <!-- 每个 NodeManager 可分配的虚拟核数,一般等于物理核数 -->
    <property>
        <name>yarn.nodemanager.resource.cpu-vcores</name>
        <value>8</value>
    </property>

    <!-- 单个容器最小内存,也是内存分配的粒度 -->
    <property>
        <name>yarn.scheduler.minimum-allocation-mb</name>
        <value>512</value>
    </property>
    <!-- 单个容器最大内存,任务申请超过此值会直接失败 -->
    <property>
        <name>yarn.scheduler.maximum-allocation-mb</name>
        <value>4096</value>
    </property>
    <property>
        <name>yarn.scheduler.minimum-allocation-vcores</name>
        <value>1</value>
    </property>
    <property>
        <name>yarn.scheduler.maximum-allocation-vcores</name>
        <value>4</value>
    </property>

    <!-- 关闭虚拟内存检查(实验环境必备,见下方说明) -->
    <property>
        <name>yarn.nodemanager.vmem-check-enabled</name>
        <value>false</value>
    </property>
    <!-- 物理内存检查一般保留开启 -->
    <property>
        <name>yarn.nodemanager.pmem-check-enabled</name>
        <value>true</value>
    </property>
    <!-- 虚拟内存与物理内存的比率上限,默认 2.1 -->
    <property>
        <name>yarn.nodemanager.vmem-pmem-ratio</name>
        <value>2.1</value>
    </property>

    <!-- 容器中间数据与日志的本地目录,多盘用逗号分隔 -->
    <property>
        <name>yarn.nodemanager.local-dirs</name>
        <value>/usr/local/hadoop/tmp/nm-local-dir</value>
    </property>
    <property>
        <name>yarn.nodemanager.log-dirs</name>
        <value>/usr/local/hadoop/logs/userlogs</value>
    </property>

    <!-- 日志聚合:任务结束后把容器日志收集到 HDFS,便于统一查看 -->
    <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.log.server.url</name>
        <value>http://hadoop01:19888/jobhistory/logs</value>
    </property>

    <!-- 调度器:容量调度器(默认,2.x)/ 公平调度器 -->
    <property>
        <name>yarn.resourcemanager.scheduler.class</name>
        <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value>
    </property>

    <!-- 环境变量白名单,3.x 中容器找不到 HADOOP_MAPRED_HOME 时需要 -->
    <property>
        <name>yarn.nodemanager.env-whitelist</name>
        <value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
    </property>
</configuration>

逐参数说明:

配置项 默认值 说明
yarn.resourcemanager.hostname 0.0.0.0 ResourceManager 主机名。配了这一项,RM 的 8030/8031/8032/8033/8088 各端口地址就自动推导出来,不必逐个写。从节点的 NodeManager 靠它找到 RM
yarn.nodemanager.aux-services 空 NodeManager 上的辅助服务列表。MapReduce 必须设为 mapreduce_shuffle,否则 Reduce 阶段无法从 Map 节点拉数据,作业卡住或报 shuffle 错误。Spark on YARN 的外部 shuffle 服务则加 spark_shuffle
yarn.nodemanager.aux-services.mapreduce_shuffle.class org.apache.hadoop.mapred.ShuffleHandler Shuffle 服务的实现类,2.2 之后有默认值可省略
yarn.nodemanager.resource.memory-mb 8192 本节点可供容器使用的物理内存总量(MB)。注意这不是自动探测的,默认写死 8192。若机器只有 4GB 内存却保留默认值,YARN 会超额分配导致 OOM;若机器有 64GB 却不改,则只能用 8GB。经验值:总内存 − OS(2~4G) − DataNode(1~2G) − NodeManager(1G) − 其他服务
yarn.nodemanager.resource.cpu-vcores 8 本节点可供容器使用的虚拟核数,一般设为物理核数(超线程可设为核数的 1~2 倍)
yarn.scheduler.minimum-allocation-mb 1024 容器内存分配的最小值与粒度。申请 1500MB 会被向上取整到该值的整数倍(如 2048)
yarn.scheduler.maximum-allocation-mb 8192 单容器内存上限。任务申请超出会抛 InvalidResourceRequestException
yarn.scheduler.minimum-allocation-vcores / maximum-allocation-vcores 1 / 4 容器 vcore 的上下限
yarn.nodemanager.vmem-check-enabled true 是否检查容器的虚拟内存使用量。强烈建议在实验/开发环境设为 false。原因:JVM 在 64 位 Linux 上会预留大量虚拟地址空间(glibc 的 arena、mmap 的堆外映射等),实际物理占用很小,但虚拟内存很容易超过 物理内存 × 2.1 的默认限制,NodeManager 于是把正常运行的容器直接 kill,报错 Container [pid=xxx] is running beyond virtual memory limits. Current usage: 200MB of 1GB physical memory used; 2.5GB of 2.1GB virtual memory used. Killing container. —— 这是新手跑 wordcount 最常见的失败原因之一
yarn.nodemanager.pmem-check-enabled true 是否检查物理内存,建议保留 true(真正的内存泄漏需要被拦住)
yarn.nodemanager.vmem-pmem-ratio 2.1 虚拟内存允许为物理内存的倍数。不想关闭检查时可调到 4.0
yarn.nodemanager.local-dirs ${hadoop.tmp.dir}/nm-local-dir 容器工作目录与 Shuffle 中间文件目录,多盘逗号分隔以并行 IO
yarn.nodemanager.log-dirs ${yarn.log.dir}/userlogs 容器 stdout/stderr 日志目录
yarn.log-aggregation-enable false 开启后任务结束时把容器日志上传到 HDFS,可通过 JobHistory 页面统一查看;否则日志散落在各节点本地且会被定期删除
yarn.log-aggregation.retain-seconds -1 聚合日志在 HDFS 上的保留秒数,604800 即 7 天
yarn.log.server.url 无 日志跳转地址,配了才能从 8088 页面点进去看日志
yarn.resourcemanager.scheduler.class CapacityScheduler 调度器实现。CapacityScheduler(按队列容量,默认)、FairScheduler(公平共享)、FifoScheduler(先进先出,不推荐)
yarn.nodemanager.disk-health-checker.min-healthy-disks 0.25 健康磁盘最低比例,低于此值 NM 变 unhealthy
yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage 90.0 单盘使用率超过该百分比即判为坏盘,磁盘写满导致 NM unhealthy 就是它
yarn.resourcemanager.am.max-attempts 2 AM 失败后的最大重试次数
yarn.acl.enable false 是否启用 YARN 队列 ACL

伪分布式最简写法(源文档):

<configuration>
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
</configuration>

完全分布式源文档写法:

<configuration>
    <property>
        <name>yarn.resourcemanager.hostname</name>
        <value>192.168.224.10</value>
    </property>
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
</configuration>

逐个端口显式指定的完整写法(源文档,适用于需要精确控制端口或防火墙放行的场景):

<configuration>
    <property>
        <name>yarn.resourcemanager.admin.address</name>
        <value>server.example.com:8033</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.resourcemanager.resource-tracker.address</name>
        <value>server.example.com:8025</value>
    </property>
    <property>
        <name>yarn.resourcemanager.address</name>
        <value>server.example.com:8050</value>
    </property>
    <property>
        <name>yarn.resourcemanager.scheduler.address</name>
        <value>server.example.com:8030</value>
    </property>
    <property>
        <name>yarn.resourcemanager.webapp.address</name>
        <value>server.example.com:8088</value>
    </property>
    <property>
        <name>yarn.resourcemanager.webapp.https.address</name>
        <value>server.example.com:8090</value>
    </property>
</configuration>

YARN 各端口说明:

配置项 端口 用途
yarn.resourcemanager.admin.address 8033 RM 管理接口(yarn rmadmin 命令使用)
yarn.resourcemanager.resource-tracker.address 8025(默认 8031) NodeManager 连接 RM 汇报心跳的接口
yarn.resourcemanager.address 8050(默认 8032) ApplicationClient 提交作业接口
yarn.resourcemanager.scheduler.address 8030 ApplicationMaster 申请资源接口
yarn.resourcemanager.webapp.address 8088 YARN Web UI
yarn.resourcemanager.webapp.https.address 8090 YARN HTTPS Web UI

注意:上表中 resource-tracker 用了 8025、resourcemanager.address 用了 8050,与 Hadoop 默认值(8031、8032)不同。若显式改了这些端口,则所有节点都必须配置一致,客户端(含 IDE 中的作业提交)也要用同一套配置,否则会连不上。若无特殊需求,推荐只配 yarn.resourcemanager.hostname,让端口保持默认。

4.3.5 slaves / workers 文件

该文件列出所有从节点的主机名(每行一个,不能有空格和空行),start-dfs.sh 与 start-yarn.sh 会遍历它逐台 SSH 启动 DataNode 与 NodeManager。

Hadoop 版本 文件名 路径
1.x / 2.x slaves $HADOOP_HOME/etc/hadoop/slaves
3.x workers(slaves 被彻底移除,写了也不生效) $HADOOP_HOME/etc/hadoop/workers

另有 masters 文件(2.x 中已非必需):早期用于指定 SecondaryNameNode 所在节点;2.x 之后 SNN 位置由 dfs.namenode.secondary.http-address 决定,masters 文件仅在部分脚本中保留兼容。

源文档写法(master 文件指定 SecondaryNameNode 所在节点,slaves 文件指定所有 DataNode/NodeManager 节点,每行一个):

# 完全分布式(按 IP)
echo 192.168.224.10 > /usr/local/hadoop/etc/hadoop/master
echo 192.168.224.11 > /usr/local/hadoop/etc/hadoop/slaves

# 伪分布式(master 与 slaves 均为本机)
echo server.example.com > /usr/local/hadoop/etc/hadoop/master
echo server.example.com > /usr/local/hadoop/etc/hadoop/slaves

三节点集群的推荐写法:

cat > /usr/local/hadoop/etc/hadoop/slaves <<'EOF'
hadoop01
hadoop02
hadoop03
EOF

# Hadoop 3.x
cat > /usr/local/hadoop/etc/hadoop/workers <<'EOF'
hadoop01
hadoop02
hadoop03
EOF

注意事项:

  • 文件中默认有一行 localhost,改为集群主机名时要删掉它,否则会在本机多启一个 DataNode;
  • 只有主节点(执行启动脚本的那台)的 slaves/workers 文件生效,但为便于在任意节点启动,建议全节点保持一致;
  • 主机名必须能被 SSH 免密登录且能被 hosts 解析;
  • 从完全分布式切换回伪分布式时,不要忘记修改 slaves 配置文件(改回 localhost)。

4.4 配置文件分发到所有节点

Hadoop 没有配置中心,每个节点读的都是自己本地的 etc/hadoop。 因此改完主节点配置后必须分发到所有节点 —— "只改了主节点忘了分发"是最高频的故障之一,典型症状:

  • DataNode 起来了但在 NameNode 页面上看不到(DataNode 的 fs.defaultFS 还指向旧地址/localhost);
  • NodeManager 无法注册到 RM,日志中反复 Retrying connect to server: 0.0.0.0/0.0.0.0:8031;
  • 改了副本数/内存参数却"完全没生效";
  • 部分任务成功部分失败(配置不一致的节点上的容器失败)。

(1)方式一:打包整个 Hadoop 目录(首次部署,源文档写法)

配置完成后,若之前运行过伪分布式,应先清理临时文件。在 Server(Master)上执行:

cd /usr/local
rm -rf ./hadoop/tmp       # 删除 Hadoop 临时文件
rm -rf ./hadoop/logs/*    # 删除日志文件
tar -zcvf /root/hadoop.master.tar.gz ./hadoop
scp /root/hadoop.master.tar.gz 192.168.224.11:/root/

在 Client(Slave)上解压:

tar -zxvf /root/hadoop.master.tar.gz -C /usr/local

在 Client 上安装 JDK 并配置环境变量:

# Java 解压到并改名为 /usr/local/jdk
vim /etc/profile
export JAVA_HOME=/usr/local/jdk
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:
export CLASS_PATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
source /etc/profile

若有更多 Slave 节点,重复执行「传输 hadoop.master.tar.gz → 解压 → 安装 JDK → 配置环境变量」即可。

(2)方式二:scp 直接复制目录或配置文件

# 复制整个 Hadoop 目录(首次)
scp -r /usr/local/hadoop hadoop@hadoop02:/usr/local/
scp -r /usr/local/jdk    hadoop@hadoop02:/usr/local/

# 只同步配置目录(日常改配置)
scp -r /usr/local/hadoop/etc/hadoop/* hadoop@hadoop02:/usr/local/hadoop/etc/hadoop/

# 只同步单个文件
scp /usr/local/hadoop/etc/hadoop/hdfs-site.xml hadoop@hadoop02:/usr/local/hadoop/etc/hadoop/

(3)方式三:rsync(推荐,只传差异、可删除多余文件、保留权限)

yum install -y rsync
# -a 归档(保留权限/时间/软链接) -v 详细 -z 压缩 --delete 删除目标端多余文件
rsync -avz --delete /usr/local/hadoop/etc/hadoop/ hadoop@hadoop02:/usr/local/hadoop/etc/hadoop/
rsync -avz /usr/local/hadoop/ hadoop@hadoop02:/usr/local/hadoop/

(4)批量分发脚本 xsync(强烈建议写一个)

vim /usr/local/bin/xsync
#!/bin/bash
# 用法: xsync <文件或目录> [更多文件...]
# 作用: 把指定文件/目录同步到集群所有其他节点的相同路径

if [ $# -lt 1 ]; then
    echo "Usage: xsync <file-or-dir> ..."
    exit 1
fi

# 集群主机列表(按实际修改)
HOSTS="hadoop01 hadoop02 hadoop03"

for host in $HOSTS; do
    # 跳过本机
    if [ "$host" == "$(hostname)" ]; then
        continue
    fi
    echo "==================== $host ===================="
    for file in "$@"; do
        if [ -e "$file" ]; then
            # 获取父目录的绝对路径(处理软链接)
            pdir=$(cd -P "$(dirname "$file")" && pwd)
            fname=$(basename "$file")
            # 在目标机上创建父目录
            ssh "$host" "mkdir -p $pdir"
            rsync -avz --delete "$pdir/$fname" "$host:$pdir"
        else
            echo "$file does not exist!"
        fi
    done
done
echo "==================== sync done ===================="
chmod +x /usr/local/bin/xsync
# 使用示例
xsync /usr/local/hadoop/etc/hadoop
xsync /etc/profile.d/hadoop.sh
xsync /etc/hosts

(5)批量执行命令脚本 xcall

vim /usr/local/bin/xcall
#!/bin/bash
# 用法: xcall <命令>
# 作用: 在集群所有节点上执行同一条命令
HOSTS="hadoop01 hadoop02 hadoop03"
for host in $HOSTS; do
    echo "==================== $host ===================="
    ssh "$host" "source /etc/profile; $*"
done
chmod +x /usr/local/bin/xcall
xcall jps                          # 一条命令查看全集群进程
xcall "ulimit -n"
xcall "df -h | grep data"

(6)分发后的必查项

# 1. 配置文件校验和一致(最可靠的验证方式)
xcall "md5sum /usr/local/hadoop/etc/hadoop/core-site.xml"
xcall "md5sum /usr/local/hadoop/etc/hadoop/hdfs-site.xml"
xcall "md5sum /etc/hosts"

# 2. 环境变量生效
xcall "echo \$HADOOP_HOME; hadoop version | head -1"

# 3. 属主正确
xcall "ls -ld /usr/local/hadoop"

注意:分发时不要连同 tmp(数据目录)和 logs 一起分发。DataNode 数据目录中的 VERSION 文件包含 storageID,各节点必须不同;若把 Master 的数据目录复制到 Slave,会造成多个 DataNode 使用相同 storageID,NameNode 只认其中一个,表现为"明明启动了 3 个 DataNode,Live datanodes 却只有 1"。

4.5 单机(本地)模式演练

单机模式下不启动任何守护进程,Hadoop 默认使用本地文件系统,无需任何 XML 配置。

(1)准备输入目录与测试文件

mkdir /usr/local/hadoop/input
echo hello hello world world > /usr/local/hadoop/input/info.txt
cd /usr/local/hadoop/

(2)运行 WordCount 示例

cd /usr/local/hadoop/
hadoop jar /usr/local/hadoop/share/hadoop/mapreduce/sources/hadoop-mapreduce-examples-2.9.2-sources.jar \
  org.apache.hadoop.examples.WordCount input output

提示:更常用的是编译好的示例 jar(不带 -sources),路径为 share/hadoop/mapreduce/hadoop-mapreduce-examples-2.9.2.jar,此时可用简写 hadoop jar ... wordcount input output。源文档使用 sources 包 + 全限定类名的写法同样可行。

(3)查看结果

cat /usr/local/hadoop/output/part-r-00000
hello   2
world   2
cd /usr/local/hadoop/output
ls

输出目录内容如下,同时生成 _SUCCESS 标记文件:

part-r-00000  _SUCCESS

单机模式下 input/output 都是本地文件系统中的相对路径(相对于当前工作目录),不涉及 HDFS。这一步的意义是验证 JDK 与 Hadoop 安装无误,之后再进入伪分布式。

4.6 伪分布式部署完整步骤(入门演练)

伪分布式在单节点上启动所有 HDFS 与 YARN 守护进程,模拟分布式行为。Hadoop 进程以分离的 Java 进程运行,节点既作为 NameNode 也作为 DataNode,读取的是 HDFS 中的文件。

完整流程:

1. 安装 JDK、配置环境变量、配置 hadoop-env.sh 的 JAVA_HOME
2. 配置 SSH 免密登录 localhost / 本机主机名 / 0.0.0.0
3. 修改配置文件:core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml
4. 创建 name 与 data 目录
5. 初始化文件系统:hdfs namenode -format
6. 启动所有进程:start-dfs.sh + start-yarn.sh(或 start-all.sh)
7. jps 验证进程;访问 Web 界面查看 Hadoop 信息
8. 运行 WordCount 实例
9. 停止服务

步骤 1:配置文件(配置内容见 4.3 节各"伪分布式写法")

vim /usr/local/hadoop/etc/hadoop/core-site.xml
vim /usr/local/hadoop/etc/hadoop/hdfs-site.xml
cp /usr/local/hadoop/etc/hadoop/mapred-site.xml.template /usr/local/hadoop/etc/hadoop/mapred-site.xml
vim /usr/local/hadoop/etc/hadoop/mapred-site.xml
vim /usr/local/hadoop/etc/hadoop/yarn-site.xml
echo server.example.com > /usr/local/hadoop/etc/hadoop/master
echo server.example.com > /usr/local/hadoop/etc/hadoop/slaves

步骤 2:创建 name 与 data 目录

mkdir /usr/local/hadoop/dfs /usr/local/hadoop/dfs/name /usr/local/hadoop/dfs/data

如果遇到启动问题,需要清空上述目录后重新格式化。

步骤 3:格式化 NameNode

hdfs namenode -format

格式化成功的标志是日志中出现:

INFO util.ExitUtil: Exiting with status 0

步骤 4:启动 HDFS

start-dfs.sh

启动输出示例:

Starting namenodes on [server.example.com]
server.example.com: starting namenode, logging to /usr/local/hadoop/logs/hadoop-root-namenode-server.cluster.com.out
localhost: starting datanode, logging to /usr/local/hadoop/logs/hadoop-root-datanode-server.cluster.com.out
Starting secondary namenodes [0.0.0.0]
0.0.0.0: starting secondarynamenode, logging to /usr/local/hadoop/logs/hadoop-root-secondarynamenode-server.cluster.com.out

步骤 5:验证进程

jps
4499 NameNode
4807 SecondaryNameNode
4938 Jps
4638 DataNode

浏览器访问 HDFS Web UI:

http://192.168.224.10:50070

(或 http://192.168.224.132:50070,视实际 IP 而定;Hadoop 3.x 为 9870 端口。)

步骤 6:启动 YARN(可选但建议)

伪分布式不启动 YARN 也可以执行程序,默认使用 mapred.LocalJobRunner。启动 YARN 后使用 mapred.YARNRunner,可通过 Web 界面查看任务运行情况。但 YARN 在单机上价值有限,反而可能使程序稍慢。

start-dfs.sh
start-yarn.sh
jps
3762 ResourceManager
3239 NameNode
4057 NodeManager
3562 SecondaryNameNode
3373 DataNode
4190 Jps

用 start-all.sh 一次启动全部(伪分布式共 5 个守护进程 + Jps 共 6 项):

start-all.sh
jps
5600 Jps
4706 DataNode
5108 ResourceManager
5288 NodeManager
4955 SecondaryNameNode
4495 NameNode

确保 6 个进程都存在(NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager、Jps),表示配置成功。

YARN Web UI:

http://192.168.224.10:8088/cluster

步骤 7:停止服务

stop-yarn.sh
stop-dfs.sh
# 或
stop-all.sh

停止后再次检查:

jps
6513 Jps

若只剩 Jps 一个进程,表示停止成功。

若不希望启动 YARN,务必将 mapred-site.xml 重命名回模板,否则运行程序时会报 Retrying connect to server: 0.0.0.0/0.0.0.0:8032:

mv /usr/local/hadoop/etc/hadoop/mapred-site.xml \
   /usr/local/hadoop/etc/hadoop/mapred-site.xml.template

4.7 完全分布式部署步骤概要

在单机/伪分布式基础上扩展为多节点集群。示例拓扑:

角色 IP 运行进程
Server(Master) 192.168.224.10 NameNode、SecondaryNameNode、ResourceManager
Client(Slave) 192.168.224.11 DataNode、NodeManager

步骤概要:

  1. 选定一台机器作为 Master。
  2. 在 Master 上安装 JDK、Hadoop 并完成配置。
  3. 在所有 Slave 上安装 JDK。
  4. 将 Master 上的 /usr/local/hadoop 目录复制到各 Slave。
  5. 在 Master 上启动 Hadoop。

配置 Master 免密登录 Slave:

在 Client(192.168.224.11)上生成密钥:

ssh-keygen

在 Server(192.168.224.10)上将公钥拷贝到 Client:

ssh-copy-id -i /root/.ssh/id_rsa.pub root@192.168.224.11

配置文件: core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml、master/slaves —— 内容见 4.3 节各"完全分布式源文档写法"。

分发: 见 4.4 节。

格式化并启动集群 —— 在 Server(Master)上执行:

hdfs namenode -format
start-dfs.sh
start-yarn.sh
jps

Master 上应看到:

5584 ResourceManager
5844 Jps
5403 SecondaryNameNode
5199 NameNode

在 Client(Slave)上执行 jps,应看到:

3526 DataNode
3691 NodeManager
3806 Jps

模式切换注意事项:

  1. 从完全分布式切换到伪分布式时,不要忘记修改 slaves 配置文件。
  2. 两种模式之间切换时若无法正常启动(尤其是 DataNode 无法启动),可删除所有节点(含 Slave)上的 /usr/local/hadoop/tmp 目录,再重新执行 hdfs namenode -format 后启动。此操作会清除已有数据,但能保证集群正确启动。

五、集群启动与验证

5.1 格式化 NameNode

hdfs namenode -format
# 旧写法(已废弃但仍可用,会提示 DEPRECATED)
hadoop namenode -format
# 指定集群名(可选,便于识别)
hdfs namenode -format -clusterId CID-hadoop-cluster

格式化做了什么:

  1. 在 dfs.namenode.name.dir 指定的目录下创建 current 子目录;
  2. 生成初始的空 fsimage(fsimage_0000000000000000000)与 edits 文件、seen_txid;
  3. 生成 VERSION 文件,其中写入新生成的 clusterID、namespaceID、blockpoolID、storageType、cTime、layoutVersion;
  4. 建立空的 HDFS 命名空间(只有根目录 /)。

注意:格式化只操作 NameNode 本地目录,不会碰 DataNode 的数据。

格式化成功的标志(在大量日志中找这几行):

INFO common.Storage: Storage directory /usr/local/hadoop/dfs/name has been successfully formatted.
INFO namenode.FSImageFormatProtobuf: Saving image file ... using no compression
INFO util.ExitUtil: Exiting with status 0
INFO namenode.NameNode: SHUTDOWN_MSG: Shutting down NameNode at server.example.com/192.168.224.10

格式化前必须确认的事项:

  • core-site.xml 的 fs.defaultFS 与 hadoop.tmp.dir 已正确配置(否则会格式化到 /tmp 下);
  • hdfs-site.xml 的 dfs.namenode.name.dir 目录已存在且属主正确;
  • JAVA_HOME 已在 hadoop-env.sh 中设置;
  • 配置已分发到所有节点;
  • 集群中所有 Hadoop 进程已停止(xcall jps 确认)。

只能格式化一次 —— 重复格式化的后果(重点)

hdfs namenode -format 会为 NameNode 生成一个全新的 clusterID,但 DataNode 数据目录中 VERSION 文件里保存的还是旧的 clusterID。DataNode 启动时会拿自己的 clusterID 与 NameNode 握手比对,不一致就拒绝注册并直接退出。

现象:jps 里没有 DataNode 进程(或启动后几秒消失),DataNode 日志 logs/hadoop-*-datanode-*.log 中出现:

java.io.IOException: Incompatible clusterIDs in /usr/local/hadoop/tmp/dfs/data:
namenode clusterID = CID-8f3b1d2a-....; datanode clusterID = CID-1a2b3c4d-....
    at org.apache.hadoop.hdfs.server.datanode.DataStorage.doTransition(DataStorage.java:775)
    ...
WARN org.apache.hadoop.hdfs.server.common.Storage: Failed to add storage directory
FATAL org.apache.hadoop.hdfs.server.datanode.DataNode: Initialization failed for Block pool

修复方法一:删除数据目录重新格式化(推荐,实验环境)

# 1. 停掉全部进程
stop-all.sh
xcall jps

# 2. 在【所有节点】删除数据目录、临时目录与日志
xcall "rm -rf /usr/local/hadoop/tmp /usr/local/hadoop/logs/*"
xcall "rm -rf /usr/local/hadoop/dfs/name/* /usr/local/hadoop/dfs/data/*"

# 3. 只在 NameNode 节点重新格式化
hdfs namenode -format

# 4. 重新启动
start-dfs.sh
start-yarn.sh

源文档写法(双节点):

cd /usr/local
rm -rf ./hadoop/tmp       # 删除 Hadoop 临时文件
rm -rf ./hadoop/logs/*    # 删除日志文件
hdfs namenode -format

此操作会清除 HDFS 上的所有数据,只适用于实验环境或集群还没有正式数据时。

修复方法二:手工改 VERSION 文件中的 clusterID(保留数据)

# 1. 查看 NameNode 的 clusterID
cat /usr/local/hadoop/dfs/name/current/VERSION
#Tue Jan 01 06:12:33 CST 2019
namespaceID=1863223491
clusterID=CID-8f3b1d2a-9c11-4d5e-8b7a-1f2e3d4c5b6a
cTime=0
storageType=NAME_NODE
blockpoolID=BP-1234567890-192.168.224.10-1546300000000
layoutVersion=-63
# 2. 在【每个】DataNode 上把 clusterID 改成与上面一致
vim /usr/local/hadoop/tmp/dfs/data/current/VERSION
storageID=DS-xxxxxxxx-xxxx-xxxx        # 保持不变,各节点必须唯一
clusterID=CID-8f3b1d2a-9c11-4d5e-8b7a-1f2e3d4c5b6a   # 改为 NameNode 的值
cTime=0
datanodeUuid=xxxxxxxx-xxxx-xxxx        # 保持不变
storageType=DATA_NODE
layoutVersion=-57
# 也可用 sed 批量替换
CID=$(grep clusterID /usr/local/hadoop/dfs/name/current/VERSION | cut -d= -f2)
xcall "sed -i 's/^clusterID=.*/clusterID=$CID/' /usr/local/hadoop/tmp/dfs/data/current/VERSION"

# 3. 单独重启 DataNode
hadoop-daemon.sh start datanode        # 2.x
hdfs --daemon start datanode           # 3.x

修复方法三:反过来改 NameNode 的 clusterID(当数据在 DataNode 上更重要且 NameNode 刚被误格式化时,需配合 -clusterId 参数重新格式化):

hdfs namenode -format -clusterId CID-1a2b3c4d-....    # 用 DataNode 上的旧 clusterID

如果确实需要重新初始化,正规做法是 hdfs namenode -format -force 之后同时清空所有 DataNode 数据目录,保证两端 clusterID 一致。

5.2 启动集群

(1)分组启动脚本(推荐)

start-dfs.sh        # 启动 HDFS:NameNode + DataNode(遍历 slaves) + SecondaryNameNode
start-yarn.sh       # 启动 YARN:ResourceManager + NodeManager(遍历 slaves)
脚本 启动的进程 在哪台机器执行
start-dfs.sh NameNode(本机或 fs.defaultFS 指定的机器)、所有 DataNode、SecondaryNameNode 建议在 NameNode 所在节点
start-yarn.sh ResourceManager(本机)、所有 NodeManager 必须在 ResourceManager 所在节点(脚本不会 SSH 去别的机器启动 RM)

重要:如果 NameNode 与 ResourceManager 不在同一台机器,必须分别到各自的机器上执行对应的脚本。

(2)start-all.sh / stop-all.sh

start-all.sh        # 同时启动 HDFS 与 YARN
stop-all.sh         # 停止所有服务

start-all.sh 内部只是依次调用 start-dfs.sh 与 start-yarn.sh。Hadoop 2.x 起该脚本已被标记为废弃,执行时会打印:

This script is Deprecated. Instead use start-dfs.sh and start-yarn.sh

功能仍然可用,但在 RM 与 NN 分离部署的集群中会启动到错误的机器上,因此生产环境不要用,教学演示单机时用起来方便。

(3)JobHistoryServer

MapReduce 历史服务器需要单独启动,start-all.sh 不会启动它:

# Hadoop 2.x
mr-jobhistory-daemon.sh start historyserver
mr-jobhistory-daemon.sh stop historyserver

# Hadoop 3.x(2.x 也支持)
mapred --daemon start historyserver
mapred --daemon stop historyserver

启动后 jps 中出现 JobHistoryServer 进程,即可访问 http://hadoop01:19888。不启动它,8088 页面上作业的 History 链接会打不开,任务日志也看不到。

(4)逐个角色单独启动(排错时最常用)

Hadoop 2.x 写法(hadoop-daemon.sh / yarn-daemon.sh,3.x 中已废弃但兼容):

# HDFS 角色
hadoop-daemon.sh start namenode
hadoop-daemon.sh start datanode
hadoop-daemon.sh start secondarynamenode
hadoop-daemon.sh stop  datanode

# 批量在所有 slaves 上启动 DataNode
hadoop-daemons.sh start datanode          # 注意是 daemons(复数)

# YARN 角色
yarn-daemon.sh  start resourcemanager
yarn-daemon.sh  start nodemanager
yarn-daemon.sh  stop  nodemanager
yarn-daemons.sh start nodemanager

Hadoop 3.x 推荐写法(hdfs --daemon / yarn --daemon):

hdfs --daemon start namenode
hdfs --daemon start datanode
hdfs --daemon start secondarynamenode
hdfs --daemon status namenode
hdfs --daemon stop  datanode

yarn --daemon start resourcemanager
yarn --daemon start nodemanager
yarn --daemon stop  nodemanager

mapred --daemon start historyserver

单独启动的价值:某个 DataNode 挂了不必重启整个集群;且报错会直接打印在终端,比翻日志快。

(5)启动顺序

1. ZooKeeper(如果配置了 HA)
2. JournalNode(如果配置了 HA)
3. NameNode  →  DataNode        (start-dfs.sh)
4. ResourceManager  →  NodeManager   (start-yarn.sh)
5. JobHistoryServer

停止顺序与启动顺序相反:

mr-jobhistory-daemon.sh stop historyserver
stop-yarn.sh
stop-dfs.sh
# ZooKeeper 最后停

为什么要先 HDFS 再 YARN:YARN 的 JobHistory 与作业 staging 目录都在 HDFS 上;若 HDFS 未就绪,RM/NM 启动时会报连不上 HDFS。

(6)Hadoop 3.x 中 root 启动报错的问题与解决

Hadoop 3.x 出于安全考虑不允许用 root 直接启动,执行 start-dfs.sh 时报:

Starting namenodes on [hadoop01]
ERROR: Attempting to operate on hdfs namenode as root
ERROR: but there is no HDFS_NAMENODE_USER defined. Aborting operation.
Starting datanodes
ERROR: Attempting to operate on hdfs datanode as root
ERROR: but there is no HDFS_DATANODE_USER defined. Aborting operation.
Starting secondary namenodes [hadoop02]
ERROR: Attempting to operate on hdfs secondarynamenode as root
ERROR: but there is no HDFS_SECONDARYNAMENODE_USER defined. Aborting operation.

以及 start-yarn.sh 报:

ERROR: Attempting to operate on yarn resourcemanager as root
ERROR: but there is no YARN_RESOURCEMANAGER_USER defined. Aborting operation.

解决方案一(推荐):改用非 root 用户 hadoop 运行

chown -R hadoop:hadoop /usr/local/hadoop
su - hadoop
start-dfs.sh

解决方案二:在 sbin 脚本头部声明运行用户

vim /usr/local/hadoop/sbin/start-dfs.sh
vim /usr/local/hadoop/sbin/stop-dfs.sh

在文件开头(#!/usr/bin/env bash 之后)加入:

HDFS_DATANODE_USER=root
HDFS_DATANODE_SECURE_USER=hdfs
HDFS_NAMENODE_USER=root
HDFS_SECONDARYNAMENODE_USER=root
HDFS_JOURNALNODE_USER=root
HDFS_ZKFC_USER=root
vim /usr/local/hadoop/sbin/start-yarn.sh
vim /usr/local/hadoop/sbin/stop-yarn.sh
YARN_RESOURCEMANAGER_USER=root
HADOOP_SECURE_DN_USER=yarn
YARN_NODEMANAGER_USER=root

解决方案三:写进 hadoop-env.sh(一处生效,最省事)

vim /usr/local/hadoop/etc/hadoop/hadoop-env.sh
export HDFS_NAMENODE_USER=root
export HDFS_DATANODE_USER=root
export HDFS_SECONDARYNAMENODE_USER=root
export YARN_RESOURCEMANAGER_USER=root
export YARN_NODEMANAGER_USER=root
export HDFS_JOURNALNODE_USER=root
export HDFS_ZKFC_USER=root
export HADOOP_SHELL_EXECNAME=root

改完记得 xsync 分发到所有节点。

5.3 验证集群

5.3.1 jps 进程清单对照表

jps                 # 查看当前节点的 Java 进程
jps -l              # 显示完整类名
jps -m              # 显示传给 main 的参数
xcall jps           # 批量查看全集群

伪分布式(单节点)应看到 5 个守护进程:

进程 所属
NameNode HDFS
DataNode HDFS
SecondaryNameNode HDFS
ResourceManager YARN
NodeManager YARN
(JobHistoryServer) MapReduce,单独启动后才有
Jps jps 命令自身,不算守护进程

双节点集群:

节点 应看到的进程
Server / Master(192.168.224.10) NameNode、SecondaryNameNode、ResourceManager、Jps
Client / Slave(192.168.224.11) DataNode、NodeManager、Jps

三节点集群(推荐规划):

节点 NameNode DataNode SecondaryNameNode ResourceManager NodeManager JobHistoryServer
hadoop01 ✓ ✓ ✓ ✓ ✓
hadoop02 ✓ ✓ ✓
hadoop03 ✓ ✓

进程缺失的排查方向:

缺失的进程 首查方向
NameNode 是否已格式化;dfs.namenode.name.dir 权限;端口 9000 是否被占用;元数据是否损坏
DataNode clusterID 是否一致;数据目录权限是否 700;磁盘是否满
SecondaryNameNode dfs.namenode.secondary.http-address 指向的机器是否可 SSH 免密;50090 端口占用
ResourceManager 是否在 RM 所在机器执行了 start-yarn.sh;8088/8032 端口占用
NodeManager yarn.resourcemanager.hostname 是否配置并分发;节点是否 unhealthy;内存配置是否合法

若 jps 命令本身不可用(command not found),说明 JAVA_HOME/bin 未加入 PATH;也可用 ps -ef | grep java 代替。

5.3.2 hdfs dfsadmin -report 输出解读

hdfs dfsadmin -report

若 Live datanodes 不为 0,说明集群启动成功。典型输出:

Configured Capacity: 107321753600 (99.95 GB)
Present Capacity: 95632394240 (89.06 GB)
DFS Remaining: 95632222208 (89.06 GB)
DFS Used: 172032 (168 KB)
DFS Used%: 0.00%
Under replicated blocks: 0
Blocks with corrupt replicas: 0
Missing blocks: 0
Missing blocks (with replication factor 1): 0

-------------------------------------------------
Live datanodes (2):

Name: 192.168.224.11:50010 (client1.example.com)
Hostname: client1.example.com
Decommission Status : Normal
Configured Capacity: 53660876800 (49.98 GB)
DFS Used: 86016 (84 KB)
Non DFS Used: 5844680704 (5.44 GB)
DFS Remaining: 47816110080 (44.53 GB)
DFS Used%: 0.00%
DFS Remaining%: 89.11%
Configured Cache Capacity: 0 (0 B)
Cache Used: 0 (0 B)
Cache Remaining: 0 (0 B)
Cache Used%: 100.00%
Cache Remaining%: 0.00%
Xceivers: 1
Last contact: Tue Jan 01 06:20:11 CST 2019
Last Block Report: Tue Jan 01 06:12:03 CST 2019

各字段含义:

字段 含义
Configured Capacity 所有 DataNode 数据目录所在文件系统的总容量之和
Present Capacity 当前实际可被 HDFS 使用的容量(已扣除 Non DFS Used 与 du.reserved)
DFS Remaining HDFS 还可写入的剩余空间
DFS Used / DFS Used% HDFS 已占用空间及百分比
Non DFS Used 同一磁盘上被非 HDFS 数据(OS、日志、YARN 中间文件)占用的空间 —— 这项异常大通常是日志或 YARN local-dirs 塞满了盘
Under replicated blocks 副本数不足设定值的块数。伪分布式把 replication 设成 3 时这里会一直很大
Blocks with corrupt replicas 有损坏副本的块数(校验和不匹配)
Missing blocks 所有副本都丢失的块数,不为 0 就意味着数据真的丢了,需用 hdfs fsck / 定位
Live datanodes (n) 存活的 DataNode 数量与清单,这是判断集群是否正常的第一指标
Dead datanodes 已判定失联的 DataNode
Decommission Status Normal 正常 / Decommission in progress 正在退役 / Decommissioned 已退役
Last contact 最后一次心跳时间,正常应在几秒内
Last Block Report 最后一次块报告时间,默认 6 小时一次
Xceivers 当前该 DataNode 上的数据传输线程数

其他常用管理命令:

hdfs dfsadmin -report -live            # 只看存活节点
hdfs dfsadmin -report -dead            # 只看死亡节点
hdfs dfsadmin -report -decommissioning # 只看退役中的节点
hdfs dfsadmin -printTopology           # 打印机架拓扑
hdfs fsck /                            # 文件系统健康检查
hdfs fsck / -files -blocks -locations  # 详细列出块与位置
hdfs fsck / -list-corruptfileblocks    # 列出损坏的块
yarn node -list                        # 查看 NodeManager 列表
yarn node -list -all                   # 含 unhealthy/lost 节点
yarn node -status hadoop02:45454       # 查看单个节点详情

5.3.3 Web UI 验证

界面 Hadoop 2.x 地址 Hadoop 3.x 地址 可以看什么
NameNode http://hadoop01:50070 http://hadoop01:9870 Summary(容量、块数、DataNode 数)、Datanodes 页、Browse the file system 浏览目录、Startup Progress、Snapshot
SecondaryNameNode http://hadoop02:50090 http://hadoop02:9868 最近一次 CheckPoint 时间、editlog 大小
DataNode http://hadoop02:50075 http://hadoop02:9864 本节点块信息、日志
ResourceManager http://hadoop01:8088 http://hadoop01:8088 集群资源总量/已用、队列、Applications 列表与状态、Nodes 页(含 unhealthy)
NodeManager http://hadoop02:8042 http://hadoop02:8042 本节点容器列表、资源使用
JobHistoryServer http://hadoop01:19888 http://hadoop01:19888 已完成作业的 Counters、Map/Reduce 任务明细、聚合日志

访问示例:

http://192.168.224.10:50070
http://192.168.224.10:50070/
http://192.168.224.10:50070/dfshealth.html#tab-datanode
http://192.168.224.10:8088/cluster
http://192.168.224.132:50070

点击 NameNode 主页的「Datanodes」标签,可看到 Client1 等各 DataNode 的信息(容量、块数、最后心跳时间)。

如果 jps 有进程但 Web UI 打不开,见 7.4 节排错清单。

5.3.4 HDFS 基本命令验证读写

HDFS Shell 命令有三种形式:

命令 适用范围
hadoop fs 适用于任何文件系统(本地 file://、HDFS、S3 等),最通用
hadoop dfs 仅适用于 HDFS,已废弃,执行时会提示 DEPRECATED: Use of this script to execute hdfs command is deprecated
hdfs dfs 与 hadoop dfs 作用相同,仅适用于 HDFS,2.x 之后的推荐写法

在 HDFS 上创建目录并查看:

hadoop fs -mkdir /input
hadoop fs -ls /
Found 1 items
drwxr-xr-x   - root supergroup          0 2019-01-01 06:12 /input

准备测试文件并上传:

echo this is my computer this is my computer > /root/input.txt
hadoop fs -put /root/input.txt /input/
hadoop fs -cat /input/input.txt
this is my computer this is my computer

更多常用命令:

hdfs dfs -ls /                        # 列出根目录
hdfs dfs -ls -R /                     # 递归列出
hdfs dfs -mkdir -p /user/hadoop/data  # 递归创建目录
hdfs dfs -put  localfile /path/       # 上传(源文件保留)
hdfs dfs -copyFromLocal localfile /   # 等价于 -put
hdfs dfs -moveFromLocal localfile /   # 上传后删除本地文件
hdfs dfs -get  /path/file ./          # 下载
hdfs dfs -copyToLocal /path/file ./   # 等价于 -get
hdfs dfs -getmerge /path/*.txt all.txt # 合并下载多个文件
hdfs dfs -cat  /path/file             # 查看内容
hdfs dfs -tail /path/file             # 查看末尾 1KB
hdfs dfs -text /path/file.gz          # 自动解压查看
hdfs dfs -appendToFile local /path/f  # 追加内容
hdfs dfs -cp /a /b                    # HDFS 内复制
hdfs dfs -mv /a /b                    # HDFS 内移动/改名
hdfs dfs -rm /path/file               # 删除文件(进回收站)
hdfs dfs -rm -r /output               # 递归删除目录
hdfs dfs -rm -r -skipTrash /output    # 跳过回收站直接删除
hdfs dfs -du -h /                     # 查看各目录占用(人类可读)
hdfs dfs -df -h                       # 查看整体容量
hdfs dfs -count /path                 # 统计目录数、文件数、大小
hdfs dfs -chmod 755 /path             # 改权限
hdfs dfs -chown hadoop:hadoop /path   # 改属主
hdfs dfs -setrep -w 3 /path/file      # 修改已有文件的副本数并等待完成
hdfs dfs -stat "%r %o %b" /path/file  # 查看副本数/块大小/文件长度
hdfs dfs -touchz /path/empty          # 创建空文件
hdfs dfs -test -e /path               # 判断存在,返回码 0 表示存在

验证读写是否真的成功:

# 写入 200MB 测试文件,观察是否被切成 2 个块
dd if=/dev/zero of=/tmp/big.dat bs=1M count=200
hdfs dfs -put /tmp/big.dat /
hdfs fsck /big.dat -files -blocks -locations
# 输出中应看到 2 个 block 及各自所在的 DataNode 列表
hdfs dfs -get /big.dat /tmp/big.dat.back
md5sum /tmp/big.dat /tmp/big.dat.back    # 两个 md5 应完全一致

5.3.5 跑通 WordCount 示例(完整命令与输出)

这是验证 HDFS + YARN + MapReduce 三者协同工作的最终验收标准。

(1)准备输入数据

hdfs dfs -rm -r /output                 # 若输出目录已存在则先删除
echo this is my computer this is my computer > /root/input.txt
hdfs dfs -mkdir -p /input
hadoop fs -put /root/input.txt /input/input.txt
hadoop fs -cat /input/input.txt
this is my computer this is my computer

(2)提交作业(源文档写法,使用 sources 包 + 全限定类名)

cd /root
hadoop jar /usr/local/hadoop/share/hadoop/mapreduce/sources/hadoop-mapreduce-examples-2.9.2-sources.jar \
  org.apache.hadoop.examples.WordCount /input/input.txt /output

(2')等价写法(使用编译好的示例 jar + 简写名,更常用)

# Hadoop 2.7.1
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.1.jar \
  wordcount /input /output
# Hadoop 2.9.2
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.9.2.jar \
  wordcount /input /output
# 查看该 jar 内所有可用示例
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.9.2.jar

(3)运行过程的典型输出

19/01/01 06:30:12 INFO client.RMProxy: Connecting to ResourceManager at hadoop01/192.168.224.10:8032
19/01/01 06:30:13 INFO input.FileInputFormat: Total input files to process : 1
19/01/01 06:30:13 INFO mapreduce.JobSubmitter: number of splits:1
19/01/01 06:30:13 INFO mapreduce.JobSubmitter: Submitting tokens for job: job_1546619214991_0001
19/01/01 06:30:14 INFO impl.YarnClientImpl: Submitted application application_1546619214991_0001
19/01/01 06:30:14 INFO mapreduce.Job: The url to track the job: http://hadoop01:8088/proxy/application_1546619214991_0001/
19/01/01 06:30:14 INFO mapreduce.Job: Running job: job_1546619214991_0001
19/01/01 06:30:25 INFO mapreduce.Job: Job job_1546619214991_0001 running in uber mode : false
19/01/01 06:30:25 INFO mapreduce.Job:  map 0% reduce 0%
19/01/01 06:30:33 INFO mapreduce.Job:  map 100% reduce 0%
19/01/01 06:30:41 INFO mapreduce.Job:  map 100% reduce 100%
19/01/01 06:30:42 INFO mapreduce.Job: Job job_1546619214991_0001 completed successfully
19/01/01 06:30:42 INFO mapreduce.Job: Counters: 49
        File System Counters
                FILE: Number of bytes read=54
                FILE: Number of bytes written=396167
                HDFS: Number of bytes read=145
                HDFS: Number of bytes written=32
        Job Counters
                Launched map tasks=1
                Launched reduce tasks=1
                Data-local map tasks=1
                Total time spent by all maps in occupied slots (ms)=5832
                Total time spent by all reduces in occupied slots (ms)=5124
        Map-Reduce Framework
                Map input records=1
                Map output records=8
                Map output bytes=76
                Combine input records=8
                Combine output records=4
                Reduce input groups=4
                Reduce input records=4
                Reduce output records=4
                Spilled Records=8
                GC time elapsed (ms)=142
        Shuffle Errors
                BAD_ID=0
                CONNECTION=0
                IO_ERROR=0
                WRONG_LENGTH=0
                WRONG_MAP=0
                WRONG_REDUCE=0
        File Input Format Counters
                Bytes Read=40
        File Output Format Counters
                Bytes Written=32

关键观察点:Data-local map tasks=1 说明实现了数据本地性;completed successfully 表示成功。

(4)查看结果

hadoop fs -cat /output/part-r-00000
computer        2
is      2
my      2
this    2
hdfs dfs -ls /output
Found 2 items
-rw-r--r--   2 root supergroup          0 2019-01-01 06:30 /output/_SUCCESS
-rw-r--r--   2 root supergroup         32 2019-01-01 06:30 /output/part-r-00000

_SUCCESS 是作业成功标记(大小为 0),part-r-00000 是第 0 个 Reduce 的输出。

访问 http://192.168.224.10:8088/cluster 可查看任务记录,在 8088 页面能看到名称类似 application_1546619214991_0001 的应用,状态为 FINISHED、FinalStatus 为 SUCCEEDED。

(5)再次运行前必须删除输出目录

若输出目录已存在,会抛出:

org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory hdfs://localhost:9000/user/hadoop/output already exists
hdfs dfs -rm -r /output

这是 MapReduce 的有意设计(防止误覆盖已有结果),不是 bug。

(6)用另一个示例验证纯计算能力(不依赖 HDFS 输入)

# 用蒙特卡洛法算 Pi:10 个 Map,每个 Map 采样 100 个点
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.9.2.jar \
  pi 10 100
Job Finished in 32.145 seconds
Estimated value of Pi is 3.14800000000000000000

5.3.6 hadoop classpath

hadoop classpath                       # 打印 Hadoop 运行所需的完整类路径
hadoop classpath --glob                # 展开所有通配符,列出每个 jar 的绝对路径
hadoop classpath --jar /tmp/hadoop.jar # 把类路径打包成一个 manifest jar(解决 Windows 命令行长度限制)

典型输出:

/usr/local/hadoop/etc/hadoop:/usr/local/hadoop/share/hadoop/common/lib/*:/usr/local/hadoop/share/hadoop/common/*:/usr/local/hadoop/share/hadoop/hdfs:/usr/local/hadoop/share/hadoop/hdfs/lib/*:/usr/local/hadoop/share/hadoop/hdfs/*:/usr/local/hadoop/share/hadoop/yarn/lib/*:/usr/local/hadoop/share/hadoop/yarn/*:/usr/local/hadoop/share/hadoop/mapreduce/lib/*:/usr/local/hadoop/share/hadoop/mapreduce/*:/contrib/capacity-scheduler/*.jar

用途:

  • 排查 ClassNotFoundException:确认某个 jar 是否在类路径中;
  • 给外部程序(Flume、Sqoop、Spark、自研 Java 程序)提供 Hadoop 依赖:export HADOOP_CLASSPATH=$(hadoop classpath);
  • Hadoop 3.x 中给 YARN 配置 mapreduce.application.classpath 时可直接取它的输出;
  • 验证 HADOOP_CONF_DIR 是否被正确加入(类路径第一项应是配置目录)。
# 常见用法:让第三方程序能连上 HDFS
export HADOOP_CLASSPATH=$(hadoop classpath)
java -cp "$HADOOP_CLASSPATH:myapp.jar" com.example.MyApp

5.4 常用端口全表(2.x 与 3.x 对照)

组件 用途 配置项 Hadoop 2.x Hadoop 3.x
NameNode RPC(客户端/DataNode 访问) fs.defaultFS 9000(官方示例常用 8020) 8020(9000 也常见)
NameNode Web UI(HTTP) dfs.namenode.http-address 50070 9870
NameNode Web UI(HTTPS) dfs.namenode.https-address 50470 9871
NameNode 服务 RPC(可选,隔离 DN 心跳) dfs.namenode.servicerpc-address 8022 8022
SecondaryNameNode Web UI(HTTP) dfs.namenode.secondary.http-address 50090 9868
SecondaryNameNode Web UI(HTTPS) dfs.namenode.secondary.https-address 50091 9869
DataNode 数据传输 dfs.datanode.address 50010 9866
DataNode IPC(元数据操作) dfs.datanode.ipc.address 50020 9867
DataNode Web UI(HTTP) dfs.datanode.http.address 50075 9864
DataNode Web UI(HTTPS) dfs.datanode.https.address 50475 9865
JournalNode RPC(HA 编辑日志) dfs.journalnode.rpc-address 8485 8485
JournalNode Web UI dfs.journalnode.http-address 8480 8480
ZKFC RPC dfs.ha.zkfc.port 8019 8019
NFS Gateway portmap / nfs / mountd — 111 / 2049 / 4242 同
ResourceManager 客户端提交作业 yarn.resourcemanager.address 8032 8032
ResourceManager AM 申请资源(Scheduler) yarn.resourcemanager.scheduler.address 8030 8030
ResourceManager NM 汇报(ResourceTracker) yarn.resourcemanager.resource-tracker.address 8031 8031
ResourceManager 管理接口(rmadmin) yarn.resourcemanager.admin.address 8033 8033
ResourceManager Web UI(HTTP) yarn.resourcemanager.webapp.address 8088 8088
ResourceManager Web UI(HTTPS) yarn.resourcemanager.webapp.https.address 8090 8090
NodeManager 容器管理(AM 连接) yarn.nodemanager.address 45454(0 表示随机) 45454
NodeManager Localizer yarn.nodemanager.localizer.address 8040 8040
NodeManager Web UI yarn.nodemanager.webapp.address 8042 8042
NodeManager Shuffle 服务 mapreduce.shuffle.port 13562 13562
JobHistoryServer RPC mapreduce.jobhistory.address 10020 10020
JobHistoryServer Web UI mapreduce.jobhistory.webapp.address 19888 19888
JobHistoryServer Admin mapreduce.jobhistory.admin.address 10033 10033
Timeline Server RPC / Web yarn.timeline-service.address / .webapp.address 10200 / 8188 10200 / 8188
MapReduce AM Web UI 随机 随机 随机
ZooKeeper 客户端 — 2181 2181

3.x 端口变化的原因:2.x 使用的 50070、50010、50020、50075、50090 等端口落在 Linux 的临时端口范围(ephemeral port range,通常 32768–60999)内,可能被随机分配给客户端连接,导致 Hadoop 服务启动时端口被占用。3.x 统一改到 9860–9880 区间避开该范围。

查看端口占用与监听:

netstat -tunlp | grep java
netstat -tunlp | grep 50070
ss -tunlp | grep 9000
lsof -i:50070
# 确认监听地址是 0.0.0.0 还是 127.0.0.1(后者外部无法访问)
netstat -tunlp | grep 50070

5.5 集群运维基础

5.5.1 增加 DataNode 节点(扩容)

方法一:简易方式(源文档写法,需重启集群)

为已有集群添加新的 Slave 节点(以 192.168.224.1x 为例)。

在 Server 上配置新节点:

ssh-copy-id -i /root/.ssh/id_rsa.pub root@192.168.224.1x
vim /usr/local/hadoop/etc/hadoop/slaves       # 加入新成员 IP
cd /usr/local/
tar -zcf /root/hadoop.master.tar.gz ./hadoop
scp /root/hadoop.master.tar.gz 192.168.224.1x:/root

在新节点上部署:

# 安装 Java 到 /usr/local/jdk
vim /etc/profile
export JAVA_HOME=/usr/local/jdk
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:
export CLASS_PATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
source /etc/profile

cd /root/
tar -zxvf hadoop.master.tar.gz -C /usr/local

重启集群并验证 —— 在 Server 上:

stop-all.sh
start-all.sh
jps
13688 SecondaryNameNode
14569 Jps
13482 NameNode
13853 ResourceManager

在各 Client 上:

jps
8339 DataNode
8457 NodeManager
8985 Jps

访问 Datanode 页面检查集群成员:

http://192.168.224.10:50070/dfshealth.html#tab-datanode

扩容后重新运行实例(源文档做法:清理临时文件并重新格式化,仅限实验环境,会丢数据):

cd /usr/local
rm -rf ./hadoop/tmp
rm -rf ./hadoop/logs/*
hdfs namenode -format

hdfs dfs -rm -r /output
echo this is my computer this is my computer > /root/input.txt
hdfs dfs -mkdir -p /input
hadoop fs -put /root/input.txt /input/input.txt

hadoop jar /usr/local/hadoop/share/hadoop/mapreduce/sources/hadoop-mapreduce-examples-2.9.2-sources.jar \
  org.apache.hadoop.examples.WordCount /input/input.txt /output

hadoop fs -cat /output/part-r-00000
computer        2
is      2
my      2
this    2

方法二:不停机热添加(生产做法,推荐)

# 1. 在新节点上完成全部前置准备(hosts、SSH 免密、JDK、ulimit、关防火墙、时间同步)
#    并从主节点分发 Hadoop 安装目录(注意:不要带 tmp/dfs 数据目录)

# 2. 在主节点的白名单文件中加入新主机名
vim /usr/local/hadoop/etc/hadoop/dfs.hosts
hadoop01
hadoop02
hadoop03
hadoop04          # 新节点
# 3. 确认 hdfs-site.xml 中已配置 dfs.hosts(若之前没配,需要重启 NameNode 才生效)
#    <property><name>dfs.hosts</name><value>/usr/local/hadoop/etc/hadoop/dfs.hosts</value></property>

# 4. 同时把新主机名加入 slaves/workers
echo hadoop04 >> /usr/local/hadoop/etc/hadoop/slaves

# 5. 分发配置
xsync /usr/local/hadoop/etc/hadoop

# 6. 刷新 NameNode 与 ResourceManager,让它们重新读取白名单(无需重启)
hdfs dfsadmin -refreshNodes
yarn rmadmin -refreshNodes

# 7. 在【新节点】上单独启动 DataNode 与 NodeManager
hadoop-daemon.sh start datanode          # 2.x
yarn-daemon.sh   start nodemanager
# 或 3.x
hdfs --daemon start datanode
yarn --daemon start nodemanager

# 8. 验证
hdfs dfsadmin -report | grep -A2 "Live datanodes"
yarn node -list

5.5.2 下线(退役)DataNode 节点

错误做法:直接关机或 kill 进程。这会让该节点上的块副本瞬间丢失,若某个块的所有副本都在该节点上就会造成数据丢失。

正确做法:通过 dfs.hosts.exclude 优雅退役,NameNode 会先把该节点上的块复制到其他节点,全部复制完成后才让它下线。

# 1. 确认 hdfs-site.xml 与 yarn-site.xml 中已配置 exclude 文件路径
<property>
    <name>dfs.hosts.exclude</name>
    <value>/usr/local/hadoop/etc/hadoop/dfs.hosts.exclude</value>
</property>
<property>
    <name>yarn.resourcemanager.nodes.exclude-path</name>
    <value>/usr/local/hadoop/etc/hadoop/yarn.hosts.exclude</value>
</property>
# 2. 把要退役的主机名写入黑名单
echo hadoop04 >> /usr/local/hadoop/etc/hadoop/dfs.hosts.exclude
echo hadoop04 >> /usr/local/hadoop/etc/hadoop/yarn.hosts.exclude

# 3. 刷新节点(不需重启集群)
hdfs dfsadmin -refreshNodes
yarn rmadmin -refreshNodes

# 4. 观察退役进度:状态会变为 Decommission in progress
hdfs dfsadmin -report | grep -B2 -A8 hadoop04
# 也可在 NameNode Web UI 的 Datanodes 页看到 "Decommissioning" 分组

# 5. 等待状态变为 Decommissioned(数据已全部迁移完毕)后停止该节点进程
ssh hadoop04 "hadoop-daemon.sh stop datanode; yarn-daemon.sh stop nodemanager"

# 6. 从 dfs.hosts 白名单与 slaves/workers 中删除该主机,再次刷新
sed -i '/hadoop04/d' /usr/local/hadoop/etc/hadoop/dfs.hosts
sed -i '/hadoop04/d' /usr/local/hadoop/etc/hadoop/slaves
xsync /usr/local/hadoop/etc/hadoop
hdfs dfsadmin -refreshNodes

# 7. 最后清空 exclude 文件(否则该主机名永久在黑名单中,日后无法重新加入)
> /usr/local/hadoop/etc/hadoop/dfs.hosts.exclude

要点:

  • dfs.hosts(白名单)与 dfs.hosts.exclude(黑名单)的组合语义:
在 dfs.hosts 在 exclude 结果
是 否 允许连接,正常工作
是 是 执行退役(先迁数据再下线)
否 否 不允许连接(白名单为空时表示不限制,所有节点都允许)
否 是 不允许连接
  • 退役期间该节点仍可读(对外提供数据),只是不再接收新写入;
  • 一次退役的节点数不要过多,否则剩余节点无法容纳副本(要保证剩余节点数 ≥ 副本数);
  • hdfs dfsadmin -refreshNodes 只刷新 HDFS,YARN 需另执行 yarn rmadmin -refreshNodes。

5.5.3 balancer 数据均衡

新增节点后新节点是空的,老节点很满;或长期运行后各节点使用率差异变大,就需要做数据均衡。

# 启动均衡器,阈值 10% 表示各节点使用率与集群平均使用率的差值不超过 10%
start-balancer.sh -threshold 10
# 等价的直接命令
hdfs balancer -threshold 10
# 指定带宽后台运行
hdfs balancer -threshold 5 -policy datanode
# 停止
stop-balancer.sh

限制均衡占用的网络带宽(避免影响业务,单位字节/秒):

# 动态设置为 20MB/s,无需重启
hdfs dfsadmin -setBalancerBandwidth 20971520

也可在 hdfs-site.xml 中固化:

<property>
    <name>dfs.datanode.balance.bandwidthPerSec</name>
    <value>20971520</value>
    <description>均衡器每个 DataNode 可用带宽,20MB/s</description>
</property>
<property>
    <name>dfs.datanode.balance.max.concurrent.moves</name>
    <value>50</value>
</property>

要点:

  • balancer 是独立的客户端程序,不是守护进程,应在一台不繁忙的机器上运行,运行完自动退出;
  • 阈值越小越均衡但耗时越长,生产常用 5~10;
  • 单节点内多块磁盘之间的均衡是另一个工具(Hadoop 3.x 的 Disk Balancer):
hdfs diskbalancer -plan hadoop02              # 生成计划
hdfs diskbalancer -execute hadoop02.plan.json # 执行
hdfs diskbalancer -query hadoop02             # 查询进度

5.5.4 安全模式(Safemode)

安全模式是 NameNode 的一种只读状态:可以查看目录和读文件,但不能创建、删除、修改文件,也不会做副本复制或删除。

hdfs dfsadmin -safemode get        # 查看状态:Safe mode is ON / OFF
hdfs dfsadmin -safemode enter      # 手动进入(做元数据备份、维护前使用)
hdfs dfsadmin -safemode leave      # 手动离开
hdfs dfsadmin -safemode wait       # 阻塞等待直到自动退出(写脚本时很有用)
hdfs dfsadmin -safemode forceExit  # 强制退出(3.x,危险,可能造成数据不一致)

什么时候会自动进入安全模式:

  1. NameNode 每次启动都会自动进入。它需要先加载 fsimage + edits 重建目录树,再等待各 DataNode 上报块报告以重建"块→DataNode"映射(这部分不持久化)。
  2. 运行期间发现可用块比例低于阈值(如大量 DataNode 同时失联)。
  3. NameNode 所在磁盘剩余空间不足(dfs.namenode.resource.du.reserved 默认 100MB),会进入安全模式保护元数据。

自动退出的条件(同时满足):

条件1:满足最小副本数(dfs.namenode.replication.min,默认 1)的块数 / 总块数
       >= dfs.namenode.safemode.threshold-pct(默认 0.999,即 99.9%)
条件2:存活的 DataNode 数 >= dfs.namenode.safemode.min.datanodes(默认 0)
条件3:以上条件持续满足 dfs.namenode.safemode.extension 毫秒(默认 30000,即 30 秒)

相关配置:

<property>
    <name>dfs.namenode.safemode.threshold-pct</name>
    <value>0.999f</value>
</property>
<property>
    <name>dfs.namenode.safemode.min.datanodes</name>
    <value>0</value>
</property>
<property>
    <name>dfs.namenode.safemode.extension</name>
    <value>30000</value>
</property>
<property>
    <name>dfs.namenode.replication.min</name>
    <value>1</value>
</property>

典型报错(在安全模式下写文件):

org.apache.hadoop.hdfs.server.namenode.SafeModeException: Cannot create directory /input.
Name node is in safe mode. The reported blocks 0 needs additional 5 blocks to reach
the threshold 0.9990 of total blocks 5. The number of live datanodes 0 has reached
the minimum number 0. Safe mode will be turned off automatically once the thresholds
have been reached.

看到这条应先等待 30 秒~几分钟(大集群块多时更久),而不是立刻 leave。

5.5.5 日志位置与查看方法

# 默认日志目录
ls $HADOOP_HOME/logs/
# 或 HADOOP_LOG_DIR 指定的目录(如 /var/log/hadoop)

日志文件命名规则:hadoop-<运行用户>-<角色>-<主机名>.log

文件 内容
hadoop-root-namenode-hadoop01.log NameNode 的 log4j 滚动日志,排错主要看这个
hadoop-root-namenode-hadoop01.out 进程的标准输出/标准错误,JVM 崩溃、OOM、JAVA_HOME not set 等在这里
hadoop-root-datanode-hadoop02.log DataNode 日志,clusterID 不匹配、磁盘错误看这里
hadoop-root-secondarynamenode-*.log SecondaryNameNode 检查点日志
yarn-root-resourcemanager-hadoop01.log ResourceManager 日志,调度、队列问题
yarn-root-nodemanager-hadoop02.log NodeManager 日志,容器被 kill 的原因在这里
mapred-root-historyserver-*.log JobHistoryServer 日志
userlogs/application_xxx/container_xxx/ 容器日志:stdout、stderr、syslog,业务代码的异常在这里
SecurityAuth-root.audit 安全审计日志
hdfs-audit.log HDFS 操作审计(谁在什么时候访问了什么路径)

常用查看方法:

# 实时跟踪
tail -f $HADOOP_HOME/logs/hadoop-root-namenode-hadoop01.log
# 只看错误
grep -E "ERROR|FATAL|Exception" $HADOOP_HOME/logs/hadoop-root-datanode-*.log
# 看最后 200 行(启动失败时最有效)
tail -200 $HADOOP_HOME/logs/hadoop-root-datanode-hadoop02.log
# .out 文件里看 JVM 级错误
cat $HADOOP_HOME/logs/hadoop-root-namenode-hadoop01.out
# 批量在所有节点搜错误
xcall "grep -l ERROR /usr/local/hadoop/logs/*.log"

# 通过 yarn 命令查看作业日志(需开启日志聚合)
yarn logs -applicationId application_1546619214991_0001
yarn logs -applicationId application_1546619214991_0001 -containerId container_xxx
yarn application -list                      # 列出运行中的应用
yarn application -list -appStates ALL       # 列出所有应用
yarn application -status application_xxx    # 查看应用状态
yarn application -kill application_xxx      # 杀掉应用

# 动态调整某个类的日志级别(排错时临时开 DEBUG)
hadoop daemonlog -setlevel hadoop01:50070 org.apache.hadoop.hdfs.StateChange DEBUG
hadoop daemonlog -getlevel hadoop01:50070 org.apache.hadoop.hdfs.StateChange

日志级别在 etc/hadoop/log4j.properties 中配置:

hadoop.root.logger=INFO,console
hadoop.log.dir=.
# 关闭令人困扰的 native library 警告
log4j.logger.org.apache.hadoop.util.NativeCodeLoader=ERROR

5.5.6 集群基准测试

判断一个 Hadoop 集群是否正确安装与性能是否达标,可以运行基准测试。Hadoop 自带一些基准测试程序,被打包在测试程序 JAR 文件中(hadoop-mapreduce-client-jobclient-x.y.z-tests.jar)。

  • 用 TestDFSIO 基准测试 HDFS 的 IO 性能;
  • 用排序测试 MapReduce:Hadoop 自带一个部分排序程序,测试过程中整个数据集都会通过洗牌(Shuffle)传输至 Reducer,可以充分测试 MapReduce 性能。
JAR=$HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-2.9.2-tests.jar

# 写测试:10 个文件,每个 100MB
hadoop jar $JAR TestDFSIO -write -nrFiles 10 -fileSize 100MB
# 读测试
hadoop jar $JAR TestDFSIO -read  -nrFiles 10 -fileSize 100MB
# 清理测试数据
hadoop jar $JAR TestDFSIO -clean
# 结果保存在 TestDFSIO_results.log

# 排序三部曲:生成随机数据 → 排序 → 校验
EX=$HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.9.2.jar
hadoop jar $EX teragen  10000000 /terasort/input      # 生成 1 千万行(约 1GB)
hadoop jar $EX terasort /terasort/input /terasort/output
hadoop jar $EX teravalidate /terasort/output /terasort/validate

# 其他常用基准
hadoop jar $JAR nnbench -operation create_write       # NameNode 压力测试
hadoop jar $JAR mrbench -numRuns 5                    # 小作业吞吐测试

5.5.7 在云计算环境中使用 Hadoop

Hadoop 不仅可以运行在企业内部集群中,也可以运行在云计算环境中。可以在 Amazon EC2 中运行 Hadoop:EC2 是一个计算服务,允许客户租用计算机(实例)运行自己的应用,客户可按需运行或终止实例,并按实际使用情况付费。Hadoop 自带一套脚本用于在 EC2 上运行 Hadoop。

在 EC2 上运行 Hadoop 尤其适用于一些工作流。例如,在 Amazon S3 中存储数据,在 EC2 上运行集群,在集群中运行 MapReduce 作业读取 S3 中的数据,最后在关闭集群前将输出写回 S3;如果长期使用集群,复制 S3 数据到运行在 EC2 上的 HDFS 中可使数据处理更高效,因为 HDFS 可充分利用数据的位置,而 S3 与 EC2 的存储不在同一个节点,做不到这一点。

5.6 常用运维命令速查

hdfs namenode -format             # 格式化 NameNode(仅首次或重建时执行)
start-dfs.sh                      # 启动 HDFS
start-yarn.sh                     # 启动 YARN
start-all.sh                      # 同时启动 HDFS 与 YARN(已废弃)
stop-dfs.sh                       # 停止 HDFS
stop-yarn.sh                      # 停止 YARN
stop-all.sh                       # 停止所有服务
jps                               # 查看 Java 进程
hdfs dfsadmin -report             # 查看 DataNode 报告
hdfs dfsadmin -safemode get       # 查看安全模式状态
hdfs dfsadmin -refreshNodes       # 刷新节点白名单/黑名单
hdfs fsck /                       # 文件系统健康检查
hdfs balancer -threshold 10       # 数据均衡
hdfs dfs -mkdir /input            # 在 HDFS 上创建目录
hadoop fs -mkdir /input           # 同上(通用写法)
hdfs dfs -put <local> <hdfs>      # 上传文件到 HDFS
hadoop fs -put <local> <hdfs>     # 同上
hdfs dfs -cat <hdfs-file>         # 查看 HDFS 文件内容
hadoop fs -cat <hdfs-file>        # 同上
hadoop fs -ls /                   # 列出 HDFS 根目录
hdfs dfs -rm -r /output           # 递归删除 HDFS 目录
hadoop version                    # 查看版本
hadoop classpath                  # 查看类路径
yarn node -list                   # 查看 NodeManager 列表
yarn application -list            # 查看运行中的应用
yarn logs -applicationId <id>     # 查看应用聚合日志
mr-jobhistory-daemon.sh start historyserver   # 启动历史服务器(2.x)
mapred --daemon start historyserver           # 启动历史服务器(3.x)

六、高可用与其他部署方式

6.1 HDFS HA(高可用)简述

要解决的问题:单点故障。 在非 HA 架构中,NameNode 是唯一的元数据服务:

  • NameNode 进程挂掉,整个 HDFS 立即不可用(DataNode 上的数据还在但无人知道块在哪);
  • NameNode 所在机器的磁盘损坏,元数据丢失则全部数据无法访问;
  • SecondaryNameNode 不是热备,只能辅助恢复到最近一次 CheckPoint,且切换需人工介入、耗时可达数十分钟;
  • 计划内维护(打补丁、升级、扩内存)也必须停机。

HA 架构的核心组件:

组件 作用
Active NameNode 对外提供服务的那个 NameNode,处理所有客户端请求
Standby NameNode 热备。实时同步 Active 的编辑日志并在内存中维护一份完全相同的元数据;同时承担 CheckPoint 工作(因此 HA 模式下不再需要 SecondaryNameNode)
JournalNode(JN) 共享编辑日志的存储集群,通常部署 3 个或 5 个(奇数)。Active 把每条 edit 写入多数(quorum,超过半数) JN 才算成功,Standby 持续从 JN 读取并重放,从而保持元数据一致。可容忍 (N-1)/2 个 JN 故障
ZKFC(ZKFailoverController) 与每个 NameNode 同机部署的独立进程。三大职责:①健康监测(定期 ping 本地 NN);②ZooKeeper 会话管理(持有临时节点表示存活);③基于 ZooKeeper 的选主,当 Active 失联,其临时节点消失,另一个 ZKFC 抢到锁后把本地 Standby 提升为 Active
ZooKeeper 集群 提供分布式锁与故障检测,通常 3/5 节点
DataNode 同时向两个 NameNode 汇报心跳与块报告,因此切换后新 Active 无需等待重新上报

详见 16-06-ZooKeeper集群.md

脑裂(Split-Brain)防护: 若 Active 只是 GC 假死(网络暂时不通),可能出现两个 Active 同时写 JN 造成元数据错乱。防护手段:

  1. JournalNode 的 epoch 机制:每次选主 epoch 号递增,JN 只接受最大 epoch 的写入,旧 Active 的写请求被拒绝;
  2. Fencing(隔离):通过 dfs.ha.fencing.methods 配置 sshfence(SSH 到旧 Active 上 kill 进程)或 shell(...)(执行自定义脚本,如通过 IPMI 断电)。

配置要点概览(hdfs-site.xml):

<configuration>
    <!-- 逻辑 nameservice 名称,客户端通过它访问而不是具体主机 -->
    <property>
        <name>dfs.nameservices</name>
        <value>mycluster</value>
    </property>
    <!-- 该 nameservice 下的两个 NameNode 逻辑 ID -->
    <property>
        <name>dfs.ha.namenodes.mycluster</name>
        <value>nn1,nn2</value>
    </property>
    <!-- 各 NameNode 的 RPC 与 HTTP 地址 -->
    <property>
        <name>dfs.namenode.rpc-address.mycluster.nn1</name>
        <value>hadoop01:8020</value>
    </property>
    <property>
        <name>dfs.namenode.rpc-address.mycluster.nn2</name>
        <value>hadoop02:8020</value>
    </property>
    <property>
        <name>dfs.namenode.http-address.mycluster.nn1</name>
        <value>hadoop01:50070</value>
    </property>
    <property>
        <name>dfs.namenode.http-address.mycluster.nn2</name>
        <value>hadoop02:50070</value>
    </property>
    <!-- JournalNode 集群地址与共享 edits 目录 -->
    <property>
        <name>dfs.namenode.shared.edits.dir</name>
        <value>qjournal://hadoop01:8485;hadoop02:8485;hadoop03:8485/mycluster</value>
    </property>
    <property>
        <name>dfs.journalnode.edits.dir</name>
        <value>/usr/local/hadoop/journaldata</value>
    </property>
    <!-- 客户端故障转移代理类:自动找到当前 Active -->
    <property>
        <name>dfs.client.failover.proxy.provider.mycluster</name>
        <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
    </property>
    <!-- 隔离机制,防止脑裂 -->
    <property>
        <name>dfs.ha.fencing.methods</name>
        <value>sshfence
shell(/bin/true)</value>
    </property>
    <property>
        <name>dfs.ha.fencing.ssh.private-key-files</name>
        <value>/root/.ssh/id_rsa</value>
    </property>
    <property>
        <name>dfs.ha.fencing.ssh.connect-timeout</name>
        <value>30000</value>
    </property>
    <!-- 开启自动故障转移 -->
    <property>
        <name>dfs.ha.automatic-failover.enabled</name>
        <value>true</value>
    </property>
</configuration>

core-site.xml 中对应的改动:

<configuration>
    <!-- 注意:这里填 nameservice 名,不再填具体主机 -->
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://mycluster</value>
    </property>
    <!-- ZooKeeper 集群地址 -->
    <property>
        <name>ha.zookeeper.quorum</name>
        <value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value>
    </property>
</configuration>

HA 集群的首次启动顺序:

# 1. 启动 ZooKeeper(所有 ZK 节点)
zkServer.sh start

# 2. 启动所有 JournalNode
hadoop-daemon.sh start journalnode          # 2.x
hdfs --daemon start journalnode             # 3.x

# 3. 在 nn1 上格式化并启动
hdfs namenode -format
hadoop-daemon.sh start namenode

# 4. 在 nn2 上同步 nn1 的元数据(关键,不能再次 format!)
hdfs namenode -bootstrapStandby
hadoop-daemon.sh start namenode

# 5. 在任一 NameNode 上初始化 ZooKeeper 中的 HA 状态节点
hdfs zkfc -formatZK

# 6. 启动 ZKFC(两个 NameNode 节点都要)与 DataNode
hadoop-daemon.sh start zkfc
start-dfs.sh

HA 常用管理命令:

hdfs haadmin -getServiceState nn1        # 查看状态:active / standby
hdfs haadmin -getAllServiceState         # 一次查看全部
hdfs haadmin -failover nn1 nn2           # 手动切换(把 nn2 变为 Active)
hdfs haadmin -transitionToActive nn1     # 强制转 Active(仅在关闭自动切换时使用)
hdfs haadmin -transitionToStandby nn1
hdfs haadmin -checkHealth nn1

说明:从非 HA 升级为 HA,dfs.namenode.secondary.http-address 应删除,SecondaryNameNode 不再启动。

6.2 YARN HA 简述

ResourceManager 同样是单点。YARN HA 的机制比 HDFS HA 简单,因为 RM 的状态(应用列表、队列状态)可以直接存到 ZooKeeper 中:

  • Active RM / Standby RM:一主一备(3.x 支持多备);
  • 基于 ZooKeeper 的 Embedded ElectorService:RM 内嵌选主逻辑,不需要独立的 ZKFC 进程;
  • RMStateStore:把应用元数据持久化到 ZooKeeper(ZKRMStateStore),新 Active 接管后可恢复正在运行的应用而不必重跑(配合 yarn.resourcemanager.recovery.enabled);
  • 客户端与 NodeManager 通过 yarn.resourcemanager.ha.rm-ids 列表自动重试找到当前 Active。

配置要点(yarn-site.xml):

<configuration>
    <property>
        <name>yarn.resourcemanager.ha.enabled</name>
        <value>true</value>
    </property>
    <property>
        <name>yarn.resourcemanager.cluster-id</name>
        <value>yarn-cluster</value>
    </property>
    <property>
        <name>yarn.resourcemanager.ha.rm-ids</name>
        <value>rm1,rm2</value>
    </property>
    <property>
        <name>yarn.resourcemanager.hostname.rm1</name>
        <value>hadoop01</value>
    </property>
    <property>
        <name>yarn.resourcemanager.hostname.rm2</name>
        <value>hadoop02</value>
    </property>
    <property>
        <name>yarn.resourcemanager.webapp.address.rm1</name>
        <value>hadoop01:8088</value>
    </property>
    <property>
        <name>yarn.resourcemanager.webapp.address.rm2</name>
        <value>hadoop02:8088</value>
    </property>
    <property>
        <name>yarn.resourcemanager.zk-address</name>
        <value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value>
    </property>
    <!-- 开启状态恢复,切换后正在运行的作业不中断 -->
    <property>
        <name>yarn.resourcemanager.recovery.enabled</name>
        <value>true</value>
    </property>
    <property>
        <name>yarn.resourcemanager.store.class</name>
        <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
    </property>
    <!-- 自动故障转移(默认已开启) -->
    <property>
        <name>yarn.resourcemanager.ha.automatic-failover.enabled</name>
        <value>true</value>
    </property>
    <property>
        <name>yarn.resourcemanager.ha.automatic-failover.embedded</name>
        <value>true</value>
    </property>
</configuration>

管理命令:

yarn rmadmin -getServiceState rm1
yarn rmadmin -getAllServiceState
yarn rmadmin -transitionToActive rm1 --forcemanual
yarn rmadmin -refreshQueues

6.3 HDFS Federation(联邦)简述

要解决的问题: HA 解决了 NameNode 的可用性,但没有解决可扩展性。单个 NameNode 的元数据全部驻留内存,内存大小决定了集群能管理的文件与块数量上限(约每 1GB 内存管理 100 万个对象);同时所有元数据请求都由一个 NameNode 处理,吞吐存在天花板。

Federation 的思路: 部署多个相互独立的 NameNode,各自管理命名空间的一部分(如 nn1 管 /user,nn2 管 /data),彼此不通信、互不影响;所有 DataNode 被所有 NameNode 共用,每个 DataNode 向所有 NameNode 注册。

核心概念:

概念 说明
Namespace(命名空间) 一个 NameNode 管理的目录树
Block Pool(块池) 一个 Namespace 对应的所有数据块集合。DataNode 上同时存储多个块池的数据,块池之间相互独立
Block Pool ID 区分不同块池,因此一个 DataNode 的 VERSION 目录下会有多个 BP-xxx 子目录
ViewFS / 客户端挂载表 客户端通过挂载表(fs.viewfs.mounttable.*)把多个 Namespace 拼成一个统一的逻辑视图
RBF(Router-Based Federation,3.x) 引入 Router 层做服务端路由,客户端无需感知挂载表,配置更简单

配置要点:

<property>
    <name>dfs.nameservices</name>
    <value>ns1,ns2</value>
</property>
<property>
    <name>dfs.namenode.rpc-address.ns1</name>
    <value>hadoop01:8020</value>
</property>
<property>
    <name>dfs.namenode.rpc-address.ns2</name>
    <value>hadoop02:8020</value>
</property>

Federation 与 HA 可同时使用:每个 Namespace 内部再配一主一备。

三者的关系总结:

方案 解决什么 不解决什么
SecondaryNameNode edits 膨胀、加速重启 不解决单点故障
HA NameNode 单点故障(可用性) 不解决元数据规模上限
Federation 元数据规模与吞吐(扩展性) 不解决单个 NameNode 的可用性(需叠加 HA)

6.4 用 Docker 快速搭建 Hadoop 实验环境

源文档在「集群的建立与安装」一节中提到,可以使用 Docker 等自动化方式替代逐台手工安装,以缓解在每个节点上重复安装与维护相同软件的负担。下面给出可直接使用的做法。

详见 13-01-Docker基础与进阶.md

方案一:使用官方/社区现成镜像(最快,几分钟起一个伪分布式)

# 拉取镜像
docker pull sequenceiq/hadoop-docker:2.7.1

# 启动并进入容器(伪分布式,已内置全部配置)
docker run -it --name hadoop-pseudo \
  -p 50070:50070 -p 8088:8088 -p 9000:9000 -p 19888:19888 \
  sequenceiq/hadoop-docker:2.7.1 /etc/bootstrap.sh -bash

# 容器内验证
jps
hdfs dfsadmin -report
cd $HADOOP_PREFIX
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.1.jar \
  wordcount /input /output

方案二:docker-compose 起多节点集群(bde2020 镜像,推荐用于教学)

mkdir -p /opt/hadoop-docker && cd /opt/hadoop-docker
vim docker-compose.yml
# docker-compose.yml 内容
cat > docker-compose.yml <<'EOF'
version: "3"

services:
  namenode:
    image: bde2020/hadoop-namenode:2.0.0-hadoop3.2.1-java8
    container_name: namenode
    hostname: namenode
    ports:
      - "9870:9870"
      - "9000:9000"
    volumes:
      - hadoop_namenode:/hadoop/dfs/name
    environment:
      - CLUSTER_NAME=test
      - CORE_CONF_fs_defaultFS=hdfs://namenode:9000
    env_file:
      - ./hadoop.env

  datanode1:
    image: bde2020/hadoop-datanode:2.0.0-hadoop3.2.1-java8
    container_name: datanode1
    hostname: datanode1
    volumes:
      - hadoop_datanode1:/hadoop/dfs/data
    environment:
      SERVICE_PRECONDITION: "namenode:9870"
    env_file:
      - ./hadoop.env

  datanode2:
    image: bde2020/hadoop-datanode:2.0.0-hadoop3.2.1-java8
    container_name: datanode2
    hostname: datanode2
    volumes:
      - hadoop_datanode2:/hadoop/dfs/data
    environment:
      SERVICE_PRECONDITION: "namenode:9870"
    env_file:
      - ./hadoop.env

  resourcemanager:
    image: bde2020/hadoop-resourcemanager:2.0.0-hadoop3.2.1-java8
    container_name: resourcemanager
    hostname: resourcemanager
    ports:
      - "8088:8088"
    environment:
      SERVICE_PRECONDITION: "namenode:9000 namenode:9870 datanode1:9864 datanode2:9864"
    env_file:
      - ./hadoop.env

  nodemanager1:
    image: bde2020/hadoop-nodemanager:2.0.0-hadoop3.2.1-java8
    container_name: nodemanager1
    hostname: nodemanager1
    environment:
      SERVICE_PRECONDITION: "namenode:9000 namenode:9870 resourcemanager:8088"
    env_file:
      - ./hadoop.env

  historyserver:
    image: bde2020/hadoop-historyserver:2.0.0-hadoop3.2.1-java8
    container_name: historyserver
    hostname: historyserver
    ports:
      - "19888:19888"
    volumes:
      - hadoop_historyserver:/hadoop/yarn/timeline
    environment:
      SERVICE_PRECONDITION: "namenode:9000 namenode:9870 datanode1:9864 resourcemanager:8088"
    env_file:
      - ./hadoop.env

volumes:
  hadoop_namenode:
  hadoop_datanode1:
  hadoop_datanode2:
  hadoop_historyserver:
EOF
# hadoop.env:以环境变量方式注入配置,命名规则为 <文件名>_<参数名,点换下划线>
cat > hadoop.env <<'EOF'
CORE_CONF_fs_defaultFS=hdfs://namenode:9000
CORE_CONF_hadoop_http_staticuser_user=root
CORE_CONF_hadoop_proxyuser_hue_hosts=*
CORE_CONF_hadoop_proxyuser_hue_groups=*

HDFS_CONF_dfs_webhdfs_enabled=true
HDFS_CONF_dfs_permissions_enabled=false
HDFS_CONF_dfs_replication=2

YARN_CONF_yarn_resourcemanager_hostname=resourcemanager
YARN_CONF_yarn_nodemanager_aux___services=mapreduce_shuffle
YARN_CONF_yarn_nodemanager_vmem___check___enabled=false
YARN_CONF_yarn_log___aggregation___enable=true
YARN_CONF_yarn_timeline___service_enabled=true

MAPRED_CONF_mapreduce_framework_name=yarn
MAPRED_CONF_mapreduce_jobhistory_address=historyserver:10020
EOF
# 启动、查看、进入、停止
docker-compose up -d
docker-compose ps
docker-compose logs -f namenode
docker exec -it namenode bash
docker-compose down            # 停止并删除容器(数据卷保留)
docker-compose down -v         # 连数据卷一起删除

容器内验证:

docker exec -it namenode hdfs dfsadmin -report
docker exec -it namenode hdfs dfs -mkdir -p /input
docker exec -it namenode bash -c "echo 'this is my computer this is my computer' > /tmp/input.txt && hdfs dfs -put /tmp/input.txt /input/"
docker exec -it namenode hdfs dfs -cat /input/input.txt

访问 http://<宿主机IP>:9870(NameNode)与 http://<宿主机IP>:8088(YARN)。

方案三:自己构建镜像(理解每一步)

mkdir -p /opt/hadoop-build && cd /opt/hadoop-build
# 把 jdk-8u191-linux-x64.tar.gz 与 hadoop-2.9.2.tar.gz 放到此目录
vim Dockerfile
cat > Dockerfile <<'EOF'
FROM centos:7

# 安装依赖:SSH(Hadoop 启动脚本需要)、which(脚本需要)
RUN yum install -y openssh-server openssh-clients which net-tools vim \
    && yum clean all

# 配置 SSH 免密
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N '' \
    && ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N '' \
    && ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N '' \
    && 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 \
    && echo "StrictHostKeyChecking no" >> /root/.ssh/config

# 安装 JDK 与 Hadoop
ADD jdk-8u191-linux-x64.tar.gz /usr/local/
ADD hadoop-2.9.2.tar.gz /usr/local/
RUN mv /usr/local/jdk1.8.0_191 /usr/local/jdk \
    && mv /usr/local/hadoop-2.9.2 /usr/local/hadoop

ENV JAVA_HOME=/usr/local/jdk
ENV HADOOP_HOME=/usr/local/hadoop
ENV HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
ENV PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
ENV HDFS_NAMENODE_USER=root HDFS_DATANODE_USER=root HDFS_SECONDARYNAMENODE_USER=root
ENV YARN_RESOURCEMANAGER_USER=root YARN_NODEMANAGER_USER=root

RUN echo "export JAVA_HOME=/usr/local/jdk" >> $HADOOP_CONF_DIR/hadoop-env.sh

COPY core-site.xml hdfs-site.xml mapred-site.xml yarn-site.xml $HADOOP_CONF_DIR/
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 9000 50070 50090 8088 19888
ENTRYPOINT ["/entrypoint.sh"]
EOF
cat > entrypoint.sh <<'EOF'
#!/bin/bash
/usr/sbin/sshd
if [ ! -d /usr/local/hadoop/tmp/dfs/name ]; then
    hdfs namenode -format -force
fi
start-dfs.sh
start-yarn.sh
mr-jobhistory-daemon.sh start historyserver
tail -f /usr/local/hadoop/logs/*namenode*.log
EOF
docker build -t myhadoop:2.9.2 .
docker run -d --name hadoop --hostname hadoop01 \
  -p 50070:50070 -p 8088:8088 -p 19888:19888 myhadoop:2.9.2
docker logs -f hadoop
docker exec -it hadoop jps

Docker 部署 Hadoop 的注意事项:

  • 容器主机名必须固定(--hostname 或 compose 的 hostname),否则重启后 DataNode 注册的主机名变化导致 NameNode 认不出;
  • HDFS 数据目录必须挂载数据卷,否则容器删除即数据丢失;
  • 容器内需要运行 sshd(Hadoop 启动脚本依赖 SSH),或改为每个角色一个容器、各自用 hdfs --daemon start 直接启动(bde2020 镜像即此思路,更符合"一个容器一个进程"的最佳实践);
  • 单机资源有限,务必设置 yarn.nodemanager.vmem-check-enabled=false 并调小容器内存;
  • 仅适合学习与功能验证,生产环境的 Hadoop 容器化应使用 Kubernetes + 专门的 Operator,或直接用云上 EMR。

6.5 Ambari 图形化部署的定位对比

维度 手工部署(本文主线) Ambari 图形化部署
学习价值 高:每个参数的含义、每个进程的作用都要亲手过一遍 低:向导点几下就完成,参数被隐藏
部署效率 低:3 节点约 1~2 小时,30 节点几乎不可行 高:几十个节点一小时内完成
配置一致性 靠人工 scp/rsync,容易漏节点 由 Ambari Server 统一下发,天然一致
组件集成 装 Hive/HBase/Spark 都要各自处理依赖与版本 一站式勾选安装,版本兼容性已验证
监控告警 需自行搭 Prometheus/Zabbix 内置 Metrics + Alerts + Dashboard
配置变更 改文件 + 分发 + 重启 界面改参数 → 自动标记需重启的服务 → 一键滚动重启
排错 需熟悉日志路径与参数 界面可直接看各节点日志与告警
适用 教学、小集群、深度定制 生产环境、中大型集群、多组件栈

建议路径:先按本文手工搭一遍双/三节点集群,理解 HDFS/YARN 的每个配置项与故障现象;再用 Ambari 部署生产集群,此时界面上每个参数都能看懂含义。

详见 16-07-Ambari部署Hadoop.md


七、排错清单

排错通用三步法:① jps 看进程是否在 → ② 看对应角色的 .log 和 .out 日志最后 200 行 → ③ 按下表定位。

xcall jps
tail -200 $HADOOP_HOME/logs/hadoop-root-datanode-$(hostname).log
grep -E "ERROR|FATAL|Exception|Caused by" $HADOOP_HOME/logs/*.log

7.1 DataNode 起不来的三大原因

原因一:clusterID 不一致(最常见)

日志特征:

java.io.IOException: Incompatible clusterIDs in /usr/local/hadoop/tmp/dfs/data:
namenode clusterID = CID-aaa...; datanode clusterID = CID-bbb...
FATAL org.apache.hadoop.hdfs.server.datanode.DataNode: Initialization failed for Block pool

原因:重复执行了 hdfs namenode -format,NameNode 换了新 clusterID,DataNode 还是旧的。

解决:见 5.1 节三种修复方法(删数据目录重新格式化 / 手工改 DataNode 的 VERSION / 用旧 clusterID 重新格式化)。

原因二:端口被占用

日志特征:

java.net.BindException: Address already in use
    at sun.nio.ch.Net.bind0(Native Method)
Problem binding to [0.0.0.0:50010] java.net.BindException: Address already in use

排查与解决:

netstat -tunlp | grep 50010
lsof -i:50010
ps -ef | grep DataNode
# 上次没停干净的僵尸进程
kill -9 <pid>
# 或 pid 文件残留导致 stop 脚本认为没运行
ls /tmp/hadoop-*.pid  /usr/local/hadoop/pids
rm -f /tmp/hadoop-root-datanode.pid
hadoop-daemon.sh start datanode

根因常常是 HADOOP_PID_DIR 默认在 /tmp,被系统清理后 stop-dfs.sh 找不到 pid 从而"停不掉",下次启动就撞端口。生产务必把 HADOOP_PID_DIR 改到 /var/run/hadoop。

原因三:目录权限或目录不存在

日志特征:

Invalid directory in dfs.datanode.data.dir: Incorrect permission for
/usr/local/hadoop/tmp/dfs/data, expected: rwxr-xr-x, while actual: rwxrwxrwx
java.io.IOException: All directories in dfs.datanode.data.dir are invalid
Directory is not writable: /data01/dfs/data

解决:

chown -R hadoop:hadoop /usr/local/hadoop/tmp
chmod 700 /usr/local/hadoop/tmp/dfs/data     # DataNode 数据目录期望 700
mkdir -p /usr/local/hadoop/tmp/dfs/data
df -h                                         # 顺便确认磁盘没满
# 若确实想放宽,可显式配置期望权限
<property>
    <name>dfs.datanode.data.dir.perm</name>
    <value>755</value>
</property>

7.2 NameNode 起不来 / 元数据损坏与恢复

症状与常见日志:

# 未格式化
java.io.IOException: NameNode is not formatted.
Directory /usr/local/hadoop/dfs/name is in an inconsistent state:
storage directory does not exist or is not accessible.

# 元数据文件损坏
java.io.IOException: Failed to load an FSImage file!
File /usr/local/hadoop/dfs/name/current/fsimage_0000000000000012345 is corrupt
java.io.EOFException: Unexpected end of file
org.apache.hadoop.hdfs.server.namenode.EditLogInputException:
  Error replaying edit log at offset ...

逐项排查:

# 1. 是否格式化过
ls /usr/local/hadoop/dfs/name/current/
# 应有 VERSION、fsimage_*、edits_*、seen_txid

# 2. 目录属主与权限
ls -ld /usr/local/hadoop/dfs/name

# 3. 磁盘是否满(NameNode 磁盘满会拒绝启动或进安全模式)
df -h

# 4. 端口占用
netstat -tunlp | grep 9000

# 5. JAVA_HOME
grep JAVA_HOME $HADOOP_HOME/etc/hadoop/hadoop-env.sh

从 SecondaryNameNode 恢复元数据(NameNode 元数据目录彻底损坏时):

方法一:把 SNN 的检查点目录直接拷回 NameNode 目录

# 1. 确认已停止 NameNode
hadoop-daemon.sh stop namenode

# 2. 在 SecondaryNameNode 所在节点上查看检查点数据
ls /usr/local/hadoop/tmp/dfs/namesecondary/current/

# 3. 拷贝到 NameNode 的元数据目录(先清空/备份原目录)
mv /usr/local/hadoop/dfs/name /usr/local/hadoop/dfs/name.bak
mkdir -p /usr/local/hadoop/dfs/name
scp -r hadoop02:/usr/local/hadoop/tmp/dfs/namesecondary/* /usr/local/hadoop/dfs/name/
# 注意:只拷 current 目录内容,不要把 in_use.lock 拷过来
rm -f /usr/local/hadoop/dfs/name/in_use.lock
chown -R hadoop:hadoop /usr/local/hadoop/dfs/name

# 4. 启动
hadoop-daemon.sh start namenode

方法二:使用官方的 -importCheckpoint

# 前提:dfs.namenode.name.dir 目录必须为空
rm -rf /usr/local/hadoop/dfs/name/*
# 把 SNN 检查点放到 dfs.namenode.checkpoint.dir 指定位置后执行
hdfs namenode -importCheckpoint
# 该命令会加载检查点并写入 name.dir,完成后 Ctrl+C 停止,再正常启动
hadoop-daemon.sh start namenode

edits 日志损坏时的应急恢复:

# 交互式修复:跳过损坏的事务(会丢失部分最新操作)
hdfs namenode -recover
# 提示 "Enter 'c' to continue, 'q' to quit, 's' to skip" 时按提示处理

# 元数据文件的离线查看工具(排查用)
hdfs oiv -i fsimage_0000000000000012345 -o /tmp/fsimage.xml -p XML   # 查看 fsimage
hdfs oev -i edits_0000000000000012340-0000000000000012350 -o /tmp/edits.xml   # 查看 edits

预防措施(比恢复重要得多):

<property>
    <name>dfs.namenode.name.dir</name>
    <value>file:///data01/dfs/name,file:///data02/dfs/name,file:///nfs_backup/dfs/name</value>
    <description>多目录冗余,其中一个放在 NFS 挂载点上</description>
</property>
# 定期备份元数据
hdfs dfsadmin -fetchImage /backup/fsimage-$(date +%F)
# 定期保存命名空间(在安全模式下)
hdfs dfsadmin -safemode enter
hdfs dfsadmin -saveNamespace
hdfs dfsadmin -safemode leave

7.3 SecondaryNameNode 起不来

0.0.0.0: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).

原因:start-dfs.sh 通过 SSH 登录 dfs.namenode.secondary.http-address 指定的主机(未配置时是 0.0.0.0)启动 SNN,但该地址不能免密登录。

解决:

ssh-copy-id -i ~/.ssh/id_rsa.pub root@0.0.0.0
ssh 0.0.0.0                          # 验证
# 或显式指定 SNN 主机名并确保对其免密

7.4 jps 有进程但 Web UI 打不开

原因一:防火墙 / SELinux 未关闭或未放行端口

# 在服务端确认端口在监听
netstat -tunlp | grep 50070
# 在服务端本机测试
curl -I http://localhost:50070
# 若本机通、外部不通 → 一定是防火墙
systemctl status firewalld
firewall-cmd --list-all
iptables -L -n
getenforce

解决:关闭防火墙或放行端口(见 3.3.4 节)。

原因二:bind 地址是 127.0.0.1,只监听本地回环

netstat -tunlp | grep 50070
# 错误:只监听回环,外部访问不到
tcp   0  0 127.0.0.1:50070   0.0.0.0:*   LISTEN   4499/java
# 正确:监听所有网卡
tcp   0  0 0.0.0.0:50070     0.0.0.0:*   LISTEN   4499/java

根因通常是 /etc/hosts 中把本机主机名映射到了 127.0.0.1,或配置里写成了 localhost。

解决:

# 1. 修正 /etc/hosts,删除 127.0.0.1 <hostname> 那一行
vim /etc/hosts
hostname -i          # 应返回真实 IP
# 2. 或显式让它监听所有网卡
<property>
    <name>dfs.namenode.http-address</name>
    <value>0.0.0.0:50070</value>
</property>
<property>
    <name>yarn.resourcemanager.webapp.address</name>
    <value>0.0.0.0:8088</value>
</property>

原因三:用了错误的端口(版本差异)

Hadoop 3.x 的 NameNode Web UI 是 9870 而不是 50070。用 2.x 的端口访问 3.x 集群自然打不开。见 5.4 节端口全表。

原因四:客户端(Windows)不认主机名

页面能打开但点 Datanodes、Browse Directory 时报错或超时,因为页面里的链接用的是主机名。需在 Windows 的 C:\Windows\System32\drivers\etc\hosts 中加入集群主机名映射。

7.5 hosts 配置错误导致节点互相不认

症状:

  • hdfs dfsadmin -report 显示 Live datanodes (0),但从节点 jps 里 DataNode 进程明明在;
  • DataNode 日志反复重试连接:
INFO ipc.Client: Retrying connect to server: hadoop01/192.168.224.10:9000.
Already tried 0 time(s); retry policy is RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)
java.net.UnknownHostException: hadoop01
java.net.NoRouteToHostException: No route to host
java.net.ConnectException: Connection refused

逐项排查:

# 1. 所有节点 hosts 是否完全一致
xcall "md5sum /etc/hosts"
xcall "cat /etc/hosts"

# 2. 本机主机名是否被错误映射到 127.0.0.1
hostname -i                # 若返回 127.0.0.1 就是错的
grep -n "127.0.0.1" /etc/hosts

# 3. 解析与连通性
ping -c 2 hadoop01
telnet hadoop01 9000        # 或 nc -zv hadoop01 9000
getent hosts hadoop01

# 4. NameNode 到底监听在哪个地址
ssh hadoop01 "netstat -tunlp | grep 9000"

关键结论:UnknownHostException → hosts 缺映射;Connection refused → 对端服务没起或监听在 127.0.0.1;No route to host → 防火墙。

7.6 SSH 免密失效导致启动脚本卡住

症状: 执行 start-dfs.sh 停在某一行等待输入密码:

hadoop02: root@hadoop02's password:

排查:

ssh -vvv hadoop02 2>&1 | grep -iE "publickey|authorized|permission"
# 服务端看 sshd 日志
tail -50 /var/log/secure

/var/log/secure 中若出现 Authentication refused: bad ownership or modes for directory /root 或 for file /root/.ssh/authorized_keys,即为权限问题。

修复:

chmod 700 ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_rsa
chown -R $(whoami):$(whoami) ~/.ssh
# 重新分发公钥
ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop02

其他失效原因:

原因 现象与解决
给错了用户配免密 用 root 启动却只给 hadoop 配了免密。为实际运行 Hadoop 的用户配置
忘了自己到自己免密 NameNode/SNN 启动不了。ssh-copy-id 到 localhost、本机主机名、0.0.0.0
known_hosts 中主机指纹变了(重装/克隆虚拟机) 报 REMOTE HOST IDENTIFICATION HAS CHANGED。执行 ssh-keygen -R hadoop02 删除旧记录
ssh-keygen 时设置了 passphrase 仍会提示输入口令。重新生成时直接回车留空
SELinux 阻止读 authorized_keys restorecon -Rv ~/.ssh 或关闭 SELinux
sshd 配置禁止了公钥认证 /etc/ssh/sshd_config 中确保 PubkeyAuthentication yes、AuthorizedKeysFile .ssh/authorized_keys,改后 systemctl restart sshd
家目录权限 777 sshd 拒绝使用密钥。chmod 700 ~

7.7 JAVA_HOME is not set

localhost: Error: JAVA_HOME is not set and could not be found.
hadoop02: Error: JAVA_HOME is not set and could not be found.

原因: start-dfs.sh 用 ssh host "command" 的非交互非登录 shell方式远程执行命令,这种 shell 不加载 /etc/profile 与 ~/.bash_profile,因此远端环境中没有 JAVA_HOME。即使你在本机 echo $JAVA_HOME 有值,远端也没有。

验证:

echo $JAVA_HOME                        # 本机有值
ssh hadoop02 'echo $JAVA_HOME'         # 远端为空 → 确认是这个问题

解决(在所有节点执行并分发):

vim $HADOOP_HOME/etc/hadoop/hadoop-env.sh
export JAVA_HOME=/usr/local/jdk         # 必须是绝对路径,不能用 ${JAVA_HOME}
# yarn-env.sh、mapred-env.sh 中同样加上
xsync $HADOOP_HOME/etc/hadoop

注意:不能写成 export JAVA_HOME=$JAVA_HOME(远端展开为空),必须写死绝对路径。

7.8 无法离开安全模式

org.apache.hadoop.hdfs.server.namenode.SafeModeException:
Cannot create directory /input. Name node is in safe mode.
The reported blocks 120 needs additional 5 blocks to reach the threshold 0.9990 of total blocks 125.

排查顺序:

# 1. 先看状态与原因
hdfs dfsadmin -safemode get
tail -100 $HADOOP_HOME/logs/hadoop-*-namenode-*.log | grep -i safe

# 2. DataNode 是否都起来了?块报告是否上报完?
hdfs dfsadmin -report | grep -E "Live datanodes|Missing|Under replicated"

# 3. 是否有丢失的块导致永远达不到阈值
hdfs fsck / | tail -30
hdfs fsck / -list-corruptfileblocks
场景 处理
刚启动,正在等块报告 正常,等 30 秒到几分钟。可用 hdfs dfsadmin -safemode wait 阻塞等待
DataNode 未全部启动 启动缺失的 DataNode,块报告齐了会自动退出
确实有块永久丢失(Missing blocks > 0) 先离开安全模式,再删掉损坏文件:hdfs dfsadmin -safemode leave → hdfs fsck / -delete(会删除损坏文件,不可逆)
NameNode 磁盘空间不足 清理磁盘。日志会有 NameNode low on available disk space
副本数设成 3 但只有 1 个 DataNode 把 dfs.replication 改为 1,或用 hdfs dfs -setrep 1 -R / 调整已有文件
急需恢复服务 hdfs dfsadmin -safemode leave 强制离开(注意:可能读到不完整数据)
hdfs dfsadmin -safemode leave           # 强制离开
hdfs fsck /                             # 离开后立刻检查健康状况
hdfs fsck / -delete                     # 删除损坏/丢失块的文件(谨慎)
hdfs fsck / -move                       # 把损坏文件移到 /lost+found

7.9 副本数不足与 Under-replicated blocks

症状: Web UI 首页出现警告,hdfs fsck / 输出:

Under-replicated blocks:  125 (100.0 %)
Mis-replicated blocks:    0
Default replication factor:   3
Average block replication:    1.0
/input/input.txt: Under replicated BP-xxx:blk_1073741825_1001.
  Target Replicas is 3 but found 1 live replica(s).
Status: HEALTHY        ← 注意状态仍是 HEALTHY,因为数据没丢

常见原因与处理:

原因 处理
伪分布式/单 DataNode 却把 dfs.replication 设成了 3 改配置为 1,并调整已有文件:hdfs dfs -setrep -R -w 1 /
某些 DataNode 宕机,副本尚未补齐 恢复节点即可;NameNode 会自动在其他节点补副本(受 dfs.namenode.replication.work.multiplier.per.iteration 限速)
集群空间不足,无处放新副本 扩容或清理数据
机架配置导致无法满足放置策略 检查机架感知脚本
# 调整整个 HDFS 的副本数(-w 表示等待完成)
hdfs dfs -setrep -R -w 2 /
# 只调整单个文件
hdfs dfs -setrep 3 /input/input.txt
# 查看某文件当前副本数
hdfs dfs -stat "%r" /input/input.txt
# 让 NameNode 更快地补副本(临时调大)
hdfs dfsadmin -setBalancerBandwidth 52428800

区分清楚:Under-replicated(副本不足)不等于数据丢失,只是冗余度不够;Missing blocks(缺失块)才是真的丢数据。

7.10 磁盘满导致 DataNode / NodeManager 变 unhealthy

症状:

  • yarn node -list -all 中该节点状态为 UNHEALTHY;
  • 8088 页面 Nodes 里显示 1/3 local-dirs are bad: /usr/local/hadoop/tmp/nm-local-dir; 1/1 log-dirs are bad;
  • 该节点不再接收新容器,集群可用资源骤减;
  • HDFS 写入报 Could not get block locations 或 There are 0 datanode(s) running and no node(s) are excluded。

排查:

df -h
du -sh /usr/local/hadoop/logs/*
du -sh /usr/local/hadoop/tmp/nm-local-dir/*
du -sh /var/log/*
hdfs dfsadmin -report | grep -E "Non DFS Used|DFS Remaining"
yarn node -list -all

解决:

# 1. 清理 YARN 本地缓存与容器日志(可安全删除)
rm -rf /usr/local/hadoop/tmp/nm-local-dir/usercache/*
rm -rf /usr/local/hadoop/logs/userlogs/*

# 2. 清理 Hadoop 自身日志
find /usr/local/hadoop/logs -name "*.log.*" -mtime +7 -delete
find /usr/local/hadoop/logs -name "*.out.*" -delete

# 3. 清理 HDFS 回收站
hdfs dfs -expunge
hdfs dfs -rm -r -skipTrash /user/root/.Trash

# 4. 删除无用的 HDFS 数据
hdfs dfs -du -h /            # 找出占用大的目录
hdfs dfs -rm -r -skipTrash /old_data

预防配置:

<!-- 每块盘为非 HDFS 预留 10GB,防止 HDFS 把盘写满 -->
<property>
    <name>dfs.datanode.du.reserved</name>
    <value>10737418240</value>
</property>
<!-- 容忍 1 块坏盘,不至于整个 DataNode 下线 -->
<property>
    <name>dfs.datanode.failed.volumes.tolerated</name>
    <value>1</value>
</property>
<!-- 放宽 NM 磁盘使用率阈值(默认 90%),或作为临时应急 -->
<property>
    <name>yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage</name>
    <value>95.0</value>
</property>
<property>
    <name>yarn.nodemanager.disk-health-checker.min-healthy-disks</name>
    <value>0.25</value>
</property>
<!-- 开启日志聚合并设置保留期,避免本地日志堆积 -->
<property>
    <name>yarn.log-aggregation-enable</name>
    <value>true</value>
</property>
<property>
    <name>yarn.nodemanager.log.retain-seconds</name>
    <value>10800</value>
</property>
<property>
    <name>yarn.nodemanager.localizer.cache.target-size-mb</name>
    <value>10240</value>
</property>

同时把 HADOOP_LOG_DIR 与 yarn.nodemanager.local-dirs 放到独立的大容量分区,并配置 logrotate 与磁盘容量监控告警。

7.11 YARN 任务一直 ACCEPTED 不进入 RUNNING

症状: 提交作业后控制台停在:

INFO mapreduce.Job: Running job: job_1546619214991_0001

8088 页面上应用状态一直是 ACCEPTED,Progress 为 0%,Diagnostics 里可能有:

[Mon Jan 01 06:40:12 +0800 2019] Application is added to the scheduler and is not yet activated.
Queue's AM resource limit exceeded.
Details : AM Partition = <DEFAULT_PARTITION>; AM Resource Request = <memory:1536, vCores:1>;
Queue Resource Limit for AM = <memory:2048, vCores:1>

根因:资源不足或资源配置不合理,YARN 无法为 ApplicationMaster 分配容器。

逐项排查:

# 1. 集群有没有可用资源?(8088 页面 Cluster Metrics 或命令行)
yarn node -list                            # NodeManager 数量是否为 0?
yarn node -list -all                       # 是否都 UNHEALTHY?
# 2. 看 8088 页面:Memory Total / Memory Used / VCores Total / VCores Used
# 3. 看队列使用率是否已满
yarn queue -status default
# 4. 看 RM 日志
tail -100 $HADOOP_HOME/logs/yarn-*-resourcemanager-*.log
原因 表现 解决
没有 NodeManager 注册 yarn node -list 返回 0 个节点;Memory Total = 0 启动 NodeManager;检查 yarn.resourcemanager.hostname 是否配置并分发;检查 8031 端口连通
所有 NodeManager 都 UNHEALTHY yarn node -list -all 显示 UNHEALTHY 磁盘满,见 7.10 节
NM 可用内存太小,装不下 AM Diagnostics 提示 AM resource limit exceeded 调大 yarn.nodemanager.resource.memory-mb,或调小 yarn.app.mapreduce.am.resource.mb
单容器申请超过 maximum-allocation-mb 报 InvalidResourceRequestException 调大 yarn.scheduler.maximum-allocation-mb 或调小任务内存
minimum-allocation-mb 设得过大 每个容器都被向上取整,很快占满 调小最小分配粒度(如 512)
队列 AM 资源占比上限 maximum-am-resource-percent 默认 0.1,即最多 10% 资源给 AM 在 capacity-scheduler.xml 中调大
已有作业占满资源 Memory Used ≈ Memory Total 等待或 yarn application -kill 掉旧作业
虚拟机内存太小(教学环境最常见) 1~2GB 内存的虚拟机跑默认配置必然不够 见下面的最小可行配置

小内存虚拟机(2GB)的最小可行配置:

<!-- yarn-site.xml -->
<property>
    <name>yarn.nodemanager.resource.memory-mb</name>
    <value>1536</value>
</property>
<property>
    <name>yarn.nodemanager.resource.cpu-vcores</name>
    <value>2</value>
</property>
<property>
    <name>yarn.scheduler.minimum-allocation-mb</name>
    <value>256</value>
</property>
<property>
    <name>yarn.scheduler.maximum-allocation-mb</name>
    <value>1536</value>
</property>
<property>
    <name>yarn.nodemanager.vmem-check-enabled</name>
    <value>false</value>
</property>
<!-- mapred-site.xml -->
<property>
    <name>yarn.app.mapreduce.am.resource.mb</name>
    <value>512</value>
</property>
<property>
    <name>yarn.app.mapreduce.am.command-opts</name>
    <value>-Xmx410m</value>
</property>
<property>
    <name>mapreduce.map.memory.mb</name>
    <value>512</value>
</property>
<property>
    <name>mapreduce.map.java.opts</name>
    <value>-Xmx410m</value>
</property>
<property>
    <name>mapreduce.reduce.memory.mb</name>
    <value>512</value>
</property>
<property>
    <name>mapreduce.reduce.java.opts</name>
    <value>-Xmx410m</value>
</property>

调大 AM 资源占比(capacity-scheduler.xml):

<property>
    <name>yarn.scheduler.capacity.maximum-am-resource-percent</name>
    <value>0.5</value>
</property>
yarn rmadmin -refreshQueues        # 刷新队列配置,无需重启 RM

7.12 虚拟内存超限被 kill

报错:

Container [pid=12345,containerID=container_1546619214991_0001_01_000002] is running
beyond virtual memory limits. Current usage: 186.4 MB of 1 GB physical memory used;
2.4 GB of 2.1 GB virtual memory used. Killing container.
Container killed on request. Exit code is 143
Container exited with a non-zero exit code 143

原因: 64 位 JVM 会预留大量虚拟地址空间(glibc 的 per-thread malloc arena、堆外内存映射、代码缓存等),实际物理占用很小。而 NodeManager 默认按 yarn.nodemanager.vmem-pmem-ratio(2.1)检查虚拟内存:申请 1GB 物理内存的容器,虚拟内存上限就是 2.1GB,很容易被突破,于是正常运行的容器被误杀。

解决方案一(推荐):关闭虚拟内存检查

<property>
    <name>yarn.nodemanager.vmem-check-enabled</name>
    <value>false</value>
    <description>关闭虚拟内存检查,避免误杀容器</description>
</property>

解决方案二:调大比率

<property>
    <name>yarn.nodemanager.vmem-pmem-ratio</name>
    <value>4.0</value>
</property>

解决方案三:调大容器物理内存(虚拟内存上限随之等比放大)

<property>
    <name>mapreduce.map.memory.mb</name>
    <value>2048</value>
</property>
<property>
    <name>mapreduce.map.java.opts</name>
    <value>-Xmx1638m</value>
</property>

解决方案四:限制 glibc 的 malloc arena 数量(减少虚拟内存占用)

# 在 hadoop-env.sh 中
export MALLOC_ARENA_MAX=4

修改后必须重启 NodeManager并分发配置:

xsync $HADOOP_HOME/etc/hadoop
xcall "yarn-daemon.sh stop nodemanager; yarn-daemon.sh start nodemanager"

区分物理内存超限: 若报错是 beyond physical memory limits,则是真的内存用超了,应该调大 mapreduce.*.memory.mb 并同步调整 java.opts(保持 -Xmx ≈ memory.mb × 0.8),而不是关闭检查。

7.13 Permission denied(HDFS 权限与代理用户)

情形一:普通 HDFS 权限不足

org.apache.hadoop.security.AccessControlException: Permission denied:
user=zhangsan, access=WRITE, inode="/user":root:supergroup:drwxr-xr-x

解决:

# 方法1:给目录授权(推荐)
hdfs dfs -mkdir -p /user/zhangsan
hdfs dfs -chown zhangsan:zhangsan /user/zhangsan
hdfs dfs -chmod 755 /user/zhangsan

# 方法2:把用户加入 supergroup(HDFS 超级组)
groupadd supergroup
usermod -aG supergroup zhangsan
hdfs dfsadmin -refreshUserToGroupsMappings

# 方法3:调整目标目录权限
hdfs dfs -chmod -R 777 /tmp

# 方法4:实验环境直接关闭权限校验(不推荐用于生产)
<property>
    <name>dfs.permissions.enabled</name>
    <value>false</value>
</property>
# 方法5:客户端伪装用户名(仅 simple 认证下有效)
export HADOOP_USER_NAME=root
hdfs dfs -put file /
# Java 程序中:System.setProperty("HADOOP_USER_NAME", "root");

情形二:Web UI 上不能删除/上传文件

Web UI 默认以 dr.who 身份操作,没有权限。解决:

<property>
    <name>hadoop.http.staticuser.user</name>
    <value>root</value>
</property>

情形三:代理用户(impersonation)未配置

org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.security.authorize.AuthorizationException):
User: root is not allowed to impersonate zhangsan

出现在 Hive/HiveServer2、Oozie、Hue、HttpFS、Spark on YARN、Sqoop 等代表其他用户提交作业的服务上。解决(core-site.xml,注意 <user> 处填发起代理的服务账号,不是被代理的用户):

<property>
    <name>hadoop.proxyuser.root.hosts</name>
    <value>*</value>
</property>
<property>
    <name>hadoop.proxyuser.root.groups</name>
    <value>*</value>
</property>
<property>
    <name>hadoop.proxyuser.hadoop.hosts</name>
    <value>*</value>
</property>
<property>
    <name>hadoop.proxyuser.hadoop.groups</name>
    <value>*</value>
</property>
<property>
    <name>hadoop.proxyuser.hive.hosts</name>
    <value>*</value>
</property>
<property>
    <name>hadoop.proxyuser.hive.groups</name>
    <value>*</value>
</property>
# 分发并重启 NameNode,或动态刷新
xsync $HADOOP_HOME/etc/hadoop
hdfs dfsadmin -refreshSuperUserGroupsConfiguration

情形四:本地文件系统权限(写日志/pid 失败)

mkdir: cannot create directory '/usr/local/hadoop/logs': Permission denied
/usr/local/hadoop/sbin/hadoop-daemon.sh: line 159: /tmp/hadoop-root-namenode.pid: Permission denied

解决:chown -R hadoop:hadoop /usr/local/hadoop,并确保 HADOOP_LOG_DIR、HADOOP_PID_DIR 对运行用户可写。

7.14 跑 wordcount 报 output 目录已存在

Exception in thread "main" org.apache.hadoop.mapred.FileAlreadyExistsException:
Output directory hdfs://localhost:9000/user/hadoop/output already exists
    at org.apache.hadoop.mapreduce.lib.output.FileOutputFormat.checkOutputSpecs(FileOutputFormat.java:146)
    at org.apache.hadoop.mapreduce.JobSubmitter.checkSpecs(JobSubmitter.java:266)

这是 MapReduce 的有意设计:为防止误覆盖已有的计算结果,作业提交前会检查输出目录,存在即报错退出(连 Map 都不会启动)。

解决:

hdfs dfs -rm -r /output
# 或使用带时间戳的输出目录,避免每次手工删除
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.9.2.jar \
  wordcount /input /output_$(date +%Y%m%d%H%M%S)
# 单机模式下删除的是本地目录
rm -rf /usr/local/hadoop/output

在自己写的程序中可以在提交前自动删除:

Path outPath = new Path(args[1]);
FileSystem fs = FileSystem.get(conf);
if (fs.exists(outPath)) {
    fs.delete(outPath, true);
}

注意:输出目录的父目录可以存在,只有最终输出目录本身不能存在。

7.15 其他常见错误速查

报错 原因 解决
WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform 官方包中的 native 库是 32 位或与本机 glibc 不匹配 可以忽略(功能正常,仅压缩性能略低)。彻底解决需自行编译;或在 log4j.properties 中屏蔽:log4j.logger.org.apache.hadoop.util.NativeCodeLoader=ERROR
Retrying connect to server: 0.0.0.0/0.0.0.0:8032 mapreduce.framework.name=yarn 但 ResourceManager 没启动,或 yarn.resourcemanager.hostname 未配置 启动 YARN;或把 mapred-site.xml 改回模板名用本地模式
Name node is in safe mode 见 7.8 节 等待或 -safemode leave
No route to host 防火墙 关防火墙或放行端口
Connection reset by peer / All datanodes are bad DataNode 端 dfs.datanode.max.transfer.threads 不足,或 ulimit 太小 调大 max.transfer.threads 到 8192,调大 ulimit -n
Too many open files 文件句柄限制 见 3.3.9 节 ulimit 配置
could only be replicated to 0 nodes instead of minReplication (=1). There are 0 datanode(s) running 没有可用 DataNode(未启动/全部 unhealthy/磁盘满/被 exclude) hdfs dfsadmin -report 查 Live datanodes;检查磁盘与黑名单
File /xxx could only be written to 0 of the 1 minReplication nodes 同上 同上
java.lang.ClassNotFoundException: org.apache.hadoop.mapreduce.v2.app.MRAppMaster 3.x 中 mapreduce.application.classpath 未配置 在 mapred-site.xml 中加 mapreduce.application.classpath,值取 hadoop classpath 输出
Exit code: 1 / Exception from container-launch 容器启动失败,真实原因在容器日志里 yarn logs -applicationId <id> 或看 logs/userlogs/
Error: Java heap space 任务 JVM 堆太小 调大 mapreduce.map.java.opts,同时调大 mapreduce.map.memory.mb
GC overhead limit exceeded(NameNode) NameNode 堆不足,文件/块太多 调大 HADOOP_NAMENODE_OPTS 的 -Xmx;合并小文件
Operation category READ is not supported in state standby HA 集群中客户端访问了 Standby NameNode 使用 nameservice 名而不是具体主机名访问;确认 dfs.client.failover.proxy.provider 已配置
stop-dfs.sh 提示 no namenode to stop 但进程还在 pid 文件被 /tmp 清理丢失 kill -9 手工停止;把 HADOOP_PID_DIR 改到 /var/run/hadoop
hdfs: command not found PATH 未包含 $HADOOP_HOME/bin source /etc/profile,检查环境变量
jps 看不到任何进程但 ps -ef|grep java 有 用不同用户执行的 jps(jps 只显示当前用户的进程) 用启动 Hadoop 的那个用户执行 jps,或用 ps -ef | grep java
页面 Datanodes 显示 0 但从节点 jps 有 DataNode clusterID 不一致 / hosts 错误 / 时间不同步 / 白名单未包含 见 7.1、7.5 节;检查 dfs.hosts
集群重启后数据全丢 hadoop.tmp.dir 在 /tmp 下被系统清理 显式配置 hadoop.tmp.dir 到非 /tmp 路径

八、附录:使用 Eclipse 编写并运行自定义 MapReduce 程序

详见 16-04-MapReduce编程.md

本节内容属于 MapReduce 编程范畴,此处保留完整流程,作为"集群搭建完成后的第一个自研作业"验收手段:只有集群的 HDFS 读写、YARN 调度、JobHistory 都正常,自定义 JAR 才能跑通。深入的 MapReduce 编程模型、Combiner/Partitioner/自定义 Writable 等内容见 16-04。

8.1 准备工作

  • Eclipse 压缩包:eclipse2018.zip(解压后直接运行,需本机安装 JDK 8)
  • 新建 Java 项目,创建类 WordCount.java,包名为 com.testing
  • 本机 JDK 版本应与集群 JDK 版本一致(都是 JDK 8),否则会报 UnsupportedClassVersionError

8.2 导入 Hadoop JAR 包

将以下路径下的所有 JAR 文件添加到项目的 Build Path(Eclipse 中右键项目 → Build Path → Configure Build Path → Libraries → Add External JARs):

/usr/local/hadoop/share/hadoop/common/
/usr/local/hadoop/share/hadoop/common/lib
/usr/local/hadoop/share/hadoop/mapreduce
/usr/local/hadoop/share/hadoop/mapreduce/lib

其中:

路径 作用
share/hadoop/common/ hadoop-common-x.x.x.jar,提供 Configuration、Path、FileSystem、Text、IntWritable 等基础类
share/hadoop/common/lib common 的第三方依赖(commons-*、guava、log4j、slf4j 等)
share/hadoop/mapreduce hadoop-mapreduce-client-core.jar 等,提供 Job、Mapper、Reducer、FileInputFormat
share/hadoop/mapreduce/lib MapReduce 的第三方依赖

若还要操作 HDFS API,再补充 share/hadoop/hdfs/ 与 share/hadoop/hdfs/lib;若用 YARN API,补充 share/hadoop/yarn/ 与 share/hadoop/yarn/lib。

提示:也可以用 Maven 管理依赖,避免手工导入几百个 JAR,只需引入 hadoop-client 一个依赖即可自动传递依赖全部所需包。

8.3 WordCount.java 源码

package com.testing;

import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.io.IntWritable;

public class WordCount {

    /**
     * Object: 输入数据的具体内容
     * Text:每行的文本数据
     * Text:每个单词分解后的统计结果
     * IntWritable:输出记录的结果
     */
    @SuppressWarnings("unused")
    private static class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> {
        /**
         * 进行 Map 处理
         */
        @Override
        protected void map(Object key, Text value,
                           Mapper<Object, Text, Text, IntWritable>.Context context)
                throws IOException, InterruptedException {
            // 获得需要处理的字符串
            String lineContent = value.toString();
            // 按空格进行过滤,将单词组成数组
            String result[] = lineContent.split(" ");
            for (int x = 0; x < result.length; x++) {
                // 参数一表示要输出的字符串,参数二表示计数 1
                context.write(new Text(result[x]), new IntWritable(1));
            }
        }
    }

    /**
     * reduce 处理
     * Text: Map 输出的文本内容
     * IntWritable:Map 处理的个数
     * Text:Reduce 输出文本
     * IntWritable:Reduce 的输出个数
     */
    private static class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values,
                              Reducer<Text, IntWritable, Text, IntWritable>.Context context)
                throws IOException, InterruptedException {
            // 单词出现次数的计数器
            int sum = 0;
            for (IntWritable count : values) {
                sum += count.get();
            }
            context.write(key, new IntWritable(sum));
        }
    }

    public static void main(String args[]) throws Exception {
        if (args.length != 2) {
            System.out.println("Usage: hadoop jar filename.jar input_filename.txt output_path");
            System.exit(1);
        }

        Configuration conf = new Configuration();
        // 将命令行参数组装成数组
        String[] argArray = new GenericOptionsParser(conf, args).getRemainingArgs();

        // 定义 Hadoop 作业
        Job job = Job.getInstance();
        job.setJarByClass(WordCount.class);

        // Mapper 设置
        job.setMapperClass(WordCountMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        // Reduce 设置
        job.setReducerClass(WordCountReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 定义路径
        FileInputFormat.addInputPath(job, new Path(argArray[0]));
        FileOutputFormat.setOutputPath(job, new Path(argArray[1]));
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }
}

代码要点说明:

代码 说明
job.setJarByClass(WordCount.class) 让 Hadoop 根据这个类所在的 JAR 定位要分发到集群的作业 JAR,不写会报 ClassNotFoundException
Mapper<Object, Text, Text, IntWritable> 四个泛型依次是:输入 key 类型(行偏移量)、输入 value 类型(一行文本)、输出 key 类型、输出 value 类型
Reducer<Text, IntWritable, Text, IntWritable> 输入必须与 Mapper 的输出类型一致
GenericOptionsParser 解析 -D、-conf、-fs、-jt、-files、-libjars 等通用参数,把剩余参数交给业务代码,使程序支持 hadoop jar x.jar -D mapreduce.job.reduces=2 /in /out 这类写法
setMapOutputKeyClass 与 setOutputKeyClass 前者是 Map 阶段输出类型,后者是最终(Reduce)输出类型;两者一致时可只写后者
context.write(...) Map/Reduce 输出键值对的唯一出口
Iterable<IntWritable> values 同一个 key 的所有 value 集合,由 Shuffle 阶段分组后传入
waitForCompletion(true) 提交作业并阻塞等待,true 表示打印进度信息

8.4 导出 JAR 包

将项目导出为 WordCount.jar,并设置主执行类为 com.testing.WordCount。

Eclipse 操作路径:右键项目 → Export → Java → Runnable JAR file(或 JAR file)→ 指定 Launch configuration / Main class 为 com.testing.WordCount → 导出为 WordCount.jar。

将 JAR 包上传到 Hadoop 虚拟机的 /root/ 目录下:

# 在 Windows 上用 Xftp / WinSCP 上传,或在 Linux 上用 scp
scp WordCount.jar root@192.168.224.132:/root/

若导出时未指定主类,运行时必须在命令行显式给出全限定类名: hadoop jar WordCount.jar com.testing.WordCount /input/input.txt /output

8.5 启动 Hadoop 并验证

start-all.sh
jps

在 HDFS 中创建测试目录:

hadoop fs -mkdir /input
hadoop fs -ls /
Found 1 items
drwxr-xr-x   - root supergroup          0 2019-01-01 06:12 /input

准备测试文件并上传:

echo this is my computer this is my computer > /root/input.txt
hadoop fs -put /root/input.txt /input/
hadoop fs -cat /input/input.txt
this is my computer this is my computer

8.6 运行自定义 JAR

执行 JAR 时,必须保证 output 目录在 HDFS 中不存在:

cd /root
hadoop jar WordCount.jar /input/input.txt /output
hadoop fs -cat /output/part-r-00000
computer        2
is      2
my      2
this    2

结果表示词频分析成功。

运行过程中可以在 http://192.168.224.132:8088 观察应用从 ACCEPTED → RUNNING → FINISHED(SUCCEEDED)的状态变化,在 http://192.168.224.132:19888 的 JobHistory 中查看已完成作业的 Map/Reduce 任务明细与计数器。

8.7 重复运行前清理输出目录

若输出目录已存在,会抛出 FileAlreadyExistsException。再次执行前需删除:

hdfs dfs -rm -r /output

详见 7.14 节对该异常的完整说明与多种规避方式。

8.8 自定义 JAR 跑不通的排查顺序

现象 排查点
Could not find or load main class 导出时未设置 Main class;命令行补上全限定类名
ClassNotFoundException: com.testing.WordCount$WordCountMapper 忘记 job.setJarByClass();或导出的 JAR 未包含内部类
UnsupportedClassVersionError: ... has been compiled by a more recent version 本机编译用的 JDK 版本高于集群 JDK;在 Eclipse 中把 Compiler compliance level 降到与集群一致(1.8)
NoClassDefFoundError(第三方库) 用 -libjars 分发依赖,或打成含依赖的 fat jar(Maven shade 插件)
Input path does not exist 输入路径写成了本地路径;HDFS 上确认 hadoop fs -ls /input
作业成功但输出为空 输入文件为空;或分隔符不匹配(源码按单个空格 split(" ") 切分,制表符/多空格会产生空串 key)
结果中出现空字符串统计项 split(" ") 对连续空格会产生空元素,改用 split("\\s+") 并过滤空串

练习

以下练习覆盖源文档中的全部实验步骤,并补充了排错与设计类题目。建议在虚拟机中逐题实操,做完一题打一次快照,便于回退。

练习 1:概念自测(Hadoop 与生态)

题目:

  1. Google 的三篇论文分别对应 Hadoop 生态中的哪三个组件?
  2. Hadoop 1.x 与 2.x 在架构上最本质的区别是什么?
  3. Hadoop 3.x 相比 2.x 有哪些关键变化(至少列 4 项)?
  4. Hadoop 的核心组件有哪些?各自解决什么问题?
  5. HDFS 为什么不适合存储大量小文件?
  6. 为什么 HDFS 默认副本数是 3?三个副本分别放在哪里?
  7. SecondaryNameNode 是 NameNode 的热备吗?它到底做什么?
  8. Hadoop 的三种运行模式有什么区别?各自适用于什么场景?

参考答案:

  1. GFS → HDFS;MapReduce → Hadoop MapReduce;BigTable → HBase。
  2. 1.x 由 JobTracker 同时负责资源管理与作业调度,存在单点瓶颈,且只能跑 MapReduce;2.x 引入 YARN,把资源管理(ResourceManager)与作业管理(ApplicationMaster)分离,使集群可同时运行 MapReduce、Spark、Flink、Tez 等多种计算框架,并支持 NameNode HA。
  3. ① 最低 JDK 由 7 提升到 8;② 支持 Erasure Coding(纠删码),存储开销从 200% 降到约 50%;③ 支持一个 NameSpace 下多个 Standby NameNode;④ 默认端口大幅调整(50070→9870、50075→9864、8088 保持、50090→9868),避开 Linux 临时端口范围;⑤ slaves 文件改名为 workers;⑥ YARN Timeline Service v2;⑦ 支持多个 Standby NN、Intra-DataNode 磁盘均衡器(diskbalancer);⑧ Shell 脚本重写,hadoop-daemon.sh 被 hdfs --daemon 取代。
  4. HDFS 解决海量数据的分布式存储与容错;YARN 解决集群资源的统一管理与调度;MapReduce 提供分布式并行计算模型;Common 提供 RPC、序列化、配置、文件系统抽象等公共支撑。
  5. 每个文件、目录、块在 NameNode 内存中约占 150 字节元数据,一亿个小文件就需要约 20GB 内存,NameNode 内存成为瓶颈;同时每个小文件都会产生一个 InputSplit 与一个 Map 任务,任务启动开销远大于计算开销。解决办法:SequenceFile、MapFile、HAR 归档、CombineFileInputFormat、HBase。
  6. 3 副本是可靠性与存储成本之间的经验平衡点(可容忍 2 个副本同时损坏)。放置策略:第 1 个副本放在写入客户端所在节点(若客户端在集群外则随机挑一个节点);第 2 个副本放在不同机架的某个节点;第 3 个副本放在第 2 个副本同机架的不同节点。这样既跨机架容灾,又避免三次跨机架写入的带宽开销。
  7. 不是热备。它周期性地(默认 3600 秒或 edits 达 100 万条事务)把 NameNode 的 fsimage 与 edits 拉过来合并成新的 fsimage 再推回去(checkpoint),目的是防止 edits 无限增长导致 NameNode 重启时回放时间过长。它保存的是滞后的元数据副本,NameNode 崩溃时只能作为最后手段做部分恢复,会丢失最后一个检查点之后的数据。真正的热备是 HA 架构中的 Standby NameNode。
  8. 单机模式(Standalone):不启动任何守护进程,用本地文件系统,一个 JVM 内跑 MapReduce,用于调试程序逻辑;伪分布式(Pseudo-Distributed):所有守护进程跑在一台机器的独立 JVM 中,用 HDFS,用于学习与功能验证;完全分布式(Fully-Distributed):守护进程分布在多台机器,用于测试与生产。

练习 2:前置环境准备(CentOS 6 / CentOS 7 双版本)

题目: 准备两台虚拟机(192.168.224.10 master、192.168.224.11 slave1),完成 Hadoop 集群的全部前置准备,并分别写出 CentOS 6 与 CentOS 7 的命令。要求:静态 IP、主机名、hosts、关防火墙与 SELinux、时间同步、创建 hadoop 用户并授予 sudo、ulimit 调优。

参考答案:

# ===== 1. 主机名 =====
# CentOS 7
hostnamectl set-hostname master
# CentOS 6
sed -i 's/^HOSTNAME=.*/HOSTNAME=master/' /etc/sysconfig/network
hostname master              # 立即生效,无需重启

# ===== 2. hosts(两台机器内容必须完全一致)=====
cat >> /etc/hosts <<'EOF'
192.168.224.10  master
192.168.224.11  slave1
EOF
# 检查:确保没有 "127.0.0.1 master" 这类会导致监听地址错误的行

# ===== 3. 防火墙 =====
# CentOS 7
systemctl stop firewalld && systemctl disable firewalld
# CentOS 6
service iptables stop && chkconfig iptables off

# ===== 4. SELinux =====
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
getenforce                   # 应输出 Permissive 或 Disabled

# ===== 5. 时间同步 =====
# CentOS 7(chrony)
yum -y install chrony
systemctl enable chronyd --now
chronyc sources -v
# CentOS 6(ntp)
yum -y install ntp
service ntpd start && chkconfig ntpd on
ntpdate -u ntp.aliyun.com

# ===== 6. hadoop 用户 =====
useradd -m hadoop -s /bin/bash
echo 'hadoop' | passwd --stdin hadoop
echo 'hadoop ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/hadoop
chmod 440 /etc/sudoers.d/hadoop

# ===== 7. ulimit =====
cat >> /etc/security/limits.conf <<'EOF'
hadoop  soft  nofile  65536
hadoop  hard  nofile  65536
hadoop  soft  nproc   32768
hadoop  hard  nproc   32768
EOF
# CentOS 7 改 /etc/security/limits.d/20-nproc.conf
# CentOS 6 改 /etc/security/limits.d/90-nproc.conf
su - hadoop -c 'ulimit -n; ulimit -u'

验证要点: 两台机器互相 ping master、ping slave1 均通;getenforce 不是 Enforcing;date 时间差在 1 秒内。

练习 3:SSH 免密登录(含自身免密)

题目: 配置 master 到 master、master 到 slave1 的免密登录,并说明为什么 master 必须能免密登录自己。给出验证方法与权限要求。

参考答案:

su - hadoop
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa       # 一路回车,不设密码短语
ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop@master   # 自己也要!
ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop@slave1
# 或手工方式
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# 验证:每条都应无需输入密码,且第一次之后不再有 yes/no 提示
ssh master  date && exit
ssh slave1  date && exit
ssh localhost date && exit
ssh 0.0.0.0  date && exit

为什么必须能免密登录自己: start-dfs.sh / start-yarn.sh 是通过 ssh <host> "启动命令" 的方式远程拉起每个守护进程的,而 NameNode、SecondaryNameNode、ResourceManager 往往就在 master 本机上,脚本同样走一次 SSH。若 master 到自身不免密,启动脚本会停下来反复索要密码(表现为脚本"卡住"),或直接报 Permission denied (publickey,password)。

权限要求: 家目录不能对组/其他人可写(chmod 700 ~ 或至少 755),~/.ssh 为 700,authorized_keys 为 600,id_rsa 为 600。权限过宽时 sshd 会静默拒绝公钥认证,排查看 /var/log/secure 中的 Authentication refused: bad ownership or modes。

练习 4:JDK 安装与环境变量

题目: 卸载系统自带的 OpenJDK,安装 Oracle JDK 8u191 到 /usr/local/jdk,配置环境变量并验证。说明为什么配好了 /etc/profile 里的 JAVA_HOME,还必须在 hadoop-env.sh 里再写一遍。

参考答案:

# 1. 检查并卸载自带 OpenJDK
rpm -qa | grep -i jdk
rpm -qa | grep java | xargs rpm -e --nodeps
java -version                     # 应报 command not found

# 2. 解压安装
tar -zxvf jdk-8u191-linux-x64.tar.gz -C /usr/local/
mv /usr/local/jdk1.8.0_191 /usr/local/jdk

# 3. 环境变量
cat >> /etc/profile <<'EOF'
export JAVA_HOME=/usr/local/jdk
export PATH=$PATH:$JAVA_HOME/bin
export CLASS_PATH=$JAVA_HOME/lib
EOF
source /etc/profile

# 4. 验证
java -version                     # java version "1.8.0_191"
javac -version
echo $JAVA_HOME

为什么 hadoop-env.sh 里还要写: start-dfs.sh 通过 SSH 以非交互、非登录方式在远端执行命令(ssh host "command"),这种 shell 不会加载 /etc/profile、~/.bash_profile(只会加载 ~/.bashrc 中位于交互性检查之前的部分),因此 $JAVA_HOME 为空,启动脚本报 Error: JAVA_HOME is not set and could not be found.。在 etc/hadoop/hadoop-env.sh 中显式写死绝对路径可彻底规避:

export JAVA_HOME=/usr/local/jdk

验证技巧:

ssh slave1 "echo \$JAVA_HOME"     # 若输出为空,就是上述问题的直接证据
ssh slave1 "java -version"

练习 5:伪分布式部署(源文档原题)

题目: 在单机 192.168.224.10 上完成 Hadoop 2.7.1 伪分布式部署,跑通 WordCount,并通过 Web UI 查看 HDFS 状态。

参考答案:

# 1. 解压
tar -zxvf hadoop-2.7.1.tar.gz -C /usr/local/
mv /usr/local/hadoop-2.7.1 /usr/local/hadoop

# 2. 环境变量
cat >> /etc/profile <<'EOF'
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
EOF
source /etc/profile
hadoop version

# 3. hadoop-env.sh
echo 'export JAVA_HOME=/usr/local/jdk' >> $HADOOP_HOME/etc/hadoop/hadoop-env.sh
<!-- core-site.xml -->
<configuration>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://192.168.224.10:9000</value>
    </property>
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/usr/local/hadoop/tmp</value>
    </property>
</configuration>
<!-- hdfs-site.xml -->
<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:///usr/local/hadoop/dfs/name</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:///usr/local/hadoop/dfs/data</value>
    </property>
</configuration>
# 4. 创建目录、格式化、启动
mkdir -p /usr/local/hadoop/dfs/name /usr/local/hadoop/dfs/data /usr/local/hadoop/tmp
hdfs namenode -format            # 看到 "Exiting with status 0" 即成功
start-dfs.sh
jps                              # 应有 NameNode / DataNode / SecondaryNameNode

# 5. 跑 WordCount
echo this is my computer this is my computer > /root/input.txt
hdfs dfs -mkdir -p /input
hdfs dfs -put /root/input.txt /input/
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.1.jar \
  wordcount /input /output
hdfs dfs -cat /output/part-r-00000

预期输出:

computer        2
is      2
my      2
this    2

Web UI:http://192.168.224.10:50070(3.x 为 9870),Overview 页应显示 Live Nodes: 1、Safemode is off。

练习 6:完全分布式部署(源文档原题)

题目: 以 192.168.224.10 为 Master(NameNode + ResourceManager + SecondaryNameNode),192.168.224.11 为 Slave(DataNode + NodeManager),搭建两节点集群。要求写出四个 XML 的关键配置、slaves 文件内容、分发方式和验证方法。

参考答案(关键配置):

<!-- core-site.xml -->
<property>
    <name>fs.defaultFS</name>
    <value>hdfs://192.168.224.10:9000</value>
</property>
<property>
    <name>hadoop.tmp.dir</name>
    <value>/usr/local/hadoop/tmp</value>
</property>
<!-- hdfs-site.xml -->
<property>
    <name>dfs.replication</name>
    <value>2</value>
</property>
<property>
    <name>dfs.namenode.secondary.http-address</name>
    <value>192.168.224.10:50090</value>
</property>
<!-- mapred-site.xml -->
<property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
</property>
<property>
    <name>mapreduce.jobhistory.address</name>
    <value>192.168.224.10:10020</value>
</property>
<property>
    <name>mapreduce.jobhistory.webapp.address</name>
    <value>192.168.224.10:19888</value>
</property>
<!-- yarn-site.xml -->
<property>
    <name>yarn.resourcemanager.hostname</name>
    <value>192.168.224.10</value>
</property>
<property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
</property>
# masters / slaves(2.x;3.x 中 slaves 改名为 workers)
echo 192.168.224.10 > $HADOOP_HOME/etc/hadoop/master
echo 192.168.224.11 > $HADOOP_HOME/etc/hadoop/slaves

# 分发(源文档打包方式)
cd /usr/local
tar -zcvf ~/hadoop.master.tar.gz ./hadoop
scp ~/hadoop.master.tar.gz root@192.168.224.11:/root/
ssh root@192.168.224.11 "tar -zxvf /root/hadoop.master.tar.gz -C /usr/local/"
# 或用 rsync 只同步配置
xsync $HADOOP_HOME/etc/hadoop

# 启动与验证(只在 Master 上执行)
hdfs namenode -format
start-dfs.sh
start-yarn.sh
mr-jobhistory-daemon.sh start historyserver
jps                                   # Master: NameNode/SecondaryNameNode/ResourceManager/JobHistoryServer
ssh slave1 jps                        # Slave: DataNode/NodeManager
hdfs dfsadmin -report                 # Live datanodes (1)

易错点: ① 只改了 Master 的配置忘记分发;② 从节点残留了旧的 dfs/data 导致 clusterID 不一致;③ slaves 文件里有空行或多余空格;④ 忘记关防火墙导致 Web UI 打不开、DataNode 注册不上。

练习 7:HDFS Shell 操作(源文档原题)

题目: 使用三种命令形式(hadoop fs、hadoop dfs、hdfs dfs)完成:创建目录、上传文件、查看内容、下载、改名、删除;并说明三者的区别。

参考答案:

hadoop fs -mkdir /input                 # 创建目录
hadoop fs -ls /                          # 列目录
echo this is my computer > /root/input.txt
hadoop fs -put /root/input.txt /input/   # 上传
hadoop fs -cat /input/input.txt          # 查看
hadoop fs -get /input/input.txt /tmp/    # 下载
hadoop fs -mv /input/input.txt /input/a.txt   # 改名/移动
hadoop fs -cp /input/a.txt /input/b.txt  # 复制
hadoop fs -du -h /input                  # 查看大小
hadoop fs -rm /input/b.txt               # 删除文件
hdfs dfs -rm -r /output                  # 递归删除目录
hadoop fs -chmod 755 /input
hadoop fs -chown hadoop:hadoop /input
hadoop fs -tail /input/a.txt
hadoop fs -appendToFile /root/more.txt /input/a.txt
hadoop fs -setrep 2 /input/a.txt         # 修改副本数

三者区别:

命令形式 说明
hadoop fs 最通用,操作任意 Hadoop 支持的文件系统(HDFS、本地 file://、S3、FTP 等),推荐使用
hadoop dfs 只能操作 HDFS,已废弃(deprecated),执行时会打印 DEPRECATED: Use of this script to execute hdfs command is deprecated.
hdfs dfs 只能操作 HDFS,是 hadoop dfs 的替代者,官方推荐用于 HDFS 专属操作

练习 8:格式化与 clusterID 排错

题目: 某学员在集群运行一段时间后,因为启动失败又执行了一次 hdfs namenode -format,结果 DataNode 起不来。请说明原因、日志报错、三种修复方法,以及"能不能保住数据"。

参考答案:

原因: 格式化会重新生成一个全新的 clusterID 写入 NameNode 的 dfs/name/current/VERSION,而 DataNode 的 dfs/data/current/VERSION 里还是旧的 clusterID。DataNode 注册时发现两者不一致,主动拒绝服务并退出。

日志(logs/hadoop-*-datanode-*.log):

java.io.IOException: Incompatible clusterIDs in /usr/local/hadoop/dfs/data:
namenode clusterID = CID-8f2a1c5e-...; datanode clusterID = CID-3b7d9a11-...
    at org.apache.hadoop.hdfs.server.datanode.DataStorage.doTransition(DataStorage.java:760)
WARN org.apache.hadoop.hdfs.server.datanode.DataNode: Ending block pool service for: ...

修复方法一(推荐,数据可丢的实验环境): 删除所有节点的数据目录后重新格式化。

stop-all.sh
xcall "rm -rf /usr/local/hadoop/dfs/data/* /usr/local/hadoop/dfs/name/* /usr/local/hadoop/tmp/*"
xcall "rm -rf /usr/local/hadoop/logs/*"
hdfs namenode -format
start-dfs.sh

修复方法二(保数据): 手工把 DataNode 的 VERSION 文件中的 clusterID 改成 NameNode 的值。

cat /usr/local/hadoop/dfs/name/current/VERSION      # 取 clusterID
vim /usr/local/hadoop/dfs/data/current/VERSION      # 改成上面的值
# 注意:还有 BP-xxx 目录下的 VERSION,也需要保持一致
hadoop-daemon.sh start datanode

修复方法三: 反过来把 NameNode 的 clusterID 改回旧值(仅在只格式化了 NameNode、DataNode 数据完好时可行,成功率更高)。

能不能保住数据: 元数据(目录树、文件与块的映射)在格式化时已被清空,即使把 clusterID 改回一致,NameNode 也不知道这些块属于哪个文件,DataNode 上报的块会全部被判为"多余块"并被删除。除非事先备份了 dfs/name 目录或有 SecondaryNameNode 的 checkpoint 可以 -importCheckpoint。结论:格式化只在集群初次搭建时执行一次,事后想重来必须先想清楚数据是否可弃。

练习 9:Web UI 打不开排错

题目: jps 显示 NameNode 正常运行,但浏览器访问 http://192.168.224.10:50070 无法打开。请给出完整排查步骤。

参考答案:

# 1. 本机能不能访问?—— 区分"服务问题"还是"网络问题"
curl -I http://localhost:50070
curl -I http://192.168.224.10:50070

# 2. 端口是否在监听?监听在哪个地址?
netstat -tnlp | grep 50070
ss -tnlp | grep 50070
# 若显示 127.0.0.1:50070 → 只监听本地回环,外部访问不到
# 应显示 0.0.0.0:50070 或 192.168.224.10:50070

# 3. 防火墙
systemctl status firewalld            # CentOS 7
iptables -L -n                        # CentOS 6
firewall-cmd --add-port=50070/tcp --permanent && firewall-cmd --reload

# 4. 版本端口对不对?
# 2.x: 50070   3.x: 9870
grep -A2 'dfs.namenode.http-address' $HADOOP_HOME/etc/hadoop/hdfs-site.xml

# 5. 日志里有没有绑定失败
grep -i 'bind\|Address already in use' $HADOOP_HOME/logs/hadoop-*-namenode-*.log

四类常见原因: ① 防火墙未关或未放行端口(最常见);② 版本端口记错(3.x 用 9870);③ 监听地址被 /etc/hosts 中 127.0.0.1 master 之类的错误行影响,绑定到了回环地址;④ 宿主机与虚拟机网段不通(VMware NAT/仅主机模式配置问题)。

显式指定监听地址:

<property>
    <name>dfs.namenode.http-address</name>
    <value>0.0.0.0:50070</value>
</property>

练习 10:YARN 作业一直 ACCEPTED

题目: 一台 2GB 内存的虚拟机上提交 WordCount,作业长时间停在 Running job: job_xxx,8088 上状态是 ACCEPTED。请给出诊断命令与修复配置。

参考答案:

yarn node -list                 # NodeManager 是否为 0?
yarn node -list -all            # 是否 UNHEALTHY(磁盘满)?
yarn queue -status default      # 队列使用率
df -h                           # 磁盘使用率是否超过 90%
tail -100 $HADOOP_HOME/logs/yarn-*-resourcemanager-*.log

修复(yarn-site.xml + mapred-site.xml,配置见 7.11 节小内存最小可行配置):把 yarn.nodemanager.resource.memory-mb 设为 1536,yarn.scheduler.minimum-allocation-mb 设为 256,yarn.app.mapreduce.am.resource.mb 设为 512,mapreduce.{map,reduce}.memory.mb 设为 512,并关闭 yarn.nodemanager.vmem-check-enabled。修改后分发配置并重启 YARN。

练习 11:Web UI 无法删除文件

题目: 在 HDFS Web UI 的 Browse Directory 页面点击删除文件,报 Permission denied: user=dr.who, access=WRITE。请解释并修复。

参考答案: Web UI 的静态用户默认是 dr.who,它对目标目录没有写权限。修复:

<!-- core-site.xml -->
<property>
    <name>hadoop.http.staticuser.user</name>
    <value>root</value>
</property>

或放宽目标目录权限:hdfs dfs -chmod -R 777 /input。生产环境应启用 Kerberos + Ranger 做真正的认证授权,而不是放开权限。

练习 12:集群扩容(源文档原题)

题目: 现有 192.168.224.10(Master)+ 192.168.224.11(Slave1)两节点集群,需要新增 192.168.224.12(Slave2)。写出两种做法:教学环境的简单做法(可停机、可重新格式化)和生产环境的热扩容做法(不停机、不丢数据)。

参考答案:

教学环境(源文档做法,可停机):

# 1. 新节点完成全部前置准备(主机名、hosts、防火墙、SSH 免密、JDK)
# 2. 所有节点的 hosts 都加入新节点
# 3. Master 的 slaves 文件加入新节点
echo 192.168.224.12 >> $HADOOP_HOME/etc/hadoop/slaves
# 4. 分发 Hadoop 目录到新节点
cd /usr/local && tar -zcvf ~/hadoop.master.tar.gz ./hadoop
scp ~/hadoop.master.tar.gz root@192.168.224.12:/root/
ssh root@192.168.224.12 "tar -zxvf /root/hadoop.master.tar.gz -C /usr/local/"
# 5. 新节点务必清空残留数据目录,否则 clusterID 冲突
ssh root@192.168.224.12 "rm -rf /usr/local/hadoop/dfs/data/* /usr/local/hadoop/tmp/*"
# 6. 重启集群(源文档做法:stop-all.sh → 重新格式化 → start-all.sh,会丢数据)
stop-all.sh
hdfs namenode -format
start-all.sh

生产环境(热扩容,不停机不丢数据):

# 1~4 步同上,但绝不执行 format
# 5. 只在新节点上单独启动进程
ssh slave2 "hadoop-daemon.sh start datanode"     # 2.x
ssh slave2 "yarn-daemon.sh   start nodemanager"
# 3.x 写法
ssh slave2 "hdfs --daemon start datanode"
ssh slave2 "yarn --daemon start nodemanager"
# 6. 若配置了白名单,需刷新
hdfs dfsadmin -refreshNodes
yarn rmadmin -refreshNodes
# 7. 验证
hdfs dfsadmin -report | grep -A3 'Name:'
yarn node -list
# 8. 数据均衡(新节点数据为 0,需要 balancer 把老节点数据迁一部分过来)
hdfs balancer -threshold 10

关键区别: 生产扩容绝不能格式化 NameNode,只需要新节点加入并启动 DataNode/NodeManager;slaves/workers 文件只影响批量启动脚本,不影响 DataNode 能否注册(真正的准入控制是 dfs.hosts 白名单)。

练习 13:节点退役(decommission)

题目: 需要把 slave1 从集群中安全下线,要求下线过程中不丢数据、不影响正在运行的作业。写出完整步骤,并说明为什么不能直接 kill 掉 DataNode 进程。

参考答案:

# 1. 配置黑名单
echo slave1 > $HADOOP_HOME/etc/hadoop/dfs.hosts.exclude
echo slave1 > $HADOOP_HOME/etc/hadoop/yarn.hosts.exclude
<!-- hdfs-site.xml -->
<property>
    <name>dfs.hosts.exclude</name>
    <value>/usr/local/hadoop/etc/hadoop/dfs.hosts.exclude</value>
</property>
<!-- yarn-site.xml -->
<property>
    <name>yarn.resourcemanager.nodes.exclude-path</name>
    <value>/usr/local/hadoop/etc/hadoop/yarn.hosts.exclude</value>
</property>
# 2. 刷新(无需重启集群)
hdfs dfsadmin -refreshNodes
yarn rmadmin -refreshNodes

# 3. 观察状态:Decommission In Progress → Decommissioned
hdfs dfsadmin -report | grep -B2 -A5 'Decommission'
# Web UI 上该节点进入 Decommissioning,块复制完成后变 Decommissioned

# 4. 状态变为 Decommissioned 后再停进程
ssh slave1 "hadoop-daemon.sh stop datanode"
ssh slave1 "yarn-daemon.sh stop nodemanager"

# 5. 从 slaves/workers 与白名单中移除该节点,再次刷新
sed -i '/slave1/d' $HADOOP_HOME/etc/hadoop/slaves

# 6. 收尾:均衡数据
hdfs balancer -threshold 10

为什么不能直接 kill: 直接杀掉 DataNode 后,NameNode 要等 2 × dfs.namenode.heartbeat.recheck-interval + 10 × dfs.heartbeat.interval(默认约 10 分 30 秒)才判定节点死亡,这段时间内该节点上的块副本数不足;如果同时下线多个节点,可能出现某些块的全部副本都在被杀节点上,导致数据永久丢失(Missing blocks)。走 decommission 流程时,NameNode 会先把该节点上的块复制到其他节点,确认副本数达标后才允许下线,全程数据安全。

练习 14:安全模式

题目: 集群重启后 HDFS 一直处于安全模式,无法写入。请说明安全模式的作用、自动退出条件、查看与手工控制命令,以及"一直退不出去"的排查方向。

参考答案:

作用: 安全模式是 NameNode 的只读保护状态。启动时 NameNode 先加载 fsimage + edits 恢复目录树,但内存中还没有块位置信息(BlockMap 不持久化),必须等 DataNode 上报块报告(block report)后才知道每个块在哪。在此期间禁止任何写操作(创建、删除、修改副本数),防止元数据与实际数据不一致。

自动退出条件(三个同时满足):

  1. 已上报块中,副本数 ≥ dfs.namenode.replication.min(默认 1)的块占比 ≥ dfs.namenode.safemode.threshold-pct(默认 0.999,即 99.9%);
  2. 存活 DataNode 数 ≥ dfs.namenode.safemode.min.datanodes(默认 0);
  3. 上述条件持续满足 dfs.namenode.safemode.extension(默认 30000 毫秒 = 30 秒)。

命令:

hdfs dfsadmin -safemode get        # 查看:Safe mode is ON / OFF
hdfs dfsadmin -safemode wait       # 阻塞等待自动退出(写脚本时用)
hdfs dfsadmin -safemode leave      # 强制离开(有风险)
hdfs dfsadmin -safemode enter      # 手工进入(做元数据维护、备份 fsimage 前用)
hdfs dfsadmin -safemode forceExit  # 3.x:强制退出

一直退不出去的排查方向:

场景 判断依据 处理
DataNode 没起来或起得少 hdfs dfsadmin -report 中 Live datanodes 数量不对 先修 DataNode(见 7.1 节)
真的有块丢失 hdfs fsck / | grep -i corrupt,NameNode 页面显示 Missing blocks hdfs fsck / -delete 删除损坏块(数据确实丢了),或找回下线节点的数据盘
磁盘满 df -h 清理空间后重启 DataNode
副本数配置大于节点数 dfs.replication=3 但只有 1 个 DataNode 调小 dfs.replication,或 hdfs dfs -setrep -R 1 /
块报告还在进行中 NameNode 日志显示 The reported blocks X needs additional Y blocks 且数字在增长 耐心等待,大集群可能几分钟到几十分钟
阈值设置不当 threshold-pct 被设成了 1.0 甚至更大 检查 hdfs-site.xml,恢复默认 0.999

强制 leave 的风险: 强制退出后,尚未上报的块会被认为丢失,若此时有写入或删除操作,可能造成元数据与真实数据永久不一致。只有在确认"块报告已完成、丢块确实无法恢复"时才使用。

练习 15:Eclipse 自定义 WordCount(源文档原题)

题目: 用 Eclipse 编写 com.testing.WordCount,导出 WordCount.jar,在集群上统计 /input/input.txt(内容 this is my computer this is my computer)的词频。写出关键代码要点、导出设置与运行命令,并说明为什么必须调用 job.setJarByClass()。

参考答案: 完整代码与步骤见第八章。关键点:

hadoop jar WordCount.jar /input/input.txt /output
hadoop fs -cat /output/part-r-00000
computer        2
is      2
my      2
this    2

为什么必须 job.setJarByClass(WordCount.class): 作业提交时,客户端需要把包含 Mapper/Reducer 类的 JAR 上传到 HDFS 的作业暂存目录,再由各节点的 Container 下载并加载。setJarByClass 让 Hadoop 通过该类的 ClassLoader 反查它所在的 JAR 文件路径,从而确定要分发哪个 JAR。若不调用(也不用 job.setJar("path") 指定),集群节点的 classpath 里没有你的类,就会报 ClassNotFoundException: com.testing.WordCount$WordCountMapper。

练习 16:设计题——集群容量与硬件规划(源文档原题)

题目: 某公司每周新增 1TB 数据,需要保存 2 年,HDFS 副本数为 3,另需预留 30% 的临时空间(MapReduce 中间结果、日志)。请估算需要多少存储容量,并给出 DataNode 与 NameNode 的硬件配置建议、机架规划建议。

参考答案:

容量估算:

  • 原始数据:1TB/周 × 52 周 × 2 年 = 104TB
  • 三副本:104TB × 3 = 312TB
  • 预留 30% 临时空间:312TB ÷ 0.7 ≈ 446TB
  • 若每台 DataNode 挂 4 块 2TB 盘(8TB/台,JBOD 不做 RAID):446 ÷ 8 ≈ 56 台
  • 考虑未来增长与故障冗余,规划 100 台左右(源文档给出的经验结论)

DataNode 硬件建议(源文档): 4 块磁盘(JBOD,不做 RAID,HDFS 自身有副本容错,RAID 反而会拖慢最慢盘的速度并浪费容量)、2 个 4 核 CPU(主频 2~2.5GHz)、16~24GB 内存、千兆或万兆以太网。

NameNode 硬件建议(源文档): 8~12 块磁盘(每块 1TB,元数据目录配多份实现冗余)、内存 16~72GB(按每百万个块约 1GB 内存估算,并预留一倍余量)、CPU 与 DataNode 相当。NameNode 是内存型服务,内存是硬约束。

机架规划(源文档): 每个机架放 30~40 台服务器,机架内用千兆交换机互联,机架之间用万兆上行汇聚。必须配置机架感知(net.topology.script.file.name),让 HDFS 把副本分散到不同机架,同时让 MapReduce 优先做 NODE_LOCAL / RACK_LOCAL 调度以减少跨机架流量。

练习 17:设计题——三种部署方式选型

题目: 分别为下列场景选择部署方式(手工部署 / Docker / Ambari 或 Cloudera Manager / 云上 EMR),并说明理由。

  1. 教学课堂,40 名学员,每人需要独立的 3 节点集群,课后即销毁;
  2. 某银行生产环境,60 台物理机,需要 HA、审计、Kerberos、监控告警;
  3. 个人在笔记本上学习 HDFS/YARN 的每个配置项与故障现象;
  4. 电商公司每月月底做一次大规模离线报表计算,其余时间几乎不用集群。

参考答案:

  1. Docker(docker-compose 一键起停,镜像统一、环境隔离、销毁干净、单机可承载)。也可以配合 Kickstart + 虚拟机模板批量克隆。
  2. Ambari / Cloudera Manager(HDP / CDH)。60 台机器手工维护配置分发不现实;企业发行版提供组件版本兼容性验证、滚动升级、集中配置管理、Kerberos 一键开启、指标采集与告警、审计(Ranger/Sentry)。
  3. 手工部署(本文的完全分布式流程)。只有手工改过每个 XML、踩过 clusterID 不一致和免密失效的坑,才真正理解各参数含义;用 Ambari 反而会把细节隐藏起来。
  4. 云上 EMR(如阿里云 EMR、AWS EMR)。按需创建、按量计费,月底跑完即释放集群,数据长期放在对象存储(OSS/S3)上,避免为了每月几天的算力常年养 60 台机器。源文档中的 Amazon EC2 + S3 方案就是这个思路的早期形态。

练习 18:综合排错演练

题目: 依次制造并修复下列故障,记录报错信息、定位过程与修复命令。

  1. 把从节点的 /etc/hosts 中 slave1 的 IP 改错,重启集群;
  2. 删除 master 上 ~/.ssh/authorized_keys,执行 start-dfs.sh;
  3. 注释掉 hadoop-env.sh 中的 JAVA_HOME,执行 start-dfs.sh;
  4. 在从节点上手工执行一次 hdfs namenode -format(模拟误操作),重启 DataNode;
  5. 用 dd 把 DataNode 数据盘写满到 95%,观察 NodeManager 状态;
  6. 连续两次运行同一条 wordcount 命令且不删除 output 目录。

参考答案(报错 → 定位 → 修复):

故障 关键报错 修复
1 hosts 错 DataNode 日志 java.net.UnknownHostException 或 Call From ... to master:9000 failed on connection exception;Web UI Live Nodes 少一个 统一所有节点 /etc/hosts 内容,见 7.5 节
2 免密失效 hadoop@master's password: 反复索要密码,或 Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password) 重新 ssh-copy-id,检查 ~/.ssh 700、authorized_keys 600、家目录权限,见 7.6 节
3 JAVA_HOME Error: JAVA_HOME is not set and could not be found. 在 hadoop-env.sh 中显式 export JAVA_HOME=/usr/local/jdk 并分发,见 7.7 节
4 误格式化 Incompatible clusterIDs in /usr/local/hadoop/dfs/data 见练习 8 与 7.1 节
5 磁盘满 yarn node -list -all 显示 UNHEALTHY,Diagnostics 为 local-dirs are bad;作业一直 ACCEPTED 清理磁盘或调大 yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage,见 7.10 节
6 输出目录已存在 org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory ... already exists hdfs dfs -rm -r /output 后重跑,见 7.14 节

排错方法论(三步法): ① 看进程(jps / ps -ef | grep java)确认谁没起来;② 看日志($HADOOP_HOME/logs/hadoop-<user>-<role>-<host>.log 的最后几十行,YARN 任务看 yarn logs -applicationId)拿到真实异常;③ 看连通性与配置(ping、telnet host port、netstat -tnlp、cat 配置文件并确认已分发到全部节点)。不要凭猜测改配置,任何修改都必须能对应到一条具体的报错。

最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏