MIT-6.824分布式系统lab1-MapReduce

2,670 阅读6分钟

前言

作为学习分布式系统的一门课程,MIT6.824一直都是国内外质量上乘的课程之一,知乎上很多经验丰富的软件开发人员也都是力推这门课程,而它的好处就是在于教授的授课是结合学生课前阅读论文、上课讲述思路以及课后的lab共同组成,会很大程度上的让学生独立地从0到1去实现一个编程模型,而不是仅仅使用各个厂家的框架。

那么我跟随的课程是2020春季学期的这门课程,课程地址为:pdos.csail.mit.edu/6.824/sched…

  • 注:其实Go很好上手,我实习的时候学了4天,然后下一周就进了项目,不会的童鞋可以参考Go语言之旅这个简短的教程来熟悉Go,地址是tour.go-zh.org/list

MapReduce原理

如果你还没有读过MapReduce的论文,那么最好还是看一下,这样能够很快速的理解它的思想,我在这里就先总结一下。

首先,MapReduce是一个能够处理和生成大量数据集的分布式的编程模型,它的结论就是通过给程序开发人员提供mapreduce两个函数来分别实现产生(k,v)中间结果和将相同key的数据进行汇总归并的操作,这样mapreduce两个函数其实就可以在不同的机器上进行运行,从而提高我们计算的效率。

在论文中,最重要的部分也就是下面这张图:

这张图里面有几个比较重要的部分:

  • Master进程

    它是用来分配任务给worker进程的角色,他来决定worker执行的是map函数还是reduce函数

  • worker进程

    如果worker进程执行的是map函数,那么它就要读取已经分片了的输入文件,处理文件内的每一行,然后将文件转换成(k,v)这样形式的中间文件;而如果它被指定为要执行reduce函数,那么worker就需要去读取中间文件(这个过程是通过RPC实现的),然后对中间文件的key先做排序,然后根据排序的结果做归并的处理。

除此之外,就是还要留意一下中间文件的个数,这个个数是由用户程序指定的,然后怎么选择用哪个reduce程序执行是通过公式hash(key) mod R进行选择。


实验

课程给出了相应的例子,具体的代码路径是src\main\mrsequential.go,这个例子给出的是非分布式版本的,而我们需要实现的是分布式版本的,具体来说我们需要完成src\mr下面的master.go, worker.gorpc.go三个程序,

首先,我从worker来开始着手写,每个worker都需要通过rpcmaster进行通信来申请一个任务(具体是map还是reduce任务是由master来决定的)

//
// main/mrworker.go calls this function.
//
func Worker(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) {

	// Your worker implementation here.

	// uncomment to send the Example RPC to the master.
	// CallExample()

}

这是作业中的Worker函数,我们也是需要在这个函数里实现我们的worker函数,那么思路就是:

  • master请求任务
  • 针对返回来的任务类型,做不同的处理
    • 返回任务类型是map:调用Map函数
    • 返回任务类型是reduce:调用Reduce函数
    • 还有两种特殊情况,就是任务等待和任务终止,分别延时一段时间和直接返回
    • 默认情况抛异常

那么代码写出来就是这样的

//
// main/mrworker.go calls this function.
//
func Worker(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) {

	// Your worker implementation here.

	// uncomment to send the Example RPC to the master.
	// CallExample()
    for {
        //请求任务(rpc调用)
        taskInfo := CallTask()
        switch taskInfo.State {
            case 0:
            	Map(mapf, )
            	break;
            case 1:
            	Reduce(reducef, )
            	break;
            case 2:
            	time.sleep(time.Duration(time.Second * 10))
            	break;
        	case 3:
            	fmt.Println(" all of tasks have completed, nothing to do...")
            	return
            default:
            	panic("Invaild State of Worker, Please try again....")
        }
    }
}

注意到这里面我们用到了CallTask()这个函数用来向master请求任务,在这个函数中我们需要调用提供给我们的call()函数,最后返回了一个taskInfo对象,根据对象中的State决定怎么样处理,那么我们在worker.go中先要实现CallTask()方法:

func CallTask() *TaskInfo {
    args := ExampleArgs{}
    reply := TaskInfo{}
    //通过rpc调用Master的RequireTask方法
    call("Master.RequireTask", &args, &reply)
    return &reply
}

func call(rpcname string, args interface{}, reply interface{}) bool {
	// c, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
    //masterSock()方法是在rpc.go中,返回的是UNIX域下socket的名字
	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
}

除了完成这两个函数,我们还需要定义一下taskInfo的内容,即TaskInfo类,写在rpc.go中:

type TaskInfo struct {
    /*** state value 
    * 0 --> map
      1 --> reduce
      2 --> wait
      3 --> nothing to do
    */
    State        int
    //要读取的文件名
    FileName     string
    //经过map后输出到哪个file中-->针对map
    FileIdx      int 
    //要写到哪个文件-->针对reduce
    OutFileIdx   int
    //分成几个reduce
    ReduceNum    int
    FileNum      int
}

完成了上面步骤之后,现在就需要回到Worker方法中,然后我们需要进入到switch中,看到我们还有两个函数没有实现,即MapReduce:对于前者而言,输入的参数除了一个mapf函数之外,还需要把任务信息传递进来,因为任务信息包含了你要读入文件的名称和index

func Map(mapf func(string, string) []KeyValue, taskInfo *TaskInfo) {
    //根据文件名读文件,并将调用mapf之后输出的kv保存在中间数组中
    interFile := []KeyValue
    file, err := os.Open(taskInfo.FileName)
    if err != nil {
        panic(err)
    }
    content, err := ioutil.ReadAll(file)
    if err != nil {
        panic(err)
    }
    file.Close()
    kvs := mapf(taskInfo.FileName, string(content))
    interFile = append(interFile, kvs...)
    
    //准备输出文件
    outPrefix = "mr-tmp/mr-"
    outPrefix += strconv.Itoa(taskInfo.FileIdx)
    outFiles := make([]*os.File, taskInfo.ReduceNum)
    outFilesEncode := make([]*json.Encoder, taskInfo.ReduceNum)
    for idx := 0; idx < taskInfo.ReduceNum; idx++ {
        //在mr-tmp文件夹下生成空的临时文件并设置编码
        outFiles[idx], _ = ioutil.TempFile("mr-tmp", "mr-tmp-*")
        outFilesEncode[idx] = json.NewEncoder(outFiles[idx])
    }
    
    //依据不同的key路由存储到到不同的文件中去
    for _, kv := range interFile {
        outputIdx := ihash(kv.Key) % taskInfo.ReduceNum
        file = outFiles[outputIdx]
    }
    
    //保存文件
    for idx, file := range outFiles {
        outName := outPrefix + strconv.Itoa(idx)
        oldPath := filepath.Join(file.Name())
        os.Rename(oldPath, outname)
        file.Close()
    }
    
    //通知master我map做完了
    TaskDone(taskInfo)
}
func Reduce(reducef func(string, []string) string, taskInfo *TaskInfo) {
    outName := "mr-out-" + strconv.Itoa(taskInfo.OutFileIdx)
    interPrefix := "mr-tmp/mr-"
    interSuffix := "-" + strconv.Itoa(taskInfo.OutFileIdx)
    
    interFile := []KeyValue{}
    for idx := 0; idx < taskInfo.FileNum; idx++ {
        inname := interPrefix + strconv.Itoa(idx) + interSuffix
		file, err := os.Open(inname)
		if err != nil {
			fmt.Printf("Open intermediate file %v failed: %v\n", inname, err)
			panic("Open file error")
		}
		dec := json.NewDecoder(file)
		for {
			var kv KeyValue
			if err := dec.Decode(&kv); err != nil {
				break
			}
			
			interFile = append(interFile, kv)
		}
		file.Close()
    }
    
    sort.Sort(ByKey(interFile))
    
    ofile, err := ioutil.TempFile("mr-tmp", "mr-*")
	if err != nil {
		fmt.Printf("Create output file %v failed: %v\n", outname, err)
		panic("Create file error")
	}
	
	i := 0
	for i < len(intermediate) {
		j := i + 1
		for j < len(interFile) && interFile[j].Key == interFile[i].Key {
			j++
		}
		values := []string{}
		for k := i; k < j; k++ {
			values = append(values, interFile[k].Value)
		}
		output := reducef(interFile[i].Key, values)

		fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)

		i = j
	}
	os.Rename(filepath.Join(ofile.Name()), outName)
	ofile.Close()
	// acknowledge master
	TaskDone(taskInfo)
} 

这样,我们就完成了worker.go的内容,现在我们需要考虑master.go的结构。

首先我们都知道每个任务都包含的信息都已经写在了rpc.go/TaskInfo中,但是在master中,我们得管理每一个任务,因此我们就需要有一个类来保存任务的状态:

type TaskStat struct{
    fileName       string
    startTime      time.Time
    fileIndex      int
    outFileIndex int
    reduceNum      int
    fileNum        int
}

然后还要定义两个类继承这个任务信息类

type MapTask struct {
    TaskStat
} 

type ReduceTask struct {
    TaskStat
}

有了对应的实现类以后我们还需要定义对外的接口,以提供获取任务信息的功能

type TaskInfoInterface interface {
    GenerateTask()      TaskInfo
    OutOfTime()         bool
    GetFileIndex()      int
    GetOutFileIndex()   int
    SetTime()
}

所有的任务类都要实现上面的方法

func (this *MapTask) GenerateTaskInfo() TaskInfo {
    return TaskInfo {
        State: 0
        FileName:   this.fileName
        FileIdx:    this.fileIndex
        OutFileIdx: this.outFileIndex
        ReduceNum:  this.reduceNum
        FileNum:    this.fileNum
    }
}

func (this *ReduceTask) GenerateTaskInfo() TaskInfo {
    return TaskInfo {
        State: 1
        FileName:   this.fileName
        FileIdx:    this.fileIndex
        OutFileIdx: this.outFileIndex
        ReduceNum:  this.reduceNum
        FileNum:    this.fileNum
    }
}

func (this *TaskStat) OutOfTime() bool {
    return time.Now().Sub(this.startTime) > time.Duration(time, Second * 60)
}

func (this *TaskStat) GetFileIndex() int {
    return this.fileIndex
}

func (this *TaskStat) GetOutFileIndex int {
    return this.outFileIndex
}

func (this *TaskStat) SetTime() {
    this.startTime = time.Now()
}

除此之外我们定义一下master类和两个由worker请求的方法:

type Master struct {
    fileNames []string
    
    mapRunningTaskChannel chan TaskStat
    mapWaitingTaskChannel chan TaskStat
    reduceRunningTaskChannel chan TaskStat
    reduceWaitingTaskChannel chan TaskStat
    
    isDone bool
    reduceNum int
}

func (this *Master) RequireTask(args *ExampleArgs, reply *TaskInfo) error {
    if this.isDone {
        reply.State = 3
        return nil
    }
    
    mapTask := <- mapWaitingTaskChannel
        
    if mapTask != nil {
        mapTask.setTime()
        mapRunningTaskChannel <- mapTask
        *reply = mapTask.GenerateTaskInfo()
        return nil
    }

    reduceTask := <- reduceWaitingTaskChannel
    if reduceTask != nil {
        reduceTask.setTime()
        reduceRunningTaskChannel <- reduceTask
        *reply = reduceTask.GenerateTaskInfo()
        return nil
    }

    if len(this.mapRunningTaskChannel) > 0 || len(this.reduceRunningTaskChannel) > 0 {
        reply.State = 2
        return nil
    }

    replr.State = 3
    this.isDone = true
    return nil
   
}

func (this *Master) TaskDone(args *TaskInfo, reple *ExampleReply)  error {
    switch args.State {
        case 0:
            mapTask := <- mapRunningTaskChannel
            if len(this.mapRunningTaskChannel) == 0 && len(this.mapWaitingTaskChannel) == 0 {
                this.distributeReduce()
            }
            break
    	case 1:
        	reduceTask := <- reduceRunningTaskChannel
        	break
        default:
        	pannic("task error")
    }
    return nil
}

func (this *Master) distributeReduce() {
	reduceTask := ReduceTaskStat{
		TaskStat{
			fileIndex: 0,
			outFileIndex: 0,
			ReduceNum:   this.ReduceNum,
			fileNum:    len(this.filenames),
		},
	}
	for reduceIndex := 0; reduceIndex < this.nReduce; reduceIndex++ {
		task := reduceTask
		task.partIndex = reduceIndex
		reduceWaitingTaskChannel <- task
	}
}

那么到此为止呢,实验的大部分内容我也都写完了,主要思路也是借鉴了zhuanlan.zhihu.com/p/260752052…


参考连接:

zhuanlan.zhihu.com/p/260752052

zhuanlan.zhihu.com/p/54243727