MapReduce原理及简单实现
MapReduce是Google在2004年发表的论文《MapReduce: Simplified Data Processing on Large Clusters》中提出的一个用于分布式的用于大规模数据处理的编程模型。
原理
MapReduce将数据的处理分成了两个步骤,Map和Reduce。Map将输入的数据集拆分成一批KV对并输出,对于每一个<k1, v1>
,Map将输出一批<k2, v2>
;Reduce将Map对Map中产生的结果进行汇总,对于每一个<k2, list(v2)>
(list(v2)
是所有key为k2
的value),Reduce将输出结果<k3, v3>
。
以单词出现次数统计程序为例,map对文档中每个单词都输出<word, 1>
,reduce则会统计每个单词对应的list
的长度,输出<word, n>
:
map(String key, String value):
// key: document name
// value: document contents
for each word w in value:
EmitIntermediate(w, “1″);
reduce(String key, Iterator values):
// key: a word
// values: a list of counts
int result = 0;
for each v in values:
result += ParseInt(v);
Emit(AsString(result));
流程
MapReduce的流程如下:
- 将输入拆分成M个段,产生M个Map任务和R个Reduce任务。
- 创建1个master和n个worker,master会将Map和Reduce分派给worker执行。
- 被分配了Map任务的worker从输入中读取解析出KV对,传递给用户提供的Map函数,得到中间的一批KV对。
- 将中间的KV对使用分区函数分配到R个区域上,并保存到磁盘中,当Map任务执行完成后将保存的位置返回给master。
- Reduce worker根据master传递的参数从文件系统中读取数据,解析出KV对,并对具有相同key的value进行聚合,产生
<k2, list(v2)>
。如果无法在内存中进行排序,就需要使用外部排序。 - 对于每一个唯一的key,将
<k2, list(v2)>
传递给用户提供的Reduce函数,将函数的返回值追加到输出文件中。 - 当所有任务都完成后,MapReduce程序返回
MapReduce的整个流程并不复杂,就是将数据分片后提交给map执行,执行产生的中间结果经过处理后再交给reduce执行,产生最终结果。
容错
当worker发生故障时,可以通过心跳等方法进行检测,当检测到故障之后就可以将任务重新分派给其他worker重新执行。
当master发生故障时,可以通过检查点(checkpoint)的方法来进行恢复。然而由于master只有一个,比较难进行恢复,因此可以让用户检测并重新执行任务。
对于输出文件来说,需要保证仍在写入中的文件不被读取,即保证操作的原子性。可以通过文件系统重命名操作的原子性来实现,先将结果保存在临时文件中,当执行完成后再进行重命名。使用这种方法就可以将有副作用的write
变为幂等(总是产生相同结果的运算,如a = 2
就是幂等的,而a += 2
则不是)的重命名。
落伍者
影响任务的总执行时间的重要因素就是落伍者:在运算中某个机器用了很长时间才完成了最后的几个任务,从而增加了总的执行时间。对于这种情况,可以在任务即将完成时,将剩余的任务交给备用者进程来执行,无论是最初的worker完成了任务还是备用者完成了,都可以将任务标记为完成。
分区函数
对于map产生的结果,通过分区函数来将相同key的KV对分配给同一个reduce来执行。默认的分区函数是hash(key) % R
,但在某些情况下也可以选择其他分区函数。如key为URL时,希望相同主机的结果在同一个输出中,那么就可以用hash(hostname(key)) % R
作为分区函数。
实现
实现部分是基于MIT 6.824的实验完成的。
type Coordinator struct {
mapJobs []Job
reduceJobs []Job
status int
nMap int
remainMap int
nReduce int
remainReduce int
lock sync.Mutex
}
func MakeCoordinator(files []string, nReduce int) *Coordinator {
c := Coordinator{}
c.status = MAP
c.nMap = len(files)
c.remainMap = c.nMap
c.nReduce = nReduce
c.remainReduce = c.nReduce
c.mapJobs = make([]Job, len(files))
c.reduceJobs = make([]Job, nReduce)
for idx, file := range files {
c.mapJobs[idx] = Job{[]string{file}, WAITTING, idx}
}
for idx := range c.reduceJobs {
c.reduceJobs[idx] = Job{[]string{}, WAITTING, idx}
}
c.server()
return &c
}
func (c *Coordinator) timer(status *int) {
time.Sleep(time.Second * 10)
c.lock.Lock()
if *status == RUNNING {
log.Printf("timeout\n")
*status = WAITTING
}
c.lock.Unlock()
}
func (c *Coordinator) AcquireJob(args *AcquireJobArgs, reply *AcquireJobReply) error {
c.lock.Lock()
defer c.lock.Unlock()
fmt.Printf("Acquire: %+v\n", args)
if args.CommitJob.Index >= 0 {
if args.Status == MAP {
if c.mapJobs[args.CommitJob.Index].Status == RUNNING {
c.mapJobs[args.CommitJob.Index].Status = FINISHED
for idx, file := range args.CommitJob.Files {
c.reduceJobs[idx].Files = append(c.reduceJobs[idx].Files, file)
}
c.remainMap--
}
if c.remainMap == 0 {
c.status = REDUCE
}
} else {
if c.reduceJobs[args.CommitJob.Index].Status == RUNNING {
c.reduceJobs[args.CommitJob.Index].Status = FINISHED
c.remainReduce--
}
if c.remainReduce == 0 {
c.status = FINISH
}
}
}
if c.status == MAP {
for idx := range c.mapJobs {
if c.mapJobs[idx].Status == WAITTING {
reply.NOther = c.nReduce
reply.Status = MAP
reply.Job = c.mapJobs[idx]
c.mapJobs[idx].Status = RUNNING
go c.timer(&c.mapJobs[idx].Status)
return nil
}
}
reply.NOther = c.nReduce
reply.Status = MAP
reply.Job = Job{Files: make([]string, 0), Index: -1}
} else if c.status == REDUCE {
for idx := range c.reduceJobs {
if c.reduceJobs[idx].Status == WAITTING {
reply.NOther = c.nMap
reply.Status = REDUCE
reply.Job = c.reduceJobs[idx]
c.reduceJobs[idx].Status = RUNNING
go c.timer(&c.reduceJobs[idx].Status)
return nil
}
}
reply.NOther = c.nMap
reply.Status = REDUCE
reply.Job = Job{Files: make([]string, 0), Index: -1}
} else {
reply.Status = FINISH
}
return nil
}
在Coordinator
中保存所有的任务信息以及执行状态,worker通过AcquireJob
来提交和申请任务,要等待所有map任务完成后才能执行reduce任务。这里就简单的将每一个文件都作为一个任务。
func doMap(mapf func(string, string) []KeyValue, job *Job, nReduce int) (files []string) {
outFiles := make([]*os.File, nReduce)
for idx := range outFiles {
outFile, err := ioutil.TempFile("./", "mr-tmp-*")
if err != nil {
log.Fatalf("create tmp file failed: %v", err)
}
defer outFile.Close()
outFiles[idx] = outFile
}
for _, filename := range job.Files {
file, err := os.Open(filename)
if err != nil {
log.Fatalf("cannot open %v", filename)
}
content, err := ioutil.ReadAll(file)
if err != nil {
log.Fatalf("cannot read %v", filename)
}
file.Close()
kva := mapf(filename, string(content))
for _, kv := range kva {
hash := ihash(kv.Key) % nReduce
js, _ := json.Marshal(kv)
outFiles[hash].Write(js)
outFiles[hash].WriteString("\n")
}
}
for idx := range outFiles {
filename := fmt.Sprintf("mr-%d-%d", job.Index, idx)
os.Rename(outFiles[idx].Name(), filename)
files = append(files, filename)
}
return
}
func doReduce(reducef func(string, []string) string, job *Job, nMap int) {
log.Printf("Start reduce %d", job.Index)
outFile, err := ioutil.TempFile("./", "mr-out-tmp-*")
defer outFile.Close()
if err != nil {
log.Fatalf("create tmp file failed: %v", err)
}
m := make(map[string][]string)
for _, filename := range job.Files {
file, err := os.Open(filename)
if err != nil {
log.Fatalf("cannot open %v", filename)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
kv := KeyValue{}
if err := json.Unmarshal(scanner.Bytes(), &kv); err != nil {
log.Fatalf("read kv failed: %v", err)
}
m[kv.Key] = append(m[kv.Key], kv.Value)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
file.Close()
}
for key, value := range m {
output := reducef(key, value)
fmt.Fprintf(outFile, "%v %v\n", key, output)
}
os.Rename(outFile.Name(), fmt.Sprintf("mr-out-%d", job.Index))
log.Printf("End reduce %d", job.Index)
}
//
// main/mrworker.go calls this function.
//
func Worker(mapf func(string, string) []KeyValue,
reducef func(string, []string) string) {
CallExample()
var status int = MAP
args := AcquireJobArgs{Job{Index: -1}, MAP}
for {
args.Status = status
reply := AcquireJobReply{}
call("Coordinator.AcquireJob", &args, &reply)
fmt.Printf("AcReply: %+v\n", reply)
if reply.Status == FINISH {
break
}
status = reply.Status
if reply.Job.Index >= 0 {
// get a job, do it
commitJob := reply.Job
if status == MAP {
commitJob.Files = doMap(mapf, &reply.Job, reply.NOther)
} else {
doReduce(reducef, &reply.Job, reply.NOther)
commitJob.Files = make([]string, 0)
}
// job finished
args = AcquireJobArgs{commitJob, status}
} else {
// no job, sleep to wait
time.Sleep(time.Second)
args = AcquireJobArgs{Job{Index: -1}, status}
}
}
}
worker通过RPC调用向Coordinator.AcquireJob
申请和提交任务,之后根据任务类型执行doMap
或doReduce
。
doMap
函数读取目标文件并将<filename, content>
传递给map函数,之后将返回值根据hash(key) % R
写入到目标中间文件中去。
doReduce
函数则从目标文件中读取KV对并加载到内存中,对相同的key进行合并(这里我是用map
来做的,但是之后看论文发现是用排序来做的,这样可以保证在每个输出文件中的key是有序的)。合并之后就将<key, list(value)>
交给reduce函数处理,最后把返回值写入到结果文件中去。
MapReduce原理及简单实现的更多相关文章
- HBase笔记:对HBase原理的简单理解
早些时候学习hadoop的技术,我一直对里面两项技术倍感困惑,一个是zookeeper,一个就是Hbase了.现在有机会专职做大数据相关的项目,终于看到了HBase实战的项目,也因此有机会搞懂Hbas ...
- MapReduce原理及其主要实现平台分析
原文:http://www.infotech.ac.cn/article/2012/1003-3513-28-2-60.html MapReduce原理及其主要实现平台分析 亢丽芸, 王效岳, 白如江 ...
- MapReduce原理讲解
简介 本文主要介绍MapReduce V2的基本原理, 也是笔者在学习MR的学习笔记整理. 本文首先大概介绍下MRV2的客户端跟服务器交互的两个协议, 然后着重介绍MRV2的核心模块MRAppMast ...
- Hapoop原理及MapReduce原理分析
Hapoop原理 Hadoop是一个开源的可运行于大规模集群上的分布式并行编程框架,其最核心的设计包括:MapReduce和HDFS.基于 Hadoop,你可以轻松地编写可处理海量数据的分布式并行程序 ...
- Hadoop学习记录(4)|MapReduce原理|API操作使用
MapReduce概念 MapReduce是一种分布式计算模型,由谷歌提出,主要用于搜索领域,解决海量数据计算问题. MR由两个阶段组成:Map和Reduce,用户只需要实现map()和reduce( ...
- hadoop笔记之MapReduce原理
MapReduce原理 MapReduce原理 简单来说就是,一个大任务分成多个小的子任务(map),并行执行后,合并结果(reduce). 例子: 100GB的网站访问日志文件,找出访问次数最多的I ...
- MapReduce 原理与 Python 实践
MapReduce 原理与 Python 实践 1. MapReduce 原理 以下是个人在MongoDB和Redis实际应用中总结的Map-Reduce的理解 Hadoop 的 MapReduce ...
- 大数据 --> MapReduce原理与设计思想
MapReduce原理与设计思想 简单解释 MapReduce 算法 一个有趣的例子:你想数出一摞牌中有多少张黑桃.直观方式是一张一张检查并且数出有多少张是黑桃? MapReduce方法则是: 给在座 ...
- MapReduce原理
MapReduce原理 WordCount例子 用mapreduce计算wordcount的例子: package org.apache.hadoop.examples; import java.io ...
随机推荐
- Cisco的互联网络操作系统IOS和安全设备管理器SDM__备份和恢复Cisco 配置
对路由器配置进行的任何修改存储在running-config文件中.如果在修改了running-config后没有输入copy run start命令,那么路由器重载或掉电后,修改的内容会丢失. 1. ...
- cassandra权威指南读书笔记--Cassandra架构(3)
分阶段事件驱动架构 SEDASEDA(Staged Event-Driven Architecture)的核心思想是把一个请求处理过程分成几个Stage,不同资源消耗的Stage使用不同数量的线程来处 ...
- AtCoder Beginner Contest 176 E - Bomber (思维)
题意:有一张\(H\)x\(W\)的图,给你\(M\)个目标的位置,你可以在图中放置一枚炸弹,炸弹可以摧毁所在的那一行和一列,问最多可以摧毁多少目标. 题解:首先我们记录某一行和某一列目标最多的数目, ...
- Codeforces Round #650 (Div. 3) B. Even Array
题目链接:https://codeforces.com/contest/1367/problem/B 题意 有一大小为 $n$ 的数组 $a$,问能否经过交换使所有元素与下标奇偶性相同(0 - ind ...
- P1020 导弹拦截(LIS)
题目描述 某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度.某天,雷达捕捉到敌国的导弹 ...
- Codeforces Round #644 (Div. 3) D. Buying Shovels (数学)
题意:商店里有\(k\)个包裹,第\(i\)个包裹中含有\(i\)个物品,现在想要买\(n\)物品,你可以选择某一个包裹购买任意次,使得物品数刚好等于\(n\),求最少的购买次数. 题解:首先,假如\ ...
- IIS Web API 长时间不连接后第一次访问非常缓慢
搭建在 IIS 上的 Web API 若长时间不访问,会出现第一次访问耗时较长的现象,这与其调用应用程序池的 Idle Time-out(minutes) 即闲置超时设置有关.默认值为20,修改为0即 ...
- OpenStack Train版-8.安装neutron网络服务(控制节点)
安装neutron网络服务(controller控制节点192.168.0.10) 创建neutron数据库 mysql -uroot CREATE DATABASE neutron; GRANT A ...
- CN_Week2_Neuron_code
CN_Week1_Neuron_code on Coursera Abstract for week2: -- 1. Technique for recording from the brain. - ...
- VuePress plugins All In One
VuePress plugins All In One VuePress & element-ui & docs $ yarn add -D vuepress-plugin-demo- ...