MIT 6.824分布式 LAB1: MapReduce

从2020年开始,实验变为使用go语言,因此写这个实验的时候使用go语言仅仅只是临时学的,写的过程中犯了不少语法错误,这也让我走了好多弯路。。。下面我也会列出这些错误,不过真的很低级。。

由于本人是go语言的初学者,代码也完全都是自己凭感觉去写,代码可能比较丑陋,大伙儿看看就好。

介绍

lab1其实就是让你利用MapReduce的原理去实现数单词的程序。

MapReduce也是谷歌的大数据三篇重要论文之一,网上已经有大佬专门翻译了这三篇论文。大家觉得英文版看的比较吃力,就去看中文版就好。

go plugin

go plugin支持将go包编译为动态链接库(.so)的形式单独发布,主程序可以在运行时动态加载这些库文件

go build -buildmode=plugin就是把一个包编译成动态链接库,程序仅需在执行的时候再加载即可。

race检测

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()
}
​

执行test-mr.sh结果展示

 

MIT 6.824分布式 LAB1: MapReduce_第1张图片

你可能感兴趣的:(mapreduce,go)