news 2026/4/29 4:26:35

大数据系列(三) MapReduce:简单粗暴的分而治之

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大数据系列(三) MapReduce:简单粗暴的分而治之

MapReduce:简单粗暴的"分而治之"

大数据系列第 3 篇:Google 的"分治"思想如何搞定海量数据计算?以及为什么它后来不香了。


从一个生活场景说起

假设你们公司要统计一本 1000 万字的小说里,每个字出现了多少次。你一个人干的话,得从头到尾看一遍,边看边计数,看完估计眼睛都花了。

但如果把这本书拆成 100 份,找 100 个同事,每人看 10 万字,各自统计自己那份里的字数,最后把 100 个人的结果汇总一下——这不就快了 100 倍吗?

这就是 MapReduce 的核心思想:把大任务拆成小任务,分发到多台机器上并行处理,最后把结果汇总。

Google 在 2004 年发表了 MapReduce 的论文,把这个思想形式化了。后来 Apache Hadoop 实现了开源版本,MapReduce 就成了大数据批处理的"开山鼻祖"。


MapReduce 的编程模型:就两个函数

MapReduce 的编程接口非常简单,就两个函数:

Map 函数:拆分+映射

Map: (key, value) → list of (new_key, new_value)

输入是一组键值对,你对每个键值对做处理,输出一组新的键值对。

Reduce 函数:聚合+汇总

Reduce: (key, list of values) → list of (key, aggregated_value)

输入是一个键和它的所有值列表,你对这些值做聚合(比如求和、计数、求最大),输出最终结果。

就这么简单。所有的分布式细节——任务调度、故障恢复、数据传输——框架帮你搞定。


WordCount:MapReduce 的"Hello World"

咱们用经典的 WordCount 案例来走一遍流程。需求很简单:统计一段文本里每个单词出现的次数。

输入数据

假设 HDFS 上有一个文件input.txt,内容就一行:

hello world hello hadoop hello spark

Map 阶段

Map 函数读取文件的每一行,把每个单词拆出来,输出(单词, 1)

// 伪代码:Map 函数voidmap(Stringkey,Stringvalue,Contextcontext){// key = 行号(这里用不到)// value = 行内容,比如 "hello world hello hadoop hello spark"String[]words=value.split(" ");for(Stringword:words){// 输出: ("hello", 1), ("world", 1), ("hello", 1) ...context.write(word,1);}}

Map 输出:

("hello", 1) ("world", 1) ("hello", 1) ("hadoop", 1) ("hello", 1) ("spark", 1)

Shuffle 阶段

这是 MapReduce 最复杂、最耗时的阶段。框架会自动做以下几件事:

  1. 分区(Partition):决定每个(单词, 1)发给哪个 Reduce 任务。默认按hash(单词) % Reduce任务数来算。
  2. 排序(Sort):每个分区内,按单词排序。这样相同单词会排在一起。
  3. 合并(Combine,可选):在 Map 端本地先做一次预聚合,减少网络传输量。
  4. 传输:把排序好的数据通过网络发给对应的 Reduce 任务。
┌─────────────────────────────────────────────────────────────────┐ │ Shuffle 阶段示意 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Map 输出(未排序): │ │ ("hello",1), ("world",1), ("hello",1), ("hadoop",1), │ │ ("hello",1), ("spark",1) │ │ │ │ ↓ 分区 + 排序 │ │ │ │ 分区 0(Reduce 0 处理): │ │ ("hadoop",1), ("hello",1), ("hello",1), ("hello",1) │ │ │ │ 分区 1(Reduce 1 处理): │ │ ("spark",1), ("world",1) │ │ │ │ ↓ 网络传输 │ │ │ │ Reduce 0 收到:("hadoop",[1]), ("hello",[1,1,1]) │ │ Reduce 1 收到:("spark",[1]), ("world",[1]) │ │ │ │ 注意:相同 Key 的所有 Value 被聚合成一个列表 │ │ │ └─────────────────────────────────────────────────────────────────┘

Reduce 阶段

Reduce 函数对每个单词的计数列表求和:

// 伪代码:Reduce 函数voidreduce(Stringkey,Iterable<Integer>values,Contextcontext){// key = "hello"// values = [1, 1, 1]intsum=0;for(intval:values){sum+=val;}// 输出: ("hello", 3)context.write(key,sum);}

Reduce 输出:

("hadoop", 1) ("hello", 3) ("spark", 1) ("world", 1)

完整流程图

┌─────────────────────────────────────────────────────────────────┐ │ WordCount 完整执行流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ HDFS 输入文件 │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ InputFormat:把文件切成多个 InputSplit │ │ │ │ (每个 Split 由一个 Map Task 处理) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Map Task 1 │ │ Map Task 2 │ │ Map Task 3 │ │ │ │ (处理 │ │ (处理 │ │ (处理 │ │ │ │ Split 1) │ │ Split 2) │ │ Split 3) │ │ │ │ │ │ │ │ │ │ │ │ 拆分单词 │ │ 拆分单词 │ │ 拆分单词 │ │ │ │ 输出(word,1)│ │ 输出(word,1)│ │ 输出(word,1)│ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ └────────────────┼────────────────┘ │ │ │ │ │ ┌───────────┴───────────┐ │ │ │ Shuffle 阶段 │ │ │ │ 分区 → 排序 → 合并 → 传输 │ │ │ └───────────┬───────────┘ │ │ │ │ │ ┌────────────────┼────────────────┐ │ │ │ │ │ │ │ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ │ │ Reduce Task │ │ Reduce Task │ │ Reduce Task │ │ │ │ 1 │ │ 2 │ │ 3 │ │ │ │ │ │ │ │ │ │ │ │ 聚合计数 │ │ 聚合计数 │ │ 聚合计数 │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ └────────────────┼────────────────┘ │ │ │ │ │ ┌───────────┴───────────┐ │ │ │ 输出到 HDFS │ │ │ │ part-r-00000 │ │ │ │ part-r-00001 │ │ │ │ part-r-00002 │ │ │ └───────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘

Combiner:在 Map 端先"偷摸"算一下

Shuffle 阶段要通过网络传输大量数据,这是个瓶颈。如果能在 Map 端先做一次本地聚合,就能大大减少传输量。

这就是Combiner的作用:

┌─────────────────────────────────────────────────────────────────┐ │ Combiner 的作用 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 不用 Combiner: │ │ Map 输出: ("hello",1), ("hello",1), ("hello",1), ... × 1000 │ │ ──────────────────────────────────────────────→ 网络传输 1000 条 │ │ │ │ 用 Combiner: │ │ Map 输出: ("hello",1), ("hello",1), ("hello",1), ... × 1000 │ │ ↓ Combiner 本地聚合 │ │ Map 输出: ("hello", 1000) │ │ ──────────────────────────────────────────────→ 网络传输 1 条 │ │ │ │ 效果:网络传输量从 1000 条降到 1 条 │ │ │ └─────────────────────────────────────────────────────────────────┘

Combiner 的逻辑和 Reduce 一样,但只在单个 Map 任务的输出范围内执行。注意:Combiner 不是万能的,它要求聚合操作满足交换律和结合律

  • 求和、计数、求最大值:可以用 Combiner ✓
  • 求平均值:不能用✗(因为需要知道总数和总和两个值)

MapReduce 的性能瓶颈在哪?

MapReduce 的设计很优雅,但性能上有几个硬伤:

1. 中间结果必须写磁盘

Map 任务的输出要写到本地磁盘,Reduce 任务再从磁盘读取。这个磁盘 I/O 是绕不开的。

Map 输出 → 本地磁盘 → Reduce 读取 → 网络传输 → Reduce 处理 → HDFS ↑______________↑ 磁盘 I/O 瓶颈

你想想,如果数据量是 TB 级,中间结果也是 TB 级,读写磁盘的时间就很可观了。

2. 迭代计算要命

机器学习算法(比如 K-Means、PageRank)需要多轮迭代。每轮迭代都要重新走一遍完整的 Map-Shuffle-Reduce 流程,重新读写 HDFS。

迭代 1: HDFS → Map → Shuffle → Reduce → HDFS 迭代 2: HDFS → Map → Shuffle → Reduce → HDFS 迭代 3: HDFS → Map → Shuffle → Reduce → HDFS ... 每轮都要重新读 HDFS、重新 Shuffle、重新写 HDFS

这就好比你每次做数学题都要重新抄一遍题目,效率能高才怪。

3. 编程模型太受限

Map 和 Reduce 两个函数,能表达的逻辑有限。复杂一点的计算(比如图计算、交互式查询),你得写多轮 MapReduce 作业拼接起来,代码又臭又长。


MapReduce vs 传统数据库

维度MapReduce关系型数据库
编程方式写 Java 代码(Map/Reduce 函数)写 SQL
数据移动计算向数据移动(Code moves to Data)数据向计算移动
扩展方式加机器(Scale-Out)买更好的机器(Scale-Up)
延迟分钟/小时级毫秒/秒级
适用数据非结构化/半结构化结构化表格
容错任务失败重算事务回滚

MapReduce 最大的创新是"计算向数据移动"。传统数据库是你把数据读到计算节点上处理,MapReduce 是把计算代码发到存数据的节点上执行,避免了海量数据在网络里传输。


MapReduce 为什么后来不香了?

2014 年之后,MapReduce 逐渐式微,主要有两个原因:

原因一:Spark 来了

Spark 的核心改进就一点:把中间结果缓存在内存里。

MapReduce: 每轮迭代都读 HDFS → 写 HDFS Spark: 第一轮读 HDFS,后续从内存直接读 性能差距:迭代计算场景下,Spark 比 MapReduce 快 10-100 倍

原因二:编程体验太差

写 MapReduce 作业要写一堆 Java 代码,配置一堆参数,调试起来痛不欲生。而 Spark 支持 Scala、Python、R,还有交互式 Shell,开发效率高了不止一个档次。

引擎推出时间核心改进
MapReduce2006分布式批处理开山鼻祖
Spark2014内存计算,迭代性能提升 10-100 倍
Flink2015原生流处理,毫秒级延迟

但 MapReduce 的历史地位不容否认。它证明了:通过适当的抽象,普通开发者也能写分布式并行程序。Spark 的 DAG 调度、Flink 的分布式快照,思想源头都可以追溯到 MapReduce。


小结

今天咱们聊了 MapReduce:

  1. 核心思想:分而治之,大任务拆成小任务并行处理
  2. 编程模型:Map 函数拆分映射,Reduce 函数聚合汇总
  3. Shuffle:最复杂最耗时的阶段,分区+排序+传输
  4. Combiner:Map 端本地预聚合,减少网络传输
  5. 性能瓶颈:中间结果落盘、迭代计算低效、编程模型受限
  6. 历史地位:大数据批处理的奠基者,但已被 Spark 取代

MapReduce 就像大数据世界的"C 语言"——它奠定了基础,但现在已经不是写业务的首选了。理解它的设计原理,是为了更好地理解 Spark、Flink 这些"后来者"为什么能做得更好。

你写过 MapReduce 作业吗?有没有被 Shuffle 阶段的性能问题折磨过?欢迎聊聊~


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/29 4:25:27

洛雪音乐app下载 落雪音乐安卓/电脑版下载

下载洛雪音乐 洛雪音乐安装包下载地址&#xff1a;洛雪音乐app下载 安装洛雪音乐 1) 下载安装包&#xff0c;双击 lx-music-desktop-v2.11.0.exe 启动安装&#xff1a; 2) 点击 “我同意”&#xff1a; 3) 点击 “下一步”&#xff1a; 4) 系统默认安装到 C 盘&#xff0c;这…

作者头像 李华
网站建设 2026/4/29 4:25:23

OTDR光纤测试技术原理与工程实践指南

1. OTDR技术原理与光纤测试基础光时域反射仪(OTDR)作为光纤测试领域的"听诊器"&#xff0c;其工作原理与医学超声检测有异曲同工之妙。当一束特定波长的光脉冲注入光纤时&#xff0c;仪器会持续监测反向散射的瑞利散射光和菲涅尔反射光信号。通过计算发射与接收信号的…

作者头像 李华
网站建设 2026/4/29 4:22:15

UltimateAndroid图片加载优化:从入门到精通

UltimateAndroid图片加载优化&#xff1a;从入门到精通 【免费下载链接】UltimateAndroid UltimateAndroid is a rapid development framework for developing your apps 项目地址: https://gitcode.com/gh_mirrors/ul/UltimateAndroid UltimateAndroid作为一款快速开发…

作者头像 李华
网站建设 2026/4/29 4:19:15

5分钟掌握AMD SMU调试工具:从零开始玩转Ryzen处理器深度调优

5分钟掌握AMD SMU调试工具&#xff1a;从零开始玩转Ryzen处理器深度调优 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: http…

作者头像 李华
网站建设 2026/4/29 4:18:54

16:买房子

""" 【题目名称】买房子 【题目来源】http://noi.openjudge.cn/ch0105/16/Author: 潘磊 <just_panleijust.edu.cn> Version: 1.0 """# 自定义函数 def year_for_buying_house(salary: int, price: int, rate: int, year: int) -> int:&…

作者头像 李华