MIT 6.824 Distributed System 学习笔记 - Lab1 MapReduce 记录

500 阅读2分钟

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

解决

  1. 6.824目录下,运行
$ go mod init 6.824-golabs-2020
  1. 全局搜索把代码中所有../mr修改为 6.824-golabs-2020/src/mr

初步理解我们需要做什么

main目录下的mrmaster.gomrworker.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函数中:

    1. 检查命令中是否指定了输入文件,没指定则报错退出

    2. 调用m := mr.MakeMaster(os.Args[1:], 10),第一个参数是所有输入文件名组成的string切片,第二个参数是nReduce,返回值是*Master

      MakeMaster函数和Master类型都定义在mr/master.go中,我们需要实现/补充MakeMastertype Master

    3. 每一秒轮询一次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 server

      func 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.KeyValue

    func Reduce(key string, values []string) string

  • main函数中:

    1. 检查命令中是否指定了插件文件,没指定则报错退出
    2. 把插件文件中定义的Map函数和Reduce函数加载出来,分别为mapf, reducef
    3. 调用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"。

注:上述6set可以用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.TempFile to create a temporary file and os.Rename to 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这样的全局文件系统。