[{"data":1,"prerenderedAt":289},["ShallowReactive",2],{"content:\u002Fnotes\u002F65840\u002Fmapreducelab":3,"surround:\u002Fnotes\u002F65840\u002Fmapreducelab":278},{"id":4,"title":5,"body":6,"categories":253,"date":255,"description":256,"draft":257,"extension":258,"image":259,"meta":260,"navigation":262,"path":263,"permalink":259,"published":259,"readingTime":264,"recommend":259,"references":259,"seo":269,"sitemap":270,"stem":271,"tags":272,"type":276,"__hash__":277},"content\u002Fposts\u002Fnotes\u002F65840\u002Fmapreducelab.md","6.5840 实验一 —— MapReduce",{"type":7,"value":8,"toc":237},"minimark",[9,13,17,24,27,30,40,48,71,82,88,103,106,109,113,116,120,124,127,137,140,146,149,153,156,162,165,168,171,177,180,183,186,189,195,198,201,207,210,213,219,222,225,228,234],[10,11,12],"h3",{"id":12},"前言",[14,15,16],"p",{},"实验一是要实现一个 MapReduce 系统，基本就是两个部分：实现 master 程序和实现 worker 程序。这个实验基本就是劝退怪了，一来是对 golang 的 rpc 和并发的使用要比较熟悉，二来就是要对 MapReduce 的整个流程机制要比较熟悉。其实有一个小秘诀，就是拼命看论文中的这张图，再拼命看下面的流程讲解：",[14,18,19],{},[20,21],"img",{"alt":22,"src":23},"mapReduce 执行流程","https:\u002F\u002Fblog-img.774352199.xyz\u002F2025\u002F6f7e7839e6f09e0d8193d530920a6f7e.jpg",[14,25,26],{},"这个实验我实现了两个版本，主要是并发控制的方式有些不同。最初是基于 mutex 锁的版本，后来重构成了基于 channel 的无锁版本。无锁版本的实现比较优雅，所以讲解也主要基于无锁版本。",[10,28,29],{"id":29},"实验讲解",[14,31,32,33,39],{},"做实验之前，首先需要读懂实验。说明书在：",[34,35,36],"a",{"href":36,"rel":37},"https:\u002F\u002Fpdos.csail.mit.edu\u002F6.824\u002Flabs\u002Flab-mr.html",[38],"nofollow","。主要这个实验需要在 Linux 环境下进行，因为进程通信基于 unix socket，MacOS 原则上来说也可以，但是据说还是会有些小问题。",[14,41,42,43,47],{},"代码中已经提供了一个单线程串行版的 MapReduce，代码在 ",[44,45,46],"code",{"code":46},"src\u002Fmain\u002Fmrsequential.go","。这个版本很重要，建议先阅读一遍，可以大致了解整体的流程。有一些内容的处理也可以直接从中 copy。",[14,49,50,51,54,55,58,59,62,63,66,67,70],{},"并行版本的 master 程序入口在 ",[44,52,53],{"code":53},"main\u002Fmrcoordinator.go","，worker 程序入口在 ",[44,56,57],{"code":57},"main\u002Fmrworker.go","。实验需要实现的有三个文件，分别是 ",[44,60,61],{"code":61},"mr\u002Fcoordinator.go","、",[44,64,65],{"code":65},"mr\u002Fworker.go","、和 ",[44,68,69],{"code":69},"mr\u002Frpc.go","，分别描述了 master 的处理代码、worker 的处理代码以及它们之间通信的 rpc 结构。",[14,72,73,74,76,77,81],{},"mrcoordinator 会调用 ",[44,75,61],{"code":61}," 中的 MakeCoordinator 函数，来构建 master 的结构，并启动 socket 监听，在返回后，主协程会不断调用 Coordinator.Done 方法，来检查是否已经完成整个 MapReduce 任务，确认完成后才会退出主协程。所以，在 MakeCoordinator 中，不应当有操作阻止函数返回，否则会阻塞后续操作。",[78,79,80],"strong",{},"相关的监听等工作应当通过新协程实现","。",[14,83,84,85,87],{},"mrworker 的处理就很简单了，只有一个主协程，直接调用了 ",[44,86,65],{"code":65}," 的 Worker 函数，在这里处理即可。一般可以直接实现成单协程程序。",[14,89,90,91,94,95,98,99,102],{},"测试脚本为 ",[44,92,93],{"code":93},"src\u002Fmain\u002Ftest-mr.sh","，它会将两个现成的 MapReduce 程序：wc 和 indexer 通过你的框架执行，并与串行执行的结果相比较。它同时还会检查并行运行相同的 Map 或 Reduce 任务、甚至 worker 执行任务期间发生 crash 时，最终是否能得到正确的结构。通常它会启动一个 master 进程和三个 worker 进程。如果在运行期间发生错误不退出时，可以通过 ",[44,96,97],{"code":97},"ps -A"," 命令，找到 mrcoordinator 进程的 pid，并 kill 掉即可。普通的 ",[44,100,101],{"code":101},"ctrl + c"," 可能无法完全退出，会影响后续的测试。",[14,104,105],{},"最后，请多阅读几遍实验指导书。",[10,107,108],{"id":108},"实现思路",[110,111,112],"h4",{"id":112},"整体流程",[14,114,115],{},"workers 会首先执行完 map 任务，生成很多中间文件“mr-X-Y”，其中，X 是 map 任务的 id，Y 是对应的 reduce 任务 id。接着 reduce 会收集所有 Y 等于 reduce 任务 id 的文件，读取并进行 reduce 操作，并将结果输出到“mr-out-Y”中。",[110,117,119],{"id":118},"master-实现","master 实现",[121,122,123],"h5",{"id":123},"无锁思路",[14,125,126],{},"由于是一个无锁的实现，要避免多协程数据冲突，所有对主要数据结构的操作应当收敛到一个协程中，这里可以称为调度协程。在 worker 通过 rpc 请求 master 时，例如获取一个 task 或者汇报完成工作，master 会通过一个自动创建的协程处理 rpc 请求，由于对主要数据结构的操作已经收敛，这个 rpc 协程就必须通过 channel 要求调度协程代办，以保证没有数据竞争。由于 worker 和 master 之间可能有多种消息，这意味着调度协程必须同时管理多个 channel。这里可以运用 golang 的 select 结构：",[128,129,135],"pre",{"className":130,"code":132,"language":133,"meta":134},[131],"language-go","\u002F\u002F 只在这个 goroutine 中操作结构\nfunc (c *Coordinator) schedule() {\n    for {\n        select {\n        case msg := \u003C-c.getTaskChan:\n            c.getTaskHandler(msg)\n        case msg := \u003C-c.doneTaskChan:\n            c.doneTaskHandler(msg)\n        case msg := \u003C-c.timeoutChan:\n            c.timeoutHandler(msg)\n        case msg := \u003C-c.doneCheckChan:\n            c.doneCheckHandler(msg)\n        }\n    }\n}\n","go","",[44,136,132],{"__ignoreMap":134},[14,138,139],{},"假设这时候有一个 worker 需要获取一个 task 来执行，请求 master 的 GetTask，GetTask 处理如下：",[128,141,144],{"className":142,"code":143,"language":133,"meta":134},[131],"func (c *Coordinator) GetTask(_ *GetTaskReq, resp *GetTaskResp) error {\n    msg := GetTaskMsg{\n        resp: resp,\n        ok:   make(chan struct{}),\n    }\n    c.getTaskChan \u003C- msg\n    \u003C-msg.ok\n    return nil\n}\n",[44,145,143],{"__ignoreMap":134},[14,147,148],{},"在向 getTaskChan 中，不止传入了 resp（getTask 不需要请求参数），还传入了一个 chan struct{} 类型的管道，这个管道是协调协程用于通知 rpc 协程处理完成的通道：当处理完成后，就会向 msg.ok 中写入一个 struct{}，rpc 协程就会返回。",[121,150,152],{"id":151},"coordinator","Coordinator",[14,154,155],{},"整个 Coordinator 结构如下：",[128,157,160],{"className":158,"code":159,"language":133,"meta":134},[131],"type Coordinator struct {\n    nMap    int\n    nReduce int\n    phase   TaskPhase\n    allDone bool\n \n    taskTimeOut map[int]time.Time\n    tasks       []*Task\n \n    getTaskChan   chan GetTaskMsg\n    doneTaskChan  chan DoneTaskMsg\n    doneCheckChan chan DoneCheckMsg\n    timeoutChan   chan TimeoutMsg\n}\n",[44,161,159],{"__ignoreMap":134},[14,163,164],{},"phase 记录了当前任务执行的阶段，由于 reduce 任务必须在所有 map 任务结束后才能进行，所以 TaskPhase 分为 Map 和 Reduce 阶段，每个阶段中，tasks 切片只有对应阶段的任务。",[14,166,167],{},"taskTimeOut 记录了当前正在执行的任务的开始时间，会有一个协程定时去扫描这个 map，找出其中运行时间大于十秒的任务（超时），将对应的任务状态设置为未开始，以进行下一次调度。当然这个扫描操作也需要通过协调协程进行。超时 map 中也只有当前阶段正在执行的任务，在切换阶段时会清空超时 map。",[14,169,170],{},"tasks 切片保存了当前阶段所有的 Task，以及相关的状态：",[128,172,175],{"className":173,"code":174,"language":133,"meta":134},[131],"type ReduceTask struct {\n    NMap int\n}\n \ntype MapTask struct {\n    FileName string\n    NReduce  int\n}\n \ntype TaskStatus int\n \nvar (\n    TaskStatus_Idle     TaskStatus = 0\n    TaskStatus_Running  TaskStatus = 1\n    TaskStatus_Finished TaskStatus = 2\n)\n \ntype Task struct {\n    TaskId     int\n    MapTask    MapTask\n    ReduceTask ReduceTask\n    TaskStatus TaskStatus\n}\n",[44,176,174],{"__ignoreMap":134},[14,178,179],{},"这里可以看到任务的状态被分成三个，分别是待执行、执行中以及执行完成。同时冗余保存了 MapTask 和 ReduceTask，具体使用哪个结构体由当前阶段来判断。",[121,181,182],{"id":182},"具体操作",[14,184,185],{},"根据 Coordinator 中的管道，可以看出有四种情况需要和协调协程通信以进行操作。",[14,187,188],{},"当 worker 请求一个任务时，可能获取到的任务类别有四种：",[128,190,193],{"className":191,"code":192,"language":133,"meta":134},[131],"type TaskType int\n \nvar (\n    TaskType_Map    TaskType = 0\n    TaskType_Reduce TaskType = 1\n    TaskType_Wait   TaskType = 2\n    TaskType_Exit   TaskType = 3\n)\n",[44,194,192],{"__ignoreMap":134},[14,196,197],{},"master 首先遍历所有的 tasks，找出其中的状态为未执行的状态，并根据当前的阶段，返回 Map 或者 Reduce 任务。如果当前没有空闲任务的话，又分为以下两种情况。当前为 Map 阶段，这时需要返回 TaskType_Wait 任务，要求 worker 等待，Map 阶段结束后还需要进行 Reduce 任务；当前为 Reduce 阶段，这时所有任务已经完成，返回 TaskType_Exit 要求 worker 退出。",[14,199,200],{},"当 worker 完成时，会通知 master 任务完成。传递的信息中会带有任务的类型和任务的 Id。master 会忽略掉非当前阶段的任务，根据 taskId 修改 tasks 中的任务状态为 finished（忽略当前任务状态，直接改为完成），并删除 timeout 中的对应结构。",[128,202,205],{"className":203,"code":204,"language":133,"meta":134},[131],"func (c *Coordinator) doneTaskHandler(msg DoneTaskMsg) {\n    req := msg.req\n    if req.TaskType == TaskType_Map && c.phase == TaskPhase_Reduce {\n        \u002F\u002F 提交非当前阶段的任务，直接返回\n        msg.ok \u003C- struct{}{}\n        return\n    }\n    for _, task := range c.tasks {\n        if task.TaskId == req.TaskId {\n            \u002F\u002F 无论当前状态，直接改为完成\n            task.TaskStatus = TaskStatus_Finished\n            break\n        }\n    }\n    \u002F\u002F 删除 timeout 结构\n    delete(c.taskTimeOut, req.TaskId)\n    allDone := true\n    for _, task := range c.tasks {\n        if task.TaskStatus != TaskStatus_Finished {\n            allDone = false\n            break\n        }\n    }\n    if allDone {\n        if c.phase == TaskPhase_Map {\n            c.initReducePhase()\n        } else {\n            c.allDone = true\n        }\n    }\n    msg.ok \u003C- struct{}{}\n}\n",[44,206,204],{"__ignoreMap":134},[14,208,209],{},"如果是在 Reduce 阶段发现所有任务都完成了，还会设置一下 allDone 标志位。",[14,211,212],{},"Coordinator 在初始化时，还会启动一个协程，这个协程每秒请求一次协调协程，检查 timeoutMap 是否有超时任务，如果超时，就将其状态置为未开始，这样在下一次 worker 请求任务时就可以调度执行了。",[128,214,217],{"className":215,"code":216,"language":133,"meta":134},[131],"func (c *Coordinator) timeoutHandler(msg TimeoutMsg) {\n    now := time.Now()\n    for taskId, start := range c.taskTimeOut {\n        if now.Sub(start).Seconds() > 10 {\n            for _, task := range c.tasks {\n                if taskId == task.TaskId {\n                    if task.TaskStatus != TaskStatus_Finished {\n                        task.TaskStatus = TaskStatus_Idle\n                    }\n                    break\n                }\n            }\n            delete(c.taskTimeOut, taskId)\n            break\n        }\n    }\n    msg.ok \u003C- struct{}{}\n    return\n}\n",[44,218,216],{"__ignoreMap":134},[14,220,221],{},"最后还有一个完成状态检查，是主线程调用 Coordinator.Done 进行的，请求协调协程时，只需要检查 allDone 标志位即可。",[110,223,224],{"id":224},"worker",[14,226,227],{},"worker 只有单个协程，循环从 master 处获取任务执行：",[128,229,232],{"className":230,"code":231,"language":133,"meta":134},[131],"func Worker(mapf func(string, string) []KeyValue,\n    reducef func(string, []string) string) {\n    for {\n        resp := callGetTask()\n        switch resp.TaskType {\n        case TaskType_Map:\n            handleMapTask(resp.Task, mapf)\n        case TaskType_Reduce:\n            handleReduceTask(resp.Task, reducef)\n        case TaskType_Wait:\n            time.Sleep(time.Second)\n        case TaskType_Exit:\n            return\n        }\n    }\n}\n",[44,233,231],{"__ignoreMap":134},[14,235,236],{},"map 和 reduce 的操作，可以参考串行单线程的实现。有一点注意是，由于可能有多个进程同时执行同一个任务，也可能会出现执行到一半崩溃的情况，遗留下的文件可能会导致后续 worker 重新执行时发生错误。所以创建输出文件时，可以通过 ioutil.TempFile 函数创建一个临时文件写入，等到写入完成后通过 os.Rename 重命名为目标文件，这样即可保证最后的输出文件一定是完整的。",{"title":134,"searchDepth":238,"depth":238,"links":239},4,[240,242,243],{"id":12,"depth":241,"text":12},3,{"id":29,"depth":241,"text":29},{"id":108,"depth":241,"text":108,"children":244},[245,246,252],{"id":112,"depth":238,"text":112},{"id":118,"depth":238,"text":119,"children":247},[248,250,251],{"id":123,"depth":249,"text":123},5,{"id":151,"depth":249,"text":152},{"id":182,"depth":249,"text":182},{"id":224,"depth":238,"text":224},[254],"笔记","2022-01-20 22:29:00","实验一的目标是实现一个 MapReduce 系统，分为 master 和 worker 两个核心部分。这个过程对 golang 的 RPC 和并发编程要求较高，同时需要深入理解 MapReduce 的流程。实验经历了两个版本的实现，从基于 mutex 锁的版本到更优雅的基于 channel 的无锁版本，后者的设计更加简洁明了。理解实验的关键在于认真阅读相关文档，特别是其中的流程图和说明。",false,"md",null,{"slots":261},{},true,"\u002Fnotes\u002F65840\u002Fmapreducelab",{"text":265,"minutes":266,"time":267,"words":268},"12 min read",11.29,677400,2258,{"title":5,"description":256},{"loc":263},"posts\u002Fnotes\u002F65840\u002Fmapreducelab",[273,274,275],"mapreduce","6.5840","6.824","tech","Gq7cbzfknFVd9Jgkax6Azw63XK4NcSTJ13jDI3bNbTo",[279,284],{"title":280,"path":281,"stem":282,"date":283,"type":276,"children":-1},"MapReduce 论文阅读","\u002Fnotes\u002F65840\u002Fmapreducepaper","posts\u002Fnotes\u002F65840\u002Fmapreducepaper","2022-01-16 17:32:00",{"title":285,"path":286,"stem":287,"date":288,"type":276,"children":-1},"Raft 论文阅读","\u002Fnotes\u002F65840\u002Freftextendedpaper","posts\u002Fnotes\u002F65840\u002Freftextendedpaper","2022-12-03 21:40:09",1787554445396]