2021 MIT6.824 Lab1 MapReduce算法总结

0 介绍

最近开始学习MIT 6.824,这是一门讲解分布式系统的课程,同时也提供了一系列有挑战性的lab。这篇博客讲述一下lab1 MapReduce算法的实现,由于课程要求不能公开代码,这里就不上很多代码了,以算法分析为主。不得不说国外的课程还是很硬核的,lab1要求两周内完成,在这两周内要求读数篇论文、学会golang、编写代码通过测试,当然如果能完成的话收获也很大。

1 MapReduce介绍

MapReduce是一个比较有名的分布式计算模型,由谷歌在2003年的一篇论文里提出,用于处理海量数据的问题,比如单词统计、倒排索引、url访问量排序等等。它的本质思想是归并,在实际实现中会有一个master主机负责调度任务,多个worker主机负责任务执行和返回任务状态。worker将数据map到不同的分区,然后使用reduce规约。map和reduce的方法可以由用户定义。以单词统计为例,现在有比如1TB的文本,要求统计出文本中每个单词的出现次数。显然这么多数据我们没法一次装入内存,所以要另寻他法。使用MapReduce的思路如下,:

  1. 将文本分片,分成很多16M~64M的小文本(设有m个),计划有n个reduce分区,master启动,等待worker申请任务
  2. worker启动之后,会一刻不停地向master申请任务。首先申请map任务,worker申请后拿到一个小文本,将小文本中的每个单词映射到对应的分区(如单词的哈希值 hashValue % n)。
  3. 在所有map任务完成后,worker申请reduce任务,通过刚才的映射,同样的单词必然在同一分区,所以worker读取每个分区的映射文本就可以统计单词数了。
    图示如下:
    在这里插入图片描述

原理是清晰的,下面分析一下lab中难点

2 Master

Master如前所述负责任务调度,它主要负责两件事,一是给worker分配任务,二是负责维护每个任务的状态。给worker分配任务如何理解?很简单,在任务列表中找到一个处于未分配的任务即可;维护任务状态如何理解?当一个worker拿走一个任务后,master需要将它标记为processing,如果worker在规定的时间内完成,并且返回任务成功的消息,就标记为任务完成;假如worker宕机了,没有在规定的时间内完成任务,master需要觉察这件事,并将任务重新标记为未分配,在本lab里我选的方法是开启一个监控线程,每隔lab推荐的10s检查一下哪些任务还在processing中,标为未分配即可。

const (
	taskUnAssigned = 0
	taskProcessing = 1
	taskFinished = 2
)

type Master struct {
	mapNum int //需要执行的map任务数
	reduceNum int //需要执行的reduce任务数
	mapFinished int //完成的map任务数
	reduceFinished int //完成的reduce任务数
	mapTaskStatus []int //map任务状态
	reduceTaskStatus []int //reduce任务状态
	mapFileNames []string //需要映射的文件名称数组
	mutex sync.Mutex //加锁保护共享数据,分配任务、修改任务状态时使用
}

3 Worker

MapReduce的worker负责具体的小任务完成,即map和reduce。worker首先通过rpc调用,请master分配任务,请求的struct格式如下

const (
	unAssigned = 0
	mapTask = 1
	reduceTask = 2
	allDone = 3
)
type TaskInfo struct {
	TaskType int //任务类型
	FilePath string //文件列表
	TaskId int //任务编号
	MapNum int //map任务总数
	ReduceNum int //reduce任务总数
}

得到任务信息后,map的执行步骤如下:

  1. 打开目标文件,将文本输入到用户自定义的map函数里
  2. 用户自定义的map函数会返回一个key-value struct数组
  3. 根据key的哈希值确定reduce区域,将key-value对以json格式写入文件中,格式mr-tem-map任务编号-reduce区域

需要值得注意的是第三步,如果有多进程对同一文件进行读写的话可能会有错误发生,特别是一些worker出现crash又恢复继续工作的情况。lab提示了论文里介绍的一种trick,就是写文件时先写入临时文件(ioutil.TempFile()),然后原子的移动到目的文件夹(os.Rename())。
reduce的执行步骤如下:

  1. 根据任务的编号id,首先读取mr-tem-*-id,然后根据key值排序
  2. 调用用户定义的reduce函数
  3. 结果写出到mr-out-id文件

第三步也要考虑读写错误的情况,依然使用前面提到的trick。

4 结果

在这里插入图片描述
测试全部通过,顺便一提,根据运行时间可知这个任务是一个IO密集型的任务,cpu使用时间大概占了总体运行时间的10%,大部分时间其实是在等IO,所以我认为若要进行优化,要从IO入手。

附录 lab提示

  1. 完成所有的map任务之后再做reduce。
  2. map和reduce函数在对应的.so文件里,由用户自定义。
  3. 在本lab中,所有worker进程共享一个文件系统,也就是说所有worker是跑在同一台机器上的。如果说worker是在不同的机器上,我们就需要GFS这种分布式文件系统了
  4. worker的map任务会需要某种方式来存储中间态的k-v对,方便在做reduce任务的时候读回来。可以考虑使用Go语言的encoding/json包,例子:
  enc := json.NewEncoder(file) //file是文件指针
  for _, kv := ... {
    err := enc.Encode(&kv)

读回来:

  dec := json.NewDecoder(file)
  for {
    var kv KeyValue
    if err := dec.Decode(&kv); err != nil {
      break
    }
    kva = append(kva, kv)
  }
  1. master是一个并发服务器,记得给共享数据加锁,比如master要维护任务分配状态的话就需要加锁。
  2. Wokrder工作的时候有时候需要等待,比如当map没有全部完成的时候,word申请reduce任务的话就需要等待。一种解决方法是在每个request间隔睡眠;另一种解决方法是使用条件变量,让发起request的那条thread阻塞直到等待结束。
  3. 可以使用ioutil.TemFile来创建临时文件,os.Rename来原子性的重命名临时文件,这样是为了防止worker执行任务时出现crash,导致文件写一般又被别的进程污染。可以执行考虑每个任务创建一些临时文件,直到它写完成为一个完整的文件再rename,这个技巧是原论文里提出来的,非常重要,测试样例的crash-test将会模拟worker crash掉的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值