【Mit6.824】Lab1 MapReduce

Lab1 MapReduce

MapReduce 简介

MapReduce的思想是,应用程序设计人员和分布式运算的使用者,只需要写简单的Map函数和Reduce函数,而不需要知道任何有关分布式的事情,MapReduce框架会处理剩下的事情。
Map函数使用一个key和一个value作为参数。入参中,key是输入文件的名字,value是输入文件的内容。
Reduce函数的入参是某个特定key的所有实例(Map输出中的key-value对中,出现了一次特定的key就可以算作一个实例)。所以Reduce函数也是使用一个key和一个value作为参数,其中value是一个数组,里面每一个元素是Map函数输出的key的一个实例的value。

MapReduce实现

Coordinator

mr/coodinator.go
Coordinator充当一个协调者或者master的角色,主要负责将任务分配给worker
Coordinator 主要维护了两个任务列表,Maper任务和Reducer任务,以及两种任务的完成状态,我们来看一下Coordinator的结构体:

type Coordinator struct {
	// Your definitions here.
	mu              sync.Mutex
	MapperFinished  bool           // mapper是否全部完成
	ReducerFinished bool           // reducer是否全部完成
	Mappers         []*MapperTask  // mapper任务
	Reducers        []*ReducerTask // reducer任务
}

其中MapperTask和ReducerTask都维护了可以完整代表一个map任务和reduce任务的属性:

type MapperTask struct {
	Index        int       // 任务编号
	Assigned     bool      // 是否分配
	AssignedTime time.Time // 分配时间
	IsFinished   bool      // 是否完成

	InputFile    string // 输入文件
	ReducerCount int    // 有多少路reducer

	timeoutTimer *time.Timer // 任务超时
}

type ReducerTask struct {
	Index        int       // 任务编号
	Assigned     bool      // 是否分配
	AssignedTime time.Time // 分配时间
	IsFinished   bool      // 是否完成

	MapperCount int //	有多少路mapper

	timeoutTimer *time.Timer // 任务超时
}

Coodinator要暴露出两个方法给worker通过rpc进行调用,分别是FetchTask()和UpdateTask()

Go RPC 的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:

  1. 函数必须是导出的 (首字母大写)
  2. 必须有两个导出类型的参数
  3. 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
  4. 函数还要有一个返回值 error
    正确的 RPC 函数格式如下:
    func (t *T) MethodName(argType T1, replyType *T2) error

FetchTask为worker分配任务,当worker通过rpc调用FetchTask,coordinator会检查map任务是否全部完成,若没完成便分配map任务,若完成则同理检查reduce任务。

func (c *Coordinator) FetchTask(request *FetchTaskRequest, response *FetchTaskResponse) (err error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if !c.MapperFinished {   //如果mapper没完成
		for _, mapper := range c.Mappers {   //遍历mappers任务列表
			if mapper.Assigned || mapper.IsFinished {
				continue
			}
			c.startMapper(mapper)
			task := *mapper // 副本
			response.MapperTask = &task
			return
		}
		return // 所有mapper任务都分配出去了,那么暂时没有工作了
	}
	if !c.ReducerFinished {   
		for _, reducer := range c.Reducers {
			if reducer.Assigned || reducer.IsFinished {
				continue
			}
			c.startReducer(reducer)
			task := *reducer
			response.ReducerTask = &task
			return
		}
		return // 所有reducer任务都分配出去了,那么暂时没有工作了
	}
	response.AllFinished = true
	return
}

UpdateTask主要是来更新任务状态的,拿到worker的请求后,如果是Mapper任务,则遍历coordinator的Mapper任务列表,找到此mapper任务,然后更改任务状态;reducer任务同理。
注意:每次修改一个任务的状态后要检查对应类型的任务是否全部完成

func (c *Coordinator) UpdateTask(request *UpdateTaskRequest, response *UpdateTaskResponse) (err error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if request.Mapper != nil {
		MapperFinished := true
		for _, mapper := range c.Mappers {
			if mapper.Index == request.Mapper.Index && mapper.Assigned && !mapper.IsFinished {
				c.finishMapper(mapper)
			}
			MapperFinished = MapperFinished && mapper.IsFinished
		}
		c.MapperFinished = MapperFinished   //检查mapper任务是否全部完成
	}
	if request.Reducer != nil {
		ReducerFinished := true
		for _, reducer := range c.Reducers {
			if reducer.Index == request.Reducer.Index && reducer.Assigned && !reducer.IsFinished {
				c.finishReducer(reducer)
			}
			ReducerFinished = ReducerFinished && reducer.IsFinished
		}
		c.ReducerFinished = ReducerFinished   //检查reducer任务是否完成
	}
	return
}

Worker

mr/worker.go
worker 的逻辑相对简单一些主要就是不断轮训做任务即可,遇到Map任务调用doMapperTask(),遇到Reduce任务就调用doReduceTask().

func Worker(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) {
	for {
		resp := CallFetchTask()
		if resp == nil {
			continue
		}
		if resp.AllFinished {
			return
		}
		if resp.MapperTask != nil {
			// 做mapper的事情
			doMapperTask(resp.MapperTask, mapf)
		}
		if resp.ReducerTask != nil {
			// 做reducer的事情
			doReducerTask(resp.ReducerTask, reducef)
		}
	}
}

doMapperTask():
在doMapperTask()方法中,进行Mapper的shuffle,经过Mapper后,输出的是Key/Value形式的,将这个键值对的key做hash%nReduce,每个mapper输出nReduce个中间文件来存储这些键值对。

func doMapperTask(mapperTask *MapperTask, mapf func(string, string) []KeyValue) {
	file, err := os.Open(mapperTask.InputFile)
	if err != nil {
		log.Fatalf("cannot open %v", mapperTask.InputFile)
	}
	content, err := ioutil.ReadAll(file)
	if err != nil {
		log.Fatalf("cannot read %v", mapperTask.InputFile)
	}
	file.Close()
	kva := mapf(mapperTask.InputFile, string(content))

	// mapper的shuffle
	// 10个reducer
	// 每1个mapper输出10路文件,对key做hash%10
	reducerKvArr := make([][]KeyValue, mapperTask.ReducerCount)

	for _, kv := range kva {
		reducerNum := ihash(kv.Key) % mapperTask.ReducerCount
		reducerKvArr[reducerNum] = append(reducerKvArr[reducerNum], kv)
	}

	for i, kvs := range reducerKvArr {
		sort.Sort(ByKey(kvs))

		filename := fmt.Sprintf("mr-%d-%d", mapperTask.Index, i)
		file, err := os.Create(filename + ".tmp")
		if err != nil {
			log.Fatalf("cannot write %v", filename+".tmp")
		}
		enc := json.NewEncoder(file)
		for _, kv := range kvs {
			err := enc.Encode(&kv)
			if err != nil {
				log.Fatalf("cannot jsonencode %v", filename+".tmp")
			}
		}
		file.Close()
		os.Rename(filename+".tmp", filename)
	}

	CallUpdateTaskForMapper(mapperTask)
}

doReducerTask():
在doReducerTask()方法中,reducer拿到自己的中间文件后,将自己对应的所有中间文件中的键值对全部取出后通过key进行排列,程序遍历排序后的中间数据,对于每一个唯一的中间 key 值,Reduce程序将这个 key 值和它相关的中间 value 值的集合传递给用户自定义的 Reduce 函数。Reduce 函数的输出被追加到所属分区的输出文件。

func doReducerTask(reducerTask *ReducerTask, reducef func(string, []string) string) {
	kvs := make([]KeyValue, 0)
	for i := 0; i < reducerTask.MapperCount; i++ {
		filename := fmt.Sprintf("mr-%d-%d", i, reducerTask.Index)
		fp, err := os.Open(filename)
		if err != nil {
			log.Fatalf("cannot open %v", filename)
		}
		dec := json.NewDecoder(fp)
		for {
			var kv KeyValue
			if err := dec.Decode(&kv); err != nil {
				break
			}
			kvs = append(kvs, kv)
		}
	}

	sort.Sort(ByKey(kvs)) // 按key排序的k-v列表

	ofileName := fmt.Sprintf("mr-out-%d", reducerTask.Index)
	ofile, err := os.Create(ofileName + ".tmp")
	if err != nil {
		log.Fatalf("cannot open %v", ofileName+".tmp")
	}

	// [i,j]
	i := 0
	for i < len(kvs) {
		j := i + 1
		for j < len(kvs) && kvs[j].Key == kvs[i].Key {
			j++
		}
		values := []string{}
		for k := i; k < j; k++ {
			values = append(values, kvs[k].Value)
		}
		output := reducef(kvs[i].Key, values)
		fmt.Fprintf(ofile, "%v %v\n", kvs[i].Key, output)
		i = j
	}
	ofile.Close()
	os.Rename(ofileName+".tmp", ofileName)
	CallUpdateTaskForReducer(reducerTask)
}
  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值