1. MapReduce的基本步骤
当用户程序调用MapReduce函数,下面的一系列操作发生:(根据MapReduce原论文)
- 在用户程序中的MapReduce库首先将输入文件分割成M片(一般每片16M至64M)。然后开启这个程序在机器集群中的许多副本(也就是在多个机器上用Map或者Reduce代码,执行不同的数据)。
- 在所有程序中,有一个master,剩下的是被master分配的worker。有M个Map任务和R个Reduce任务需要分配,master选择空闲的worker并且分配每一个节点一个Map任务或者Reduce任务。(在实现中,Map和Reduce都可以抽象为Worker,也就是说Worker机、既可以做Map也可以做Reduce)
- 被分配Map任务的Worker读取了相关的被分割的输入内容。它将会从输入数据中解析出key/value pair,然后将key/value pair传给用户定义的Map函数。由Map函数生产的中间值key/value pair将会被缓存在内存中。
- 每隔一段时间,被缓存的键值对将会被写到本地磁盘中,由切分函数分割成R个区域。缓存在本地磁盘的位置将会传给master,master将会负责传递这些位置给Reduce Worker。
- 当一个Reduce Worker被master通知这些缓存键值对的位置,Reduce Worker将会使用RPC从Map Worker的本地磁盘中读取缓存数据。当一个Reduce Worker已经读取了所有中间值数据,它将会根据中间值键值来进行排序从而所有相同键值的出现都会被分到同一个组中。
- Reduce Worker迭代排序好的中间值数据,对于每个遇到的唯一的中间值键,它将会将键和它相关的中间值集合传递给用户定义的Reduce Function。Reduce Function的输出将会追加到最终的输出文件
- 当所有的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 不足
- 代码写的不够简洁与规范
- 只是实现了单机的MapReduce,还没有完成正真意义上的MapReduce
- 当一个Worker超时时,只能当该Worker的任务结束后,才会被销毁。而不是监测到超时,会立刻销毁。这个不足,可以通过HeartBeat改进,如果发现超时,Coordinator不会给该Worker返回HeartBeat.
4. 参考
相关的所有代码可以在: gitee仓库RT_Enzyme中查看,不过还是建议自己独立完成