6.824-Lab1 详细记录

可用工具

  • go 语言
    • goroutine
    • 信道 chan
    • waitgroup
    • mutex 互斥锁
  • MapReduce 计算模型

目标

实现一个分布式的 MapReduce 框架,这个框架由两个程序组成,分别是 coordinatorworker。在执行一个 MapReduce 任务时,只会有一个 coordinator 进程和多个 worker 进程。要求 coordinatorworker 之间通过 RPC 交流。workers 会通过 RPCcoordinator 请求任务,之后从本地文件读取任务的输入,执行任务,然后把任务的输出写入本地文件当中。如果一个 worker 没有在一定的时间内完成任务(10s),coordinator 应当注意到并且把相同的任务派发给另一个 worker
该框架的实现应当补充到 mr/coordinator.go mr/worker.gomr/rpc.go里。

框架设计

首先,先设计整体框架结构。根据给定的代码结构,该框架主要由两个组件构成,分别是 coordinatorworker 。其中 coordinator 负责任务的派发,协调各 worker 等。worker 负责执行请求到的任务,读取相关文件并将结果写入相应的文件当中。

RPC 设计

根据需求,可以总结出框架共需要两种 RPC 调用,分别是 请求任务 以及 任务完成通知

请求任务
任务完成通知
Coordinator
Worker

两种 RPC 调用的请求体和返回体分别需要包括的内容:

  • 请求任务
    • 请求体:
      • (无内容)
    • 返回体:
      • 任务类型(Map 或者 Reduce
      • 需要处理的文件名
      • 如果是 Map 任务,需要提供 Reduce 任务的数目以确定输出文件的名称(shuffle)。
      • 如果是 Map 任务,需要提供当前输入文件的索引,在任务完成时返回来方便 coordinator 管理。
      • 如果是 Reduce 任务,需要提供当前 Reduce 任务的索引来确定输出文件的名称。
  • 任务完成通知
    • 请求体:
      • 完成任务的类型(Map 或者 Reduce
      • 完成任务的索引
    • 返回体:
      • (无内容)

Coodinator

coordinator 需要负责:

  1. 派发未完成的任务,以及当任务超时时重新派发
  2. 接收任务完成的通知并记录,当所有 Map 任务完成时进行 Reduce 任务的派发。当所有 Reduce 任务完成时,在给定的 Done 方法中返回true

那么,coordinator 需要保存以下信息:

  • 待处理文件的文件名
  • map 任务状态集合,记录所有 map 任务的完成状态
  • reduce 任务状态集合,记录所有 reduce 任务的完成状态
  • map 阶段任务是否全部完成
  • reduce 阶段任务是否全部完成

派发任务的流程可以表示为:

Start
依次派发所有 map 任务
等待 map 阶段所有任务完成
依次派发所有 reduce 任务
End

任务处理超时重发的流程可以表示为:

N
Y
Start
派发任务
启用一个 goroutine
设置计时器10s
检查任务是否已经完成
重新派发任务
End

接收任务完成通知的流程可以表示为:

Y
N
Y
N
Start
收到任务完成通知
得到任务类型和任务索引
检查该任务是否已经完成
End
记录状态
检查当前阶段任务
是否全部完成
更改相应的标志位

Worker

worker 的职责是处理领取到的任务,并把处理结果写入相应的文件当中。worker 会得到 map 函数 和 reduce 函数的实现,领取到任务之后需要判断任务类型调用相应的函数,并写入相应的输出文件当中。
worker 的工作流程可以表示为:

map
reduce
Start
rpc 请求领取任务
判断任务类型
执行 map 函数
读取所有中间文件
聚合同 key 值的 value
利用 key 值计算中间结果对应的 reduce 任务序号
执行 reduce 函数
写入输出文件
rpc 通知任务完成
End

代码实现

RPC

根据 RPC 设计内容,编写以下 RPC 中间定义:

// Add your RPC definitions here.
// 请求任务
type GetTaskRequest struct{}

type GetTaskResponse struct {
	TaskType  int      // 任务类型 0-map 1-reduce
	Filenames []string // 待处理的文件名,有可能是输入文件也可能是中间文件

	// for map
	MapIndex  int // map 任务索引
	ReduceNum int // reduce 任务数目

	// for reduce
	ReduceIndex int // reduce 任务索引
}

// 任务完成通知
type NotifyTaskDoneRequest struct {
	TaskType  int // 任务类型 0-map 1-reduce
	TaskIndex int // 任务索引,可以是 map 任务的索引也可以是  reduce 任务的索引
}

type NotifyTaskDoneResponse struct{}

Coordinator

字段

根据设计内容,首先给 Coordinator 定义以下字段:

type Coordinator struct {
	// Your definitions here.
	filenames []string // 输入文件
	mapNum    int      // map 任务数量
	reduceNum int      // reduce 任务数量

	isMapPhaseDone    bool // map 阶段是否完成
	isReducePhaseDone bool // reduce 阶段是否完成

	// masks,记录任务完成状态
	mapDoneMask     []int // 0-not done 1-done
	reduceDoneMask  []int // 0-not done 1-done
	mapDoneCount    int   // map任务完成数量
	reduceDoneCount int   // reduce 任务完成数量

	// channels 事件
	mapPhaseDone chan struct{} // map阶段完成信号
	taskDispatch chan int      // 任务分配信号
	taskDone     chan int      // 任务完成信号

	// mutex
	lock sync.Mutex
}

任务分配

编写主要的任务分配线程,依次分配所有任务:

func (c *Coordinator) dispatchTasks() {
	for i := range c.filenames {
		c.taskDispatch <- i
	}

	<-c.mapPhaseDone // 等待 map 阶段完成

	for i := 0; i < c.reduceNum; i++ {
		c.taskDispatch <- i
	}
}

编写超时处理线程函数:

// 超时等待
func (c *Coordinator) timeout(taskType int, taskIndex int) {
	time.Sleep(10 * time.Second) // 10s
	var done bool
	c.lock.Lock()
	if taskType == 0 {
		done = c.mapDoneMask[taskIndex] == 1
	} else {
		done = c.reduceDoneMask[taskIndex] == 1
	}
	c.lock.Unlock()

	// 任务仍未完成
	if !done {
		c.taskDispatch <- taskIndex
	}
}

编写接收 RPC 请求任务函数,主要逻辑先等待是接收任务分配的信号,再根据信号内容进行分配:

func (c *Coordinator) GetTask(req *GetTaskRequest, resp *GetTaskResponse) error {
	// 等待接收派发任务信号
	taskIndex := <-c.taskDispatch
	if !c.isMapPhaseDone {
		// map 阶段
		resp.TaskType = 0
		resp.MapIndex = taskIndex
		resp.ReduceNum = c.reduceNum
		resp.Filenames = []string{c.filenames[taskIndex]}

		go c.timeout(0, taskIndex)
	} else {
		// reduce 阶段
		resp.TaskType = 1
		resp.ReduceIndex = taskIndex
		resp.Filenames = make([]string, 0, 10)

		// 遍历当前文件夹
		dir, err := ioutil.ReadDir(".")
		if err != nil {
			return err
		}

		for _, fi := range dir {
			if fi.IsDir() { // 目录, 跳过
				continue
			} else {
				// 过滤指定格式
				ok := strings.HasPrefix(fi.Name(), "intermedia") && strings.HasSuffix(fi.Name(), strconv.Itoa(taskIndex))
				if ok {
					resp.Filenames = append(resp.Filenames, fi.Name())
				}
			}
		}

		go c.timeout(1, taskIndex)
	}

	// log.Printf("dispacth task : type : %v, filenames: %v", resp.TaskType, resp.Filenames)

	return nil
}

任务完成通知

首先编写 RPC 任务完成通知函数:

func (c *Coordinator) NotifyTaskDone(req *NotifyTaskDoneRequest, resp *NotifyTaskDoneResponse) error {
	if req.TaskType == 0 && c.isMapPhaseDone {
		// map阶段的冗余通知忽略
		return nil
	}

	// log.Printf("task done: type: %v, taskIndex: %v", req.TaskType, req.TaskIndex)
	c.taskDone <- req.TaskIndex

	return nil
}

再编写处理任务完成信号的函数,主要逻辑是更新一系列状态变量:

func (c *Coordinator) updateTaskStatus() {
	for taskIndex := range c.taskDone {
		c.lock.Lock()
		if !c.isMapPhaseDone && c.mapDoneMask[taskIndex] == 0 {
			// 处理 map 任务
			c.mapDoneMask[taskIndex] = 1
			c.mapDoneCount++
			if c.mapDoneCount == c.mapNum {
				// 如果完成任务数等于总任务数,发出 map 阶段完成的通知,让任务分配线程分配所有的reduce任务
				c.isMapPhaseDone = true
				c.mapPhaseDone <- struct{}{}
			}
			// log.Printf("map mask: %v", c.mapDoneMask)
		} else if c.isMapPhaseDone && c.reduceDoneMask[taskIndex] == 0 {
			// 处理 reduce 任务
			c.reduceDoneMask[taskIndex] = 1
			c.reduceDoneCount++
			if c.reduceDoneCount == c.reduceNum {
				c.isReducePhaseDone = true

				// 删掉所有中间结果文件
				files, err := ioutil.ReadDir(".")
				if err != nil {
					log.Fatal("cannot open current dir")
				}
				for _, f := range files {
					if strings.HasPrefix(f.Name(), "intermedia") {
						if err := os.Remove(f.Name()); err != nil {
							log.Fatal("cannot remove intermedia files")
						}

					}
				}

			}
		}
		c.lock.Unlock()
	}
}

Done函数

lab要求实现这个函数,测试主函数会周期调用该函数查询任务完成状态:

// main/mrcoordinator.go calls Done() periodically to find out
// if the entire job has finished.
func (c *Coordinator) Done() bool {
	c.lock.Lock()
	res := c.isReducePhaseDone
	c.lock.Unlock()
	return res
}

Worker

worker 主要只有一个函数流程,需要注意的是 lab 要求 worker 持续向 coordinator 请求任务,而非完成一个任务之后就退出。

func run(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) bool {

	// Your worker implementation here.
	// 请求任务
	req := GetTaskRequest{}
	resp := GetTaskResponse{}

	if success := call("Coordinator.GetTask", &req, &resp); !success {
		// 如果 rpc 失败,就认定 coordinator 已退出,任务已完成,此时 map 也退出
		return false
	}

	if resp.TaskType == 0 {
		// Map
		filename := resp.Filenames[0]

		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()
		kva := mapf(filename, string(content))

		// 计算所有 key 的 hash 值,放入map中等待存储
		kvmap := make(map[int][]KeyValue)

		for _, kv := range kva {
			hash := ihash(kv.Key) % resp.ReduceNum
			v, ok := kvmap[hash]
			if !ok {
				kvmap[hash] = make([]KeyValue, 0)
			}

			kvmap[hash] = append(v, kv)
		}

		// 存储,根据计算出的 hash 值来确定输出到哪个文件
		for k, v := range kvmap {
			tempFile, err := os.CreateTemp(".", "temp-"+strconv.Itoa(resp.MapIndex)+"-")
			if err != nil {
				log.Fatalf("cannot create temp file %v", tempFile.Name())
			}
			enc := json.NewEncoder(tempFile)
			for _, kv := range v {
				err := enc.Encode(&kv)
				if err != nil {
					log.Fatalf("cannot write data %v", tempFile.Name())
				}
			}

			os.Rename(tempFile.Name(), "intermedia-"+strconv.Itoa(resp.MapIndex)+"-"+strconv.Itoa(k))
		}

		// 通知任务完成
		notifyReq := NotifyTaskDoneRequest{TaskType: 0, TaskIndex: resp.MapIndex}
		notifyResp := NotifyTaskDoneResponse{}
		call("Coordinator.NotifyTaskDone", &notifyReq, &notifyResp)
	} else {
		// Reduce
		kva := make([]KeyValue, 0)

		for _, filename := range resp.Filenames {
			file, err := os.Open(filename)
			if err != nil {
				log.Fatalf("cannot open file %v", filename)
			}
			dec := json.NewDecoder(file)

			for {
				var kv KeyValue
				if err := dec.Decode(&kv); err != nil {
					break
				}
				kva = append(kva, kv)
			}
		}

		sort.Sort(ByKey(kva))

		tempFile, err := os.CreateTemp(".", "temp-")
		if err != nil {
			log.Fatalf("cannot create temp file %v", tempFile.Name())
		}

		for i := 0; i < len(kva); {
			j := i + 1
			for j < len(kva) && kva[j].Key == kva[i].Key {
				j++
			}
			values := []string{}
			for k := i; k < j; k++ {
				values = append(values, kva[k].Value)
			}
			output := reducef(kva[i].Key, values)

			// this is the correct format for each line of Reduce output.
			fmt.Fprintf(tempFile, "%v %v\n", kva[i].Key, output)

			i = j
		}

		os.Rename(tempFile.Name(), "mr-out-"+strconv.Itoa(resp.ReduceIndex))
		notifyReq := NotifyTaskDoneRequest{TaskType: 1, TaskIndex: resp.ReduceIndex}
		notifyResp := NotifyTaskDoneResponse{}
		call("Coordinator.NotifyTaskDone", &notifyReq, &notifyResp)
	}
	return true
}

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

	for {
		if !run(mapf, reducef) {
			break
		}
	}
}

测试与总结

通过所有测试用例:
通过测试
总体来说非常简单,主要是了解一下 MapReduce 计算框架的原理,然后熟悉一下 go 语言的特性,为之后的 lab 做准备。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值