MIT 6.824 学习记录:MapReduce Lab 学习与实践
在阅读完 MapReduce 论文之后,我根据课程要求花了一天快速入门了 Go 语言,通过官方的 A Tour of Go 学习基础语法与特性。随后观看了课程的 Lecture 1 和 Lecture 2,结合老师讲解,再次梳理了 MapReduce 的核心思想和分布式背景。
紧接着,我花了大约 7 个小时完成了 Lab1,复现了一个简易的 MapReduce 框架,并成功通过了所有测试 🥰🥰
这次实践让我深刻体会到:计算机知识终究需要通过动手实践来掌握。过程中我意识到自己对 Go 的掌握还不够熟练,也发现自己对 MapReduce 原理存在一些误解,尤其在并发调度和系统状态管理方面,暴露出不少问题。但这些“踩坑”经历,也正是我收获最多的地方🫠
架构设计概览
实验主要围绕三个文件展开:coordinator.go
、rpc.go
和 worker.go
,基本对应论文中的角色:
Coordinator 负责任务调度与系统状态管理
Worker 负责执行任务
双方之间通过 RPC 通信,完成任务分配与反馈
整体流程简述
我设计的流程如下:
Worker 向 Coordinator 主动拉取任务
Coordinator 从任务池中分配一个待执行任务
Worker 执行后向 Coordinator 提交结果
Worker 重复拉取新任务,直到接收到结束信号
Coordinator 在所有任务完成后终止服务
在初次调试时,我遇到了 RPC 通信失败的问题,最后发现是因为结构体中字段名没有大写,导致 Go 无法导出字段,RPC 无法正常解析数据。这是 Go 的常见“陷阱”,也提醒我对语言细节要更上心。
任务结构设计
任务结构的设计是整个实验的关键部分。我设计了两个核心结构体:
type Task struct {
TaskType taskType
TaskId int
NMap int
NReduce int
Files []string
}
type TaskMeta struct {
taskType taskType
taskStatus taskStatus
taskId int // mapId or reduceId
startTime time.Time
files []string
}
Task
用于 RPC 通信,在 worker 和 coordinator 之间传输TaskMeta
用于协调器内部的任务管理与状态记录
在实现过程中,我不断根据需要迭代和完善字段,力求结构清晰、信息完整、便于状态维护
流程控制与状态划分
为了实现状态机风格的调度机制,我为任务类型和任务状态进行了枚举定义:
type taskStatus int
const (
IDLE taskStatus = iota
IN_PROCESS
COMPLETED
)
type taskType int
const (
MAP taskType = iota
REDUCE
WAIT
EXIT
)
任务状态划分较为直接:未开始(IDLE)、执行中(IN_PROCESS)、已完成(COMPLETED)。
而任务类型则经历了多次调整:起初只定义了 MAP 和 REDUCE,但我发现无法处理任务“过渡”状态,于是加入了 WAIT 和 EXIT 两种类型:
WAIT:任务还未准备好,例如 REDUCE 阶段需等待所有 MAP 任务完成
EXIT:表示所有任务完成,worker 可退出
这种设计尤其适用于多 worker 并发执行的情况。比如在 MAP 阶段并发执行时,若 Coordinator 发现没有新 MAP 任务分配,却仍有未完成任务,就不能直接进入 REDUCE 阶段,这时通过向 worker 返回 WAIT 任务即可。
系统状态结构
我使用以下结构体维护全局状态:
type JobStatus int8
const (
JOB_MAP JobStatus = iota
JOB_REDUCE
JOB_FINISHED
)
type Coordinator struct {
mapTasks map[int]*TaskMeta
reduceTasks map[int]*TaskMeta
intermediateFiles map[int][]string // key: reduceId
jobStatus JobStatus
nReduce int
nMap int
lock sync.Mutex
}
这里通过 mapTasks
和 reduceTasks
管理任务状态,intermediateFiles
用于存储 map 输出,供 reduce 使用。整体调度逻辑遵循状态机的设计思想,worker 作为客户端,不断拉取任务,协调器根据任务状态变化推进系统状态🤗
核心函数解析
最核心的部分自然是 Worker 中执行 MAP 和 REDUCE 的函数实现:
MAP:应用 map 函数,按 key 分区写入中间文件(
nReduce
个),以 reduceId 分文件命名;REDUCE:从对应的
nMap
个中间文件中读取键值对,按 key 分组后调用 reduce 函数生成最终结果文件。
需要注意的是,在文件写入部分我遵循了论文中的建议,先写临时文件再改名为最终文件名,这样可以借助文件系统的原子性保障输出一致性。
此外,我设计了两组核心 RPC 交互函数:
AssignTask
: worker 向 coordinator 拉取任务;SubmitTask
: worker 向 coordinator 提交任务结果。
由于系统是多线程并发的,为了避免数据竞争,我在 coordinator 的所有对共享任务状态读写的函数(如 assign/submit)中加锁,确保线程安全。
总结
在本次 MapReduce Lab1 实验中,我实现了一个基本但具备容错能力的分布式任务调度系统,核心模块包括任务调度、状态管理、worker 协作与失败恢复🤗
成功实现的关键点
任务分配与状态管理:将任务按
IDLE
、IN_PROCESS
、COMPLETED
三种状态进行管理,调度器根据任务当前状态与开始时间判断是否需要重新分配任务。超时重试机制:为每个任务记录
startTime
,在任务分配时,如果任务处于IN_PROCESS
状态且距离当前时间超过 10 秒,则判断任务执行失败并重新分配。这种“基于时间窗口的失败检测”是 MapReduce 容错的重要机制,能有效应对 worker crash 或长时间卡住的情况。任务类型区分与阶段切换:实现了 Map 阶段与 Reduce 阶段的调度切换逻辑,在所有 Map 任务完成后进入 Reduce 阶段,确保处理过程有明确的阶段性。
任务提交与去重控制:通过任务 ID 映射控制任务提交状态,防止任务重复执行结果被二次提交。
遇到的问题与后续优化
中间结果文件缺乏可视化:虽然中间文件如
mr-X-Y
已成功生成并被 Reduce 消费,但目前日志中未展示这些文件的生成与读取过程。计划后续补充如下调试信息:每个 worker 在执行 Map/Reduce 任务时生成的中间文件名
Coordinator 当前任务状态快照(定时或事件触发打印)
Reduce 阶段合并完成后输出结果文件名
调试不够直观:目前日志主要记录调度行为,缺乏任务流转路径的整体视图。后续可能考虑实现一个简单的状态快照打印函数,或用 JSON 结构结构化输出任务执行历史,便于回溯问题
个人收获
这一实验让我首次从“系统视角”理解了分布式任务调度的本质:状态驱动、超时检测、阶段划分、容错重试。相比以往实现 RPC 或单体程序,更加注重系统的健壮性和边界条件处理,很多设计思想都可以迁移到未来的项目中🫠🫠
涉及学习资源链接:
A Tour of Go —— https://go.dev/tour/
MIT 6.824 课程和实验资料 —— https://pdos.csail.mit.edu/6.824/schedule.html