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结果展示

 

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MIT 6.824 课程的 Lab1 是关于 Map 的实现,这里单介绍一下实现过程。 MapReduce 是一种布式计算模型,它可以用来处理大规模数据集。MapReduce 的核心想是将数据划分为多个块,每个块都可以在不同的节点上并行处理,然后将结果合并在一起。 在 Lab1 中,我们需要实现 MapReduce 的基本功能,包括 Map 函数、Reduce 函数、分区函数、排序函数以及对作业的整体控制等。 首先,我们需要实现 Map 函数。Map 函数会读取输入文件,并将其解析成一系列键值对。对于每个键值对,Map 函数会将其传递给用户定义的 Map 函数,生成一些新的键值对。这些新的键值对会被分派到不同的 Reduce 任务中,进行进一步的处理。 接着,我们需要实现 Reduce 函数。Reduce 函数接收到所有具有相同键的键值对,并将它们合并成一个结果。Reduce 函数将结果写入输出文件。 然后,我们需要实现分区函数和排序函数。分区函数将 Map 函数生成的键值对映射到不同的 Reduce 任务中。排序函数将键值对按键进行排序,确保同一键的所有值都被传递给同一个 Reduce 任务。 最后,我们需要实现整个作业的控制逻辑。这包括读取输入文件、调用 Map 函数、分区、排序、调用 Reduce 函数以及写入输出文件。 Lab1 的实现可以使用 Go 语言、Python 或者其他编程语言。我们可以使用本地文件系统或者分布式文件系统(比如 HDFS)来存储输入和输出文件。 总体来说,Lab1 是一个比较简单的 MapReduce 实现,但它奠定了 MapReduce 的基础,为后续的 Lab 提供了良好的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值