一、回顾:我们手里有什么,目标是什么
上篇我们造出了 1000 万条卡口过车记录(约 400 多 MB 的 gateway_records2.txt),并发现单机一次性统计"每个城市的车流量"既费内存又费时间。这一篇,我们按照"分而治之"的思想,用 Python 依次实现 MapReduce 的三个阶段:Split(分片)→ Map(映射)→ Reduce(归约),完整跑通一次"迷你 MapReduce"。
二、第一阶段 Split:把大文件切成 1000 个小文件
"分"是第一步。Demo03 把 400 多 MB 的大文件按每 10000 行一片,切成 1000 个小文件,命名为 part-0、part-1......part-999,存放在 split 目录下:
将400M的文件切分成1000个小文件
page = 0
split_f = open(f"split\\part-{page}", mode="w", encoding="utf-8")
count = 0
line = f.readline() # 逐行读取,而不是一次性readlines
while line: # 还有数据就继续循环
count += 1
split_f.write(line) # 写入当前分片
line = f.readline() # 读取下一行
if count == 10000: # 当前分片写满1万行
print(f"已生成:part-{page}")
count = 0
page += 1
split_f.close() # 关闭当前分片
split_f = open(f"split\\part-{page}", mode="w", encoding="utf-8")
注意两个细节:一是这里用 readline() 逐行读取,而不是 readlines() 一次性读入内存------这是处理大文件的通用技巧,上篇单机版的 OOM 问题就源于此;二是"每写满 1 万行就换下一个文件"的写法,正是 Hadoop"按块切分"思想的简化版。
补充知识:在真实的 Hadoop 中,切分由 HDFS 完成------文件上传时就被物理切成块(默认 128MB 一块),每个块默认存 3 份副本分散在不同机器上(既防止数据丢失,又方便并行读取)。Map 任务不是按"行数"而是按"块"来分配数据的。
三、第二阶段 Map:并行处理每一个分片
数据切好后,把每个小文件交给一个"工人"去统计。Demo04 遍历 split 目录下的 1000 个小文件,对每个文件独立执行"按城市分组计数",并把局部结果写入 map 目录下的同名文件:
import os
base_dir = "split\\"
files = os.listdir(base_dir) # 1000个小文件
for file in files: # 每个文件独立处理,互不干扰
file_path = base_dir + file
with open(file_path, mode="r", encoding="utf-8") as f:
lines = line.strip() for line in f.readlines()
citys = line.split(",")\[2 for line in lines]
city_num = {} # 局部统计字典
for city in citys:
if city not in city_num:
city_numcity = 1
else:
city_numcity += 1
把本文件的局部结果写出去
with open(f"map\\{file}", mode="w", encoding="utf-8") as w_f:
for city, num in city_num.items():
w_f.write(f"{city},{num}\n")
这里有两个关键点值得细品:
-
每个小文件的统计逻辑与上篇单机版 Demo02 完全一样,只是数据量从 1000 万行缩小到了 1 万行------单机"吃不下"的大问题,被拆成了 1000 个"吃得下"的小问题;
-
文件之间的统计互不依赖------这正是并行计算的前提。在真实集群中,1000 个 Map 任务可以被调度到成百上千台机器上同时执行,总耗时不再等于"处理 400MB 的时间",而是约等于"处理 1 万行的时间"。单机要跑几分钟的活,并行起来几秒钟就能完成。
四、第三阶段 Reduce:汇总 1000 份局部结果
Map 阶段结束后,map 目录下出现了 1000 个中间结果文件,内容都是"城市,数量"的键值对,例如"武汉,1006"。最后一步"合":把所有局部统计汇总成最终结果。Demo05 读取 map 目录下全部文件,用同一个字典做全局累加,最终输出到 reduce/part-0:
import os
base_dir = "map\\"
files = os.listdir(base_dir)
city_num = {}
for file in files: # 遍历1000个中间结果文件
with open(file_path, mode="r", encoding="utf-8") as f:
lines = line.strip() for line in f.readlines() # 武汉,1006
for line in lines:
city = line.split(",")0
num = int(line.split(",")1)
if city not in city_num:
city_numcity = num
else:
city_numcity += num
保存最终结果
with open("reduce\\part-0", mode="w", encoding="utf-8") as w_f:
for city, num in city_num.items():
w_f.write(f"{city},{num}\n")
到这里,"分 → 算 → 合"三步全部完成。reduce/part-0 中的每一行,就是每个城市的最终车流总量。
五、串起来看:一次完整的迷你 MapReduce
把三个阶段连起来,完整流水线是这样的:
Split:400MB 大文件 → 1000 个小分片(每片 1 万行)
Map:1000 个任务并行统计 → 1000 个中间文件(城市,数量)
Reduce:汇总 1000 份中间结果 → 1 个最终文件(每个城市的车流总量)
补充一个进阶知识点:Demo04 里"每个文件先在内部用字典归约一次"的做法,其实就是真实 MapReduce 中 Combiner 的雏形。Combiner 在 Map 端先做一次局部合并(比如把 <天津,1><天津,1> 先合并成 <天津,2>),大幅减少需要跨机器网络传输的数据量,是 MapReduce 最重要的性能优化手段之一。真实的 Shuffle 阶段还会把中间结果按 key 分区、排序,再通过网络送到对应的 Reducer 机器上------这是 MapReduce 中最复杂、也最容易成为性能瓶颈的一环。
六、真实的 Hadoop 长什么样
上面的 Python 模拟是为了理解思想。真实的 Hadoop 是一套完整的分布式计算框架,由三大核心组件构成:
HDFS(分布式文件系统):解决"数据怎么存"。大文件被切成块(默认 128MB),每个块存 3 份副本,分散在集群不同机器的磁盘上。NameNode 记录元数据(文件由哪些块组成、块在哪些机器上),DataNode 实际存放数据。数据天生就是"分散"的,这正是 Hadoop 能处理 PB 级数据的底气。
YARN(资源调度器):解决"任务怎么排"。Map/Reduce 任务运行前,先向 YARN 申请 CPU 和内存,YARN 在集群范围内统一调度分配,让上千个任务有条不紊地并行执行。
MapReduce(计算框架):解决"怎么算"。开发者只需用 Java 写 Mapper 和 Reducer 两个类,框架自动完成分片、调度、Shuffle、排序、容错(某个任务挂了自动换机器重跑)等所有脏活累活。
经典的 WordCount 程序就是本文 Demo 的"官方版本":Mapper 把每一行拆成单词,输出 <单词,1>;Reducer 把同一个单词的所有 1 相加,输出 <单词,总数>。与我们"按城市统计"的差别仅在于:真实的 key/value 会在集群机器之间通过网络流动,而我们用本地文件模拟了同样的数据流。