1. 为什么今天还要啃透Hadoop这套“老架构”?
很多人看到Hadoop第一反应是:“这不早过时了吗?现在都上云原生、Flink、Spark了。”我去年在某高校实验室带一个分布式日志分析项目时,也这么想。结果刚把Spark Streaming跑通,数据源突然从Kafka切回了一套老旧的离线日志归档系统——每天20TB原始文本,按天分卷,存放在本地NAS集群里,没有API,没有元数据服务,只有目录树和一堆.log.gz文件。团队里两个应届生花三天搭好Spark读取逻辑,一跑就OOM;换Flink做流式预处理?数据根本不是流,是“冻住的冰川”。
最后我们退回HDFS+MapReduce重写了核心清洗模块。不是因为怀旧,而是因为HDFS的块级定位能力、机架感知副本策略、NameNode+DataNode的强一致性语义,在面对这种“海量静态文件+弱计算资源+高容错要求”的场景时,比任何抽象层更高的引擎都更可控、更可预测。这不是技术倒退,是回归本质:当你的数据规模突破单机IO吞吐瓶颈,当你的任务失败成本远高于调度开销,Hadoop这套设计哲学——用简单、确定、可推演的机制解决分布式基础问题——反而成了最锋利的刀。
它不是被替代了,是被封装了。Spark的spark-defaults.conf里还默认配着fs.defaultFS=hdfs://namenode:9000;Flink on Yarn的ApplicationMaster启动前,第一步就是向ResourceManager申请Container;连Kubernetes上的Alluxio,底层缓存策略也大量借鉴HDFS的BlockReport机制。所以这篇不讲“Hadoop有多老”,只讲清楚三件事:HDFS怎么把一块128MB的文件块,稳稳钉在三台不同机架的磁盘上,并让你读写时几乎感觉不到网络存在;MapReduce怎么把“统计每个URL访问次数”这种业务逻辑,拆解成可并行、可重试、可追踪的确定性步骤;Yarn怎么从MapReduce的资源管家,进化成整个集群的通用调度中枢。这些不是历史文档里的名词,是今天排查FileNotFoundException、优化Shuffle spill、诊断Container killed by YARN时,你真正要翻的底层说明书。
2. HDFS:不是“分布式文件系统”,而是“分布式块存储系统”
很多人误以为HDFS是Linux文件系统的网络版,能直接ls /data/logs就看到所有文件。这是最大的认知偏差。HDFS的本质,是把文件切割成固定大小的块(Block),再把块作为独立单元进行存储、复制、定位。文件本身只是逻辑容器,块才是物理实体。这个设计决定了它的所有行为逻辑。
2.1 块(Block):一切调度与容错的原子单位
HDFS默认块大小是128MB(Hadoop 3.x),远大于传统文件系统(如ext4的4KB)。为什么这么大?不是为了炫技,而是为了解决寻道时间(Seek Time)与传输时间(Transfer Time)的比值问题。假设磁盘寻道平均10ms,顺序读取速度100MB/s,那么读取1MB数据:寻道10ms + 传输10ms = 20ms;读取128MB:寻道10ms + 传输1280ms ≈ 1290ms。前者寻道开销占50%,后者仅0.8%。HDFS的设计目标,是让一次磁盘IO尽可能多地传输数据,把昂贵的寻道成本摊薄到极致。
提示:块大小不是越大越好。如果块设为1GB,单个MapTask处理时间可能长达数小时,一旦失败,重跑成本极高。128MB是经验平衡点:既大幅降低寻道占比,又保证单Task执行时间在分钟级,便于Yarn调度和故障恢复。
块在HDFS中是不可变(Immutable)的。你无法对一个已写入的块追加内容或修改中间字节。所有写操作都是创建新块。这看似限制,实则是可靠性的基石——避免了并发修改导致的数据不一致,简化了副本同步逻辑。当你执行hdfs dfs -put local.txt /hdfs/path.txt,客户端做的第一件事,不是传文件,而是向NameNode申请一批块ID(如blk_1073741825,blk_1073741826),每个块对应一个128MB的存储单元。NameNode根据机架拓扑、DataNode负载、现有副本数,为每个块分配3个DataNode(默认副本数),返回给客户端一个“写入管道(Pipeline)”。
2.2 写入流程:一次写入,三次落盘,全程可追溯
HDFS写入不是“发给NameNode,它分发给DataNode”,而是一个客户端驱动的流水线写入(Pipeline Write)。以写入一个256MB文件为例(含2个块),完整流程如下:
客户端发起请求:调用
FileSystem.create(),指定路径/data/app.log。客户端向NameNode发送RPC请求:“我要创建这个文件,并获取第一个块的写入位置”。NameNode分配块与节点:NameNode检查路径权限、父目录是否存在、是否已有同名文件。确认后,生成第一个块ID
blk_1073741825,并根据机架感知(Rack Awareness)算法选择3个DataNode。典型策略是:1个副本在本机架的DataNode A,1个在同机架的DataNode B,第3个在不同机架的DataNode C。这样既保证同机架内低延迟传输,又确保单机架故障时数据不丢失。建立写入管道:NameNode将块ID和3个DataNode地址(A→B→C)返回给客户端。客户端与DataNode A建立TCP连接,A再与B建立连接,B再与C建立连接,形成一条A→B→C的管道。
数据流式写入与校验:客户端将数据按包(Packet)切分(默认64KB),每个Packet附带校验和(Checksum)。它把Packet发给A,A收到后:
- 立即写入本地磁盘(但不刷盘,先存Buffer)
- 同时将Packet转发给B
- B收到后同样操作,再转发给C
- C收到后写入本地磁盘,并向B发送ACK
- B收到C的ACK,向A发送ACK
- A收到B的ACK,向客户端发送ACK
ACK确认与块提交:客户端收到A的ACK,表示该Packet已在3个节点落盘(内存Buffer)。当一个块(128MB)的所有Packet都得到ACK,客户端向NameNode发送“块完成”报告。NameNode将该块的元数据(块ID、所属文件、3个DataNode地址)写入内存,并持久化到EditLog(事务日志)。此时,该块才被视为“已提交”,对读操作可见。
重复流程写入下一块:客户端继续为第二个块
blk_1073741826向NameNode申请位置,重复步骤2-5。
注意:整个过程,NameNode不参与数据传输,只负责元数据管理和节点调度。数据流完全由客户端驱动,在DataNode间管道式传递。这极大减轻了NameNode压力,也避免了中心化数据瓶颈。这也是为什么HDFS能支撑PB级数据——瓶颈在磁盘IO和网络带宽,不在NameNode CPU。
2.3 读取流程:就近原则下的多副本协同
读取比写入更轻量,但也充满智慧。当你执行hdfs dfs -cat /data/app.log,流程如下:
客户端解析路径:客户端向NameNode发送RPC:“请告诉我
/data/app.log这个文件,由哪些块组成,每个块在哪些DataNode上?”NameNode返回块位置列表:NameNode查询内存中的元数据,返回一个有序的块列表。对每个块,返回其3个副本所在的DataNode地址,并按网络距离(Network Distance)排序。排序规则是:本机(Client Host)> 同机架(Same Rack)> 不同机架(Different Rack)。例如,块
blk_1073741825的副本在dn1-rack1,dn2-rack1,dn3-rack2,而客户端恰好运行在dn1-rack1上,则返回顺序为[dn1-rack1, dn2-rack1, dn3-rack2]。客户端直连最优DataNode:客户端拿到第一个块的位置列表后,不经过NameNode,直接与列表中第一个DataNode(
dn1-rack1)建立连接,发起读取请求。DataNode从本地磁盘读取该块数据,通过TCP流式返回给客户端。自动故障转移:如果客户端连接
dn1-rack1失败(超时或拒绝),它会立即尝试列表中的第二个节点dn2-rack1,无需再次询问NameNode。NameNode只在第一次读取时提供位置,后续重试完全由客户端自主决策。并行读取多块:对于大文件,客户端会为每个块单独建立连接,并行读取。HDFS Client内部有连接池管理,避免频繁建连开销。
这个设计的关键在于去中心化读取。NameNode只做一次“指路”,之后所有数据流动都在客户端和DataNode之间发生。这使得读取性能几乎线性扩展——增加DataNode就能提升总吞吐,NameNode不会成为瓶颈。而“就近原则”则最小化了跨机架网络流量,降低了延迟和带宽消耗。
2.4 NameNode:集群的“大脑”与“心脏”,也是单点风险所在
NameNode是HDFS的绝对核心,但它并非传统意义上的“服务器”。它是一个纯内存元数据服务,其状态由两部分构成:
- 内存元数据(In-Memory Namespace):包含所有文件/目录的树状结构(INode)、每个文件对应的块列表(BlocksMap)、每个块的副本位置(LocatedBlocks)。这是所有读写操作的实时依据,必须常驻内存。
- 持久化日志(EditLog + FsImage):EditLog记录所有元数据变更(如
create file,delete block,add replica),是WAL(Write-Ahead Log);FsImage是某一时刻内存元数据的完整快照。NameNode启动时,先加载FsImage到内存,再重放EditLog中所有后续操作,恢复到最新状态。
踩坑经验:NameNode的JVM堆内存必须足够大。一个拥有1亿个文件的集群,INode对象可能占用20GB以上内存。如果堆内存不足,频繁GC会导致NameNode响应迟钝,客户端出现
Connection refused或Standby Exception。我们曾在一个测试集群因堆内存设为4G,导致NameNode每5分钟Full GC一次,整个集群读写延迟飙升至秒级。最终调至32G并启用G1GC,问题消失。
NameNode的单点问题,通过HA(High Availability)模式解决。HA模式下,有两个NameNode:Active(主)和Standby(备)。它们共享一个Quorum Journal Manager(QJM)集群(通常3或5个JournalNode)。Active NameNode将所有EditLog写入QJM,Standby NameNode持续从QJM读取并应用这些日志,保持内存状态与Active完全同步。当Active故障,ZooKeeper触发故障转移(Failover),Standby升级为Active,整个过程秒级完成,客户端无感知。这彻底消除了SPOF(Single Point of Failure)。
3. MapReduce:不是编程模型,而是分布式计算的“确定性契约”
MapReduce常被误解为一种“写两个函数(map/reduce)就能跑分布式”的简易框架。实际上,它是Hadoop为解决“如何在不可靠硬件上,执行任意用户代码并保证结果正确”这一难题,所定义的一套严格、可验证、可审计的执行契约。它的价值不在于语法简洁,而在于用一套固定的、可穷举的阶段,将混沌的分布式执行,约束在确定性的轨道上。
3.1 架构:四层角色,各司其职,边界清晰
MapReduce v1(经典MR)的架构非常清晰,分为四个核心角色:
Client(客户端):用户提交作业(Job)的入口。它负责:
- 将用户编写的Mapper/Reducer类、配置文件(
job.xml)、输入输出路径等打包成一个Jar。 - 向JobTracker申请一个Job ID。
- 将Jar、配置、切片信息(Split)上传到HDFS的
/user/history目录。 - 向JobTracker提交作业。
- 将用户编写的Mapper/Reducer类、配置文件(
JobTracker(作业跟踪器):集群的“总调度官”。它负责:
- 接收Client提交的作业,将其放入作业队列。
- 资源管理:监控所有TaskTracker的心跳,掌握集群可用Slot(CPU核+内存)。
- 任务调度:根据数据本地性(Data Locality)原则,将Map Task分配给存储了该InputSplit数据的DataNode上的TaskTracker(即“计算向数据移动”);将Reduce Task分配给资源充足的TaskTracker。
- 容错管理:监控TaskTracker心跳,若超时未收到,标记其上所有Task为失败;监控Task状态,若某个Task失败超过4次(默认),则整个Job失败。
TaskTracker(任务跟踪器):运行在每个DataNode上的代理进程。它负责:
- 定期向JobTracker发送心跳(包含本节点资源使用率、正在运行的Task列表)。
- 接收JobTracker下发的Map/Reduce Task指令。
- 在本地JVM中启动一个子进程(
Child JVM)来执行具体的Mapper或Reducer逻辑。 - 将Task的stdout/stderr、计数器(Counter)值、状态更新上报给JobTracker。
Task(任务):实际执行用户代码的单元。分为两类:
- Map Task:处理一个InputSplit(通常是HDFS的一个块或多个小文件合并成的逻辑切片),输出<key, value>对到本地磁盘的环形缓冲区(Spill Buffer)。
- Reduce Task:从所有Map Task的输出中,拉取(Shuffle)属于自己分区(Partition)的数据,进行合并(Sort & Merge),然后调用用户Reducer处理。
关键理解:MapReduce的“分布式”不是靠用户代码实现的,而是由JobTracker和TaskTracker这套基础设施强制注入的。用户只需关注
map()和reduce()函数的逻辑,框架保证了:1)输入被均匀切分;2)Map输出按Key哈希分区;3)Reduce能拉取到所有属于自己的数据;4)失败任务自动重试。这是一种“契约式编程”——你遵守接口,框架保证分布式语义。
3.2 执行原理:从InputSplit到OutputFile的七步炼金术
一个MapReduce Job的生命周期,是七个严格定义的阶段。理解每一步的输入、输出、触发条件和失败影响,是调试性能和错误的核心。
步骤1:InputSplit切分(客户端完成)
Client在提交Job前,调用InputFormat.getSplits()方法。对HDFS文件,默认是FileInputFormat,它会:
- 计算文件总大小(如1GB)。
- 根据
mapreduce.input.fileinputformat.split.minsize(默认1B)和maxsize(默认Long.MAX_VALUE),结合HDFS块大小(128MB),决定切分数。 - 对于1GB文件,理想切分是8个128MB的Split(
1024/128=8)。每个Split是一个逻辑描述,包含:文件路径、起始偏移量(start)、长度(length)、所在主机列表(用于数据本地性)。
注意:Split不是物理块!一个Split可以跨越多个HDFS块(如小文件合并),也可以一个HDFS块被切成多个Split(如果
minSize设得很大)。Split的目的是让Map Task有“合适大小”的工作单元,而非绑定HDFS块。
步骤2:Map Task执行(TaskTracker上)
每个Map Task启动一个Child JVM,执行以下流程:
- Read:
RecordReader(如LineRecordReader)从InputSplit指定的文件位置开始,逐行读取,解析成<key, value>(如<offset, line_content>)。 - Map:调用用户
Mapper.map(key, value, context)。Context将输出的<k, v>写入环形缓冲区(Spill Buffer,默认100MB)。 - Spill & Sort:当缓冲区使用率达80%(默认
spill.percent),启动溢出(Spill)线程:- 对缓冲区内所有
<k, v>按Key进行快速排序(QuickSort)。 - 按Partitioner(默认HashPartitioner)计算每个
<k, v>的分区号(0~R-1,R为Reduce Task数)。 - 将排序后的数据按分区写入本地磁盘的
spillN.out文件,并生成索引文件spillN.out.index(记录每个分区的起始偏移)。
- 对缓冲区内所有
- Merge:当Map Task结束,所有
spill*.out文件需要合并成一个file.out。合并过程是归并排序(MergeSort),同时进行Combiner(如果配置了)的局部聚合,减少网络传输量。
步骤3:Shuffle(Reduce端主动拉取)
这是MapReduce最耗时、最易出错的阶段,完全由Reduce Task驱动:
- Copy:Reduce Task启动
Fetcher线程,向所有Map Task所在节点的HttpServer(MapTask启动的轻量HTTP服务)发起HTTP GET请求,拉取属于自己分区的数据(如partition_0)。为防网络拥塞,Reduce Task默认同时拉取5个Map的输出(mapreduce.reduce.shuffle.parallelcopies)。 - Merge:拉取到的数据先存入内存缓冲区(
inMemoryMerger),当内存满或拉取完成,启动OnDiskMerger,将内存数据和磁盘上的spill*.out文件进行归并排序,生成最终的reduce_input文件。此文件按键(Key)全局有序。
踩坑经验:Shuffle阶段
OutOfMemoryError极其常见。根本原因不是Reduce内存不够,而是mapreduce.reduce.merge.inmem.threshold(内存合并阈值)和mapreduce.reduce.shuffle.input.buffer.percent(用于拉取的堆内存比例)设置不合理。我们曾将inmem.threshold设为1000(即1000个键就触发内存合并),导致大量小文件在内存中堆积,最终OOM。改为0(禁用内存合并,全部走磁盘)后,虽然慢一点,但稳定了。
步骤4:Reduce执行(TaskTracker上)
Reduce Task从reduce_input文件中,按Key分组(GroupingComparator),对每个Key调用一次Reducer.reduce(key, values, context)。values是一个迭代器,遍历所有属于该Key的Value。Reducer的输出,由OutputFormat(如TextOutputFormat)写入HDFS的/output/part-r-00000等文件。
步骤5-7:Commit、Cleanup与History
- Commit:所有Reduce Task成功后,JobTracker通知Client,Client调用
OutputCommitter.commitJob(),将临时输出目录(如/output/_temporary/0/)重命名为最终输出目录/output/。这是原子操作,保证作业要么全成功,要么全失败。 - Cleanup:无论成功失败,
OutputCommitter.cleanupJob()都会被调用,清理临时文件。 - History:Job的历史信息(配置、计数器、任务日志链接)被写入HDFS的
/history/目录,供mapred job -history命令查询。
这套七步流程,每一个环节都有明确的输入输出契约、失败重试策略和日志记录点。当你看到Task attempt failed to report status,就知道是TaskTracker与JobTracker心跳断了;看到Shuffle failed with too many fetch failures,就知道是网络或Map端服务挂了。这不是黑盒,而是一张可追踪、可审计的执行地图。
4. YARN:从MapReduce专属管家,到集群通用操作系统
YARN(Yet Another Resource Negotiator)的诞生,是Hadoop生态的一次范式革命。它回答了一个根本问题:为什么一个集群只能运行MapReduce一种计算框架?YARN将“资源管理”和“作业调度”彻底解耦,把Hadoop集群变成了一个通用的分布式操作系统,MapReduce、Spark、Flink、Presto等,都只是运行在其上的“应用程序”。
4.1 工作机制:双层调度,责任分离
YARN的核心思想是“两级调度”:第一级是集群资源的宏观分配,第二级是单个应用的微观任务调度。这打破了MRv1中JobTracker既是资源管家又是应用调度官的“大包大揽”模式。
ResourceManager(RM):集群的“总资源经理”。它只有一个(HA模式下有Active/Standby),职责纯粹:
- 全局资源视图:维护所有NodeManager(NM)的心跳,掌握集群总CPU、内存、磁盘、网络资源。
- 资源分配:接收来自ApplicationMaster(AM)的资源请求(如“我要2个Container,每个2核4GB内存”),根据调度策略(如CapacityScheduler或FairScheduler)进行分配,并通过
AllocateResponse返回给AM。 - 不关心应用逻辑:RM不知道你跑的是MapReduce还是Spark,它只认Container这个资源单元。
NodeManager(NM):运行在每个节点上的“资源代理”。它取代了MRv1的TaskTracker,职责聚焦:
- 本地资源监控:定期向RM汇报本节点的资源使用情况(CPU、内存、磁盘、网络)。
- Container生命周期管理:接收RM下发的
StartContainer指令,启动一个Container(本质是Linux cgroup隔离的进程组),监控其资源使用,若超限则Kill。 - 日志管理:收集Container的标准输出和错误日志,上传到HDFS。
ApplicationMaster(AM):每个应用的“专属管家”。它是一个由客户端提交的应用程序,运行在某个Container中。它的核心职责是:
- 应用级调度:与RM协商,为应用的各个任务(如Map Task、Spark Executor)申请、释放Container。
- 任务协调:与NM通信,启动、监控、重启具体任务进程。
- 容错:若自身失败,RM会重新启动一个新的AM实例,并恢复其状态(需应用支持CheckPoint)。
关键对比:在MRv1中,JobTracker既要管“集群有多少资源”,又要管“这个Job的第3个Map Task该在哪跑”。在YARN中,RM只管“集群还有多少空闲资源”,AM自己去抢、去调度。这就像酒店前台(RM)只告诉你“还有10个房间空着”,而每个旅行团(AM)自己决定带多少人、住哪几间、谁睡上铺。
4.2 Container:YARN的“进程沙箱”,统一资源计量单位
Container是YARN最核心的抽象,它是一个资源受限的进程执行环境。你可以把它理解为一个轻量级的、由cgroup控制的Linux进程组。每个Container被分配:
- CPU资源:通过
yarn.nodemanager.resource.cpu-vcores配置,以虚拟核(vcore)为单位。YARN本身不精确控制CPU时间片,而是依赖Linux cgroup的cpu.shares进行权重分配。 - 内存资源:通过
yarn.nodemanager.resource.memory-mb配置,以MB为单位。这是硬性限制,超出会被cgroup OOM Killer杀死。 - 其他资源:可扩展,如GPU、FPGA,通过
ResourceTypes配置。
当AM向RM申请资源时,它请求的是“N个Container,每个X vcore + Y MB内存”。RM批准后,将Container的规格(IP、端口、环境变量、启动命令)发给目标NM。NM收到后,执行docker run或cgroup exec,启动一个隔离进程。这个进程,就是你的Map Task、Spark Executor或Flink TaskManager。
实操心得:Container内存设置是性能关键。
mapreduce.map.memory.mb(Map Task内存)和mapreduce.reduce.memory.mb(Reduce Task内存)必须小于NM的yarn.nodemanager.resource.memory-mb,否则RM无法分配。我们曾将map.memory.mb设为8192MB,而NM只配置了6144MB,导致所有Map Task永远处于ACCEPTED状态,卡在资源等待队列。查看RM Web UI的Scheduler页面,一眼就能看到“Pending Resources”居高不下,根源在此。
4.3 Application生命周期:从提交到退出的完整闭环
一个YARN应用的启动,是一个标准的、可预测的流程:
Client提交:用户执行
yarn jar xxx.jar MyApp。Client将应用Jar、启动类、配置打包,向RM发送SubmitApplicationRequest。RM启动AM:RM为该应用分配第一个Container(称为
AppMaster Container),并将启动命令(如java -Xmx1g MyAppMaster)发给目标NM。NM启动AM进程。AM注册与资源申请:AM启动后,向RM注册自己,并开始循环调用
allocate()方法,申请更多Container来运行实际任务。例如,一个MapReduce AM会申请:- N个Map Container(数量=InputSplit数)
- M个Reduce Container(数量=
mapreduce.job.reduces配置值)
NM启动任务:RM批准Container后,将规格发给NM。NM启动任务进程(如
java -Xmx2g Mapper),任务进程开始执行用户逻辑。AM监控与容错:AM通过
NMClient与NM通信,监控任务状态。若任务失败,AM可申请新的Container重跑;若NM宕机,AM会收到NodeRemovedEvent,将该NM上所有任务标记为失败并重试。应用完成:当所有任务成功,AM向RM发送
FinishApplicationMasterRequest,声明应用结束。RM清理该应用的所有资源记录。AM进程自行退出。日志归档:NM将所有Container的日志,按应用ID归档到HDFS的
/tmp/logs/目录,供yarn logs -applicationId <id>命令查询。
这个流程的精妙之处在于完全解耦。RM的代码库不需要知道MapReduce的任何细节,它只处理Container;AM的代码库也不需要知道YARN的调度算法,它只和RM的RPC接口打交道。这正是YARN能支撑Spark、Flink等数十种框架的根本原因——它们各自实现一个符合YARN AM规范的“管家”,就能无缝接入。
5. 三者关系:HDFS、MapReduce、YARN如何拧成一股绳?
把HDFS、MapReduce、YARN看作三个独立模块是危险的。它们是一个精密咬合的齿轮组,共同构成了Hadoop分布式数据处理的基石。理解它们如何协同,才能真正驾驭这个系统。
5.1 数据流视角:从存储到计算的闭环
想象一个典型的WordCount作业:
- 起点:HDFS。输入文件
/input/words.txt(1GB)被HDFS按128MB切分成8个块,分布在集群10个DataNode上,每个块有3个副本。 - 触发:Client提交MR Job。Client读取HDFS的
/input/words.txt元数据,计算出8个InputSplit,并将Job提交给YARN。 - 资源调度:YARN介入。YARN RM为该Job分配一个AM Container。AM启动后,向RM申请8个Map Container。RM根据数据本地性(Data Locality)策略,优先将Map Container分配给存储了该InputSplit主副本的DataNode。例如,Split1的主副本在
dn1-rack1,那么RM会尽量把Map1 Container分配给dn1-rack1上的NM。 - 计算执行:Map Task读取本地数据。
dn1-rack1上的NM启动Map1 Container。该Container内的Mapper进程,通过HDFS Client API,直接从本地dn1-rack1的DataNode读取Split1的数据(零网络IO!)。处理完后,输出到本地磁盘的spill.out。 - Shuffle:跨节点数据搬运。Reduce Task(运行在
dn5-rack2)需要从dn1-rack1,dn2-rack1,dn3-rack2等所有Map节点拉取属于自己分区的数据。这时,HDFS的DataNode HTTP服务(http://dn1:50075)被用作数据传输通道,YARN的Container网络隔离保证了传输安全。 - 结果落盘:HDFS再登场。Reduce Task完成计算,将最终结果
<word, count>写入HDFS的/output/目录。HDFS Client再次介入,将结果块按规则写入DataNode。
整个链条中,HDFS提供可靠、本地化的数据存储;YARN提供灵活、隔离的资源调度;MapReduce(作为YARN上的一个Application)提供确定、可审计的计算逻辑。三者各守其位,又环环相扣。
5.2 容错视角:层层防御的可靠性体系
Hadoop的高可用,不是靠单一组件的强壮,而是靠三层冗余设计:
HDFS层容错:基于块的多副本(3份)和机架感知。单个DataNode宕机,数据仍可从其他副本读取;单个机架断电,仍有跨机架副本可用。NameNode HA通过QJM和ZooKeeper实现秒级故障转移。
YARN层容错:ResourceManager HA(Active/Standby)保证资源调度不中断;NodeManager的健康检查(心跳超时)自动剔除故障节点;ApplicationMaster的失败重启(由RM触发)保证应用不因AM崩溃而终止。
MapReduce层容错:这是最细粒度的容错。Map Task失败,AM会重新申请Container,在另一个节点重跑;Reduce Task失败,AM会重新拉取数据重算;甚至单个Map输出文件损坏,Shuffle阶段的校验和(Checksum)会检测到并触发重拉。MapReduce的“重试上限”(
mapreduce.map.maxattempts)和“失败容忍度”(mapreduce.reduce.failures.allowed)是可配置的,允许你在“严格正确性”和“快速失败”间权衡。
这三层容错,像三道防火墙。HDFS保证数据不死,YARN保证计算不瘫,MapReduce保证逻辑不错。任何一层的故障,都不会导致整个作业的雪崩式失败。
5.3 性能调优视角:参数背后的物理世界
所有Hadoop调优,最终都指向对物理资源的精准控制。以下是几个关键参数及其物理意义:
| 参数 | 默认值 | 物理含义 | 调优建议 |
|---|---|---|---|
dfs.blocksize | 128MB | 单次磁盘IO的数据量 | 大文件(TB级)可增至256MB;小文件(GB级)保持128MB;极小文件(MB级)考虑用SequenceFile合并 |
mapreduce.map.memory.mb | 1024MB | Map Task JVM堆内存上限 | 必须 ≤ NM的yarn.nodemanager.resource.memory-mb;若Map逻辑复杂(如JSON解析),可增至2048MB,但需相应减少并发Map数 |
mapreduce.task.io.sort.mb | 100MB | Map环形缓冲区大小 | 影响Spill频率;增大可减少Spill次数,但占用更多内存;建议设为map.memory.mb的30%-50% |
yarn.scheduler.minimum-allocation-mb | 1024MB | RM分配Container的最小内存单位 | 若NM有32GB内存,可设为2048MB,避免碎片化;但设得过大,小任务无法获得资源 |
mapreduce.reduce.shuffle.parallelcopies | 5 | Reduce并发拉取Map输出的线程数 | 网络带宽充足时,可增至10-15,加速Shuffle;但过多线程会增加NM CPU压力 |
最后一个实战技巧:永远用
yarn top和hdfs dfsadmin -report作为调优起点。yarn top显示实时的Container分布和资源使用,一眼看出哪个节点过载;hdfs dfsadmin -report显示DataNode的磁盘使用率、块数、健康状态,帮你判断是否需要均衡(hdfs balancer)或下线故障节点。工具永远比猜测更可靠。