Lab1:第一天

96 阅读4分钟

拿到新项目第一步:按照教程看看能不能运行成功,然后分析跟步骤有关的代码

$ cd ~/6.824
$ cd src/main
$ go build -buildmode=plugin ../mrapps/wc.go
$ rm mr-out*
$ go run mrsequential.go wc.so pg*.txt
$ more mr-out-0

// go build -buildmode=plugin ../mrapps/wc.go这段会执行构建一个插件wc.so以便mrsequential.go调用。
// go run mrsequential.go wc.so pg*.txt表示编译并运行mrsequential.go。其中wc.so是插件,pg*是输入数据集。
// more mr-out-0可以查看输出数据集,其中mr-out-0即输出数据。

分析所涉及到的文件:

// wc.go

// map程序
// map程序对于每一个输入文件会执行一次
// 第一个参数是输入文件的名称,第二个参数是输入文件的完整内容
// 输出是key/value对的切片集合(相当于数组,数组里每一个对象是key/value对)
func Map(filename string, contents string) []mr.KeyValue {
    // 定义一个方法,判断输入r是否是一个字母字符(可以用来切分文章中的单词遇到空格返回false)
    // 输入是一个rune的数据类型(等同于int32,常用来处理unicode或utf-8字符)
    // 返回一个布尔值,r是字母字符则返回false,否则返回true
    ff := func(r rune) bool { return !unicode.IsLetter(r) }
    
    // 将文章内容切分成单词数组
    // 例如"hello world"就会切分成["hello" "world"]
    // FieldsFunc是一个可以按照自定义规则切分字符的函数
    words := strings.FieldsFunc(contents, ff)
    
    // 生成一个key/value对集合,其中key是文章中的单词,value都为1
    kva := []mr.KeyValue{}
    for _, w := range words {
        kv := mr.KeyValue{w, "1"}
        kva = append(kva, kv)
    }
    return kva
}
// reduce程序
// 所有map任务生成的中间结果集会作为reduce的入参被执行一次
// 入参key是单词,values是key出现次数的集合,values中每一个的值都为1,所以values的大小就是单词key出现的次数
// 可参考下图:
// Input1 -> Map -> a,1 b,1 c,1
// Input2 -> Map ->     b,1
// Input3 -> Map -> a,1     c,1
//                  |   |   |
//                  |   |   -> Reduce -> c,2
//                  |   -----> Reduce -> b,2
//                  ---------> Reduce -> a,2
func Reduce(key string, values []string) string {
    // strconv函数将int转化为string返回 
    return strconv.Itoa(len(values))
}

// mrsequential.go

// ByKey是用于将中间结果集进行排序用的
// 排序后key相同的就会排在一起,就方便作为reduce的入参
type ByKey []mr.KeyValue

func (a ByKey) Len() int           { return len(a) }
func (a ByKey) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key }

func main() {
    
    // 运行时的参数个数若少于3则报错并退出程序
    // 在go run mrsequential.go ../mrapps/wc.so pg*.txt中
    // 参数0:mrsequential.go
    // 参数1:wc.so
    // 参数2:pg*.txt(这里其实输入文件算是多个参数)
    if len(os.Args) < 3 {
        fmt.Fprintf(os.Stderr, "Usage: mrsequential ../mrapps/xxx.so inputfiles...\n")
	    os.Exit(1)
	}

    // --------------------------map任务开始--------------------------
    // 从参数1(wc.so)中读取map程序和reduce程序
    mapf, reducef := loadPlugin(os.Args[1])
    
    // 读取每一个文件作为map程序的入参,并输出中间结果
    intermediate := []mr.KeyValue{}
    for _, filename := range os.Args[2:] {
        // 打开文件
        file, err := os.Open(filename)
        if err != nil {
            log.Fatalf("cannot open %v", filename)
        }
        // 读取文件内容
        content, err := ioutil.ReadAll(file)
        if err != nil {
            log.Fatalf("cannot read %v", filename)
        }
        file.Close()
        // 输入map程序
        kva := mapf(filename, string(content))
        // 中间结果
        intermediate = append(intermediate, kva...)
    }
    // --------------------------map任务结束--------------------------

    // 对中间结果进行排序
    sort.Sort(ByKey(intermediate))
    
    // 创建输出文件mr-out-0
    oname := "mr-out-0"
    ofile, _ := os.Create(oname)
    
    // --------------------------reduce任务开始--------------------------
    // 由于中间结果集是有序的,所以相同的key/value对会连续放置在一起
    // 只需要将key相同的中间结果集作为reduce程序的输入即可
    i := 0
    for i < len(intermediate) {
        // i表示key相同的单词的第一个的位置
        // j表示key相同的单词的最后一个的后一位
        j := i + 1
        for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
            j++
        }
        values := []string{}
        // 遍历从i到j之间的key/value对,全部都是同样的key并且value都为1
        // 并作为reduce的入参
        for k := i; k < j; k++ {
            values = append(values, intermediate[k].Value)
        }
        output := reducef(intermediate[i].Key, values)

        // 输出reduce的结果到mr-out-0文件中
        fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)

        i = j
	}
    // --------------------------reduce任务结束--------------------------

    ofile.Close()
}

// 加载插件中的方法
// 入参插件文件名
// 返回值为map方法和reduce方法
func loadPlugin(filename string) (func(string, string) []mr.KeyValue, func(string, []string) string) {
    p, err := plugin.Open(filename)
    if err != nil {
        log.Fatalf("cannot load plugin %v", filename)
    }
    // 查找插件中Map方法,并赋给mapf
    xmapf, err := p.Lookup("Map")
    if err != nil {
        log.Fatalf("cannot find Map in %v", filename)
    }
    mapf := xmapf.(func(string, string) []mr.KeyValue)
    
    // 查找插件中的Reduce方法并赋给reducef
    xreducef, err := p.Lookup("Reduce")
    if err != nil {
        log.Fatalf("cannot find Reduce in %v", filename)
    }
    reducef := xreducef.(func(string, []string) string)
    
    //返回map和reduce方法
    return mapf, reducef
}




第二步:梳理文件结构

src 
├─mrapps :定义了Map和Reduce两个函数,而不必关心后面的任务(分布式or单机式)
│   ├─wc.go   
│   ├─indexer.go
│   └─...
├─main
│   ├─mrsequential.go : 单机式的MapReduce实现
│   ├─mrcoordinator.go :coordinator启动程序
|   ├─mrworker.go :      worker启动程序
|   ├─pg*.txt
|   └─...
├─mr
|   ├─coordinator.go  : coordinator的具体实现
|   ├─worker.go       :worker的具体实现
|   └─rpc.go          :规定了 RPC 通信中需要传递的变量等信息
  • mrapps:包括一些已经编写好map和reduce程序的示例代码,如wc.go是计数程序,indexer.go是一个文本索引器程序
  • main:包括一个串行调度mapreduce的程序mrsequential.go以及一些输入数据pg*.go
  • mr:我们包括了mster.go、worker.go、rpc.go,是我们需要实现的并行版本的mapreduce调度器

第三步:根据课程要求初步写出相应的模块功能:

image.png

未解之谜:

nReduce是干什么的?