Mit6.824 Lab1 MapReduce实现
paper地址:http://nil.csail.mit.edu/6.824/2021/schedule.html
MapReduce 原理
- 启动MapReduce, 将输入文件切分成大小在16-64MB之间的文件。然后在一组多个机器上启动用户程序
- 其中一个副本将成为master, 余下成为worker. master给worker指定任务(M个map任务,R个reduce任务)。master选择空闲的worker给予map或reduce任务
- Map worker 接收切分后的input,执行Map函数,将结果缓存到内存
- 缓存后的中间结果会周期性的写到本地磁盘,并切分成R份(reducer数量)。R个文件的位置会发送给master, master转发给reducer
- Reduce worker 收到中间文件的位置信息,通过RPC读取。读取完先根据中间<k, v>排序,然后按照key分组、合并。
- Reduce worker在排序后的数据上迭代,将中间<k, v> 交给reduce 函数处理。最终结果写给对应的output文件(分片)
- 所有map和reduce任务结束后,master唤醒用户程序
MapReduce 实现流程
Master
论文提到每个(Map或者Reduce)Task有分为idle, in-progress, completed 三种状态。
// 枚举,表示任务执行阶段,根据论文,分为空闲、执行中、已完成
const (
Idle MasterTaskStatus = iota
InProgress
Completed
)
Master 保存Task的信息
// Master记录的任务信息,包含任务执行阶段、任务开始时间,Task对象的指针
type MasterTask struct {
TaskStatus MasterTaskStatus // 任务执行阶段
StartTime time.Time // 任务开始执行时间
TaskReference *Task // 表示当前执行的是哪个任务
}
Master存储Map任务产生的R个中间文件的信息。
// Master节点对象
type Master struct {
TaskQueue chan *Task // 保存Task的队列,通过channel通道实现队列
TaskMeta map[int]*MasterTask // 当前系统所有task的信息,key为taskId
MasterPhase State // Master阶段
NReduce int // R个Reduce工作线程
InputFiles []string // 输入文件名
Intermediates [][]string // M行R列的二维数组,保存Map任务产生的M*R个中间文件
}
Map和Reduce使用同一个Task结构,完全可以兼顾两个阶段的任务。
// 任务对象
type Task struct {
Input string // 任务负责处理的输入文件名
TaskState State // 任务状态
NReducer int // R个Reducer
TaskNumber int // TaskId
Intermediates []string // 保存Map任务产生的R个中间文件的磁盘路径
Output string // 输出文件名
}
将task和master的状态合并成一个State
type State int
// 枚举,表示Master和Task的状态
const (
Map State = iota // 从0开始枚举
Reduce
Exit
Wait
)
MapReduce执行Map和Reduce实现
1. 启动master
// create a Master.
// main/mrmaster.go calls this function.
// nReduce is the number of reduce tasks to use.
// 创建Master节点,负责分发任务,作为服务注册中心、服务调度中心
func MakeMaster(files []string, nReduce int) *Master {
// 创建Master节点
m := Master{
// 保存task的队列,通过chan通道实现先进先出
TaskQueue: make(chan *Task, max(nReduce, len(files))),
// 主要作用是通过taskId这个key获取到对应的Task信息
TaskMeta: make(map[int]*MasterTask),
// 一开始Master和Task都处于Map阶段
MasterPhase: Map,
NReduce: nReduce,
InputFiles: files,
// 创建二维数组保存Map阶段生成的中间文件路径,设置列数为nReduce
Intermediates: make([][]string, nReduce),
}
// TODO 将files中的文件切分成16MB-64MB的文件
// 创建Map任务
m.createMapTask()
// 启动Master节点,将Master的方法都注册到注册中心,worker就可以通过RPC访问Master的方法
m.server()
// crash,启动一个协程来不断检查超时的任务
go m.catchTimeOut()
return &m
}
创建Map任务
// 创建Map任务
func (m *Master) createMapTask() {
// 遍历所有的输入文件,每个文件用一个Map任务处理
for idx, fileName := range m.InputFiles {
// 创建Map Task对象
taskMeta := Task{
Input: fileName,
TaskState: Map,
NReducer: m.NReduce,
TaskNumber: idx,
}
// Task对象放入队列
m.TaskQueue <- &taskMeta
// 填充Master对当前队列中所有Task的信息, taskId为key,value保存task信息
m.TaskMeta[idx] = &MasterTask{
TaskStatus: Idle,
TaskReference: &taskMeta,
}
}
}
不断检查超时任务,提高执行效率
// crash,启动一个协程来不断检查超时的任务
func (m *Master) catchTimeOut() {
for {
time.Sleep(5 * time.Second)
// 锁住其他线程可能会使用的m.MasterPhase
mu.Lock()
// Master节点的执行状态是退出状态,则退出检查
if m.MasterPhase == Exit {
mu.Unlock()
return
}
// 检查所有任务
for _, masterTask := range m.TaskMeta {
// 任务执行中并且执行时间大于10秒,则重新放入队列等待被其他worker执行
if masterTask.TaskStatus == InProgress && time.Now().Sub(masterTask.StartTime) > 10*time.Second {
m.TaskQueue <- masterTask.TaskReference
masterTask.TaskStatus = Idle
}
}
mu.Unlock()
}
}
2. master监听worker RPC调用,分配任务
// 等待worker通过rpc请求Master的服务
func (m *Master) AssignTask(args *ExampleArgs, reply *Task) error {
// 锁住Master节点
mu.Lock()
defer mu.Unlock()
// 队列里还有空闲任务
if len(m.TaskQueue) > 0 {
// taskQueue还有空闲的task就发出一个Task指针给一个worker
*reply = *<-m.TaskQueue
// 设置Task状态
m.TaskMeta[reply.TaskNumber].TaskStatus = InProgress
m.TaskMeta[reply.TaskNumber].StartTime = time.Now()
} else if m.MasterPhase == Exit {
// 队列里还有任务但是Master状态为Exit
// 返回一个带着Exit状态的Task,表示Master已经终止服务了
*reply = Task{
TaskState: Exit,
}
} else {
// 队列里没有任务,则让请求的worker等待
*reply = Task{
TaskState: Wait,
}
}
return nil
}
3. 启动worker
// main/mrworker.go calls this function.
// 启动Worker
func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) {
for {
// 通过RPC获取空闲任务
task := getTask()
// 根据任务当前的执行状态进行相应处理
switch task.TaskState {
case Map:
mapper(&task, mapf)
case Reduce:
reducer(&task, reducef)
case Wait:
time.Sleep(5 * time.Second)
case Exit:
return
}
}
}
4. worker向master发送RPC请求任务
// 通过RPC获取空闲任务
func getTask() Task {
args := ExampleArgs{}
reply := Task{}
// RPC请求调用Master的服务来获取Task
call("Master.AssignTask", &args, &reply)
return reply
}
5. worker获得MapTask,交给mapper处理
// 执行Map任务
func mapper(task *Task, mapf func(string, string) []KeyValue) {
// 获取任务对应的文件路径
content, err := ioutil.ReadFile(task.Input)
if err != nil {
log.Fatal("Failed to read file: "+task.Input, err)
}
// 执行wc.go中的mapf方法,进行MapReduce的map阶段,得到nReduce个中间文件路径的字符串数组
intermediates := mapf(task.Input, string(content))
// 将map阶段生成的中间文件路径保存到列数为NReducer的二维数组中
buffer := make([][]KeyValue, task.NReducer)
// 保存结果到内存buffer中
for _, intermediate := range intermediates {
// 根据key进行hash,将结果切分成NReducer份
slot := ihash(intermediate.Key) % task.NReducer
buffer[slot] = append(buffer[slot], intermediate)
}
// 周期性地从内存保存到磁盘中
mapOutput := make([]string, 0)
for i := 0; i < task.NReducer; i++ {
// 将中间结果写入到NReducer个中间临时文件中
mapOutput = append(mapOutput, writeToLocalFile(task.TaskNumber, i, &buffer[i]))
}
// NReducer个文件的路径保存到内存,Master就可以获取到
task.Intermediates = mapOutput
// 设置该任务状态为已完成
TaskCompleted(task)
}
6. worker任务完成后通知master
func TaskCompleted(task *Task) {
reply := ExampleReply{}
call("Master.TaskCompleted", task, &reply)
}
7. master收到完成后的Task
// 更新Task状态为已完成并检查
func (m *Master) TaskCompleted(task *Task, reply *ExampleReply) error {
mu.Lock()
defer mu.Unlock()
// 容错、检查节点状态、检查重复任务
if task.TaskState != m.MasterPhase || m.TaskMeta[task.TaskNumber].TaskStatus == Completed {
// 重复任务要丢弃
return nil
}
m.TaskMeta[task.TaskNumber].TaskStatus = Completed
go m.processTaskResult(task)
return nil
}
- 如果所有的ReduceTask都已经完成,转入Exit阶段
// master通过协程获取任务执行的结果
func (m *Master) processTaskResult(task *Task) {
mu.Lock()
defer mu.Unlock()
switch task.TaskState {
case Map:
// Map阶段则收集中间结果到Master内存中
// key为taskId,value为文件路径的字符串数组,一个task有NReducer个filePath
for reduceTaskId, filePath := range task.Intermediates {
m.Intermediates[reduceTaskId] = append(m.Intermediates[reduceTaskId], filePath)
}
// 所有任务已完成则进入reduce阶段
if m.allTaskDone() {
m.createReduceTask()
m.MasterPhase = Reduce
}
case Reduce:
// Reduce则设置状态为Exit
if m.allTaskDone() {
m.MasterPhase = Exit
}
}
}
8. 如果所有的MapTask都已经完成,创建ReduceTask,转入Reduce阶段
// 执行Reduce任务
func reducer(task *Task, reducef func(string, []string) string) {
// 从磁盘中读取中间文件
intermediate := *readFromLocalFile(task.Intermediates)
// 根据key进行字典序排序
sort.Sort(ByKey(intermediate))
dir, _ := os.Getwd()
tempFile, err := ioutil.TempFile(dir, "mr-2021-tmp-*")
if err != nil {
log.Fatal("Failed to create temp file", err)
}
i := 0
// 遍历每一个key
for i < len(intermediate) {
j := i + 1
// 相同的key分组合并
for j < len(intermediate) && intermediate[i].Key == intermediate[j].Key {
j++
}
// 保存该key的最终计数, 即对相同key的计数进行合并统计
values := []string{}
for k := i; k < j; k++ {
values = append(values, intermediate[k].Value)
}
// 结果交给reducef进行统计
output := reducef(intermediate[i].Key, values)
// 最终结果的字符串内容保存到临时文件里
fmt.Fprintf(tempFile, "%v %v\n", intermediate[i].Key, output)
i = j
}
tempFile.Close()
// 定义输出文件的文件名
oname := fmt.Sprintf("mr-2021-out-%d", task.TaskNumber)
os.Rename(tempFile.Name(), oname)
task.Output = oname
TaskCompleted(task)
}
9. master确认所有ReduceTask都已经完成,转入Exit阶段,终止所有master和worker goroutine
//
// main/mrmaster.go calls Done() periodically to find out
// if the entire job has finished.
//
func (m *Master) Done() bool {
mu.Lock()
defer mu.Unlock()
ret := m.MasterPhase == Exit
return ret
}
- 并发
因为Master保存Task相关的信息,因此在worker执行任务时,是需要对Master进行并发修改的,所以需要进行上锁。master跟多个worker通信,master的数据是共享的。
// Master节点对象
type Master struct {
TaskQueue chan *Task // 保存Task的队列,通过channel通道实现队列
TaskMeta map[int]*MasterTask // 当前系统所有task的信息,key为taskId
MasterPhase State // Master阶段
NReduce int // R个Reduce工作线程
InputFiles []string // 输入文件名
Intermediates [][]string // M行R列的二维数组,保存Map任务产生的M*R个中间文件
}
其中TaskMeta, Phase, Intermediates, TaskQueue
都有读写发生。TaskQueue
使用channel
实现,自己带锁。只有涉及Intermediates, TaskMeta, Phase
的操作需要上锁,InputFiles 和 NReduce 因为是在创建Master时一次性写入,所以不会出现并发写的场景。
11.容错
- 周期性向worker发送心跳检测
- 如果worker失联一段时间,master将worker标记成failed
- worker失效之后,已完成的map task被重新标记为idle,已完成的reduce task不需要改变
- 对于in-progress 且超时的任务,则重新放入队列等待被其他worker执行
// crash,启动一个协程来不断检查超时的任务
func (m *Master) catchTimeOut() {
for {
time.Sleep(5 * time.Second)
// 锁住其他线程可能会使用的m.MasterPhase
mu.Lock()
// Master节点的执行状态是退出状态,则退出检查
if m.MasterPhase == Exit {
mu.Unlock()
return
}
// 检查所有任务
for _, masterTask := range m.TaskMeta {
// 任务执行中并且执行时间大于10秒,则重新放入队列等待被其他worker执行
if masterTask.TaskStatus == InProgress && time.Now().Sub(masterTask.StartTime) > 10*time.Second {
m.TaskQueue <- masterTask.TaskReference
masterTask.TaskStatus = Idle
}
}
mu.Unlock()
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步