从2020年开始,实验变为使用go语言,因此写这个实验的时候使用go语言仅仅只是临时学的,写的过程中犯了不少语法错误,这也让我走了好多弯路。。。下面我也会列出这些错误,不过真的很低级。。
由于本人是go语言的初学者,代码也完全都是自己凭感觉去写,代码可能比较丑陋,大伙儿看看就好。
lab1其实就是让你利用MapReduce的原理去实现数单词的程序。
MapReduce也是谷歌的大数据三篇重要论文之一,网上已经有大佬专门翻译了这三篇论文。大家觉得英文版看的比较吃力,就去看中文版就好。
go plugin支持将go包编译为动态链接库(.so)的形式单独发布,主程序可以在运行时动态加载这些库文件
go build -buildmode=plugin就是把一个包编译成动态链接库,程序仅需在执行的时候再加载即可。
go程序中的多个goroutine在共享变量之间进行操作的时候,多个goroutine在同时操作这个变量的时候,如果不加锁的话,大概率会发生竞争导致程序执行错误。但是当我们在写一个庞大的项目的时候,很可能会发生忘记给共享变量加锁了,同时这种竞争带来的错误很难被注意到,大部分时间不会发生,因此很难发现。但是go语言的race检测,可以动态分析代码,帮助你找出会发生竞争的地方,十分方便。
注意这个检测是动态分析代码实现的,因此go run -race执行完程序没有提醒你并不表明这个程序就是正确的,有可能是本次执行的过程中没有执行到有问题的代码。
实现一个分布式的MapReduce,包含两个程序:coordinator和worker。前者就只有一个对应,后者可以有多个并行执行。本次实验是在一台机子上进行的。
coordinator和worker之间通过rpc来进行沟通。由于都是在一台机子上进行的,所以rpc的交互通过unix domain socket来进行ipc即可。
worker
worker:从coordinator那儿接受任务(map或者reduce)。
如果是Map的话,就是读取指定的文件中的单词,然后经过map处理写到指定的mr-X-Y文件中。X为worker的序号,Y为单词经过哈希后的intermediate的序号。如果完成了该项任务就报告给coordinator
如果是Reduce的话,就是读取指定的intermediate的序号Y,然后读取mr-*-Y的文件,然后经过reduce处理后汇总写到mr-out-Y中。如果完成了该项任务就报告给coordinator
coordinator
coordinator:负责给worker派发任务。并需要开启一个计时器,如果一个任务派发出去10秒内没完成,就把这个任务再派发给其他人去。仅有所有的map任务完成后才能进行reduce任务。
1、 map阶段需要把intermediate的key经过hash分配到nReduce个桶去。其实就是Y=hash(key) % nReduce,然后根据这个Y将这个intermediate写到指定的文件中去,如果这个worker的序号为X,那么这个指定的文件名为mr-X-Y。因此每个worker在处理一个文件的map的时候,都会将这些key分配到nReduce个桶去,也就是会产生nReduce个文件来写intermediate。
2、 reduce阶段,worker被分配到解决Y号的intermediate,那么就读取mr-*-Y的文件,然后进行reduce处理,将结果写到mr-out-Y中。
3、mr-out-Y文件中应该是%v %v格式的,每行一个key value
4、worker进行map产生的mr-X-Y应该生成在当前目录下,毕竟后续reduce就又要去读mr-X-Y文件。
5、/mr/coordinator.go中应当实现Done()函数,这个函数就是检查MapReduce工作是否完成,如果完成就退出。
6、 如果任务都完成了,worker应当退出。
本实验仅需修改/mr目录下的三个文件coordinator.go、rpc.go、worker.go即可,其余文件不用动。
定义Coordinator的数据结构
/mr/coordinator.go
type Coordinator struct { // Your definitions here. MapFile chan string //每次都从管道中取出一个文件给worker File2Num map[string]int //给每个文件取一个id MapOver map[int]bool //记录文件的Map是否完成 ReduceId chan int //每次都从管道中取出一个id给worker,worker根据这个id去选取指定的中间文件来进行reduce ReduceOver map[int]bool //记录中间文件的reduce是否完成 MapNum int //对应的就是进行map的文件数量 ReduceNum int //对应的就是需要进行reduce的中间文件的数量 CheckReduce sync.Mutex //确保ReduceOver变量被同步互斥使用 CheckMap sync.Mutex //确保MapOver变量被同步互斥使用 }
RPC过程中使用到的数据结构,我都将放到rpc.go文件中
定义Task,任务的数据结构
worker将从coordinator那儿接受到这样的任务
/mr/rpc.go
type Task struct { Operation string //这个任务是map还是reduce FileName string //如果是map任务,那么指定的是哪个文件;reduce任务无需考虑该变量 Empty bool //是否结束没任务了 Number int //该任务对应的序号 NReduce int }
定义InformMessage,通知信息的数据结构
coordinator将从worker那儿收到任务完成的通知信息
/mr/rpc.go
type InformMessage struct { Operation string //map还是reduce Id int //如果是Map的话,内容为文件名;Reduce的话,内容为完成的intermediate的编号 }
Coordinator.go
MakeCoordinator
mrcoordinator.go调用MakeCoordinator函数返回一个Coordinator对象,因此这个函数主要就是进行各种初始化,需要注意一点就是:go里面管道和map这些引用类型都需要make初始化才能用
// // create a Coordinator. // main/mrcoordinator.go calls this function. // nReduce is the number of reduce tasks to use. // func MakeCoordinator(files []string, nReduce int) *Coordinator { // Your code here. c := Coordinator{} c.MapNum = len(files) c.ReduceNum = nReduce c.MapFile = make(chan string, c.MapNum) c.ReduceId = make(chan int, nReduce) c.MapOver = make(map[int]bool) c.ReduceOver = make(map[int]bool) c.File2Num = make(map[string]int) for index, file := range files { c.MapFile <- file //依次将文件存放到MapFile管道中 c.File2Num[file] = index //给每个文件名分配一个id来对应 c.MapOver[index] = false //初始阶段没有一个文件是map完成的 } c.server() return &c }
GetTask
这个函数用于给worker调用,给worker分配一个任务
// Your code here -- RPC handlers for the worker to call. func (c *Coordinator) GetTask(args int, reply *Task) error { //这个函数仅仅只是获取任务而已,因此无需参数,这个args参数仅仅只是rpc函数语法需要加上的 fileName, ok := <-c.MapFile //从MapFile中取出一个需要worker去map的文件名 //注意:MapFile是管道,如果Map阶段没有结束,但是MapFile中没有文件名了,表明任务分完了 //coordinator正在等待worker完成map进入reduce阶段,那个时候MapFile被关闭了,就不会被堵塞在这儿了 if ok { reply.Empty = false reply.Operation = "map" reply.FileName = fileName reply.Number = c.File2Num[fileName] reply.NReduce = c.ReduceNum //每次分配任务的同时,开启一个goroutine来计时,如果10秒后,发现这个任务还没完成就将这个任务重新放回到分配列表MapFile中去,后续分配给其他worker go c.CheckTimeOut("map", fileName) return nil } // 执行到这里表明Map任务已经完成,MapFile管道已经被关闭 id, ok := <-c.ReduceId if ok { reply.NReduce = c.ReduceNum reply.Empty = false reply.Operation = "reduce" reply.Number = id //每次分配任务的同时,开启一个goroutine来计时,如果10秒后,发现这个任务还没完成就将这个任务重新放回到分配列表MapFile中去,后续分配给其他worker go c.CheckTimeOut("reduce", id) return nil } else { //执行到这儿表明reduce也完成了,ReduceId管道也被关闭了,仅需返回一个空的任务通知没事了 reply.Empty = true } return nil }
Inform
这个函数用于给worker调用,通知coordinator分配的任务完成了
func (c *Coordinator) Inform(arg *InformMessage, reply *int) error { //这个函数无需任何返回值,这个reply参数仅仅只是为了满足rpc函数的语法需要设置的 if arg.Operation == "map" { c.CheckMap.Lock() // 如果该任务已经完成了,后续再来通知完成就无视 if c.MapOver[arg.Id] == true { c.CheckMap.Unlock() return nil } c.MapOver[arg.Id] = true // fmt.Printf("map %d is over\n", arg.Id) for _, val := range c.MapOver { //遍历MapOver,查看是否有任务还是false,未完成的 if !val { c.CheckMap.Unlock() return nil } } //执行到这里表明map任务已经全部完成,可以关闭MapFile管道 // fmt.Println("------------reduce---------------") close(c.MapFile) for i := 0; i < c.ReduceNum; i++ { //由于reduce任务有ReduceNum个,初始化reduce任务的序号:0-(ReduceNum-1),每次分配任务就从管道中拿出一个序号 c.ReduceId <- i //初始化每个reduce任务初始都是false,未完成 c.ReduceOver[i] = false } c.CheckMap.Unlock() } else if arg.Operation == "reduce" { c.CheckReduce.Lock() // 如果该任务已经完成了,后续再来通知完成就无视 if c.ReduceOver[arg.Id] == true { c.CheckReduce.Unlock() return nil } c.ReduceOver[arg.Id] = true // fmt.Printf("reduce %d is over\n", arg.Id) for _, val := range c.ReduceOver { if !val { c.CheckReduce.Unlock() return nil } } //执行到这里表明reduce任务也完成了,也可以关闭ReduceId管道 close(c.ReduceId) c.CheckReduce.Unlock() } return nil }
CheckTimeOut
这个函数用于检查任务是否超时,如果超时则将这个任务重新放回到分配管道去,让这个任务分配给新的worker去做
func (c *Coordinator) CheckTimeOut(op string, mes interface{}) { time.Sleep(10 * time.Second) if op == "map" { //map任务的话,传来的mes参数是文件名 fileName := mes.(string) id := c.File2Num[fileName] c.CheckMap.Lock() if !c.MapOver[id] { c.MapFile <- fileName } c.CheckMap.Unlock() } else if op == "reduce" { //reduce任务的话,传来的mes参数是reduce任务的序号 id := mes.(int) c.CheckReduce.Lock() if !c.ReduceOver[id] { c.ReduceId <- id } c.CheckReduce.Unlock() } }
Done
这个函数会被周期调用,用于检查MapReduce是否完成。
原理也十分简单,就是每次都检查Map任务是否全部完成,以及Reduce任务是否全部完成
// // main/mrcoordinator.go calls Done() periodically to find out // if the entire job has finished. // func (c *Coordinator) Done() bool { // Your code here. ret := true c.CheckMap.Lock() //检查Map任务 for _, val := range c.MapOver { if !val { c.CheckMap.Unlock() return false } } c.CheckMap.Unlock() //检查Reduce任务 // fmt.Println("check reduce work") c.CheckReduce.Lock() for _, val := range c.ReduceOver { if !val { c.CheckReduce.Unlock() return false } } c.CheckReduce.Unlock() return ret }
worker.go
Worker
执行map和reduce任务的函数
这个里面需要注意的一点就是,写文件需要具备原子性,由于可能发生往同一个文件写的冲突。实验提示说,使用ioutil.TempFile来创建一个临时文件,这个文件仅当前进程可见,其他worker看不见,仅在数据写完后,使用os.Rename将这个临时文件变成目标文件即可实现写文件的原子性。
以及文件的写和读都是使用json包来实现的
//以下的这部分是参考mrsequential.go type KeyValue struct { Key string Value string } type ArrKey []KeyValue func (a ArrKey) Len() int { return len(a) } func (a ArrKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ArrKey) Less(i, j int) bool { return a[i].Key < a[j].Key } // // main/mrworker.go calls this function. // func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) { // Your worker implementation here. var replyTemp int//这个变量无意义,仅仅符合rpc函数的语法,用于给Coordinator.Inform函数当做reply参数的 for { var task Task //获取任务 ok := call("Coordinator.GetTask", 0, &task) if !ok { log.Fatal("call Coordinator.GetTask failed") } // fmt.Println(task) if task.Empty { //如果返回的任务是空的,表明已经结束了 // fmt.Println("no task left, exit") return } else { // fmt.Println(task) if task.Operation == "map" { // fmt.Printf("map%d start!\n", task.Number) file, err := os.Open(task.FileName) if err != nil { log.Fatalf("can't open %v", task.FileName) } content, err := ioutil.ReadAll(file) if err != nil { log.Fatalf("can't read %v", task.FileName) } kva := mapf(task.FileName, string(content)) var tmpFileList []*os.File for i := 0; i < task.NReduce; i++ { //创建NReduce个临时文件,生成的intermediate需要划分到NReduce个临时文件去 tmpFile, err := ioutil.TempFile("", "") if err != nil { log.Fatal("create tmpFile error") } defer tmpFile.Close() tmpFileList = append(tmpFileList, tmpFile) } for _, val := range kva { //使用ihash函数来获取key的哈希值,并用取余的方式将这些单词划分到NReduce个桶中去 reduceId := ihash(val.Key) % task.NReduce //使用json包来进行写文件 enc := json.NewEncoder(tmpFileList[reduceId]) err = enc.Encode(&val) if err != nil { log.Fatalf("can't write to tmpFile") } } for index, temp := range tmpFileList { //处理结束,数据也都已经写到临时文件中了,最后就是将临时文件变成我们当前目录下的mr-X-Y格式的文件 var finalName = fmt.Sprintf("./mr-%d-%d", task.Number, index) os.Rename(temp.Name(), finalName) } // fmt.Printf("map%d work over!\n", task.Number) var args = InformMessage{Operation: "map", Id: task.Number} // fmt.Println(args) //通知coordinator任务完成了 ok := call("Coordinator.Inform", &args, &replyTemp) if !ok { log.Fatal("call Coordinator.Inform fail") } } else if task.Operation == "reduce" { // fmt.Printf("reduce%d work start!\n", task.Number) //调用shell来筛选出当前目录下指定的intermediate序号的文件 var command = fmt.Sprintf("ls mr-*-%d", task.Number) in := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil) cmd := exec.Command("sh") cmd.Stdin = in cmd.Stdout = out in.WriteString(command) err := cmd.Run() if err != nil { log.Fatal("filter file error", err) } outs := out.String() //这里就是把文件划分开来,放到切片中去 fileList := strings.Split(outs, "\n") fileList = fileList[0 : len(fileList)-1] tmpFile, err := ioutil.TempFile("", "") if err != nil { log.Fatal("create tmpFile error") } intermediate := []KeyValue{} for _, fileName := range fileList { reduceFile, err := os.Open(fileName) if err != nil { log.Fatal("can't open file") } doc := json.NewDecoder(reduceFile) for { var kv KeyValue if err = doc.Decode(&kv); err != nil { break } intermediate = append(intermediate, kv) } } //这里参考mrsequential.go即可 sort.Sort(ArrKey(intermediate)) var finalName = fmt.Sprintf("./mr-out-%d", task.Number) i := 0 for i < len(intermediate) { j := i + 1 for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key { j++ } values := []string{} for k := i; k < j; k++ { values = append(values, intermediate[k].Value) } output := reducef(intermediate[i].Key, values) fmt.Fprintf(tmpFile, "%v %v\n", intermediate[i].Key, output) i = j } os.Rename(tmpFile.Name(), finalName) // fmt.Printf("reduce%d work over!\n", task.Number) var args = InformMessage{Operation: "reduce", Id: task.Number} ok := call("Coordinator.Inform", &args, &replyTemp) if !ok { log.Fatal("call Coordinator.Inform fail") } } } } // uncomment to send the Example RPC to the coordinator. // CallExample() }