1. 介紹
準備工作
閱讀 MapReduce
做什麼
實現一個分佈式的 Map - Reduce 結構,在原先的代碼結構中 6.824/src/main/mrsequential.go
實現了單機版的 Map - Reduce ,我們需要將其改造爲多進程版本的 Map - Reduce 。一個經典的 Map - Reduce 結構如下
2. 思路
該 lab 中,主要的進程分爲 Worker
和 Coordinator
,Coordinator
主要負責分發任務,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 調用,RequestTask
和 RequestTaskDone
,分別用來處理請求任務和提交任務,下圖爲 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 需要參數名首字符大寫,否則可能無法正確傳輸