Mit 6.824 Lab:1 MapReduce 实现历程

955 阅读8分钟

实验原址:mit 6.824 Lab1 MapReduce

中文翻译:mit 6.824 实验1 MapReduce

建议先了解一下MapReduce他可以用来做什么。可以看看我的上一篇文章MapReduce 介绍 ,不过最好还是阅读原论文,这样你会更加清楚。

前言

分布式理论接触的挺多的,例如:

  • CAP理论
  • BASE理论

但是关于分布式理论实践,我也找不到很好的简单的入门课程。不过,感谢互联网让我找到了Mit的公开课,为我打开了分布式的新的大门~~

实现历程

本次实验需要我们自己补充完整课程所提供的代码,完成一个用来单词计数的MapReduce程序

环境搭建

我们跟着实验搭建环境。

  1. 首先确认自己的环境是否是Linux。本人是使用虚拟机搭建Ubantu的Linux环境。
  2. 安装VS Code,因为VS Code轻量便捷,即便是你电脑性能不好也不会太卡。VSCode自带Git,我们就不用再配置了。
  3. 安装go Sdk。注意!!!请务必和实验提供的环境一致,否则可能会有莫名其妙的Bug。Pasted image 20231223104130.png
  4. 使用Ctrl + Alt + t在Linux下打开命令行,使用code命令启动VSCode。
  5. 在VSCode中,新建一个终端,在里面使用Git将实验代码clone下来。图片.png
  6. 此时检查自己是否搭好环境,以跟着下面的测试检查程序运行是否正常 图片.png

实验目录结构

Pasted image 20231223105121.png 本次实验,我们需要用到的这三个目录。

main目录

Pasted image 20231223105826.png

mr目录

Pasted image 20231223110329.png 包含三个文件 master rpc worker 我们的代码需要编写在这三个文件中

mrapps

测试程序所需要使用的,用来检测我们编写代码的健壮性。

Pasted image 20231223111815.png

本次实验一共有五个测试

  1. wc test:验证编写的程序是否得到正确的单词统计结果
  2. indexer test:验证编写的程序结果是否来自正确的文件
  3. map parallelism test:验证Map任务是否多线程执行
  4. reduce parallelism test:验证Reduce任务是否多线程执行
  5. crash test:健壮性测试,测试worker崩溃或执行太慢时,本程序依然可以获得正确的结果

在测试的时候,咱们有时候可以一个一个来,也可以直接全部进行测试。

实验目的

实现一个分布式MapReduce,它由两个程序(master程序和worker程序)组成。只有一个 master进程,一个或多个worker进程并行执行。在真实的系统中,工作人员将在一堆不同的机器上运 行,但是对于本实验,您将全部在单个机器上运行它们。worker将通过RPC与master服务器对话。每个 工作进程都会向主服务器请求一个任务,从一个或多个文件中读取任务的输入,执行任务,并将任务 的输出写入一个或多个文件。master应注意一个工人是否在合理的时间内没有完成任务(在本实验 中,使用十秒钟),并将同一任务交给另一个worker。

实验要求

  • Map阶段应将中间键划分为用于nReduce reduce任务的存储桶 ,其中nReduce是 main/mrmaster.go传递给MakeMaster()的参数。
  • 工作程序实现应将第X个reduce任务的输出放入文件mr-out-X中。
  • 一个mr-out-X文件的每个Reduce函数输出应包含一行。该行应以Go "%v %v" 格式生成,并 使用键和值进行调用。在main/mrsequential.go中 查看注释为“这是正确的格式”的行。如 果您的实现不按该格式输出,则测试脚本将失败。
  • 您可以修改mr/worker.go,mr/master.go,和mr/rpc.go。您可以临时修改其他文件以进行 测试,但是请确保您的代码可以与原始版本一起使用;我们将使用原始版本进行测试。
  • worker应将中间Map输出放置在当前目录中的文件中,您的worker以后可以在其中读取它们, 作为Reduce任务的输入。
  • main/mrmaster.go期望mr/master.go实现 Done()方法,该方法在MapReduce作业完全完成时 返回true;届时,mrmaster.go将退出。
  • 全部完成后,工作进程应退出。一种简单的实现方法是使用call()的返回值:如果worker程序 无法与master服务器联系,则可以假定worker服务器由于作业完成而退出,因此worker程序也 可以终止。根据您的设计,您可能还会发现拥有主人可以交给工作人员的“请退出”伪任务会 很有帮助。

实验实现

小技巧

  • 这种多线程调试比较麻烦,所以需要用到两个以上的终端。一个终端用于启动Master,另外的一些终端用来启动Worker,这样双方的日志不会混合。
  • 实验提供了一个Word-Count插件,该插件也提供了Map函数与Reduce函数,因此每次运行的时候,我们都要编译他,我们就可以将编译与运行Worker弄成一个简单的shell脚本。

Pasted image 20231223141115.png

Pasted image 20231223141238.png 这样我们就可以一行命令执行一堆命令了。

sh master.sh
sh worker.sh

下面涉及一点点实现思路与核心代码,请你谨慎观看。我更希望下面是在你做了实验后,当做一个交流。

---------------------------------------------分割线---------------------------------------------

整体思路

Pasted image 20231223141347.png

注意:在RPC中定义的各个参数一定要用大驼峰,否则将报错。 Pasted image 20231223141948.png

在Master中,还有必要的存储的数据结构,个人定义如下

//任务状态
const (
	WAITING =  0
	WORKING = 1
	FINISHED = 2 
)

type Master struct {
	// Your definitions here.
	filesCnt              int
	nReduce               int     
	files              [] string  //所有的文件名称
	mapStatus          [] int     //0 - 等待中 1 - 处理中 2 - 成功
	mapDone               bool    //所有的mapTask是否完成
	reduceStatus       [] int     //0 - 等待中 1 - 处理中 2 - 成功
	reduceDone            bool    //所有的reduceTask是否完成
}

实现步骤

  1. 在每一个Worker中,存在一个死循环,不断向Master要任务。这边也需要定义好双方通信的约定。
//任务类型
const(
	MAP_TASK = 0    //map任务
	REDUCE_TASK = 1 // reduce任务
)
//请求类型
const(
	ASK_FOR_TASK = 0 //请求任务
	NOTICE = 1       // 通知
)

//请求
type Request struct {
	RequestType int // 0 - 请求任务 1- 通知
	Seq int // 完成的任务序号 
	TaskType int // 完成的任务类型
}
//回复
type Response struct {
	TaskType int // 0-Map 1-Reduce
	Seq int //任务序号
	Filename string // 文件名称
	FileContent string // 文件内容
	NReduce int //
}
  1. 在Master中,有一个不断监听Woker任务的线程,Master将会把任务发给Worker。下面是派发任务的逻辑代码
func selectTask  (m * Master) (int,int)  {
	if !m.mapDone  {
		for i,j := range m.mapStatus {
			if j == WAITING{
				m.mapStatus[i] = WORKING
				return MAP_TASK, i
			}
		}
		for _,j := range m.mapStatus {
			if j != FINISHED{
				return 2, 0
			}
		}
		m.mapDone = true
		//fmt.Println("All Map Task is Done")
	} else if  m.mapDone  &&  !m.reduceDone {
		for i,j := range m.reduceStatus {
			if j == WAITING{
				m.reduceStatus[i] = WORKING
				return REDUCE_TASK, i
			}
		}
		for _,j := range m.reduceStatus {
			if j != FINISHED{
				return 2, 0
			}
		}
		m.reduceDone = true
	//	fmt.Println("All Reduce Task is Done")
	}else{
		//fmt.Println("All task were done!" )
		os.Exit(0)
		return 2,0
	}
	return 2,0
}
  1. Worker拿到Map任务后,把所有的文件都调用一遍Map函数,并且遍历每一个Key,按照ihash()函数将该Key分到对应的Reduce任务中。我个人倾向将Reduce序号分个文件夹,就像下面这样 Pasted image 20231223145828.png 如此以来,后续分发Reduce任务时,读取数据比较方便。
  2. 等待Map任务完成之后,Master将会进行Reduce任务的分发,Worker拿到Reduce任务后,可以从mrsequential.go借鉴一些代码,以读取Map输入文件,对Map和Reduce之间的中间键/值对进行排序,以及将Reduce输出存储在文件中。
  3. 最后,为了应对最后的Crash Test,对于每派发出的一个任务,我设置了一个监听队列以及一个新的线程来判断该任务是否超时。一些核心代码定义如下
type Monitor struct{
	taskType int    //监听任务类型
	seq int         //任务序号
	startTime time.Time//任务派发时间
	overTime string //结束时间
}
var moitorTaskList = make([] Monitor,0)

var lockSelect sync.RWMutex
var lockMonitor sync.RWMutex

func (m * Master) moitor() {
	for true {
            lockMonitor.Lock()
            var t [] Monitor
            for i,j  :=  range moitorTaskList  {
                    if time.Now().After(j.startTime) {
                            if j.taskType == MAP_TASK {
                                    if m.mapStatus[j.seq] == WORKING {
                                            m.mapStatus[j.seq] = WAITING
                                    }else{
                                            t = append(moitorTaskList[:i],moitorTaskList[i+1:]...)
                                    }
                            }else{
                                    if m.reduceStatus[j.seq] == WORKING {
                                            m.reduceStatus[j.seq] = WAITING
                                    }else{
                                            t  = append(moitorTaskList[:i],moitorTaskList[i+1:]...)
                                    }
                            }
                    }
            }
            moitorTaskList = t
            lockMonitor.Unlock()
            time.Sleep(3*time.Second)
	}
}

一些小细节:

  • 在修改共享数据时,一定要加锁
  • 有时候,Worker需要进行等待任务,所以任务类型可以新增一个等待类型
  • 当所有的任务结束后,避免无用程序一直执行,可以在Done函数中,告知Worker任务执行完成可下线做到shutdown gracefully

总结

在这我想说几点

  • 其实实验最难的不是实现,也不是使用另一种语言,而是开始做。这个才是最难的,万事开头难。
  • 在本实验中最烦的应该就是调试了,所以在调试的过程中,最好每一步都打个日志,这样你的思路就会很清晰。
  • 最后,真的难遇到这种可以有检验自己学习成果的课程,希望你且做且珍惜。这篇也算是记录一下自己的实现思路吧。祝你们都能看到最后的PASS ALL TEST

附上本人简陋实现:mit6.824 lab1 mapreduce