Hadoop中的MapReduce学习 - Mapper和shuffle阶段

MapReduce主要分为三个阶段

  • mapper
  • shuffle
  • reduce

MAPPER阶段

以下是 Mapper 阶段的详细工作流程(以 Hadoop 2.x/3.x 的 YARN 架构为例):

1. 输入分片(Input Split)
  • 问题: 如何将超大文件(如 1TB 日志)分发给多个 Mapper 并行处理?

  • 解决方案:

    • InputFormat(如 TextInputFormat)将文件逻辑切分为 分片(Split) (如 128MB / 片)。
    • 关键点: 分片是逻辑划分(不实际切割文件),记录每个分片的文件偏移量和长度。
  • 协同机制:

    ResourceManager 根据分片数量动态分配 Mapper 任务到不同 NodeManager。

2. RecordReader 读取数据
  • 问题: 如何从分片中逐条读取记录?

  • 解决方案:

    • RecordReader(如 LineRecordReader)将分片解析为一条条 <Key, Value> (如 <行偏移量, 行内容>)。
    • 示例:
      输入 "192.168.1.1 - - [01/Jan/2023:00:00:01] GET /home" → 输出 <0, "192.168.1.1 ...">
  • 技术细节:

    通过 FileInputStream 按偏移量读取 HDFS 块,避免全文件加载。

3. Map 函数处理
  • 问题: 如何执行用户自定义逻辑?

  • 解决方案:

    开发者实现 Mapper.map() 方法,对每条记录生成 中间键值对
    示例(统计单词):

    scss 复制代码
    // 输入:<行偏移量, "hello world">
    map(LongWritable key, Text value, Context context) {
      String[] words = value.toString().split(" ");
      for (String word : words) {
        context.write(new Text(word), new IntWritable(1)); // 输出:<"hello", 1>, <"world", 1>
      }
    }
4. 环形缓冲区(Circular Buffer)
  • 问题: Map 输出如何暂存?直接写磁盘效率低。

  • 解决方案:

    • 内存中开辟 环形缓冲区(默认 100MB)。

    • Map 输出先写入缓冲区,包含:

      • Key/Value 数据
      • 分区信息(Partition,决定数据归属哪个 Reducer)
      • 元数据(Key/Value 的起始位置、长度)
  • 技术细节:

    使用序列化(如 Writable 接口)减少内存占用。

5. 溢写(Spill)
  • 问题: 缓冲区满了怎么办?

  • 解决方案:

    • 当缓冲区使用率 > 阈值(默认 80%)时,启动 后台溢写线程

    • 溢写流程:

      1. 分区排序: 按分区分区内按 Key 排序(快速排序)
      2. Combiner 优化(可选): 本地聚合(如合并相同 Key 的 Value)
      3. 写磁盘: 生成临时文件(spill1.out, spill2.out...
  • 关键点:

    溢写与 Map 处理 并行执行,避免阻塞。

6. 归并合并(Merge)
  • 问题: 多个溢写文件如何整合?

  • 解决方案:

    • Map 任务结束时,将所有溢写文件 归并排序 为一个大文件。

    • 归并策略:

      • 多路归并(默认一次合并 10 个文件)
      • 保持分区内 Key 有序(为 Reduce 阶段做准备)
  • 输出文件:

    最终生成一个 分区且排序 的文件(如 map_out_0.index + map_out_0.data)。


核心问题及解决策略

核心问题 解决方案 技术实现
数据倾斜 自定义分区器(Partitioner) 重写 getPartition() 分散热点 Key
内存溢出 环形缓冲区 + 溢写机制 阈值触发异步磁盘写入
中间数据过大 Combiner 本地聚合 在 Map 端预合并相同 Key 的数据
分片不均 自定义 InputFormat 重写 getSplits() 控制分片逻辑
容错机制 Task 重试机制 NodeManager 监控 Task,失败后重新调度

shuffle

核心目标是 高效、可靠地将 Map 的输出数据分组、排序并传输给对应的 Reduce 任务

一、Shuffle 阶段的核心目标(本质)

  1. 分组 (Grouping) :将 Map 任务输出的键值对,按照 Key 的哈希值 (或自定义分区器)分配到不同的 分区 (Partition) 中。每个 Partition 对应一个 Reduce 任务【分区数量由reduce数量决定,有8个reduce就有8个partition】。

  2. 排序 (Sorting) :确保发送给同一个 Reduce 任务的所有数据(即同一个 Partition 内),是 按键 (Key) 排序 的。

  3. 数据传输 (Transfer) :将每个 Map 任务【map任务数量由文件类型和】输出的多个 Partition 数据,高效、可靠地传输到各个运行 Reduce 任务的节点上。

    map任务数量由输入的数据类型和大小决定。例子 :如果你有1个1GB的文件存储在HDFS上,而HDFS的块大小设置为128MB,那么Hadoop会尝试创建 1024MB / 128MB = 8 个InputSplit,从而启动8个Map任务

  4. 合并 (Optional Merge) :在 Reduce 端拉取到来自多个 Map 任务的对应 Partition 数据后,进行合并排序,形成最终的、完全排序的输入流。


二、Shuffle 阶段的技术实现细节(分 Map 端和 Reduce 端)

(一) Map 端 Shuffle
一句话总结:
  1. 流入数据处理:数据进入mapper阶段的缓冲区之前,会先通过hash算出未来该数据要进入的partition (partitionId会随着元数据一起写入)。

  2. 内存缓冲区溢出:当内存缓冲区满了,会写到文件(溢出写文件的时候,缓冲区禁止数据进入)。

  3. 文件过多就合并,文件过大就压缩。最终合并出来2个文件,一个file.out一个file.index(index就是索引,避免从头查询。记录的是partitionId在out文件中的起始位置和offset)

这是 Shuffle 的起点,发生在每个 Map 任务内部。

  1. 内存缓冲区 (In-Memory Buffer)

    • 目的 :Map 输出键值对不是直接写磁盘或网络,而是先写入一个 环形内存缓冲区 (mapreduce.task.io.sort.mb 参数控制大小,默认 100MB)。

    • 优点:避免频繁的小 I/O 操作,极大提升效率。

    • 内部结构:缓冲区本质是一个字节数组。写入时:

      • 序列化 <Key, Value> 成字节。
      • 同时写入数据的 元数据 (Metadata):包括 Key 的起始位置、Value 的起始位置、分区号 (Partition ID)、Value 的长度。元数据占固定大小(通常 16 bytes)。
      • 数据和元数据在缓冲区中分开存放。
  2. 分区 (Partitioning)

    • 写入缓冲区时,会立即调用 分区器 (Partitioner) 。默认是 HashPartitionerpartition = (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks
    • 元数据中就包含了这个计算出的 Partition ID

    也就是这个键值对该去哪个分区,在map的shuffle阶段就已经确定好了。

  3. 溢出 (Spill)

    • 触发条件

      • 当内存缓冲区使用量达到一个阈值 (mapreduce.map.sort.spill.percent,默认 80% = 80MB 满时)。
      • 或者 Map 任务结束时(清空最后的数据)。
    • 过程

      • 锁定缓冲区:暂停新的写入。

      • 后台线程执行溢出

        • 排序 (Sort) :对要溢出的 元数据区 进行排序。排序依据是:先按 Partition ID,然后在同一个 Partition 内按 Key
        • 合并 (Combine - Optional) :如果用户配置了 Combiner,且溢出的次数 >= min.num.spills.for.combine (默认 3),则在排序后、写入磁盘前,对同一个 Partition 内、同一 Key 的 Value 执行 Combiner 进行本地聚合。
        • 写入磁盘 :根据排序后的元数据顺序,从缓冲区数据区读取对应的序列化字节块,写入到 Map 任务本地工作目录的一个 溢出文件 (Spill File) 中。一个溢出过程产生一个文件。
      • 解锁缓冲区:继续接收新的 Map 输出。

  4. 文件合并 (Merge)

    • 当 Map 任务结束前(所有数据处理完),或者达到合并条件时。

    • 过程

      • 将之前产生的 所有 Spill File内存缓冲区中剩余的数据(如果有)一起读取。
      • 使用 多路归并排序 (Multi-way Merge Sort) 算法,将这些文件合并成一个最终的、分区且分区内有序 的大文件 (file.out),通常还有一个对应的索引文件 (file.out.index)。
      • 索引文件的作用 :记录每个 Partition ID 在最终输出文件 file.out 中的 起始偏移量 (Start Offset)结束偏移量 (End Offset) 。这极大加速了 Reduce 端拉取对应分区的数据。
    • 最终输出 :这个 file.outfile.index 就是 Map 任务最终的输出结果,存储在 Map 任务所在节点的本地磁盘上。


(二) Reduce 端 Shuffle
一句话总结
  1. 拉数据:向AM询问Mapper地址,然后多线程从mapper拉文件(rpc或者特定协议),拉到的文件放在Reduce的内存缓冲区
  2. 内存缓冲区溢出处理: 排序合并->写到临时文件
  3. 合并所有临时文件和缓冲区数据成为一个排序好的大文件(如果太多太多合并不过来,需要分轮次处理)

Reduce 任务启动后,主动从各个 Map 任务节点拉取属于自己的数据。

  1. 复制阶段 (Copy Phase / Fetch Phase)

    • 获取位置信息 :Reduce 任务启动后,会向 ApplicationMaster (AM) 询问所有已完成的 Map 任务的主机位置 (NodeManager 地址) 及其输出文件信息(包含文件位置和索引文件位置)。
    • 拉取线程 (Fetcher Threads) :Reduce 任务启动多个 HTTP/Fetcher 线程 (mapreduce.reduce.shuffle.parallelcopies 控制线程数,默认 5)。每个线程负责从一个或多个 Map 节点拉取数据。
    • 拉取目标 :每个 Fetcher 线程会去对应的 Map 节点,读取其最终输出文件 (file.out) 中 属于本 Reduce 任务负责的 Partition 的数据块。它利用索引文件 (file.index) 快速定位到该 Partition 在 file.out 文件中的精确偏移范围。
    • 传输方式:数据通过 HTTP 协议传输(早期版本)。更现代的框架可能使用更高效的 RPC 或专用协议。
    • 内存缓冲区 (Shuffle Buffer) :拉取到的数据并非直接写入 Reduce 的输入文件,而是先放入 Reduce 任务所在节点的一个 内存缓冲区 (mapreduce.reduce.shuffle.input.buffer.percent 控制占堆内存比例,默认 70%)。
  2. 内存管理与溢出 (Memory Management & Spill)

    • 触发条件 :当 Reduce 端内存缓冲区的数据量达到一定阈值 (mapreduce.reduce.shuffle.merge.percent,默认 66%)。

    • 后台线程执行溢出

      • 将缓冲区中的数据 排序 (Sort)合并 (Merge) 后,写入 Reduce 任务本地磁盘的一个临时文件中。
    • 目的:避免内存溢出 (OOM),并开始进行数据的初步聚合 / 排序。

  3. 合并阶段 (Merge Phase)

    • 输入来源

      • 来自内存缓冲区的数据(当缓冲区没满,但所有 Map 任务都完成,或需要最后合并时)。
      • 之前内存溢写到磁盘的临时文件。
    • 过程 :同样使用 多路归并排序 (Multi-way Merge Sort) 算法。

    • 轮次 (Passes) :如果文件太多,可能进行多轮合并,每轮合并多个小文件成少量大文件。

    • 最终输出 :当所有 Map 输出的数据都拉取并合并完成后,会得到一个或多个 在 Reduce 本地磁盘上、按键完全排序 的文件。如果进行了多轮合并,最终通常是一个大文件。

    • 可选操作 :在合并过程中,如果配置了 Combiner,且合并轮次满足条件,也会执行 Combiner。

  4. 最终输入 (Reduce Input)

    • 合并完成后得到的最终排序好的文件,就是 Reduce 任务的输入
    • Reduce 任务此时可以开始执行用户定义的 reduce() 函数。reduce() 函数接收一个 Key 和这个 Key 对应的 已排序好的所有 Value 的迭代器 (Iterable<Value>) 。这个分组和排序的能力,正是前面的 Shuffle 阶段辛苦工作的结果。

三、关键优化技术 & 问题解决

  1. 内存缓冲区:核心优化点,减少昂贵的磁盘 I/O 次数。设置不当会导致频繁溢出或 OOM。

  2. 排序算法:高效的内存排序 (QuickSort) 和磁盘归并排序 (MergeSort) 是关键。Hadoop 采用优化过的排序器。

  3. 索引文件:避免 Reduce 拉取整个 Map 输出文件,只拉取所需的分区数据,节省网络带宽和磁盘 I/O。

  4. Combiner :在 Map 端和 Reduce 端 Shuffle 过程中 本地聚合 相同 Key 的 Value,大幅减少需要传输和处理的数据量。解决数据膨胀问题。

  5. 并行拉取 (Fetcher Threads) :多个线程同时从不同 Map 节点拉取数据,加速复制阶段。解决网络传输瓶颈。

  6. 压缩 (Compression)

    • Map 输出压缩 (mapreduce.map.output.compress) : 在 Map 端输出到磁盘或网络前压缩数据 (e.g., Snappy, LZ4, Gzip)。显著减少磁盘写入量和网络传输量。
    • Shuffle 拉取压缩:如果 Map 输出压缩了,Reduce 拉取后需要先解压。
  7. 自定义分区器 (Partitioner) :解决数据倾斜问题 (Data Skew),避免某些 Reduce 任务负载过重。例如,对 UserId 分区而不是默认的 Hash

  8. 直接内存传输 (Shuffle Plugin / Optimized Fetch) :较新版本 (如 Hadoop 3.1+) 支持通过 RPC 或共享内存等更高效方式传输数据,减少 HTTP 开销和序列化 / 反序列化成本。

  9. 错误处理

    • Map 任务失败:Reduce 会重新从其他成功的副本拉取数据。
    • Reduce 任务失败:需要重新启动该 Reduce 任务,重新拉取所有数据(因为数据在本地磁盘)。

总结

Shuffle 的本质是 大规模分布式排序分组传输。Hadoop MapReduce 的实现核心在于:

  1. 内存优先:用大缓冲区减少 I/O。

  2. 分区排序:在 Map 端按 Key 和 Partition 排序,为传输和 Reduce 输入做好准备。

  3. 索引定位:用索引文件精确定位目标数据块。

  4. 并行传输:多线程拉取加速。

  5. 多级归并:处理海量中间数据。

  6. 本地聚合 (Combiner) :大幅减少数据量。

理解这些细节对于 调优 MapReduce 作业性能 (如调整缓冲区大小、启用压缩、优化分区器、设计 Combiner) 和 排查 Shuffle 相关的问题 (如 OOM、数据倾斜、网络瓶颈) 至关重要。

相关推荐
IT研究室7 小时前
大数据毕业设计选题推荐-基于大数据的北京市医保药品数据分析系统-Spark-Hadoop-Bigdata
大数据·hadoop·spark·毕业设计·源码·数据可视化
一枚小小程序员哈11 小时前
大数据、hadoop、爬虫、spark项目开发设计之基于数据挖掘的交通流量分析研究
大数据·hadoop·爬虫
计算机编程小咖1 天前
《基于大数据的农产品交易数据分析与可视化系统》选题不当,毕业答辩可能直接挂科
java·大数据·hadoop·python·数据挖掘·数据分析·spark
小四的快乐生活2 天前
Hive 存储管理测试用例设计指南
hive·hadoop·测试用例
三劫散仙2 天前
mac m1上使用Kerberos访问远程linux hadoop集群的正确姿势
linux·hadoop·macos
源图客2 天前
Apache Ozone 2.0.0集群部署
hadoop·ozone
计算机毕设残哥2 天前
大数据毕业设计推荐:基于Hadoop+Spark的手机信息分析系统完整方案
大数据·hadoop·课程设计
越来越无动于衷3 天前
Spring Boot 整合 Spring MVC:自动配置与扩展实践
数据仓库·hive·hadoop
Lx3523 天前
Hadoop数据倾斜问题诊断与解决方案
大数据·hadoop