Hadoop MapReduce
MapReduce的运行机制到底是怎么样的?一起了解一下吧
一、MapReduce的整体架构
Client(客户端)
↓(提交Job)
ResourceManager(资源管理器)
↓(分配容器)
NodeManager(节点管理器)→ 启动AppMaster(作业管家)
↓(管理任务执行)
MapTask(多个) → Shuffle → ReduceTask(多个)
↓
Output(输出到HDFS)
阶段1:作业提交与初始化
1.1 客户端准备
// 客户端配置作业参数
Job job = Job.getInstance(conf, "wordcount");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setReducerClass(IntSumReducer.class);
// 提交作业到ResourceManager
job.submit(); // 或 job.waitForCompletion(true)
客户端执行步骤
- 向RM请求新的作业ID(job_000001)
- 检查输出路径是否存在(避免覆盖)
- 计算输入分片(InputSplit)信息
- 将作业资源(JAR、配置、分片信息)上传到HDFS
1.2 ResourceManager分配AppMaster
// RM收到请求后的处理逻辑
class ResourceManager {
public void submitJob(JobSubmitRequest request) {
// 1. 选择空闲NodeManager
NodeManager targetNM = selectAvailableNode();
// 2. 分配容器启动AppMaster
ContainerLaunchContext ctx = createAMContainer(request);
targetNM.startContainer(ctx); // 启动作业管家
}
}
阶段2:AppMaster调度执行
2.1 AppMaster初始化
AppMaster启动后立即执行:
1. 解析分片信息:根据输入数据创建Map任务列表
2. 资源计算:确定需要多少Map/Reduce容器
3. 任务调度优化
class ApplicationMaster {
public void run() {
// 计算需要多少个Map任务
int numMaps = calculateNumMaps(inputSplits);
// 向RM申请Map任务容器
for (int i = 0; i < numMaps; i++) {
allocateMapContainer(i);
}
}
}
2.2 Map任务调度策略
数据本地优先级:
- NODE_LOCAL(节点本地):数据与计算在同一节点
- RACK_LOCAL(机架本地):同一机架不同节点
- OFF_SWITCH(跨机架):不同机架,网络运输
阶段3:Map任务调度执行
3.1 Map任务调度生命周期
容器启动 → 任务初始化 → Map执行 → 分区排序 → 溢出写入 → 合并清理
3.2 核心处理逻辑
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
// 每个Map任务处理一个InputSplit
protected void map(LongWritable key, Text value, Context context) {
String line = value.toString();
String[] words = line.split(" ");
for (String word : words) {
context.write(new Text(word), new IntWritable(1));
// 输出示例:("hello", 1), ("world", 1)
}
}
}
3.3 Map端的Shuffle准备
关键数据结构:
// 内存中的环形缓冲区
class MapOutputBuffer {
private byte[] kvbuffer; // KV对存储区
private int[] kvindices; // 索引数组
private int bufindex = 0; // 当前写入位置
private int bufmark = 0; // 缓冲区标记
// 当缓冲区达到阈值(默认80%)时触发溢出
public synchronized void collect(K key, V value) {
// 1. 序列化KV对到缓冲区
// 2. 维护分区索引
// 3. 检查是否需要溢出到磁盘
}
}
阶段4:Shuffle机制深度解析
4.1 Map端Shuffle流程
解释:Map输出时候的海量数据不是一次性怼到内存中,会先放入一个环形内存缓存区,默认是100MB,并设定了一个阈值(内存的80%),同时Map还会启动一个守护线程,如果缓存区达到了阈值的80%,守护线程会把内存上的内容写到磁盘里,这个过程叫做spill,另外的20%的内存支持继续写入要写进磁盘的数据,写入磁盘和写入内存是互不干扰的,如果缓存区撑满了,Map会阻塞写入内存的操作,让写入磁盘的操作完成后,再继续执行写入内存的操作。
写入磁盘前会有个排序的操作,这个操作在内容溢出到磁盘的时候,不是写入内存的时候。
内存收集 → 分区排序 → 溢出文件 → 磁盘合并
详细步骤:
1. 环形缓冲区收集:
- 每个KV对先写入内存缓冲区
- 同时写入元数据(分区号、key起始位置等)
2. 溢出到(Spill)磁盘
class MapTask {
public void spillAndMerge() {
// 当缓冲区达到80%时启动溢出线程
if (bufindex >= softLimit) {
startSpillThread();
}
// 溢出过程:
// 1. 对缓冲区内的数据按分区、key排序
// 2. 每个分区生成一个溢写文件
// 3. 合并多个小文件为大文件
}
}
3. Combiner优化操作(可选)
// 在Map端先进行本地聚合,减少网络传输
job.setCombinerClass(IntSumReducer.class);
// 比如:("hello", [1,1,1]) → ("hello", 3)
4.2 Reduce端的Shuffle流程
Reduce任务的三阶段:
Copy Phase → Sort Phase → Reduce Phase
Copy阶段详解:
class ReduceTask {
public void copyMapOutputs() {
// 1. 从所有完成的Map任务拉取数据
for (MapTask mapTask : completedMaps) {
// 通过HTTP从Map节点拉取对应分区的数据
Fetcher fetcher = new Fetcher(mapTask.getOutputLocations());
fetcher.fetchPartition(partitionId);
}
// 2. 数据先存入内存,再溢出到磁盘
// 3. 所有数据拉取完成后进入排序阶段
}
}
这里有个小插曲,Map的shuffle流程和Reduce的shuffle流程到底区别在哪?
Map端Shuffle:
Map输出 → 内存缓冲区 → 分区排序 → 溢写磁盘 → 磁盘合并
(本地操作) (本地操作) (本地IO) (本地IO)
Reduce端Shuffle:
网络拉取 → 内存缓冲 → 溢写磁盘 → 归并排序 → Reduce输入
(网络IO) (内存管理) (本地IO) (CPU密集) (计算)
阶段5:Reduce任务执行
5.1 归并排序(Merge Sort)
磁盘文件1 + 磁盘文件2 + ... + 内存数据 → 排序后统一输入
二次排序机制:
// Reduce收到的输入已经是按key分组排序的
// 输入格式:("apple", [1,1,1]), ("banana", [1,1]), ("cherry", [1])
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
protected void reduce(Text key, Iterable<IntWritable> values, Context context) {
int sum = 0;
for (IntWritable val : values) {
sum += val.get(); // 对同一key的所有value求和
}
context.write(key, new IntWritable(sum));
// 输出:("apple", 3), ("banana", 2), ("cherry", 1)
}
}
阶段6:输出与清理
6.1 结果写入
// Reduce结果写入HDFS
class ReduceTask {
private void writeOutput(RecordWriter output,
Iterator<KeyValuePair> iter) {
while (iter.hasNext()) {
KeyValuePair pair = iter.next();
output.write(pair.key, pair.value);
}
output.close(context);
}
}
输出目录结构
/output/wordcount/
├── part-r-00000 # Reduce任务0的输出
├── part-r-00001 # Reduce任务1的输出
├── _SUCCESS # 成功标记文件
└── _logs/ # 作业日志
二、MapReduce通俗解释
🍕 披萨店比喻
想象你开了一家披萨店,要统计一天中哪种披萨最受欢迎。
传统方式(没有MapReduce):
-
你一个人翻看所有订单小票
-
一张一张数:玛格丽特×1,海鲜×1,玛格丽特×1...
-
数到晚上才完成,累死了
MapReduce方式:
1. Map阶段(分工)
你把所有订单小票分给5个员工:
-
员工A:统计1-100号订单
-
员工B:统计101-200号订单
-
员工C:统计201-300号订单
-
...
每个员工并行工作,快速统计自己那堆小票:
员工A的统计结果:
玛格丽特: 15份
海鲜: 8份
培根: 12份
员工B的统计结果:
玛格丽特: 18份
海鲜: 10份
蔬菜: 5份
2. Shuffle阶段(整理归类)
现在把相同类型的披萨统计结果放在一起:
- 把所有"玛格丽特"的统计放一堆
- 把所有"海鲜"的统计放一堆
- 把所有"培根"的统计放一堆
3. Reduce阶段(汇总)
让专门的员工进行最终汇总:
- 员工X专门汇总"玛格丽特":15 + 18 = 33份
- 员工Y专门汇总"海鲜":8 + 10 = 18份
- 员工Z"专门汇总"培根":12份
💻 技术对应关系
| 生活比喻 | MapReduce技术术语 |
|---|---|
| 订单小票 | 输入数据块 |
| 5个统计员工 | Map任务 |
| 按披萨类型分类 | Shuffle排序 |
| 3个汇总员工 | Reduce任务 |
| 最终销量统计 | 输出结果 |
🔄 完整流程
原始数据 → 拆分数据块 → Map处理 → 排序分组 → Reduce汇总 → 最终结果
↓ ↓ ↓ ↓ ↓ ↓
所有订单 → 分给5个员工 各自统计 按披萨分类 专人汇总 销量报表
⚡ 为什么快?
- 并行处理:5个员工同时统计,比一个人快5倍
- 专业化:每个员工只做自己擅长的那部分
- 可扩展:如果订单更多,就雇更多员工
📊 实际大数据例子
统计网站访问日志中的热门页面:
• Map:每个服务器统计自己日志中的页面访问次数
• Shuffle:把相同页面的统计结果分组
• Reduce:汇总每个页面的总访问量
这就是为什么MapReduce能快速处理TB级数据——"分而治之,并行处理"!
浙公网安备 33010602011771号