MIT 6.824 Distributed System 学习笔记 - Lab1 MapReduce 记录
学习用时:3天
项目用时:3天
go build插件文件
问题
按指引build plugin时
go build -buildmode=plugin ../mrapps/wc.go
报错找不到/mr
注:如果更改了mr/目录下的内容, 需要重新build要用的MapReduce plugin
解决
- 到
6.824目录下,运行
$ go mod init 6.824-golabs-2020
- 全局搜索把代码中所有
../mr修改为6.824-golabs-2020/src/mr
初步理解我们需要做什么
main目录下的mrmaster.go和mrworker.go分别是master machine和worker machine的"main" routine,我们不需更改,但要理解他们是怎样启动的。
注:它们的package declaration都是package main
我们需要更改的是mr/目录下的三个文件,分别是master.go,worker.go,rpc.go,
注:它们的package declaration都是package mr
理解main/mrmaster.go和main/mrworker.go做了什么 + 详细思考我们需要做什么
main/mrmaster.go
-
此文件package declaration为
package main,且有main函数,说明此文件可编译运行。且参考lab指引可知,最后运行mrmaster.go的命令如下,参数为所有输入文件$ go run mrmaster.go pg-*.txt -
main函数中:
-
检查命令中是否指定了输入文件,没指定则报错退出
-
调用m := mr.MakeMaster(os.Args[1:], 10),第一个参数是所有输入文件名组成的string切片,第二个参数是nReduce,返回值是
*Master。MakeMaster函数和Master类型都定义在mr/master.go中,我们需要实现/补充MakeMaster和type Master。 -
每一秒轮询一次m.Done(),直至其返回值为true,main函数返回。
func (m *Master) Done() bool定义在mr/master.go中,我们需要实现/补充*Master的Done方法,这个方法应该在监测到整个mapreduce job完成时返回true,否则返回false
-
-
上面涉及到的mr中的方法:
-
mr/master.go文件中mr.MakeMaster函数新建一个Master类型结构体并调用它的.server()方法启动master serverfunc MakeMaster(files []string, nReduce int) *Master { m := Master{} // Your code here. m.server() //启动master server return &m } -
mr/master.go文件中 (m *Master)的server()方法:// start a thread that listens for RPCs from worker.go func (m *Master) server() { rpc.Register(m) rpc.HandleHTTP() //l, e := net.Listen("tcp", ":1234") sockname := masterSock() os.Remove(sockname) l, e := net.Listen("unix", sockname) if e != nil { log.Fatal("listen error:", e) } go http.Serve(l, nil) }调用
net/rpc库中的方法,将Mater结构体指针m注册,监听端口并开启服务,之后worker就可以通过这个socket与它进行通信待补充!!!!!!!!!!
-
main/mrworker.go
-
此文件package declaration为
package main,且有main函数,说明此文件可编译运行。且参考lab指引可知,最后运行mrmaster.go的命令如下,参数为用go build编译好的插件.so文件$ go run mrworker.go wc.so注:.so文件编译前的.go文件中有用户自定义的Map函数和Reduce函数
func Map(filename string, contents string) []mr.KeyValuefunc Reduce(key string, values []string) string -
main函数中:
- 检查命令中是否指定了插件文件,没指定则报错退出
- 把插件文件中定义的Map函数和Reduce函数加载出来,分别为mapf, reducef
- 调用mr.Worker
mr.Worker(mapf, reducef),我们需要实现/补充mr/worker.go中的Worker函数
如何发送RPC request的示例
mr/worker.go中为我们定义了CallExample()函数,是一个向master发送RPC request的示例:
// example function to show how to make an RPC call to the master.
// the RPC argument and reply types are defined in rpc.go.
func CallExample() {
// declare an argument structure.
args := ExampleArgs{}
// fill in the argument(s).
args.X = 99
// declare a reply structure.
reply := ExampleReply{}
// send the RPC request, wait for the reply.
call("Master.Example", &args, &reply)
// reply.Y should be 100.
fmt.Printf("reply.Y %v\n", reply.Y)
}
其中关键代码call()函数为:(也在mr/worker.go中定义)
// send an RPC request to the master, wait for the response.
// usually returns true.
// returns false if something goes wrong.
func call(rpcname string, args interface{}, reply interface{}) bool {
// c, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
sockname := masterSock()
c, err := rpc.DialHTTP("unix", sockname)
if err != nil {
log.Fatal("dialing:", err)
}
defer c.Close()
err = c.Call(rpcname, args, reply)
if err == nil {
return true
}
fmt.Println(err)
return false
}
call函数首先通过master的sockname与master建立连接并得到一个*rpc.Client类型的变量c,之后调用c.Call(方法名,参数,返回值)即可得到rpc call的返回值,成功返回true,失败返回false(这一点之后用到了)。
实现思路
worker会使用rpc调用master的getTask方法,得到一个task,可能是map task(则master要返回输入文件名string和map任务编号),也可能是reduce task(则master返回他对应的reduce任务编号)。worker得到task后就调用插件对应的Map/Reduce函数,并把结果存入中间文件/输出文件中。
为了表示上述"task",rpc.go文件中需要定义一个自定义结构体类型type Task,包含Type字段(string "map"/"reduce"),和Filename字段(string),由于中间文件名mr-X-Y和输出文件名mr-out-Y中,X是map task的编号,Y是reduce task的编号,所以还需要TaskNo字段。master.go文件中需要定义一个rpc handler —— func (m *Master) getTask() Task。
由于map task也需知道nReduce(为了知道怎么分片),所以再把nReduce作为Task的字段传入;由于reduce task也需知道nMap(为了知道中间文件名mr-X-Y中的X是0到几),所以再把nMap作为Task的字段传入;
而且master.go的Master结构体为了记录有哪些file已经被分配给worker处理了,哪些处理完了,哪些还未处理,要记录6个set:idleMapTask, inprogMapTask, completedMapTask, idleReduceTask, inprogReduceTask, completedReduceTask。每次收到worker的getTask调用时,要先判断应该给它分配什么任务,如果是map task全completed了,才能分配reduce task,所以Master结构体中还要记录一个map task总数nMap。如果map task没有全completed,就从idleMapTask中分配一个给它,如果没有idleMapTask了,(此时只有未完成的map task,没有未分配的map task),就返回一个等待的指示,worker收到后可以等待一段时间再来申请task,由此可知Task.type除了 "map"/"reduce"外,还要有一个"wait"。
注:上述6个set可以用map[uint]string来存储,uint表示对应task的taskno,string表示对应文件名,这样从某个set中取任务时可以同时取出其编号和文件名。
访问和更改Master结构体的上述数据时,各个rpc handler是并发执行的,所以需要一个锁来保护Master结构体中的各个数据结构,每次访问或更改那6个set时加锁,访问或更改完再解锁。
worker是在for循环中不断调用getTask领取任务,一个完成后领取下一个,直至整个mareduce job结束。又由于master的main函数在Master.Done()返回true时退出,所以worker可以在与master断开连接时退出for循环,也就是worker.go中定义的call()函数返回false时。因此worker.go的CallGetTask()函数可以有两个返回值(task Task, exit bool),call()函数返回false时CallGetTask()返回Task{}, true。另外,还可以在task的类型中加一种"finish",如果此时master还没结束,worker又去getTask,就可以直接返回一个"finish"类型的task,worker收到后直接退出
map task完成后,要通知master自己完成了这个map task,从而让master把对应的map task从inprogMapTask集合中移除,加入completedMapTask集合。也就是在master要定义一个rpc handler:func (m *Master) CompleteMapTask(args uint, reply string) error,参数是taskNo,reply是空字符串即可。
错误处理实现思路
master分配任务后开始10s,10s到了还没收到worker完成任务的rpc call,就把这个inprog的task重新变为idle,把对应worker标记为failed,如果有其他worker来取任务就分配给其他worker。若这个被标记为failed的worker一段时间后完成了任务,调用了completexxxtask这个rpc call,那么master需要告知它已经不需要它了。所以我们需要临时文件,做map/reduce task时先把结果写入临时文件,调用completexxxtask后确认master接受它完成这个任务了,才把临时文件rename成真正的输出文件/输出文件。这种临时文件的策略是官方给的hint
To ensure that nobody observes partially written files in the presence of crashes, the MapReduce paper mentions the trick of using a temporary file and atomically renaming it once it is completely written. You can use
ioutil.TempFileto create a temporary file andos.Renameto atomically rename it.
另外为了知道各个worker是否被标记成failed了,是否要接受这个worker完成的task,在master中还要记录一个workingWorkers集合和failedWorkers集合,可用worker的pid进行存储。
对于已经被标记为failed的worker又来get task,目前使用丢弃策略(因为它可能运行的太慢了),给它返回一个finish任务。
对于已经被标记为failed的worker来complete task,目前使用丢弃策略(因为它可能运行的太慢了,且它的任务可能已经分配给另一个worker了),给它返回一个Accept字段为false的CompleteReply
过程中的其他思考和bug
实现map task时,我最开始在想写文件需不需要锁保护,后来反应过来是不需要的,因为map task不会出现并发写的情况,因为对文件名为mr-X-Y的写入只有在执行X号map任务的worker才会向这个文件里面写,其他不会
map task向中间文件写入时,不要取一个KeyValue就打开一次对应文件写一次,会发生系统中socket过多的错误,即使每次写后关闭,频繁打开关闭会拖慢速度。改进:用map[string]*os.File记录所有打开了的文件的文件描述符,之后从这个map中取对应的文件描述符进行写入即可。
测试sh test-mr.sh时的坑
timeout命令
$ brew install coreutils
map parallism test 失败
我发现如果直接用sh test-mr.sh,map parallism test这个test就无法通过,但如果把这个test单独提出来测试就可以通过。所以我猜测时之前的测试产生的文件影响了它,可以看到map parallism test之前进行了rm -f mr-out* mr-worker*,所以猜测是mr-X-Y这样的中间文件产生的问题。
解决:
在每次执行map task之前,把这个map涉及到的所有mr-X-Y文件移除。
注
- 这个实验室依赖workers共享一个文件系统。当所有工作人员在同一台机器上运行时,这很直观,但如果workers在不同的机器上运行,则需要像GFS这样的全局文件系统。