2022-6.824-Lab1:Map&Reduce

lab 地址 : https://pdos.csail.mit.edu/6.824/labs/lab-mr.html

1. 介紹

準備工作

閱讀 MapReduce

做什麼

實現一個分佈式的 Map - Reduce 結構,在原先的代碼結構中 6.824/src/main/mrsequential.go 實現了單機版的 Map - Reduce ,我們需要將其改造爲多進程版本的 Map - Reduce 。一個經典的 Map - Reduce 結構如下
image.png

2. 思路

該 lab 中,主要的進程分爲 WorkerCoordinatorCoordinator 主要負責分發任務,Worker 負責執行任務。

Coordinator

Coordinator 負責管理協調任務,本身具有狀態(Map phase 和 Reduce phase),根據不同的 phase 分發不同的任務。
其結構設計如下:

type Coordinator struct {
    // Your definitions here.
    phase        CoordinatorPhase   // 當前處於哪個階段 (Map or Reduce)
    MRTasks      []MRTask           // 記錄當前階段所有任務
    lock         sync.Mutex
    RunningTasks chan MRTask        // channel, 用來做任務隊列
    nReduce      int                // reduce 任務的數量
    nMap         int                // map 任務的數量
}

Coordinator 默認執行流程如下,只是一個簡單的 For Loop,worker 通過 rpc 來請求獲取任務:

Coordinator 需要提供兩個 RPC 調用,RequestTaskRequestTaskDone ,分別用來處理請求任務和提交任務,下圖爲 RequestTask 的處理流程

RequestTaskDone 主要在 Worker 執行完 Task 後向 Coordinator 彙報其工作完成了,由 Coordinator 進行最後的確認,將中間文件 Rename 爲目標結果文件。

參考了 Google MapReduce 的做法,Worker 在寫出數據時可以先寫出到臨時文件,最終確認沒有問題後再將其重命名爲正式結果文件,區分開了 Write 和 Commit 的過程。Commit 的過程可以是 Coordinator 來執行,也可以是 Worker 來執行:

  • Coordinator Commit:Worker 向 Coordinator 彙報 Task 完成,Coordinator 確認該 Task 是否仍屬於該 Worker,是則進行結果文件 Commit,否則直接忽略
  • Worker Commit:Worker 向 Coordinator 彙報 Task 完成,Coordinator 確認該 Task 是否仍屬於該 Worker 並響應 Worker,是則 Worker 進行結果文件 Commit,再向 Coordinator 彙報 Commit 完成

這裏兩種方案都是可行的,各有利弊。

Worker

Worker 的邏輯比較簡單,主要根據 RPC 返回的任務類型,進行 Map/Reduce 任務,並將中間結果輸出到文件中,再通過 RPC 向 Coordinator 通知任務完成。
Worker 本身無限循環,一直請求 Map/Reduce 任務,其退出的條件是請求任務時,收到的消息中 phase 已經切換爲結束。
兩種 RPC 的結構如下:

// 通知任務完成
type NotifyArgs struct {
    TaskID   int
    TaskType CoordinatorPhase
    WorkerID int
}

type NotifyReplyArgs struct {
    Confirm bool
}

// 請求任務
type RequestArgs struct {
}

type ReplyArgs struct {
    FileName  string // map task
    TaskID    int
    TaskType  CoordinatorPhase
    ReduceNum int
    MapNum    int
}

3. 實現

Coordinator 初始化

//
// create a Coordinator.
// main/mrcoordinator.go calls this function.
// nReduce is the number of reduce tasks to use.
//
func MakeCoordinator(files []string, nReduce int) *Coordinator {
    c := Coordinator{}
    c.nReduce = nReduce
    // Your code here.
    c.phase = PHASE_MAP
    c.RunningTasks = make(chan MRTask, len(files)+1)
    c.nMap = len(files)
    fmt.Printf("start make coordinator ... file count=%d\n", len(files))
    for index, fileName := range files {
        task := MRTask{
            fileName: fileName, // task file
            taskID:   index,    // task id
            status:   INIT,
            taskType: PHASE_MAP,
        }
        c.MRTasks = append(c.MRTasks, task)
        fmt.Printf("[PHASE_MAP]Add Task %v %v\n", fileName, index)
        c.RunningTasks <- task
    }

    c.server()
    return &c
}

Coordinator 任務超時機制

lab 中要求任務有一定超時時間,當 worker 超過 10s 沒有上報任務成功,則將任務重新放回 RunningTasks 隊列

// 任務超時檢查
func (c *Coordinator) CheckTimeoutTask() bool {
    /*
        1. 如果沒有超時,則直接 return,等待任務完成 or 超時
        2. 有超時,則直接分配該任務給 worker
    */
    TaskTimeout := false
    now := time.Now().Unix()
    for _, task := range c.MRTasks {
        if (now-task.startTime) > 10 && task.status != DONE {
            fmt.Printf("now=%d,task.startTime=%d\n", now, task.startTime)
            c.RunningTasks <- task
            TaskTimeout = true
        }
    }
    return TaskTimeout
}

Coordnator 處理請求任務 RPC

由於設計了只要存在 worker,就會一直請求任務,因此將超時檢查放在申請任務的前置檢查中。

// Your code here -- RPC handlers for the worker to call.
// worker 申請 task
func (c *Coordinator) RequestTask(args *RequestArgs, reply *ReplyArgs) error {
    if len(c.RunningTasks) == 0 {
        fmt.Printf("not running task ...\n")
        // 先檢查是否所有任務都已完成
        if c.AllTaskDone() {
            fmt.Printf("All Task Done ... \n")
            c.TransitPhase() // 任務結束,則切換狀態
        } else if !c.CheckTimeoutTask() { // 檢查是否有任務超時
            // 沒有任務超時,則返回當前狀態, 讓 worker 等待所有任務完成
            fmt.Printf("waiting task finish ... \n")
            reply.TaskType = PHASE_WAITTING
            return nil
        }
    }

    if c.phase == PHASE_FINISH {
        fmt.Printf("all mr task finish ... close coordinator\n")
        reply.TaskType = PHASE_FINISH
        return nil
    }

    task, ok := <-c.RunningTasks
    if !ok {
        fmt.Printf("task queue empty ...\n")
        return nil
    }

    c.lock.Lock()
    defer c.lock.Unlock()
    c.setupTaskById(task.taskID)
    reply.FileName = task.fileName
    reply.TaskID = task.taskID
    reply.TaskType = c.phase
    reply.ReduceNum = c.nReduce
    reply.MapNum = c.nMap

    return nil
}

Coordnator 處理階段流轉

階段流轉只存在兩種情況:

  • map 階段切換到 reduce 階段
  • reduce 階段切換到結束

主要關注第一種情況,當 map 階段切換到 reduce 階段時,清空記錄的任務列表 Coordinator.MRTask ,resize RunningTasks channel,因爲 reduce 任務數量可能比 map 任務數量要多,需要重新 resize,否則 channel 可能會阻塞。

// 階段流轉
func (c *Coordinator) TransitPhase() {
    // 生成對應階段 task
    c.lock.Lock()
    newPhase := c.phase
    switch c.phase {
    case PHASE_MAP:
        fmt.Printf("TransitPhase: PHASE_MAP -> PHASE_REDUCE\n")
        newPhase = PHASE_REDUCE
        c.MRTasks = []MRTask{}                          // 清空 map task
        c.RunningTasks = make(chan MRTask, c.nReduce+1) // resize
        for i := 0; i < c.nReduce; i++ {
            task := MRTask{
                taskID:   i, // task id
                status:   INIT,
                taskType: PHASE_REDUCE,
            }
            c.MRTasks = append(c.MRTasks, task)
            fmt.Printf("[PHASE_REDUCE]Add Task %v\n", task)
            c.RunningTasks <- task
        }
    case PHASE_REDUCE:
        fmt.Printf("TransitPhase: PHASE_REDUCE -> PHASE_FINISH\n")
        newPhase = PHASE_FINISH
    }
    c.phase = newPhase
    c.lock.Unlock()
}

Coordnator 處理提交任務 RPC

主要根據當前階段,對任務的中間輸出結果進行確認(即把 tmp file rename 爲 final file)

func (c *Coordinator) CommitTask(args *NotifyArgs) {
    switch c.phase {
    case PHASE_MAP:
        fmt.Printf("[PHASE_MAP] Commit Task %v\n", args)
        for i := 0; i < c.nReduce; i++ {
            err := os.Rename(tmpMapOutFile(args.WorkerID, args.TaskID, i),
                finalMapOutFile(args.TaskID, i))
            if err != nil {
                fmt.Printf("os.Rename failed ... err=%v\n", err)
                return
            }
        }

    case PHASE_REDUCE:
        fmt.Printf("[PHASE_REDUCE] Commit Task %v\n", args)
        err := os.Rename(tmpReduceOutFile(args.WorkerID, args.TaskID),
            finalReduceOutFile(args.TaskID))
        if err != nil {
            fmt.Printf("os.Rename failed ... err=%v\n", err)
            return
        }
    }
}

func (c *Coordinator) RequestTaskDone(args *NotifyArgs, reply *NotifyReplyArgs) error {
    for idx := range c.MRTasks {
        task := &c.MRTasks[idx]
        if task.taskID == args.TaskID {
            task.status = DONE
            c.CommitTask(args)
            break
        }
    }
    return nil
}

Worker 初始化

根據請求的任務類型(MAP,REDUCE,FINISH,WAITING),做不同處理

  • MAP :執行 Map 任務
  • REDUCE : 執行 Reduce 任務
  • WAITTING :等待,這種情況意味 Coordinator 沒有空閒任務,也沒有完成所有任務,有任務還在運行當中
  • FINISH :表示所有任務已經完成,可以退出 Worker
//
// main/mrworker.go calls this function.
//
func Worker(mapf func(string, string) []KeyValue,
    reducef func(string, []string) string) {

    // Your worker implementation here.
    for {
        args := RequestArgs{}
        reply := ReplyArgs{}
        ok := call("Coordinator.RequestTask", &args, &reply)
        if !ok {
            fmt.Printf("call request task failed ...\n")
            return
        }

        fmt.Printf("call finish ... file name %v\n", reply)
        switch reply.TaskType {
        case PHASE_MAP:
            DoMapTask(reply, mapf)
        case PHASE_REDUCE:
            DoReduceTask(reply, reducef)
        case PHASE_WAITTING: // 當前 coordinator 任務已經分配完了,worker 等待一會再試
            time.Sleep(5 * time.Second)
        case PHASE_FINISH:
            fmt.Printf("coordinator all task finish ... close worker")
            return
        }
    }
}

Worker 處理 Map

參考 6.824/src/main/mrsequential.go


func DoMapTask(Task ReplyArgs, mapf func(string, string) []KeyValue) bool {
    fmt.Printf("starting do map task ...\n")
    file, err := os.Open(Task.FileName)
    if err != nil {
        fmt.Printf("Open File Failed %s\n", Task.FileName)
        return false
    }

    content, err := ioutil.ReadAll(file)
    if err != nil {
        fmt.Printf("ReadAll file Failed %s\n", Task.FileName)
        return false
    }

    file.Close()
    fmt.Printf("starting map %s \n", Task.FileName)
    kva := mapf(Task.FileName, string(content))
    hashedKva := make(map[int][]KeyValue)
    for _, kv := range kva {
        hashed := ihash(kv.Key) % Task.ReduceNum
        hashedKva[hashed] = append(hashedKva[hashed], kv)
    }

    for i := 0; i < Task.ReduceNum; i++ {
        outFile, _ := os.Create(tmpMapOutFile(os.Getpid(), Task.TaskID, i))
        for _, kv := range hashedKva[i] {
            fmt.Fprintf(outFile, "%v\t%v\n", kv.Key, kv.Value)
        }
        outFile.Close()
    }
    NotifiyTaskDone(Task.TaskID, Task.TaskType)
    return true
}

Worker 處理 Reduce

參考 6.824/src/main/mrsequential.go


func DoReduceTask(Task ReplyArgs, reducef func(string, []string) string) bool {
    /*
     1. 先獲取所有 tmp-{mapid}-{reduceid} 中 reduce id 相同的 task
    */
    fmt.Printf("starting do reduce task ...\n")
    var lines []string
    for i := 0; i < Task.MapNum; i++ {
        filename := finalMapOutFile(i, Task.TaskID)
        file, err := os.Open(filename)
        if err != nil {
            log.Fatalf("cannot open %v", filename)
        }
        content, err := ioutil.ReadAll(file)
        if err != nil {
            log.Fatalf("cannot read %v", filename)
        }
        /*
            2. 將所有文件的內容讀取出來,合併到一個數組中
        */
        lines = append(lines, strings.Split(string(content), "\n")...)
    }
    /*
        3. 過濾數據,將每行字符串轉成 KeyValue, 歸併到數組
    */
    var kva []KeyValue
    for _, line := range lines {
        if strings.TrimSpace(line) == "" {
            continue
        }
        split := strings.Split(line, "\t")
        kva = append(kva, KeyValue{
            Key:   split[0],
            Value: split[1],
        })
    }

    /*
        4. 模仿 mrsequential.go 的 reduce 操作,將結果寫入到文件,並 commit
    */
    sort.Sort(ByKey(kva))
    outFile, _ := os.Create(tmpReduceOutFile(os.Getpid(), Task.TaskID))
    i := 0
    for i < len(kva) {
        j := i + 1
        for j < len(kva) && kva[j].Key == kva[i].Key {
            j++
        }
        var values []string
        for k := i; k < j; k++ {
            values = append(values, kva[k].Value)
        }
        output := reducef(kva[i].Key, values)

        fmt.Fprintf(outFile, "%v %v\n", kva[i].Key, output)
        i = j
    }
    outFile.Close()
    NotifiyTaskDone(Task.TaskID, Task.TaskType)
    return true
}

Worker 通知任務完成


func NotifiyTaskDone(taskId int, taskType CoordinatorPhase) {
    args := NotifyArgs{}
    reply := NotifyReplyArgs{}
    args.TaskID = taskId
    args.TaskType = taskType
    args.WorkerID = os.Getpid()
    ok := call("Coordinator.RequestTaskDone", &args, &reply)
    if !ok {
        fmt.Printf("Call Coordinator.RequestTaskDone failed ...")
        return
    }

    if reply.Confirm {
        fmt.Printf("Task %d Success, Continue Next Task ...", taskId)
    }
}

4. 各種異常情況

  • worker crash 處理

coordinator 有超時機制,只有 worker 完成並且成功 commit ,纔會標記任務結束,因此 crash 之後,當前處理中的任務最後會重新返回到 task channel 中

  • rpc call 需要參數名首字符大寫,否則可能無法正確傳輸
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章