Mit6.824 Lab1 MapReduce实现

paper地址:http://nil.csail.mit.edu/6.824/2021/schedule.html

MapReduce 原理

  1. 启动MapReduce, 将输入文件切分成大小在16-64MB之间的文件。然后在一组多个机器上启动用户程序
  2. 其中一个副本将成为master, 余下成为worker. master给worker指定任务(M个map任务,R个reduce任务)。master选择空闲的worker给予map或reduce任务
  3. Map worker 接收切分后的input,执行Map函数,将结果缓存到内存
  4. 缓存后的中间结果会周期性的写到本地磁盘,并切分成R份(reducer数量)。R个文件的位置会发送给master, master转发给reducer
  5. Reduce worker 收到中间文件的位置信息,通过RPC读取。读取完先根据中间<k, v>排序,然后按照key分组、合并。
  6. Reduce worker在排序后的数据上迭代,将中间<k, v> 交给reduce 函数处理。最终结果写给对应的output文件(分片)
  7. 所有map和reduce任务结束后,master唤醒用户程序

MapReduce 实现流程

Master

论文提到每个(Map或者Reduce)Task有分为idle, in-progress, completed 三种状态。

// 枚举,表示任务执行阶段,根据论文,分为空闲、执行中、已完成
const (
	Idle MasterTaskStatus = iota
	InProgress
	Completed
)

Master 保存Task的信息

// Master记录的任务信息,包含任务执行阶段、任务开始时间,Task对象的指针
type MasterTask struct {
	TaskStatus    MasterTaskStatus // 任务执行阶段
	StartTime     time.Time        // 任务开始执行时间
	TaskReference *Task            // 表示当前执行的是哪个任务
}

Master存储Map任务产生的R个中间文件的信息。

// Master节点对象
type Master struct {
	TaskQueue     chan *Task          // 保存Task的队列,通过channel通道实现队列
	TaskMeta      map[int]*MasterTask // 当前系统所有task的信息,key为taskId
	MasterPhase   State               // Master阶段
	NReduce       int                 // R个Reduce工作线程
	InputFiles    []string            // 输入文件名
	Intermediates [][]string          // M行R列的二维数组,保存Map任务产生的M*R个中间文件
}

Map和Reduce使用同一个Task结构,完全可以兼顾两个阶段的任务。

// 任务对象
type Task struct {
	Input         string   // 任务负责处理的输入文件名
	TaskState     State    // 任务状态
	NReducer      int      // R个Reducer
	TaskNumber    int      // TaskId
	Intermediates []string // 保存Map任务产生的R个中间文件的磁盘路径
	Output        string   // 输出文件名
}

将task和master的状态合并成一个State

type State int
// 枚举,表示Master和Task的状态
const (
	Map State = iota  // 从0开始枚举
	Reduce
	Exit
	Wait
)

MapReduce执行Map和Reduce实现

1. 启动master

// create a Master.
// main/mrmaster.go calls this function.
// nReduce is the number of reduce tasks to use.
// 创建Master节点,负责分发任务,作为服务注册中心、服务调度中心
func MakeMaster(files []string, nReduce int) *Master {
	// 创建Master节点
	m := Master{
		// 保存task的队列,通过chan通道实现先进先出
		TaskQueue: make(chan *Task, max(nReduce, len(files))),
		// 主要作用是通过taskId这个key获取到对应的Task信息
		TaskMeta: make(map[int]*MasterTask),
		// 一开始Master和Task都处于Map阶段
		MasterPhase: Map,
		NReduce:     nReduce,
		InputFiles:  files,
		// 创建二维数组保存Map阶段生成的中间文件路径,设置列数为nReduce
		Intermediates: make([][]string, nReduce),
	}
	// TODO 将files中的文件切分成16MB-64MB的文件

	// 创建Map任务
	m.createMapTask()
	// 启动Master节点,将Master的方法都注册到注册中心,worker就可以通过RPC访问Master的方法
	m.server()
	// crash,启动一个协程来不断检查超时的任务
	go m.catchTimeOut()
	return &m
}

创建Map任务

// 创建Map任务
func (m *Master) createMapTask() {
	// 遍历所有的输入文件,每个文件用一个Map任务处理
	for idx, fileName := range m.InputFiles {
		// 创建Map Task对象
		taskMeta := Task{
			Input:      fileName,
			TaskState:  Map,
			NReducer:   m.NReduce,
			TaskNumber: idx,
		}
		// Task对象放入队列
		m.TaskQueue <- &taskMeta
		// 填充Master对当前队列中所有Task的信息, taskId为key,value保存task信息
		m.TaskMeta[idx] = &MasterTask{
			TaskStatus:    Idle,
			TaskReference: &taskMeta,
		}
	}
}

不断检查超时任务,提高执行效率

// crash,启动一个协程来不断检查超时的任务
func (m *Master) catchTimeOut() {
	for {
		time.Sleep(5 * time.Second)
		// 锁住其他线程可能会使用的m.MasterPhase
		mu.Lock()
		// Master节点的执行状态是退出状态,则退出检查
		if m.MasterPhase == Exit {
			mu.Unlock()
			return
		}
		// 检查所有任务
		for _, masterTask := range m.TaskMeta {
			// 任务执行中并且执行时间大于10秒,则重新放入队列等待被其他worker执行
			if masterTask.TaskStatus == InProgress && time.Now().Sub(masterTask.StartTime) > 10*time.Second {
				m.TaskQueue <- masterTask.TaskReference
				masterTask.TaskStatus = Idle
			}
		}
		mu.Unlock()
	}
}

2. master监听worker RPC调用,分配任务

// 等待worker通过rpc请求Master的服务
func (m *Master) AssignTask(args *ExampleArgs, reply *Task) error {
	// 锁住Master节点
	mu.Lock()
	defer mu.Unlock()
	// 队列里还有空闲任务
	if len(m.TaskQueue) > 0 {
		// taskQueue还有空闲的task就发出一个Task指针给一个worker
		*reply = *<-m.TaskQueue
		// 设置Task状态
		m.TaskMeta[reply.TaskNumber].TaskStatus = InProgress
		m.TaskMeta[reply.TaskNumber].StartTime = time.Now()
	} else if m.MasterPhase == Exit {
		// 队列里还有任务但是Master状态为Exit
		// 返回一个带着Exit状态的Task,表示Master已经终止服务了
		*reply = Task{
			TaskState: Exit,
		}
	} else {
		// 队列里没有任务,则让请求的worker等待
		*reply = Task{
			TaskState: Wait,
		}
	}
	return nil
}

3. 启动worker

// main/mrworker.go calls this function.
// 启动Worker
func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) {
	for {
		// 通过RPC获取空闲任务
		task := getTask()
		// 根据任务当前的执行状态进行相应处理
		switch task.TaskState {
		case Map:
			mapper(&task, mapf)
		case Reduce:
			reducer(&task, reducef)
		case Wait:
			time.Sleep(5 * time.Second)
		case Exit:
			return
		}
	}
}

4. worker向master发送RPC请求任务

// 通过RPC获取空闲任务
func getTask() Task {
	args := ExampleArgs{}
	reply := Task{}
	// RPC请求调用Master的服务来获取Task
	call("Master.AssignTask", &args, &reply)
	return reply
}

5. worker获得MapTask,交给mapper处理

// 执行Map任务
func mapper(task *Task, mapf func(string, string) []KeyValue) {
	// 获取任务对应的文件路径
	content, err := ioutil.ReadFile(task.Input)
	if err != nil {
		log.Fatal("Failed to read file: "+task.Input, err)
	}
	// 执行wc.go中的mapf方法,进行MapReduce的map阶段,得到nReduce个中间文件路径的字符串数组
	intermediates := mapf(task.Input, string(content))
	// 将map阶段生成的中间文件路径保存到列数为NReducer的二维数组中
	buffer := make([][]KeyValue, task.NReducer)
	// 保存结果到内存buffer中
	for _, intermediate := range intermediates {
		// 根据key进行hash,将结果切分成NReducer份
		slot := ihash(intermediate.Key) % task.NReducer
		buffer[slot] = append(buffer[slot], intermediate)
	}
	// 周期性地从内存保存到磁盘中
	mapOutput := make([]string, 0)
	for i := 0; i < task.NReducer; i++ {
		// 将中间结果写入到NReducer个中间临时文件中
		mapOutput = append(mapOutput, writeToLocalFile(task.TaskNumber, i, &buffer[i]))
	}
	// NReducer个文件的路径保存到内存,Master就可以获取到
	task.Intermediates = mapOutput
	// 设置该任务状态为已完成
	TaskCompleted(task)
}

6. worker任务完成后通知master

func TaskCompleted(task *Task) {
	reply := ExampleReply{}
	call("Master.TaskCompleted", task, &reply)
}

7. master收到完成后的Task

// 更新Task状态为已完成并检查
func (m *Master) TaskCompleted(task *Task, reply *ExampleReply) error {
	mu.Lock()
	defer mu.Unlock()
	// 容错、检查节点状态、检查重复任务
	if task.TaskState != m.MasterPhase || m.TaskMeta[task.TaskNumber].TaskStatus == Completed {
		// 重复任务要丢弃
		return nil
	}
	m.TaskMeta[task.TaskNumber].TaskStatus = Completed
	go m.processTaskResult(task)
	return nil
}
  • 如果所有的ReduceTask都已经完成,转入Exit阶段
// master通过协程获取任务执行的结果
func (m *Master) processTaskResult(task *Task) {
	mu.Lock()
	defer mu.Unlock()
	switch task.TaskState {
	case Map:
		// Map阶段则收集中间结果到Master内存中
		// key为taskId,value为文件路径的字符串数组,一个task有NReducer个filePath
		for reduceTaskId, filePath := range task.Intermediates {
			m.Intermediates[reduceTaskId] = append(m.Intermediates[reduceTaskId], filePath)
		}
		// 所有任务已完成则进入reduce阶段
		if m.allTaskDone() {
			m.createReduceTask()
			m.MasterPhase = Reduce
		}
	case Reduce:
		// Reduce则设置状态为Exit
		if m.allTaskDone() {
			m.MasterPhase = Exit
		}
	}
}

8. 如果所有的MapTask都已经完成,创建ReduceTask,转入Reduce阶段

// 执行Reduce任务
func reducer(task *Task, reducef func(string, []string) string) {
	// 从磁盘中读取中间文件
	intermediate := *readFromLocalFile(task.Intermediates)
	// 根据key进行字典序排序
	sort.Sort(ByKey(intermediate))

	dir, _ := os.Getwd()
	tempFile, err := ioutil.TempFile(dir, "mr-2021-tmp-*")
	if err != nil {
		log.Fatal("Failed to create temp file", err)
	}
	i := 0
	// 遍历每一个key
	for i < len(intermediate) {
		j := i + 1
		// 相同的key分组合并
		for j < len(intermediate) && intermediate[i].Key == intermediate[j].Key {
			j++
		}
		// 保存该key的最终计数, 即对相同key的计数进行合并统计
		values := []string{}
		for k := i; k < j; k++ {
			values = append(values, intermediate[k].Value)
		}
		// 结果交给reducef进行统计
		output := reducef(intermediate[i].Key, values)
		// 最终结果的字符串内容保存到临时文件里
		fmt.Fprintf(tempFile, "%v %v\n", intermediate[i].Key, output)
		i = j
	}
	tempFile.Close()
	// 定义输出文件的文件名
	oname := fmt.Sprintf("mr-2021-out-%d", task.TaskNumber)
	os.Rename(tempFile.Name(), oname)
	task.Output = oname
	TaskCompleted(task)
}

9. master确认所有ReduceTask都已经完成,转入Exit阶段,终止所有master和worker goroutine

//
// main/mrmaster.go calls Done() periodically to find out
// if the entire job has finished.
//
func (m *Master) Done() bool {
	mu.Lock()
	defer mu.Unlock()
	ret := m.MasterPhase == Exit
	return ret
}
  1. 并发

因为Master保存Task相关的信息,因此在worker执行任务时,是需要对Master进行并发修改的,所以需要进行上锁。master跟多个worker通信,master的数据是共享的。

// Master节点对象
type Master struct {
	TaskQueue     chan *Task          // 保存Task的队列,通过channel通道实现队列
	TaskMeta      map[int]*MasterTask // 当前系统所有task的信息,key为taskId
	MasterPhase   State               // Master阶段
	NReduce       int                 // R个Reduce工作线程
	InputFiles    []string            // 输入文件名
	Intermediates [][]string          // M行R列的二维数组,保存Map任务产生的M*R个中间文件
}

其中TaskMeta, Phase, Intermediates, TaskQueue 都有读写发生。TaskQueue使用channel实现,自己带锁。只有涉及Intermediates, TaskMeta, Phase的操作需要上锁,InputFiles 和 NReduce 因为是在创建Master时一次性写入,所以不会出现并发写的场景。

11.容错

  1. 周期性向worker发送心跳检测
  • 如果worker失联一段时间,master将worker标记成failed
  • worker失效之后,已完成的map task被重新标记为idle,已完成的reduce task不需要改变
  1. 对于in-progress 且超时的任务,则重新放入队列等待被其他worker执行
// crash,启动一个协程来不断检查超时的任务
func (m *Master) catchTimeOut() {
	for {
		time.Sleep(5 * time.Second)
		// 锁住其他线程可能会使用的m.MasterPhase
		mu.Lock()
		// Master节点的执行状态是退出状态,则退出检查
		if m.MasterPhase == Exit {
			mu.Unlock()
			return
		}
		// 检查所有任务
		for _, masterTask := range m.TaskMeta {
			// 任务执行中并且执行时间大于10秒,则重新放入队列等待被其他worker执行
			if masterTask.TaskStatus == InProgress && time.Now().Sub(masterTask.StartTime) > 10*time.Second {
				m.TaskQueue <- masterTask.TaskReference
				masterTask.TaskStatus = Idle
			}
		}
		mu.Unlock()
	}
}
posted @ 2022-03-05 17:35  JavaJayV  阅读(139)  评论(0)    收藏  举报