MapReduce讲解
为什么要MapReduce
现在的应用程序大多数需要分析海量数据,并及时对用户进行反馈。像搜索引擎,推荐系统等,都需要处理相当大的用户数据和材料。但是,一台计算机的处理能力是存在上限的,可能对于海量的数据没法快速进行处理。于是人们想到可以利用相互连接的”一群“计算机来并行处理数据,这样就能够大大提升效率。这种思路首先体现在”分布式文件系统“上面,即把一台计算机存不下的文件分散到一群计算机上,需要调用某个文件的时候利用存储时的一些关键信息进行定位然后传输。在这些文件系统的基础上,人们又开发出了许多更高层次的编程系统。这些编程系统的中心,便是MapReduce。它能高效地对计算集群进行许多常见的大型数据统计计算,并且能够处理一些计算过程中的硬件故障。
Map
Map是把输入的文档转换成键值对的形式。这么说大家可能难以理解,因为键值对是需要自己设计的,不同的键值对设计对应了不同的Reduce处理方式。我们来看一个例子。
Example:比如我们需要对一个词汇表里面,各种单词(w1, w2, w3...)出现的频率进行统计。那么,在Map阶段,我们可以对每一个出现的单词,输出(wi,1)这种键值对,在reduce过程中,根据键和值分别进行统计,比如(w1, 1)有2个,那么reduce输出就为(w1, 2),最后再汇总,就能够得到每个单词的频率了。
看到这里,大家 应该会有觉得这样好像有点多此一举,明明可以直接进行计算,为什么一定要搞一个Map呢。其实这样是为了方便并行运算。我们如果省略Map过程,直接进行reduce,那我们并不会知道每个reduce上的任务量到底有多少,这样就会出现等待,导致用户反馈不及时。相反,如果我们采用一个Map阶段,然后再利用哈希办法,将每个键值对哈希后,随机地分配到一个reduce机器上。这样能够确保负载是基本均衡地,从而不会有若干台机器等待一台机器的现象发生。同时,我们知道从磁盘读取数据是时间开销很大的。如果放在一台机器上串行执行,那么reduce需要等带读入全部完成才能开始,而把map放到另一台机器上,读取达到一定的数量就传送给reduce机器,这样我们就可以边读边处理,是不是效率就能升上去了呢。
Reduce
reduce阶段就是对map发送来的结果进行group by key。也就是相同键的分到一组,然后输出一些键值对给最后整合。还是上面那个例子,假设有两个reduce机器,一个reduce输出[(w1, 2), (w2, 3],另一个输出[(w2, 3), (w3, 7)]那最后我们通过整合就能得到整个文档从词频。
Shuffle&Combine
Combiner:就是reduce类似的工作,当w1再一个map中出现两次,普通的map输出会是[(w1, 1), (w1, 1)],combiner后就输出[(w1, 2)]
Shuffle:就是设计哈希函数,尽可能的把Map输出的键值对均匀的分配到reduce机器上。通常可以通过Hash()%reduce_num进行分配。
Coding Time
这是我们学校实验的一个代码,任务是对维基百科下的一些词条文件进行词频统计
Map过程
import os
import re
import pickle
import threading
def get_text(d, file_name):
target = d + '\\' + file_name
txt = open(target, "r", encoding='utf-8').read()
# 变成小写
txt = txt.lower()
# re库进行正则匹配更快找到单词
txt = re.findall(r'\b\w+\b', txt)
return txt
# 利用模块化的方法把代码变得更加可维护
class mapper():
def __init__(self, wtb=".", dp="source_data", shuffle=False, reducer_num=3):
# dp是需要统计的单词的存放路径
self.data_path = dp
# wtb是词汇表存放路径,作为参数传入,默认放在当前文件夹啊下
self.words_table_path = wtb
# 进行word_count时的目标词汇表
self.words_dict = {key: 1 for key in get_text(self.words_table_path, 'words.txt')}
# shuffle 变量决定Map阶段是否进行随机分配
self.shuffle = shuffle
# reducer_num模拟多少台reduce机器,方便shuffle时候取模
self.reducer_num = reducer_num
# map部分的工作:把单词处理成键值对
def work(self, directory, idx):
# idx是编号,方便文件存储,因为pickle.dump会覆盖写入,无法追加写入
output = []
for r, _, f in os.walk(directory):
for name in f:
txt = get_text(r, name)
for word in txt:
# 判断是否在词表中出现
if self.words_dict.get(word, 0):
# name[:-4]利用切片去除.txt
output.append(tuple([(name[:-4], word), 1]))
with open(f'map_out//reducer{idx}.pkl', 'wb') as f:
pickle.dump(output, f)
def work_with_shuffle(self, directory, index):
output = {}
for r, _, f in os.walk(directory):
for name in f:
txt = get_text(r, name)
for word in txt:
# 判断是否在词表中出现
if self.words_dict.get(word, 0):
# 进行combiner
output[tuple([name[:-4], word])] = output.get(tuple([name[:-4], word]), 0) + 1
# shuffle就是利用哈希后取模选择分配到那一个reducer上,这样能让任务量均衡
slice = [{} for _ in range(self.reducer_num)]
for (title, keyword), val in output.items():
idx = hash(keyword) % self.reducer_num
slice[idx][(title, keyword)] = val
# 将哈希后的写入文件
for i in range(self.reducer_num):
with open(f"map_out_with_shuffle//reducer{index * 3 + i - 2}.pkl", "wb") as f:
pickle.dump(slice[i], f)
# 主函数,利用多线程模拟分布式计算
def main(self):
data_root = []
for root, dirs, files in os.walk(self.data_path):
data_root.append(root)
data_root = data_root[1:]
Thread = []
if self.shuffle:
thread_func = [self.work_with_shuffle for _ in range(9)]
else:
thread_func = [self.work for _ in range(9)]
for i, path in enumerate(data_root, 1):
new_thread = threading.Thread(target=thread_func[i-1], args=(path, i,))
new_thread.start()
Thread.append(new_thread)
for thd in Thread:
thd.join()
test = mapper(shuffle=True)
test.main()
Reduce过程
def reduce(index_numbers):
# 初步统计对数
for idx in index_numbers:
count_pairs = {}
with open(f"map_out/reducer{idx}.pkl", "rb") as f:
tmp = pickle.load(f)
for i, tp in enumerate(tmp):
try:
k, v = tp
except ValueError:
print(i, tp)
continue
try:
count_pairs[k] = count_pairs.get(k, 0) + v
except TypeError:
print(tp, i, "v", v)
final_words = {}
for (title, keyword) in count_pairs.keys():
final_words[keyword] = final_words.get(keyword, 0) + count_pairs[(title, keyword)]
# 统计完成后排序取前1000名
# 有可能在这部分不是前1000, 但是另外的reduce在前1000, 直接取前1000会导致统计数目错误
candidate_word_values = sorted(final_words.items(), key=lambda x: x[1], reverse=True)
# print(candidate_word_values)
# 写入输出文件
with open(f'reduce_out//reduce{idx}.pkl', 'wb') as f:
pickle.dump(candidate_word_values, f)
possible_word = dict(candidate_word_values)
# 保存一下title和keword
title_keywords = {}
for (title, keyword) in count_pairs.keys():
if title not in title_keywords.keys():
title_keywords[title] = set()
title_keywords[title].add(keyword)
with open(f'reduce_out//reduce_pairs{idx}.pkl', 'wb') as f:
pickle.dump(title_keywords, f)