建议先了解一下MapReduce他可以用来做什么。可以看看我的上一篇文章MapReduce 介绍 ,不过最好还是阅读原论文,这样你会更加清楚。
前言
分布式理论接触的挺多的,例如:
- CAP理论
- BASE理论
但是关于分布式理论实践,我也找不到很好的简单的入门课程。不过,感谢互联网让我找到了Mit的公开课,为我打开了分布式的新的大门~~
实现历程
本次实验需要我们自己补充完整课程所提供的代码,完成一个用来单词计数的MapReduce程序
环境搭建
我们跟着实验搭建环境。
- 首先确认自己的环境是否是Linux。本人是使用虚拟机搭建Ubantu的Linux环境。
- 安装VS Code,因为VS Code轻量便捷,即便是你电脑性能不好也不会太卡。VSCode自带Git,我们就不用再配置了。
- 安装go Sdk。注意!!!请务必和实验提供的环境一致,否则可能会有莫名其妙的Bug。
- 使用Ctrl + Alt + t在Linux下打开命令行,使用code命令启动VSCode。
- 在VSCode中,新建一个终端,在里面使用Git将实验代码clone下来。
- 此时检查自己是否搭好环境,以跟着下面的测试检查程序运行是否正常
实验目录结构
本次实验,我们需要用到的这三个目录。
main目录
mr目录
包含三个文件 master rpc worker 我们的代码需要编写在这三个文件中
mrapps
测试程序所需要使用的,用来检测我们编写代码的健壮性。
本次实验一共有五个测试
- wc test:验证编写的程序是否得到正确的单词统计结果
- indexer test:验证编写的程序结果是否来自正确的文件
- map parallelism test:验证Map任务是否多线程执行
- reduce parallelism test:验证Reduce任务是否多线程执行
- 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脚本。
这样我们就可以一行命令执行一堆命令了。
bash
sh master.sh
sh worker.sh
下面涉及一点点实现思路与核心代码,请你谨慎观看。我更希望下面是在你做了实验后,当做一个交流。
---------------------------------------------分割线---------------------------------------------
整体思路
注意:在RPC中定义的各个参数一定要用大驼峰 ,否则将报错。
在Master中,还有必要的存储的数据结构,个人定义如下
go
//任务状态
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是否完成
}
实现步骤
- 在每一个Worker中,存在一个死循环,不断向Master要任务。这边也需要定义好双方通信的约定。
go
//任务类型
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 //
}
- 在Master中,有一个不断监听Woker任务的线程,Master将会把任务发给Worker。下面是派发任务的逻辑代码
go
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
}
- Worker拿到Map任务后,把所有的文件都调用一遍Map函数,并且遍历每一个Key,按照ihash()函数将该Key分到对应的Reduce任务中。我个人倾向将Reduce序号分个文件夹,就像下面这样 如此以来,后续分发Reduce任务时,读取数据比较方便。
- 等待Map任务完成之后,Master将会进行Reduce任务的分发,Worker拿到Reduce任务后,可以从mrsequential.go借鉴一些代码,以读取Map输入文件,对Map和Reduce之间的中间键/值对进行排序,以及将Reduce输出存储在文件中。
- 最后,为了应对最后的Crash Test,对于每派发出的一个任务,我设置了一个监听队列以及一个新的线程来判断该任务是否超时。一些核心代码定义如下
go
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