在讨论我们是否真的需要Map-Reduce这一分布式计算技术之前,我们先面对一个问题,这可以为我们讨论这个问题提供一个直观的背景。

问题

我们先从最直接和直观的方式出发,来尝试解决这个问题:
先伪一下这个问题:

SELECT COUNT(DISTINCT surname)
FROM big_name_file

我们用一个指针来关联这个文件.

接着考察每一行的数据,解析出里面的姓氏,这里我们可能需要一个姓氏字典或者对照表,然后我们可以利用最长前缀匹配来解析出姓氏。这很像命名实体识别所干的事。

拿到了姓氏,我们还需要一个链表L,这个链表的每个元素存储两个信息,一个是姓氏或者姓氏的编号,另一个是这个姓氏出现的次数。

在考察每一行的数据时,我们解析出姓氏,然后在链表L中查找这个姓氏对应的元素是否存在,如果存在就将这个元素的姓氏出现次数加一,否则就新增一个元素,然后置这个元素的姓氏出现次数为1。

当所有的行都遍历完毕,链表L的长度就是不同的姓氏的个数出现的次数。

    /**
* 直接法伪代码
*/
int distinctCount(file) {
//将磁盘文件file关联到一个内存中的指针f上
f <- file;
//初始化一个链表
L <- new LinkedList();
while(true) {
line <- f.readline();
if(line == null)
break;
//解析出此行的姓氏
surname <- parse(line);
//如果链表中没有这个姓氏,就新增一个,如果有,就将这个姓氏的出现次数+1
L.addOrUpdate(surname,1);
}
//链表的长度就是文件中不同姓氏的个数
return L.size();
}

ok,这个方法在不关心效率和内存空间的情况下是个解决办法。
但是却有一些值得注意的问题:

在进行addOrUpdate操作时,我们需要进行一个find的操作来找到元素是否已在链表中了。对于无序链表来说,我们必须采取逐一比较的方式来实现这个find的语义。

对于上面的考虑,显然我们知道如果能按下标直接找出元素就最好不过了,我们可以在常量时间找出元素并更新姓氏出现的次数。

哈希表法

对于这一点,我们可以采取哈希表来做,采取这个结构,我们可以用常量时间来找到元素并更新。

    int distinctCountWithHashTable(file) {
//将磁盘文件file关联到一个内存中的指针f上
f <- file;
//初始化一个哈希表
T <- new HashTable();
while(true) {
line <- f.readline();
if(line == null)
break;
//解析出此行的姓氏
surname <- parse(line);
//如果哈希表中没有这个姓氏,就新增一个,如果有,就将这个姓氏的出现次数+1
T.addOrUpdate(surname,1);
} //哈希表中实际存储的元素个数就是文件中不同姓氏的个数
return T.size();
}

假设给定文件是有序的

哈希表法看起来很美,但还是有潜在的问题,如果内存不够大怎么办,哈希表在内存中放不下。这个问题同样存在于直接法中。

想想看,如果这个文件是个排好序的文件,那该多好。
所有重复的姓氏都会连着出现,这样我们只需要标记一个计数器,每次读取一行文本,如果解析出的姓氏和上一行的不同,计数器就增1.
那么代码就像下面这样:

    int distinctCountWithSortedFile(file) {
//将磁盘文件file关联到一个内存中的指针f上
f <- file;
//不同姓氏的计数器,初始为0
C <- 0;
//上一行的姓氏
last_surname <- empty;
while(true) {
line <- f.readline();
if(line == null)
break;
//解析出此行的姓氏
surname <- parse(line);
//如果和上一行的姓氏不同,计数器加1
if(!last_surname.equals(surname))
C++;
last_surname <- surname;
} return C;
}

遗憾的是,我们并不能保证给定的文件是有序的。但上面方法的优点是可以破除内存空间的限制,对内存的需求很小很小。

那么能不能先排个序呢?
肯定是可以的,那么多排序算法在。但是有了内存空间的限制,能用到的排序算法大概只有位图法和外排了吧。

位图法

假设13亿/32 + 1个int(这里设32位)的内存空间还是有的,那么我们用位图法来做。
位图法很简单,基本上需要两个操作:

    /**
* 将i编码
*/
void encode(M,i) {
(M[i >> 5]) |= (1 << (i & 0x1F));
}
/**
*将i解码
*/
int decode(M,i) {
return (M[i >> 5]) & (1 << (i & 0x1F));
}

假设我们采取和姓氏字典一样的编号,我们做一个自然升序,那么这个方法就像下面这样:

    int distinctCountWithBitMap(file) {
//将磁盘文件file关联到一个内存中的指针f上
f <- file;
//初始化一个位图结构M,长度为13亿/32 + 1
M <- new Array();
//不同姓氏的个数,初始为0
C <- 0;
while(true) {
line <- f.readline();
if(line == null)
break;
//解析出此行的姓氏编号
surname_index <- parse(line);
//将姓氏编号编码到位图对应的位上
encode(M,surname_index);
} //找出位图中二进制1的个数
C <- findCountOfOneBits(M); return C;
}

ok,一切看起来很完美,但如何有效地找出位图中的二进制1的个数呢?上面使用了一个findCountOfOneBits方法,找出二进制1的个数,好吧,这是另外一个问题,但我们为了完整,可以给出它的一些算法:

int findCountOfOneBits_1(int[] array) {
int c = 0;
for(int i = 0 ; i < array.length; i++)
c += __popcnt(array[i]);
return c;
} int findCountOfOneBits_2(int[] array) {
int c = 0;
for(int i = 0 ; i < array.length; i++) {
while(array[i]) {
array[i] &= array[i] - 1;
c++;
}
} return c;
} int findCountOfOneBits_3(int[] array) {
int c = 0;
unsigned int t;
int e = 0;
for(int i = 0 ; i < array.length; i++) {
e = array[i];
t = e
- ((e >> 1) & 033333333333)
- ((e >> 2) & 011111111111); t = (t + (t >> 3)) & 030707070707
c += (t%63);
} return c;
}

上面的算法哪种效率最高呢?老三。

合并法

ok,位图法看起来破除了内存的限制,的确如此吗?如果内存小到连位图都放不下怎么办?
不解决这个问题了!开玩笑~

既然内存严重不足,那么我们只能每次处理一小部分数据,然后对这部分数据进行不同姓氏的个数的统计,用一个{key,count}的结构去维护这个统计,其中key就代表了我们的姓氏,count代表了它出现的次数。

处理完毕一小批数据后,我们需要将统计结果持久化到硬盘,以备最后累计,这牵扯到一个合并的问题。

如何进行有效地合并也值得思索,因为一开始文件内的姓名是无序的,所以不能在最后时刻进行简单合并,因为同一种姓氏可能出现在不同的统计结果分组中,这会使得统计结果出现重复。
所以我们必须对每批统计结果维护一个group结构或者如下的结构:

统计结果1:{{key=赵,count=631}...}
统计结果2:{{key=赵,count=3124}...}

统计结果N : {{key=赵,count=9956}...}

这样,我们在最后可以按key进行合并,得出如下的结构:

汇总结果1:{{key=赵,count=20234520}...}
汇总结果2:{{key=王,count=33000091...}

汇总结果M:{{key=钱,count=20009323}...}

BTW,数据是瞎编的,我个人并不知道到底哪个姓氏最多。
这样M就是我们不同姓氏的个数。

合并的过程如下图:

由于不断地将部分的统计结果合并到硬盘中,这种方式非常类似LSM算法,不同的是,我们对硬盘上中间文件的合并是on-line的,不是off-line的。

分布式法 Map-Reduce

合并法中,显然需要多次的访问硬盘,这有点问题:

如果是机械硬盘,那么磁盘的寻道时间令人头痛。
并且,合并的算法是串行的,我们无法降低摊还寻道代价。

面对内存容量有限的假设,我们可以推广到单机的计算资源有限的场景中来,设想一下,上面所列举的算法中,如果文档是有序的,那么我们仅仅使用极小的内存就可以解决问题,那么我们不需要分布式,也不需要Map-Reduce。

当然,如果我们不仅需要统计不同姓氏的个数,还想知道不同姓氏出现的频率,以研究到底姓王的多还是姓张的多,那么我们需要一些新思路。

如果我们能将姓名数据仔细分组,使得同样的姓氏会出现在同一组中.
然后将这些组分派到不同的计算节点上,由这些节点并行计算出若干个数C1、C2、...、Cn,最终我们的答案就是:n.
而每个姓氏的频率可以表示为:
frequencyi=Ci∑ni=1Ci,其中i是姓氏的编号,Ci表示第i个姓氏的出现的个数 。

而对应这种分布式计算模型的,就是Map-Reduce.
一个典型的Map-Reduce模型,大概像下图这样:

注:上图来自Search Engines:Information Retrieval In Practice.

对应我们这个问题,伪代码如下:

function Map(file) {
while(true) {
line <- file.readline();
if(line == null)
break;
surname <- parse(line);
count <- 1;
Emit(surname,count);
}
} function Reduce(key,values) {
C <- 0;
surname <- key;
while(!values.empty()) {
C <- C + values.next();
} Emit(surname,C);
}

使用Map-Reduce技术,不仅可以并行处理姓氏频率,同时也可以应对big、big、big-data(比如全银河系的“人”的姓名)。前提是你有足够的计算节点或者机器。

这里还有一个问题需要注意,就是上面的Reduce算法默认了数据已经按姓氏分组了,这个目标我们依靠Shuffle来完成。

在Shuffle阶段,依靠哈希表来完成group by surname.

在这里,将所有数据按姓氏分组并将每一组分派到一个计算节点上显得有些奢侈,所以如果在机器不足的情况下,可以将分组的粒度变大,比如100个姓氏为一组,然后通过多次的Map-Reduce来获得最终结果。

最后,希望我说明白了为什么我们需要Map-Reduce技术。
同时,不得不承认这个问题的设定是比较尴尬的= _ =,因为在对姓氏的parse阶段,我们用到了一个全姓氏字典,显然这个字典本身(Trie or Hash)可以告诉我们不同姓氏的个数。但如果问题的设定不是全部的姓氏都出现在文件中,或许这篇文章就能起到抛砖引玉的效果,那么其中的过程也值得书写下来。

我们为什么需要Map-Reduce?的更多相关文章

  1. MapReduce剖析笔记之三:Job的Map/Reduce Task初始化

    上一节分析了Job由JobClient提交到JobTracker的流程,利用RPC机制,JobTracker接收到Job ID和Job所在HDFS的目录,够早了JobInProgress对象,丢入队列 ...

  2. python--函数式编程 (高阶函数(map , reduce ,filter,sorted),匿名函数(lambda))

    1.1函数式编程 面向过程编程:我们通过把大段代码拆成函数,通过一层一层的函数,可以把复杂的任务分解成简单的任务,这种一步一步的分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计的基本单元. ...

  3. 记一次MongoDB Map&Reduce入门操作

    需求说明 用Map&Reduce计算几个班级中,每个班级10岁和20岁之间学生的数量: 需求分析 学生表的字段: db.students.insert({classid:1, age:14, ...

  4. filter,map,reduce,lambda(python3)

    1.filter filter(function,sequence) 对sequence中的item依次执行function(item),将执行的结果为True(符合函数判断)的item组成一个lis ...

  5. map reduce

    作者:Coldwings链接:https://www.zhihu.com/question/29936822/answer/48586327来源:知乎著作权归作者所有,转载请联系作者获得授权. 简单的 ...

  6. python基础——map/reduce

    python基础——map/reduce Python内建了map()和reduce()函数. 如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Pro ...

  7. Map/Reduce 工作机制分析 --- 作业的执行流程

    前言 从运行我们的 Map/Reduce 程序,到结果的提交,Hadoop 平台其实做了很多事情. 那么 Hadoop 平台到底做了什么事情,让 Map/Reduce 程序可以如此 "轻易& ...

  8. Map/Reduce个人实战--生成数据测试集

    背景: 在大数据领域, 由于各方面的原因. 有时需要自己来生成测试数据集, 由于测试数据集较大, 因此采用Map/Reduce的方式去生成. 在这小编(mumuxinfei)结合自身的一些实战经历, ...

  9. 用通俗易懂的大白话讲解Map/Reduce原理

    Hadoop简介 Hadoop就是一个实现了Google云计算系统的开源系统,包括并行计算模型Map/Reduce,分布式文件系统HDFS,以及分布式数据库Hbase,同时Hadoop的相关项目也很丰 ...

  10. map/reduce of python

    [map/reduce of python] 参考: http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac92 ...

随机推荐

  1. 06 Frequently Asked Questions (FAQ) 常见问题解答 (常见问题)

    Frequently Asked Questions (FAQ) Origins 起源 What is the purpose of the project? What is the history ...

  2. HttpRunner接口自动化测试框架

    简介 2018年python开发者大会上,了解到HttpRuuner开源自动化测试框架,采用YAML/JSON格式管理用例,能录制和转换生成用例功能,充分做到用例与测试代码分离,相比excel维护测试 ...

  3. html meta标签使用总结(转)

    之前学习前端中,对meta标签的了解仅仅只是这一句. <meta charset="UTF-8"> 但是打开任意的网站,其head标签内都有一列的meta标签.比如我博 ...

  4. #JS 异步处理机制的几种方式

    Javascript语言的执行环境是"单线程"(single thread,就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推) ...

  5. 【LOJ】#2542. 「PKUWC2018」随机游走

    题解 虽然我知道minmax容斥,但是--神仙能想到把这个dp转化成一个一次函数啊= = 我们相当于求给定的\(S\)集合里最后一个被访问到的点的时间,对于这样的max的问题,我们可以用容斥把它转化成 ...

  6. Bootstrap--响应式表格布局

    <div class="row"> <div class="col-sm-2 col-md-2" style="min-height ...

  7. python opencv入门-形态学转换

    目标: 学习不同的形态操作 例如 腐蚀.膨胀.开运算.闭运算 等. 我们要学习的函数有 cv2.erode(),cv2.dilate(),cv2.morphologyEx() 等. 原理 :一般对二值 ...

  8. Ionic实战六:日期选择控件

    onic日期选择控件,用于ionic项目开发中的日期选择以及日期插件   

  9. thinkphp和ueditor自定义后台处理方法整合

    先了解一下ueditor后台请求参数与返回参数格式规范: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ...

  10. 牛客练习赛1 B - 树

    链接:https://www.nowcoder.com/acm/contest/2/B来源:牛客网 题目描述 shy有一颗树,树有n个结点.有k种不同颜色的染料给树染色.一个染色方案是合法的,当且仅当 ...