文章

MIT 6.824 学习记录:MapReduce Lab 学习与实践

在阅读完 MapReduce 论文之后,我根据课程要求花了一天快速入门了 Go 语言,通过官方的 A Tour of Go 学习基础语法与特性。随后观看了课程的 Lecture 1 和 Lecture 2,结合老师讲解,再次梳理了 MapReduce 的核心思想和分布式背景。

紧接着,我花了大约 7 个小时完成了 Lab1,复现了一个简易的 MapReduce 框架,并成功通过了所有测试 🥰🥰

这次实践让我深刻体会到:计算机知识终究需要通过动手实践来掌握。过程中我意识到自己对 Go 的掌握还不够熟练,也发现自己对 MapReduce 原理存在一些误解,尤其在并发调度和系统状态管理方面,暴露出不少问题。但这些“踩坑”经历,也正是我收获最多的地方🫠

架构设计概览

实验主要围绕三个文件展开:coordinator.gorpc.goworker.go,基本对应论文中的角色:

  • Coordinator 负责任务调度与系统状态管理

  • Worker 负责执行任务

  • 双方之间通过 RPC 通信,完成任务分配与反馈

整体流程简述

我设计的流程如下:

  1. Worker 向 Coordinator 主动拉取任务

  2. Coordinator 从任务池中分配一个待执行任务

  3. Worker 执行后向 Coordinator 提交结果

  4. Worker 重复拉取新任务,直到接收到结束信号

  5. 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
}

这里通过 mapTasksreduceTasks 管理任务状态,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 协作与失败恢复🤗

成功实现的关键点

  • 任务分配与状态管理:将任务按 IDLEIN_PROCESSCOMPLETED 三种状态进行管理,调度器根据任务当前状态与开始时间判断是否需要重新分配任务。

  • 超时重试机制:为每个任务记录 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

License:  CC BY 4.0