mit6.824 lab1 MapReduce

1. MapReduce的基本步骤

在这里插入图片描述

当用户程序调用MapReduce函数,下面的一系列操作发生:(根据MapReduce原论文)

  1. 在用户程序中的MapReduce库首先将输入文件分割成M片(一般每片16M至64M)。然后开启这个程序在机器集群中的许多副本(也就是在多个机器上用Map或者Reduce代码,执行不同的数据)。
  2. 在所有程序中,有一个master,剩下的是被master分配的worker。有M个Map任务和R个Reduce任务需要分配,master选择空闲的worker并且分配每一个节点一个Map任务或者Reduce任务。(在实现中,Map和Reduce都可以抽象为Worker,也就是说Worker机、既可以做Map也可以做Reduce)
  3. 被分配Map任务的Worker读取了相关的被分割的输入内容。它将会从输入数据中解析出key/value pair,然后将key/value pair传给用户定义的Map函数。由Map函数生产的中间值key/value pair将会被缓存在内存中。
  4. 每隔一段时间,被缓存的键值对将会被写到本地磁盘中,由切分函数分割成R个区域。缓存在本地磁盘的位置将会传给master,master将会负责传递这些位置给Reduce Worker。
  5. 当一个Reduce Worker被master通知这些缓存键值对的位置,Reduce Worker将会使用RPC从Map Worker的本地磁盘中读取缓存数据。当一个Reduce Worker已经读取了所有中间值数据,它将会根据中间值键值来进行排序从而所有相同键值的出现都会被分到同一个组中。
  6. Reduce Worker迭代排序好的中间值数据,对于每个遇到的唯一的中间值键,它将会将键和它相关的中间值集合传递给用户定义的Reduce Function。Reduce Function的输出将会追加到最终的输出文件
  7. 当所有的Map任务和Reduce任务已被完成,master将会唤起用户程序。

2. MapReduce实现

2.1 Coordinator

在mit8.624中,master被称为Coordinator。

2.1.1 Coordinator的抽象

根据MapReduce论文中的描述,Coordinator的需要存储的属性主要有:

  • 记录一些基本的属性,比如:任务数量(输入文件数量),分区数量(reduce数量)。也包括为了检测超时,设定的超时时间。
  • 记录当前任务进度。Coordinator需要一种数据结构记录当前Map任务有哪些待处理、有哪些正在处理和有哪些已经处理;对于Reduce任务也应有相应的数据结构记录。
  • 记录当前所处于的阶段。需要一个标志属性,来记录当前是处于pending, mapping, reducing还是done阶段。
  • worker的状态。由于需要对worker进行监控,所以需要对worker的状态进行存储。
  • 对并发读写的控制所需要的锁。
//
// Coordinator is a base struct for MapReduces' master
// nReduce determines the intermediate keys' region by "iHash(key) % nReduce"
// step is representing the step: INPUTTING, MAPPING, REDUCING or DONE
// pendingMaps/Reduces, workingMaps/Reduces and doneMaps/Reduces are the linked list for the same status Task
// taskNum is the total sum of inputting Task
// outputSet is the Map worker output file path set. Every Reduce worker will iterate the output set to resume all intermediate files
// workerStatus is a map which represents the worker status: IDLE, DIE, MAPPING or REDUCING
// mux is a mutex to sync the parallelism thread to update the Coordinator struct properties
//
type Coordinator struct {
	nReduce        int
	step           int
	timeLimit      int64
  pendingMaps    TaskList  // TaskList是自定义的一个链表结构体
	workingMaps    TaskList
	doneMaps       TaskList
	pendingReduces TaskList
	workingReduces TaskList
	doneReduces    TaskList

	taskNum   int
	outputSet map[string]struct{}

	workerStatus WorkerStatus  // WorkerStatus是一个自定义的map结构体

	mux sync.RWMutex
}

Coordinator的职责主要有:

  • 给worker分配map或者reduce任务。
  • 更新任务状态。当一个worker完成一个任务后,通过RPC更新任务的状态(pending->working, working->done)
  • 心跳机制。与worker进行Heartbeat,如果失败,worker需要自行销毁。
  • 监测运行阶段。需要根据记录map任务或者reduce任务的数据结构状态,来更新整体的运行阶段,比如:如果map任务完成数量与总任务数量相同,则运行阶段更新为reduce; 如果reduce任务完成数量与nReduce相同,则更新运行阶段为done。
  • 监测任务运行状态。每隔几秒需要对正在工作的任务进行监测,如果该任务运行时间超过了设定的超时时间,则对该任务进行重新分配,并销毁该worker。

抽象为接口如下:

type Coordinator interface {
  // workers RPC这个函数从而获取Map或者Reduce任务
	Get(args *GetTaskArgs, reply *GetTaskReply) error
  // workers RPC这个函数从而更新任务状态,比如:working->done
  Update(args UpdateTaskStatusArgs, reply *UpdateTaskStatusReply) error
  // worker RPC这个函数进行HeartBeat
  HeartBeat(args HeartBeatArgs, reply *HeartBeatReply) error
  // 定时调用Done()来监测任务有没有结束
  Done() bool
  // 查看需要不需要更新coordinator.step,通过检查len(doneMap) == taskNum或者len(doneReduce) == nReduce
  Check(args DoneArgs, reply *DoneReply) error
  // 监测是否有任务超时,或者有无worker长时间未heartbeat。如果有,需要对任务进行重新调度。
  Monitor()
}

2.1.3 Coordinator实现

主要列举重要的一个链表数据结构和三个基本的函数。

存储任务的数据结构:

type Task struct {
	Id         int  // 任务的Id
	FilePath   []string  // 任务的inputPath,对于map只有一个,对于reduce有多个
	OutputPath string  // map/reduce 的输出文件地址
	TimeStamp  int64  // 如果处于运行状态,则为任务的开始时间
	WorkerId   string  // 哪一个worker正在处理该任务
}

记录任务进度所用到的数据结构TaskList:

type TaskList struct {
	mux  sync.Mutex  // 并发控制
	next *TaskNode
  num  int  // 链表中有多少个结点,主要用在Check()监测任务阶段
}

type TaskNode struct {
	next *TaskNode
	task *Task
}

func (t *TaskList) insert(task *Task) {  // 插入一个结点
	t.mux.Lock()  // 一定需要上锁
	defer t.mux.Unlock()
	node := &TaskNode{
		task: task,
	}
	node.next = t.next
	t.next = node
	t.num++
}

func (t *TaskList) pop() *Task {  // pop出第一个结点
	t.mux.Lock()  // 一定需要上锁
	defer t.mux.Unlock()
	if t.next == nil {
		return nil
	}
	head := t.next.task
	t.next = t.next.next
	t.num--
	return head
}

func (t *TaskList) delete(id int) error {  // 删除Id为id的Task结点
	t.mux.Lock()  // 一定需要上锁
	defer t.mux.Unlock()
	if t.num == 0 {
		return errors.New("id not found")
	}
	if t.next.task.Id == id {
		t.next = t.next.next
		return nil
	}
	pre := t.next
	for i := pre.next; i != nil; i = i.next {
		if i.task.Id == id {
			pre.next = i.next
			t.num--
			return nil
		}
		pre = i
	}
	return errors.New("id not found")
}

分配给worker Map或者Reduce任务的Get函数

type GetTaskArgs struct {
	WorkerId string  // Worker的Id
}

type GetTaskReply struct {
	WorkType      int  // 表明返回的是Map还是Reduce
	HoldTask      bool
	GetMapTask    *Task
	GetReduceTask *Task
	TimeStamp     int64
	NRegion       int
}

func (c *Coordinator) Get(args *GetTaskArgs, reply *GetTaskReply) error {
	if c.step == PENDING {
		return nil
	}

	if c.step == MAPPING {
		reply.WorkType = TYPE_MAP
		task := c.pendingMaps.pop()
		if task == nil {
			return nil
		}
		reply.HoldTask = true  // 告诉worker reply中含有task
		reply.GetMapTask = task
		reply.NRegion = c.nReduce
		c.workingMaps.insert(task)
		c.mux.Lock()  // 由于map不是线程安全的,需要上锁
		c.workerStatus.workerMap[args.WorkerId] = worker{MAPPING, time.Now().Unix()} // 对worker状态进行更新
		c.mux.Unlock()
		fmt.Printf("distribute a Map task %d for a worker\n", task.Id)
	} else {
		reply.WorkType = TYPE_REDUCE
		task := c.pendingReduces.pop()
		if task == nil {
			return nil
		}
		reply.HoldTask = true
		reply.GetReduceTask = task
		reply.NRegion = c.nReduce
		c.workingReduces.insert(task)
		c.mux.Lock()
		c.workerStatus.workerMap[args.WorkerId] = worker{REDUCING, time.Now().Unix()}
		c.mux.Unlock()
		fmt.Printf("distribute a Reduce task %d for a worker\n", task.Id)

	}
	return nil
}

更新任务阶段函数Update()

type UpdateTaskStatusArgs struct {
	WorkerId string
	WorkType int
	Task     *Task
}

type UpdateTaskStatusReply struct {
	Action string
}

func (c *Coordinator) Update(args UpdateTaskStatusArgs, reply *UpdateTaskStatusReply) error {
	workType := args.WorkType
	worker, ok := c.workerStatus.workerMap[args.WorkerId]  // 获取worker的状态
	if ok && (worker.status == IDLE || worker.status == DIE) { // 如果worker为DIE、IDLE却来更新状态,则是致命的逻辑错误
		reply.Action = "fatal error"
		return nil
	} else if !ok {
		reply.Action = "fatal error"
		return nil
	}
	if workType == TYPE_MAP { // 如果是Map任务更新,则是将task从workingMap删除,插入到doneMap
		if err := c.workingMaps.delete(args.Task.Id); err != nil { 
			return err
		}
		c.doneMaps.insert(args.Task)
		worker.status = IDLE
		c.outputSet[args.Task.OutputPath] = struct{}{} // 向outputSet添加输出文件路径
		fmt.Printf("update a %d Map task\n", args.Task.Id)
	} else if workType == TYPE_REDUCE {
		if err := c.workingReduces.delete(args.Task.Id); err != nil {
			return err
		}
		c.doneReduces.insert(args.Task)
		worker.status = IDLE
		fmt.Printf("update a %d Reduce task\n", args.Task.Id)
	}
	go c.Check(DoneArgs{}, &DoneReply{}) // check if Map/Reduce tasks have done
	return nil
}

监测coordinator.step函数

func (c *Coordinator) Check(args DoneArgs, reply *DoneReply) error {
	var err error
	fmt.Printf("docheck: step: %d, taskNum: %d, mapNUm: %d, reduce: %d \n", c.step, c.taskNum, c.doneMaps.num, c.doneReduces.num)
	if c.step == MAPPING && c.taskNum == c.doneMaps.num {
		//如果阶段为MAPPING,且doneMaps数量等于taskNum则说明阶段应更新为Reduce
		outputPath := make([]string, 0)
		for path, _ := range c.outputSet { // 构造reduce任务的输入文件路径
			outputPath = append(outputPath, path)
		}
		for i := 0; i < c.nReduce; i++ { // 向pendingRedceus中插入任务
			c.pendingReduces.insert(&Task{
				Id:         i,
				FilePath:   outputPath,
				OutputPath: "",
			})
		}
		c.mux.Lock()
		c.step = REDUCING
		c.doneMaps.next = nil
		c.doneMaps.num = 0
		c.mux.Unlock()
	}
	if c.step == REDUCING && c.nReduce == c.doneReduces.num {
		// 如果阶段为REDUCEING,且nReduce数量等于doneReduces则说明任务已经结束
		c.mux.Lock()
		c.step = DONE
		c.mux.Unlock()
	}
	if c.step == DONE {
		err = errors.New("entire jobs have been finished\n")
	}
	return err
}

2.2 Worker

在mit8.624实验中,将mapping worker与reduce worker都抽象成worker。

2.2.1 Worker 抽象

根据MapReduce原论文并结合lab的代码框架,Worker的职能主要有以下:

  • 定期通过RPC向Coordinator获取任务。
  • 执行用户定义的doMap或者doReduce函数
  • 成功执行doMap或者doReduce后,更新task的状态
  • 与Coordinator进行HeartBeat

2.2.2 Worker实现

func Worker(mapFunc func(string, string) []KeyValue,
	reduceFunc func(string, []string) string) {
	workerId := fmt.Sprintf("%08v", rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(100000000))
	var wg sync.WaitGroup
	outputPath := ""
	// 本来计划通过context.WithCancel对子groutine进行销毁,但是没想好怎么正确的做
  // 可以去除掉相关的逻辑
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

  wg.Add(1)  // 从而通过wg.Wait()进行阻塞等待
	go func(ctx context.Context, outputPath string) {
		defer wg.Done()
		for {
			select {
			case <-ctx.Done():
				return
			default:
				getTask := AskForTask(workerId)
				if getTask.HoldTask {
					if getTask.WorkType == TYPE_MAP {
            // 进行封装好的doMap
						if err := doMap(mapFunc, getTask.GetMapTask, outputPath, getTask.NRegion); err != nil {
							fmt.Printf("worker %s encounter a fatal error in doMap: %v", workerId, err)
							return
						}
						fmt.Printf("%s worker: %d Map task has finished\n", workerId, getTask.GetMapTask.Id)
            // 更新task状态
						reply := UpdateTaskStatus(workerId, getTask.WorkType, getTask.GetMapTask)
						if reply.Action == "fatal error" {
							fmt.Printf("worker %s encounter a fatal error in update map task status", workerId)
							return
						}
					} else {
						if err := doReduce(reduceFunc, getTask.GetReduceTask); err != nil {
							fmt.Println("doReduce error: ", err)
							return
						}
						fmt.Printf("%s worker: %d Reduce task has finished\n", workerId, getTask.GetReduceTask.Id)
						reply := UpdateTaskStatus(workerId, getTask.WorkType, getTask.GetReduceTask)
						if reply.Action == "fatal error" {
							fmt.Printf("worker %s encounter a fatal error in update reduce task status", workerId)
							return
						}
					}
				}
				time.Sleep(1 * time.Second)
			}
		}
	}(ctx, outputPath)
	go func() {
		// heartbeat
		defer wg.Done()  // 如果doWork goroutine和heartBeat的任意一个先结束了,则整个Worker将会结束
		for {
			time.Sleep(time.Second * 5)
			arg := &HeartBeatArgs{workerId, time.Now().Unix()}
			reply := &HeartBeatReply{}
			ret := call("Coordinator.HeartBeat", arg, reply)
			if !ret {  // @todo 之后应该加上容错措施,比如如果两次不成功才会退出
				break
			}
		}
	}()
	wg.Wait()
}

3 不足

  1. 代码写的不够简洁与规范
  2. 只是实现了单机的MapReduce,还没有完成正真意义上的MapReduce
  3. 当一个Worker超时时,只能当该Worker的任务结束后,才会被销毁。而不是监测到超时,会立刻销毁。这个不足,可以通过HeartBeat改进,如果发现超时,Coordinator不会给该Worker返回HeartBeat.

4. 参考

MIT6.824-Lab1

相关的所有代码可以在: gitee仓库RT_Enzyme中查看,不过还是建议自己独立完成

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值